test(catalog): tighten + isolate real shipped catalog-file checks
Apply review suggestions to the real-files block in ai-agent-roles-catalog.provider.spec.ts (test-only): 1. Fix inaccurate comment: there are 5 content YAML files (index + four per-bundle/lang files), not 6. 2. Improve isolation: read/parse the real index lazily inside tests (via loadRealIndex) instead of in the describe body, so a broken real file fails only these catalog tests, not collection of the whole spec (incl. the unrelated mocked-remote provider tests). 3. Add the symmetric slug check: each language file's slug set must equal the declared slug set (no undeclared/extra roles), matching scripts/check.mjs's exact two-way correspondence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -371,7 +371,9 @@ describe('AiAgentRolesCatalogProvider', () => {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Pin the REAL shipped catalog files (not synthetic fixtures). The JSON->YAML
|
// Pin the REAL shipped catalog files (not synthetic fixtures). The JSON->YAML
|
||||||
// migration was a hand conversion, so the realistic failure is a hand-edit
|
// migration was a hand conversion, so the realistic failure is a hand-edit
|
||||||
// error in one of the 6 real files (a quote/colon in a description, a broken
|
// error in one of the 5 content YAML files (the index + the four per-bundle/
|
||||||
|
// lang files: index.yaml plus bundles/{editorial,research}/{en,ru}.yaml) — a
|
||||||
|
// quote/colon in a description, a broken
|
||||||
// emoji/arrow, a block-scalar indent slip that silently changes or drops
|
// emoji/arrow, a block-scalar indent slip that silently changes or drops
|
||||||
// instructions). Nothing else in CI parses these files — `scripts/check.mjs`
|
// instructions). Nothing else in CI parses these files — `scripts/check.mjs`
|
||||||
// is not wired into any turbo/husky/CI step — so this is the only automated
|
// is not wired into any turbo/husky/CI step — so this is the only automated
|
||||||
@@ -394,38 +396,55 @@ describe('AiAgentRolesCatalogProvider', () => {
|
|||||||
return parseYaml(readFileSync(join(CATALOG_DIR, rel), 'utf8'), PARSE_OPTS);
|
return parseYaml(readFileSync(join(CATALOG_DIR, rel), 'utf8'), PARSE_OPTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load + validate the real index lazily (only when a test runs), so a broken
|
||||||
|
// real file fails ONLY these catalog tests — not collection of the entire
|
||||||
|
// spec, which also holds the unrelated mocked-remote provider tests above.
|
||||||
|
function loadRealIndex() {
|
||||||
|
const parsed = readCatalogYaml('index.yaml');
|
||||||
|
if (!isCatalogIndex(parsed)) {
|
||||||
|
throw new Error('Real index.yaml is not a valid catalog index');
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
it('index.yaml parses + validates with the provider guard', () => {
|
it('index.yaml parses + validates with the provider guard', () => {
|
||||||
expect(isCatalogIndex(readCatalogYaml('index.yaml'))).toBe(true);
|
expect(isCatalogIndex(readCatalogYaml('index.yaml'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Read the real index once to drive per-bundle/per-language assertions, so a
|
|
||||||
// bundle or language added later is automatically covered. A broken index
|
|
||||||
// fails the test above; here we only need its shape to enumerate files.
|
|
||||||
const parsedIndex = readCatalogYaml('index.yaml');
|
|
||||||
if (!isCatalogIndex(parsedIndex)) {
|
|
||||||
throw new Error('Real index.yaml is not a valid catalog index');
|
|
||||||
}
|
|
||||||
|
|
||||||
it('editorial bundle still ships the fact-checker role', () => {
|
it('editorial bundle still ships the fact-checker role', () => {
|
||||||
const editorial = parsedIndex.bundles.find((b) => b.id === 'editorial');
|
const editorial = loadRealIndex().bundles.find((b) => b.id === 'editorial');
|
||||||
expect(editorial).toBeDefined();
|
expect(editorial).toBeDefined();
|
||||||
expect(editorial?.roles.map((r) => r.slug)).toContain('fact-checker');
|
expect(editorial?.roles.map((r) => r.slug)).toContain('fact-checker');
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const bundle of parsedIndex.bundles) {
|
// Driven by the real index (read inside the test, so it's lazy): every
|
||||||
const declaredSlugs = bundle.roles.map((r) => r.slug);
|
// declared bundle + language file must parse, validate, and be in EXACT slug
|
||||||
for (const lang of bundle.languages) {
|
// correspondence with the index — every declared role present AND no
|
||||||
const rel = `bundles/${bundle.id}/${lang}.yaml`;
|
// undeclared extras — mirroring scripts/check.mjs, which requires both
|
||||||
it(`${rel} parses, validates, and carries every declared role with non-empty instructions`, () => {
|
// directions. A bundle or language added later is covered automatically.
|
||||||
|
it('every declared bundle/language file is valid and in exact slug correspondence', () => {
|
||||||
|
const index = loadRealIndex();
|
||||||
|
// Guard against an empty index silently passing the loops below.
|
||||||
|
expect(index.bundles.length).toBeGreaterThan(0);
|
||||||
|
for (const bundle of index.bundles) {
|
||||||
|
const declaredSlugs = bundle.roles.map((r) => r.slug);
|
||||||
|
expect(bundle.languages.length).toBeGreaterThan(0);
|
||||||
|
for (const lang of bundle.languages) {
|
||||||
|
const rel = `bundles/${bundle.id}/${lang}.yaml`;
|
||||||
const file = readCatalogYaml(rel);
|
const file = readCatalogYaml(rel);
|
||||||
expect(isCatalogBundleFile(file)).toBe(true);
|
expect(isCatalogBundleFile(file)).toBe(true);
|
||||||
// Narrow for TS and access fields safely.
|
// Narrow for TS and access fields safely.
|
||||||
if (!isCatalogBundleFile(file)) return;
|
if (!isCatalogBundleFile(file)) continue;
|
||||||
expect(file.language).toBe(lang);
|
expect(file.language).toBe(lang);
|
||||||
const fileSlugs = file.roles.map((r) => r.slug);
|
const fileSlugs = file.roles.map((r) => r.slug);
|
||||||
|
// Existing direction: every declared role is present in the file.
|
||||||
for (const slug of declaredSlugs) {
|
for (const slug of declaredSlugs) {
|
||||||
expect(fileSlugs).toContain(slug);
|
expect(fileSlugs).toContain(slug);
|
||||||
}
|
}
|
||||||
|
// Symmetric direction: the file carries NO undeclared/extra roles, so
|
||||||
|
// file slugs and declared slugs must be the SAME set (exact match).
|
||||||
|
// Catches a hand-edit that copies a stray role into a bundle file.
|
||||||
|
expect([...fileSlugs].sort()).toEqual([...declaredSlugs].sort());
|
||||||
expect(file.roles.length).toBeGreaterThan(0);
|
expect(file.roles.length).toBeGreaterThan(0);
|
||||||
for (const role of file.roles) {
|
for (const role of file.roles) {
|
||||||
expect(isCatalogRole(role)).toBe(true);
|
expect(isCatalogRole(role)).toBe(true);
|
||||||
@@ -433,8 +452,8 @@ describe('AiAgentRolesCatalogProvider', () => {
|
|||||||
expect(role.instructions.trim().length).toBeGreaterThan(0);
|
expect(role.instructions.trim().length).toBeGreaterThan(0);
|
||||||
expect(role.name.trim().length).toBeGreaterThan(0);
|
expect(role.name.trim().length).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user