diff --git a/CHANGELOG.md b/CHANGELOG.md index 603e912..4d20f3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### Fixed +- Fixed `crabbox run --junit` so all-passing JUnit files record results instead of leaving the coordinator run stuck when the failure list is empty. - Fixed native Windows `--shell` runs so multi-statement PowerShell scripts keep their quotes instead of being re-parsed by a nested PowerShell process. - Removed the static macOS managed-login path so static host VNC cannot be mistaken for a Crabbox-created external instance. - Excluded macOS AppleDouble `._*` sidecar files from default sync manifests so native Windows archives do not transfer invalid TypeScript/package sidecars. diff --git a/internal/cli/results_parse.go b/internal/cli/results_parse.go index 4499273..463ad36 100644 --- a/internal/cli/results_parse.go +++ b/internal/cli/results_parse.go @@ -49,7 +49,11 @@ func parseJUnitResults(files map[string]string) (*TestResultSummary, error) { if len(files) == 0 { return nil, nil } - summary := &TestResultSummary{Format: "junit", Files: make([]string, 0, len(files))} + summary := &TestResultSummary{ + Format: "junit", + Files: make([]string, 0, len(files)), + Failed: []TestFailure{}, + } for name, data := range files { trimmed := strings.TrimSpace(data) if trimmed == "" { diff --git a/internal/cli/results_parse_test.go b/internal/cli/results_parse_test.go index 8f657a8..86d03f8 100644 --- a/internal/cli/results_parse_test.go +++ b/internal/cli/results_parse_test.go @@ -18,6 +18,24 @@ func TestParseJUnitResults(t *testing.T) { } } +func TestParseJUnitResultsInitializesEmptyFailureList(t *testing.T) { + results, err := parseJUnitResults(map[string]string{"junit.xml": ` + +`}) + if err != nil { + t.Fatal(err) + } + if results == nil { + t.Fatal("results nil") + } + if results.Failed == nil { + t.Fatalf("failed slice is nil: %#v", results) + } + if len(results.Failed) != 0 { + t.Fatalf("failed=%#v", results.Failed) + } +} + func TestParseMarkedFiles(t *testing.T) { files := parseMarkedFiles("\n__CRABBOX_RESULT_FILE__:a.xml\n\n__CRABBOX_RESULT_FILE__:b.xml\n\n") if files["a.xml"] != "" || files["b.xml"] != "" { diff --git a/worker/src/fleet.ts b/worker/src/fleet.ts index 2c11540..0553a3c 100644 --- a/worker/src/fleet.ts +++ b/worker/src/fleet.ts @@ -1527,12 +1527,14 @@ function phaseForRunEvent(event: RunEventRecord): string { } function boundedTestResults(results: TestResultSummary): TestResultSummary { + const files = Array.isArray(results.files) ? results.files : []; + const failed = Array.isArray(results.failed) ? results.failed : []; return { ...results, - files: results.files + files: files .slice(0, MAX_RESULT_FILES) .map((file) => truncateString(file, MAX_RESULT_STRING_BYTES)), - failed: results.failed.slice(0, MAX_RESULT_FAILURES).map(boundedTestFailure), + failed: failed.slice(0, MAX_RESULT_FAILURES).map(boundedTestFailure), }; } diff --git a/worker/test/fleet.test.ts b/worker/test/fleet.test.ts index a8681c6..b514621 100644 --- a/worker/test/fleet.test.ts +++ b/worker/test/fleet.test.ts @@ -825,6 +825,49 @@ describe("fleet run history", () => { expect(await logs.text()).toBe("ok\n"); }); + it("accepts Go nil slices in passing test results", async () => { + const fleet = testFleet(); + const create = await fleet.fetch( + request("POST", "/v1/runs", { + body: { + leaseID: "cbx_000000000001", + provider: "aws", + class: "beast", + serverType: "c7a.48xlarge", + command: ["go", "test", "./..."], + }, + }), + ); + expect(create.status).toBe(201); + const { run } = (await create.json()) as { run: { id: string } }; + + const finish = await fleet.fetch( + request("POST", `/v1/runs/${run.id}/finish`, { + body: { + exitCode: 0, + log: "ok\n", + results: { + format: "junit", + files: null, + suites: 1, + tests: 1, + failures: 0, + errors: 0, + skipped: 0, + timeSeconds: 0.001, + failed: null, + }, + }, + }), + ); + expect(finish.status).toBe(200); + const finished = (await finish.json()) as { + run: { results?: { files: string[]; failed: unknown[] } }; + }; + expect(finished.run.results?.files).toEqual([]); + expect(finished.run.results?.failed).toEqual([]); + }); + it("records chunked run logs so failures do not disappear from long output", async () => { const storage = new MemoryStorage(); const fleet = testFleet(storage);