From eda601c46acb0d5c82e1fc943e4c4dc8d7ef43fe Mon Sep 17 00:00:00 2001 From: Artem Khoroshilov Date: Fri, 30 Jan 2026 04:37:44 +0200 Subject: [PATCH] Doctor update: skip restore control-ui when untracked --- src/infra/update-runner.test.ts | 36 ++++++++++++++++++++ src/infra/update-runner.ts | 60 +++++++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/src/infra/update-runner.test.ts b/src/infra/update-runner.test.ts index 6a49a85c0..97ab434ff 100644 --- a/src/infra/update-runner.test.ts +++ b/src/infra/update-runner.test.ts @@ -112,6 +112,9 @@ describe("runGatewayUpdate", () => { "pnpm install": { stdout: "" }, "pnpm build": { stdout: "" }, "pnpm ui:build": { stdout: "" }, + [`git -C ${tempDir} ls-files -- dist/control-ui/`]: { + stdout: "dist/control-ui/index.html\n", + }, [`git -C ${tempDir} checkout -- dist/control-ui/`]: { stdout: "" }, "pnpm moltbot doctor --non-interactive": { stdout: "" }, }); @@ -128,6 +131,39 @@ describe("runGatewayUpdate", () => { expect(calls).not.toContain(`git -C ${tempDir} checkout --detach ${betaTag}`); }); + it("skips restoring control-ui when dist/control-ui is not tracked", async () => { + await fs.mkdir(path.join(tempDir, ".git")); + await fs.writeFile( + path.join(tempDir, "package.json"), + JSON.stringify({ name: "moltbot", version: "1.0.0", packageManager: "pnpm@8.0.0" }), + "utf-8", + ); + const stableTag = "v1.0.1-1"; + const { runner, calls } = createRunner({ + [`git -C ${tempDir} rev-parse --show-toplevel`]: { stdout: tempDir }, + [`git -C ${tempDir} rev-parse HEAD`]: { stdout: "abc123" }, + [`git -C ${tempDir} status --porcelain -- :!dist/control-ui/`]: { stdout: "" }, + [`git -C ${tempDir} fetch --all --prune --tags`]: { stdout: "" }, + [`git -C ${tempDir} tag --list v* --sort=-v:refname`]: { stdout: `${stableTag}\n` }, + [`git -C ${tempDir} checkout --detach ${stableTag}`]: { stdout: "" }, + "pnpm install": { stdout: "" }, + "pnpm build": { stdout: "" }, + "pnpm ui:build": { stdout: "" }, + [`git -C ${tempDir} ls-files -- dist/control-ui/`]: { stdout: "" }, + "pnpm moltbot doctor --non-interactive": { stdout: "" }, + }); + + const result = await runGatewayUpdate({ + cwd: tempDir, + runCommand: async (argv, _options) => runner(argv), + timeoutMs: 5000, + channel: "beta", + }); + + expect(result.status).toBe("ok"); + expect(calls).not.toContain(`git -C ${tempDir} checkout -- dist/control-ui/`); + }); + it("skips update when no git root", async () => { await fs.writeFile( path.join(tempDir, "package.json"), diff --git a/src/infra/update-runner.ts b/src/infra/update-runner.ts index 0735edb39..8f9ff746c 100644 --- a/src/infra/update-runner.ts +++ b/src/infra/update-runner.ts @@ -275,6 +275,41 @@ async function runStep(opts: RunStepOptions): Promise { }; } +function skipStep( + opts: Pick & { + stdoutTail?: string | null; + stderrTail?: string | null; + }, +): UpdateStepResult { + const { name, argv, cwd, progress, stepIndex, totalSteps, stdoutTail, stderrTail } = opts; + const command = argv.join(" "); + + const stepInfo: UpdateStepInfo = { + name, + command, + index: stepIndex, + total: totalSteps, + }; + + progress?.onStepStart?.(stepInfo); + progress?.onStepComplete?.({ + ...stepInfo, + durationMs: 0, + exitCode: 0, + stderrTail: stderrTail ?? null, + }); + + return { + name, + command, + cwd, + durationMs: 0, + exitCode: 0, + stdoutTail: stdoutTail ?? null, + stderrTail: stderrTail ?? null, + }; +} + function managerScriptArgs(manager: "pnpm" | "bun" | "npm", script: string, args: string[] = []) { if (manager === "pnpm") return ["pnpm", script, ...args]; if (manager === "bun") return ["bun", "run", script, ...args]; @@ -678,13 +713,26 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise< // Restore dist/control-ui/ to committed state to prevent dirty repo after update // (ui:build regenerates assets with new hashes, which would block future updates) - const restoreUiStep = await runStep( - step( - "restore control-ui", - ["git", "-C", gitRoot, "checkout", "--", "dist/control-ui/"], - gitRoot, - ), + const restoreUiOpts = step( + "restore control-ui", + ["git", "-C", gitRoot, "checkout", "--", "dist/control-ui/"], + gitRoot, ); + const lsFilesResult = await runCommand( + ["git", "-C", gitRoot, "ls-files", "--", "dist/control-ui/"], + { + cwd: gitRoot, + timeoutMs, + }, + ); + const hasTrackedControlUiFiles = + lsFilesResult.code === 0 && lsFilesResult.stdout.trim().length > 0; + const restoreUiStep = hasTrackedControlUiFiles + ? await runStep(restoreUiOpts) + : skipStep({ + ...restoreUiOpts, + stdoutTail: "Skipped: dist/control-ui/ is not tracked in git.", + }); steps.push(restoreUiStep); const doctorStep = await runStep(