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 <noreply@anthropic.com>
This commit is contained in:
Giancarmine Salucci
2026-03-29 01:15:58 +01:00
parent cd4ea7112c
commit bbc67f8064
2 changed files with 75 additions and 7 deletions

View File

@@ -869,15 +869,17 @@ describe('IndexingPipeline', () => {
await pipeline.run(job as never); 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 const repoRow = db
.prepare( .prepare(
`SELECT rules FROM repository_configs WHERE repository_id = '/test/repo' AND version_id IS NULL` `SELECT rules FROM repository_configs WHERE repository_id = '/test/repo' AND version_id IS NULL`
) )
.get() as { rules: string } | undefined; .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 const versionRow = db
.prepare( .prepare(
`SELECT rules FROM repository_configs WHERE repository_id = '/test/repo' AND version_id = ?` `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.']); 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 () => { it('persists rules from CrawlResult.config even when trueref.json is absent from files (folders allowlist bug)', async () => {
// Regression test for MULTIVERSION-0001: // Regression test for MULTIVERSION-0001:
// When trueref.json specifies a `folders` allowlist (e.g. ["src/"]), // When trueref.json specifies a `folders` allowlist (e.g. ["src/"]),

View File

@@ -276,10 +276,13 @@ export class IndexingPipeline {
// ---- Stage 6: Persist rules from config ---------------------------- // ---- Stage 6: Persist rules from config ----------------------------
if (parsedConfig?.config.rules?.length) { if (parsedConfig?.config.rules?.length) {
// Repo-wide rules (versionId = null). if (!normJob.versionId) {
this.upsertRepoConfig(repo.id, null, parsedConfig.config.rules); // Main-branch job: write the repo-wide entry only.
// Version-specific rules stored separately when indexing a version. this.upsertRepoConfig(repo.id, null, parsedConfig.config.rules);
if (normJob.versionId) { } 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); this.upsertRepoConfig(repo.id, normJob.versionId, parsedConfig.config.rules);
} }
} }