diff --git a/packages/loopover-miner/lib/migrate-cli.js b/packages/loopover-miner/lib/migrate-cli.js index 65af977cc8..f35ba85837 100644 --- a/packages/loopover-miner/lib/migrate-cli.js +++ b/packages/loopover-miner/lib/migrate-cli.js @@ -23,6 +23,7 @@ import { initAttemptLog, resolveAttemptLogDbPath } from "./attempt-log.js"; import { openReplaySnapshotStore, resolveReplaySnapshotDbPath } from "./replay-snapshot.js"; import { openWorktreeAllocator, resolveWorktreeAllocatorDbPath } from "./worktree-allocator.js"; import { initContributionProfileCache, resolveContributionProfileCacheDbPath } from "./contribution-profile-cache.js"; +import { initPolicyVerdictCacheStore, resolvePolicyVerdictCacheDbPath } from "./policy-verdict-cache.js"; const MIGRATE_USAGE = "Usage: loopover-miner migrate [--json]"; @@ -39,6 +40,7 @@ const STORES = [ { name: "replay-snapshot", resolveDbPath: resolveReplaySnapshotDbPath, open: openReplaySnapshotStore }, { name: "worktree-allocator", resolveDbPath: resolveWorktreeAllocatorDbPath, open: (dbPath) => openWorktreeAllocator({ dbPath }) }, { name: "contribution-profile", resolveDbPath: resolveContributionProfileCacheDbPath, open: initContributionProfileCache }, + { name: "policy-verdict-cache", resolveDbPath: resolvePolicyVerdictCacheDbPath, open: initPolicyVerdictCacheStore }, ]; /** Read a store file's stamped schema version without ever creating it -- matches checkStoreIntegrity's diff --git a/packages/loopover-miner/lib/policy-verdict-cache.d.ts b/packages/loopover-miner/lib/policy-verdict-cache.d.ts index 8fab1f4aef..9616e46781 100644 --- a/packages/loopover-miner/lib/policy-verdict-cache.d.ts +++ b/packages/loopover-miner/lib/policy-verdict-cache.d.ts @@ -24,6 +24,8 @@ export type PolicyVerdictCacheStore = { etag: string, verdict: AiPolicyVerdict, ): PolicyVerdictCacheWrite; + /** Delete every cached verdict row for one repo scope (#6987); returns the number of rows removed. */ + purgeByRepo(repoScope: string): number; close(): void; }; diff --git a/packages/loopover-miner/lib/policy-verdict-cache.js b/packages/loopover-miner/lib/policy-verdict-cache.js index 88048cdc36..83b4103009 100644 --- a/packages/loopover-miner/lib/policy-verdict-cache.js +++ b/packages/loopover-miner/lib/policy-verdict-cache.js @@ -1,5 +1,6 @@ import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js"; import { applySchemaMigrations } from "./schema-version.js"; +import { POLICY_VERDICT_CACHE_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js"; // Local cache of resolved AI-usage-policy verdicts (#4843). Even with #4842's conditional-GET doc cache, the small // but non-zero cost of resolving `resolveAiPolicyVerdict` from raw doc text was still paid on every discover run. @@ -99,6 +100,14 @@ export function initPolicyVerdictCacheStore(dbPath = resolvePolicyVerdictCacheDb putStatement.run(normalizedRepoScope, normalizedDecisiveDoc, normalizedEtag, serializedVerdict, updatedAt); return { repoScope: normalizedRepoScope, decisiveDoc: normalizedDecisiveDoc, etag: normalizedEtag, verdict, updatedAt }; }, + /** + * Delete every cached verdict row for one repo scope (#6987) -- the right-to-be-forgotten path + * `loopover-miner purge` invokes. Returns the number of rows removed. Reuses store-maintenance.js's + * identifier-guarded purgeStoreByRepo, exactly like the other repo-scoped stores. + */ + purgeByRepo(repoScope) { + return purgeStoreByRepo(db, POLICY_VERDICT_CACHE_PURGE_SPEC, normalizeRepoScope(repoScope)); + }, close() { db.close(); }, diff --git a/packages/loopover-miner/lib/purge-cli.js b/packages/loopover-miner/lib/purge-cli.js index 7441223b55..0160723172 100644 --- a/packages/loopover-miner/lib/purge-cli.js +++ b/packages/loopover-miner/lib/purge-cli.js @@ -20,6 +20,7 @@ import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "./portfoli import { initRunStateStore, resolveRunStateDbPath } from "./run-state.js"; import { initContributionProfileCache, resolveContributionProfileCacheDbPath } from "./contribution-profile-cache.js"; import { openGovernorState, resolveGovernorStateDbPath } from "./governor-state.js"; +import { initPolicyVerdictCacheStore, resolvePolicyVerdictCacheDbPath } from "./policy-verdict-cache.js"; import { resolveAttemptLogDbPath } from "./attempt-log.js"; import { CLAIM_LEDGER_PURGE_SPEC, @@ -31,6 +32,7 @@ import { CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC, GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC, + POLICY_VERDICT_CACHE_PURGE_SPEC, countStoreByRepo, describeError, } from "./store-maintenance.js"; @@ -52,6 +54,7 @@ const REAL_PURGE_TARGETS = [ // governor-state holds TWO repo-scoped tables in one DB file; its store.purgeByRepo deletes both against a // single handle (never reopening the file), and its dry-run count sums both via `specs` (#7091). { name: "governor-state", optionKey: "openGovernorState", opener: openGovernorState, resolveDbPath: resolveGovernorStateDbPath, specs: [GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC] }, + { name: "policy-verdict-cache", optionKey: "initPolicyVerdictCacheStore", opener: initPolicyVerdictCacheStore, resolveDbPath: resolvePolicyVerdictCacheDbPath, spec: POLICY_VERDICT_CACHE_PURGE_SPEC }, ]; function parseRepoArg(value, usage) { diff --git a/packages/loopover-miner/lib/status.js b/packages/loopover-miner/lib/status.js index ec6b9252c7..e77f52764a 100644 --- a/packages/loopover-miner/lib/status.js +++ b/packages/loopover-miner/lib/status.js @@ -26,6 +26,7 @@ import { resolveAttemptLogDbPath } from "./attempt-log.js"; import { resolveReplaySnapshotDbPath } from "./replay-snapshot.js"; import { resolveWorktreeAllocatorDbPath } from "./worktree-allocator.js"; import { resolveContributionProfileCacheDbPath } from "./contribution-profile-cache.js"; +import { resolvePolicyVerdictCacheDbPath } from "./policy-verdict-cache.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, @@ -318,6 +319,7 @@ function storeIntegrityChecks(env) { ["replay-snapshot", resolveReplaySnapshotDbPath(env)], ["worktree-allocator", resolveWorktreeAllocatorDbPath(env)], ["contribution-profile", resolveContributionProfileCacheDbPath(env)], + ["policy-verdict-cache", resolvePolicyVerdictCacheDbPath(env)], ]; return stores.map(([name, dbPath]) => checkStoreIntegrity(`store-integrity:${name}`, dbPath)); } diff --git a/packages/loopover-miner/lib/store-maintenance.d.ts b/packages/loopover-miner/lib/store-maintenance.d.ts index 3b499bbc35..ddc82cdce0 100644 --- a/packages/loopover-miner/lib/store-maintenance.d.ts +++ b/packages/loopover-miner/lib/store-maintenance.d.ts @@ -18,6 +18,7 @@ export const RUN_STATE_PURGE_SPEC: LedgerPurgeSpec; export const CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC: LedgerPurgeSpec; export const GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC: LedgerPurgeSpec; export const GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC: LedgerPurgeSpec; +export const POLICY_VERDICT_CACHE_PURGE_SPEC: LedgerPurgeSpec; export type StoreIntegrityResult = { name: string; ok: boolean; detail: string }; export type LedgerRetentionPolicy = { maxAgeMs?: number; maxRows?: number }; diff --git a/packages/loopover-miner/lib/store-maintenance.js b/packages/loopover-miner/lib/store-maintenance.js index ca0c7fdda6..687e4ae017 100644 --- a/packages/loopover-miner/lib/store-maintenance.js +++ b/packages/loopover-miner/lib/store-maintenance.js @@ -48,6 +48,12 @@ export const CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC = { table: CONTRIBUTION_PROFI export const GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC = { table: "governor_reputation_history", repoColumn: "repo_full_name" }; export const GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC = { table: "governor_own_submissions", repoColumn: "repo_full_name" }; +/** policy-verdict-cache (#6987), another repo-scoped store the earlier sweeps missed. Its `repo_scope TEXT + * PRIMARY KEY` is the per-repo column (a tenant forge host + `owner/repo`), the same `repoColumn` shape and + * internal-constant-only discipline as the specs above. `policy-doc-cache.js` stays out (keyed by URL, no repo + * column, exactly like `attempt-log.js`). */ +export const POLICY_VERDICT_CACHE_PURGE_SPEC = { table: "policy_verdict_cache", repoColumn: "repo_scope" }; + const SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/; /** A readable message for a caught value, whether or not it is an Error. */ diff --git a/test/unit/miner-migrate-cli.test.ts b/test/unit/miner-migrate-cli.test.ts index 3b9125c7d3..776a76b304 100644 --- a/test/unit/miner-migrate-cli.test.ts +++ b/test/unit/miner-migrate-cli.test.ts @@ -30,6 +30,7 @@ const STORE_NAMES = [ "replay-snapshot", "worktree-allocator", "contribution-profile", + "policy-verdict-cache", ]; afterEach(() => { @@ -38,7 +39,7 @@ afterEach(() => { }); describe("loopover-miner migrate (#4871)", () => { - it("covers the exact same twelve 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 thirteen 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); diff --git a/test/unit/miner-policy-verdict-cache.test.ts b/test/unit/miner-policy-verdict-cache.test.ts index 7ed6ff732d..0227ddb55a 100644 --- a/test/unit/miner-policy-verdict-cache.test.ts +++ b/test/unit/miner-policy-verdict-cache.test.ts @@ -135,4 +135,17 @@ describe("loopover-miner policy-verdict cache store (#4843)", () => { it("throws on an empty explicit db path", () => { expect(() => initPolicyVerdictCacheStore("")).toThrow("invalid_policy_verdict_cache_db_path"); }); + + it("purgeByRepo deletes only the given repo scope's row and returns the count (#6987)", () => { + const store = openStore(); + store.put("acme/widgets", "AI-USAGE.md", '"v1"', VERDICT); + store.put("acme/other", "AI-USAGE.md", '"v2"', VERDICT); + expect(store.purgeByRepo("acme/widgets")).toBe(1); + expect(store.get("acme/widgets")).toBeNull(); + expect(store.get("acme/other")).not.toBeNull(); + }); + + it("purgeByRepo returns 0 when the repo scope has no cached verdict (#6987)", () => { + expect(openStore().purgeByRepo("acme/widgets")).toBe(0); + }); }); diff --git a/test/unit/miner-purge-cli.test.ts b/test/unit/miner-purge-cli.test.ts index afb151fc2e..98e69f2a35 100644 --- a/test/unit/miner-purge-cli.test.ts +++ b/test/unit/miner-purge-cli.test.ts @@ -16,6 +16,7 @@ import { initContributionProfileCache, closeDefaultContributionProfileCache, } from "../../packages/loopover-miner/lib/contribution-profile-cache.js"; +import { initPolicyVerdictCacheStore } from "../../packages/loopover-miner/lib/policy-verdict-cache.js"; import { openGovernorState } from "../../packages/loopover-miner/lib/governor-state.js"; import { emptyContributionProfile } from "../../packages/loopover-miner/lib/contribution-profile.js"; import { @@ -26,6 +27,7 @@ import { const roots: string[] = []; const closeables: Array<{ close(): void }> = []; +const POLICY_VERDICT = { allowed: true, matchedPhrase: null, source: "AI-USAGE.md" } as const; function tempDir() { const root = mkdtempSync(join(tmpdir(), "loopover-miner-purge-cli-")); @@ -83,7 +85,7 @@ describe("parsePurgeArgs (#5564)", () => { }); describe("runPurge --dry-run (#5564, #6599)", () => { - it("counts matching rows across the eight real stores without writing anything, and reports attempt-log as not-purgeable", async () => { + it("counts matching rows across the nine real stores without writing anything, and reports attempt-log as not-purgeable", async () => { const root = tempDir(); const claimDbPath = join(root, "claim-ledger.sqlite3"); const eventDbPath = join(root, "event-ledger.sqlite3"); @@ -92,6 +94,7 @@ describe("runPurge --dry-run (#5564, #6599)", () => { const portfolioDbPath = join(root, "portfolio-queue.sqlite3"); const runStateDbPath = join(root, "run-state.sqlite3"); const cacheDbPath = join(root, "contribution-profile-cache.sqlite3"); + const policyVerdictCacheDbPath = join(root, "policy-verdict-cache.sqlite3"); const governorStateDbPath = join(root, "governor-state.sqlite3"); const attemptLogDbPath = join(root, "attempt-log.sqlite3"); // never created — dry run must not touch it @@ -142,6 +145,11 @@ describe("runPurge --dry-run (#5564, #6599)", () => { cache.put(emptyContributionProfile("acme/other", "2026-07-17T00:00:00.000Z")); cache.close(); + const policyVerdictCache = initPolicyVerdictCacheStore(policyVerdictCacheDbPath); + policyVerdictCache.put("acme/widgets", "AI-USAGE.md", '"v1"', POLICY_VERDICT); + policyVerdictCache.put("acme/other", "AI-USAGE.md", '"v2"', POLICY_VERDICT); + policyVerdictCache.close(); + // governor-state's two repo-scoped tables. reputation history for acme/widgets is recorded under TWO // api_base_urls (both count for the repo, since the purge filters on repo_full_name alone) plus two own // submissions; the whole-run scalar row (governor_scalar_state) is not repo-scoped and never counted. @@ -161,6 +169,7 @@ describe("runPurge --dry-run (#5564, #6599)", () => { "portfolio-queue": () => portfolioDbPath, "run-state": () => runStateDbPath, "contribution-profile-cache": () => cacheDbPath, + "policy-verdict-cache": () => policyVerdictCacheDbPath, "governor-state": () => governorStateDbPath, "attempt-log": () => attemptLogDbPath, }; @@ -181,6 +190,7 @@ describe("runPurge --dry-run (#5564, #6599)", () => { { store: "contribution-profile-cache", wouldPurge: 1 }, // governor-state sums BOTH tables: 2 reputation rows (two api_base_urls) + 2 own submissions = 4. { store: "governor-state", wouldPurge: 4 }, + { store: "policy-verdict-cache", wouldPurge: 1 }, ], attemptLogNote: ATTEMPT_LOG_NOT_PURGEABLE_NOTE, attemptLogTotalRows: 0, @@ -211,13 +221,14 @@ describe("runPurge --dry-run (#5564, #6599)", () => { "portfolio-queue": () => join(root, "portfolio-queue.sqlite3"), "run-state": () => join(root, "run-state.sqlite3"), "contribution-profile-cache": () => join(root, "contribution-profile-cache.sqlite3"), + "policy-verdict-cache": () => join(root, "policy-verdict-cache.sqlite3"), "governor-state": () => join(root, "governor-state.sqlite3"), "attempt-log": () => join(root, "attempt-log.sqlite3"), }; const log = vi.spyOn(console, "log").mockImplementation(() => undefined); expect(runPurge(["--repo", "acme/widgets", "--dry-run", "--json"], { resolveDbPaths })).toBe(0); const result = JSON.parse(String(log.mock.calls[0]?.[0])); - expect(result.stores).toHaveLength(8); + expect(result.stores).toHaveLength(9); expect(result.stores.every((entry: { wouldPurge: number }) => entry.wouldPurge === 0)).toBe(true); expect(result.attemptLogTotalRows).toBe(0); for (const resolve of Object.values(resolveDbPaths)) { @@ -253,6 +264,7 @@ describe("runPurge --dry-run (#5564, #6599)", () => { "portfolio-queue": () => join(root, "portfolio-queue.sqlite3"), "run-state": () => join(root, "run-state.sqlite3"), "contribution-profile-cache": () => join(root, "contribution-profile-cache.sqlite3"), + "policy-verdict-cache": () => join(root, "policy-verdict-cache.sqlite3"), "governor-state": () => join(root, "governor-state.sqlite3"), "attempt-log": () => attemptLogDbPath, }; @@ -280,6 +292,7 @@ describe("runPurge --dry-run (#5564, #6599)", () => { "portfolio-queue": () => join(root, "portfolio-queue.sqlite3"), "run-state": () => join(root, "run-state.sqlite3"), "contribution-profile-cache": () => join(root, "contribution-profile-cache.sqlite3"), + "policy-verdict-cache": () => join(root, "policy-verdict-cache.sqlite3"), "governor-state": () => join(root, "governor-state.sqlite3"), "attempt-log": () => join(root, "attempt-log.sqlite3"), }; @@ -320,6 +333,7 @@ describe("runPurge --dry-run (#5564, #6599)", () => { LOOPOVER_MINER_PORTFOLIO_QUEUE_DB: process.env.LOOPOVER_MINER_PORTFOLIO_QUEUE_DB, LOOPOVER_MINER_RUN_STATE_DB: process.env.LOOPOVER_MINER_RUN_STATE_DB, LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB: process.env.LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB, + LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB: process.env.LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB, LOOPOVER_MINER_GOVERNOR_STATE_DB: process.env.LOOPOVER_MINER_GOVERNOR_STATE_DB, LOOPOVER_MINER_ATTEMPT_LOG_DB: process.env.LOOPOVER_MINER_ATTEMPT_LOG_DB, }; @@ -330,13 +344,14 @@ describe("runPurge --dry-run (#5564, #6599)", () => { process.env.LOOPOVER_MINER_PORTFOLIO_QUEUE_DB = join(root, "portfolio-queue.sqlite3"); process.env.LOOPOVER_MINER_RUN_STATE_DB = join(root, "run-state.sqlite3"); process.env.LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB = join(root, "contribution-profile-cache.sqlite3"); + process.env.LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB = join(root, "policy-verdict-cache.sqlite3"); process.env.LOOPOVER_MINER_GOVERNOR_STATE_DB = join(root, "governor-state.sqlite3"); process.env.LOOPOVER_MINER_ATTEMPT_LOG_DB = join(root, "attempt-log.sqlite3"); try { const log = vi.spyOn(console, "log").mockImplementation(() => undefined); expect(runPurge(["--repo", "acme/widgets", "--dry-run", "--json"])).toBe(0); const result = JSON.parse(String(log.mock.calls[0]?.[0])); - expect(result.stores).toHaveLength(8); + expect(result.stores).toHaveLength(9); expect(result.stores.every((entry: { wouldPurge: number }) => entry.wouldPurge === 0)).toBe(true); // Nothing was created — dry run against nonexistent default-path stores makes zero writes. expect(existsSync(process.env.LOOPOVER_MINER_CLAIM_LEDGER_DB)).toBe(false); @@ -374,6 +389,7 @@ describe("runPurge (real, #5564, #6599)", () => { initRunStateStore: () => runState, initContributionProfileCache: () => cache, openGovernorState: () => governorState, + initPolicyVerdictCacheStore: () => fakeStore(0), }; const log = vi.spyOn(console, "log").mockImplementation(() => undefined); @@ -392,6 +408,7 @@ describe("runPurge (real, #5564, #6599)", () => { { store: "run-state", purged: 1 }, { store: "contribution-profile-cache", purged: 1 }, { store: "governor-state", purged: 4 }, + { store: "policy-verdict-cache", purged: 0 }, { store: "attempt-log", purged: null, note: ATTEMPT_LOG_NOT_PURGEABLE_NOTE }, ], }); @@ -432,6 +449,7 @@ describe("runPurge (real, #5564, #6599)", () => { initRunStateStore: () => runState, initContributionProfileCache: () => fakeStore(0), openGovernorState: () => fakeStore(0), + initPolicyVerdictCacheStore: () => fakeStore(0), }; const log = vi.spyOn(console, "log").mockImplementation(() => undefined); @@ -467,6 +485,7 @@ describe("runPurge (real, #5564, #6599)", () => { initRunStateStore: () => fakeStore(0), initContributionProfileCache: () => fakeStore(0), openGovernorState: () => fakeStore(0), + initPolicyVerdictCacheStore: () => fakeStore(0), }; const log = vi.spyOn(console, "log").mockImplementation(() => undefined); expect(runPurge(["--repo", "acme/widgets", "--json"], options as never)).toBe(2); @@ -486,6 +505,7 @@ describe("runPurge (real, #5564, #6599)", () => { initRunStateStore: () => fakeStore(0), initContributionProfileCache: () => fakeStore(0), openGovernorState: () => fakeStore(0), + initPolicyVerdictCacheStore: () => fakeStore(0), }; const log = vi.spyOn(console, "log").mockImplementation(() => undefined); expect(runPurge(["--repo", "acme/widgets", "--json"], options as never)).toBe(2); @@ -506,6 +526,7 @@ describe("runPurge (real, #5564, #6599)", () => { LOOPOVER_MINER_PORTFOLIO_QUEUE_DB: process.env.LOOPOVER_MINER_PORTFOLIO_QUEUE_DB, LOOPOVER_MINER_RUN_STATE_DB: process.env.LOOPOVER_MINER_RUN_STATE_DB, LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB: process.env.LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB, + LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB: process.env.LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB, LOOPOVER_MINER_GOVERNOR_STATE_DB: process.env.LOOPOVER_MINER_GOVERNOR_STATE_DB, }; const claimDbPath = join(root, "claim-ledger.sqlite3"); @@ -518,6 +539,7 @@ describe("runPurge (real, #5564, #6599)", () => { process.env.LOOPOVER_MINER_PORTFOLIO_QUEUE_DB = portfolioDbPath; process.env.LOOPOVER_MINER_RUN_STATE_DB = runStateDbPath; process.env.LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB = join(root, "contribution-profile-cache.sqlite3"); + process.env.LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB = join(root, "policy-verdict-cache.sqlite3"); process.env.LOOPOVER_MINER_GOVERNOR_STATE_DB = join(root, "governor-state.sqlite3"); try { // Seed real rows via the default store paths before purging through them. @@ -584,6 +606,7 @@ describe("runPurge (real, #5564, #6599)", () => { "portfolio-queue": () => portfolioDbPath, "run-state": () => runStateDbPath, "contribution-profile-cache": () => join(root, "contribution-profile-cache.sqlite3"), + "policy-verdict-cache": () => join(root, "policy-verdict-cache.sqlite3"), "governor-state": () => join(root, "governor-state.sqlite3"), "attempt-log": () => join(root, "attempt-log.sqlite3"), }; @@ -608,6 +631,7 @@ describe("runPurge (real, #5564, #6599)", () => { initRunStateStore: () => runStateStore, initContributionProfileCache: () => fakeStore(0), openGovernorState: () => fakeStore(0), + initPolicyVerdictCacheStore: () => fakeStore(0), } as never), ).toBe(0); const purged = JSON.parse(String(log.mock.calls[0]?.[0])); @@ -654,6 +678,7 @@ describe("runPurge (real, #5564, #6599)", () => { initRunStateStore: () => fakeStore(0), initContributionProfileCache: () => cacheStore, openGovernorState: () => governorStore, + initPolicyVerdictCacheStore: () => fakeStore(0), } as never), ).toBe(0); const summary = JSON.parse(String(log.mock.calls[0]?.[0])); @@ -672,4 +697,36 @@ describe("runPurge (real, #5564, #6599)", () => { expect(governorStore.listRecentOwnSubmissions({ repoFullName: "acme/other" })).toHaveLength(1); expect(governorStore.loadPauseState()).toMatchObject({ paused: true, reason: "maintenance" }); }); + + it("REGRESSION (#6987): really deletes policy-verdict-cache rows for the repo, leaving other repos intact", () => { + const root = tempDir(); + const policyDbPath = join(root, "policy-verdict-cache.sqlite3"); + + const seeded = initPolicyVerdictCacheStore(policyDbPath); + seeded.put("acme/widgets", "AI-USAGE.md", '"v1"', POLICY_VERDICT); + seeded.put("acme/other", "AI-USAGE.md", '"v2"', POLICY_VERDICT); + seeded.close(); + + const policyStore = initPolicyVerdictCacheStore(policyDbPath); + closeables.push(policyStore); + + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + expect( + runPurge(["--repo", "acme/widgets", "--json"], { + openClaimLedger: () => fakeStore(0), + initEventLedger: () => fakeStore(0), + initGovernorLedger: () => fakeStore(0), + initPredictionLedger: () => fakeStore(0), + initPortfolioQueueStore: () => fakeStore(0), + initRunStateStore: () => fakeStore(0), + initContributionProfileCache: () => fakeStore(0), + openGovernorState: () => fakeStore(0), + initPolicyVerdictCacheStore: () => policyStore, + } as never), + ).toBe(0); + const summary = JSON.parse(String(log.mock.calls[0]?.[0])); + expect(summary.stores).toContainEqual({ store: "policy-verdict-cache", purged: 1 }); + expect(policyStore.get("acme/widgets")).toBeNull(); + expect(policyStore.get("acme/other")).not.toBeNull(); + }); }); diff --git a/test/unit/miner-status.test.ts b/test/unit/miner-status.test.ts index 21821cb0f5..6898013096 100644 --- a/test/unit/miner-status.test.ts +++ b/test/unit/miner-status.test.ts @@ -138,6 +138,7 @@ describe("loopover-miner status/doctor (#2288)", () => { "store-integrity:replay-snapshot", "store-integrity:worktree-allocator", "store-integrity:contribution-profile", + "store-integrity:policy-verdict-cache", ]); // REGRESSION (#6768): doctor previously omitted these four durable local stores from the integrity sweep. expect(checks.map((check) => check.name)).toEqual(