package runtime import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" "time" "browser.local/run/config" "browser.local/run/protocol" ) func TestLifecycleExecutorHandlesSupportedJobs(t *testing.T) { executor := NewLifecycleExecutor() for _, capability := range SupportedLifecycleCapabilities() { assignment := lifecycleAssignment(capability) result := executor.Execute(assignment) if result.State != "succeeded" || result.Progress.Percent != 100 || result.ResultRef == "" { t.Fatalf("expected successful bounded result for %s, got %+v", capability, result) } for _, forbidden := range []string{"host path", "/Users/", "run socket", "api_key", "sk-"} { if strings.Contains(result.Message, forbidden) || strings.Contains(result.ResultRef, forbidden) { t.Fatalf("lifecycle result exposed forbidden content %q: %+v", forbidden, result) } } } } func TestLifecycleExecutorRejectsUnsupportedJobs(t *testing.T) { result := NewLifecycleExecutor().Execute(lifecycleAssignment("files.write")) if result.State != "failed" || result.ErrorCode != "unsupported_lifecycle_capability" || result.ResultRef != "" { t.Fatalf("expected unsupported lifecycle failure, got %+v", result) } } func TestLifecycleResultRequestUsesAssignmentLease(t *testing.T) { assignment := lifecycleAssignment(protocol.RunCapabilityProcessStart) execution := NewLifecycleExecutor().Execute(assignment) request := LifecycleResultRequest(assignment, "session-token", execution) if request.RunEndpointID != assignment.RunEndpointID || request.JobID != assignment.JobID || request.LeaseToken != assignment.LeaseToken || request.Attempt != assignment.Attempt { t.Fatalf("expected result request to use assignment lease, got %+v", request) } if request.SessionToken != "session-token" || request.State != "succeeded" { t.Fatalf("unexpected result request: %+v", request) } } func TestLifecycleExecutorRunsScopedCommandTemplateAndHooks(t *testing.T) { root := t.TempDir() assignment := lifecycleAssignment(protocol.RunCapabilityProcessStart) serverRoot := filepath.Join(root, assignment.ServerInstanceID) if err := os.MkdirAll(filepath.Join(serverRoot, "actions"), 0o755); err != nil { t.Fatalf("create action dir: %v", err) } template := LifecycleActionTemplate{ Command: []string{"echo", "server-ready"}, Env: map[string]string{"GAME_MODE": "test"}, } body, err := json.Marshal(template) if err != nil { t.Fatalf("marshal template: %v", err) } if err := os.WriteFile(filepath.Join(serverRoot, "actions", "start.json"), body, 0o644); err != nil { t.Fatalf("write template: %v", err) } assignment.TargetKey = "actions/start.json" logSink := &recordingLogSink{} artifactHook := &recordingArtifactHook{} result := NewLifecycleExecutor( WithLifecycleWorkspaceRoot(root), WithProcessLogSink(logSink), WithLifecycleArtifactHook(artifactHook), ).Execute(assignment) if result.State != "succeeded" || result.ResultRef != "artifact://jobs/job-1/lifecycle-result" { t.Fatalf("expected scoped lifecycle success, got %+v", result) } if len(logSink.lines) != 1 || logSink.lines[0] != "stdout:server-ready" { t.Fatalf("expected process stdout to be logged, got %+v", logSink.lines) } if !artifactHook.called { t.Fatal("expected artifact hook to be called") } } func TestLifecycleExecutorRejectsUnsafeTemplates(t *testing.T) { root := t.TempDir() assignment := lifecycleAssignment(protocol.RunCapabilityProcessStart) serverRoot := filepath.Join(root, assignment.ServerInstanceID) if err := os.MkdirAll(filepath.Join(serverRoot, "actions"), 0o755); err != nil { t.Fatalf("create action dir: %v", err) } for name, template := range map[string]LifecycleActionTemplate{ "absolute": {Command: []string{"/bin/echo", "nope"}}, "shell": {Command: []string{"sh", "-c", "echo nope"}}, "secret": {Command: []string{"echo", "sk-secret"}}, "env": {Command: []string{"echo", "ok"}, Env: map[string]string{"AWS_SECRET_ACCESS_KEY": "secret"}}, } { body, err := json.Marshal(template) if err != nil { t.Fatalf("marshal %s: %v", name, err) } actionPath := filepath.Join(serverRoot, "actions", name+".json") if err := os.WriteFile(actionPath, body, 0o644); err != nil { t.Fatalf("write %s: %v", name, err) } unsafeAssignment := assignment unsafeAssignment.TargetKey = "actions/" + name + ".json" result := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root)).Execute(unsafeAssignment) if result.State != "failed" || result.ErrorCode != "unsafe_lifecycle_command" { t.Fatalf("expected unsafe command rejection for %s, got %+v", name, result) } } } func TestLifecycleExecutorRejectsWorkspaceEscapes(t *testing.T) { root := t.TempDir() assignment := lifecycleAssignment(protocol.RunCapabilityProcessStart) assignment.TargetKey = "../outside.json" result := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root)).Execute(assignment) if result.State != "failed" || result.ErrorCode != "unsafe_lifecycle_command" { t.Fatalf("expected workspace escape rejection, got %+v", result) } } func TestLifecycleExecutorKeepsSiblingInstanceWorkspacesIsolated(t *testing.T) { root := t.TempDir() first := lifecycleAssignment(protocol.RunCapabilityProcessStart) first.ServerInstanceID = "server-alpha" second := lifecycleAssignment(protocol.RunCapabilityProcessStop) second.JobID = "job-2" second.ServerInstanceID = "server-beta" for _, assignment := range []protocol.RunJobAssignment{first, second} { serverRoot := filepath.Join(root, assignment.ServerInstanceID) if err := os.MkdirAll(filepath.Join(serverRoot, "actions"), 0o755); err != nil { t.Fatalf("create action dir for %s: %v", assignment.ServerInstanceID, err) } body, err := json.Marshal(LifecycleActionTemplate{Command: []string{"echo", assignment.ServerInstanceID}}) if err != nil { t.Fatalf("marshal template: %v", err) } if err := os.WriteFile(filepath.Join(serverRoot, "actions", "lifecycle.json"), body, 0o644); err != nil { t.Fatalf("write template for %s: %v", assignment.ServerInstanceID, err) } } first.TargetKey = "actions/lifecycle.json" second.TargetKey = "actions/lifecycle.json" logSink := &recordingLogSink{} executor := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root), WithProcessLogSink(logSink)) firstResult := executor.Execute(first) secondResult := executor.Execute(second) if firstResult.State != "succeeded" || secondResult.State != "succeeded" { t.Fatalf("expected both lifecycle jobs to succeed, got first=%+v second=%+v", firstResult, secondResult) } joined := strings.Join(logSink.lines, "\n") if !strings.Contains(joined, "stdout:server-alpha") || !strings.Contains(joined, "stdout:server-beta") { t.Fatalf("expected instance-specific output, got %q", joined) } if _, err := os.Stat(filepath.Join(root, "server-alpha", "actions", "lifecycle.json")); err != nil { t.Fatalf("expected alpha template to remain scoped: %v", err) } if _, err := os.Stat(filepath.Join(root, "server-beta", "actions", "lifecycle.json")); err != nil { t.Fatalf("expected beta template to remain scoped: %v", err) } } func TestLifecycleExecutorCancelsRunningCommand(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() result := NewLifecycleExecutor(WithProcessSupervisor(blockingSupervisor{})).ExecuteContext(ctx, lifecycleAssignment(protocol.RunCapabilityProcessStart)) if result.State != "cancelled" || result.ErrorCode != "lifecycle_cancelled" { t.Fatalf("expected cancelled lifecycle result, got %+v", result) } } func TestSmokeSummaryReportsLifecycleCapabilities(t *testing.T) { summary := SmokeSummary(config.Config{Mode: "smoke", PlatformURL: "http://platform.test"}) for _, capability := range SupportedLifecycleCapabilities() { if !containsCapability(summary.Capabilities, capability) { t.Fatalf("expected smoke capabilities to include %s, got %+v", capability, summary.Capabilities) } } } func TestSmokeSummaryReportsLogReadCapability(t *testing.T) { summary := SmokeSummary(config.Config{Mode: "smoke", PlatformURL: "http://platform.test"}) if !containsCapability(summary.Capabilities, protocol.RunCapabilityLogsRead) { t.Fatalf("expected smoke capabilities to include %s, got %+v", protocol.RunCapabilityLogsRead, summary.Capabilities) } } type recordingLogSink struct { lines []string } func (sink *recordingLogSink) Append(_ context.Context, _ protocol.RunJobAssignment, stream string, line string) error { sink.lines = append(sink.lines, stream+":"+line) return nil } type recordingArtifactHook struct { called bool } func (hook *recordingArtifactHook) QueueLifecycleResult(_ context.Context, assignment protocol.RunJobAssignment, _ ProcessResult) (string, error) { hook.called = true return fmt.Sprintf("artifact://jobs/%s/lifecycle-result", assignment.JobID), nil } type blockingSupervisor struct{} func (blockingSupervisor) Run(ctx context.Context, _ ProcessCommand) (ProcessResult, error) { <-ctx.Done() return ProcessResult{ExitCode: -1}, ctx.Err() } func lifecycleAssignment(capability string) protocol.RunJobAssignment { now := time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC) return protocol.RunJobAssignment{ JobID: "job-1", ServerInstanceID: "server-1", RunEndpointID: "run-local", Capability: capability, IdempotencyKey: "idem-1", State: "accepted", LeaseToken: "lease-1", Attempt: 1, CreatedAt: now, UpdatedAt: now, } } func containsCapability(capabilities []string, capability string) bool { for _, item := range capabilities { if item == capability { return true } } return false }