import { vi } from "vitest"; // Mock external dependencies vi.mock("../pi.js", () => ({ createFnAgent: vi.fn(), describeModel: vi.fn(() => "mock-provider/mock-model"), promptWithFallback: vi.fn(async (session, prompt, options) => { if (options === undefined) { await session.prompt(prompt); } else { await session.prompt(prompt, options); } }), compactSessionContext: vi.fn(), })); vi.mock("node:child_process", async () => { const { promisify } = await import("node:util"); const { EventEmitter } = await import("node:events"); const execSyncFn = vi.fn(); const spawnFn = vi.fn((cmd: string, opts?: any) => { const child = new EventEmitter() as any; child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.pid = 12345; child.exitCode = null; child.signalCode = null; child.kill = vi.fn(); queueMicrotask(() => { try { const out = execSyncFn(cmd, opts); const stdout = out === undefined ? "" : out.toString(); if (stdout) child.stdout.emit("data", Buffer.from(stdout)); child.exitCode = 0; child.emit("close", 0, null); } catch (err) { const error = err as { stdout?: string; stderr?: string; status?: number; code?: number }; const stdout = error?.stdout?.toString?.() ?? ""; const stderr = error?.stderr?.toString?.() ?? ""; if (stdout) child.stdout.emit("data", Buffer.from(stdout)); if (stderr) child.stderr.emit("data", Buffer.from(stderr)); child.exitCode = error.status ?? error.code ?? 1; child.emit("close", child.exitCode, null); } }); return child; }); const execFn: any = vi.fn((cmd: any, opts: any, cb: any) => { const callback = typeof opts === "function" ? opts : cb; try { const out = execSyncFn(cmd, { stdio: ["pipe", "pipe", "pipe"] }); const stdout = out === undefined ? "" : out.toString(); if (typeof callback === "function") callback(null, stdout, ""); } catch (err: any) { if (typeof callback === "function") { callback(err, err?.stdout?.toString?.() ?? "", err?.stderr?.toString?.() ?? ""); } } }); execFn[promisify.custom] = (cmd: any, opts?: any) => new Promise((resolve, reject) => { execFn(cmd, opts, (err: any, stdout: any, stderr: any) => { if (err) { err.stdout = stdout; err.stderr = stderr; reject(err); } else { resolve({ stdout, stderr }); } }); }); const execFileFn: any = vi.fn((file: any, args: any, opts: any, cb: any) => { const callback = typeof opts === "function" ? opts : cb; const options = typeof opts === "function" ? {} : opts; const cmd = [file, ...(Array.isArray(args) ? args : [])].join(" "); try { const out = execSyncFn(cmd, { stdio: ["pipe", "pipe", "pipe"], ...options }); const stdout = out === undefined ? "" : out.toString(); if (typeof callback === "function") callback(null, stdout, ""); } catch (err: any) { if (typeof callback === "function") { callback(err, err?.stdout?.toString?.() ?? "", err?.stderr?.toString?.() ?? ""); } } }); execFileFn[promisify.custom] = (file: any, args?: any, opts?: any) => new Promise((resolve, reject) => { execFileFn(file, args, opts, (err: any, stdout: any, stderr: any) => { if (err) { err.stdout = stdout; err.stderr = stderr; reject(err); } else { resolve({ stdout, stderr }); } }); }); return { execSync: execSyncFn, exec: execFn, execFile: execFileFn, spawn: spawnFn }; }); vi.mock("node:fs", () => ({ existsSync: vi.fn().mockReturnValue(true), readFileSync: vi.fn(), })); vi.mock("../rate-limit-retry.js", () => ({ withRateLimitRetry: (fn: () => Promise) => fn(), })); vi.mock("../context-limit-detector.js", () => ({ isContextLimitError: vi.fn(), })); import { aiMergeTask, pushToRemoteAfterMerge, findWorktreeUser, detectResolvableConflicts, autoResolveFile, resolveConflicts, classifyConflict, getConflictedFiles, isTrivialWhitespaceConflict, resolveWithOurs, resolveWithTheirs, resolveTrivialWhitespace, LOCKFILE_PATTERNS, GENERATED_PATTERNS, parseDiffStat, extractFileScope, validateDiffScope, assertSquashOverlapsFileScope, FileScopeViolationError, shouldSyncDependenciesForMerge, summarizeVerificationOutput, inferDefaultTestCommand, resolveTaskDiffBaseRef, commitOrAmendMergeWithFixes, MergeAbortedError, buildSourceIssueRef, buildMergePrompt, type ConflictCategory, } from "../merger.js"; import { mergerLog } from "../logger.js"; import { createFnAgent } from "../pi.js"; import { execSync, exec } from "node:child_process"; import * as core from "@fusion/core"; import { type TaskStore, type Task, DEFAULT_SETTINGS } from "@fusion/core"; export { aiMergeTask, pushToRemoteAfterMerge, findWorktreeUser, detectResolvableConflicts, autoResolveFile, resolveConflicts, classifyConflict, getConflictedFiles, isTrivialWhitespaceConflict, resolveWithOurs, resolveWithTheirs, resolveTrivialWhitespace, LOCKFILE_PATTERNS, GENERATED_PATTERNS, parseDiffStat, extractFileScope, validateDiffScope, assertSquashOverlapsFileScope, FileScopeViolationError, shouldSyncDependenciesForMerge, summarizeVerificationOutput, inferDefaultTestCommand, resolveTaskDiffBaseRef, commitOrAmendMergeWithFixes, MergeAbortedError, buildSourceIssueRef, buildMergePrompt, mergerLog, core, }; export type { ConflictCategory, Task }; export const mockedCreateFnAgent = vi.mocked(createFnAgent); export const mockedExecSync = vi.mocked(execSync); export const mockedExec = vi.mocked(exec); const { existsSync: mockedExistsSyncRaw, readFileSync: mockedReadFileSyncRaw } = await import("node:fs"); export const mockedExistsSync = vi.mocked(mockedExistsSyncRaw); export const mockedReadFileSync = vi.mocked(mockedReadFileSyncRaw); export function createMockStore(taskOverrides: Partial = {}, allTasks: Task[] = []) { const baseTask: Task = { id: "FN-050", title: "Test task", description: "Test", column: "in-review", dependencies: [], worktree: "/tmp/root/.worktrees/KB-050", steps: [], currentStep: 0, log: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), ...taskOverrides, }; return { getTask: vi.fn().mockResolvedValue({ ...baseTask, prompt: "# test" }), listTasks: vi.fn().mockResolvedValue(allTasks), updateTask: vi.fn().mockResolvedValue(baseTask), moveTask: vi.fn().mockResolvedValue(baseTask), logEntry: vi.fn().mockResolvedValue(undefined), appendAgentLog: vi.fn().mockResolvedValue(undefined), updateSettings: vi.fn().mockResolvedValue({}), getSettings: vi.fn().mockResolvedValue({ ...DEFAULT_SETTINGS }), getActiveMergingTask: vi.fn().mockReturnValue(null), parseFileScopeFromPrompt: vi.fn().mockResolvedValue([]), emit: vi.fn(), on: vi.fn(), clearStaleExecutionStartBranchReferences: vi.fn().mockReturnValue([]), getVerificationCacheHit: vi.fn().mockReturnValue(null), recordVerificationCachePass: vi.fn(), } as unknown as TaskStore; } export function setupHappyPathExecSync() { mockedExecSync.mockImplementation((cmd: any) => { const cmdStr = String(cmd); if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123"); if (cmdStr === "git rev-parse HEAD" || cmdStr.startsWith("git rev-parse HEAD ")) return "mergedcommit123"; if (cmdStr.includes("git log")) return "- feat: something" as any; if (cmdStr.includes("merge-base")) return Buffer.from("abc123"); if (cmdStr.includes("git diff") && cmdStr.includes("--stat")) return "1 file changed" as any; if (cmdStr.includes("merge --squash")) return Buffer.from(""); if (cmdStr.includes("merge -X theirs --squash") || cmdStr.includes("merge -X ours --squash")) { return Buffer.from(""); } if (cmdStr.includes("diff --cached --quiet")) return "1" as any; if (cmdStr.includes("diff --cached")) return "0" as any; if (cmdStr.includes("show --shortstat")) return "3 files changed, 10 insertions(+), 2 deletions(-)" as any; if (cmdStr.includes("branch -d") || cmdStr.includes("branch -D")) return Buffer.from(""); if (cmdStr.includes("worktree remove")) return Buffer.from(""); return Buffer.from(""); }); } export function setupFailingFallbackStrategy() { mockedExecSync.mockImplementation((cmd: any) => { const cmdStr = String(cmd); if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123"); if (cmdStr === "git rev-parse HEAD" || cmdStr.startsWith("git rev-parse HEAD ")) return "mergedcommit123"; if (cmdStr.includes("git log")) return "- feat: something" as any; if (cmdStr.includes("merge-base")) return Buffer.from("abc123"); if (cmdStr.includes("git diff") && cmdStr.includes("--stat")) return "1 file changed" as any; if (cmdStr.includes("merge --squash")) return Buffer.from(""); if (cmdStr.includes("merge -X theirs --squash") || cmdStr.includes("merge -X ours --squash")) { const err = new Error("fatal: git merge -X fallback failed with unresolved conflicts"); err.name = "ExecSyncError"; throw err; } if (cmdStr.includes("diff --cached --quiet")) return "1" as any; if (cmdStr.includes("diff --cached")) return "0" as any; if (cmdStr.includes("show --shortstat")) return "3 files changed, 10 insertions(+), 2 deletions(-)" as any; if (cmdStr.includes("branch -d") || cmdStr.includes("branch -D")) return Buffer.from(""); if (cmdStr.includes("worktree remove")) return Buffer.from(""); return Buffer.from(""); }); } export const setupFailingTheirsStrategy = setupFailingFallbackStrategy;