Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ guide doesn't cover, search through the source code in `node_modules/effect/src`
## Sharp Edges

- Missing config/env and child-command failures should not print stack traces.
- CLI tests default to in-process entry points (`mainEffect` from `src/cli.ts`
or the command effects) so V8 coverage attributes them. Spawn
`dist/rokit.mjs` only when the process boundary itself is under test; those
runs are invisible to coverage on vitest 4 (see `vite.config.ts`).
- `ROKIT_PASSWORD` is required only for developer-installer operations such as
install and screenshot.
- `ROKU_DEV_TARGET` and `ROKU_DEV_PASSWORD` are optional fallback aliases, not
Expand Down
51 changes: 46 additions & 5 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { NodeServices } from "@effect/platform-node";
import { Effect } from "effect";
import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test";
import { afterAll, beforeAll, describe, expect, it, vi } from "vite-plus/test";
import { mainEffect } from "../src/cli.js";
import { parseEffectCliEffect } from "../src/cli-command.js";
import { describeCli } from "../src/cli-describe.js";
import { parseInputJsonEffect } from "../src/cli-input-json.js";
Expand All @@ -27,13 +28,47 @@ afterAll(() => {
rmSync(testDistDir, { force: true, recursive: true });
});

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

const runRokitInProcess = async (
args: readonly string[],
): Promise<{ readonly status: number; readonly stderr: string; readonly stdout: string }> => {
const stdout: string[] = [];
const stderr: string[] = [];
const format = (parts: readonly unknown[]) => parts.map(String).join(" ");
const logSpy = vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => {
stdout.push(format(parts));
});
const errorSpy = vi.spyOn(console, "error").mockImplementation((...parts: unknown[]) => {
stderr.push(format(parts));
});
const previousExitCode = process.exitCode;
process.exitCode = 0;

try {
await Effect.runPromise(mainEffect(args).pipe(Effect.provide(NodeServices.layer)));
const joinLines = (lines: readonly string[]) => lines.map((line) => `${line}\n`).join("");

return {
status: process.exitCode === 0 ? 0 : 1,
stderr: joinLines(stderr),
stdout: joinLines(stdout),
};
} finally {
process.exitCode = previousExitCode;
logSpy.mockRestore();
errorSpy.mockRestore();
}
};

const childCliEnv = (): NodeJS.ProcessEnv => {
const excludedNames = new Set([
"NODE_OPTIONS",
Expand Down Expand Up @@ -89,8 +124,8 @@ describe("rokit CLI functionality", () => {
expect(shortResult.stderr).toBe("");
});

it("prints advertised shell completions", () => {
const result = runRokit(["--completions", "bash"]);
it("prints advertised shell completions", async () => {
const result = await runRokitInProcess(["--completions", "bash"]);

expect(result.status).toBe(0);
expect(result.stdout).toContain("rokit");
Expand Down Expand Up @@ -272,8 +307,14 @@ describe("rokit CLI functionality", () => {
);
});

it("prints package output usage errors without stack traces", () => {
const result = runRokit(["--json", "package", "out/channel", "--out", "out/other"]);
it("prints package output usage errors without stack traces", async () => {
const result = await runRokitInProcess([
"--json",
"package",
"out/channel",
"--out",
"out/other",
]);

expect(result.status).toBe(1);
expect(result.stdout).toBe("");
Expand Down
14 changes: 10 additions & 4 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ import { defineConfig } from "vite-plus";
export default defineConfig({
test: {
coverage: {
// Coverage blind spot: the boot test spawns the packaged dist/rokit.mjs
// to prove the real binary starts; V8 coverage cannot attribute
// subprocess execution on vitest 4, so the bin shim src/rokit.ts is
// excluded by design. All other CLI tests run the same entry in-process
// (mainEffect and the command effects). When vite-plus ships vitest 5,
// coverage.autoAttachSubprocess can close the remaining gap.
exclude: ["src/**/*.d.ts", "src/rokit.ts"],
include: ["src/**/*.ts"],
provider: "v8",
reporter: ["text", "lcov"],
thresholds: {
branches: 51,
functions: 55,
lines: 63,
statements: 63,
branches: 53,
functions: 58,
lines: 66,
statements: 66,
},
},
exclude: ["dist/**", "node_modules/**", ".repos/**"],
Expand Down