Skip to content
Open
175 changes: 175 additions & 0 deletions test/cli-smoke.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { execSync, spawnSync } from "node:child_process";
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 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"),
) as { version: string };

let projectRoot: string | undefined;

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[]): {
status: number | null;
stdout: string;
stderr: 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",
env: { ...process.env, MEX_TELEMETRY: "0", NO_COLOR: "1" },
});
const stdout = result.stdout ?? "";
const stderr = result.stderr ?? "";
return {
status: result.status,
stdout,
stderr,
output: [stdout, stderr].filter(Boolean).join("\n"),
};
}

function expectSuccess(result: ReturnType<typeof runMex>): void {
expect(result.status, result.output).toBe(0);
}

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 });
execSync("git add -A", { cwd: projectRoot });
execSync('git -c user.email=smoke@test -c user.name=smoke commit -q -m "init"', {
cwd: projectRoot,
});
});

afterAll(() => {
if (projectRoot) {
rmSync(projectRoot, { recursive: true, force: true });
}
});

describe("CLI smoke", () => {
it("--version matches package.json", () => {
const result = runMex(["--version"]);
expectSuccess(result);
expect(result.stdout.trim()).toBe(pkg.version);
});

it("commands lists top-level commands", () => {
const result = runMex(["commands"]);
expectSuccess(result);
expect(result.stdout).toContain("CLI Commands");
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);
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);
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"]);
expectSuccess(log);
const timeline = runMex(["timeline", "--limit", "1"]);
expectSuccess(timeline);
expect(timeline.stdout).toContain("smoke test note");
});

it("heartbeat --json reports status", () => {
const result = runMex(["heartbeat", "--json"]);
expectSuccess(result);
expect(result.stdout).toContain("{");
});

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", () => {
expectSuccess(runMex(["sync", "--dry-run"]));
});

it("setup --dry-run previews scaffold", () => {
const result = runMex(["setup", "--dry-run"]);
expectSuccess(result);
expect(result.stdout.length).toBeGreaterThan(0);
});

it("init --json emits scanner brief", () => {
const result = runMex(["init", "--json"]);
expectSuccess(result);
expect(result.stdout.trim().startsWith("{")).toBe(true);
});

it("watch --uninstall is a no-op success", () => {
expectSuccess(runMex(["watch", "--uninstall"]));
});

it("pattern add creates a scaffold file", () => {
const result = runMex(["pattern", "add", "smoke-pattern"]);
expectSuccess(result);
expect(result.stdout).toContain("smoke-pattern.md");
});
});
19 changes: 19 additions & 0 deletions test/fixtures/smoke-project/.mex/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions test/fixtures/smoke-project/.mex/ROUTER.md
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions test/fixtures/smoke-project/.mex/context/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: architecture
description: Smoke architecture context
last_updated: 2026-06-01
---

# Architecture

Single-file smoke fixture.
9 changes: 9 additions & 0 deletions test/fixtures/smoke-project/.mex/context/conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: conventions
description: Smoke conventions context
last_updated: 2026-06-01
---

# Conventions

Test-only conventions.
9 changes: 9 additions & 0 deletions test/fixtures/smoke-project/.mex/context/decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: decisions
description: Smoke decisions log
last_updated: 2026-06-01
---

# Decisions

None yet.
9 changes: 9 additions & 0 deletions test/fixtures/smoke-project/.mex/context/setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: setup
description: Smoke setup context
last_updated: 2026-06-01
---

# Setup

Run `npm test` from the mex repo.
9 changes: 9 additions & 0 deletions test/fixtures/smoke-project/.mex/context/stack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: stack
description: Smoke stack context
last_updated: 2026-06-01
---

# Stack

Node.js test fixture.
9 changes: 9 additions & 0 deletions test/fixtures/smoke-project/.mex/patterns/INDEX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
name: patterns-index
description: Pattern index for smoke tests
last_updated: 2026-06-01
---

# Patterns

No patterns in this fixture.
4 changes: 4 additions & 0 deletions test/fixtures/smoke-project/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "mex-smoke-fixture",
"private": true
}
1 change: 1 addition & 0 deletions test/fixtures/smoke-project/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Smoke fixture entry point for path drift checks.
26 changes: 26 additions & 0 deletions test/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { execSync, spawnSync } 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`);
}

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}`,
);
}
}
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading