diff --git a/apps/loopover-ui/src/lib/ams-env-reference.ts b/apps/loopover-ui/src/lib/ams-env-reference.ts index 10389b1a6..c940b04ca 100644 --- a/apps/loopover-ui/src/lib/ams-env-reference.ts +++ b/apps/loopover-ui/src/lib/ams-env-reference.ts @@ -38,7 +38,7 @@ export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [ }, { name: "LOOPOVER_MINER_CONFIG_DIR", - firstReference: "lib/deny-hook-synthesis.ts", + firstReference: "lib/laptop-init.ts", defaultValue: "", }, { @@ -64,7 +64,7 @@ export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [ { name: "LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB", firstReference: "lib/deny-hook-synthesis.ts", - defaultValue: "", + defaultValue: null, }, { name: "LOOPOVER_MINER_DISABLE_EGRESS_FIREWALL", @@ -106,6 +106,11 @@ export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [ firstReference: "lib/governor-state.ts", defaultValue: null, }, + { + name: "LOOPOVER_MINER_LAPTOP_STATE_DB", + firstReference: "lib/laptop-init.ts", + defaultValue: null, + }, { name: "LOOPOVER_MINER_LOG_LEVEL", firstReference: "lib/logger.ts", @@ -134,7 +139,7 @@ export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [ { name: "LOOPOVER_MINER_ORB_EXPORT_DB", firstReference: "lib/orb-export.ts", - defaultValue: "", + defaultValue: null, }, { name: "LOOPOVER_MINER_PLAN_STORE_DB", @@ -241,12 +246,12 @@ export const AMS_ENV_REFERENCE_MARKDOWN = [ "| `LOOPOVER_MINER_AMS_POLICY_PATH` | `lib/ams-policy.ts` | (none) |", "| `LOOPOVER_MINER_ATTEMPT_LOG_DB` | `lib/attempt-log.ts` | (none) |", "| `LOOPOVER_MINER_CLAIM_LEDGER_DB` | `lib/claim-ledger.ts` | (none) |", - '| `LOOPOVER_MINER_CONFIG_DIR` | `lib/deny-hook-synthesis.ts` | `""` |', + '| `LOOPOVER_MINER_CONFIG_DIR` | `lib/laptop-init.ts` | `""` |', "| `LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB` | `lib/contribution-profile-cache.ts` | (none) |", '| `LOOPOVER_MINER_CONTROL_PLANE` | `lib/tenant-client.ts` | `""` |', '| `LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN` | `lib/tenant-client.ts` | `""` |', '| `LOOPOVER_MINER_CONTROL_PLANE_URL` | `lib/tenant-client.ts` | `""` |', - '| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.ts` | `""` |', + "| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.ts` | (none) |", "| `LOOPOVER_MINER_DISABLE_EGRESS_FIREWALL` | `lib/generate-egress-firewall-config.ts` | (none) |", '| `LOOPOVER_MINER_DISCOVERY_INDEX_URL` | `lib/discovery-index-client.ts` | `""` |', '| `LOOPOVER_MINER_DISCOVERY_PLANE` | `lib/discovery-index-client.ts` | `""` |', @@ -255,12 +260,13 @@ export const AMS_ENV_REFERENCE_MARKDOWN = [ "| `LOOPOVER_MINER_EVENT_LEDGER_DB` | `lib/event-ledger.ts` | (none) |", "| `LOOPOVER_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.ts` | (none) |", "| `LOOPOVER_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.ts` | (none) |", + "| `LOOPOVER_MINER_LAPTOP_STATE_DB` | `lib/laptop-init.ts` | (none) |", '| `LOOPOVER_MINER_LOG_LEVEL` | `lib/logger.ts` | `""` |', "| `LOOPOVER_MINER_NEON_API_KEY` | `lib/egress-allowlist.ts` | (none) |", "| `LOOPOVER_MINER_NEON_PARENT_BRANCH_ID` | `lib/egress-allowlist.ts` | (none) |", "| `LOOPOVER_MINER_NEON_PROJECT_ID` | `lib/egress-allowlist.ts` | (none) |", '| `LOOPOVER_MINER_NO_UPDATE_CHECK` | `lib/update-check.ts` | `""` |', - '| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.ts` | `""` |', + "| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.ts` | (none) |", "| `LOOPOVER_MINER_PLAN_STORE_DB` | `lib/plan-store.ts` | (none) |", "| `LOOPOVER_MINER_POLICY_DOC_CACHE_DB` | `lib/policy-doc-cache.ts` | (none) |", "| `LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB` | `lib/policy-verdict-cache.ts` | (none) |", diff --git a/packages/loopover-engine/README.md b/packages/loopover-engine/README.md index ff389a4ce..44bad975a 100644 --- a/packages/loopover-engine/README.md +++ b/packages/loopover-engine/README.md @@ -41,6 +41,12 @@ have a root test under `test/unit/` (e.g. `test/unit/reviewer-consensus-calibrat `src/miner/loop-reentry-policy.ts` — #8347). The package-local `node:test` suite remains the package's own gate; the root mirror is what makes the same scenarios gradeable by Codecov. +Miner path-resolution refactors that only touch `packages/loopover-miner/lib/**` (e.g. delegating +`resolve*DbPath` to `resolveLocalStoreDbPath` — #8336) also keep this README line so CI's `engine` +path filter trips and validate-tests runs the **unscoped** suite: scoped `--changed` + +`--coverage.all=false` has uploaded empty patch coverage for those miner lib lines even when the +matching `test/unit/miner-*.test.ts` files passed (#8496). + ## `opportunity-ranker` The Phase-1 miner-discovery ranker. It composes five already-normalized `[0, 1]` signals into one ordinal score: diff --git a/packages/loopover-miner/docs/env-reference.md b/packages/loopover-miner/docs/env-reference.md index 507bcfa58..794869abd 100644 --- a/packages/loopover-miner/docs/env-reference.md +++ b/packages/loopover-miner/docs/env-reference.md @@ -10,12 +10,12 @@ Generated by `npm run miner:env-reference`. Do not edit manually. | `LOOPOVER_MINER_AMS_POLICY_PATH` | `lib/ams-policy.ts` | (none) | | `LOOPOVER_MINER_ATTEMPT_LOG_DB` | `lib/attempt-log.ts` | (none) | | `LOOPOVER_MINER_CLAIM_LEDGER_DB` | `lib/claim-ledger.ts` | (none) | -| `LOOPOVER_MINER_CONFIG_DIR` | `lib/deny-hook-synthesis.ts` | `""` | +| `LOOPOVER_MINER_CONFIG_DIR` | `lib/laptop-init.ts` | `""` | | `LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB` | `lib/contribution-profile-cache.ts` | (none) | | `LOOPOVER_MINER_CONTROL_PLANE` | `lib/tenant-client.ts` | `""` | | `LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN` | `lib/tenant-client.ts` | `""` | | `LOOPOVER_MINER_CONTROL_PLANE_URL` | `lib/tenant-client.ts` | `""` | -| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.ts` | `""` | +| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.ts` | (none) | | `LOOPOVER_MINER_DISABLE_EGRESS_FIREWALL` | `lib/generate-egress-firewall-config.ts` | (none) | | `LOOPOVER_MINER_DISCOVERY_INDEX_URL` | `lib/discovery-index-client.ts` | `""` | | `LOOPOVER_MINER_DISCOVERY_PLANE` | `lib/discovery-index-client.ts` | `""` | @@ -24,12 +24,13 @@ Generated by `npm run miner:env-reference`. Do not edit manually. | `LOOPOVER_MINER_EVENT_LEDGER_DB` | `lib/event-ledger.ts` | (none) | | `LOOPOVER_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.ts` | (none) | | `LOOPOVER_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.ts` | (none) | +| `LOOPOVER_MINER_LAPTOP_STATE_DB` | `lib/laptop-init.ts` | (none) | | `LOOPOVER_MINER_LOG_LEVEL` | `lib/logger.ts` | `""` | | `LOOPOVER_MINER_NEON_API_KEY` | `lib/egress-allowlist.ts` | (none) | | `LOOPOVER_MINER_NEON_PARENT_BRANCH_ID` | `lib/egress-allowlist.ts` | (none) | | `LOOPOVER_MINER_NEON_PROJECT_ID` | `lib/egress-allowlist.ts` | (none) | | `LOOPOVER_MINER_NO_UPDATE_CHECK` | `lib/update-check.ts` | `""` | -| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.ts` | `""` | +| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.ts` | (none) | | `LOOPOVER_MINER_PLAN_STORE_DB` | `lib/plan-store.ts` | (none) | | `LOOPOVER_MINER_POLICY_DOC_CACHE_DB` | `lib/policy-doc-cache.ts` | (none) | | `LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB` | `lib/policy-verdict-cache.ts` | (none) | diff --git a/packages/loopover-miner/lib/deny-hook-synthesis.ts b/packages/loopover-miner/lib/deny-hook-synthesis.ts index 4033a18b2..8fa520540 100644 --- a/packages/loopover-miner/lib/deny-hook-synthesis.ts +++ b/packages/loopover-miner/lib/deny-hook-synthesis.ts @@ -4,8 +4,7 @@ // refresh + maintainer review before any synthesized rule takes effect. Approved rules merge with // {@link DEFAULT_DENY_RULES}; unapproved proposals never block tool calls. No behavior change. import { chmodSync, mkdirSync } from "node:fs"; -import { homedir } from "node:os"; -import { dirname, join } from "node:path"; +import { dirname } from "node:path"; import { DatabaseSync } from "node:sqlite"; import { aggregateBlockerHistory, @@ -24,6 +23,7 @@ import { import type { DenyRuleProposal, SynthesisConfig } from "@loopover/engine"; import { DEFAULT_FORGE_CONFIG } from "./forge-config.js"; import type { DenyRule } from "./deny-hooks.js"; +import { resolveLocalStoreDbPath } from "./local-store.js"; import { DENY_HOOK_SYNTHESIS_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js"; // Re-export the pure synthesis helpers from the engine so this module's public API is unchanged after #5667 @@ -86,20 +86,7 @@ function normalizeApiBaseUrl(apiBaseUrl?: string): string { } export function resolveDenyHookSynthesisDbPath(env: Record = process.env): string { - const explicitPath = typeof env.LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB === "string" - ? env.LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB.trim() - : ""; - if (explicitPath) return explicitPath; - - const explicitConfigDir = typeof env.LOOPOVER_MINER_CONFIG_DIR === "string" - ? env.LOOPOVER_MINER_CONFIG_DIR.trim() - : ""; - if (explicitConfigDir) return join(explicitConfigDir, defaultDbFileName); - - const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim() - ? env.XDG_CONFIG_HOME.trim() - : join(homedir(), ".config"); - return join(configHome, "loopover-miner", defaultDbFileName); + return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB", env); } // `dbPath` is always a real string here: this function's only caller (initDenyHookSynthesisStore) already diff --git a/packages/loopover-miner/lib/laptop-init.ts b/packages/loopover-miner/lib/laptop-init.ts index 17f7c24b8..dae99ab47 100644 --- a/packages/loopover-miner/lib/laptop-init.ts +++ b/packages/loopover-miner/lib/laptop-init.ts @@ -5,6 +5,7 @@ import { DatabaseSync } from "node:sqlite"; import { applySchemaMigrations } from "./schema-version.js"; import { reportCliFailure } from "./cli-error.js"; import { resolveGitHubToken } from "./github-token-resolution.js"; +import { resolveLocalStoreDbPath } from "./local-store.js"; const githubApiBaseUrl = "https://api.github.com"; const githubApiVersion = "2022-11-28"; @@ -43,9 +44,9 @@ function resolveMinerStateDir(env: Record = process. return join(configHome, "loopover-miner"); } -/** Path to the laptop-mode SQLite bootstrap file inside the miner state directory. */ +/** Path to the laptop-mode SQLite bootstrap file (shared local-store resolution + optional explicit override). */ export function resolveLaptopStateDbPath(env: Record = process.env): string { - return join(resolveMinerStateDir(env), defaultDbFileName); + return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_LAPTOP_STATE_DB", env); } /** Create the state dir and SQLite file. Re-running is idempotent and never clobbers existing rows. */ diff --git a/packages/loopover-miner/lib/orb-export.ts b/packages/loopover-miner/lib/orb-export.ts index 911b78c31..98138c164 100644 --- a/packages/loopover-miner/lib/orb-export.ts +++ b/packages/loopover-miner/lib/orb-export.ts @@ -1,6 +1,5 @@ import { chmodSync, mkdirSync } from "node:fs"; -import { homedir } from "node:os"; -import { dirname, join } from "node:path"; +import { dirname } from "node:path"; import { DatabaseSync } from "node:sqlite"; import { createHash, createHmac } from "node:crypto"; import { generateAnonSecret, hmacAnonymize as engineHmacAnonymize } from "@loopover/engine"; @@ -8,6 +7,7 @@ import { readPrOutcomes } from "./pr-outcome.js"; import type { NormalizedPrOutcomePayload, PrOutcomeLedgerReader } from "./pr-outcome.js"; import { initEventLedger } from "./event-ledger.js"; import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; +import { resolveLocalStoreDbPath } from "./local-store.js"; // Optional anonymized Orb telemetry export (#4277, network send wired in #5681). The self-host Orb collector // (src/selfhost/orb-collector.ts, #1255) is ALWAYS-ON for a maintainer's own instance; a miner runs on a @@ -52,19 +52,7 @@ const CURSOR_KEY = "export_cursor"; const defaultDbFileName = "orb-export.sqlite3"; export function resolveOrbExportDbPath(env: Record = process.env): string { - const explicitPath = - typeof env.LOOPOVER_MINER_ORB_EXPORT_DB === "string" ? env.LOOPOVER_MINER_ORB_EXPORT_DB.trim() : ""; - if (explicitPath) return explicitPath; - - const explicitConfigDir = - typeof env.LOOPOVER_MINER_CONFIG_DIR === "string" ? env.LOOPOVER_MINER_CONFIG_DIR.trim() : ""; - if (explicitConfigDir) return join(explicitConfigDir, defaultDbFileName); - - const configHome = - typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim() - ? env.XDG_CONFIG_HOME.trim() - : join(homedir(), ".config"); - return join(configHome, "loopover-miner", defaultDbFileName); + return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_ORB_EXPORT_DB", env); } // `dbPath` is always a real string here: this function's only caller (openOrbExportStore) already defaults its diff --git a/test/unit/miner-deny-hook-synthesis.test.ts b/test/unit/miner-deny-hook-synthesis.test.ts index 9da4426fd..26f443285 100644 --- a/test/unit/miner-deny-hook-synthesis.test.ts +++ b/test/unit/miner-deny-hook-synthesis.test.ts @@ -1,5 +1,5 @@ import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { DatabaseSync } from "node:sqlite"; import { afterEach, describe, expect, it } from "vitest"; @@ -21,6 +21,7 @@ import type { DenyRuleProposal } from "../../packages/loopover-engine/src/miner/ // #7525: normalizeRepoFullName is defined in the engine and re-exported unchanged by the miner-lib module // above; import it from the engine source directly so the guard's src branches are the ones exercised. import { normalizeRepoFullName } from "../../packages/loopover-engine/src/miner/deny-hook-synthesis"; +import { resolveLocalStoreDbPath } from "../../packages/loopover-miner/lib/local-store.js"; const tempDirs: string[] = []; const stores: Array<{ close(): void }> = []; @@ -60,12 +61,28 @@ describe("resolveDenyHookSynthesisDbPath() (#4522)", () => { "/custom/d.sqlite3", ); expect(resolveDenyHookSynthesisDbPath({ LOOPOVER_MINER_CONFIG_DIR: "/custom/config" })).toBe( - "/custom/config/deny-hook-synthesis.sqlite3", + join("/custom/config", "deny-hook-synthesis.sqlite3"), ); expect(resolveDenyHookSynthesisDbPath({ XDG_CONFIG_HOME: "/xdg" })).toBe( - "/xdg/loopover-miner/deny-hook-synthesis.sqlite3", + join("/xdg", "loopover-miner", "deny-hook-synthesis.sqlite3"), ); - expect(resolveDenyHookSynthesisDbPath({})).toMatch(/\/\.config\/loopover-miner\/deny-hook-synthesis\.sqlite3$/); + expect(resolveDenyHookSynthesisDbPath({})).toBe( + join(homedir(), ".config", "loopover-miner", "deny-hook-synthesis.sqlite3"), + ); + }); + + it("stays byte-identical to resolveLocalStoreDbPath for representative env combinations (#8336)", () => { + const cases: Array> = [ + { LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB: "/custom/d.sqlite3" }, + { LOOPOVER_MINER_CONFIG_DIR: "/custom/config" }, + { XDG_CONFIG_HOME: "/xdg" }, + {}, + ]; + for (const env of cases) { + expect(resolveDenyHookSynthesisDbPath(env)).toBe( + resolveLocalStoreDbPath("deny-hook-synthesis.sqlite3", "LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB", env), + ); + } }); }); diff --git a/test/unit/miner-laptop-init.test.ts b/test/unit/miner-laptop-init.test.ts index 41b17f720..e769c9cb0 100644 --- a/test/unit/miner-laptop-init.test.ts +++ b/test/unit/miner-laptop-init.test.ts @@ -54,15 +54,23 @@ afterEach(() => { describe("loopover-miner laptop init (#2329)", () => { it("resolves the laptop SQLite path from the state-dir override and XDG fallback", () => { expect(resolveLaptopStateDbPath({ LOOPOVER_MINER_CONFIG_DIR: "/custom/state" })) - .toBe("/custom/state/laptop-state.sqlite3"); + .toBe(join("/custom/state", "laptop-state.sqlite3")); expect(resolveLaptopStateDbPath({ XDG_CONFIG_HOME: "/xdg" })) - .toBe("/xdg/loopover-miner/laptop-state.sqlite3"); + .toBe(join("/xdg", "loopover-miner", "laptop-state.sqlite3")); }); it("falls back to ~/.config when neither LOOPOVER_MINER_CONFIG_DIR nor XDG_CONFIG_HOME is set", () => { expect(resolveLaptopStateDbPath({})).toBe(join(homedir(), ".config", "loopover-miner", "laptop-state.sqlite3")); }); + it("honors LOOPOVER_MINER_LAPTOP_STATE_DB over config-dir and XDG fallbacks (#8336)", () => { + expect(resolveLaptopStateDbPath({ + LOOPOVER_MINER_LAPTOP_STATE_DB: "/custom/laptop-state.sqlite3", + LOOPOVER_MINER_CONFIG_DIR: "/custom/state", + XDG_CONFIG_HOME: "/xdg", + })).toBe("/custom/laptop-state.sqlite3"); + }); + it("fresh init creates the state dir and SQLite file", () => { const root = tempRoot(); const env = { LOOPOVER_MINER_CONFIG_DIR: join(root, "state") }; diff --git a/test/unit/miner-orb-export.test.ts b/test/unit/miner-orb-export.test.ts index f41e708e1..08dcc7603 100644 --- a/test/unit/miner-orb-export.test.ts +++ b/test/unit/miner-orb-export.test.ts @@ -1,5 +1,5 @@ import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -19,6 +19,7 @@ import { sendAmsExportBatch, } from "../../packages/loopover-miner/lib/orb-export.js"; import type { OrbExportOutcome, OrbExportRow } from "../../packages/loopover-miner/lib/orb-export.js"; +import { resolveLocalStoreDbPath } from "../../packages/loopover-miner/lib/local-store.js"; let dir: string; function storePath() { @@ -210,10 +211,28 @@ describe("resolveOrbExportDbPath (#4277)", () => { it("resolves the DB path from env override, miner config dir, XDG config, then the home default", () => { expect(resolveOrbExportDbPath({ LOOPOVER_MINER_ORB_EXPORT_DB: "/custom/d.sqlite3" })).toBe("/custom/d.sqlite3"); expect(resolveOrbExportDbPath({ LOOPOVER_MINER_CONFIG_DIR: "/custom/config" })).toBe( - "/custom/config/orb-export.sqlite3", + join("/custom/config", "orb-export.sqlite3"), ); - expect(resolveOrbExportDbPath({ XDG_CONFIG_HOME: "/xdg" })).toBe("/xdg/loopover-miner/orb-export.sqlite3"); - expect(resolveOrbExportDbPath({})).toMatch(/\/\.config\/loopover-miner\/orb-export\.sqlite3$/); + expect(resolveOrbExportDbPath({ XDG_CONFIG_HOME: "/xdg" })).toBe( + join("/xdg", "loopover-miner", "orb-export.sqlite3"), + ); + expect(resolveOrbExportDbPath({})).toBe( + join(homedir(), ".config", "loopover-miner", "orb-export.sqlite3"), + ); + }); + + it("stays byte-identical to resolveLocalStoreDbPath for representative env combinations (#8336)", () => { + const cases: Array> = [ + { LOOPOVER_MINER_ORB_EXPORT_DB: "/custom/d.sqlite3" }, + { LOOPOVER_MINER_CONFIG_DIR: "/custom/config" }, + { XDG_CONFIG_HOME: "/xdg" }, + {}, + ]; + for (const env of cases) { + expect(resolveOrbExportDbPath(env)).toBe( + resolveLocalStoreDbPath("orb-export.sqlite3", "LOOPOVER_MINER_ORB_EXPORT_DB", env), + ); + } }); });