Skip to content

Commit 926c1ca

Browse files
committed
test(e2e): add weekly unit gap review
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
1 parent 5275c73 commit 926c1ca

5 files changed

Lines changed: 817 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"test:coverage:cli": "npm run clean:cli && npm run build:cli && tsx scripts/check-dist-sourcemaps.mts dist && vitest run --project cli --project integration --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.reportsDirectory=coverage/cli --coverage.include=\"bin/**/*.js\" --coverage.include=\"src/**/*.ts\" --coverage.exclude=\"test/**/*.js\" --coverage.exclude=\"test/**/*.ts\" && tsx scripts/check-coverage-ratchet.mts coverage/cli/coverage-summary.json ci/coverage-threshold-cli.json \"CLI coverage\"",
3838
"test:coverage:plugin": "vitest run --project plugin --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.reportsDirectory=coverage/plugin --coverage.include=\"nemoclaw/src/**/*.ts\" --coverage.include=\"nemoclaw/src/**/*.cts\" --coverage.exclude=\"**/*.test.ts\" && tsx scripts/check-coverage-ratchet.mts coverage/plugin/coverage-summary.json ci/coverage-threshold-plugin.json \"Plugin coverage\"",
3939
"test:live-e2e": "npm run clean:cli && npm run build:cli && NEMOCLAW_RUN_LIVE_E2E=1 vitest run --project e2e-live",
40+
"e2e:unit-gaps": "tsx tools/e2e/unit-test-gaps.mts",
4041
"test:imports:check": "tsx scripts/checks/no-test-dist-imports.mts",
4142
"test:projects:check": "tsx scripts/checks/vitest-project-overlap.mts",
4243
"test:titles:check": "tsx scripts/checks/test-title-style.mts",

test/e2e/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,10 +639,65 @@ graph as the live targets:
639639
pass/fail/skip counts, failure rate, pass/fail flips, current failure streak,
640640
and the most common failed phase. The failure-rate denominator and
641641
pass/fail-flip count exclude skips.
642+
642643
- Selective dispatches remain silent unless they run on `main` with
643644
`post_to_slack=true`, which uses the preview Slack route. Branch-dispatched
644645
runs never receive Slack webhook secrets.
645646

647+
### Weekly unit-test gap review
648+
649+
Treat every automatic `main` E2E failure as a test-gap review input. Generate a
650+
report for the preceding 168 hours with GitHub CLI authentication already
651+
configured:
652+
653+
```bash
654+
evidence_dir="$(mktemp -d)"
655+
chmod 700 "$evidence_dir"
656+
npm run e2e:unit-gaps -- \
657+
--days 7 \
658+
--output "$evidence_dir/unit-test-gaps.md" \
659+
--json-output "$evidence_dir/unit-test-gaps.json"
660+
```
661+
662+
The command reads push runs from `e2e.yaml` and `portable-profile-e2e.yaml` on
663+
`main`. It keeps failed logs in memory, applies the shared full secret redactor,
664+
removes volatile identifiers, paths, URLs, sandbox names, and durations from
665+
each selected cause candidate, and writes report files with mode `0600` in the
666+
mode-`0700` directory. Treat the reports as credential-bearing until a human
667+
reviews them; redaction reduces exposure but does not prove that a report is
668+
credential-free. The command exits nonzero when a selected run is unfinished or
669+
failed-run evidence is unavailable. Do not accept a partial report as the
670+
weekly ledger.
671+
672+
Review one row per cause candidate instead of one row per failed job. Confirm
673+
the selected candidate against the first causal line and identify the owning
674+
component before changing code. Then apply the row's test action:
675+
676+
- For a deterministic product failure, add a unit or package-contract
677+
regression test that fails for the observed behavior before changing the
678+
product code.
679+
- For a harness failure, add an `e2e-support` test for the decision, cleanup
680+
path, or diagnostic.
681+
- For an external failure, test NemoClaw's retry and diagnostic response with
682+
fault injection. Do not reproduce the provider, registry, network, or runner
683+
outage in a unit test.
684+
- For a row that needs triage, name the missing contract only after confirming
685+
the cause from the linked run.
686+
687+
The Markdown and JSON reports start each row with review status `open` and no
688+
regression test. During review, record the test file and complete test title in
689+
the row and change the status only after the test fails without the fix and
690+
passes with it. A cause candidate is complete when that test evidence and a
691+
later passing run of the linked E2E target are both recorded. Delete the report
692+
directory after publishing only the reviewed, credential-free conclusions in
693+
the owning issue or pull request. Raw logs remain in process memory only until
694+
the command exits. Remove the named directory and confirm its absence:
695+
696+
```bash
697+
rm -rf -- "$evidence_dir"
698+
test ! -e "$evidence_dir"
699+
```
700+
646701
A manual run with `jobs=staging-brev-launchable` runs only `Publish staging Brev Launchable image`.
647702
Push runs do not select this job.
648703

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { describe, expect, it } from "vitest";
5+
6+
import {
7+
buildUnitGapReport,
8+
classifyFailureSignature,
9+
extractJobSignatures,
10+
formatUnitGapReport,
11+
normalizeFailureSignature,
12+
type RunLogEvidence,
13+
} from "../../../tools/e2e/unit-test-gaps-core.mts";
14+
import { rollingRange } from "../../../tools/e2e/unit-test-gaps.mts";
15+
16+
function evidence(overrides: Partial<RunLogEvidence> = {}): RunLogEvidence {
17+
return {
18+
log: "job\tstep\t2026-08-12T10:00:00.0000000Z AssertionError: expected UPGRADE, received 400\n",
19+
run: {
20+
attempt: 1,
21+
conclusion: "failure",
22+
createdAt: "2026-08-12T10:00:00Z",
23+
databaseId: 12345678,
24+
event: "push",
25+
headBranch: "main",
26+
headSha: "1234567890abcdef1234567890abcdef12345678",
27+
name: "E2E main",
28+
status: "completed",
29+
url: "https://github.com/NVIDIA/NemoClaw/actions/runs/12345678",
30+
},
31+
...overrides,
32+
};
33+
}
34+
35+
describe("weekly E2E unit-test gap analysis", () => {
36+
it("redacts volatile identifiers, paths, URLs, sandboxes, and durations", () => {
37+
const signature = normalizeFailureSignature(
38+
"Error: sandbox e2e-sbx-a at /home/runner/work/NemoClaw failed after 180000ms for 1234567890abcdef1234567890abcdef12345678 via https://example.test/path?token=secret",
39+
);
40+
41+
expect(signature).toBe(
42+
"Error: sandbox <sandbox> at <path> failed after <duration> for <sha> via <url>",
43+
);
44+
expect(signature).not.toContain("secret");
45+
});
46+
47+
it("redacts credential-shaped values before writing a cause candidate", () => {
48+
const signature = normalizeFailureSignature(
49+
"Error: Authorization: Bearer ghp_EXAMPLE012345678901234 AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE session=eyJhbGciOiJIUzI1NiJ9.cGF5bG9hZA.c2lnbmF0dXJl",
50+
);
51+
52+
expect(signature).toBe(
53+
"Error: Authorization: Bearer <REDACTED> AWS_ACCESS_KEY_ID=<REDACTED> session=<REDACTED>",
54+
);
55+
expect(signature).not.toContain("EXAMPLE");
56+
});
57+
58+
it("uses an exact rolling window for --days", () => {
59+
expect(rollingRange(7, new Date("2026-08-16T19:30:00.000Z"))).toEqual({
60+
from: "2026-08-09T19:30:00.000Z",
61+
to: "2026-08-16T19:30:00.000Z",
62+
});
63+
});
64+
65+
it("groups volatile BuildKit references under the missing build-input contract", () => {
66+
expect(
67+
normalizeFailureSignature(
68+
'ERROR: failed to build: failed to solve: failed to compute cache key: failed to calculate checksum of ref 12345678-1234-4234-9234-123456789abc::sztmu18osbm95fj41qvxlgdie: "/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle": not found',
69+
),
70+
).toBe("ERROR: reviewed runtime bundle is missing from the image build context");
71+
});
72+
73+
it("uses the earliest high-specificity causal line and ignores wrappers and echoed shell", () => {
74+
const signatures = extractJobSignatures(
75+
[
76+
"portable-launch\tstep\t2026-08-12T10:00:00.0000000Z ##[error]Process completed with exit code 1.",
77+
'portable-launch\tstep\t2026-08-12T10:00:00.5000000Z echo "::error::a shell guard failed"',
78+
"portable-launch\tstep\t2026-08-12T10:00:01.0000000Z Error: Portable Podman readiness failed at service activation",
79+
"rootless-linux\tstep\t2026-08-12T10:00:02.0000000Z npm error code EAI_AGAIN",
80+
].join("\n"),
81+
);
82+
83+
expect(signatures).toEqual([
84+
{
85+
job: "portable-launch",
86+
signature: "Error: Portable Podman readiness failed at service activation",
87+
},
88+
{ job: "rootless-linux", signature: "npm error code EAI_AGAIN" },
89+
]);
90+
});
91+
92+
it("keeps a failed job in the queue when its causal line needs manual review", () => {
93+
expect(
94+
extractJobSignatures(
95+
"job\tstep\t2026-08-12T10:00:00.0000000Z ##[error]Process completed with exit code 1.\n",
96+
),
97+
).toEqual([{ job: "job", signature: "Failed job log requires manual causal-line review" }]);
98+
});
99+
100+
it.each([
101+
["AssertionError: expected UPGRADE, received 400", "deterministic"],
102+
["npm error code EAI_AGAIN", "external"],
103+
["Error: E2E cleanup failed: gateway unavailable", "harness"],
104+
["Error: Local BuildKit build failed", "needs-triage"],
105+
] as const)("classifies %s as %s", (signature, classification) => {
106+
expect(classifyFailureSignature(signature)).toBe(classification);
107+
});
108+
109+
it("groups the same normalized cause across runs and keeps the required test action", () => {
110+
const first = evidence();
111+
const second = evidence({
112+
run: {
113+
...evidence().run,
114+
databaseId: 23456789,
115+
url: "https://github.com/NVIDIA/NemoClaw/actions/runs/23456789",
116+
},
117+
});
118+
const report = buildUnitGapReport(
119+
[first, second],
120+
{ from: "2026-08-09", to: "2026-08-16" },
121+
"2026-08-16T20:00:00.000Z",
122+
);
123+
124+
expect(report.groups).toHaveLength(1);
125+
expect(report.groups[0]).toMatchObject({
126+
classification: "deterministic",
127+
regressionTest: null,
128+
reviewStatus: "open",
129+
runCount: 2,
130+
runIds: [12345678, 23456789],
131+
});
132+
expect(report.groups[0]!.requiredAction).toContain("unit or package-contract regression test");
133+
});
134+
135+
it("fails the evidence ledger visibly when a failed log is unavailable", () => {
136+
const report = buildUnitGapReport(
137+
[evidence({ error: "too many API requests needed to fetch logs", log: undefined })],
138+
{ from: "2026-08-09", to: "2026-08-16" },
139+
"2026-08-16T20:00:00.000Z",
140+
);
141+
const markdown = formatUnitGapReport(report);
142+
143+
expect(report.incompleteRuns).toEqual([
144+
{
145+
error: "too many API requests needed to fetch logs",
146+
runId: 12345678,
147+
url: "https://github.com/NVIDIA/NemoClaw/actions/runs/12345678",
148+
},
149+
]);
150+
expect(markdown).toContain("The report is incomplete.");
151+
});
152+
153+
it("keeps a run active at the cutoff from producing a complete ledger", () => {
154+
const active = evidence({
155+
log: undefined,
156+
run: { ...evidence().run, conclusion: "", status: "in_progress" },
157+
});
158+
const report = buildUnitGapReport(
159+
[active],
160+
{ from: "2026-08-09T20:00:00.000Z", to: "2026-08-16T20:00:00.000Z" },
161+
"2026-08-16T20:00:00.000Z",
162+
);
163+
164+
expect(report.incompleteRuns).toEqual([
165+
{
166+
error: "run was in_progress at the collection cutoff",
167+
runId: 12345678,
168+
url: "https://github.com/NVIDIA/NemoClaw/actions/runs/12345678",
169+
},
170+
]);
171+
});
172+
});

0 commit comments

Comments
 (0)