feat(FN-4834): complete Step 1 — surface init diagnostics in task log

Fusion-Task-Id: FN-4834
Fusion-Task-Lineage: 714c9c73-ca66-4c3f-980c-d03e6b50316a
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 21:29:00 -07:00
committed by gsxdsm
parent 405d24f9ab
commit ed8c8752f0
2 changed files with 69 additions and 4 deletions

View File

@@ -117,6 +117,29 @@ describe("acquireTaskWorktree", () => {
});
expect(runConfiguredCommand).not.toHaveBeenCalled();
});
it("FN-4834: logs worktree init stderr in task log outcome", async () => {
const runConfiguredCommand = vi.fn().mockResolvedValue({
exitCode: 1,
stderr: "ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE Cannot install with \"frozen-lockfile\" because pnpm-lock.yaml is not up to date",
stdout: "",
});
await expect(acquireTaskWorktree({
task,
rootDir: process.cwd(),
store,
settings: { worktreeInitCommand: "pnpm install --frozen-lockfile" } as any,
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/new", branch: "fusion/fn-1" }),
runConfiguredCommand,
runInitCommand: true,
logger: { log: vi.fn(), warn: vi.fn(), error: vi.fn() },
})).resolves.toBeTruthy();
const failureCall = store.logEntry.mock.calls.find((call: unknown[]) => String(call[1]).startsWith("Worktree init command failed"));
expect(failureCall).toBeDefined();
expect(failureCall?.[2]).toContain("ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE");
});
});
describe("acquireTaskWorktree foreign start-point warning", () => {

View File

@@ -51,7 +51,15 @@ export interface AcquireTaskWorktreeOptions {
startPoint?: string,
allowSiblingBranchRename?: boolean,
) => Promise<{ path: string; branch: string }>;
runConfiguredCommand?: (command: string, cwd: string, timeoutMs: number, env?: NodeJS.ProcessEnv) => Promise<{ spawnError?: string | Error; timedOut?: boolean; exitCode?: number | null }>;
runConfiguredCommand?: (command: string, cwd: string, timeoutMs: number, env?: NodeJS.ProcessEnv) => Promise<{
spawnError?: string | Error;
timedOut?: boolean;
exitCode?: number | null;
signal?: NodeJS.Signals | null;
stdout?: string;
stderr?: string;
bufferExceeded?: boolean;
}>;
taskEnv?: NodeJS.ProcessEnv;
backend?: WorktreeBackend;
}
@@ -68,12 +76,44 @@ export interface AcquireTaskWorktreeResult {
};
}
type InitCommandResult = Awaited<ReturnType<NonNullable<AcquireTaskWorktreeOptions["runConfiguredCommand"]>>>;
const INIT_OUTCOME_MAX_CHARS = 2_000;
function configuredCommandErrorMessage(result: { spawnError?: string | Error; timedOut?: boolean; exitCode?: number | null }): string {
if (result.spawnError) return `Failed to start command: ${result.spawnError}`;
if (result.timedOut) return "Command timed out";
return `Command exited with code ${result.exitCode ?? "unknown"}`;
}
function truncateInitCommandOutput(output: string): string {
if (output.length <= INIT_OUTCOME_MAX_CHARS) return output;
return `... output truncated to last ${INIT_OUTCOME_MAX_CHARS} chars ...\n${output.slice(-INIT_OUTCOME_MAX_CHARS)}`;
}
function formatInitFailureOutcome(initResult: InitCommandResult | undefined, err: unknown): string {
const stderr = initResult?.stderr?.trim();
if (stderr) return truncateInitCommandOutput(stderr);
const stdout = initResult?.stdout?.trim();
if (stdout) return truncateInitCommandOutput(stdout);
if (initResult?.spawnError) {
return typeof initResult.spawnError === "string" ? initResult.spawnError : initResult.spawnError.message;
}
const parts: string[] = [];
if (initResult?.timedOut) parts.push("Command timed out");
if (initResult?.exitCode !== undefined && initResult.exitCode !== null) parts.push(`exit code: ${initResult.exitCode}`);
if (initResult?.signal) parts.push(`signal: ${initResult.signal}`);
if (parts.length > 0) return parts.join("; ");
if (err instanceof Error && err.message.trim().length > 0) return err.message;
const fallback = String(err).trim();
return fallback.length > 0 ? fallback : "Command failed";
}
async function maybeWarnForeignTaskStartPoint(
input: {
baseBranch: string | null;
@@ -321,8 +361,9 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro
if (runInitCommand && settings.worktreeInitCommand && runConfiguredCommand) {
const initStartedAt = Date.now();
let initResult: InitCommandResult | undefined;
try {
const initResult = await runConfiguredCommand(settings.worktreeInitCommand, worktreePath, 300_000, taskEnv);
initResult = await runConfiguredCommand(settings.worktreeInitCommand, worktreePath, 300_000, taskEnv);
if (initResult.spawnError || initResult.timedOut || initResult.exitCode !== 0) {
throw new Error(configuredCommandErrorMessage(initResult));
}
@@ -330,8 +371,9 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro
} catch (err) {
await store.logEntry(task.id, `[timing] Worktree init command failed after ${Date.now() - initStartedAt}ms`, undefined, runContext);
const message = err instanceof Error ? err.message : String(err);
logger?.error?.(`${task.id}: worktree init command failed — first test run will likely fail: ${message}`);
await store.logEntry(task.id, `Worktree init command failed (first test run will likely fail): ${message}`, undefined, runContext);
const outcome = formatInitFailureOutcome(initResult, err);
logger?.error?.(`${task.id}: worktree init command failed first test run will likely fail: ${message} (stderr captured in task log outcome)`);
await store.logEntry(task.id, `Worktree init command failed (first test run will likely fail): ${message}`, outcome, runContext);
}
}