Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/loopover-miner/lib/migrate-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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]";

Expand All @@ -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
Expand Down
11 changes: 10 additions & 1 deletion packages/loopover-miner/lib/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)],
Expand All @@ -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));
}
Expand Down
11 changes: 6 additions & 5 deletions test/unit/mcp-tool-rename-aliases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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());
});
});
Expand Down
22 changes: 21 additions & 1 deletion test/unit/miner-migrate-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 { openWorktreeAllocator, resolveWorktreeAllocatorDbPath } from "../../packages/loopover-miner/lib/worktree-allocator.js";

const roots: string[] = [];

Expand All @@ -24,6 +25,10 @@ const STORE_NAMES = [
"claim-ledger",
"run-state",
"plan-store",
"governor-state",
"attempt-log",
"replay-snapshot",
"worktree-allocator",
];

afterEach(() => {
Expand All @@ -32,11 +37,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");
Expand All @@ -62,6 +69,19 @@ describe("loopover-miner migrate (#4871)", () => {
expect(portfolioQueue?.versionBefore).toBeGreaterThan(0);
});

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 it proves the adapter runs.
const env = tempEnv();
openWorktreeAllocator({ dbPath: resolveWorktreeAllocatorDbPath(env) }).close();

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", () => {
const env = tempEnv();
const dbPath = resolvePortfolioQueueDbPath(env);
Expand Down
13 changes: 13 additions & 0 deletions test/unit/miner-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down