From bbc67f80649332e3800d1f00edebd043e8b2e06d Mon Sep 17 00:00:00 2001 From: Giancarmine Salucci Date: Sun, 29 Mar 2026 01:15:58 +0100 Subject: [PATCH] fix(MULTIVERSION-0001): prevent version jobs from overwriting repo-wide NULL rules entry Version jobs now write rules only to the version-specific (repo, versionId) row. Previously every version job unconditionally wrote to the (repo, NULL) row as well, causing whichever version indexed last to contaminate the repo-wide rules that the context API merges into every query response. Adds a regression test (Bug5b) that indexes the main branch, then indexes a version with different rules, and asserts the NULL row still holds the main-branch rules. Co-Authored-By: Claude Sonnet 4.6 --- .../server/pipeline/indexing.pipeline.test.ts | 71 ++++++++++++++++++- src/lib/server/pipeline/indexing.pipeline.ts | 11 +-- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/lib/server/pipeline/indexing.pipeline.test.ts b/src/lib/server/pipeline/indexing.pipeline.test.ts index 51a1ba7..f057d0c 100644 --- a/src/lib/server/pipeline/indexing.pipeline.test.ts +++ b/src/lib/server/pipeline/indexing.pipeline.test.ts @@ -869,15 +869,17 @@ describe('IndexingPipeline', () => { await pipeline.run(job as never); - // Repo-wide row (version_id IS NULL) must exist. + // Repo-wide row (version_id IS NULL) must NOT be written by a version job — + // writing it here would contaminate the NULL entry with version-specific rules + // (Bug 5b regression guard). const repoRow = db .prepare( `SELECT rules FROM repository_configs WHERE repository_id = '/test/repo' AND version_id IS NULL` ) .get() as { rules: string } | undefined; - expect(repoRow).toBeDefined(); + expect(repoRow).toBeUndefined(); - // Version-specific row must also exist. + // Version-specific row must exist with the correct rules. const versionRow = db .prepare( `SELECT rules FROM repository_configs WHERE repository_id = '/test/repo' AND version_id = ?` @@ -888,6 +890,69 @@ describe('IndexingPipeline', () => { expect(rules).toEqual(['This is v2. Use the new Builder API.']); }); + it('regression(Bug5b): version job does not overwrite the repo-wide NULL rules entry', async () => { + // Arrange: index the main branch first to establish a repo-wide rules entry. + const mainBranchRules = ['Always use TypeScript strict mode.']; + const mainPipeline = makePipeline({ + files: [ + { + path: 'trueref.json', + content: JSON.stringify({ rules: mainBranchRules }), + sha: 'sha-main-config', + language: 'json' + } + ], + totalFiles: 1 + }); + const mainJob = makeJob('/test/repo'); // no versionId → main-branch job + await mainPipeline.run(mainJob as never); + + // Confirm the repo-wide entry was written. + const afterMain = db + .prepare( + `SELECT rules FROM repository_configs WHERE repository_id = '/test/repo' AND version_id IS NULL` + ) + .get() as { rules: string } | undefined; + expect(afterMain).toBeDefined(); + expect(JSON.parse(afterMain!.rules)).toEqual(mainBranchRules); + + // Act: index a version with different rules. + const versionId = insertVersion(db, { tag: 'v3.0.0', state: 'pending' }); + const versionRules = ['v3 only: use the streaming API.']; + const versionPipeline = makePipeline({ + files: [ + { + path: 'trueref.json', + content: JSON.stringify({ rules: versionRules }), + sha: 'sha-v3-config', + language: 'json' + } + ], + totalFiles: 1 + }); + const versionJob = makeJob('/test/repo', versionId); + await versionPipeline.run(versionJob as never); + + // Assert: the repo-wide NULL entry must still contain the main-branch rules, + // not the version-specific ones. + const afterVersion = db + .prepare( + `SELECT rules FROM repository_configs WHERE repository_id = '/test/repo' AND version_id IS NULL` + ) + .get() as { rules: string } | undefined; + expect(afterVersion).toBeDefined(); + expect(JSON.parse(afterVersion!.rules)).toEqual(mainBranchRules); + + // And the version-specific row must contain the version rules. + const versionRow = db + .prepare( + `SELECT rules FROM repository_configs WHERE repository_id = '/test/repo' AND version_id = ?` + ) + .get(versionId) as { rules: string } | undefined; + expect(versionRow).toBeDefined(); + expect(JSON.parse(versionRow!.rules)).toEqual(versionRules); + }); + it('persists rules from CrawlResult.config even when trueref.json is absent from files (folders allowlist bug)', async () => { // Regression test for MULTIVERSION-0001: // When trueref.json specifies a `folders` allowlist (e.g. ["src/"]), diff --git a/src/lib/server/pipeline/indexing.pipeline.ts b/src/lib/server/pipeline/indexing.pipeline.ts index 6056e0e..9e8e9ff 100644 --- a/src/lib/server/pipeline/indexing.pipeline.ts +++ b/src/lib/server/pipeline/indexing.pipeline.ts @@ -276,10 +276,13 @@ export class IndexingPipeline { // ---- Stage 6: Persist rules from config ---------------------------- if (parsedConfig?.config.rules?.length) { - // Repo-wide rules (versionId = null). - this.upsertRepoConfig(repo.id, null, parsedConfig.config.rules); - // Version-specific rules stored separately when indexing a version. - if (normJob.versionId) { + if (!normJob.versionId) { + // Main-branch job: write the repo-wide entry only. + this.upsertRepoConfig(repo.id, null, parsedConfig.config.rules); + } else { + // Version job: write only the version-specific entry. + // Writing to the NULL row here would overwrite repo-wide rules + // with whatever the last-indexed version happened to carry. this.upsertRepoConfig(repo.id, normJob.versionId, parsedConfig.config.rules); } }