Skip to content

Commit 14ec717

Browse files
committed
test(e2e): tighten fake-passing assertions across the suite
Several tests were passing without actually exercising the path their name claimed. The common patterns: - Loose regexes like `/checking availability|deploy/i` always matched because "deploy" is the command name itself, so the test passed even when nothing of substance ran. - exitCode-only assertions on failure paths — any unrelated crash that produced a non-zero exit satisfied the check. - Generic word matches (`/not found/i`, `/signer|init|log.?in/i`) that could match help text, stack traces, or transient gateway errors rather than the specific failure mode under test. Fixes per file: - deploy.test.ts: replaced `/checking availability|deploy/i` with the literal "Checking availability" checkpoint (only printed after preflight succeeds). Tightened the mainnet, --contracts-no-project, and "domain taken by another account" tests with exit-code assertions and exact error fragments. - mod.test.ts: registry-miss now asserts the exact "not found in registry" wording plus the domain; signer-suggestion asserts the exact "No signer available" / "dot init" text rather than matching any of those words anywhere. - diagnostic.test.ts: DOT_DEPLOY_VERBOSE test switched off ALICE (unfunded → would abort before producing verbose output) onto SIGNER + --no-build so it actually reaches the availability check. - session.test.ts: "build does not touch sessions" now verifies both builds returned exit 0 (a crashed build can't write any file, so the previous version was tautological); "logout with no session" now asserts exitCode === 0. - init.test.ts: dropped `-y` from the session-detection tests — with -y, init skips the connect()/login block entirely, so the tests were really just verifying that toolchain installation in a fresh tempHome takes longer than 15s. Now they assert on the actual "Scan with the Polkadot mobile app" / "Login skipped" output. Added a new `dot init — toolchain detection` test that strips rustup/cargo/foundry from PATH and verifies init does NOT report ✓ rustup — catches detection regressions even though CI runners have toolchains pre-installed.
1 parent f0b6875 commit 14ec717

5 files changed

Lines changed: 231 additions & 38 deletions

File tree

e2e/cli/deploy.test.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ function absBuildDir(fixture: string, dir = "dist"): string {
3030
return resolve(fixture, dir);
3131
}
3232

33+
/**
34+
* Assertion notes for the preflight tests below:
35+
* - "Checking availability" is printed by `src/commands/deploy/index.ts` ONLY
36+
* after preflight (signer + mapping + balance) has succeeded. Asserting on
37+
* it is a real checkpoint — a deploy that crashes earlier won't print it.
38+
* - Avoid loose regexes like `/deploy/i`: the literal word "deploy" appears
39+
* in the command banner, error-help text, and stack traces, so it matches
40+
* even when nothing meaningful happened.
41+
* - For tests that expect failure, assert `exitCode !== 0` so an early crash
42+
* that prints something tangentially matching the regex can't slip through.
43+
*/
44+
3345
describe("dot deploy — preflight and validation", () => {
3446
test("reports mainnet not yet supported", async () => {
3547
const result = await dot([
@@ -43,8 +55,15 @@ describe("dot deploy — preflight and validation", () => {
4355
"--dir", frontendOnly,
4456
], { timeout: 400_000 });
4557
const output = result.stdout + result.stderr;
46-
expect(output).toMatch(/mainnet/i);
47-
expect(output).toMatch(/not.*supported/i);
58+
expect(
59+
result.exitCode,
60+
`expected non-zero exit for --env mainnet, got 0\n${output}`,
61+
).not.toBe(0);
62+
// Exact wording from src/commands/deploy/index.ts: "`--env mainnet` is
63+
// not yet supported. Use `--env testnet` (default) while mainnet launch
64+
// is pending."
65+
expect(output).toContain("not yet supported");
66+
expect(output).toContain("--env testnet");
4867
});
4968

5069
test("detects foundry contracts type in project", async () => {
@@ -62,8 +81,11 @@ describe("dot deploy — preflight and validation", () => {
6281
const output = result.stdout + result.stderr;
6382
// foundry.toml present → should not complain about missing contract project
6483
expect(output).not.toContain("no foundry/hardhat/cdm project was detected");
65-
// Should proceed to at least the availability check
66-
expect(output).toMatch(/checking availability|deploy/i);
84+
// Real checkpoint: only printed after preflight succeeds.
85+
expect(
86+
output,
87+
`expected to reach availability check\n${output}`,
88+
).toContain("Checking availability");
6789
});
6890

6991
test("detects hardhat contracts type in project", async () => {
@@ -80,7 +102,10 @@ describe("dot deploy — preflight and validation", () => {
80102
]);
81103
const output = result.stdout + result.stderr;
82104
expect(output).not.toContain("no foundry/hardhat/cdm project was detected");
83-
expect(output).toMatch(/checking availability|deploy/i);
105+
expect(
106+
output,
107+
`expected to reach availability check\n${output}`,
108+
).toContain("Checking availability");
84109
});
85110

86111
test("detects CDM/Rust contracts type in project", async () => {
@@ -97,7 +122,10 @@ describe("dot deploy — preflight and validation", () => {
97122
]);
98123
const output = result.stdout + result.stderr;
99124
expect(output).not.toContain("no foundry/hardhat/cdm project was detected");
100-
expect(output).toMatch(/checking availability|deploy/i);
125+
expect(
126+
output,
127+
`expected to reach availability check\n${output}`,
128+
).toContain("Checking availability");
101129
});
102130

103131
test("detects multiple contracts in multi-contract project", async () => {
@@ -114,7 +142,10 @@ describe("dot deploy — preflight and validation", () => {
114142
]);
115143
const output = result.stdout + result.stderr;
116144
expect(output).not.toContain("no foundry/hardhat/cdm project was detected");
117-
expect(output).toMatch(/checking availability|deploy/i);
145+
expect(
146+
output,
147+
`expected to reach availability check\n${output}`,
148+
).toContain("Checking availability");
118149
});
119150

120151
test("--contracts reports error when no contract project detected", async () => {
@@ -130,6 +161,10 @@ describe("dot deploy — preflight and validation", () => {
130161
"--dir", frontendOnly,
131162
], { timeout: 400_000 });
132163
const output = result.stdout + result.stderr;
164+
expect(
165+
result.exitCode,
166+
`expected non-zero exit when --contracts has no project\n${output}`,
167+
).not.toBe(0);
133168
expect(output).toContain("no foundry/hardhat/cdm project was detected");
134169
});
135170

@@ -145,8 +180,10 @@ describe("dot deploy — preflight and validation", () => {
145180
"--dir", frontendOnly,
146181
], { timeout: 400_000 });
147182
const output = result.stdout + result.stderr;
148-
expect(output).toContain("Checking availability");
149-
expect(output).toContain(domain);
183+
// Availability banner names the domain; this is the strongest signal we
184+
// have that the availability check actually executed against this run's
185+
// domain (rather than echoing the arg in a usage/error string).
186+
expect(output).toContain(`Checking availability of ${domain}.dot`);
150187
});
151188
});
152189

@@ -234,6 +271,15 @@ describe("dot deploy --playground — full pipeline (requires Paseo + IPFS)", ()
234271
bobDeploy.exitCode,
235272
`bob deploy unexpectedly succeeded: ${bobDeploy.stdout}\n${bobDeploy.stderr}`,
236273
).not.toBe(0);
237-
expect(output.toLowerCase()).toMatch(/revert|taken|registered|owned|unavailable|already/);
274+
// Exact wording from src/utils/deploy/availability.ts:
275+
// "{domain}.dot is already registered by {owner} — transfer it or use
276+
// a different name"
277+
// The previous /revert|taken|registered|owned|unavailable|already/
278+
// regex matched any of those words anywhere — including transient
279+
// network errors and unrelated runtime stack traces — so it could not
280+
// distinguish "Bob hit the right ownership conflict" from "Bob hit
281+
// some other failure that happened to mention 'registered'".
282+
expect(output).toContain("already registered");
283+
expect(output).toContain(domain);
238284
});
239285
});

e2e/cli/diagnostic.test.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,40 @@
66
*/
77

88
import { describe, test, expect } from "vitest";
9+
import { resolve } from "node:path";
910
import { dot } from "./helpers/dot.js";
10-
import { ALICE } from "./fixtures/accounts.js";
11+
import { SIGNER, E2E_DOMAINS } from "./fixtures/accounts.js";
1112
import { fixturePath } from "./fixtures/templates.js";
1213

1314
const frontendOnly = fixturePath("frontend-only");
1415

1516
describe("diagnostic mode", () => {
1617
test("DOT_DEPLOY_VERBOSE=1 does not break deploy preflight", async () => {
17-
// Run a deploy that will reach preflight with verbose enabled.
18-
// We don't need it to succeed — just verify verbose doesn't crash anything.
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.
1921
const result = await dot([
2022
"deploy",
2123
"--signer", "dev",
22-
"--domain", "diag-verbose-test",
23-
"--buildDir", "dist",
24+
"--domain", E2E_DOMAINS.preflight,
25+
"--buildDir", resolve(frontendOnly, "dist"),
26+
"--no-build",
2427
"--playground",
25-
"--suri", ALICE.suri,
28+
"--suri", SIGNER.suri,
2629
"--dir", frontendOnly,
2730
], {
2831
env: { DOT_DEPLOY_VERBOSE: "1" },
29-
timeout: 30_000,
32+
timeout: 120_000,
3033
});
3134
const output = result.stdout + result.stderr;
32-
// Should reach the availability check even with verbose on
33-
expect(output).toMatch(/checking availability|deploy|mainnet/i);
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.
39+
expect(
40+
output,
41+
`expected to reach availability check with verbose on\n${output}`,
42+
).toContain("Checking availability");
3443
});
3544

3645
test("DOT_MEMORY_TRACE=1 does not prevent normal operation", async () => {
@@ -39,6 +48,10 @@ describe("diagnostic mode", () => {
3948
timeout: 15_000,
4049
});
4150
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.
4254
expect(result.stdout).toContain("deploy");
55+
expect(result.stdout).toContain("Usage:");
4356
});
4457
});

e2e/cli/init.test.ts

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@
66
* - Behavior when no session exists (prompts for QR, times out)
77
* - Corrupted session file handling
88
* - Dev signer (--suri) bypasses session requirement
9+
*
10+
* KNOWN GAP — toolchain install paths (rustup, IPFS, foundry, cdm) are NOT
11+
* exercised in CI because the runner image already has those tools on PATH
12+
* before tests run. As a result, regressions in the install / post-install
13+
* path-config logic (e.g. paritytech/playground-app#118 — newly-installed
14+
* rustup not reachable from the same init process) will pass these tests
15+
* silently. To catch that class of bug, init has to be exercised in a fresh
16+
* sandbox (Docker / VM with no Rust toolchain). The test below at least
17+
* pins the toolchain *detection* output so the dependency table can't be
18+
* silently dropped, but it does NOT validate the install-then-use flow.
919
*/
1020

1121
import { describe, test, expect, beforeEach, afterEach } from "vitest";
@@ -14,6 +24,20 @@ import { join } from "node:path";
1424
import { tmpdir } from "node:os";
1525
import { dot } from "./helpers/dot.js";
1626

27+
/** PATH stripped of rustup/cargo/foundry locations. The init process must
28+
* still be able to find `node`/`bun`/`pnpm`/`curl`/`bash`, so we keep the
29+
* rest of PATH intact. Patterns are deliberately broad (cargo/rustup/
30+
* foundry anywhere in the segment) so we strip both `~/.cargo/bin` style
31+
* paths and `/opt/cargo/bin` / `/usr/local/cargo/bin` style ones. */
32+
function pathWithoutToolchains(): string {
33+
const original = process.env.PATH ?? "/usr/bin:/bin";
34+
const stripPatterns = [/cargo/i, /rustup/i, /foundry/i];
35+
return original
36+
.split(":")
37+
.filter((p) => p.length > 0 && !stripPatterns.some((re) => re.test(p)))
38+
.join(":");
39+
}
40+
1741
function makeTempHome(): string {
1842
const dir = mkdtempSync(join(tmpdir(), "dot-e2e-init-"));
1943
mkdirSync(join(dir, ".polkadot-apps"), { recursive: true });
@@ -36,28 +60,46 @@ describe("dot init — session detection", () => {
3660
} catch { /* best-effort */ }
3761
});
3862

39-
test("init with no session times out waiting for QR scan", async () => {
40-
const result = await dot(["init", "-y"], {
63+
test("init with no session prompts for QR scan", async () => {
64+
// IMPORTANT: do NOT pass `-y` here. With `-y`, init skips the
65+
// connect()/login block entirely — there's no session probe and no
66+
// QR. The previous version of this test used `-y` and only asserted
67+
// `exitCode !== 0`, which simply verified that toolchain installation
68+
// in a fresh tempHome takes longer than 15s — nothing about sessions.
69+
const result = await dot(["init"], {
4170
home: tempHome,
4271
timeout: 15_000,
4372
});
44-
// With no session files and no phone to scan, the CLI should either:
45-
// - time out waiting for the statement store QR
46-
// - exit non-zero because -y can't complete without a session
47-
// Either way it should not succeed silently.
48-
expect(result.exitCode).not.toBe(0);
73+
const output = result.stdout + result.stderr;
74+
expect(
75+
result.exitCode,
76+
`expected non-zero exit while waiting for QR\n${output}`,
77+
).not.toBe(0);
78+
// Exact string from src/commands/init/index.ts when no existing session
79+
// is found. Either we see the QR prompt, or the login service was
80+
// unreachable and the CLI logs "Login skipped".
81+
expect(output).toMatch(
82+
/Scan with the Polkadot mobile app to log in|Login skipped/,
83+
);
4984
});
5085

51-
test("init with corrupted session file does not succeed", async () => {
86+
test("init with corrupted session file does not silently succeed", async () => {
5287
const sessionFile = join(tempHome, ".polkadot-apps", "dot-cli_SsoSessions.json");
5388
writeFileSync(sessionFile, "{{{{not valid json!!");
5489

55-
const result = await dot(["init", "-y"], {
90+
const result = await dot(["init"], {
5691
home: tempHome,
5792
timeout: 15_000,
5893
});
59-
// Corrupted session should cause a parse error or fall through to QR timeout
94+
const output = result.stdout + result.stderr;
6095
expect(result.exitCode).not.toBe(0);
96+
// A corrupted session file must NOT lead to an "existing session"
97+
// branch — that would be a security-relevant failure. We accept either
98+
// the fresh-login QR prompt or a login-skipped notice; both prove the
99+
// corrupt file was rejected rather than trusted.
100+
expect(output).toMatch(
101+
/Scan with the Polkadot mobile app to log in|Login skipped/,
102+
);
61103
});
62104
});
63105

@@ -75,3 +117,62 @@ describe("dot init — dev signer bypass", () => {
75117
}
76118
});
77119
});
120+
121+
describe("dot init — toolchain detection", () => {
122+
let tempHome: string;
123+
124+
beforeEach(() => {
125+
tempHome = makeTempHome();
126+
});
127+
128+
afterEach(() => {
129+
try {
130+
rmSync(tempHome, { recursive: true, force: true });
131+
} catch { /* best-effort */ }
132+
});
133+
134+
test(
135+
"detects rustup as missing when not on PATH",
136+
{ timeout: 30_000 },
137+
async () => {
138+
// Strip rustup/cargo from PATH and verify init reports rustup as
139+
// a missing dependency rather than skipping straight to "✓ rustup".
140+
//
141+
// Why this matters: CI runners pre-install rustup, so the regular
142+
// init tests never exercise the missing-tool detection or the
143+
// post-install path-config logic. This test forces init to hit
144+
// the "rustup not found" branch by removing it from PATH.
145+
//
146+
// We use a 5s timeout — long enough for init's TUI to render the
147+
// dependency table (and start a curl install attempt), short
148+
// enough that we don't actually finish a real rustup install.
149+
// We do NOT assert exitCode here; execa terminates the process at
150+
// the timeout, so exitCode is the kill signal, not a real result.
151+
const result = await dot(["init"], {
152+
home: tempHome,
153+
env: { PATH: pathWithoutToolchains() },
154+
timeout: 5_000,
155+
});
156+
const output = result.stdout + result.stderr;
157+
// The TUI prints each dependency on its own row. Seeing "rustup"
158+
// proves the detection table rendered. Pair with one of the later
159+
// rows so a single-line corruption can't pass.
160+
expect(
161+
output,
162+
`expected dependency table to render\n${output}`,
163+
).toContain("rustup");
164+
expect(
165+
output,
166+
`expected later toolchain rows in dependency table\n${output}`,
167+
).toMatch(/Rust nightly|cdm|foundry|IPFS/);
168+
// A "✓ rustup" with this PATH would mean init falsely concluded
169+
// rustup is installed — exactly the class of bug that lets fresh
170+
// users hit broken installs in production. (Fresh installs render
171+
// the row as "· rustup" or "⠋ rustup", not "✓".)
172+
expect(
173+
output,
174+
`init reported rustup as installed despite stripped PATH:\n${output}`,
175+
).not.toMatch(/\s+rustup\b/);
176+
},
177+
);
178+
});

e2e/cli/mod.test.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,24 @@ describe("dot mod", () => {
5959
},
6060
);
6161

62-
test("reports a registry-miss for an unknown domain", async () => {
62+
test("reports a registry-miss for an unknown domain", { timeout: 120_000 }, async () => {
6363
const cwd = makeTempDir("dot-e2e-mod-unknown-");
64+
const domain = "nonexistent-domain-xyz-12345.dot";
6465
const result = await dot(
65-
["mod", "nonexistent-domain-xyz-12345.dot", "--suri", ALICE.suri],
66-
{ cwd },
66+
["mod", domain, "--suri", ALICE.suri],
67+
{ cwd, timeout: 120_000 },
6768
);
6869
const output = result.stdout + result.stderr;
69-
expect(result.exitCode).not.toBe(0);
70-
expect(output).toMatch(/not found/i);
70+
expect(
71+
result.exitCode,
72+
`expected non-zero exit for unknown domain\n${output}`,
73+
).not.toBe(0);
74+
// Exact wording from src/commands/mod/SetupScreen.tsx:
75+
// throw new Error(`App "${domain}" not found in registry`);
76+
// Matching both fragments rules out an unrelated "not found" landing
77+
// in output (e.g., a transient 404 from an IPFS gateway probe).
78+
expect(output).toContain(domain);
79+
expect(output).toContain("not found in registry");
7180
});
7281

7382
test("exits non-zero with signer suggestion when no signer available", async () => {
@@ -76,6 +85,12 @@ describe("dot mod", () => {
7685
const result = await dot(["mod", "some-app.dot"], { home: tempHome, cwd });
7786
expect(result.exitCode).not.toBe(0);
7887
const output = result.stdout + result.stderr;
79-
expect(output).toMatch(/signer|init|log.?in/i);
88+
// Exact wording from src/utils/signer.ts SignerNotAvailableError:
89+
// `No signer available. Run "dot init" to log in, or pass --suri //Alice for dev.`
90+
// The previous regex /signer|init|log.?in/i matched any of those words
91+
// anywhere — including help text — so it passed even on early crashes
92+
// that never reached the signer-resolution path.
93+
expect(output).toContain("No signer available");
94+
expect(output).toContain("dot init");
8095
});
8196
});

0 commit comments

Comments
 (0)