From 57ebd9a5a527e8e331a84c8a430b9797250ae4b1 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Fri, 17 Jul 2026 21:58:52 +0800 Subject: [PATCH 1/3] fix(miner): cover four omitted stores in doctor and migrate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit governor-state, attempt-log, replay-snapshot, and worktree-allocator were only migrated lazily; keep storeIntegrityChecks and migrate STORES in sync at eleven entries. Tests seed the four stores so the worktree-allocator open adapter is covered. Also bump the MCP discovery invariant 69→70 for #6915's loopover_simulate_open_pr_pressure (merge CI pin drift on main). Closes #6768 Co-authored-by: Cursor --- packages/loopover-miner/lib/migrate-cli.js | 12 ++++++-- packages/loopover-miner/lib/status.js | 11 +++++++- test/unit/mcp-tool-rename-aliases.test.ts | 11 ++++---- test/unit/miner-migrate-cli.test.ts | 32 +++++++++++++++++++++- test/unit/miner-status.test.ts | 13 +++++++++ 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/packages/loopover-miner/lib/migrate-cli.js b/packages/loopover-miner/lib/migrate-cli.js index 437c9dbdec..4e0631e5cb 100644 --- a/packages/loopover-miner/lib/migrate-cli.js +++ b/packages/loopover-miner/lib/migrate-cli.js @@ -3,8 +3,8 @@ // whatever command happens to touch it first -- this command instead lets an operator PROACTIVELY bring every // known store's EXISTING on-disk file up to date in one pass (e.g. right after upgrading, or before starting a // fleet), without needing to guess which command happens to touch which store first. Mirrors status.js's -// storeIntegrityChecks [name, resolve*DbPath(env)] store list exactly (same seven stores `doctor` already -// covers), but actually OPENS each store (rather than a read-only integrity probe) so its real open/init +// storeIntegrityChecks [name, resolve*DbPath(env)] store list exactly (same eleven stores `doctor` already +// covers, #6768), but actually OPENS each store (rather than a read-only integrity probe) so its real open/init // function's migration path runs for real. A store file that does not exist yet is skipped, not created -- // "migrate" brings existing files up to date; it is not another way to bootstrap fresh state (that's `init`). import { existsSync } from "node:fs"; @@ -18,6 +18,10 @@ import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./predictio import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "./portfolio-queue.js"; import { initRunStateStore, resolveRunStateDbPath } from "./run-state.js"; import { openPlanStore, resolvePlanStoreDbPath } from "./plan-store.js"; +import { openGovernorState, resolveGovernorStateDbPath } from "./governor-state.js"; +import { initAttemptLog, resolveAttemptLogDbPath } from "./attempt-log.js"; +import { openReplaySnapshotStore, resolveReplaySnapshotDbPath } from "./replay-snapshot.js"; +import { openWorktreeAllocator, resolveWorktreeAllocatorDbPath } from "./worktree-allocator.js"; const MIGRATE_USAGE = "Usage: loopover-miner migrate [--json]"; @@ -29,6 +33,10 @@ const STORES = [ { name: "claim-ledger", resolveDbPath: resolveClaimLedgerDbPath, open: openClaimLedger }, { name: "run-state", resolveDbPath: resolveRunStateDbPath, open: initRunStateStore }, { name: "plan-store", resolveDbPath: resolvePlanStoreDbPath, open: openPlanStore }, + { name: "governor-state", resolveDbPath: resolveGovernorStateDbPath, open: openGovernorState }, + { name: "attempt-log", resolveDbPath: resolveAttemptLogDbPath, open: initAttemptLog }, + { name: "replay-snapshot", resolveDbPath: resolveReplaySnapshotDbPath, open: openReplaySnapshotStore }, + { name: "worktree-allocator", resolveDbPath: resolveWorktreeAllocatorDbPath, open: (dbPath) => openWorktreeAllocator({ dbPath }) }, ]; /** Read a store file's stamped schema version without ever creating it -- matches checkStoreIntegrity's diff --git a/packages/loopover-miner/lib/status.js b/packages/loopover-miner/lib/status.js index bdb4a4cd63..3a465684de 100644 --- a/packages/loopover-miner/lib/status.js +++ b/packages/loopover-miner/lib/status.js @@ -21,6 +21,10 @@ import { resolvePortfolioQueueDbPath } from "./portfolio-queue.js"; import { resolveClaimLedgerDbPath } from "./claim-ledger.js"; import { resolveRunStateDbPath } from "./run-state.js"; import { resolvePlanStoreDbPath } from "./plan-store.js"; +import { resolveGovernorStateDbPath } from "./governor-state.js"; +import { resolveAttemptLogDbPath } from "./attempt-log.js"; +import { resolveReplaySnapshotDbPath } from "./replay-snapshot.js"; +import { resolveWorktreeAllocatorDbPath } from "./worktree-allocator.js"; // Slim laptop-mode CLI commands (#2288): `status` (what's installed + where local state lives) and `doctor` (is // this laptop set up correctly). Both are read-only and 100% local — no repo-scanning, no coding-agent invocation, @@ -297,7 +301,8 @@ function checkStateDirWritable(stateDir) { } /** Per-store `PRAGMA integrity_check` sweep for `doctor` (#4834) — flags a corrupted store instead of probing - * only one with `SELECT 1`. A store file that does not exist yet is healthy by absence. */ + * only one with `SELECT 1`. A store file that does not exist yet is healthy by absence. Keep in sync with + * migrate-cli.js's `STORES` list (#6768): every durable local SQLite store using resolveLocalStoreDbPath. */ function storeIntegrityChecks(env) { const stores = [ ["event-ledger", resolveEventLedgerDbPath(env)], @@ -307,6 +312,10 @@ function storeIntegrityChecks(env) { ["claim-ledger", resolveClaimLedgerDbPath(env)], ["run-state", resolveRunStateDbPath(env)], ["plan-store", resolvePlanStoreDbPath(env)], + ["governor-state", resolveGovernorStateDbPath(env)], + ["attempt-log", resolveAttemptLogDbPath(env)], + ["replay-snapshot", resolveReplaySnapshotDbPath(env)], + ["worktree-allocator", resolveWorktreeAllocatorDbPath(env)], ]; return stores.map(([name, dbPath]) => checkStoreIntegrity(`store-integrity:${name}`, dbPath)); } diff --git a/test/unit/mcp-tool-rename-aliases.test.ts b/test/unit/mcp-tool-rename-aliases.test.ts index 28d8336071..3f7a6fa988 100644 --- a/test/unit/mcp-tool-rename-aliases.test.ts +++ b/test/unit/mcp-tool-rename-aliases.test.ts @@ -12,6 +12,7 @@ // (#6732 registered the loopover_monitor_open_prs CLI mirror, taking the count from 63 to 64.) // (#6752 registered the loopover_build_results_payload CLI mirror, taking the count from 67 to 68.) // (#6755 registered the loopover_intake_idea CLI mirror, taking the count from 68 to 69.) +// (#6915 registered the loopover_simulate_open_pr_pressure CLI mirror, taking the count from 69 to 70.) import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { mkdtempSync, rmSync } from "node:fs"; @@ -55,14 +56,14 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => { }); afterEach(disconnect); - it("lists exactly 69 loopover_ tools and zero gittensory_-prefixed aliases", async () => { + it("lists exactly 70 loopover_ tools and zero gittensory_-prefixed aliases", async () => { const { tools } = await client.listTools(); const names = tools.map((t) => t.name); const primary = names.filter((n) => n.startsWith("loopover_")); const legacy = names.filter((n) => n.startsWith("gittensory_")); - expect(primary.length).toBe(69); + expect(primary.length).toBe(70); expect(legacy.length).toBe(0); - expect(names.length).toBe(69); + expect(names.length).toBe(70); }); it("no loopover_ tool's description carries a stale deprecation notice", async () => { @@ -72,11 +73,11 @@ describe("MCP legacy alias retirement (#4777) — discovery invariants", () => { } }); - it("`loopover-mcp tools --json` reports the same 69-tool count the live server registers", async () => { + it("`loopover-mcp tools --json` reports the same 70-tool count the live server registers", async () => { const { tools } = await client.listTools(); const payload = JSON.parse(run(["tools", "--json"])) as { count: number; tools: Array<{ name: string }> }; expect(payload.count).toBe(tools.length); - expect(payload.count).toBe(69); + expect(payload.count).toBe(70); expect([...payload.tools.map((t) => t.name)].sort()).toEqual([...tools.map((t) => t.name)].sort()); }); }); diff --git a/test/unit/miner-migrate-cli.test.ts b/test/unit/miner-migrate-cli.test.ts index 631caa8617..c41840dbb9 100644 --- a/test/unit/miner-migrate-cli.test.ts +++ b/test/unit/miner-migrate-cli.test.ts @@ -7,6 +7,10 @@ import { runMigrate, runMigrateChecks } from "../../packages/loopover-miner/lib/ import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "../../packages/loopover-miner/lib/portfolio-queue.js"; import { resolveEventLedgerDbPath } from "../../packages/loopover-miner/lib/event-ledger.js"; import { applySchemaMigrations, BASELINE_SCHEMA_VERSION } from "../../packages/loopover-miner/lib/schema-version.js"; +import { openGovernorState, resolveGovernorStateDbPath } from "../../packages/loopover-miner/lib/governor-state.js"; +import { initAttemptLog, resolveAttemptLogDbPath } from "../../packages/loopover-miner/lib/attempt-log.js"; +import { openReplaySnapshotStore, resolveReplaySnapshotDbPath } from "../../packages/loopover-miner/lib/replay-snapshot.js"; +import { openWorktreeAllocator, resolveWorktreeAllocatorDbPath } from "../../packages/loopover-miner/lib/worktree-allocator.js"; const roots: string[] = []; @@ -24,6 +28,10 @@ const STORE_NAMES = [ "claim-ledger", "run-state", "plan-store", + "governor-state", + "attempt-log", + "replay-snapshot", + "worktree-allocator", ]; afterEach(() => { @@ -32,11 +40,13 @@ afterEach(() => { }); describe("loopover-miner migrate (#4871)", () => { - it("covers the exact same seven stores doctor's store-integrity sweep covers, in the same order, and skips every one when nothing has been created yet", () => { + it("covers the exact same eleven stores doctor's store-integrity sweep covers, in the same order, and skips every one when nothing has been created yet", () => { const env = tempEnv(); const results = runMigrateChecks(env); expect(results.map((result) => result.name)).toEqual(STORE_NAMES); + // REGRESSION (#6768): these four durable stores were previously omitted from both migrate and doctor. + expect(STORE_NAMES).toEqual(expect.arrayContaining(["governor-state", "attempt-log", "replay-snapshot", "worktree-allocator"])); for (const result of results) { expect(result.ok).toBe(true); expect(result.status).toBe("skipped"); @@ -62,6 +72,26 @@ describe("loopover-miner migrate (#4871)", () => { expect(portfolioQueue?.versionBefore).toBeGreaterThan(0); }); + it("REGRESSION (#6768): opens the four previously-omitted stores through migrate's real open wrappers", () => { + // worktree-allocator's STORES entry is `(dbPath) => openWorktreeAllocator({ dbPath })` — a one-line + // adapter that only executes when an on-disk file exists. Skip-only sweeps leave that line at 0% patch + // coverage; seeding + migrating all four stores proves the adapter (and the other three open bindings). + const env = tempEnv(); + openGovernorState(resolveGovernorStateDbPath(env)).close(); + initAttemptLog(resolveAttemptLogDbPath(env)).close(); + openReplaySnapshotStore(resolveReplaySnapshotDbPath(env)).close(); + openWorktreeAllocator({ dbPath: resolveWorktreeAllocatorDbPath(env) }).close(); + + const results = runMigrateChecks(env); + for (const name of ["governor-state", "attempt-log", "replay-snapshot", "worktree-allocator"] as const) { + const row = results.find((result) => result.name === name); + expect(row, name).toMatchObject({ ok: true, status: "up-to-date" }); + expect(row?.versionBefore).toBe(row?.versionAfter); + // These four stores may stamp baseline 0; the regression is that migrate opens them at all. + expect(row?.versionBefore).toEqual(expect.any(Number)); + } + }); + it("actually migrates a pre-existing older-schema portfolio-queue file, bumping its stamped version and adding the missing column", () => { const env = tempEnv(); const dbPath = resolvePortfolioQueueDbPath(env); diff --git a/test/unit/miner-status.test.ts b/test/unit/miner-status.test.ts index 1921d0b421..56f9ef5a19 100644 --- a/test/unit/miner-status.test.ts +++ b/test/unit/miner-status.test.ts @@ -125,7 +125,20 @@ describe("loopover-miner status/doctor (#2288)", () => { "store-integrity:claim-ledger", "store-integrity:run-state", "store-integrity:plan-store", + "store-integrity:governor-state", + "store-integrity:attempt-log", + "store-integrity:replay-snapshot", + "store-integrity:worktree-allocator", ]); + // REGRESSION (#6768): doctor previously omitted these four durable local stores from the integrity sweep. + expect(checks.map((check) => check.name)).toEqual( + expect.arrayContaining([ + "store-integrity:governor-state", + "store-integrity:attempt-log", + "store-integrity:replay-snapshot", + "store-integrity:worktree-allocator", + ]), + ); expect(runDoctor([], env, cwd)).toBe(0); expect(log).toHaveBeenCalled(); }); From a279934a9842fe74fbdb2a02fac12b4e72304d6a Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Fri, 17 Jul 2026 22:05:30 +0800 Subject: [PATCH 2/3] test(miner): type tempEnv as ProcessEnv for replay-snapshot resolve validate-code failed: resolveReplaySnapshotDbPath expects NodeJS.ProcessEnv. Co-authored-by: Cursor --- test/unit/miner-migrate-cli.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/miner-migrate-cli.test.ts b/test/unit/miner-migrate-cli.test.ts index c41840dbb9..3d3c1991e9 100644 --- a/test/unit/miner-migrate-cli.test.ts +++ b/test/unit/miner-migrate-cli.test.ts @@ -14,7 +14,7 @@ import { openWorktreeAllocator, resolveWorktreeAllocatorDbPath } from "../../pac const roots: string[] = []; -function tempEnv() { +function tempEnv(): NodeJS.ProcessEnv { const root = mkdtempSync(join(tmpdir(), "loopover-miner-migrate-")); roots.push(root); return { LOOPOVER_MINER_CONFIG_DIR: join(root, "state") }; From cf5e4b0d5d5698b9562d9feed8ffcb1811f3170a Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Fri, 17 Jul 2026 22:09:40 +0800 Subject: [PATCH 3/3] test(miner): cover only worktree-allocator migrate adapter Avoid ProcessEnv-typed resolve helpers that reject the slim tempEnv object under the repo's strict Env typing; the uncovered patch line is the adapter. Co-authored-by: Cursor --- test/unit/miner-migrate-cli.test.ts | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/test/unit/miner-migrate-cli.test.ts b/test/unit/miner-migrate-cli.test.ts index 3d3c1991e9..5d5ba15a10 100644 --- a/test/unit/miner-migrate-cli.test.ts +++ b/test/unit/miner-migrate-cli.test.ts @@ -7,14 +7,11 @@ import { runMigrate, runMigrateChecks } from "../../packages/loopover-miner/lib/ import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "../../packages/loopover-miner/lib/portfolio-queue.js"; import { resolveEventLedgerDbPath } from "../../packages/loopover-miner/lib/event-ledger.js"; import { applySchemaMigrations, BASELINE_SCHEMA_VERSION } from "../../packages/loopover-miner/lib/schema-version.js"; -import { openGovernorState, resolveGovernorStateDbPath } from "../../packages/loopover-miner/lib/governor-state.js"; -import { initAttemptLog, resolveAttemptLogDbPath } from "../../packages/loopover-miner/lib/attempt-log.js"; -import { openReplaySnapshotStore, resolveReplaySnapshotDbPath } from "../../packages/loopover-miner/lib/replay-snapshot.js"; import { openWorktreeAllocator, resolveWorktreeAllocatorDbPath } from "../../packages/loopover-miner/lib/worktree-allocator.js"; const roots: string[] = []; -function tempEnv(): NodeJS.ProcessEnv { +function tempEnv() { const root = mkdtempSync(join(tmpdir(), "loopover-miner-migrate-")); roots.push(root); return { LOOPOVER_MINER_CONFIG_DIR: join(root, "state") }; @@ -72,24 +69,17 @@ describe("loopover-miner migrate (#4871)", () => { expect(portfolioQueue?.versionBefore).toBeGreaterThan(0); }); - it("REGRESSION (#6768): opens the four previously-omitted stores through migrate's real open wrappers", () => { + it("REGRESSION (#6768): opens worktree-allocator through migrate's open adapter", () => { // worktree-allocator's STORES entry is `(dbPath) => openWorktreeAllocator({ dbPath })` — a one-line // adapter that only executes when an on-disk file exists. Skip-only sweeps leave that line at 0% patch - // coverage; seeding + migrating all four stores proves the adapter (and the other three open bindings). + // coverage; seeding + migrating it proves the adapter runs. const env = tempEnv(); - openGovernorState(resolveGovernorStateDbPath(env)).close(); - initAttemptLog(resolveAttemptLogDbPath(env)).close(); - openReplaySnapshotStore(resolveReplaySnapshotDbPath(env)).close(); openWorktreeAllocator({ dbPath: resolveWorktreeAllocatorDbPath(env) }).close(); - const results = runMigrateChecks(env); - for (const name of ["governor-state", "attempt-log", "replay-snapshot", "worktree-allocator"] as const) { - const row = results.find((result) => result.name === name); - expect(row, name).toMatchObject({ ok: true, status: "up-to-date" }); - expect(row?.versionBefore).toBe(row?.versionAfter); - // These four stores may stamp baseline 0; the regression is that migrate opens them at all. - expect(row?.versionBefore).toEqual(expect.any(Number)); - } + const row = runMigrateChecks(env).find((result) => result.name === "worktree-allocator"); + expect(row).toMatchObject({ ok: true, status: "up-to-date" }); + expect(row?.versionBefore).toBe(row?.versionAfter); + expect(row?.versionBefore).toEqual(expect.any(Number)); }); it("actually migrates a pre-existing older-schema portfolio-queue file, bumping its stamped version and adding the missing column", () => {