From c3425358da631e2c34de942f063c0a729a463f9b Mon Sep 17 00:00:00 2001 From: root Date: Thu, 4 Jun 2026 09:10:46 -0900 Subject: [PATCH 01/10] test(cli): add smoke tests against a minimal fixture scaffold Run top-level CLI commands in a git-backed smoke project and assert exit codes and key output, including --version matching package.json. --- test/cli-smoke.test.ts | 109 ++++++++++++++++++ test/fixtures/smoke-project/.mex/AGENTS.md | 19 +++ test/fixtures/smoke-project/.mex/ROUTER.md | 18 +++ .../.mex/context/architecture.md | 9 ++ .../smoke-project/.mex/context/conventions.md | 9 ++ .../smoke-project/.mex/context/decisions.md | 9 ++ .../smoke-project/.mex/context/setup.md | 9 ++ .../smoke-project/.mex/context/stack.md | 9 ++ .../smoke-project/.mex/patterns/INDEX.md | 9 ++ test/fixtures/smoke-project/package.json | 4 + test/fixtures/smoke-project/src/index.ts | 1 + 11 files changed, 205 insertions(+) create mode 100644 test/cli-smoke.test.ts create mode 100644 test/fixtures/smoke-project/.mex/AGENTS.md create mode 100644 test/fixtures/smoke-project/.mex/ROUTER.md create mode 100644 test/fixtures/smoke-project/.mex/context/architecture.md create mode 100644 test/fixtures/smoke-project/.mex/context/conventions.md create mode 100644 test/fixtures/smoke-project/.mex/context/decisions.md create mode 100644 test/fixtures/smoke-project/.mex/context/setup.md create mode 100644 test/fixtures/smoke-project/.mex/context/stack.md create mode 100644 test/fixtures/smoke-project/.mex/patterns/INDEX.md create mode 100644 test/fixtures/smoke-project/package.json create mode 100644 test/fixtures/smoke-project/src/index.ts diff --git a/test/cli-smoke.test.ts b/test/cli-smoke.test.ts new file mode 100644 index 0000000..b7087ae --- /dev/null +++ b/test/cli-smoke.test.ts @@ -0,0 +1,109 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { execSync, spawnSync } from "node:child_process"; +import { cpSync, existsSync, mkdtempSync, rmSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; +import { readFileSync } from "node:fs"; + +const here = dirname(fileURLToPath(import.meta.url)); +const CLI = join(here, "..", "dist", "cli.js"); +const FIXTURE_SRC = join(here, "fixtures", "smoke-project"); +const pkg = JSON.parse( + readFileSync(join(here, "..", "package.json"), "utf8"), +) as { version: string }; + +let projectRoot: string; + +function runMex(args: string[]): { status: number | null; stdout: string; stderr: string } { + const result = spawnSync(process.execPath, [CLI, ...args], { + cwd: projectRoot, + encoding: "utf8", + env: { ...process.env, NO_COLOR: "1" }, + }); + return { + status: result.status, + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + }; +} + +beforeAll(() => { + projectRoot = mkdtempSync(join(tmpdir(), "mex-smoke-")); + cpSync(FIXTURE_SRC, projectRoot, { recursive: true }); + execSync("git init -q", { cwd: projectRoot }); + execSync("git add -A", { cwd: projectRoot }); + execSync('git -c user.email=smoke@test -c user.name=smoke commit -q -m "init"', { + cwd: projectRoot, + }); +}); + +describe("CLI smoke", () => { + it("--version matches package.json", () => { + const { status, stdout } = runMex(["--version"]); + expect(status).toBe(0); + expect(stdout.trim()).toBe(pkg.version); + }); + + it("commands lists top-level commands", () => { + const { status, stdout } = runMex(["commands"]); + expect(status).toBe(0); + expect(stdout).toContain("CLI Commands"); + expect(stdout).toContain("mex check"); + }); + + it("check --quiet exits successfully on fixture", () => { + const { status, stdout } = runMex(["check", "--quiet"]); + expect(status).toBe(0); + expect(stdout.length).toBeGreaterThan(0); + }); + + it("doctor prints health summary", () => { + const { status, stdout } = runMex(["doctor"]); + expect(status).toBe(0); + expect(stdout).toContain("mex doctor"); + expect(stdout).toContain("Drift"); + }); + + it("log and timeline round-trip", () => { + const log = runMex(["log", "smoke test note", "--type", "note"]); + expect(log.status).toBe(0); + const timeline = runMex(["timeline", "--limit", "1"]); + expect(timeline.status).toBe(0); + expect(timeline.stdout).toContain("smoke test note"); + }); + + it("heartbeat --json reports status", () => { + const { status, stdout } = runMex(["heartbeat", "--json"]); + expect(status).toBe(0); + expect(stdout).toContain("{"); + }); + + it("completion bash emits script", () => { + const { status, stdout } = runMex(["completion", "bash"]); + expect(status).toBe(0); + expect(stdout).toContain("complete"); + }); + + it("sync --dry-run runs without error", () => { + const { status } = runMex(["sync", "--dry-run"]); + expect(status).toBe(0); + }); + + it("setup --dry-run previews scaffold", () => { + const { status, stdout } = runMex(["setup", "--dry-run"]); + expect(status).toBe(0); + expect(stdout.length).toBeGreaterThan(0); + }); + + it("init --json emits scanner brief", () => { + const { status, stdout } = runMex(["init", "--json"]); + expect(status).toBe(0); + expect(stdout.trim().startsWith("{")).toBe(true); + }); + + it("watch --uninstall is a no-op success", () => { + const { status } = runMex(["watch", "--uninstall"]); + expect(status).toBe(0); + }); +}); diff --git a/test/fixtures/smoke-project/.mex/AGENTS.md b/test/fixtures/smoke-project/.mex/AGENTS.md new file mode 100644 index 0000000..93cb88a --- /dev/null +++ b/test/fixtures/smoke-project/.mex/AGENTS.md @@ -0,0 +1,19 @@ +--- +name: agents +description: Smoke test project anchor +last_updated: 2026-06-01 +--- + +# Smoke Project + +## What This Is + +Minimal fixture for mex CLI smoke tests. + +## Non-Negotiables + +- Used only in automated tests + +## Commands + +- npm test diff --git a/test/fixtures/smoke-project/.mex/ROUTER.md b/test/fixtures/smoke-project/.mex/ROUTER.md new file mode 100644 index 0000000..b4bbfd0 --- /dev/null +++ b/test/fixtures/smoke-project/.mex/ROUTER.md @@ -0,0 +1,18 @@ +--- +name: router +description: Smoke test scaffold router +last_updated: 2026-06-01 +--- + +# Smoke Project + +## Current Project State + +**Working:** +- CLI smoke fixture + +**Not yet built:** +- Nothing + +**Known issues:** +- None diff --git a/test/fixtures/smoke-project/.mex/context/architecture.md b/test/fixtures/smoke-project/.mex/context/architecture.md new file mode 100644 index 0000000..e1479d8 --- /dev/null +++ b/test/fixtures/smoke-project/.mex/context/architecture.md @@ -0,0 +1,9 @@ +--- +name: architecture +description: Smoke architecture context +last_updated: 2026-06-01 +--- + +# Architecture + +Single-file smoke fixture. diff --git a/test/fixtures/smoke-project/.mex/context/conventions.md b/test/fixtures/smoke-project/.mex/context/conventions.md new file mode 100644 index 0000000..fb79799 --- /dev/null +++ b/test/fixtures/smoke-project/.mex/context/conventions.md @@ -0,0 +1,9 @@ +--- +name: conventions +description: Smoke conventions context +last_updated: 2026-06-01 +--- + +# Conventions + +Test-only conventions. diff --git a/test/fixtures/smoke-project/.mex/context/decisions.md b/test/fixtures/smoke-project/.mex/context/decisions.md new file mode 100644 index 0000000..5441ad6 --- /dev/null +++ b/test/fixtures/smoke-project/.mex/context/decisions.md @@ -0,0 +1,9 @@ +--- +name: decisions +description: Smoke decisions log +last_updated: 2026-06-01 +--- + +# Decisions + +None yet. diff --git a/test/fixtures/smoke-project/.mex/context/setup.md b/test/fixtures/smoke-project/.mex/context/setup.md new file mode 100644 index 0000000..d156d00 --- /dev/null +++ b/test/fixtures/smoke-project/.mex/context/setup.md @@ -0,0 +1,9 @@ +--- +name: setup +description: Smoke setup context +last_updated: 2026-06-01 +--- + +# Setup + +Run `npm test` from the mex repo. diff --git a/test/fixtures/smoke-project/.mex/context/stack.md b/test/fixtures/smoke-project/.mex/context/stack.md new file mode 100644 index 0000000..8e96de8 --- /dev/null +++ b/test/fixtures/smoke-project/.mex/context/stack.md @@ -0,0 +1,9 @@ +--- +name: stack +description: Smoke stack context +last_updated: 2026-06-01 +--- + +# Stack + +Node.js test fixture. diff --git a/test/fixtures/smoke-project/.mex/patterns/INDEX.md b/test/fixtures/smoke-project/.mex/patterns/INDEX.md new file mode 100644 index 0000000..006d742 --- /dev/null +++ b/test/fixtures/smoke-project/.mex/patterns/INDEX.md @@ -0,0 +1,9 @@ +--- +name: patterns-index +description: Pattern index for smoke tests +last_updated: 2026-06-01 +--- + +# Patterns + +No patterns in this fixture. diff --git a/test/fixtures/smoke-project/package.json b/test/fixtures/smoke-project/package.json new file mode 100644 index 0000000..fd6260f --- /dev/null +++ b/test/fixtures/smoke-project/package.json @@ -0,0 +1,4 @@ +{ + "name": "mex-smoke-fixture", + "private": true +} diff --git a/test/fixtures/smoke-project/src/index.ts b/test/fixtures/smoke-project/src/index.ts new file mode 100644 index 0000000..fc551e1 --- /dev/null +++ b/test/fixtures/smoke-project/src/index.ts @@ -0,0 +1 @@ +// Smoke fixture entry point for path drift checks. From bedb54fef4c2d1fe0f705ea63d47274ef8e09ed3 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 6 Jun 2026 04:15:18 +0000 Subject: [PATCH 02/10] test(cli): extend smoke coverage and tidy fixture harness Add pattern add to the CLI smoke suite, clean up unused imports, and remove the temp project directory after tests finish. --- test/cli-smoke.test.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/cli-smoke.test.ts b/test/cli-smoke.test.ts index b7087ae..eb2806d 100644 --- a/test/cli-smoke.test.ts +++ b/test/cli-smoke.test.ts @@ -1,10 +1,9 @@ -import { describe, it, expect, beforeAll } from "vitest"; +import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { execSync, spawnSync } from "node:child_process"; -import { cpSync, existsSync, mkdtempSync, rmSync } from "node:fs"; +import { cpSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { join, dirname } from "node:path"; import { tmpdir } from "node:os"; import { fileURLToPath } from "node:url"; -import { readFileSync } from "node:fs"; const here = dirname(fileURLToPath(import.meta.url)); const CLI = join(here, "..", "dist", "cli.js"); @@ -38,6 +37,10 @@ beforeAll(() => { }); }); +afterAll(() => { + rmSync(projectRoot, { recursive: true, force: true }); +}); + describe("CLI smoke", () => { it("--version matches package.json", () => { const { status, stdout } = runMex(["--version"]); @@ -106,4 +109,10 @@ describe("CLI smoke", () => { const { status } = runMex(["watch", "--uninstall"]); expect(status).toBe(0); }); + + it("pattern add creates a scaffold file", () => { + const { status, stdout } = runMex(["pattern", "add", "smoke-pattern"]); + expect(status).toBe(0); + expect(stdout).toContain("smoke-pattern.md"); + }); }); From e59f86b317bac7b01d8d795f369673691f59dba9 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 6 Jun 2026 04:55:37 +0000 Subject: [PATCH 03/10] test(cli): improve smoke harness diagnostics and cleanup Surface CLI stdout/stderr in assertion messages on non-zero exits and guard fixture teardown when setup fails before projectRoot is assigned. --- test/cli-smoke.test.ts | 94 ++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 41 deletions(-) diff --git a/test/cli-smoke.test.ts b/test/cli-smoke.test.ts index eb2806d..14a844e 100644 --- a/test/cli-smoke.test.ts +++ b/test/cli-smoke.test.ts @@ -12,21 +12,33 @@ const pkg = JSON.parse( readFileSync(join(here, "..", "package.json"), "utf8"), ) as { version: string }; -let projectRoot: string; - -function runMex(args: string[]): { status: number | null; stdout: string; stderr: string } { +let projectRoot: string | undefined; + +function runMex(args: string[]): { + status: number | null; + stdout: string; + stderr: string; + output: string; +} { const result = spawnSync(process.execPath, [CLI, ...args], { cwd: projectRoot, encoding: "utf8", env: { ...process.env, NO_COLOR: "1" }, }); + const stdout = result.stdout ?? ""; + const stderr = result.stderr ?? ""; return { status: result.status, - stdout: result.stdout ?? "", - stderr: result.stderr ?? "", + stdout, + stderr, + output: [stdout, stderr].filter(Boolean).join("\n"), }; } +function expectSuccess(result: ReturnType): void { + expect(result.status, result.output).toBe(0); +} + beforeAll(() => { projectRoot = mkdtempSync(join(tmpdir(), "mex-smoke-")); cpSync(FIXTURE_SRC, projectRoot, { recursive: true }); @@ -38,81 +50,81 @@ beforeAll(() => { }); afterAll(() => { - rmSync(projectRoot, { recursive: true, force: true }); + if (projectRoot) { + rmSync(projectRoot, { recursive: true, force: true }); + } }); describe("CLI smoke", () => { it("--version matches package.json", () => { - const { status, stdout } = runMex(["--version"]); - expect(status).toBe(0); - expect(stdout.trim()).toBe(pkg.version); + const result = runMex(["--version"]); + expectSuccess(result); + expect(result.stdout.trim()).toBe(pkg.version); }); it("commands lists top-level commands", () => { - const { status, stdout } = runMex(["commands"]); - expect(status).toBe(0); - expect(stdout).toContain("CLI Commands"); - expect(stdout).toContain("mex check"); + const result = runMex(["commands"]); + expectSuccess(result); + expect(result.stdout).toContain("CLI Commands"); + expect(result.stdout).toContain("mex check"); }); it("check --quiet exits successfully on fixture", () => { - const { status, stdout } = runMex(["check", "--quiet"]); - expect(status).toBe(0); - expect(stdout.length).toBeGreaterThan(0); + const result = runMex(["check", "--quiet"]); + expectSuccess(result); + expect(result.stdout.length).toBeGreaterThan(0); }); it("doctor prints health summary", () => { - const { status, stdout } = runMex(["doctor"]); - expect(status).toBe(0); - expect(stdout).toContain("mex doctor"); - expect(stdout).toContain("Drift"); + const result = runMex(["doctor"]); + expectSuccess(result); + expect(result.stdout).toContain("mex doctor"); + expect(result.stdout).toContain("Drift"); }); it("log and timeline round-trip", () => { const log = runMex(["log", "smoke test note", "--type", "note"]); - expect(log.status).toBe(0); + expectSuccess(log); const timeline = runMex(["timeline", "--limit", "1"]); - expect(timeline.status).toBe(0); + expectSuccess(timeline); expect(timeline.stdout).toContain("smoke test note"); }); it("heartbeat --json reports status", () => { - const { status, stdout } = runMex(["heartbeat", "--json"]); - expect(status).toBe(0); - expect(stdout).toContain("{"); + const result = runMex(["heartbeat", "--json"]); + expectSuccess(result); + expect(result.stdout).toContain("{"); }); it("completion bash emits script", () => { - const { status, stdout } = runMex(["completion", "bash"]); - expect(status).toBe(0); - expect(stdout).toContain("complete"); + const result = runMex(["completion", "bash"]); + expectSuccess(result); + expect(result.stdout).toContain("complete"); }); it("sync --dry-run runs without error", () => { - const { status } = runMex(["sync", "--dry-run"]); - expect(status).toBe(0); + expectSuccess(runMex(["sync", "--dry-run"])); }); it("setup --dry-run previews scaffold", () => { - const { status, stdout } = runMex(["setup", "--dry-run"]); - expect(status).toBe(0); - expect(stdout.length).toBeGreaterThan(0); + const result = runMex(["setup", "--dry-run"]); + expectSuccess(result); + expect(result.stdout.length).toBeGreaterThan(0); }); it("init --json emits scanner brief", () => { - const { status, stdout } = runMex(["init", "--json"]); - expect(status).toBe(0); - expect(stdout.trim().startsWith("{")).toBe(true); + const result = runMex(["init", "--json"]); + expectSuccess(result); + expect(result.stdout.trim().startsWith("{")).toBe(true); }); it("watch --uninstall is a no-op success", () => { - const { status } = runMex(["watch", "--uninstall"]); - expect(status).toBe(0); + expectSuccess(runMex(["watch", "--uninstall"])); }); it("pattern add creates a scaffold file", () => { - const { status, stdout } = runMex(["pattern", "add", "smoke-pattern"]); - expect(status).toBe(0); - expect(stdout).toContain("smoke-pattern.md"); + const result = runMex(["pattern", "add", "smoke-pattern"]); + expectSuccess(result); + expect(result.stdout).toContain("smoke-pattern.md"); }); }); From aceba8aa97bb8d361121f863d86379533359d7fa Mon Sep 17 00:00:00 2001 From: root Date: Sun, 7 Jun 2026 05:59:54 +0000 Subject: [PATCH 04/10] test(cli): assert default check output in smoke suite Cover the non-quiet check path explicitly so regressions in the standard drift report are caught alongside --quiet. --- test/cli-smoke.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/cli-smoke.test.ts b/test/cli-smoke.test.ts index 14a844e..9c3ca09 100644 --- a/test/cli-smoke.test.ts +++ b/test/cli-smoke.test.ts @@ -69,6 +69,12 @@ describe("CLI smoke", () => { expect(result.stdout).toContain("mex check"); }); + it("check prints drift summary on fixture", () => { + const result = runMex(["check"]); + expectSuccess(result); + expect(result.stdout).toContain("Drift score"); + }); + it("check --quiet exits successfully on fixture", () => { const result = runMex(["check", "--quiet"]); expectSuccess(result); From b4bd3577b070ead61684809e50931fd887712877 Mon Sep 17 00:00:00 2001 From: Array Fleet Date: Tue, 9 Jun 2026 10:23:35 +0000 Subject: [PATCH 05/10] test(cli): harden smoke harness against parallel dist rebuilds Ensure dist/cli.js exists before each smoke invocation so parallel vitest files (e.g. cli.test.ts beforeAll build) cannot delete the binary mid-suite and cause flaky MODULE_NOT_FOUND failures. --- test/cli-smoke.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/cli-smoke.test.ts b/test/cli-smoke.test.ts index 9c3ca09..cf68c1c 100644 --- a/test/cli-smoke.test.ts +++ b/test/cli-smoke.test.ts @@ -1,12 +1,13 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { execSync, spawnSync } from "node:child_process"; -import { cpSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { join, dirname } from "node:path"; import { tmpdir } from "node:os"; import { fileURLToPath } from "node:url"; const here = dirname(fileURLToPath(import.meta.url)); -const CLI = join(here, "..", "dist", "cli.js"); +const repoRoot = join(here, ".."); +const CLI = join(repoRoot, "dist", "cli.js"); const FIXTURE_SRC = join(here, "fixtures", "smoke-project"); const pkg = JSON.parse( readFileSync(join(here, "..", "package.json"), "utf8"), @@ -14,12 +15,19 @@ const pkg = JSON.parse( let projectRoot: string | undefined; +function ensureCliBuilt(): void { + if (!existsSync(CLI)) { + execSync("npm run build", { cwd: repoRoot, stdio: "pipe" }); + } +} + function runMex(args: string[]): { status: number | null; stdout: string; stderr: string; output: string; } { + ensureCliBuilt(); const result = spawnSync(process.execPath, [CLI, ...args], { cwd: projectRoot, encoding: "utf8", @@ -40,6 +48,7 @@ function expectSuccess(result: ReturnType): void { } beforeAll(() => { + execSync("npm run build", { cwd: repoRoot, stdio: "pipe" }); projectRoot = mkdtempSync(join(tmpdir(), "mex-smoke-")); cpSync(FIXTURE_SRC, projectRoot, { recursive: true }); execSync("git init -q", { cwd: projectRoot }); From 5c312a4ae49bc62cbea1c898054d4ac4ff8f3e1b Mon Sep 17 00:00:00 2001 From: Array Fleet Date: Tue, 9 Jun 2026 10:26:20 +0000 Subject: [PATCH 06/10] test(cli): extend smoke coverage for JSON check and shell completions Assert check --json returns a clean drift report on the fixture scaffold (context/ and patterns/ frontmatter included) and cover zsh/fish completion scripts alongside bash. --- test/cli-smoke.test.ts | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/test/cli-smoke.test.ts b/test/cli-smoke.test.ts index cf68c1c..ea24c12 100644 --- a/test/cli-smoke.test.ts +++ b/test/cli-smoke.test.ts @@ -90,6 +90,19 @@ describe("CLI smoke", () => { expect(result.stdout.length).toBeGreaterThan(0); }); + it("check --json reports clean drift on fixture scaffold", () => { + const result = runMex(["check", "--json"]); + expectSuccess(result); + const report = JSON.parse(result.stdout) as { + score: number; + issues: Array<{ code?: string; file?: string }>; + filesChecked: number; + }; + expect(report.score).toBe(100); + expect(report.issues).toEqual([]); + expect(report.filesChecked).toBeGreaterThanOrEqual(8); + }); + it("doctor prints health summary", () => { const result = runMex(["doctor"]); expectSuccess(result); @@ -111,10 +124,18 @@ describe("CLI smoke", () => { expect(result.stdout).toContain("{"); }); - it("completion bash emits script", () => { - const result = runMex(["completion", "bash"]); - expectSuccess(result); - expect(result.stdout).toContain("complete"); + it("completion emits scripts for bash, zsh, and fish", () => { + const bash = runMex(["completion", "bash"]); + expectSuccess(bash); + expect(bash.stdout).toContain("complete"); + + const zsh = runMex(["completion", "zsh"]); + expectSuccess(zsh); + expect(zsh.stdout).toContain("#compdef mex"); + + const fish = runMex(["completion", "fish"]); + expectSuccess(fish); + expect(fish.stdout).toContain("complete -c mex"); }); it("sync --dry-run runs without error", () => { From 9505b18231486778221e7073cc3a251d0f8e726d Mon Sep 17 00:00:00 2001 From: Array Fleet Date: Tue, 9 Jun 2026 10:52:20 +0000 Subject: [PATCH 07/10] fix(test): harden CLI smoke harness against missing dist/cli.js Assert dist/cli.js exists after npm run build in beforeAll and ensureCliBuilt so smoke tests fail fast with a clear error instead of opaque spawn failures. Removes accidental corrupted internal/exporter/mkdocs.go from a prior commit. --- test/cli-smoke.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/cli-smoke.test.ts b/test/cli-smoke.test.ts index ea24c12..80033b0 100644 --- a/test/cli-smoke.test.ts +++ b/test/cli-smoke.test.ts @@ -19,6 +19,9 @@ function ensureCliBuilt(): void { if (!existsSync(CLI)) { execSync("npm run build", { cwd: repoRoot, stdio: "pipe" }); } + if (!existsSync(CLI)) { + throw new Error(`CLI executable not found at ${CLI} after build`); + } } function runMex(args: string[]): { @@ -28,6 +31,9 @@ function runMex(args: string[]): { output: string; } { ensureCliBuilt(); + if (!existsSync(CLI)) { + throw new Error(`CLI executable not found at ${CLI}`); + } const result = spawnSync(process.execPath, [CLI, ...args], { cwd: projectRoot, encoding: "utf8", @@ -49,6 +55,9 @@ function expectSuccess(result: ReturnType): void { beforeAll(() => { execSync("npm run build", { cwd: repoRoot, stdio: "pipe" }); + if (!existsSync(CLI)) { + throw new Error(`CLI build failed: ${CLI} not found`); + } projectRoot = mkdtempSync(join(tmpdir(), "mex-smoke-")); cpSync(FIXTURE_SRC, projectRoot, { recursive: true }); execSync("git init -q", { cwd: projectRoot }); From 43d4ac1f8596222751c9ae3324c6bd35716235d0 Mon Sep 17 00:00:00 2001 From: Array Fleet Date: Tue, 9 Jun 2026 10:56:34 +0000 Subject: [PATCH 08/10] test(cli): verify dist/cli.js via vitest globalSetup before suite Run npm run build and assert the CLI artifact exists in a global setup hook so smoke and integration tests fail fast when the build step is skipped or broken, addressing peer-review feedback on build verification. --- test/global-setup.ts | 14 ++++++++++++++ vitest.config.ts | 1 + 2 files changed, 15 insertions(+) create mode 100644 test/global-setup.ts diff --git a/test/global-setup.ts b/test/global-setup.ts new file mode 100644 index 0000000..4e0a999 --- /dev/null +++ b/test/global-setup.ts @@ -0,0 +1,14 @@ +import { execSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const cli = join(repoRoot, "dist", "cli.js"); + +export default function setup(): void { + execSync("npm run build", { cwd: repoRoot, stdio: "pipe" }); + if (!existsSync(cli)) { + throw new Error(`CLI build failed: ${cli} not found before running tests`); + } +} diff --git a/vitest.config.ts b/vitest.config.ts index b26d4f1..15ad485 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { + globalSetup: ["./test/global-setup.ts"], // Tests must NEVER emit real telemetry to PostHog. The dev-repo guard only // catches commands run from inside this repo; tests spawn the built CLI in // temp dirs where that guard does not fire, so disable telemetry globally. From 03eb883c76801f9584977da8668d6fcf9ccf1ace Mon Sep 17 00:00:00 2001 From: Array Fleet Date: Tue, 9 Jun 2026 19:58:20 +0000 Subject: [PATCH 09/10] test(cli): probe CLI execution in vitest globalSetup Run --version after build so smoke tests fail fast with a clear error when runtime dependencies are missing, not only when dist/cli.js is absent. --- test/global-setup.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/global-setup.ts b/test/global-setup.ts index 4e0a999..6c9a832 100644 --- a/test/global-setup.ts +++ b/test/global-setup.ts @@ -1,4 +1,4 @@ -import { execSync } from "node:child_process"; +import { execSync, spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -11,4 +11,16 @@ export default function setup(): void { if (!existsSync(cli)) { throw new Error(`CLI build failed: ${cli} not found before running tests`); } + + const probe = spawnSync(process.execPath, [cli, "--version"], { + cwd: repoRoot, + encoding: "utf8", + env: { ...process.env, MEX_TELEMETRY: "0", NO_COLOR: "1" }, + }); + if (probe.status !== 0) { + const detail = [probe.stdout, probe.stderr].filter(Boolean).join("\n"); + throw new Error( + `CLI probe failed (run npm install if dependencies are missing): ${detail}`, + ); + } } From 99f80adf237434c6587f4340f42b8a1f87125429 Mon Sep 17 00:00:00 2001 From: Array Fleet Date: Tue, 9 Jun 2026 20:02:33 +0000 Subject: [PATCH 10/10] test(cli): disable telemetry in smoke subprocess env Set MEX_TELEMETRY=0 when spawning the CLI in smoke tests so temp-dir runs never emit telemetry even if vitest env inheritance changes. Co-authored-by: Cursor --- test/cli-smoke.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli-smoke.test.ts b/test/cli-smoke.test.ts index 80033b0..a938c5b 100644 --- a/test/cli-smoke.test.ts +++ b/test/cli-smoke.test.ts @@ -37,7 +37,7 @@ function runMex(args: string[]): { const result = spawnSync(process.execPath, [CLI, ...args], { cwd: projectRoot, encoding: "utf8", - env: { ...process.env, NO_COLOR: "1" }, + env: { ...process.env, MEX_TELEMETRY: "0", NO_COLOR: "1" }, }); const stdout = result.stdout ?? ""; const stderr = result.stderr ?? "";