Skip to content

Commit 6a5d30e

Browse files
committed
test(e2e): apply adversarial-QA review findings across the suite
Second pass on the same fake-passing class of bug, this time catching weaknesses the previous tighten-up missed: - install.test.ts: `dot update` now asserts on the actual outcome line ("already on latest" / "Updated dot to") instead of just exitCode 0. A silent no-op update was previously indistinguishable from a real one. - build.test.ts: each test stages its fixture into a tempdir and runs the build there, instead of mutating the source-controlled fixture's dist/. This removes a hidden cross-file order dependency where build tests would delete the dist that later deploy --no-build tests needed. Error-path assertions tightened to the exact strings the fixture/runner emit ("Compile error: unexpected token", "No build strategy detected") rather than /error|fail|detect/. - session.test.ts: tightened "no signer" / "logout" assertions to the exact "No signer available" / "No account is signed in" strings. - init.test.ts: the no-session and corrupted-session tests now fail loudly when init falls into the "Login skipped" branch (login service unreachable from the runner) instead of silently accepting it as a pass — which was masking the QR path's testability gap. Corrupted-session test additionally asserts the corrupt file is unchanged on disk, catching a hypothetical regression where init "fixes" parse failures by deleting user data. - diagnostic.test.ts: both verbose-mode tests now assert on a verbose-only marker that proves the env var actually engaged its code path (`[+<seconds>s]` for DOT_DEPLOY_VERBOSE, `[mem +<seconds>s]` for DOT_MEMORY_TRACE). Previously the tests could pass with the env var silently ignored. - deploy.test.ts: - "frontend-only deploy completes end-to-end" now queries the registry independently after deploy and verifies the on-chain metadata CID matches what the CLI claimed it published. - "re-deploy same domain" now mutates the build output between the two deploys and asserts the second deploy produced a *different* metadata CID. A silently-no-op re-deploy would otherwise still print "Deploy complete" and pass. - "domain availability check runs before build/upload" now verifies the temporal ordering claimed in its name — availability banner must precede any build-runner header or storage-phase marker. - Multi-contract test renamed; it cannot prove plurality from headless output (logHeadlessEvent doesn't surface contract names), so the old name was misleading. - e2e/cli/setup/global.ts: SIGNER funding failure now throws by default with a clear message, instead of swallowing into a console.warn that let downstream deploy tests fail with cryptic "Invalid Payment" errors. Set E2E_ALLOW_OFFLINE_SETUP=1 to opt back into the lenient behaviour for local dev on a flaky network.
1 parent 14ec717 commit 6a5d30e

7 files changed

Lines changed: 295 additions & 93 deletions

File tree

e2e/cli/build.test.ts

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,79 @@
1-
import { describe, test, expect, beforeEach, afterEach } from "vitest";
2-
import { existsSync, rmSync } from "node:fs";
3-
import { resolve } from "node:path";
1+
import { describe, test, expect, afterEach } from "vitest";
2+
import { cpSync, existsSync, mkdtempSync, rmSync } from "node:fs";
3+
import { join, resolve } from "node:path";
4+
import { tmpdir } from "node:os";
45
import { dot } from "./helpers/dot.js";
56
import { fixturePath } from "./fixtures/templates.js";
67

7-
describe("dot build", () => {
8-
const frontendOnly = fixturePath("frontend-only");
9-
const distDir = resolve(frontendOnly, "dist");
8+
/**
9+
* Copy a fixture into a fresh temp dir so the build output lives there,
10+
* not inside the source-controlled fixture. This keeps build.test.ts
11+
* order-independent: previously we deleted `frontend-only/dist` between
12+
* tests, which left the dir absent for any later test that ran with
13+
* `--no-build` (deploy, diagnostic). With a copy, the original fixture
14+
* is never touched.
15+
*/
16+
function stageFixture(name: string): string {
17+
const dir = mkdtempSync(join(tmpdir(), `dot-e2e-build-${name}-`));
18+
cpSync(fixturePath(name), dir, { recursive: true });
19+
return dir;
20+
}
1021

11-
beforeEach(() => {
12-
if (existsSync(distDir)) {
13-
rmSync(distDir, { recursive: true });
14-
}
15-
});
22+
describe("dot build", () => {
23+
const stagedDirs: string[] = [];
1624

1725
afterEach(() => {
18-
if (existsSync(distDir)) {
19-
rmSync(distDir, { recursive: true });
26+
for (const d of stagedDirs) {
27+
try {
28+
rmSync(d, { recursive: true, force: true });
29+
} catch { /* best-effort */ }
2030
}
31+
stagedDirs.length = 0;
2132
});
2233

34+
function stage(name: string): string {
35+
const d = stageFixture(name);
36+
stagedDirs.push(d);
37+
return d;
38+
}
39+
2340
test("builds a frontend-only project", async () => {
24-
const result = await dot(["build", "--dir", frontendOnly]);
25-
expect(result.exitCode).toBe(0);
41+
const project = stage("frontend-only");
42+
// The fixture ships a pre-built dist for use by deploy tests.
43+
// Remove it from the staged copy so we can assert that *this*
44+
// build invocation produced the artifacts.
45+
const distDir = resolve(project, "dist");
46+
if (existsSync(distDir)) rmSync(distDir, { recursive: true });
47+
48+
const result = await dot(["build", "--dir", project]);
49+
expect(
50+
result.exitCode,
51+
`build failed: ${result.stdout}\n${result.stderr}`,
52+
).toBe(0);
2653
expect(result.stdout).toContain("Build succeeded");
2754
expect(existsSync(resolve(distDir, "index.html"))).toBe(true);
2855
});
2956

3057
test("exits non-zero on build failure with error output", async () => {
31-
const broken = fixturePath("broken-contract");
32-
const result = await dot(["build", "--dir", broken]);
58+
const project = stage("broken-contract");
59+
const result = await dot(["build", "--dir", project]);
3360
expect(result.exitCode).not.toBe(0);
34-
// The broken-contract fixture's build script writes to stderr
61+
// The fixture's package.json runs:
62+
// "build": "echo 'Compile error: unexpected token' >&2 && exit 1"
63+
// Match the exact text — proves the runner invoked the user's script
64+
// and surfaced its stderr, not just any pipeline-level error.
3565
const output = result.stdout + result.stderr;
36-
expect(output).toMatch(/error|fail/i);
66+
expect(output).toContain("Compile error: unexpected token");
3767
});
3868

3969
test("exits non-zero when no build strategy can be detected", async () => {
40-
const contractsOnly = fixturePath("contracts-only");
41-
const result = await dot(["build", "--dir", contractsOnly]);
70+
const project = stage("contracts-only");
71+
const result = await dot(["build", "--dir", project]);
4272
expect(result.exitCode).not.toBe(0);
43-
// contracts-only has no package.json — no build strategy
73+
// Exact wording from src/utils/build/detect.ts:
74+
// `No build strategy detected. Add a "build" script to package.json,
75+
// or install vite/next/typescript.`
4476
const output = result.stdout + result.stderr;
45-
expect(output).toMatch(/no build|detect|strategy|package\.json/i);
77+
expect(output).toContain("No build strategy detected");
4678
});
4779
});

e2e/cli/deploy.test.ts

Lines changed: 104 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,21 @@
1414
*/
1515

1616
import { describe, test, expect } from "vitest";
17+
import { readFileSync, writeFileSync } from "node:fs";
1718
import { resolve } from "node:path";
1819
import { dot } from "./helpers/dot.js";
1920
import { SIGNER, BOB, E2E_DOMAINS } from "./fixtures/accounts.js";
2021
import { fixturePath } from "./fixtures/templates.js";
22+
import { getApp } from "./fixtures/registry.js";
23+
24+
/** Pull the metadata CID out of the headless deploy summary. The CLI
25+
* prints ` Metadata CID bafy...` once per successful deploy
26+
* (see src/commands/deploy/index.ts printFinalResult). Returns null
27+
* if absent — that itself is a meaningful signal. */
28+
function extractMetadataCid(stdout: string): string | null {
29+
const m = stdout.match(/Metadata CID\s+(\S+)/);
30+
return m ? m[1] : null;
31+
}
2132

2233
const frontendOnly = fixturePath("frontend-only");
2334
const foundry = fixturePath("foundry");
@@ -128,7 +139,14 @@ describe("dot deploy — preflight and validation", () => {
128139
).toContain("Checking availability");
129140
});
130141

131-
test("detects multiple contracts in multi-contract project", async () => {
142+
test("--contracts works on a multi-contract project", async () => {
143+
// Renamed from "detects multiple contracts" — the headless logger
144+
// (logHeadlessEvent in src/commands/deploy/index.ts) does not
145+
// surface the per-contract names that compile-detected events
146+
// carry, so the CLI output cannot prove plurality. We can only
147+
// prove the contract-type detector accepted the project. If
148+
// per-contract names get logged in headless mode in future, add
149+
// `expect(output).toContain("TokenA")` + `"TokenB"` here.
132150
const result = await dot([
133151
"deploy",
134152
"--signer", "dev",
@@ -183,7 +201,32 @@ describe("dot deploy — preflight and validation", () => {
183201
// Availability banner names the domain; this is the strongest signal we
184202
// have that the availability check actually executed against this run's
185203
// domain (rather than echoing the arg in a usage/error string).
186-
expect(output).toContain(`Checking availability of ${domain}.dot`);
204+
const availIdx = output.indexOf(`Checking availability of ${domain}.dot`);
205+
expect(
206+
availIdx,
207+
`availability banner not found:\n${output}`,
208+
).toBeGreaterThan(-1);
209+
// Verify the *ordering* claim in the test name: availability must
210+
// precede any build-runner output. Without this, the test only proves
211+
// availability ran, not that it ran first. Build runners emit the
212+
// header `> <strategy description>` (see src/commands/build.ts:14)
213+
// and bulletin-deploy's storage phase emits `▸ storage-and-dotns…`
214+
// (logHeadlessEvent). Either appearing before availability would
215+
// break the contract.
216+
const buildIdx = output.search(/\n>\s+\w/);
217+
const storageIdx = output.indexOf("▸ storage-and-dotns");
218+
if (buildIdx > -1) {
219+
expect(
220+
availIdx,
221+
`build header appeared before availability check:\n${output}`,
222+
).toBeLessThan(buildIdx);
223+
}
224+
if (storageIdx > -1) {
225+
expect(
226+
availIdx,
227+
`storage phase started before availability check:\n${output}`,
228+
).toBeLessThan(storageIdx);
229+
}
187230
});
188231
});
189232

@@ -207,6 +250,19 @@ describe("dot deploy --playground — full pipeline (requires Paseo + IPFS)", ()
207250
expect(result.stdout).toContain("Deploy complete");
208251
expect(result.stdout).toContain("URL");
209252
expect(result.stdout).toContain(domain);
253+
254+
// Don't trust the CLI's own success message — query the registry
255+
// independently to prove the entry was actually written. A regression
256+
// where deploy prints "Deploy complete" but never sends the registry
257+
// extrinsic would otherwise pass.
258+
const cliCid = extractMetadataCid(result.stdout);
259+
expect(cliCid, "CLI did not print Metadata CID").not.toBeNull();
260+
const entry = await getApp(`${domain}.dot`);
261+
expect(entry, `registry has no entry for ${domain}.dot`).not.toBeNull();
262+
// Belt-and-braces: the on-chain CID should match what the CLI claims
263+
// it published. A divergence here means the CLI is reporting one CID
264+
// to the user while writing a different one to the chain.
265+
expect(entry!.metadataUri).toContain(cliCid!);
210266
});
211267

212268
test("re-deploy same domain succeeds for same owner", { timeout: 900_000 }, async () => {
@@ -222,22 +278,53 @@ describe("dot deploy --playground — full pipeline (requires Paseo + IPFS)", ()
222278
], { timeout: 400_000 });
223279
expect(first.exitCode, `first deploy failed: ${first.stdout}\n${first.stderr}`).toBe(0);
224280
expect(first.stdout).toContain("Deploy complete");
281+
const firstCid = extractMetadataCid(first.stdout);
282+
expect(firstCid, "first deploy did not print Metadata CID").not.toBeNull();
225283

226-
const second = await dot([
227-
"deploy",
228-
"--signer", "dev",
229-
"--domain", domain,
230-
"--buildDir", absBuildDir(frontendOnly),
231-
"--no-build",
232-
"--playground",
233-
"--suri", SIGNER.suri,
234-
"--dir", frontendOnly,
235-
], { timeout: 400_000 });
236-
expect(
237-
second.exitCode,
238-
`re-deploy failed: ${second.stdout}\n${second.stderr}`,
239-
).toBe(0);
240-
expect(second.stdout).toContain("Deploy complete");
284+
// Mutate the build output between the two deploys so the second
285+
// deploy must produce a DIFFERENT metadata CID. Without this, a
286+
// regression where the second deploy silently no-ops (returns the
287+
// previous result without re-publishing) would still print
288+
// "Deploy complete" with the old CID and the test would pass.
289+
const indexHtml = resolve(absBuildDir(frontendOnly), "index.html");
290+
const original = readFileSync(indexHtml, "utf8");
291+
writeFileSync(
292+
indexHtml,
293+
`${original}\n<!-- redeploy marker ${Date.now()} -->\n`,
294+
);
295+
296+
try {
297+
const second = await dot([
298+
"deploy",
299+
"--signer", "dev",
300+
"--domain", domain,
301+
"--buildDir", absBuildDir(frontendOnly),
302+
"--no-build",
303+
"--playground",
304+
"--suri", SIGNER.suri,
305+
"--dir", frontendOnly,
306+
], { timeout: 400_000 });
307+
expect(
308+
second.exitCode,
309+
`re-deploy failed: ${second.stdout}\n${second.stderr}`,
310+
).toBe(0);
311+
expect(second.stdout).toContain("Deploy complete");
312+
const secondCid = extractMetadataCid(second.stdout);
313+
expect(secondCid, "re-deploy did not print Metadata CID").not.toBeNull();
314+
// Different content → different metadata CID. If these match,
315+
// the second deploy didn't actually re-publish.
316+
expect(
317+
secondCid,
318+
`re-deploy produced same CID as first — content didn't change on chain`,
319+
).not.toBe(firstCid);
320+
// And the registry should reflect the latest publish.
321+
const entry = await getApp(`${domain}.dot`);
322+
expect(entry).not.toBeNull();
323+
expect(entry!.metadataUri).toContain(secondCid!);
324+
} finally {
325+
// Restore so subsequent tests see the original fixture content.
326+
writeFileSync(indexHtml, original);
327+
}
241328
});
242329

243330
test("domain taken by another account shows unavailable", { timeout: 900_000 }, async () => {

e2e/cli/diagnostic.test.ts

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,50 @@ import { fixturePath } from "./fixtures/templates.js";
1414
const frontendOnly = fixturePath("frontend-only");
1515

1616
describe("diagnostic mode", () => {
17-
test("DOT_DEPLOY_VERBOSE=1 does not break deploy preflight", async () => {
18-
// Use SIGNER (funded by globalSetup) so preflight reaches the
19-
// availability check; ALICE's balance is too low and the run would
20-
// abort before producing any verbose-eligible output.
17+
test(
18+
"DOT_DEPLOY_VERBOSE=1 produces timestamped log lines during storage phase",
19+
{ timeout: 300_000 },
20+
async () => {
21+
// Need a deploy that actually reaches the storage phase — that's
22+
// where bulletin-deploy logs and verbose-mode wraps its output
23+
// with "[+<seconds>s] " timestamps (see src/utils/deploy/storage.ts).
24+
// Re-deploying a domain SIGNER already owns is the cheapest way
25+
// to get there.
26+
const result = await dot([
27+
"deploy",
28+
"--signer", "dev",
29+
"--domain", E2E_DOMAINS.preflight,
30+
"--buildDir", resolve(frontendOnly, "dist"),
31+
"--no-build",
32+
"--playground",
33+
"--suri", SIGNER.suri,
34+
"--dir", frontendOnly,
35+
], {
36+
env: { DOT_DEPLOY_VERBOSE: "1" },
37+
timeout: 280_000,
38+
});
39+
const output = result.stdout + result.stderr;
40+
// Real preflight checkpoint — only printed after signer/mapping
41+
// passes.
42+
expect(
43+
output,
44+
`expected to reach availability check with verbose on\n${output}`,
45+
).toContain("Checking availability");
46+
// Verbose-only marker. Format: "[+12.3s] <line>". This prefix
47+
// appears nowhere else, so matching it is the only way to prove
48+
// DOT_DEPLOY_VERBOSE wasn't silently ignored.
49+
expect(
50+
output,
51+
`expected verbose-only "[+Ns] ..." marker in output\n${output}`,
52+
).toMatch(/\[\+\d+\.\d+s\]/);
53+
},
54+
);
55+
56+
test("DOT_MEMORY_TRACE=1 produces RSS sample lines during a real command", async () => {
57+
// `--help` exits before the memory watchdog has a chance to sample.
58+
// Run a deploy preflight instead — the watchdog samples once per
59+
// second and writes RSS/heap/external lines to stderr when the env
60+
// var is set (see src/utils/process-guard.ts startMemoryWatchdog).
2161
const result = await dot([
2262
"deploy",
2363
"--signer", "dev",
@@ -28,30 +68,18 @@ describe("diagnostic mode", () => {
2868
"--suri", SIGNER.suri,
2969
"--dir", frontendOnly,
3070
], {
31-
env: { DOT_DEPLOY_VERBOSE: "1" },
71+
env: { DOT_MEMORY_TRACE: "1" },
3272
timeout: 120_000,
3373
});
3474
const output = result.stdout + result.stderr;
35-
// Reaching this banner is the real signal that preflight survived.
36-
// Earlier this asserted /checking availability|deploy|mainnet/i — the
37-
// `deploy` alternative matched the command name itself, so the test
38-
// passed even when nothing of substance ran.
75+
// Verbose-only prefix from src/utils/process-guard.ts watchdog worker:
76+
// "[mem +<seconds>s] rss=<bytes> heap=<used>/<total> external=... peak=..."
77+
// That bracketed prefix is unique to this code path, so matching it
78+
// proves DOT_MEMORY_TRACE actually engaged the sampler — not just
79+
// that some other code wrote the word "rss" somewhere.
3980
expect(
4081
output,
41-
`expected to reach availability check with verbose on\n${output}`,
42-
).toContain("Checking availability");
43-
});
44-
45-
test("DOT_MEMORY_TRACE=1 does not prevent normal operation", async () => {
46-
const result = await dot(["--help"], {
47-
env: { DOT_MEMORY_TRACE: "1" },
48-
timeout: 15_000,
49-
});
50-
expect(result.exitCode).toBe(0);
51-
// `deploy` appears in the subcommand list — that's the meaningful
52-
// signal here. Pair with another known help string to make sure
53-
// we got real `--help` output, not a stray match.
54-
expect(result.stdout).toContain("deploy");
55-
expect(result.stdout).toContain("Usage:");
82+
`expected memory-trace markers in output\n${output}`,
83+
).toMatch(/\[mem \+\d+\.\d+s\]/);
5684
});
5785
});

0 commit comments

Comments
 (0)