diff --git a/packages/gittensory-miner/lib/claim-ledger.d.ts b/packages/gittensory-miner/lib/claim-ledger.d.ts index 15ec048874..206eb15a18 100644 --- a/packages/gittensory-miner/lib/claim-ledger.d.ts +++ b/packages/gittensory-miner/lib/claim-ledger.d.ts @@ -23,9 +23,11 @@ export type ListClaimsFilter = { export type ClaimLedger = { dbPath: string; recordClaim(claim: RecordClaimInput): ClaimEntry; + claimIssue(repoFullName: string, issueNumber: number, note?: string): ClaimEntry; releaseClaim(repoFullName: string, issueNumber: number): ClaimEntry | null; expireClaim(repoFullName: string, issueNumber: number): ClaimEntry | null; listClaims(filter?: ListClaimsFilter): ClaimEntry[]; + listActiveClaims(repoFullName?: string): ClaimEntry[]; close(): void; }; @@ -43,4 +45,8 @@ export function expireClaim(repoFullName: string, issueNumber: number): ClaimEnt export function listClaims(filter?: ListClaimsFilter): ClaimEntry[]; +export function claimIssue(repoFullName: string, issueNumber: number, note?: string): ClaimEntry; + +export function listActiveClaims(repoFullName?: string): ClaimEntry[]; + export function closeDefaultClaimLedger(): void; diff --git a/packages/gittensory-miner/lib/claim-ledger.js b/packages/gittensory-miner/lib/claim-ledger.js index dfdf77f5e0..7e4a496da3 100644 --- a/packages/gittensory-miner/lib/claim-ledger.js +++ b/packages/gittensory-miner/lib/claim-ledger.js @@ -136,7 +136,7 @@ export function openClaimLedger(dbPath = resolveClaimLedgerDbPath()) { return status; } - return { + const ledger = { dbPath: resolvedPath, recordClaim(claim) { const repoFullName = normalizeRepoFullName(claim?.repoFullName); @@ -178,10 +178,19 @@ export function openClaimLedger(dbPath = resolveClaimLedgerDbPath()) { } return rows.map(rowToClaim); }, + claimIssue(repoFullName, issueNumber, note) { + return ledger.recordClaim({ repoFullName, issueNumber, note }); + }, + listActiveClaims(repoFullName) { + const filter = { status: "active" }; + if (repoFullName !== undefined) filter.repoFullName = repoFullName; + return ledger.listClaims(filter); + }, close() { db.close(); }, }; + return ledger; } function getDefaultClaimLedger() { @@ -205,6 +214,16 @@ export function listClaims(filter) { return getDefaultClaimLedger().listClaims(filter); } +/** Foundation-phase alias for `recordClaim({ repoFullName, issueNumber, note })`. (#3351) */ +export function claimIssue(repoFullName, issueNumber, note) { + return getDefaultClaimLedger().claimIssue(repoFullName, issueNumber, note); +} + +/** List only `active` claims, optionally scoped to one repo. (#3351) */ +export function listActiveClaims(repoFullName) { + return getDefaultClaimLedger().listActiveClaims(repoFullName); +} + export function closeDefaultClaimLedger() { if (!defaultClaimLedger) return; defaultClaimLedger.close(); diff --git a/test/unit/miner-claim-ledger.test.ts b/test/unit/miner-claim-ledger.test.ts index dbe5b36942..07bb9c6457 100644 --- a/test/unit/miner-claim-ledger.test.ts +++ b/test/unit/miner-claim-ledger.test.ts @@ -4,7 +4,9 @@ import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { CLAIM_STATUSES, + claimIssue, closeDefaultClaimLedger, + listActiveClaims, openClaimLedger, resolveClaimLedgerDbPath, } from "../../packages/gittensory-miner/lib/claim-ledger.js"; @@ -23,6 +25,8 @@ function tempLedger() { afterEach(() => { for (const ledger of ledgers.splice(0)) ledger.close(); closeDefaultClaimLedger(); + vi.restoreAllMocks(); + vi.unstubAllEnvs(); vi.useRealTimers(); for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); @@ -136,4 +140,45 @@ describe("gittensory-miner claim ledger (#2314)", () => { expect(source).toContain("does NOT adjudicate contested duplicates"); expect(source).toContain("isDuplicateClusterWinnerByClaim"); }); + + it("claimIssue and listActiveClaims expose the foundation-phase API surface (#3351)", () => { + const ledger = tempLedger(); + const claim = ledger.claimIssue("o/a", 42, "via-alias"); + expect(claim).toMatchObject({ repoFullName: "o/a", issueNumber: 42, status: "active", note: "via-alias" }); + expect(ledger.listActiveClaims()).toEqual([claim]); + ledger.claimIssue("o/b", 1); + ledger.releaseClaim("o/a", 42); + expect(ledger.listActiveClaims("o/a")).toEqual([]); + expect(ledger.listActiveClaims("o/b").map((c) => c.issueNumber)).toEqual([1]); + expect(ledger.listActiveClaims().map((c) => c.repoFullName)).toEqual(["o/b"]); + expect(ledger.claimIssue("o/c", 3).note).toBeNull(); + }); + + it("claimIssue on an already-active claim is idempotent (#3353)", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-07-05T00:00:00Z")); + const ledger = tempLedger(); + const first = ledger.claimIssue("o/a", 7, "first"); + vi.setSystemTime(new Date("2026-07-05T01:00:00Z")); + const second = ledger.claimIssue("o/a", 7, "ignored"); + expect(second).toEqual(first); + expect(ledger.listActiveClaims()).toHaveLength(1); + }); + + it("top-level claimIssue and listActiveClaims use the default ledger store", () => { + const root = tempRoot(); + vi.stubEnv("GITTENSORY_MINER_CLAIM_LEDGER_DB", join(root, "claim-ledger.sqlite3")); + closeDefaultClaimLedger(); + const claim = claimIssue("o/a", 99, "default-store"); + expect(claim).toMatchObject({ issueNumber: 99, status: "active" }); + expect(listActiveClaims()).toEqual([claim]); + expect(listActiveClaims("o/a")).toEqual([claim]); + expect(listActiveClaims("o/missing")).toEqual([]); + }); }); + +function tempRoot() { + const root = mkdtempSync(join(tmpdir(), "gittensory-miner-claim-default-")); + roots.push(root); + return root; +}