diff --git a/src/db/repo-identity-rename.ts b/src/db/repo-identity-rename.ts index 9814311585..f8178e7bb1 100644 --- a/src/db/repo-identity-rename.ts +++ b/src/db/repo-identity-rename.ts @@ -23,9 +23,23 @@ // types don't generalize cleanly across tables with different secondary keys, and this codebase's own // convention (repositories.ts) is explicit per-table queries throughout, not a shared query abstraction. // New tables extend this function directly, following the same shape. -import { and, eq, inArray, sql } from "drizzle-orm"; +import { and, eq, inArray, isNull, sql } from "drizzle-orm"; import { getDb } from "./client"; -import { auditEvents, issues, pullRequests, repositories, repositorySettings } from "./schema"; +import { + activeReviewTracking, + advisories, + auditEvents, + checkSummaries, + gateOutcomes, + issues, + pullRequestDetailSyncState, + pullRequestFiles, + pullRequestReviews, + pullRequests, + recentMergedPullRequests, + repositories, + repositorySettings, +} from "./schema"; function repoParts(fullName: string): { owner: string; name: string } { const slash = fullName.indexOf("/"); @@ -92,6 +106,133 @@ export async function renameRepositoryIdentity(env: Env, oldFullName: string, ne }) .where(eq(issues.repoFullName, oldFullName)); + // gateOutcomes: unique (repo_full_name, pull_number) -- same fold-then-rename shape as pullRequests/issues. + const collidingGateOutcomePulls = ( + await db.select({ pullNumber: gateOutcomes.pullNumber }).from(gateOutcomes).where(eq(gateOutcomes.repoFullName, oldFullName)) + ).map((row) => row.pullNumber); + if (collidingGateOutcomePulls.length > 0) { + await db.delete(gateOutcomes).where(and(eq(gateOutcomes.repoFullName, newFullName), inArray(gateOutcomes.pullNumber, collidingGateOutcomePulls))); + } + await db + .update(gateOutcomes) + .set({ repoFullName: newFullName, id: sql`replace(${gateOutcomes.id}, ${oldFullName}, ${newFullName})` }) + .where(eq(gateOutcomes.repoFullName, oldFullName)); + + // activeReviewTracking: unique (repo_full_name, pull_number) -- same shape. + const collidingActiveReviewPulls = ( + await db.select({ pullNumber: activeReviewTracking.pullNumber }).from(activeReviewTracking).where(eq(activeReviewTracking.repoFullName, oldFullName)) + ).map((row) => row.pullNumber); + if (collidingActiveReviewPulls.length > 0) { + await db + .delete(activeReviewTracking) + .where(and(eq(activeReviewTracking.repoFullName, newFullName), inArray(activeReviewTracking.pullNumber, collidingActiveReviewPulls))); + } + await db + .update(activeReviewTracking) + .set({ repoFullName: newFullName, id: sql`replace(${activeReviewTracking.id}, ${oldFullName}, ${newFullName})` }) + .where(eq(activeReviewTracking.repoFullName, oldFullName)); + + // pullRequestDetailSyncState: unique (repo_full_name, pull_number) -- same shape. + const collidingSyncStatePulls = ( + await db + .select({ pullNumber: pullRequestDetailSyncState.pullNumber }) + .from(pullRequestDetailSyncState) + .where(eq(pullRequestDetailSyncState.repoFullName, oldFullName)) + ).map((row) => row.pullNumber); + if (collidingSyncStatePulls.length > 0) { + await db + .delete(pullRequestDetailSyncState) + .where(and(eq(pullRequestDetailSyncState.repoFullName, newFullName), inArray(pullRequestDetailSyncState.pullNumber, collidingSyncStatePulls))); + } + await db + .update(pullRequestDetailSyncState) + .set({ repoFullName: newFullName, id: sql`replace(${pullRequestDetailSyncState.id}, ${oldFullName}, ${newFullName})` }) + .where(eq(pullRequestDetailSyncState.repoFullName, oldFullName)); + + // recentMergedPullRequests: unique (repo_full_name, number) -- same shape as pullRequests. + const collidingRecentMergedNumbers = ( + await db.select({ number: recentMergedPullRequests.number }).from(recentMergedPullRequests).where(eq(recentMergedPullRequests.repoFullName, oldFullName)) + ).map((row) => row.number); + if (collidingRecentMergedNumbers.length > 0) { + await db + .delete(recentMergedPullRequests) + .where(and(eq(recentMergedPullRequests.repoFullName, newFullName), inArray(recentMergedPullRequests.number, collidingRecentMergedNumbers))); + } + await db + .update(recentMergedPullRequests) + .set({ + repoFullName: newFullName, + id: sql`replace(${recentMergedPullRequests.id}, ${oldFullName}, ${newFullName})`, + htmlUrl: sql`replace(${recentMergedPullRequests.htmlUrl}, ${oldFullName}, ${newFullName})`, + }) + .where(eq(recentMergedPullRequests.repoFullName, oldFullName)); + + // pullRequestFiles: unique (repo_full_name, pull_number, path) -- a 3-column key, so the collision check + // is per-(pullNumber, path) PAIR rather than a single-column inArray. Row counts here are small (a + // rename is a rare, one-time event; a PR's file list is bounded), so one scoped delete per pair is simple + // and dialect-portable rather than reaching for a raw composite-tuple IN clause. + const collidingFileKeys = await db + .select({ pullNumber: pullRequestFiles.pullNumber, path: pullRequestFiles.path }) + .from(pullRequestFiles) + .where(eq(pullRequestFiles.repoFullName, oldFullName)); + for (const key of collidingFileKeys) { + await db + .delete(pullRequestFiles) + .where(and(eq(pullRequestFiles.repoFullName, newFullName), eq(pullRequestFiles.pullNumber, key.pullNumber), eq(pullRequestFiles.path, key.path))); + } + await db + .update(pullRequestFiles) + .set({ repoFullName: newFullName, id: sql`replace(${pullRequestFiles.id}, ${oldFullName}, ${newFullName})` }) + .where(eq(pullRequestFiles.repoFullName, oldFullName)); + + // checkSummaries: unique (repo_full_name, head_sha, name) -- same per-pair fold as pullRequestFiles above, + // but head_sha is nullable, so the collision lookup branches on isNull vs eq per row instead of a single + // eq() (SQL NULL never equals NULL via `=`). + const collidingCheckKeys = await db + .select({ headSha: checkSummaries.headSha, name: checkSummaries.name }) + .from(checkSummaries) + .where(eq(checkSummaries.repoFullName, oldFullName)); + for (const key of collidingCheckKeys) { + await db + .delete(checkSummaries) + .where( + and( + eq(checkSummaries.repoFullName, newFullName), + key.headSha === null ? isNull(checkSummaries.headSha) : eq(checkSummaries.headSha, key.headSha), + eq(checkSummaries.name, key.name), + ), + ); + } + await db + .update(checkSummaries) + .set({ repoFullName: newFullName, id: sql`replace(${checkSummaries.id}, ${oldFullName}, ${newFullName})` }) + .where(eq(checkSummaries.repoFullName, oldFullName)); + + // pullRequestReviews: no separate unique index (PK `id` alone) -- id is `${repoFullName}#${pullNumber}# + // ${githubReviewId}` (github/backfill.ts), so the fold checks for a PK collision on the id the rename + // would PRODUCE rather than a business-key tuple. GitHub review ids are globally unique, so this never + // fires in practice; kept for defensive correctness rather than assuming that invariant holds forever. + const oldReviewIds = ( + await db.select({ id: pullRequestReviews.id }).from(pullRequestReviews).where(eq(pullRequestReviews.repoFullName, oldFullName)) + ).map((row) => row.id); + const renamedReviewIds = oldReviewIds.map((id) => id.split(oldFullName).join(newFullName)); + if (renamedReviewIds.length > 0) { + await db.delete(pullRequestReviews).where(inArray(pullRequestReviews.id, renamedReviewIds)); + } + await db + .update(pullRequestReviews) + .set({ repoFullName: newFullName, id: sql`replace(${pullRequestReviews.id}, ${oldFullName}, ${newFullName})` }) + .where(eq(pullRequestReviews.repoFullName, oldFullName)); + + // advisories: `id` is a random UUID (never repo-derived) and there is no unique constraint on repo + // columns, so this is a plain rename -- repoFullName plus the `targetKey` business identifier + // (`${repoFullName}#${pullNumber|issueNumber|"unknown"}`, src/rules/advisory.ts), same LIKE+replace + // shape as auditEvents.target_key below. + await db + .update(advisories) + .set({ repoFullName: newFullName, targetKey: sql`replace(${advisories.targetKey}, ${oldFullName}, ${newFullName})` }) + .where(eq(advisories.repoFullName, oldFullName)); + // auditEvents.target_key: an append-only log with no uniqueness on target_key (many rows legitimately // share one), so a plain substring rename with no dedupe step is correct and sufficient. await db diff --git a/test/unit/repo-identity-rename.test.ts b/test/unit/repo-identity-rename.test.ts index c15551103a..95080479c1 100644 --- a/test/unit/repo-identity-rename.test.ts +++ b/test/unit/repo-identity-rename.test.ts @@ -3,12 +3,22 @@ import { renameRepositoryIdentity } from "../../src/db/repo-identity-rename"; import { getIssue, getPullRequest, + getPullRequestDetailSyncState, getRepository, getRepositorySettings, listPullRequests, + listRecentMergedPullRequests, + persistAdvisory, recordAuditEvent, + recordGateBlockOutcome, + startActiveReviewTracking, + upsertCheckSummary, upsertIssueFromGitHub, + upsertPullRequestDetailSyncState, + upsertPullRequestFile, upsertPullRequestFromGitHub, + upsertPullRequestReview, + upsertRecentMergedPullRequest, upsertRepositoryFromGitHub, upsertRepositorySettings, } from "../../src/db/repositories"; @@ -133,6 +143,216 @@ describe("renameRepositoryIdentity", () => { }); }); + describe("gate_outcomes", () => { + it("renames repo_full_name and id for the PR's gate-block row", async () => { + const env = createTestEnv(); + await recordGateBlockOutcome(env, { repoFullName: OLD, pullNumber: 5, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + await renameRepositoryIdentity(env, OLD, NEW); + const oldRow = await env.DB.prepare("select count(*) as n from gate_outcomes where repo_full_name = ?").bind(OLD).first<{ n: number }>(); + expect(oldRow?.n).toBe(0); + const renamed = await env.DB.prepare("select id, blocker_codes_json as blockerCodesJson from gate_outcomes where repo_full_name = ? and pull_number = ?").bind(NEW, 5).first<{ id: string; blockerCodesJson: string }>(); + expect(renamed?.id).toBe(`gate:${NEW}#5`); + expect(renamed?.blockerCodesJson).toContain("missing_linked_issue"); + }); + + it("REGRESSION (#repo-rename-migration): folds away a stray new-name gate-block row on the same PR number", async () => { + const env = createTestEnv(); + await recordGateBlockOutcome(env, { repoFullName: OLD, pullNumber: 5, blockerCodes: ["slop_risk"] }); + await recordGateBlockOutcome(env, { repoFullName: NEW, pullNumber: 5, blockerCodes: ["duplicate_pr_risk"] }); + await renameRepositoryIdentity(env, OLD, NEW); + const rows = await env.DB.prepare("select blocker_codes_json as blockerCodesJson from gate_outcomes where repo_full_name = ? and pull_number = ?").bind(NEW, 5).all<{ blockerCodesJson: string }>(); + expect(rows.results).toHaveLength(1); + expect(rows.results[0]?.blockerCodesJson).toContain("slop_risk"); + }); + }); + + describe("active_review_tracking", () => { + it("renames repo_full_name and id for the PR's active-review row", async () => { + const env = createTestEnv(); + await startActiveReviewTracking(env, { repoFullName: OLD, pullNumber: 9, headSha: "def456", deliveryId: "delivery-1" }); + await renameRepositoryIdentity(env, OLD, NEW); + const oldRow = await env.DB.prepare("select count(*) as n from active_review_tracking where repo_full_name = ?").bind(OLD).first<{ n: number }>(); + expect(oldRow?.n).toBe(0); + const renamed = await env.DB.prepare("select id, head_sha as headSha from active_review_tracking where repo_full_name = ? and pull_number = ?").bind(NEW, 9).first<{ id: string; headSha: string }>(); + expect(renamed?.id).toBe(`active-review:${NEW}#9`); + expect(renamed?.headSha).toBe("def456"); + }); + + it("REGRESSION (#repo-rename-migration): folds away a stray new-name active-review row on the same PR number", async () => { + const env = createTestEnv(); + await startActiveReviewTracking(env, { repoFullName: OLD, pullNumber: 9, headSha: "old-head", deliveryId: "delivery-old" }); + await startActiveReviewTracking(env, { repoFullName: NEW, pullNumber: 9, headSha: "stray-head", deliveryId: "delivery-stray" }); + await renameRepositoryIdentity(env, OLD, NEW); + const rows = await env.DB.prepare("select head_sha as headSha from active_review_tracking where repo_full_name = ? and pull_number = ?").bind(NEW, 9).all<{ headSha: string }>(); + expect(rows.results).toHaveLength(1); + expect(rows.results[0]?.headSha).toBe("old-head"); + }); + }); + + describe("pull_request_detail_sync_state", () => { + it("renames repo_full_name and id for the PR's sync-state row", async () => { + const env = createTestEnv(); + await upsertPullRequestDetailSyncState(env, { repoFullName: OLD, pullNumber: 3, status: "complete", headSha: "sha-old" }); + await renameRepositoryIdentity(env, OLD, NEW); + expect(await getPullRequestDetailSyncState(env, OLD, 3)).toBeNull(); + const renamed = await getPullRequestDetailSyncState(env, NEW, 3); + expect(renamed).toMatchObject({ repoFullName: NEW, status: "complete", headSha: "sha-old" }); + const idRow = await env.DB.prepare("select id from pull_request_detail_sync_state where repo_full_name = ? and pull_number = ?").bind(NEW, 3).first<{ id: string }>(); + expect(idRow?.id).toBe(`${NEW}#3`); + }); + + it("REGRESSION (#repo-rename-migration): folds away a stray new-name sync-state row on the same PR number", async () => { + const env = createTestEnv(); + await upsertPullRequestDetailSyncState(env, { repoFullName: OLD, pullNumber: 3, status: "complete" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: NEW, pullNumber: 3, status: "never_synced" }); + await renameRepositoryIdentity(env, OLD, NEW); + const rows = await env.DB.prepare("select status from pull_request_detail_sync_state where repo_full_name = ? and pull_number = ?").bind(NEW, 3).all<{ status: string }>(); + expect(rows.results).toHaveLength(1); + expect(rows.results[0]?.status).toBe("complete"); + }); + }); + + describe("recent_merged_pull_requests", () => { + it("renames repo_full_name, id, and html_url for a merged-PR row", async () => { + const env = createTestEnv(); + await upsertRecentMergedPullRequest(env, { repoFullName: OLD, number: 11, title: "Merged PR", htmlUrl: `https://github.com/${OLD}/pull/11`, labels: [], linkedIssues: [], changedFiles: [], payload: {} }); + await renameRepositoryIdentity(env, OLD, NEW); + const rows = await listRecentMergedPullRequests(env, NEW); + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ title: "Merged PR", htmlUrl: `https://github.com/${NEW}/pull/11` }); + expect(await listRecentMergedPullRequests(env, OLD)).toHaveLength(0); + }); + + it("REGRESSION (#repo-rename-migration): folds away a stray new-name row on the same PR number", async () => { + const env = createTestEnv(); + await upsertRecentMergedPullRequest(env, { repoFullName: OLD, number: 11, title: "Original", labels: [], linkedIssues: [], changedFiles: [], payload: {} }); + await upsertRecentMergedPullRequest(env, { repoFullName: NEW, number: 11, title: "Fragment", labels: [], linkedIssues: [], changedFiles: [], payload: {} }); + await renameRepositoryIdentity(env, OLD, NEW); + const rows = await listRecentMergedPullRequests(env, NEW); + expect(rows.filter((pr) => pr.number === 11)).toHaveLength(1); + expect(rows.find((pr) => pr.number === 11)?.title).toBe("Original"); + }); + }); + + describe("pull_request_files", () => { + it("renames repo_full_name and id for every file row under the old name", async () => { + const env = createTestEnv(); + await upsertPullRequestFile(env, { repoFullName: OLD, pullNumber: 4, path: "src/a.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); + await upsertPullRequestFile(env, { repoFullName: OLD, pullNumber: 4, path: "src/b.ts", additions: 2, deletions: 1, changes: 3, payload: {} }); + await renameRepositoryIdentity(env, OLD, NEW); + const oldRows = await env.DB.prepare("select count(*) as n from pull_request_files where repo_full_name = ?").bind(OLD).first<{ n: number }>(); + expect(oldRows?.n).toBe(0); + const renamed = await env.DB.prepare("select id, path from pull_request_files where repo_full_name = ? order by path").bind(NEW).all<{ id: string; path: string }>(); + expect(renamed.results).toEqual([ + { id: `${NEW}#4#src/a.ts`, path: "src/a.ts" }, + { id: `${NEW}#4#src/b.ts`, path: "src/b.ts" }, + ]); + }); + + it("REGRESSION (#repo-rename-migration): folds away a stray new-name row that collides on the same (pull_number, path) pair", async () => { + const env = createTestEnv(); + await upsertPullRequestFile(env, { repoFullName: OLD, pullNumber: 4, path: "src/a.ts", additions: 10, deletions: 0, changes: 10, payload: {} }); + await upsertPullRequestFile(env, { repoFullName: NEW, pullNumber: 4, path: "src/a.ts", additions: 1, deletions: 1, changes: 2, payload: {} }); + await renameRepositoryIdentity(env, OLD, NEW); + const rows = await env.DB.prepare("select additions from pull_request_files where repo_full_name = ? and pull_number = ? and path = ?").bind(NEW, 4, "src/a.ts").all<{ additions: number }>(); + expect(rows.results).toHaveLength(1); + expect(rows.results[0]?.additions).toBe(10); + }); + + it("does not disturb a same-numbered PR's file at a DIFFERENT path (pair, not just pull_number, must match to fold)", async () => { + const env = createTestEnv(); + await upsertPullRequestFile(env, { repoFullName: OLD, pullNumber: 4, path: "src/a.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); + await upsertPullRequestFile(env, { repoFullName: NEW, pullNumber: 4, path: "src/other.ts", additions: 5, deletions: 0, changes: 5, payload: {} }); + await renameRepositoryIdentity(env, OLD, NEW); + const rows = await env.DB.prepare("select path from pull_request_files where repo_full_name = ? and pull_number = ? order by path").bind(NEW, 4).all<{ path: string }>(); + expect(rows.results.map((r) => r.path)).toEqual(["src/a.ts", "src/other.ts"]); + }); + }); + + describe("check_summaries", () => { + it("renames repo_full_name and a repo-embedded id", async () => { + const env = createTestEnv(); + await upsertCheckSummary(env, { id: `${OLD}#sha1#build`, repoFullName: OLD, pullNumber: 6, headSha: "sha1", name: "build", status: "completed", conclusion: "success", payload: {} }); + await renameRepositoryIdentity(env, OLD, NEW); + const renamed = await env.DB.prepare("select id from check_summaries where repo_full_name = ? and head_sha = ? and name = ?").bind(NEW, "sha1", "build").first<{ id: string }>(); + expect(renamed?.id).toBe(`${NEW}#sha1#build`); + }); + + it("leaves a non-repo-embedded id (e.g. a raw check-run id) untouched aside from repo_full_name", async () => { + const env = createTestEnv(); + await upsertCheckSummary(env, { id: "998877", repoFullName: OLD, pullNumber: 6, headSha: "sha2", name: "LoopOver Orb Review Agent", status: "completed", payload: {} }); + await renameRepositoryIdentity(env, OLD, NEW); + const renamed = await env.DB.prepare("select id from check_summaries where repo_full_name = ? and head_sha = ? and name = ?").bind(NEW, "sha2", "LoopOver Orb Review Agent").first<{ id: string }>(); + expect(renamed?.id).toBe("998877"); // replace() on a non-matching id is a no-op -- id stable, repo_full_name still renamed + }); + + it("REGRESSION (#repo-rename-migration): folds away a stray new-name row colliding on (head_sha, name)", async () => { + const env = createTestEnv(); + await upsertCheckSummary(env, { id: "1", repoFullName: OLD, pullNumber: 6, headSha: "sha1", name: "build", status: "completed", conclusion: "success", payload: {} }); + await upsertCheckSummary(env, { id: "2", repoFullName: NEW, pullNumber: 6, headSha: "sha1", name: "build", status: "in_progress", payload: {} }); + await renameRepositoryIdentity(env, OLD, NEW); + const rows = await env.DB.prepare("select conclusion from check_summaries where repo_full_name = ? and head_sha = ? and name = ?").bind(NEW, "sha1", "build").all<{ conclusion: string | null }>(); + expect(rows.results).toHaveLength(1); + expect(rows.results[0]?.conclusion).toBe("success"); + }); + + it("REGRESSION (#repo-rename-migration): a NULL head_sha row folds correctly (SQL NULL never equals NULL via '=')", async () => { + const env = createTestEnv(); + await upsertCheckSummary(env, { id: "3", repoFullName: OLD, pullNumber: null, headSha: null, name: "queued-check", status: "queued", payload: {} }); + await upsertCheckSummary(env, { id: "4", repoFullName: NEW, pullNumber: null, headSha: null, name: "queued-check", status: "stale-stray", payload: {} }); + await renameRepositoryIdentity(env, OLD, NEW); + const rows = await env.DB.prepare("select status from check_summaries where repo_full_name = ? and head_sha is null and name = ?").bind(NEW, "queued-check").all<{ status: string }>(); + expect(rows.results).toHaveLength(1); + expect(rows.results[0]?.status).toBe("queued"); + }); + }); + + describe("pull_request_reviews", () => { + it("renames repo_full_name and a repo-embedded id", async () => { + const env = createTestEnv(); + await upsertPullRequestReview(env, { id: `${OLD}#8#555`, repoFullName: OLD, pullNumber: 8, state: "APPROVED", payload: {} }); + await renameRepositoryIdentity(env, OLD, NEW); + const oldRows = await env.DB.prepare("select count(*) as n from pull_request_reviews where repo_full_name = ?").bind(OLD).first<{ n: number }>(); + expect(oldRows?.n).toBe(0); + const renamed = await env.DB.prepare("select id, state from pull_request_reviews where repo_full_name = ?").bind(NEW).first<{ id: string; state: string }>(); + expect(renamed).toMatchObject({ id: `${NEW}#8#555`, state: "APPROVED" }); + }); + + it("does not disturb a review row that only ever existed under the new name", async () => { + const env = createTestEnv(); + await upsertPullRequestReview(env, { id: `${OLD}#8#555`, repoFullName: OLD, pullNumber: 8, state: "APPROVED", payload: {} }); + await upsertPullRequestReview(env, { id: `${NEW}#8#556`, repoFullName: NEW, pullNumber: 8, state: "COMMENTED", payload: {} }); + await renameRepositoryIdentity(env, OLD, NEW); + const rows = await env.DB.prepare("select id from pull_request_reviews where repo_full_name = ? order by id").bind(NEW).all<{ id: string }>(); + expect(rows.results.map((r) => r.id)).toEqual([`${NEW}#8#555`, `${NEW}#8#556`]); + }); + }); + + describe("advisories", () => { + it("renames repo_full_name and the repo-embedded target_key, leaving the random-UUID id untouched", async () => { + const env = createTestEnv(); + const advisoryId = "11111111-1111-1111-1111-111111111111"; + await persistAdvisory(env, { + id: advisoryId, + targetType: "pull_request", + targetKey: `${OLD}#12`, + repoFullName: OLD, + pullNumber: 12, + conclusion: "neutral", + severity: "info", + title: "LoopOver advisory available", + summary: "1 advisory finding generated.", + findings: [], + generatedAt: "2026-07-14T00:00:00.000Z", + }); + await renameRepositoryIdentity(env, OLD, NEW); + const renamed = await env.DB.prepare("select id, target_key as targetKey from advisories where repo_full_name = ?").bind(NEW).first<{ id: string; targetKey: string }>(); + expect(renamed).toEqual({ id: advisoryId, targetKey: `${NEW}#12` }); + const oldRows = await env.DB.prepare("select count(*) as n from advisories where repo_full_name = ?").bind(OLD).first<{ n: number }>(); + expect(oldRows?.n).toBe(0); + }); + }); + describe("audit_events", () => { it("renames every target_key containing the old full name, including composite repo#number keys, leaving unrelated keys untouched", async () => { const env = createTestEnv();