mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
* docs(agents): compact root guidance and preserve task-specific knowledge * fix(agents): parse guidance navigation with Markdown tokens * fix(agents): validate generic literal repository paths * fix(agents): distinguish code symbols and shortcut images * fix(agents): recognize SCSS filename literals * fix(agents): handle fenced imports and encoded paths * fix(agents): parse prose and rendered HTML anchors * fix(agents): validate rendered HTML navigation * fix(agents): use GitHub-compatible heading slugs * fix(agents): require standalone top-level Claude import * fix(agents): exclude HTML-contained guidance imports * fix(agents): handle image fragments and quoted imports * fix(agents): validate visible HTML and image source sets * fix(agents): recognize package scopes and route source work * fix(agents): parse JSONC and constrain package exemptions * fix(agents): decode link entities and allow package subpaths * fix(agents): route source work and check extensionless files * fix(agents): support package versions and source fragments * fix(agents): accept qualified package prose * fix(agents): retain rendered context for Markdown references * fix(agents): validate visible headings and spaced paths * fix(agents): validate media and hyphenated literal paths * fix(agents): decode full HTML entities and media assets * fix(agents): recognize possessive package mentions * fix(agents): validate extensionless imports and version comparators * fix(agents): retain visible backticks and explicit path punctuation * fix(agents): validate image-map navigation targets * fix(agents): count all Markdown line endings in budgets * fix(agents): delimit package prose at Unicode punctuation * fix(agents): normalize punctuation for extensionless imports * fix(agents): preserve filenames across prose punctuation * fix(agents): validate iframe document references * fix(agents): inspect document suffix before URL fragments * fix(agents): unify Markdown suffix and encoded import guards * fix(agents): handle wildcard versions and alternate documents * fix(agents): validate document formats and trim HTML URLs * fix(agents): cover document families and guidance basenames * fix(agents): require files for media references * fix(agents): preserve block boundaries and validate embeds * fix(agents): normalize internal HTML URL whitespace * fix(agents): reject empty media and ignore URL at-signs * fix(agents): validate srcdoc references and empty srcset * fix(agents): honor HTML bases and preserve adjacent imports * fix(agents): convert base file URLs to native paths * fix(agents): preserve imports after bare URL punctuation * fix(agents): exclude opaque URI prose from import scans * fix(agents): keep import tokens outside URI scheme matches * fix(agents): restrict opaque URI exemptions to parsed links * fix(agents): handle opening prose delimiters * fix(agents): scan nested imports and share document suffixes * fix(agents): reject pathless media and direct file URLs * fix(agents): reject file bases and preserve quoted URL boundaries * fix(agents): distinguish URL quotes and cover guidance variants * fix(agents): validate SVG images and conventional guides * fix(agents): handle declared package names handles and SVG use * fix(agents): normalize closing punctuation on federated handles * fix(agents): normalize Unicode punctuation on handles * fix(agents): normalize possessive federated handles * fix(agents): separate parenthetical prose from handles * fix(agents): exclude www autolinks from import scanning * ci: allow manual CodeQL validation of PR branches * fix(agents): reject nonportable Windows drive links
1869 lines
58 KiB
JavaScript
1869 lines
58 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
|
||
import { tmpdir } from 'node:os';
|
||
import { dirname, join } from 'node:path';
|
||
import test from 'node:test';
|
||
import { validateAgentGuidance } from './validate-agent-guidance.mjs';
|
||
|
||
const map = 'docs/maintenance/agent-context-map.md';
|
||
const migration = 'docs/maintenance/agent-guidance-migration.md';
|
||
async function fixture(t, overrides = {}) {
|
||
const rootDir = await mkdtemp(join(tmpdir(), 'agent-guidance-'));
|
||
t.after(() => rm(rootDir, { recursive: true, force: true }));
|
||
for (const [path, body] of Object.entries({
|
||
'AGENTS.md': '# Guidance\n',
|
||
'tsconfig.base.json': JSON.stringify({
|
||
compilerOptions: { paths: { '@iptvnator/*': ['libs/*'] } },
|
||
}),
|
||
'CLAUDE.md': '@AGENTS.md\n',
|
||
[map]: '# Context\n',
|
||
[migration]: '# Migration\n',
|
||
'docs/example.md': '# Hello, `World`!\n\n## Repeat\n## Repeat\n',
|
||
...overrides,
|
||
})) {
|
||
await mkdir(dirname(join(rootDir, path)), { recursive: true });
|
||
await writeFile(join(rootDir, path), body);
|
||
}
|
||
return rootDir;
|
||
}
|
||
async function diagnostics(t, overrides) {
|
||
return (
|
||
await validateAgentGuidance({ rootDir: await fixture(t, overrides) })
|
||
).diagnostics;
|
||
}
|
||
|
||
test('valid guidance and remote links pass', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'[Doc](docs/example.md#hello-world) and `@iptvnator/services`\n',
|
||
[map]: '[Repeat](../example.md#repeat-1)\n',
|
||
[migration]:
|
||
'[Original](https://github.com/example/repo/blob/main/AGENTS.md#old)\n',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
for (const [path, limit] of [
|
||
['AGENTS.md', 200],
|
||
['CLAUDE.md', 30],
|
||
]) {
|
||
for (const eol of ['\n', '\r\n']) {
|
||
test(`${path}: exact line limit accepts ${JSON.stringify(eol)}`, async (t) => {
|
||
const prefix = path === 'CLAUDE.md' ? '@AGENTS.md' : '# Guidance';
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
[path]:
|
||
[prefix, ...Array(limit - 1).fill('x')].join(eol) + eol,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
test(`${path}: one line over fails ${JSON.stringify(eol)}`, async (t) => {
|
||
const prefix = path === 'CLAUDE.md' ? '@AGENTS.md' : '# Guidance';
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
[path]: [prefix, ...Array(limit).fill('x')].join(eol),
|
||
})
|
||
).join('\n'),
|
||
/lines/
|
||
);
|
||
});
|
||
}
|
||
}
|
||
for (const [path, limit] of [
|
||
['AGENTS.md', 16384],
|
||
['CLAUDE.md', 2048],
|
||
]) {
|
||
test(`${path}: UTF-8 byte budget is exact`, async (t) => {
|
||
const prefix = path === 'CLAUDE.md' ? '@AGENTS.md\n' : '';
|
||
const remaining = limit - Buffer.byteLength(prefix);
|
||
const body =
|
||
prefix +
|
||
'é'.repeat(Math.floor(remaining / 2)) +
|
||
'x'.repeat(remaining % 2);
|
||
assert.deepEqual(await diagnostics(t, { [path]: body }), []);
|
||
assert.match(
|
||
(await diagnostics(t, { [path]: body + 'é' })).join('\n'),
|
||
/bytes/
|
||
);
|
||
});
|
||
}
|
||
for (const body of [
|
||
'',
|
||
'See @AGENTS.md',
|
||
'@AGENTS.md\n@AGENTS.md',
|
||
'@AGENTS.md\n@docs/example.md',
|
||
'@AGENTS.md\nRead @docs/example.md for more.',
|
||
]) {
|
||
test(`rejects invalid CLAUDE imports: ${JSON.stringify(body)}`, async (t) => {
|
||
assert.match(
|
||
(await diagnostics(t, { 'CLAUDE.md': body })).join('\n'),
|
||
/import/
|
||
);
|
||
});
|
||
}
|
||
for (const source of ['AGENTS.md', 'CLAUDE.md']) {
|
||
for (const target of [
|
||
'package.json',
|
||
'libs/extra.txt',
|
||
'arbitrary.custom',
|
||
'tools/instructions',
|
||
'LICENSE',
|
||
'.nvmrc',
|
||
'./instructions',
|
||
]) {
|
||
test(`${source}: rejects inline file import @${target}`, async (t) => {
|
||
const prefix = source === 'CLAUDE.md' ? '@AGENTS.md\n' : '';
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
[source]: `${prefix}Read @${target} for instructions.\n`,
|
||
})
|
||
).join('\n'),
|
||
/import/
|
||
);
|
||
});
|
||
}
|
||
}
|
||
test('AGENTS forbids imports while aliases in prose stay valid', async (t) => {
|
||
assert.match(
|
||
(await diagnostics(t, { 'AGENTS.md': '@docs/example.md\n' })).join(
|
||
'\n'
|
||
),
|
||
/import/
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'Use @iptvnator/services and `@iptvnator/ui/components`.\n',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
for (const path of ['AGENTS.md', 'CLAUDE.md', map, migration]) {
|
||
test(`${path}: missing local links and anchors fail`, async (t) => {
|
||
const prefix = path === 'CLAUDE.md' ? '@AGENTS.md\n' : '';
|
||
const target = path.startsWith('docs/')
|
||
? '../example.md'
|
||
: 'docs/example.md';
|
||
const errors = await diagnostics(t, {
|
||
[path]:
|
||
prefix + `[Missing](missing.md) [Anchor](${target}#missing)`,
|
||
});
|
||
assert.match(errors.join('\n'), /missing.md/);
|
||
assert.match(errors.join('\n'), /anchor.*missing/);
|
||
});
|
||
}
|
||
test('literal root paths validate without interpreting commands or globs', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
[map]: '`docs/example.md` `pnpm nx test web` `apps/*` `libs/<domain>/` `.plans/YYYY-MM-DD-short-topic.md`',
|
||
}),
|
||
[]
|
||
);
|
||
assert.match(
|
||
(await diagnostics(t, { [map]: '`docs/missing.md`' })).join('\n'),
|
||
/does not exist/
|
||
);
|
||
});
|
||
for (const path of [
|
||
'lib/definitely-missing.ts',
|
||
'services/definitely-missing.ts',
|
||
'electron-builder.json',
|
||
'missing.scss',
|
||
'custom.config-format',
|
||
'.custom-config',
|
||
'.env.local',
|
||
'.eslintrc.json',
|
||
'arbitrary/directory',
|
||
]) {
|
||
test(`checks generic literal repository path ${path}`, async (t) => {
|
||
assert.match(
|
||
(await diagnostics(t, { [map]: '`' + path + '`' })).join('\n'),
|
||
/does not exist/
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, { [map]: '`' + path + '`', [path]: '' }),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
test('AGENTS rejects the reported unlisted directory and root filename', async (t) => {
|
||
const errors = await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'`services/definitely-missing.ts` and `electron-builder.json`',
|
||
});
|
||
assert.equal(errors.length, 2);
|
||
assert.match(
|
||
errors.join('\n'),
|
||
/does not exist: services\/definitely-missing\.ts/
|
||
);
|
||
assert.match(errors.join('\n'), /does not exist: electron-builder\.json/);
|
||
});
|
||
test('ambiguous dotted filenames can be explicitly marked as paths', async (t) => {
|
||
for (const reference of ['`./custom.symbol`', '[File](custom.symbol)']) {
|
||
assert.match(
|
||
(await diagnostics(t, { [map]: reference })).join('\n'),
|
||
/does not exist/
|
||
);
|
||
}
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
[map]: '`./custom.symbol`',
|
||
'custom.symbol': '',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
test('literal paths retain fragment checks', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, { [map]: '`docs/example.md#hello-world`' }),
|
||
[]
|
||
);
|
||
assert.match(
|
||
(await diagnostics(t, { [map]: '`docs/example.md#missing`' })).join(
|
||
'\n'
|
||
),
|
||
/missing anchor/
|
||
);
|
||
});
|
||
test('literal detection excludes commands, templates, URLs, aliases and code expressions', async (t) => {
|
||
const examples = [
|
||
'pnpm nx test web',
|
||
'node tools/example.mjs',
|
||
'--config=missing.json',
|
||
'docs/*.md',
|
||
'docs/{one,two}.md',
|
||
'docs/<topic>.md',
|
||
'.plans/YYYY-MM-DD-short-topic.md',
|
||
'docs/.../example.md',
|
||
'https://example.com/guide.md',
|
||
'file:///tmp/example.md',
|
||
'@iptvnator/services',
|
||
'@angular/core',
|
||
'node:fs/promises',
|
||
'window.electron',
|
||
'Date.now',
|
||
'String.raw',
|
||
'Buffer.from',
|
||
'customStore.selectedItem',
|
||
'process.env.NODE_ENV',
|
||
'document.body',
|
||
'process.env',
|
||
'this.store',
|
||
'store.setState()',
|
||
'source?.id',
|
||
'a/b+c',
|
||
'$HOME/example.md',
|
||
];
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
[map]: examples.map((value) => '`' + value + '`').join(' '),
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
test('rejects traversal and symlink escapes', async (t) => {
|
||
const rootDir = await fixture(t, {
|
||
'AGENTS.md': '[Escape](../outside.md) [Symlink](outside)',
|
||
});
|
||
await symlink(tmpdir(), join(rootDir, 'outside'));
|
||
const errors = (await validateAgentGuidance({ rootDir })).diagnostics;
|
||
assert.equal(errors.filter((error) => error.includes('escapes')).length, 2);
|
||
});
|
||
test('requires all four guidance surfaces', async (t) => {
|
||
const rootDir = await fixture(t);
|
||
await rm(join(rootDir, map));
|
||
assert.match(
|
||
(await validateAgentGuidance({ rootDir })).diagnostics.join('\n'),
|
||
/agent-context-map.md.*missing/
|
||
);
|
||
});
|
||
|
||
test('balanced parentheses and formatted labels retain complete destinations', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'[Read **[the guide]**](docs/example(1).md#hello-world)\n',
|
||
'docs/example(1).md': '# Hello, `World`!\n',
|
||
}),
|
||
[]
|
||
);
|
||
const errors = await diagnostics(t, {
|
||
'AGENTS.md': '[Read **[the guide]**](docs/missing(1).md)\n',
|
||
});
|
||
assert.match(errors.join('\n'), /does not exist: docs\/missing\(1\)\.md/);
|
||
});
|
||
|
||
test('angle and escaped Markdown destinations resolve literal filenames', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'[Angle](<docs/guide (one).md>)\n[Escaped](docs/example\\(1\\).md)\n',
|
||
'docs/guide (one).md': '# Guide\n',
|
||
'docs/example(1).md': '# Example\n',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
test('reference definitions resolve full, collapsed and shortcut links', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'[Read **guide**][GUIDE]\n[Guide][]\n[Guide]\n\n[guide]: <docs/example(1).md#hello-world> "Title"\n',
|
||
'docs/example(1).md': '# Hello, `World`!\n',
|
||
}),
|
||
[]
|
||
);
|
||
const errors = await diagnostics(t, {
|
||
'AGENTS.md': '[Read guide][guide]\n\n[guide]: docs/missing(1).md\n',
|
||
});
|
||
assert.match(errors.join('\n'), /does not exist: docs\/missing\(1\)\.md/);
|
||
});
|
||
|
||
for (const reference of [
|
||
'[Guide][missing]',
|
||
'[Guide][]',
|
||
'[**Guide**][missing]',
|
||
'[Guide `code`][missing]',
|
||
'![Guide][missing]',
|
||
'![Diagram]',
|
||
]) {
|
||
test(`diagnoses unresolved explicit reference ${reference}`, async (t) => {
|
||
assert.match(
|
||
(await diagnostics(t, { 'AGENTS.md': reference + '\n' })).join(
|
||
'\n'
|
||
),
|
||
/unresolved.*reference/i
|
||
);
|
||
});
|
||
}
|
||
|
||
test('lexer visits navigation inside tables, lists, and blockquotes', async (t) => {
|
||
const errors = await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'| Guide |\n| --- |\n| [Table](docs/table(1).md) |\n\n- [List](docs/list(1).md)\n\n> [Quote][missing]\n',
|
||
});
|
||
assert.equal(errors.length, 3);
|
||
assert.match(errors.join('\n'), /docs\/table\(1\)\.md/);
|
||
assert.match(errors.join('\n'), /docs\/list\(1\)\.md/);
|
||
assert.match(errors.join('\n'), /unresolved.*reference/i);
|
||
});
|
||
|
||
test('fenced, indented, inline and escaped examples are not navigation', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': [
|
||
'```md',
|
||
'[Code](missing.md)',
|
||
'[Code][missing]',
|
||
'```',
|
||
'',
|
||
' [Indented](missing.md)',
|
||
' [Indented][]',
|
||
'',
|
||
'`[Inline](missing.md)` and ``[Code `label`][missing]``.',
|
||
'\\[Escaped][missing]',
|
||
].join('\n'),
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
test('heading anchors use nested token text, link labels and code spans', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'[Heading](docs/example.md#a-bold-emphasis-label-code-hi)\n[Duplicate](docs/example.md#a-bold-emphasis-label-code-hi-1)',
|
||
'docs/example.md':
|
||
'# A **bold *emphasis*** [Label `code`](one(two).md) <em>Hi</em>\n\n# A **bold *emphasis*** [Label `code`](one(two).md) <em>Hi</em>\n',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
test('HTML heading tokens are ignored without rendering or tag stripping', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'[HTML](docs/example.md#safe-heading)\n[Nested](docs/example.md#unsafe-scriptalert1script)',
|
||
'docs/example.md':
|
||
'# Safe <span title="a > b">Heading</span>\n\n# Unsafe <scr<script>ipt>alert(1)</scr</script>ipt>\n',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
test('explicit HTML anchors work but anchors in code examples do not', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '[Anchor](docs/example.md#explicit)',
|
||
'docs/example.md': '<a id="explicit"></a>\n',
|
||
}),
|
||
[]
|
||
);
|
||
const errors = await diagnostics(t, {
|
||
'AGENTS.md': '[Fake](docs/example.md#fake)',
|
||
'docs/example.md': '```html\n<a id="fake"></a>\n```\n',
|
||
});
|
||
assert.match(errors.join('\n'), /missing anchor/);
|
||
});
|
||
|
||
for (const source of ['AGENTS.md', 'CLAUDE.md']) {
|
||
test(`${source}: fenced decorators are not root imports`, async (t) => {
|
||
const prefix = source === 'CLAUDE.md' ? '@AGENTS.md\n' : '';
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
[source]:
|
||
prefix + '```ts\n@Injectable()\n@docs/missing.md\n```\n',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
test('a fenced import cannot satisfy the required CLAUDE import', async (t) => {
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md': '```text\n@AGENTS.md\n```\n',
|
||
})
|
||
).join('\n'),
|
||
/standalone @AGENTS.md/
|
||
);
|
||
});
|
||
for (const [encoded, filename] of [
|
||
['guide%23one.md', 'guide#one.md'],
|
||
['guide%3Fone.md', 'guide?one.md'],
|
||
]) {
|
||
test(`encoded filename delimiters stay in path: ${encoded}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `[Doc](docs/${encoded}?view=1#hello%2Dworld)`,
|
||
[`docs/${filename}`]: '# Hello World\n',
|
||
}),
|
||
[]
|
||
);
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `[Doc](docs/${encoded}#missing)`,
|
||
[`docs/${filename}`]: '# Hello World\n',
|
||
})
|
||
).join('\n'),
|
||
/missing anchor/
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const source of ['AGENTS.md', 'CLAUDE.md']) {
|
||
test(`${source}: indented decorators are not imports`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
[source]:
|
||
(source === 'CLAUDE.md' ? '@AGENTS.md\n\n' : '') +
|
||
' @Injectable()\n @docs/missing.md\n',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
for (const body of [
|
||
'<!-- <a id="old"></a> -->',
|
||
`<script>const s = '<a id="old"></a>';</script>`,
|
||
'<template><a id="old"></a></template>',
|
||
]) {
|
||
test(`non-rendered HTML does not define anchors: ${body}`, async (t) => {
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '[Old](docs/example.md#old)',
|
||
'docs/example.md': body,
|
||
})
|
||
).join('\n'),
|
||
/missing anchor/
|
||
);
|
||
});
|
||
}
|
||
for (const [heading, anchor] of [
|
||
['A & B', 'a--b'],
|
||
['Café A B', 'café-a-b'],
|
||
]) {
|
||
test(`heading entities produce rendered anchors: ${heading}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `[Heading](docs/example.md#${anchor})`,
|
||
'docs/example.md': '# ' + heading,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const html of [
|
||
'<a href="docs/missing.md#section">Guide</a>',
|
||
'<img src="docs/missing.png">',
|
||
]) {
|
||
test(`validates rendered HTML navigation: ${html}`, async (t) => {
|
||
assert.match(
|
||
(await diagnostics(t, { 'AGENTS.md': html })).join('\n'),
|
||
/does not exist/
|
||
);
|
||
});
|
||
}
|
||
test('HTML navigation supports valid anchors and decoded attributes', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'<a href="docs/example.md#hello-world">Guide</a> <img src="docs/a&b.png">',
|
||
'docs/a&b.png': '',
|
||
}),
|
||
[]
|
||
);
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '<a href="docs/example.md#missing">Guide</a>',
|
||
})
|
||
).join('\n'),
|
||
/missing anchor/
|
||
);
|
||
});
|
||
test('non-rendered HTML navigation is ignored', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'<!-- <a href="missing.md"> -->\n<script>const a = \'<img src="missing.png">\';</script>\n\n<template><a href="missing.md">Hidden</a></template>',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
test('GitHub heading slugs remove non-ASCII whitespace', async (t) => {
|
||
for (const heading of ['A B', 'A\u00a0B', 'A\u2003B']) {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '[Heading](docs/example.md#ab)',
|
||
'docs/example.md': '# ' + heading,
|
||
}),
|
||
[]
|
||
);
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '[Heading](docs/example.md#a-b)',
|
||
'docs/example.md': '# ' + heading,
|
||
})
|
||
).join('\n'),
|
||
/missing anchor/
|
||
);
|
||
}
|
||
});
|
||
|
||
for (const body of [
|
||
'> @AGENTS.md',
|
||
'- @AGENTS.md',
|
||
'# @AGENTS.md',
|
||
'**@AGENTS.md**',
|
||
]) {
|
||
test(`structured Markdown cannot satisfy Claude import: ${body}`, async (t) => {
|
||
assert.match(
|
||
(await diagnostics(t, { 'CLAUDE.md': body })).join('\n'),
|
||
/standalone @AGENTS.md/
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const tag of ['template', 'div']) {
|
||
test(`HTML container cannot supply Claude import: ${tag}`, async (t) => {
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md': `<${tag}>\n\n@AGENTS.md\n\n</${tag}>`,
|
||
})
|
||
).join('\n'),
|
||
/standalone @AGENTS.md/
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md': `<${tag}>\n\nExample\n\n</${tag}>\n\n@AGENTS.md`,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const punctuation of ['"', "'", '[', '{', ':']) {
|
||
test(`rejects inline imports after ${punctuation}`, async (t) => {
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nRead ' +
|
||
punctuation +
|
||
'@docs/example.md',
|
||
})
|
||
).join('\n'),
|
||
/imports are not allowed/
|
||
);
|
||
});
|
||
}
|
||
for (const image of [
|
||
'',
|
||
'<img src="docs/diagram.png#gh-light-mode-only">',
|
||
'![Diagram][image]\n\n[image]: docs/diagram.png#gh-dark-mode-only',
|
||
]) {
|
||
test(`image fragments are not Markdown headings: ${image}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': image,
|
||
'docs/diagram.png': 'image fixture',
|
||
}),
|
||
[]
|
||
);
|
||
assert.match(
|
||
(await diagnostics(t, { 'AGENTS.md': image })).join('\n'),
|
||
/does not exist/
|
||
);
|
||
});
|
||
}
|
||
|
||
test('sharing an image reference does not suppress document anchor checks', async (t) => {
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'![Preview][target] [Read][target]\n\n[target]: docs/example.md#missing',
|
||
})
|
||
).join('\n'),
|
||
/missing anchor/
|
||
);
|
||
});
|
||
|
||
test('root literals cannot suppress source-relative definitions', async (t) => {
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
[map]: '`README.md`\n\n[unused]: README.md',
|
||
'README.md': '# Root',
|
||
})
|
||
).join('\n'),
|
||
/does not exist: README.md/
|
||
);
|
||
});
|
||
test('visible HTML text cannot hide inline imports', async (t) => {
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md': '@AGENTS.md\n\n<p>Read @docs/example.md</p>',
|
||
})
|
||
).join('\n'),
|
||
/imports are not allowed/
|
||
);
|
||
});
|
||
for (const html of [
|
||
'<picture><source srcset="docs/dark.png"><img src="docs/light.png"></picture>',
|
||
'<img src="docs/light.png" srcset="docs/light.png 1x, docs/dark.png 2x">',
|
||
]) {
|
||
test(`validates every srcset asset: ${html}`, async (t) => {
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': html,
|
||
'docs/light.png': '',
|
||
})
|
||
).join('\n'),
|
||
/does not exist: docs\/dark.png/
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': html,
|
||
'docs/light.png': '',
|
||
'docs/dark.png': '',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
test('srcset data URLs do not become local filenames', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'<img srcset="data:image/png;base64,AAAA 1x, docs/image.png 2x">',
|
||
'docs/image.png': '',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
test('non-rendered HTML text and code do not introduce imports', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\n<!-- @docs/missing.md -->\n<script>@docs/missing.md</script>\n<style>@docs/missing.md</style>\n<template>@docs/missing.md</template>\n<code>@docs/missing.md</code>',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
test('declared scoped dependencies and aliases are not inline imports', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*', '@nx/devkit': '*' },
|
||
}),
|
||
'tsconfig.base.json': JSON.stringify({
|
||
compilerOptions: { paths: { '@custom/*': ['libs/*'] } },
|
||
}),
|
||
'AGENTS.md': 'Use @angular/core, @nx/* and @custom/services.',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
test('TypeScript scope discovery supports JSONC', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'tsconfig.base.json':
|
||
'{ // comment\n "compilerOptions": { "paths": { "@custom/*": ["libs/*"], }, }, }',
|
||
'AGENTS.md': 'Use @custom/services.',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
for (const target of [
|
||
'angular/../docs/example.md',
|
||
'angular/missing.md',
|
||
'angular/undeclared',
|
||
]) {
|
||
test(`declared scopes do not exempt undeclared imports: ${target}`, async (t) => {
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'CLAUDE.md': '@AGENTS.md\n\nRead @' + target,
|
||
})
|
||
).join('\n'),
|
||
/imports are not allowed/
|
||
);
|
||
});
|
||
}
|
||
|
||
test('Markdown destination entities resolve rendered filenames', async (t) => {
|
||
for (const reference of [
|
||
'[Guide](docs/a&b.md)',
|
||
'[Guide][ref]\n\n[ref]: docs/a&b.md',
|
||
]) {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': reference,
|
||
'docs/a&b.md': '# Guide',
|
||
}),
|
||
[]
|
||
);
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': reference,
|
||
'docs/a&b.md': '# Wrong name',
|
||
})
|
||
).join('\n'),
|
||
/does not exist/
|
||
);
|
||
}
|
||
});
|
||
test('declared packages allow safe subpaths', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'AGENTS.md': 'Use @angular/core/testing.',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
for (const filename of [
|
||
'Dockerfile',
|
||
'Makefile',
|
||
'LICENSE',
|
||
'NOTICE',
|
||
'Procfile',
|
||
]) {
|
||
test(`conventional extensionless filename is validated: ${filename}`, async (t) => {
|
||
assert.match(
|
||
(await diagnostics(t, { 'AGENTS.md': '`' + filename + '`' })).join(
|
||
'\n'
|
||
),
|
||
/does not exist/
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '`' + filename + '`',
|
||
[filename]: '',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const version of [
|
||
'22.1.6',
|
||
'^22.1.6',
|
||
'next',
|
||
'>=22.0.0',
|
||
'<=22.0.0',
|
||
'>22.0.0',
|
||
'<22.0.0',
|
||
]) {
|
||
test(`declared package can include version ${version}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'AGENTS.md': 'Use @angular/core@' + version,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
for (const [path, fragment] of [
|
||
['apps/example.ts', 'L20'],
|
||
['docs/guide.pdf', 'page=3'],
|
||
]) {
|
||
test(`non-Markdown fragments are not headings: ${path}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `[Target](${path}#${fragment})`,
|
||
[path]: 'fixture',
|
||
}),
|
||
[]
|
||
);
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `[Target](${path}#${fragment})`,
|
||
})
|
||
).join('\n'),
|
||
/does not exist/
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const mention of [
|
||
'@marked@17.0.0',
|
||
'@marked@^17.0.0',
|
||
'@angular/core?',
|
||
'@angular/core!',
|
||
]) {
|
||
test(`package mention accepts qualifier or punctuation: ${mention}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { marked: '*', '@angular/core': '*' },
|
||
}),
|
||
'AGENTS.md': 'Use ' + mention,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const content of [
|
||
'[example](docs/missing.md)',
|
||
'[example][ref]\n\n[ref]: docs/missing.md',
|
||
'[unused]: docs/missing.md',
|
||
'`docs/missing.md`',
|
||
'[example][undefined]',
|
||
]) {
|
||
test(`template Markdown is inert: ${content}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '<template>\n\n' + content + '\n\n</template>',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
test('inert headings do not define or consume anchor slugs', async (t) => {
|
||
const body =
|
||
'<template>\n\n# Hidden\n\n# Visible\n\n</template>\n\n# Visible';
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '[Hidden](docs/example.md#hidden)',
|
||
'docs/example.md': body,
|
||
})
|
||
).join('\n'),
|
||
/missing anchor/
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '[Visible](docs/example.md#visible)',
|
||
'docs/example.md': body,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
test('explicit relative literal paths support spaces', async (t) => {
|
||
assert.match(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '`./docs/My Guide.md`',
|
||
})
|
||
).join('\n'),
|
||
/does not exist/
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '`./docs/My Guide.md`',
|
||
'docs/My Guide.md': '',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
for (const filename of ['./docs/Design - Copy.md', './docs/Design -Copy.md']) {
|
||
test(`explicit spaced filename is checked: ${filename}`, async (t) => {
|
||
assert.match(
|
||
(await diagnostics(t, { 'AGENTS.md': '`' + filename + '`' })).join(
|
||
'\n'
|
||
),
|
||
/does not exist/
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '`' + filename + '`',
|
||
[filename]: '',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
for (const html of [
|
||
'<video><source src="docs/demo.mp4"></video>',
|
||
'<video src="docs/demo.mp4"></video>',
|
||
'<audio src="docs/demo.mp4"></audio>',
|
||
]) {
|
||
test(`rendered media source is checked: ${html}`, async (t) => {
|
||
assert.match(
|
||
(await diagnostics(t, { 'AGENTS.md': html })).join('\n'),
|
||
/does not exist/
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, { 'AGENTS.md': html, 'docs/demo.mp4': '' }),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const heading of ['A© B', 'A© B', 'A© B']) {
|
||
test(`semicolonless heading entities decode: ${heading}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '[Heading](docs/example.md#a-b)',
|
||
'docs/example.md': '# ' + heading,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
test('semicolonless definition entities match rendered destinations', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '[ref]: docs/a©.md',
|
||
'docs/a©.md': '',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
for (const html of [
|
||
'<video poster="docs/asset.png"></video>',
|
||
'<video><track src="docs/asset.png"></video>',
|
||
]) {
|
||
test(`remaining media assets are checked: ${html}`, async (t) => {
|
||
assert.match(
|
||
(await diagnostics(t, { 'AGENTS.md': html })).join('\n'),
|
||
/does not exist/
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, { 'AGENTS.md': html, 'docs/asset.png': '' }),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const suffix of ["'s", '’s']) {
|
||
test(`declared package may be possessive: ${suffix}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'AGENTS.md': '@angular/core' + suffix + ' API',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const suffix of ['', '.']) {
|
||
test(`existing extensionless inline import is rejected: ${suffix}`, async (t) => {
|
||
const result = await diagnostics(t, {
|
||
'CLAUDE.md': '@AGENTS.md\n\nRead @INSTRUCTIONS' + suffix,
|
||
INSTRUCTIONS: 'Additional guidance',
|
||
});
|
||
assert.ok(
|
||
result.some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
test('ordinary unknown handle is not a file import', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, { 'AGENTS.md': 'Ask @maintainer' }),
|
||
[]
|
||
);
|
||
});
|
||
|
||
for (const wrapper of ['\\`', '`']) {
|
||
test(`visible literal backticks preserve imports: ${wrapper}`, async (t) => {
|
||
const result = await diagnostics(t, {
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nRead ' + wrapper + '@docs/example.md' + wrapper,
|
||
});
|
||
assert.ok(
|
||
result.some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
for (const filename of [
|
||
'Guide (old).md',
|
||
"Author's guide.md",
|
||
'Guide [draft].md',
|
||
]) {
|
||
test(`explicit path punctuation is validated: ${filename}`, async (t) => {
|
||
const guidance = '`./docs/' + filename + '`';
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': guidance,
|
||
['docs/' + filename]: '# Guide',
|
||
}),
|
||
[]
|
||
);
|
||
assert.ok((await diagnostics(t, { 'AGENTS.md': guidance })).length > 0);
|
||
});
|
||
}
|
||
|
||
for (const target of ['docs/missing.md', 'docs/example.md#missing']) {
|
||
test(`image-map navigation is validated: ${target}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `<map><area href="${target}"></map>`,
|
||
})
|
||
).length > 0
|
||
);
|
||
});
|
||
}
|
||
test('valid image-map navigation and inert areas pass', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'<map><area href="docs/example.md#repeat"></map>\n<template><map><area href="missing.md"></map></template>',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
for (const ending of ['\n', '\r\n', '\r']) {
|
||
for (const [source, limit] of [
|
||
['AGENTS.md', 200],
|
||
['CLAUDE.md', 30],
|
||
]) {
|
||
test(`line budget handles ${JSON.stringify(ending)} in ${source}`, async (t) => {
|
||
const lines = [
|
||
source === 'CLAUDE.md' ? '@AGENTS.md' : '# Guidance',
|
||
...Array(limit - 1).fill('# Section'),
|
||
];
|
||
assert.deepEqual(
|
||
await diagnostics(t, { [source]: lines.join(ending) + ending }),
|
||
[]
|
||
);
|
||
const result = await diagnostics(t, {
|
||
[source]: [...lines, '# Extra'].join(ending) + ending,
|
||
});
|
||
assert.ok(
|
||
result.some((message) =>
|
||
message.includes(
|
||
`at most ${limit} lines allowed (received ${limit + 1})`
|
||
)
|
||
)
|
||
);
|
||
});
|
||
}
|
||
}
|
||
|
||
for (const punctuation of ['—its', '–its', '…', ',next', '。next', '”']) {
|
||
test(`package mention stops at Unicode punctuation: ${punctuation}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'AGENTS.md': 'Use @angular/core' + punctuation + ' APIs',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const suffix of ['—then', ',next', '”']) {
|
||
test(`extensionless imports stop at Unicode punctuation: ${suffix}`, async (t) => {
|
||
const result = await diagnostics(t, {
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nRead @INSTRUCTIONS' + suffix + ' continue',
|
||
INSTRUCTIONS: 'Additional guidance',
|
||
});
|
||
assert.ok(
|
||
result.some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const name of ['INSTRUCTIONS—v2', 'INSTRUCTIONS,extra']) {
|
||
test(`full punctuation filename remains an import: ${name}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md': '@AGENTS.md\n\nRead @' + name,
|
||
[name]: 'Guidance',
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
for (const suffix of [',then', ';then', ':then', '!then', '?then']) {
|
||
test(`ASCII prose boundary handles imports and packages: ${suffix}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md': '@AGENTS.md\n\nRead @INSTRUCTIONS' + suffix,
|
||
INSTRUCTIONS: 'Guidance',
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': 'Use @angular/core' + suffix + ' continue',
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const target of ['docs/missing.html', 'docs/example.md#missing']) {
|
||
test(`iframe document target is validated: ${target}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `<iframe src="${target}"></iframe>`,
|
||
})
|
||
).length > 0
|
||
);
|
||
});
|
||
}
|
||
test('valid iframe and inert iframe targets pass', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'<iframe src="docs/example.md#repeat"></iframe>\n<template><iframe src="missing.html"></iframe></template>',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
for (const suffix of [
|
||
'.md#rules',
|
||
'.md?view=raw#rules',
|
||
'.txt#rules',
|
||
'.json#rules',
|
||
]) {
|
||
test(`package document imports retain suffix guards: ${suffix}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nRead @angular/core/docs/extra' + suffix,
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const suffix of [
|
||
'.markdown',
|
||
'.mdown',
|
||
'.mkd',
|
||
'.md%23rules',
|
||
'.md%3Fraw',
|
||
'%2Emd',
|
||
'/%2e%2e/extra.md',
|
||
]) {
|
||
test(`document package exemptions reject alternate and encoded paths: ${suffix}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nRead @angular/core/docs/extra' + suffix,
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const version of ['*', '22.*', '22.1.*']) {
|
||
test(`wildcard package version is prose: ${version}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'AGENTS.md': 'Use @angular/core@' + version,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
for (const extension of ['rst', 'rest', 'adoc', 'asciidoc']) {
|
||
test(`non-Markdown document is not a package exemption: ${extension}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nRead @angular/core/docs/guide.' +
|
||
extension,
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const extension of ['pdf', 'doc', 'docx', 'odt', 'rtf', 'org', 'tex']) {
|
||
test(`document format is not a package exemption: ${extension}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nRead @angular/core/docs/guide.' +
|
||
extension,
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
for (const html of [
|
||
'<a href=" docs/example.md#repeat ">Guide</a>',
|
||
'<iframe src="	docs/example.md "></iframe>',
|
||
'<img src=" docs/example.md ">',
|
||
]) {
|
||
test(`HTML URL edge whitespace is ignored: ${html}`, async (t) => {
|
||
assert.deepEqual(await diagnostics(t, { 'AGENTS.md': html }), []);
|
||
});
|
||
}
|
||
|
||
for (const name of [
|
||
'guide.ods',
|
||
'guide.odp',
|
||
'guide.docm',
|
||
'guide.dotx',
|
||
'guide.latex',
|
||
'AGENTS',
|
||
'CLAUDE',
|
||
'README',
|
||
'INSTRUCTIONS',
|
||
'LICENSE',
|
||
]) {
|
||
test(`package subpath cannot import guidance document ${name}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nRead @angular/core/docs/' + name,
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const reference of [
|
||
'',
|
||
'<img src="docs/">',
|
||
'<video src="docs/"></video>',
|
||
'<video poster="docs/"></video>',
|
||
]) {
|
||
test(`media target must be a file: ${reference}`, async (t) => {
|
||
assert.ok(
|
||
(await diagnostics(t, { 'AGENTS.md': reference })).some((message) =>
|
||
message.includes('not a file')
|
||
)
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const html of [
|
||
'<details><summary>Use @angular/core</summary><p>for Angular</p></details>',
|
||
'<table><tr><td>Use @angular/core</td><td>for Angular</td></tr></table>',
|
||
'<section>Use @angular/core</section><aside>for Angular</aside>',
|
||
]) {
|
||
test(`HTML blocks separate package prose: ${html}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': html,
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
for (const tag of ['object', 'embed']) {
|
||
test(`embedded document targets are checked: ${tag}`, async (t) => {
|
||
const markup = (target) =>
|
||
`<${tag} ${tag === 'object' ? 'data' : 'src'}="${target}"></${tag}>`;
|
||
for (const target of ['docs/missing.pdf', 'docs/example.md#missing']) {
|
||
assert.ok(
|
||
(await diagnostics(t, { 'AGENTS.md': markup(target) })).length >
|
||
0
|
||
);
|
||
}
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': markup('docs/example.md#repeat'),
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const entity of ['	', ' ', ' ']) {
|
||
test(`HTML URLs remove internal tab/newline: ${entity}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `<a href="docs/exa${entity}mple.md#repeat">Guide</a>`,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const markup of ['![logo]()', '<img src="">', '<video src=" "></video>']) {
|
||
test(`empty media references are rejected: ${markup}`, async (t) => {
|
||
assert.ok((await diagnostics(t, { 'AGENTS.md': markup })).length > 0);
|
||
});
|
||
}
|
||
for (const url of [
|
||
'https://example.com/@docs/guide',
|
||
'https://example.com/user?next=@docs/guide',
|
||
'//example.com/@docs/guide',
|
||
]) {
|
||
test(`external URL at-sign is not an import: ${url}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, { 'AGENTS.md': 'Visit ' + url }),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const value of ['', ' ']) {
|
||
test(`empty srcset is rejected: ${JSON.stringify(value)}`, async (t) => {
|
||
assert.ok(
|
||
(await diagnostics(t, { 'AGENTS.md': `<img srcset="${value}">` }))
|
||
.length > 0
|
||
);
|
||
});
|
||
}
|
||
for (const content of [
|
||
"<a href='docs/missing.md'>Guide</a>",
|
||
"<img src='missing.png'>",
|
||
"<a href='#missing'>Guide</a>",
|
||
]) {
|
||
test(`srcdoc references are validated: ${content}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `<iframe srcdoc="${content}"></iframe>`,
|
||
})
|
||
).length > 0
|
||
);
|
||
});
|
||
}
|
||
test('srcdoc anchors are scoped and templates inert', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
"<iframe srcdoc=\"<p id=local>Hi</p><a href='#local'>Go</a><template><img src='missing.png'></template>\"></iframe>",
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
test('autolink does not hide adjacent import', async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\n<https://example.com>).@docs/guide.md',
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
test('srcdoc base resolves local document paths', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
"<iframe srcdoc=\"<base href='docs/'><a href='example.md#repeat'>Go</a>\"></iframe>",
|
||
}),
|
||
[]
|
||
);
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
"<iframe srcdoc=\"<base href='missing/'><a href='docs/example.md'>Go</a>\"></iframe>",
|
||
})
|
||
).length > 0
|
||
);
|
||
});
|
||
|
||
test('srcdoc bases retain remote and repository containment rules', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
"<iframe srcdoc=\"<base href='https://example.com/'><a href='guide.md'>Go</a>\"></iframe>",
|
||
}),
|
||
[]
|
||
);
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
"<iframe srcdoc=\"<base href='../'><a href='guide.md'>Go</a>\"></iframe>",
|
||
})
|
||
).some((message) => message.includes('escapes repository'))
|
||
);
|
||
});
|
||
|
||
test('local HTML base uses native filesystem paths', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'<base href="docs/"><a href="native%20%23%25.md#heading">Guide</a>',
|
||
'docs/native #%.md': '# Heading',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
for (const boundary of [').', '];', '}']) {
|
||
test(`bare URL preserves adjacent import after ${boundary}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nhttps://example.com/path' +
|
||
boundary +
|
||
'@docs/guide.md',
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const uri of [
|
||
'<mailto:user@example.com?subject=@docs/guide.md>',
|
||
'<urn:example:@docs/guide.md>',
|
||
]) {
|
||
test(`opaque URI is not a guidance import: ${uri}`, async (t) => {
|
||
assert.deepEqual(await diagnostics(t, { 'AGENTS.md': uri }), []);
|
||
});
|
||
}
|
||
test('colon directly before an import remains checked', async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md': '@AGENTS.md\n\nRead:@docs/guide.md',
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
|
||
test('colon-labeled prose cannot hide imports', async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md': '@AGENTS.md\n\nFallback:then;@docs/guide.md',
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
|
||
for (const opening of ['(', '[', '{']) {
|
||
test(`opening delimiter separates package and import prose: ${opening}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'AGENTS.md': 'Use @angular/core' + opening + 'test helpers)',
|
||
}),
|
||
[]
|
||
);
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nRead @INSTRUCTIONS' +
|
||
opening +
|
||
'then continue)',
|
||
INSTRUCTIONS: 'Guidance',
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
|
||
test('package prose cannot hide nested guidance import', async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'CLAUDE.md': '@AGENTS.md\n\nUse @angular/core(@INSTRUCTIONS)',
|
||
INSTRUCTIONS: 'Guidance',
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
for (const extension of ['pdf', 'rst', 'adoc', 'markdown', 'docm', 'latex']) {
|
||
test(`bare document literal is checked: ${extension}`, async (t) => {
|
||
const name = 'manual.' + extension;
|
||
assert.ok(
|
||
(await diagnostics(t, { 'AGENTS.md': '`' + name + '`' })).length > 0
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '`' + name + '`',
|
||
[name]: 'Document',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const markup of [
|
||
'',
|
||
'<img src="?v=1">',
|
||
'[Guide](file:///tmp/missing.md)',
|
||
'<a href="file:///etc/hosts">Guide</a>',
|
||
]) {
|
||
test(`nonportable or pathless target is rejected: ${markup}`, async (t) => {
|
||
assert.ok((await diagnostics(t, { 'AGENTS.md': markup })).length > 0);
|
||
});
|
||
}
|
||
|
||
for (const quote of ['"', "'", '”']) {
|
||
test(`quoted URL preserves adjacent import: ${quote}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nVisit ' +
|
||
quote +
|
||
'https://example.com/path' +
|
||
quote +
|
||
'.@docs/guide.md',
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
for (const embedded of [false, true]) {
|
||
test(`file URL base is rejected: embedded=${embedded}`, async (t) => {
|
||
const rootDir = await fixture(t);
|
||
const { pathToFileURL } = await import('node:url');
|
||
const { realpath } = await import('node:fs/promises');
|
||
const base = pathToFileURL((await realpath(rootDir)) + '/').href;
|
||
const html = `<base href='${base}'><a href='docs/example.md'>Guide</a>`;
|
||
await writeFile(
|
||
join(rootDir, 'AGENTS.md'),
|
||
embedded ? `<iframe srcdoc="${html}"></iframe>` : html
|
||
);
|
||
assert.ok(
|
||
(await validateAgentGuidance({ rootDir })).diagnostics.length > 0
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const url of [
|
||
"https://example.com/don't@docs/guide.md",
|
||
"https://example.com/path'@docs/guide.md",
|
||
]) {
|
||
test(`internal URL apostrophe remains URL prose: ${url}`, async (t) => {
|
||
assert.deepEqual(await diagnostics(t, { 'AGENTS.md': url }), []);
|
||
});
|
||
}
|
||
for (const name of ['readme', 'instructions', 'Agents']) {
|
||
test(`guidance basename matching ignores case: ${name}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nRead @angular/core/docs/' + name,
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
test('image input source is validated', async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '<input type="IMAGE" src="missing.png">',
|
||
})
|
||
).length > 0
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': '<input type="text" src="missing.png">',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
for (const attribute of ['href', 'xlink:href']) {
|
||
test(`SVG image reference is checked: ${attribute}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `<svg><image ${attribute}="missing.png"/></svg>`,
|
||
})
|
||
).length > 0
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `<svg><image ${attribute}="docs/example.md"/></svg>`,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
for (const name of ['CONTRIBUTING', 'SECURITY', 'code_of_conduct', 'SUPPORT']) {
|
||
test(`conventional guidance basename is not a package: ${name}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@angular/core': '*' },
|
||
}),
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\nRead @angular/core/docs/' + name,
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
|
||
test('versioned declared package may share a guidance basename', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'package.json': JSON.stringify({
|
||
dependencies: { '@scope/support': '*' },
|
||
}),
|
||
'AGENTS.md': 'Use @scope/support@^2',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
test('federated handle is not an import', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, { 'AGENTS.md': 'Contact @alice@example.social' }),
|
||
[]
|
||
);
|
||
});
|
||
for (const attribute of ['href', 'xlink:href']) {
|
||
test(`SVG use references are checked: ${attribute}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `<svg><use ${attribute}="docs/missing.svg#icon"/></svg>`,
|
||
})
|
||
).length > 0
|
||
);
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': `<svg><symbol id="icon"></symbol><use ${attribute}="#icon"/></svg>`,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
test('srcdoc SVG use keeps its own anchors', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'<iframe srcdoc="<svg><symbol id=icon></symbol><use href=\'#icon\'/></svg>"></iframe>',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
|
||
for (const suffix of [').', '];', '”']) {
|
||
test(`federated handle accepts closing punctuation: ${suffix}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': 'Contact (@alice@example.social' + suffix,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
test('multiple at-signs cannot disguise an adjacent document import', async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md': '@AGENTS.md\n\n@guide.md@alice@example.social',
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
|
||
for (const suffix of ['…', '。', '!', '—']) {
|
||
test(`federated handle accepts Unicode sentence ending: ${suffix}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': 'Contact @alice@example.social' + suffix,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const suffix of ["'s", '’s']) {
|
||
test(`federated handle may be possessive: ${suffix}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md':
|
||
'Contact @alice@example.social' + suffix + ' administrator',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const suffix of ['(admin)', '[admin]', '{admin}']) {
|
||
test(`federated handle permits parenthetical prose: ${suffix}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': 'Contact @alice@example.social' + suffix,
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
test('federated handle cannot hide a nested import', async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md':
|
||
'@AGENTS.md\n\n@alice@example.social(@INSTRUCTIONS)',
|
||
INSTRUCTIONS: 'Guidance',
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
|
||
test('www autolinks do not introduce guidance imports', async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': 'See www.example.com/@docs/guide',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
for (const prose of [
|
||
'www.example.com/path).@docs/guide.md',
|
||
'"www.example.com/path".@docs/guide.md',
|
||
]) {
|
||
test(`www URL preserves adjacent import: ${prose}`, async (t) => {
|
||
assert.ok(
|
||
(
|
||
await diagnostics(t, {
|
||
'CLAUDE.md': '@AGENTS.md\n\n' + prose,
|
||
})
|
||
).some((message) => message.includes('additional or inline'))
|
||
);
|
||
});
|
||
}
|
||
for (const prose of [
|
||
'foo@INSTRUCTIONS',
|
||
'@alice@example.social(foo@INSTRUCTIONS)',
|
||
'@alice@example.social(email@example.com)',
|
||
]) {
|
||
test(`embedded at-sign is not an import boundary: ${prose}`, async (t) => {
|
||
assert.deepEqual(
|
||
await diagnostics(t, {
|
||
'AGENTS.md': prose,
|
||
INSTRUCTIONS: 'Guidance',
|
||
}),
|
||
[]
|
||
);
|
||
});
|
||
}
|
||
|
||
for (const reference of [
|
||
'[Guide](C:/workspace/docs/missing.md)',
|
||
'<a href="c:\\workspace\\docs\\missing.md">Guide</a>',
|
||
'<base href="https://example.com/"><a href="D:/docs/guide.md">Guide</a>',
|
||
'<base href="C:/workspace/"><a href="docs/example.md">Guide</a>',
|
||
'<base href="c:\\workspace\\"><a href="docs/example.md">Guide</a>',
|
||
]) {
|
||
test(`Windows drive paths require portable references: ${reference}`, async (t) => {
|
||
assert.ok(
|
||
(await diagnostics(t, { 'AGENTS.md': reference })).some((message) =>
|
||
message.includes('repository-relative')
|
||
)
|
||
);
|
||
});
|
||
}
|