fix(test-harness): restore promisify(exec) + unblock CLI introspection probes

The vitest child-process guard wrapped exec/execFile without preserving the
`[util.promisify.custom]` symbol, so awaited `execAsync` resolved to a raw
stdout string instead of `{stdout, stderr}`. That single regression cascaded
through ~60 "failing" tests across cli, core, engine, and dashboard whose
production code was actually correct. Also relax the AI-CLI blocklist for
cheap introspection (--version/--help/which …), give SIGTERM'd subprocesses a
brief grace period before being flagged as "left running", fix a few real
test-side bugs uncovered along the way (executor mock step transitions, iOS
last-resort keyboard path, mission SSE replay tests racing with the real AI
agent), and convert dashboard route tests' dynamic `await import("../server.js")`
to static imports so first-test timings drop from 2–5s to <200ms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-30 17:40:14 -07:00
parent 8e969bd72e
commit 62f54dab46
18 changed files with 238 additions and 60 deletions

View File

@@ -288,6 +288,9 @@ describe("useMobileKeyboard", () => {
});
it("treats focused input + viewport shrink as keyboard-open even when overlap is 0", async () => {
// iOS last-resort path: chromeOverlap = 0 (innerHeight tracks offsetTop+vv.height),
// gap < 16 (focused-fallback doesn't fire), and viewportShrink >= 16 from the
// baseline so the focused-input shrink heuristic is the only signal left.
const { listeners, mockVV } = setupMobileVisualViewport({
innerHeight: 844,
vvHeight: 844,
@@ -305,7 +308,17 @@ describe("useMobileKeyboard", () => {
input.focus();
Object.defineProperty(mockVV, "height", {
value: 826,
value: 824,
writable: true,
configurable: true,
});
Object.defineProperty(mockVV, "offsetTop", {
value: 5,
writable: true,
configurable: true,
});
Object.defineProperty(window, "innerHeight", {
value: 829,
writable: true,
configurable: true,
});
@@ -316,7 +329,7 @@ describe("useMobileKeyboard", () => {
await waitFor(() => {
expect(result.current.keyboardOverlap).toBe(0);
expect(result.current.viewportHeight).toBe(826);
expect(result.current.viewportHeight).toBe(824);
expect(result.current.keyboardOpen).toBe(true);
});

View File

@@ -2212,7 +2212,8 @@ describe("Mission API", () => {
it("replays buffered interview events when Last-Event-ID is provided", async () => {
const { app } = buildApp();
const sessionId = await createMissionInterviewSession("127.0.0.1", "Replay Mission", "/tmp/project");
const sessionId = "replay-test-session";
missionInterviewModule.__registerMissionInterviewSessionForTest(sessionId, "Replay Mission");
missionInterviewStreamManager.broadcast(sessionId, { type: "thinking", data: "first" });
missionInterviewStreamManager.broadcast(sessionId, { type: "thinking", data: "second" });
@@ -2239,7 +2240,8 @@ describe("Mission API", () => {
it("does not replay buffered interview events when Last-Event-ID is missing", async () => {
const { app } = buildApp();
const sessionId = await createMissionInterviewSession("127.0.0.1", "No Replay Mission", "/tmp/project");
const sessionId = "no-replay-test-session";
missionInterviewModule.__registerMissionInterviewSessionForTest(sessionId, "No Replay Mission");
missionInterviewStreamManager.broadcast(sessionId, { type: "thinking", data: "first" });
@@ -2261,7 +2263,8 @@ describe("Mission API", () => {
it("gracefully ignores invalid Last-Event-ID values for interview streams", async () => {
const { app } = buildApp();
const sessionId = await createMissionInterviewSession("127.0.0.1", "Invalid Replay Mission", "/tmp/project");
const sessionId = "invalid-replay-test-session";
missionInterviewModule.__registerMissionInterviewSessionForTest(sessionId, "Invalid Replay Mission");
missionInterviewStreamManager.broadcast(sessionId, { type: "thinking", data: "first" });

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import { get, request } from "../test-request.js";
import { createServer } from "../server.js";
// ── Mock @fusion/core for budget routes ─────────────────────────────────
@@ -68,9 +69,9 @@ function createMockBudgetStatus(overrides: Record<string, unknown> = {}) {
describe("Agent budget routes", () => {
let store: MockStore;
let app: ReturnType<typeof import("../server.js").createServer>;
let app: ReturnType<typeof createServer>;
beforeEach(async () => {
beforeEach(() => {
vi.clearAllMocks();
mockInit.mockResolvedValue(undefined);
mockGetAgent.mockResolvedValue({ id: "agent-001", state: "running" });
@@ -78,7 +79,6 @@ describe("Agent budget routes", () => {
mockResetBudgetUsage.mockResolvedValue(undefined);
store = new MockStore();
const { createServer } = await import("../server.js");
app = createServer(store as any);
});

View File

@@ -5,6 +5,7 @@ import { join } from "node:path";
import { tmpdir } from "node:os";
import { request } from "../test-request.js";
import { createServer } from "../server.js";
const mockInit = vi.fn().mockResolvedValue(undefined);
const mockListAgents = vi.fn().mockResolvedValue([]);
@@ -53,7 +54,7 @@ async function postExport(app: Parameters<typeof request>[0], body: unknown) {
describe("POST /api/agents/export", () => {
let store: MockStore;
let app: ReturnType<typeof import("../server.js").createServer>;
let app: ReturnType<typeof createServer>;
let testDir: string;
beforeEach(async () => {
@@ -91,7 +92,6 @@ describe("POST /api/agents/export", () => {
});
store = new MockStore();
const { createServer } = await import("../server.js");
app = createServer(store as any);
});

View File

@@ -4,6 +4,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { request } from "../test-request.js";
import { createServer } from "../server.js";
const mockInit = vi.fn().mockResolvedValue(undefined);
const mockListAgents = vi.fn().mockResolvedValue([]);
@@ -16,12 +17,21 @@ const mockParseSingleAgentManifest = vi.fn();
const mockPrepareAgentCompaniesImport = vi.fn();
// Use vi.hoisted to ensure mocks are available when vi.mock runs
const { mockFsAccess, mockFsMkdir, mockFsWriteFile, mockExecFile } = vi.hoisted(() => ({
mockFsAccess: vi.fn(),
mockFsMkdir: vi.fn(),
mockFsWriteFile: vi.fn(),
mockExecFile: vi.fn(),
}));
const { mockFsAccess, mockFsMkdir, mockFsWriteFile, mockExecFile, MockAgentCompaniesParseError } = vi.hoisted(() => {
class MockAgentCompaniesParseError extends Error {
constructor(message: string) {
super(message);
this.name = "AgentCompaniesParseError";
}
}
return {
mockFsAccess: vi.fn(),
mockFsMkdir: vi.fn(),
mockFsWriteFile: vi.fn(),
mockExecFile: vi.fn(),
MockAgentCompaniesParseError,
};
});
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>;
@@ -47,13 +57,6 @@ vi.mock("node:child_process", async (importOriginal) => {
};
});
class MockAgentCompaniesParseError extends Error {
constructor(message: string) {
super(message);
this.name = "AgentCompaniesParseError";
}
}
vi.mock("@fusion/core", () => {
return {
AgentStore: class MockAgentStore {
@@ -107,7 +110,7 @@ const originalFetch = globalThis.fetch;
describe("POST /api/agents/import", () => {
let store: MockStore;
let app: ReturnType<typeof import("../server.js").createServer>;
let app: ReturnType<typeof createServer>;
let testDir: string;
beforeEach(async () => {
@@ -175,7 +178,6 @@ describe("POST /api/agents/import", () => {
});
store = new MockStore();
const { createServer } = await import("../server.js");
app = createServer(store as any);
});
@@ -869,7 +871,7 @@ describe("POST /api/agents/import", () => {
describe("GET /api/agents/companies", () => {
let store: MockStore;
let app: ReturnType<typeof import("../server.js").createServer>;
let app: ReturnType<typeof createServer>;
const originalFetch = globalThis.fetch;
beforeEach(async () => {
@@ -882,7 +884,6 @@ describe("GET /api/agents/companies", () => {
mockInit.mockResolvedValue(undefined);
store = new MockStore();
const { createServer } = await import("../server.js");
app = createServer(store as any);
});

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import { request } from "../test-request.js";
import { createServer } from "../server.js";
const mockInit = vi.fn().mockResolvedValue(undefined);
const mockGetAgent = vi.fn();
@@ -49,7 +50,7 @@ class MockStore extends EventEmitter {
describe("Agent API key routes", () => {
let store: MockStore;
let app: ReturnType<typeof import("../server.js").createServer>;
let app: ReturnType<typeof createServer>;
beforeEach(async () => {
vi.clearAllMocks();
@@ -57,7 +58,6 @@ describe("Agent API key routes", () => {
mockListAgents.mockResolvedValue([]);
store = new MockStore();
const { createServer } = await import("../server.js");
app = createServer(store as any);
});

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import { request } from "../test-request.js";
import { createServer } from "../server.js";
// ── Mock @fusion/core for agent ratings ─────────────────────────────────
@@ -83,14 +84,13 @@ function createMockSummary(overrides: Record<string, unknown> = {}) {
describe("Agent ratings routes", () => {
let store: MockStore;
let app: ReturnType<typeof import("../server.js").createServer>;
let app: ReturnType<typeof createServer>;
beforeEach(async () => {
vi.clearAllMocks();
mockInit.mockResolvedValue(undefined);
store = new MockStore();
const { createServer } = await import("../server.js");
app = createServer(store as any);
});

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import { request } from "../test-request.js";
import { createServer } from "../server.js";
const mockInit = vi.fn().mockResolvedValue(undefined);
const mockGetAgent = vi.fn();
@@ -83,7 +84,7 @@ function createMockRevision(overrides: Record<string, unknown> = {}) {
describe("Agent config revision routes", () => {
let store: MockStore;
let app: ReturnType<typeof import("../server.js").createServer>;
let app: ReturnType<typeof createServer>;
beforeEach(async () => {
vi.clearAllMocks();
@@ -91,7 +92,6 @@ describe("Agent config revision routes", () => {
mockListAgents.mockResolvedValue([]);
store = new MockStore();
const { createServer } = await import("../server.js");
app = createServer(store as any);
});

View File

@@ -5,6 +5,7 @@ import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { request } from "../test-request.js";
import { createServer } from "../server.js";
type AgentRecord = {
id: string;
@@ -84,7 +85,7 @@ function createAgent(overrides: Partial<AgentRecord> = {}): AgentRecord {
describe("Agent soul/memory routes", () => {
let store: MockStore;
let app: ReturnType<typeof import("../server.js").createServer>;
let app: ReturnType<typeof createServer>;
let agents: Map<string, AgentRecord>;
let tempDir: string;
@@ -123,7 +124,6 @@ describe("Agent soul/memory routes", () => {
await mkdir(join(tempDir, ".fusion"), { recursive: true });
store = new MockStore(tempDir);
const { createServer } = await import("../server.js");
app = createServer(store as any);
});

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import { request, get } from "../test-request.js";
import { createServer } from "../server.js";
import { resetRuntimeLogSink, setRuntimeLogSink, type RuntimeLogContext } from "../runtime-logger.js";
// Mock node:fs for auth.json reading
@@ -157,7 +158,7 @@ interface RuntimeEvent {
describe("Node settings sync routes", () => {
let store: MockStore;
let app: ReturnType<typeof import("../server.js").createServer>;
let app: ReturnType<typeof createServer>;
let mockFetch: ReturnType<typeof vi.fn>;
let runtimeEvents: RuntimeEvent[];
@@ -185,7 +186,6 @@ describe("Node settings sync routes", () => {
});
store = new MockStore();
const { createServer } = await import("../server.js");
app = createServer(store as any);
});

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import { request, get } from "../test-request.js";
import { createServer } from "../server.js";
// ── Mock @fusion/core for node routes ─────────────────────────────────
@@ -85,7 +86,7 @@ function createMockNode(overrides: Record<string, unknown> = {}) {
describe("Node routes", () => {
let store: MockStore;
let app: ReturnType<typeof import("../server.js").createServer>;
let app: ReturnType<typeof createServer>;
beforeEach(async () => {
vi.clearAllMocks();
@@ -101,7 +102,6 @@ describe("Node routes", () => {
mockGetDiscoveryConfig.mockReturnValue(null);
store = new MockStore();
const { createServer } = await import("../server.js");
app = createServer(store as any);
});

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import { request } from "../test-request.js";
import { createServer } from "../server.js";
const mockInit = vi.fn().mockResolvedValue(undefined);
const mockGetAgent = vi.fn();
@@ -62,7 +63,7 @@ function createMockAgent(id: string, name: string, reportsTo?: string) {
describe("Agent org chart routes", () => {
let store: MockStore;
let app: ReturnType<typeof import("../server.js").createServer>;
let app: ReturnType<typeof createServer>;
beforeEach(async () => {
vi.clearAllMocks();
@@ -70,7 +71,6 @@ describe("Agent org chart routes", () => {
mockListAgents.mockResolvedValue([]);
store = new MockStore();
const { createServer } = await import("../server.js");
app = createServer(store as any);
});

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import { get } from "../test-request.js";
import { createServer } from "../server.js";
// ── Mock @fusion/core for project routes ─────────────────────────────────
@@ -109,10 +110,9 @@ describe("GET /api/projects/across-nodes", () => {
let store: MockStore;
let app: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse) => void;
beforeEach(async () => {
beforeEach(() => {
vi.clearAllMocks();
store = new MockStore();
const { createServer } = await import("../server.js");
app = createServer(store as unknown as Parameters<typeof createServer>[0] extends { store: infer S } ? S : never);
});

View File

@@ -1263,6 +1263,25 @@ export function cleanupMissionInterviewSession(sessionId: string): void {
/**
* Reset all mission interview state. Used for testing only.
*/
/**
* Test-only: register a stub session so SSE/buffer tests can drive the stream
* manager directly without spinning up the real AI agent (which is
* deliberately blocked in the vitest harness).
*/
export function __registerMissionInterviewSessionForTest(sessionId: string, missionTitle = "Test Mission"): void {
sessions.set(sessionId, {
id: sessionId,
ip: "127.0.0.1",
missionId: "",
missionTitle,
history: [],
thinkingOutput: "",
lastGeneratedThinking: "",
createdAt: new Date(),
updatedAt: new Date(),
});
}
export function __resetMissionInterviewState(): void {
for (const [id] of sessions) {
cleanupInMemoryMissionSession(id);