Skip to content

Commit 70de550

Browse files
altaywtfclaude
andauthored
test: run CLI entry tests in-process so V8 coverage attributes them (#65)
Mirrors the design landed in putdotio/taizn#62. Vitest 4 cannot attribute V8 coverage from spawned subprocesses (coverage.autoAttachSubprocess lands in vitest 5), and NODE_V8_COVERAGE plumbing plus bundle remapping is disproportionate machinery. rokit's mainEffect(argv) was already importable, so the completions and usage-error spawn tests now run it in-process with console spies; only the boot test still spawns the packaged binary, and that blind spot (the src/rokit.ts shim, already excluded) is documented in vite.config.ts and AGENTS.md. Coverage: statements 65.91->68.96, branches 53.42->55.88, functions 57.86->60.73, lines 65.83->68.73 (same denominators). Floors raised 63/51/55/63 -> 66/53/58/66. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 12838c7 commit 70de550

3 files changed

Lines changed: 60 additions & 9 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ guide doesn't cover, search through the source code in `node_modules/effect/src`
7474
## Sharp Edges
7575

7676
- Missing config/env and child-command failures should not print stack traces.
77+
- CLI tests default to in-process entry points (`mainEffect` from `src/cli.ts`
78+
or the command effects) so V8 coverage attributes them. Spawn
79+
`dist/rokit.mjs` only when the process boundary itself is under test; those
80+
runs are invisible to coverage on vitest 4 (see `vite.config.ts`).
7781
- `ROKIT_PASSWORD` is required only for developer-installer operations such as
7882
install and screenshot.
7983
- `ROKU_DEV_TARGET` and `ROKU_DEV_PASSWORD` are optional fallback aliases, not

test/cli.test.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { dirname, join, resolve } from "node:path";
44
import { fileURLToPath } from "node:url";
55
import { NodeServices } from "@effect/platform-node";
66
import { Effect } from "effect";
7-
import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test";
7+
import { afterAll, beforeAll, describe, expect, it, vi } from "vite-plus/test";
8+
import { mainEffect } from "../src/cli.js";
89
import { parseEffectCliEffect } from "../src/cli-command.js";
910
import { describeCli } from "../src/cli-describe.js";
1011
import { parseInputJsonEffect } from "../src/cli-input-json.js";
@@ -27,13 +28,47 @@ afterAll(() => {
2728
rmSync(testDistDir, { force: true, recursive: true });
2829
});
2930

31+
// Kept for the boot test below, which proves the packaged binary itself.
32+
// Everything else runs the same CLI entry in-process (mainEffect or the
33+
// command effects) so V8 coverage attributes it.
3034
const runRokit = (args: readonly string[]) =>
3135
spawnSync(process.execPath, [cliPath, ...args], {
3236
cwd: repoRoot,
3337
encoding: "utf8",
3438
env: childCliEnv(),
3539
});
3640

41+
const runRokitInProcess = async (
42+
args: readonly string[],
43+
): Promise<{ readonly status: number; readonly stderr: string; readonly stdout: string }> => {
44+
const stdout: string[] = [];
45+
const stderr: string[] = [];
46+
const format = (parts: readonly unknown[]) => parts.map(String).join(" ");
47+
const logSpy = vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => {
48+
stdout.push(format(parts));
49+
});
50+
const errorSpy = vi.spyOn(console, "error").mockImplementation((...parts: unknown[]) => {
51+
stderr.push(format(parts));
52+
});
53+
const previousExitCode = process.exitCode;
54+
process.exitCode = 0;
55+
56+
try {
57+
await Effect.runPromise(mainEffect(args).pipe(Effect.provide(NodeServices.layer)));
58+
const joinLines = (lines: readonly string[]) => lines.map((line) => `${line}\n`).join("");
59+
60+
return {
61+
status: process.exitCode === 0 ? 0 : 1,
62+
stderr: joinLines(stderr),
63+
stdout: joinLines(stdout),
64+
};
65+
} finally {
66+
process.exitCode = previousExitCode;
67+
logSpy.mockRestore();
68+
errorSpy.mockRestore();
69+
}
70+
};
71+
3772
const childCliEnv = (): NodeJS.ProcessEnv => {
3873
const excludedNames = new Set([
3974
"NODE_OPTIONS",
@@ -89,8 +124,8 @@ describe("rokit CLI functionality", () => {
89124
expect(shortResult.stderr).toBe("");
90125
});
91126

92-
it("prints advertised shell completions", () => {
93-
const result = runRokit(["--completions", "bash"]);
127+
it("prints advertised shell completions", async () => {
128+
const result = await runRokitInProcess(["--completions", "bash"]);
94129

95130
expect(result.status).toBe(0);
96131
expect(result.stdout).toContain("rokit");
@@ -272,8 +307,14 @@ describe("rokit CLI functionality", () => {
272307
);
273308
});
274309

275-
it("prints package output usage errors without stack traces", () => {
276-
const result = runRokit(["--json", "package", "out/channel", "--out", "out/other"]);
310+
it("prints package output usage errors without stack traces", async () => {
311+
const result = await runRokitInProcess([
312+
"--json",
313+
"package",
314+
"out/channel",
315+
"--out",
316+
"out/other",
317+
]);
277318

278319
expect(result.status).toBe(1);
279320
expect(result.stdout).toBe("");

vite.config.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ import { defineConfig } from "vite-plus";
33
export default defineConfig({
44
test: {
55
coverage: {
6+
// Coverage blind spot: the boot test spawns the packaged dist/rokit.mjs
7+
// to prove the real binary starts; V8 coverage cannot attribute
8+
// subprocess execution on vitest 4, so the bin shim src/rokit.ts is
9+
// excluded by design. All other CLI tests run the same entry in-process
10+
// (mainEffect and the command effects). When vite-plus ships vitest 5,
11+
// coverage.autoAttachSubprocess can close the remaining gap.
612
exclude: ["src/**/*.d.ts", "src/rokit.ts"],
713
include: ["src/**/*.ts"],
814
provider: "v8",
915
reporter: ["text", "lcov"],
1016
thresholds: {
11-
branches: 51,
12-
functions: 55,
13-
lines: 63,
14-
statements: 63,
17+
branches: 53,
18+
functions: 58,
19+
lines: 66,
20+
statements: 66,
1521
},
1622
},
1723
exclude: ["dist/**", "node_modules/**", ".repos/**"],

0 commit comments

Comments
 (0)