fix(MULTIVERSION-0001): eliminate NULL-row contamination in getRules
When a versioned query is made, getRules() now returns only the version-specific repository_configs row. The NULL (HEAD/repo-wide) row is no longer merged in, preventing v4 rules from bleeding into v1/v2/v3 versioned context responses. Tests updated to assert the isolation: versioned queries return only their own rules row; a new test verifies that a version with no config row returns an empty rules array even when a NULL row exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -446,7 +446,11 @@ describe('API contract integration', () => {
|
||||
const repositoryId = seedRepo(db);
|
||||
const versionId = seedVersion(db, repositoryId, 'v18.3.0');
|
||||
const documentId = seedDocument(db, repositoryId, versionId);
|
||||
seedRules(db, repositoryId, ['Prefer hooks over classes']);
|
||||
// Insert version-specific rules (versioned queries no longer inherit the NULL row).
|
||||
db.prepare(
|
||||
`INSERT INTO repository_configs (repository_id, version_id, rules, updated_at)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
).run(repositoryId, versionId, JSON.stringify(['Prefer hooks over classes']), NOW_S);
|
||||
seedSnippet(db, {
|
||||
documentId,
|
||||
repositoryId,
|
||||
@@ -497,12 +501,12 @@ describe('API contract integration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('GET /api/v1/context returns merged repo-wide and version-specific rules', async () => {
|
||||
it('GET /api/v1/context returns only version-specific rules for versioned queries (no NULL row contamination)', async () => {
|
||||
const repositoryId = seedRepo(db);
|
||||
const versionId = seedVersion(db, repositoryId, 'v2.0.0');
|
||||
const documentId = seedDocument(db, repositoryId, versionId);
|
||||
|
||||
// Insert repo-wide rules (version_id IS NULL).
|
||||
// Insert repo-wide rules (version_id IS NULL) — these must NOT appear in versioned queries.
|
||||
db.prepare(
|
||||
`INSERT INTO repository_configs (repository_id, version_id, rules, updated_at)
|
||||
VALUES (?, NULL, ?, ?)`
|
||||
@@ -529,8 +533,8 @@ describe('API contract integration', () => {
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
// Both repo-wide and version-specific rules should appear (deduped).
|
||||
expect(body.rules).toEqual(['Repo-wide rule', 'Version-specific rule']);
|
||||
// Only the version-specific rule should appear — NULL row must not contaminate.
|
||||
expect(body.rules).toEqual(['Version-specific rule']);
|
||||
});
|
||||
|
||||
it('GET /api/v1/context returns only repo-wide rules when no version is requested', async () => {
|
||||
@@ -556,12 +560,13 @@ describe('API contract integration', () => {
|
||||
expect(body.rules).toEqual(['Repo-wide rule only']);
|
||||
});
|
||||
|
||||
it('GET /api/v1/context deduplicates rules that appear in both repo-wide and version config', async () => {
|
||||
it('GET /api/v1/context versioned query returns only the version-specific rules row', async () => {
|
||||
const repositoryId = seedRepo(db);
|
||||
const versionId = seedVersion(db, repositoryId, 'v3.0.0');
|
||||
const documentId = seedDocument(db, repositoryId, versionId);
|
||||
|
||||
const sharedRule = 'Use TypeScript strict mode';
|
||||
// Insert repo-wide NULL row — must NOT bleed into versioned query results.
|
||||
db.prepare(
|
||||
`INSERT INTO repository_configs (repository_id, version_id, rules, updated_at)
|
||||
VALUES (?, NULL, ?, ?)`
|
||||
@@ -582,10 +587,35 @@ describe('API contract integration', () => {
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
// sharedRule appears once, version-only rule appended.
|
||||
// Returns only the version-specific row as stored — no NULL row merge.
|
||||
expect(body.rules).toEqual([sharedRule, 'Version-only rule']);
|
||||
});
|
||||
|
||||
it('GET /api/v1/context versioned query returns empty rules when only NULL row exists (no NULL contamination)', async () => {
|
||||
const repositoryId = seedRepo(db);
|
||||
const versionId = seedVersion(db, repositoryId, 'v1.0.0');
|
||||
const documentId = seedDocument(db, repositoryId, versionId);
|
||||
|
||||
// Only a repo-wide NULL row exists — no version-specific config.
|
||||
db.prepare(
|
||||
`INSERT INTO repository_configs (repository_id, version_id, rules, updated_at)
|
||||
VALUES (?, NULL, ?, ?)`
|
||||
).run(repositoryId, JSON.stringify(['HEAD rules that must not contaminate v1']), NOW_S);
|
||||
|
||||
seedSnippet(db, { documentId, repositoryId, versionId, content: 'v1 content' });
|
||||
|
||||
const response = await getContext({
|
||||
url: new URL(
|
||||
`http://test/api/v1/context?libraryId=${encodeURIComponent(`${repositoryId}/v1.0.0`)}&query=${encodeURIComponent('v1 content')}`
|
||||
)
|
||||
} as never);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
// No version-specific config row → empty rules. NULL row must not bleed in.
|
||||
expect(body.rules).toEqual([]);
|
||||
});
|
||||
|
||||
it('GET /api/v1/context returns 404 with VERSION_NOT_FOUND when version does not exist', async () => {
|
||||
const repositoryId = seedRepo(db);
|
||||
|
||||
|
||||
@@ -69,35 +69,25 @@ function getRules(
|
||||
repositoryId: string,
|
||||
versionId?: string
|
||||
): string[] {
|
||||
// Repo-wide rules (version_id IS NULL).
|
||||
const repoRow = db
|
||||
if (!versionId) {
|
||||
// Unversioned query: return repo-wide (HEAD) rules only.
|
||||
const row = db
|
||||
.prepare<
|
||||
[string],
|
||||
RawRepoConfig
|
||||
>(`SELECT rules FROM repository_configs WHERE repository_id = ? AND version_id IS NULL`)
|
||||
.get(repositoryId);
|
||||
return parseRulesJson(row?.rules);
|
||||
}
|
||||
|
||||
const repoRules = parseRulesJson(repoRow?.rules);
|
||||
|
||||
if (!versionId) return repoRules;
|
||||
|
||||
// Version-specific rules.
|
||||
const versionRow = db
|
||||
// Versioned query: return only version-specific rules (no NULL row merge).
|
||||
const row = db
|
||||
.prepare<
|
||||
[string, string],
|
||||
RawRepoConfig
|
||||
>(`SELECT rules FROM repository_configs WHERE repository_id = ? AND version_id = ?`)
|
||||
.get(repositoryId, versionId);
|
||||
|
||||
const versionRules = parseRulesJson(versionRow?.rules);
|
||||
|
||||
// Merge: repo-wide first, then version-specific (deduped by content).
|
||||
const seen = new Set(repoRules);
|
||||
const merged = [...repoRules];
|
||||
for (const r of versionRules) {
|
||||
if (!seen.has(r)) merged.push(r);
|
||||
}
|
||||
return merged;
|
||||
return parseRulesJson(row?.rules);
|
||||
}
|
||||
|
||||
interface RawRepoState {
|
||||
|
||||
Reference in New Issue
Block a user