Skip to content

Commit 7920160

Browse files
ci(e2e): nightly-chaos-sigint cell — SIGINT mid-deploy cleanup test (Phase 6)
1 parent 5c4b491 commit 7920160

5 files changed

Lines changed: 141 additions & 1 deletion

File tree

.changeset/e2e-phase-6-sigint.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"playground-cli": patch
3+
---
4+
5+
Nightly E2E now exercises the SIGINT cleanup path: a new `nightly-chaos-sigint` cell sends SIGINT to `dot deploy` mid-flight and asserts the process-guard's runAllCleanupAndExit handler exits cleanly within 5s with code 130 (or SIGINT signal).

.github/workflows/e2e.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ jobs:
192192
- cell: nightly-rejections
193193
# source: e2e/cli/deploy.test.ts → describe("dot deploy — rejects --no-contract-build with no artefacts")
194194
pattern: "rejects --no-contract-build"
195+
- cell: nightly-chaos-sigint
196+
# source: e2e/cli/chaos.test.ts → describe("dot deploy — chaos")
197+
pattern: "dot deploy — chaos"
198+
testFile: e2e/cli/chaos.test.ts
195199
steps:
196200
# checkout must run before the local composite action can be loaded.
197201
- uses: actions/checkout@v4

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ These are things that aren't self-evident from reading the code and have bitten
5656

5757
- **Local launcher:** `tools/e2e-local.sh [smoke|pr|nightly]` — also callable via `pnpm test:e2e:smoke`, `pnpm test:e2e:pr`, `pnpm test:e2e:nightly`.
5858
- **CI workflow:** `.github/workflows/e2e.yml` — runs on PR / push:main / cron 06:00 UTC / workflow_dispatch.
59-
- **CI matrix:** 11 cells across four matrices — `test-no-publish` (parallel: pr-install, pr-preflight, pr-mod, pr-init-session) + `test-publish` (max-parallel: 1: pr-deploy-frontend, pr-deploy-foundry) + `test-nightly-no-publish` (parallel, schedule/dispatch only: nightly-mod-miss, nightly-diagnostic, nightly-rejections) + `test-nightly-publish` (max-parallel: 1, schedule/dispatch only: nightly-deploy-hardhat, nightly-deploy-multi). Each cell runs a subset via `vitest -t "<pattern>"`.
59+
- **CI matrix:** 12 cells across four matrices — `test-no-publish` (parallel: pr-install, pr-preflight, pr-mod, pr-init-session) + `test-publish` (max-parallel: 1: pr-deploy-frontend, pr-deploy-foundry) + `test-nightly-no-publish` (parallel, schedule/dispatch only: nightly-mod-miss, nightly-diagnostic, nightly-rejections, nightly-chaos-sigint) + `test-nightly-publish` (max-parallel: 1, schedule/dispatch only: nightly-deploy-hardhat, nightly-deploy-multi). Each cell runs a subset via `vitest -t "<pattern>"`.
6060
- **Test files:** `e2e/cli/*.test.ts` (vitest, spawned via `bun run src/index.ts`).
6161
- **Reports directory:** `e2e-reports/junit.xml` + `e2e-reports/dot-runs.log` (gitignored).
6262
- **Tag prefix:** `DOT_TAG=e2e-{ci|local}-{trigger}` so Sentry dashboards filter test traffic. The CLI plumbs `DOT_TAG` into the `cli.tag` root-span attribute via `src/telemetry-config.ts`.

e2e/cli/chaos.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Chaos tests for `dot deploy` — verifies that the process-guard cleanup
3+
* + SIGINT handling work end-to-end.
4+
*
5+
* These run only in the nightly-chaos-sigint cell. PR runs skip them.
6+
*
7+
* Design notes
8+
* ------------
9+
* • We spawn the CLI directly via execa (not the `dot()` helper) so we get a
10+
* child handle we can signal mid-flight.
11+
* • The sentinel `"▸ storage-and-dotns"` appears in headless-mode stdout
12+
* exactly once, from `logHeadlessEvent` in src/commands/deploy/index.ts
13+
* when the storage phase starts. It is specific to the headless code path
14+
* and does not rely on bulletin-deploy banner text (which is intercepted by
15+
* the log parser and never reaches stdout).
16+
* • If the sentinel never fires within 60 s (e.g. the deploy errors during
17+
* preflight), the test still sends SIGINT and asserts clean exit semantics.
18+
* The 5 s wall-clock assertion still exercises the process-guard — just
19+
* against a process that may already be winding down rather than one in
20+
* the middle of a chunk upload.
21+
*/
22+
23+
import { describe, test, expect } from "vitest";
24+
import { execa } from "execa";
25+
import { resolve } from "node:path";
26+
import { SIGNER, E2E_DOMAINS } from "./fixtures/accounts.js";
27+
import { fixturePath } from "./fixtures/templates.js";
28+
29+
const REPO_ROOT = resolve(import.meta.dirname, "../..");
30+
const CLI_ENTRY = resolve(REPO_ROOT, "src/index.ts");
31+
32+
/** How long to wait for the storage-phase sentinel before sending SIGINT anyway. */
33+
const SENTINEL_WAIT_MS = 60_000;
34+
35+
/** Expected maximum elapsed time from SIGINT to process exit. */
36+
const MAX_EXIT_MS = 5_000;
37+
38+
describe("dot deploy — chaos", () => {
39+
test("SIGINT mid-deploy exits with code 130 within 5s", { timeout: 120_000 }, async () => {
40+
const frontendOnly = fixturePath("frontend-only");
41+
42+
const child = execa(
43+
"bun",
44+
[
45+
"run",
46+
CLI_ENTRY,
47+
"deploy",
48+
"--signer",
49+
"dev",
50+
"--domain",
51+
E2E_DOMAINS.chaos,
52+
"--buildDir",
53+
resolve(frontendOnly, "dist"),
54+
"--playground",
55+
"--suri",
56+
SIGNER.suri,
57+
"--dir",
58+
frontendOnly,
59+
],
60+
{
61+
cwd: REPO_ROOT,
62+
env: { ...process.env, DOT_TAG: "e2e-chaos-sigint", DOT_TELEMETRY: "1" },
63+
reject: false,
64+
},
65+
);
66+
67+
// Accumulate output from both streams so the diagnostic tail is
68+
// available if the assertion fails.
69+
let buf = "";
70+
child.stdout?.on("data", (chunk: Buffer) => {
71+
buf += chunk.toString();
72+
});
73+
child.stderr?.on("data", (chunk: Buffer) => {
74+
buf += chunk.toString();
75+
});
76+
77+
// Wait for the storage phase to start OR for the process to exit early
78+
// (e.g. availability check fails, network error during preflight, etc.)
79+
// OR for the 60 s sentinel budget to expire.
80+
//
81+
// Sentinel: headless logHeadlessEvent writes `▸ storage-and-dotns…\n`
82+
// to stdout when the storage-and-dotns phase begins.
83+
const sentinel = "▸ storage-and-dotns";
84+
await new Promise<void>((resolve) => {
85+
const timeout = setTimeout(() => resolve(), SENTINEL_WAIT_MS);
86+
87+
const checkBuf = () => {
88+
if (buf.includes(sentinel)) {
89+
clearTimeout(timeout);
90+
resolve();
91+
}
92+
};
93+
94+
child.stdout?.on("data", checkBuf);
95+
child.on("exit", () => {
96+
clearTimeout(timeout);
97+
resolve();
98+
});
99+
});
100+
101+
// Send SIGINT and measure how long the process takes to exit.
102+
const t0 = Date.now();
103+
child.kill("SIGINT");
104+
const result = await child;
105+
const elapsedMs = Date.now() - t0;
106+
107+
// process-guard.ts calls runAllCleanupAndExit(130) on SIGINT.
108+
// execa reflects this as exitCode=130. If the handler doesn't get
109+
// to run (e.g. bun propagates the OS signal before our handler fires),
110+
// execa sets signal="SIGINT" instead. Both outcomes are acceptable:
111+
// what we're asserting is that the process is GONE cleanly within 5 s.
112+
const cleanExit = result.exitCode === 130 || result.signal === "SIGINT";
113+
expect(
114+
cleanExit,
115+
`expected exit code 130 or signal SIGINT, got exitCode=${result.exitCode} signal=${result.signal}\nlast output:\n${buf.slice(-500)}`,
116+
).toBe(true);
117+
118+
expect(
119+
elapsedMs,
120+
`process took ${elapsedMs} ms to exit after SIGINT (limit ${MAX_EXIT_MS} ms)\nlast output:\n${buf.slice(-500)}`,
121+
).toBeLessThan(MAX_EXIT_MS);
122+
});
123+
});

e2e/cli/fixtures/accounts.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,12 @@ export const E2E_DOMAINS = {
102102
cdm: "e2e-cli-cdm",
103103
hardhat: "e2e-cli-hardhat",
104104
multi: "e2e-cli-multi",
105+
/**
106+
* Used by the nightly-chaos-sigint cell only. The deploy is interrupted by
107+
* SIGINT before it completes, so this domain is never actually registered.
108+
* It is kept separate from `storage` to avoid any race with the happy-path
109+
* storage test in test-publish when both run in a nightly that triggers all
110+
* matrices.
111+
*/
112+
chaos: "e2e-cli-chaos",
105113
} as const;

0 commit comments

Comments
 (0)