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:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user