mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 14:37:44 +02:00
* feat(desktop): restart local daemon when bundled CLI version differs Desktop bundles a multica CLI binary at build time via bundle-cli.mjs. If a local daemon is already running from a previous session with an older CLI, the newly bundled version never takes effect until the user manually restarts. Fix that on the login/auto-start path. - Expose the daemon's CLI version on GET /health as cli_version (sourced from cfg.CLIVersion, which is already set from the ldflag at daemon startup in cmd_daemon.go). - In the desktop main process, query the resolved CLI binary's version once via `multica version --output json` and cache it for the process lifetime. - On daemon:auto-start, if the daemon is already running, compare the two versions. Restart only when BOTH sides are known and the strings differ — a restart kills in-flight agent tasks, so any uncertainty (bundled CLI unknown, older daemon without cli_version field, read failure) fails safe and leaves the daemon alone. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(daemon): defer version-mismatch restart until active tasks drain Previous iteration restarted the daemon immediately on a confirmed CLI version mismatch, which would kill any agent tasks mid-execution. Gate the restart on an active-task counter so in-flight work always finishes. - Daemon: add `activeTasks atomic.Int64` on the Daemon struct, increment/decrement it around handleTask, and expose it as `active_task_count` on GET /health. - Desktop: when a version mismatch is confirmed but active_task_count > 0, set a pendingVersionRestart flag instead of restarting. The 5s pollOnce loop retries ensureRunningDaemonVersionMatches on each tick and fires the restart the moment the count drops to 0. - Eventual consistency: if the user keeps the daemon permanently busy, the version stays out of date — that's a strictly better failure mode than silently killing hour-long agent runs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(daemon): cover version-check decision + /health counter exposure Addresses the test-coverage gap from the second review. - Go: extract the /health handler into a named method `(d *Daemon) healthHandler(startedAt time.Time)` so it can be exercised via httptest without spinning up a listener. Add health_test.go covering cli_version + active_task_count field exposure and the increment / decrement protocol used by pollLoop. - Desktop: extract the pure version-check decision logic into version-decision.ts (no electron, no I/O, no module state). The ensureRunningDaemonVersionMatches wrapper now delegates the "what should we do" decision to decideVersionAction and owns only the side effects (logging, flag mutation, restartDaemon call). - Desktop: bolt vitest onto apps/desktop (vitest.config.ts + catalog devDep + test script) so main-process unit tests have a home. Add version-decision.test.ts covering all four action branches and the busy→idle drain transition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(daemon): bust CLI version cache on retry-install, lock wire-level JSON keys Two polish items from review. - daemon:retry-install now also clears cachedCliBinaryVersion. Previously a retry that landed a newly-downloaded CLI at a different version would false-negative on the next version check because the cached version string was sticky for the process lifetime. - TestHealthHandlerReportsCLIVersionAndActiveTaskCount now decodes into a raw map[string]any and asserts the exact snake_case keys (cli_version, active_task_count, status). The desktop TS client keys on these literal strings, so a silent struct-tag rename must fail the test. Typed struct round-trip kept as a separate value check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
89 lines
2.4 KiB
TypeScript
89 lines
2.4 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { decideVersionAction } from "./version-decision";
|
|
|
|
describe("decideVersionAction", () => {
|
|
it("returns not_running when health payload is null", () => {
|
|
expect(decideVersionAction("v1.0.0", null)).toBe("not_running");
|
|
});
|
|
|
|
it("returns not_running when status is not 'running'", () => {
|
|
expect(
|
|
decideVersionAction("v1.0.0", { status: "stopped", cli_version: "v1.0.0" }),
|
|
).toBe("not_running");
|
|
});
|
|
|
|
it("returns ok when bundled version is unknown (fail safe)", () => {
|
|
expect(
|
|
decideVersionAction(null, {
|
|
status: "running",
|
|
cli_version: "v1.0.0",
|
|
active_task_count: 0,
|
|
}),
|
|
).toBe("ok");
|
|
});
|
|
|
|
it("returns ok when running daemon does not report cli_version (older daemon)", () => {
|
|
expect(
|
|
decideVersionAction("v1.0.0", {
|
|
status: "running",
|
|
active_task_count: 0,
|
|
}),
|
|
).toBe("ok");
|
|
});
|
|
|
|
it("returns ok when versions match exactly", () => {
|
|
expect(
|
|
decideVersionAction("v1.2.3", {
|
|
status: "running",
|
|
cli_version: "v1.2.3",
|
|
active_task_count: 5,
|
|
}),
|
|
).toBe("ok");
|
|
});
|
|
|
|
it("returns restart when versions differ and daemon is idle", () => {
|
|
expect(
|
|
decideVersionAction("v1.2.3", {
|
|
status: "running",
|
|
cli_version: "v1.2.2",
|
|
active_task_count: 0,
|
|
}),
|
|
).toBe("restart");
|
|
});
|
|
|
|
it("treats missing active_task_count as 0 (old daemon that still reports cli_version)", () => {
|
|
expect(
|
|
decideVersionAction("v1.2.3", {
|
|
status: "running",
|
|
cli_version: "v1.2.2",
|
|
}),
|
|
).toBe("restart");
|
|
});
|
|
|
|
it("returns defer when versions differ but daemon is busy", () => {
|
|
expect(
|
|
decideVersionAction("v1.2.3", {
|
|
status: "running",
|
|
cli_version: "v1.2.2",
|
|
active_task_count: 2,
|
|
}),
|
|
).toBe("defer");
|
|
});
|
|
|
|
it("transitions defer → restart as tasks drain", () => {
|
|
// Same bundled version across three observations while the daemon ages.
|
|
const bundled = "v2.0.0";
|
|
const base = { status: "running", cli_version: "v1.9.0" } as const;
|
|
|
|
expect(
|
|
decideVersionAction(bundled, { ...base, active_task_count: 3 }),
|
|
).toBe("defer");
|
|
expect(
|
|
decideVersionAction(bundled, { ...base, active_task_count: 1 }),
|
|
).toBe("defer");
|
|
expect(
|
|
decideVersionAction(bundled, { ...base, active_task_count: 0 }),
|
|
).toBe("restart");
|
|
});
|
|
});
|