test: centralize @clack/prompts mock in test-helpers.ts (#2090)

* test: centralize @clack/prompts mock in test-helpers.ts

Adds mockClackPrompts() factory to test-helpers.ts, eliminating ~15-line
duplicate mock.module blocks from 19 test files. When @clack/prompts adds
a new export, only one file needs updating instead of 19.

Fixes #2080

Agent: test-engineer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* style: fix Biome formatting after merge with main

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-01 20:27:24 -08:00 committed by GitHub
parent 65b872afa3
commit aa7584096d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 225 additions and 528 deletions

View file

@ -1,4 +1,5 @@
import { describe, it, expect, beforeEach, mock } from "bun:test";
import { describe, it, expect, beforeEach } from "bun:test";
import { mockClackPrompts } from "./test-helpers";
import type { Manifest } from "../manifest";
/**
@ -14,32 +15,7 @@ import type { Manifest } from "../manifest";
* 4. No match at all: just the listCmd hint (existing)
*/
// ── Mock @clack/prompts ─────────────────────────────────────────────────────
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mock(() => {}),
stop: mock(() => {}),
message: mock(() => {}),
}),
log: {
step: mock(() => {}),
info: mockLogInfo,
warn: mock(() => {}),
error: mockLogError,
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const { logError: mockLogError, logInfo: mockLogInfo } = mockClackPrompts();
// Import after mocking
const { checkEntity } = await import("../commands.js");

View file

@ -1,7 +1,8 @@
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { mockClackPrompts } from "./test-helpers";
import type { SpawnRecord } from "../history.js";
import { clearHistory, loadHistory, saveSpawnRecord, filterHistory, getHistoryPath } from "../history.js";
@ -282,31 +283,7 @@ describe("clearHistory", () => {
// ── cmdListClear via mock.module ─────────────────────────────────────────────
const mockLogInfo = mock(() => {});
const mockLogSuccess = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mock(() => {}),
stop: mock(() => {}),
message: mock(() => {}),
}),
log: {
step: mock(() => {}),
info: mockLogInfo,
error: mock(() => {}),
warn: mock(() => {}),
success: mockLogSuccess,
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
confirm: mock(() => Promise.resolve(true)),
}));
const { logInfo: mockLogInfo, logSuccess: mockLogSuccess } = mockClackPrompts();
// Import after mock setup
const { cmdListClear } = await import("../commands.js");

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import { loadManifest } from "../manifest";
import { isString } from "@openrouter/spawn-shared";
@ -24,36 +24,19 @@ let selectCallIndex = 0;
let selectReturnValues: any[] = [];
let isCancelValues: Set<any> = new Set();
// Mock @clack/prompts
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockLogWarn = mock(() => {});
const mockIntro = mock(() => {});
const mockOutro = mock(() => {});
const mockCancel = mock(() => {});
const mockConfirm = mock(async () => true);
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
const mockSpinnerMessage = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mockSpinnerMessage,
}),
log: {
step: mockLogStep,
info: mockLogInfo,
error: mockLogError,
warn: mockLogWarn,
},
const {
logError: mockLogError,
logInfo: mockLogInfo,
logStep: mockLogStep,
logWarn: mockLogWarn,
intro: mockIntro,
outro: mockOutro,
cancel: mockCancel,
confirm: mockConfirm,
text: mock(async () => undefined),
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
spinnerMessage: mockSpinnerMessage,
} = mockClackPrompts({
autocomplete: mock(async () => {
const value = selectReturnValues[selectCallIndex] ?? "claude";
selectCallIndex++;
@ -64,8 +47,8 @@ mock.module("@clack/prompts", () => ({
selectCallIndex++;
return value;
}),
isCancel: (value: any) => isCancelValues.has(value),
}));
isCancel: (value: unknown) => isCancelValues.has(value),
});
// Import commands after mock setup
const { cmdInteractive } = await import("../commands.js");

View file

@ -2,7 +2,7 @@ import type { spyOn } from "bun:test";
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
import type { Manifest } from "../manifest";
import { loadManifest } from "../manifest";
import { createConsoleMocks, restoreMocks } from "./test-helpers";
import { createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
/**
* Tests for cmdMatrix, cmdAgents, and cmdClouds listing command output.
@ -130,32 +130,7 @@ const multiTypeManifest: Manifest = {
},
};
// ── Mock @clack/prompts ─────────────────────────────────────────────────────
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mock(() => {}),
}),
log: {
step: mock(() => {}),
info: mock(() => {}),
warn: mock(() => {}),
error: mock(() => {}),
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const { spinnerStart: mockSpinnerStart, spinnerStop: mockSpinnerStop } = mockClackPrompts();
const { cmdMatrix, cmdAgents, cmdClouds, getTerminalWidth } = await import("../commands.js");

View file

@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:te
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import type { SpawnRecord } from "../history";
/**
@ -20,33 +20,12 @@ import type { SpawnRecord } from "../history";
const mockManifest = createMockManifest();
// Mock @clack/prompts
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mock(() => {}),
}),
log: {
step: mockLogStep,
info: mockLogInfo,
error: mock(() => {}),
warn: mock(() => {}),
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const {
logInfo: mockLogInfo,
logStep: mockLogStep,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
} = mockClackPrompts();
// Import after mock setup
const { cmdLast, buildRecordLabel, buildRecordSubtitle } = await import("../commands.js");

View file

@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:te
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import type { SpawnRecord } from "../history";
/**
@ -27,35 +27,14 @@ import type { SpawnRecord } from "../history";
const mockManifest = createMockManifest();
// Mock @clack/prompts
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockLogSuccess = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mock(() => {}),
}),
log: {
step: mockLogStep,
info: mockLogInfo,
error: mockLogError,
warn: mock(() => {}),
success: mockLogSuccess,
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const {
logError: mockLogError,
logInfo: mockLogInfo,
logStep: mockLogStep,
logSuccess: mockLogSuccess,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
} = mockClackPrompts();
// Import after mock setup
const { cmdList, resolveDisplayName } = await import("../commands.js");

View file

@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:te
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import { loadManifest } from "../manifest";
import { isString } from "@openrouter/spawn-shared";
@ -21,40 +21,20 @@ const mockManifest = createMockManifest();
// ── Clack mock refs ──────────────────────────────────────────────────────────
const mockLogWarn = mock(() => {});
const mockLogStep = mock(() => {});
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
const mockSpinnerMessage = mock(() => {});
// select returns "rerun" to exercise the "Spawn a new VM" path
const mockSelect = mock(async () => "rerun");
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mockSpinnerMessage,
}),
log: {
step: mockLogStep,
info: mockLogInfo,
warn: mockLogWarn,
error: mockLogError,
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
const {
logWarn: mockLogWarn,
logStep: mockLogStep,
logError: mockLogError,
logInfo: mockLogInfo,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
spinnerMessage: mockSpinnerMessage,
} = mockClackPrompts({
select: mockSelect,
autocomplete: mock(async () => "claude"),
// First call: returns SPAWN_NAME value (set by --name).
// Second call (after rerun clears SPAWN_NAME): returns undefined so no duplicate re-check.
text: mock(async () => undefined),
isCancel: () => false,
}));
});
const { cmdRun } = await import("../commands.js");

View file

@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:te
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import { loadManifest } from "../manifest";
import { isString } from "@openrouter/spawn-shared";
@ -29,36 +29,14 @@ import { isString } from "@openrouter/spawn-shared";
const mockManifest = createMockManifest();
// ── Mock clack/prompts ───────────────────────────────────────────────────────
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
const mockSpinnerMessage = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mockSpinnerMessage,
}),
log: {
step: mockLogStep,
info: mockLogInfo,
warn: mock(() => {}),
error: mockLogError,
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const {
logError: mockLogError,
logInfo: mockLogInfo,
logStep: mockLogStep,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
spinnerMessage: mockSpinnerMessage,
} = mockClackPrompts();
const { cmdRun } = await import("../commands.js");

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import { loadManifest } from "../manifest";
/**
@ -48,34 +48,14 @@ const manifestWithCloudNotes = {
},
};
// Mock @clack/prompts
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockLogWarn = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mock(() => {}),
}),
log: {
step: mockLogStep,
info: mockLogInfo,
error: mockLogError,
warn: mockLogWarn,
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const {
logError: mockLogError,
logInfo: mockLogInfo,
logStep: mockLogStep,
logWarn: mockLogWarn,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
} = mockClackPrompts();
// Import commands after mock setup
const { cmdCloudInfo } = await import("../commands.js");

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import { loadManifest } from "../manifest";
/**
@ -77,35 +77,14 @@ const manyCloudManifest = {
},
};
// Mock @clack/prompts
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockLogWarn = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mock(() => {}),
}),
log: {
step: mockLogStep,
info: mockLogInfo,
error: mockLogError,
warn: mockLogWarn,
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const {
logError: mockLogError,
logInfo: mockLogInfo,
logStep: mockLogStep,
logWarn: mockLogWarn,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
} = mockClackPrompts();
// Import commands after mock setup
const { cmdAgentInfo, cmdHelp } = await import("../commands.js");

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import { loadManifest } from "../manifest";
import { isString } from "@openrouter/spawn-shared";
@ -18,34 +18,14 @@ import { isString } from "@openrouter/spawn-shared";
const mockManifest = createMockManifest();
// Mock @clack/prompts to prevent TTY output and capture error/info messages
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockLogWarn = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mock(() => {}),
}),
log: {
step: mockLogStep,
info: mockLogInfo,
error: mockLogError,
warn: mockLogWarn,
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const {
logError: mockLogError,
logInfo: mockLogInfo,
logStep: mockLogStep,
logWarn: mockLogWarn,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
} = mockClackPrompts();
// Import commands after @clack/prompts mock is set up
const { cmdRun, cmdAgentInfo } = await import("../commands.js");

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import { loadManifest } from "../manifest";
/**
@ -101,35 +101,14 @@ const manifestWithDistinctNames = {
},
};
// Mock @clack/prompts
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockLogWarn = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mock(() => {}),
}),
log: {
step: mockLogStep,
info: mockLogInfo,
error: mockLogError,
warn: mockLogWarn,
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const {
logError: mockLogError,
logInfo: mockLogInfo,
logStep: mockLogStep,
logWarn: mockLogWarn,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
} = mockClackPrompts();
// Import commands after mock setup
const { cmdRun, cmdAgentInfo, cmdCloudInfo, findClosestMatch } = await import("../commands.js");

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import { loadManifest } from "../manifest";
import { isString } from "@openrouter/spawn-shared";
@ -124,35 +124,14 @@ const noCloudManifest = {
},
};
// Mock @clack/prompts
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockLogWarn = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mock(() => {}),
}),
log: {
step: mockLogStep,
info: mockLogInfo,
error: mockLogError,
warn: mockLogWarn,
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const {
logError: mockLogError,
logInfo: mockLogInfo,
logStep: mockLogStep,
logWarn: mockLogWarn,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
} = mockClackPrompts();
// Import commands after mock setup
const { cmdRun } = await import("../commands.js");

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import { loadManifest } from "../manifest";
import { isString } from "@openrouter/spawn-shared";
@ -21,35 +21,14 @@ import { isString } from "@openrouter/spawn-shared";
* - Edge case: resolution to a key that then fails validation
*/
// Mock @clack/prompts
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockLogWarn = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mock(() => {}),
}),
log: {
step: mockLogStep,
info: mockLogInfo,
error: mockLogError,
warn: mockLogWarn,
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const {
logError: mockLogError,
logInfo: mockLogInfo,
logStep: mockLogStep,
logWarn: mockLogWarn,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
} = mockClackPrompts();
// Import commands after mock setup
const { cmdRun } = await import("../commands.js");

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import { loadManifest } from "../manifest";
import { isString } from "@openrouter/spawn-shared";
import pkg from "../../package.json" with { type: "json" };
@ -21,34 +21,14 @@ const VERSION = pkg.version;
const mockManifest = createMockManifest();
// Mock @clack/prompts
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
const mockSpinnerMessage = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mockSpinnerMessage,
}),
log: {
step: mockLogStep,
info: mockLogInfo,
warn: mock(() => {}),
error: mockLogError,
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const {
logError: mockLogError,
logInfo: mockLogInfo,
logStep: mockLogStep,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
spinnerMessage: mockSpinnerMessage,
} = mockClackPrompts();
// Mock node:child_process to prevent real subprocess calls in tests:
// - execSync: used by performUpdate() to run curl|bash install — without this mock,

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { createMockManifest, createConsoleMocks, restoreMocks, mockClackPrompts } from "./test-helpers";
import { loadManifest } from "../manifest";
import { isString } from "@openrouter/spawn-shared";
@ -22,36 +22,15 @@ import { isString } from "@openrouter/spawn-shared";
const mockManifest = createMockManifest();
// Mock @clack/prompts
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockLogWarn = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
const mockSpinnerMessage = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mockSpinnerMessage,
}),
log: {
step: mockLogStep,
info: mockLogInfo,
error: mockLogError,
warn: mockLogWarn,
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
const {
logError: mockLogError,
logInfo: mockLogInfo,
logStep: mockLogStep,
logWarn: mockLogWarn,
spinnerStart: mockSpinnerStart,
spinnerStop: mockSpinnerStop,
spinnerMessage: mockSpinnerMessage,
} = mockClackPrompts();
// Import after mock setup
const { cmdRun } = await import("../commands.js");

View file

@ -1,30 +1,17 @@
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
import { mockClackPrompts } from "./test-helpers";
import { preflightCredentialCheck } from "../commands";
import type { Manifest } from "../manifest";
// Mock @clack/prompts
const mockLog = {
warn: mock(() => {}),
info: mock(() => {}),
};
const mockConfirm = mock(() => Promise.resolve(true));
const mockIsCancel = mock(() => false);
mock.module("@clack/prompts", () => ({
log: mockLog,
confirm: mockConfirm,
const clackMocks = mockClackPrompts({
isCancel: mockIsCancel,
// Stubs for other imports commands.ts might use
intro: mock(() => {}),
outro: mock(() => {}),
select: mock(() => Promise.resolve("")),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
spinner: mock(() => ({
start: mock(() => {}),
stop: mock(() => {}),
message: mock(() => {}),
})),
}));
});
const mockLog = {
warn: clackMocks.logWarn,
info: clackMocks.logInfo,
};
const mockConfirm = clackMocks.confirm;
function makeManifest(cloudAuth: string): Manifest {
const m: Manifest = {

View file

@ -1,4 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { mockClackPrompts } from "./test-helpers";
import type { Manifest } from "../manifest";
/**
@ -123,30 +124,9 @@ const mockExit = spyOn(process, "exit").mockImplementation(() => {
throw new Error("process.exit called");
});
const mockLog = {
step: mock(() => {}),
info: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
success: mock(() => {}),
};
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mock(() => {}),
stop: mock(() => {}),
message: mock(() => {}),
}),
log: mockLog,
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
mockClackPrompts({
select: mock(() => Promise.resolve("hetzner")),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
confirm: mock(() => Promise.resolve(true)),
isCancel: () => false,
}));
});
// Import after mocks are set up
const {

View file

@ -8,26 +8,12 @@
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
import { join } from "node:path";
import { mockClackPrompts } from "./test-helpers";
// ── Mock @clack/prompts ─────────────────────────────────────────────────────
mock.module("@clack/prompts", () => ({
multiselect: mock(() => Promise.resolve([])),
isCancel: () => false,
log: {
info: mock(() => {}),
warn: mock(() => {}),
error: mock(() => {}),
step: mock(() => {}),
message: mock(() => {}),
},
spinner: () => ({
start: mock(() => {}),
stop: mock(() => {}),
}),
mockClackPrompts({
select: mock(() => Promise.resolve("")),
text: mock(() => Promise.resolve("")),
}));
});
// ── Import after @clack/prompts mock ────────────────────────────────────────

View file

@ -87,6 +87,88 @@ export function restoreMocks(
});
}
// ── @clack/prompts Mock ──────────────────────────────────────────────────────
export interface ClackPromptsMock {
logStep: ReturnType<typeof mock>;
logInfo: ReturnType<typeof mock>;
logError: ReturnType<typeof mock>;
logWarn: ReturnType<typeof mock>;
logSuccess: ReturnType<typeof mock>;
logMessage: ReturnType<typeof mock>;
spinnerStart: ReturnType<typeof mock>;
spinnerStop: ReturnType<typeof mock>;
spinnerMessage: ReturnType<typeof mock>;
intro: ReturnType<typeof mock>;
outro: ReturnType<typeof mock>;
cancel: ReturnType<typeof mock>;
select: ReturnType<typeof mock>;
autocomplete: ReturnType<typeof mock>;
text: ReturnType<typeof mock>;
confirm: ReturnType<typeof mock>;
multiselect: ReturnType<typeof mock>;
isCancel: (...args: unknown[]) => boolean;
}
/**
* Creates a centralized @clack/prompts mock and registers it via mock.module().
*
* Returns an object of individual mock refs that tests can use for assertions.
* Pass `overrides` to customize specific functions (e.g., custom `select` behavior).
*
* MUST be called at module top level (before dynamic imports of modules that use @clack/prompts).
*/
export function mockClackPrompts(overrides?: Partial<ClackPromptsMock>): ClackPromptsMock {
const mocks: ClackPromptsMock = {
logStep: mock(() => {}),
logInfo: mock(() => {}),
logError: mock(() => {}),
logWarn: mock(() => {}),
logSuccess: mock(() => {}),
logMessage: mock(() => {}),
spinnerStart: mock(() => {}),
spinnerStop: mock(() => {}),
spinnerMessage: mock(() => {}),
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
confirm: mock(async () => true),
multiselect: mock(() => Promise.resolve([])),
isCancel: () => false,
...overrides,
};
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mocks.spinnerStart,
stop: mocks.spinnerStop,
message: mocks.spinnerMessage,
}),
log: {
step: mocks.logStep,
info: mocks.logInfo,
error: mocks.logError,
warn: mocks.logWarn,
success: mocks.logSuccess,
message: mocks.logMessage,
},
intro: mocks.intro,
outro: mocks.outro,
cancel: mocks.cancel,
select: mocks.select,
autocomplete: mocks.autocomplete,
text: mocks.text,
confirm: mocks.confirm,
multiselect: mocks.multiselect,
isCancel: mocks.isCancel,
}));
return mocks;
}
// ── Fetch Mocks ────────────────────────────────────────────────────────────────
export function mockSuccessfulFetch(data: any) {