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);