forked from mosaicstack/stack
665 lines
22 KiB
TypeScript
665 lines
22 KiB
TypeScript
import { readFile, readdir } from 'node:fs/promises';
|
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { parseRosterV2, validateRosterV2Semantics } from './roster-v2.js';
|
|
|
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const repositoryRoot = resolve(packageRoot, '..', '..');
|
|
const fleetDocs = join(repositoryRoot, 'docs', 'fleet');
|
|
const frameworkFleet = join(packageRoot, 'framework', 'fleet');
|
|
|
|
const REQUIRED_FLEET_PAGES = [
|
|
'README.md',
|
|
'concepts/desired-vs-observed-state.md',
|
|
'concepts/identity-class-runtime.md',
|
|
'concepts/role-authority-and-leases.md',
|
|
'concepts/generated-env-launch-chain.md',
|
|
'reference/roster-v2.schema.json',
|
|
'reference/roster-v2-fields.md',
|
|
'reference/cli.md',
|
|
'reference/role-classes.md',
|
|
'reference/lifecycle-transitions.md',
|
|
'reference/status-and-drift.md',
|
|
'how-to/create-update-delete-agent.md',
|
|
'how-to/start-stop-restart.md',
|
|
'how-to/configure-tess-interaction.md',
|
|
'how-to/configure-ultron-validator.md',
|
|
'how-to/customize-roles.md',
|
|
'operations/reconcile-and-recover.md',
|
|
'operations/env-quarantine.md',
|
|
'operations/systemd-tmux-troubleshooting.md',
|
|
'operations/backup-restore.md',
|
|
'operations/upgrade-assets.md',
|
|
'migration/v1-to-v2.md',
|
|
'migration/example-profile-disposition.md',
|
|
'migration/legacy-class-aliases.md',
|
|
] as const;
|
|
|
|
async function markdownFiles(root: string): Promise<string[]> {
|
|
const entries = await readdir(root, { withFileTypes: true });
|
|
const paths = await Promise.all(
|
|
entries.map(async (entry): Promise<string[]> => {
|
|
const path = join(root, entry.name);
|
|
if (entry.isDirectory()) return markdownFiles(path);
|
|
return extname(entry.name) === '.md' ? [path] : [];
|
|
}),
|
|
);
|
|
return paths.flat().sort();
|
|
}
|
|
|
|
function localMarkdownTargets(source: string): string[] {
|
|
const link =
|
|
/\[[^\]]*\]\(\s*(?:<([^>]+)>|((?:\\.|[^()\s]|\([^()]*\))+))(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\s*\)/g;
|
|
return [...source.matchAll(link)]
|
|
.map((match): string => match[1] ?? match[2] ?? '')
|
|
.filter(
|
|
(target): boolean =>
|
|
target !== '' &&
|
|
!target.startsWith('http://') &&
|
|
!target.startsWith('https://') &&
|
|
!target.startsWith('mailto:'),
|
|
);
|
|
}
|
|
|
|
function markdownHeadingAnchors(source: string): Set<string> {
|
|
const anchors = new Set<string>();
|
|
let fence: { readonly marker: string; readonly length: number } | undefined;
|
|
|
|
for (const line of source.split('\n')) {
|
|
const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
|
|
if (fence === undefined && fenceMatch !== null) {
|
|
const run = fenceMatch[1] ?? '';
|
|
fence = { marker: run[0] ?? '', length: run.length };
|
|
continue;
|
|
}
|
|
if (fence !== undefined) {
|
|
const closingRun = line.match(/^\s{0,3}(`{3,}|~{3,})\s*$/)?.[1];
|
|
if (
|
|
closingRun !== undefined &&
|
|
closingRun[0] === fence.marker &&
|
|
closingRun.length >= fence.length
|
|
) {
|
|
fence = undefined;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const heading = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/)?.[1];
|
|
if (heading === undefined) continue;
|
|
const base = heading
|
|
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
.replace(/<[^>]*>/g, '')
|
|
.replace(/[`*_~]/g, '')
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(/[^\p{L}\p{N}\s-]/gu, '')
|
|
.replace(/\s+/g, '-');
|
|
let anchor = base;
|
|
let duplicate = 0;
|
|
while (anchors.has(anchor)) {
|
|
duplicate += 1;
|
|
anchor = `${base}-${duplicate}`;
|
|
}
|
|
anchors.add(anchor);
|
|
}
|
|
return anchors;
|
|
}
|
|
|
|
function markdownLinkViolations(
|
|
sourcePath: string,
|
|
source: string,
|
|
documents: Readonly<Record<string, string>>,
|
|
): string[] {
|
|
const violations: string[] = [];
|
|
for (const target of localMarkdownTargets(source)) {
|
|
const [encodedPath = '', encodedFragment] = target.split('#', 2);
|
|
const targetPath = decodeURIComponent(encodedPath);
|
|
const normalizedTarget = resolve('/', dirname(sourcePath), targetPath).slice(1);
|
|
const targetSource = documents[normalizedTarget];
|
|
if (targetSource === undefined) {
|
|
violations.push(`${sourcePath} -> ${target}: missing file`);
|
|
continue;
|
|
}
|
|
if (encodedFragment !== undefined) {
|
|
const fragment = decodeURIComponent(encodedFragment);
|
|
if (fragment === '' || !markdownHeadingAnchors(targetSource).has(fragment)) {
|
|
violations.push(`${sourcePath} -> ${target}: missing heading`);
|
|
}
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
function commandBasename(token: string): string {
|
|
return token.slice(token.lastIndexOf('/') + 1);
|
|
}
|
|
|
|
function shellCommandSegments(source: string): string[] {
|
|
const segments: string[] = [];
|
|
let segment = '';
|
|
let quote = '';
|
|
let escaped = false;
|
|
|
|
const flush = (): void => {
|
|
if (segment.trim() !== '') segments.push(segment);
|
|
segment = '';
|
|
};
|
|
|
|
for (const character of source) {
|
|
if (escaped) {
|
|
segment += character;
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (character === '\\' && quote !== "'") {
|
|
segment += character;
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (quote !== '') {
|
|
segment += character;
|
|
if (character === quote) quote = '';
|
|
continue;
|
|
}
|
|
if (character === "'" || character === '"') {
|
|
segment += character;
|
|
quote = character;
|
|
continue;
|
|
}
|
|
if (character === '\n' || character === ';' || character === '&' || character === '|') {
|
|
flush();
|
|
continue;
|
|
}
|
|
segment += character;
|
|
}
|
|
flush();
|
|
return segments;
|
|
}
|
|
|
|
function shellCommandTokens(source: string): string[] {
|
|
const tokens: string[] = [];
|
|
let token = '';
|
|
let quote = '';
|
|
let escaped = false;
|
|
|
|
for (const character of source) {
|
|
if (escaped) {
|
|
token += character;
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (character === '\\' && quote !== "'") {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (quote !== '') {
|
|
if (character === quote) quote = '';
|
|
else token += character;
|
|
continue;
|
|
}
|
|
if (character === "'" || character === '"') {
|
|
quote = character;
|
|
continue;
|
|
}
|
|
if (/\s/.test(character)) {
|
|
if (token !== '') {
|
|
tokens.push(token);
|
|
token = '';
|
|
}
|
|
continue;
|
|
}
|
|
token += character;
|
|
}
|
|
if (escaped) token += '\\';
|
|
if (token !== '') tokens.push(token);
|
|
return tokens;
|
|
}
|
|
|
|
function normalizedCommandTokens(segment: string): string[] {
|
|
const tokens = shellCommandTokens(segment.trim().replace(/^(?:[$#>]\s*)/, ''));
|
|
let index = 0;
|
|
|
|
while (index < tokens.length) {
|
|
const wrapper = commandBasename(tokens[index] ?? '');
|
|
if (wrapper === 'env') {
|
|
index += 1;
|
|
while (index < tokens.length) {
|
|
const token = tokens[index] ?? '';
|
|
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (token === '--') {
|
|
index += 1;
|
|
break;
|
|
}
|
|
if (/^(?:--unset|--chdir)$/.test(token)) {
|
|
index += 2;
|
|
continue;
|
|
}
|
|
if (/^(?:--unset=|--chdir=)/.test(token)) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (token === '--split-string') {
|
|
const splitTokens = shellCommandTokens(tokens[index + 1] ?? '');
|
|
tokens.splice(index, 2, ...splitTokens);
|
|
continue;
|
|
}
|
|
const longSplitString = token.match(/^--split-string=(.*)$/)?.[1];
|
|
if (longSplitString !== undefined) {
|
|
tokens.splice(index, 1, ...shellCommandTokens(longSplitString));
|
|
continue;
|
|
}
|
|
if (/^-[^-]/.test(token)) {
|
|
const options = token.slice(1);
|
|
let consumed = false;
|
|
for (let optionIndex = 0; optionIndex < options.length; optionIndex += 1) {
|
|
const option = options[optionIndex] ?? '';
|
|
if (option === 'u' || option === 'C') {
|
|
index += options.slice(optionIndex + 1) === '' ? 2 : 1;
|
|
consumed = true;
|
|
break;
|
|
}
|
|
if (option === 'S') {
|
|
const attached = options.slice(optionIndex + 1);
|
|
const operand = attached === '' ? (tokens[index + 1] ?? '') : attached;
|
|
tokens.splice(index, attached === '' ? 2 : 1, ...shellCommandTokens(operand));
|
|
consumed = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!consumed) index += 1;
|
|
continue;
|
|
}
|
|
if (token.startsWith('-')) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
if (wrapper === 'command') {
|
|
index += 1;
|
|
let availabilityQuery = false;
|
|
while ((tokens[index] ?? '').startsWith('-')) {
|
|
const option = tokens[index] ?? '';
|
|
index += 1;
|
|
if (option === '--') break;
|
|
if (/^-[^-]*[vV]/.test(option)) availabilityQuery = true;
|
|
}
|
|
if (availabilityQuery) return [];
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return tokens.slice(index);
|
|
}
|
|
|
|
function containsPrivilegedCommand(source: string): boolean {
|
|
const privilegedCommands = new Set([
|
|
'sudo',
|
|
'doas',
|
|
'pkexec',
|
|
'systemctl',
|
|
'service',
|
|
'mount',
|
|
'umount',
|
|
'reboot',
|
|
'shutdown',
|
|
'poweroff',
|
|
'halt',
|
|
'chown',
|
|
'chmod',
|
|
'chroot',
|
|
'useradd',
|
|
'usermod',
|
|
'groupadd',
|
|
'visudo',
|
|
'apt',
|
|
'apt-get',
|
|
'apt-cache',
|
|
'dpkg',
|
|
'yum',
|
|
'dnf',
|
|
'rpm',
|
|
'apk',
|
|
'pacman',
|
|
'zypper',
|
|
'emerge',
|
|
'snap',
|
|
'flatpak',
|
|
'brew',
|
|
'nix-env',
|
|
]);
|
|
|
|
return shellCommandSegments(source).some((segment): boolean => {
|
|
const tokens = normalizedCommandTokens(segment);
|
|
const command = commandBasename(tokens[0] ?? '');
|
|
if (privilegedCommands.has(command)) return true;
|
|
if (command !== 'su') return false;
|
|
|
|
let index = 1;
|
|
while (index < tokens.length) {
|
|
const token = tokens[index] ?? '';
|
|
if (token === '--') {
|
|
index += 1;
|
|
break;
|
|
}
|
|
if (
|
|
/^(?:--command|--session-command|--group|--supp-group|--shell|--whitelist-environment)$/.test(
|
|
token,
|
|
)
|
|
) {
|
|
index += 2;
|
|
continue;
|
|
}
|
|
if (
|
|
/^(?:--command=|--session-command=|--group=|--supp-group=|--shell=|--whitelist-environment=)/.test(
|
|
token,
|
|
)
|
|
) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (/^-[^-]/.test(token)) {
|
|
const options = token.slice(1);
|
|
let consumed = false;
|
|
for (let optionIndex = 0; optionIndex < options.length; optionIndex += 1) {
|
|
const option = options[optionIndex] ?? '';
|
|
if ('cgGsw'.includes(option)) {
|
|
index += options.slice(optionIndex + 1) === '' ? 2 : 1;
|
|
consumed = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!consumed) index += 1;
|
|
continue;
|
|
}
|
|
if (token.startsWith('-')) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
const user = tokens[index];
|
|
return user === undefined || user === 'root';
|
|
});
|
|
}
|
|
|
|
function exampleSafetyViolationKinds(source: string): string[] {
|
|
const kinds = new Set<string>();
|
|
const sensitiveKey = /(?:secret|token|password|credential|MOSAIC_AGENT_COMMAND)/i;
|
|
const credentialFormat =
|
|
/(?:\bAKIA[0-9A-Z]{16}\b|\bAIza[0-9A-Za-z_-]{35}\b|\bgh[pousr]_[A-Za-z0-9]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}\b|\bglpat-[A-Za-z0-9_-]{20,}\b|\bnpm_[A-Za-z0-9]{20,}\b|\bsk-ant-(?:api\d{2}-)?[A-Za-z0-9_-]{20,}\b|\bsk-proj-[A-Za-z0-9_-]{20,}\b|\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b|\bxox[baprs]-[A-Za-z0-9-]{10,}\b|\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b|\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b|-----BEGIN [A-Z ]*PRIVATE KEY-----|\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s/:]+:[^\s/@]+@)/;
|
|
|
|
if (sensitiveKey.test(source)) kinds.add('sensitive-key');
|
|
if (credentialFormat.test(source)) kinds.add('credential-format');
|
|
if (containsPrivilegedCommand(source)) kinds.add('privileged-command');
|
|
if (/\b(?:Tess|Ultron)\b/.test(source)) kinds.add('identity');
|
|
return [...kinds].sort();
|
|
}
|
|
|
|
function fencedCodeBlocks(source: string): string[] {
|
|
const blocks: string[] = [];
|
|
let fence: { readonly marker: string; readonly length: number } | undefined;
|
|
let lines: string[] = [];
|
|
|
|
for (const line of source.split('\n')) {
|
|
const openingRun = line.match(/^\s{0,3}(`{3,}|~{3,})[^\n]*$/)?.[1];
|
|
if (fence === undefined && openingRun !== undefined) {
|
|
fence = { marker: openingRun[0] ?? '', length: openingRun.length };
|
|
lines = [];
|
|
continue;
|
|
}
|
|
if (fence === undefined) continue;
|
|
|
|
const closingRun = line.match(/^\s{0,3}(`{3,}|~{3,})\s*$/)?.[1];
|
|
if (
|
|
closingRun !== undefined &&
|
|
closingRun[0] === fence.marker &&
|
|
closingRun.length >= fence.length
|
|
) {
|
|
blocks.push(lines.join('\n'));
|
|
fence = undefined;
|
|
lines = [];
|
|
continue;
|
|
}
|
|
lines.push(line);
|
|
}
|
|
|
|
if (fence !== undefined) blocks.push(lines.join('\n'));
|
|
return blocks;
|
|
}
|
|
|
|
const UNSAFE_EXAMPLE_FIXTURES = [
|
|
{
|
|
expected: 'privileged-command',
|
|
source: ['cd /srv && su', 'do systemctl restart example'].join(''),
|
|
},
|
|
{ expected: 'privileged-command', source: '/usr/bin/sudo systemctl restart example' },
|
|
{ expected: 'privileged-command', source: '/usr/bin/apt-get install example' },
|
|
{
|
|
expected: 'privileged-command',
|
|
source: '/usr/bin/env -i /usr/bin/command -- /usr/bin/apt-get install example',
|
|
},
|
|
{
|
|
expected: 'privileged-command',
|
|
source: '/usr/bin/env -u FOO /usr/bin/apt-get install example',
|
|
},
|
|
{
|
|
expected: 'privileged-command',
|
|
source: 'env -iu FOO /usr/bin/apt-get --version',
|
|
},
|
|
{
|
|
expected: 'privileged-command',
|
|
source: 'env -ivS/usr/bin/apt-get --version',
|
|
},
|
|
{
|
|
expected: 'privileged-command',
|
|
source: 'env --unset FOO /usr/bin/apt-get install example',
|
|
},
|
|
{ expected: 'privileged-command', source: 'env -S apt-get install example' },
|
|
{ expected: 'privileged-command', source: "env -S 'apt-get update'" },
|
|
{ expected: 'privileged-command', source: "env -S'/usr/bin/apt-get --version'" },
|
|
{
|
|
expected: 'privileged-command',
|
|
source: "env --split-string='apt-get update'",
|
|
},
|
|
{
|
|
expected: 'privileged-command',
|
|
source: "env --split-string='-i /usr/bin/apt-get update'",
|
|
},
|
|
{
|
|
expected: 'privileged-command',
|
|
source: "env --split-string='FOO=x /usr/bin/apt-get update'",
|
|
},
|
|
{
|
|
expected: 'privileged-command',
|
|
source: "env --split-string='-- /usr/bin/apt-get update'",
|
|
},
|
|
{ expected: 'privileged-command', source: 'reboot' },
|
|
{ expected: 'privileged-command', source: 'su --command=/bin/sh' },
|
|
{ expected: 'privileged-command', source: 'su -lc /usr/bin/id' },
|
|
{ expected: 'privileged-command', source: 'su -l root' },
|
|
{ expected: 'privileged-command', source: 'su -l -s /bin/sh root' },
|
|
{ expected: 'privileged-command', source: '/usr/bin/su --login root' },
|
|
{ expected: 'privileged-command', source: 'su root' },
|
|
{ expected: 'privileged-command', source: '/usr/sbin/chroot /srv/example' },
|
|
{ expected: 'credential-format', source: ['ghp_', 'a'.repeat(36)].join('') },
|
|
{
|
|
expected: 'credential-format',
|
|
source: ['sk-ant-api03-', 'a'.repeat(80)].join(''),
|
|
},
|
|
{
|
|
expected: 'credential-format',
|
|
source: ['sk-proj-', 'a'.repeat(80)].join(''),
|
|
},
|
|
{
|
|
expected: 'credential-format',
|
|
source: ['rk_live_', 'a'.repeat(24)].join(''),
|
|
},
|
|
{
|
|
expected: 'credential-format',
|
|
source: ['eyJ', 'a'.repeat(12), '.', 'b'.repeat(12), '.', 'c'.repeat(12)].join(''),
|
|
},
|
|
] as const;
|
|
|
|
describe('documentation validation regressions', (): void => {
|
|
it('rejects a local Markdown link whose heading fragment does not exist', (): void => {
|
|
expect(
|
|
markdownLinkViolations('docs/fleet/source.md', '[broken](target.md#missing)', {
|
|
'docs/fleet/target.md': '# Present',
|
|
}),
|
|
).toEqual(['docs/fleet/source.md -> target.md#missing: missing heading']);
|
|
});
|
|
|
|
it('accepts balanced-parenthesis link destinations and visible-text heading anchors', (): void => {
|
|
expect(localMarkdownTargets('[guide](guide-(legacy).md#setup "Guide")')).toEqual([
|
|
'guide-(legacy).md#setup',
|
|
]);
|
|
expect(markdownHeadingAnchors('# [Fleet API](cli.md) behavior')).toContain(
|
|
'fleet-api-behavior',
|
|
);
|
|
});
|
|
|
|
it('keeps longer fenced blocks closed only by an equal-or-longer fence', (): void => {
|
|
expect(markdownHeadingAnchors('````markdown\n```\n# Not a heading\n````\n# Present')).toEqual(
|
|
new Set(['present']),
|
|
);
|
|
});
|
|
|
|
it('assigns a free suffix when a prior heading already occupies the next duplicate anchor', (): void => {
|
|
expect(markdownHeadingAnchors('# Foo\n# Foo-1\n# Foo')).toEqual(
|
|
new Set(['foo', 'foo-1', 'foo-2']),
|
|
);
|
|
});
|
|
|
|
it('allows command availability queries without classifying the queried command', (): void => {
|
|
expect(exampleSafetyViolationKinds('command -v apt-get')).not.toContain('privileged-command');
|
|
});
|
|
|
|
it('keeps quoted command separators as inert argument data', (): void => {
|
|
expect(
|
|
exampleSafetyViolationKinds("printf '%s\\n' 'safe; /usr/bin/apt-get update'"),
|
|
).not.toContain('privileged-command');
|
|
});
|
|
|
|
it('still detects commands after unquoted command separators', (): void => {
|
|
expect(exampleSafetyViolationKinds('printf safe; /usr/bin/apt-get update')).toContain(
|
|
'privileged-command',
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"env --split-string='-i -- /usr/bin/printf safe'",
|
|
"env --split-string='FOO=x /usr/bin/printf safe'",
|
|
'env -iu FOO /usr/bin/printf safe',
|
|
'env -ivS/usr/bin/printf safe',
|
|
'su -lc /usr/bin/id operator',
|
|
'su -l -s /bin/sh operator',
|
|
'su -s /bin/sh -- operator',
|
|
'su -c /usr/bin/id operator',
|
|
'su --command=/usr/bin/id operator',
|
|
'su --session-command=/usr/bin/id operator',
|
|
])('keeps valid non-privileged env and su forms safe', (source): void => {
|
|
expect(exampleSafetyViolationKinds(source)).not.toContain('privileged-command');
|
|
});
|
|
|
|
it.each(UNSAFE_EXAMPLE_FIXTURES)(
|
|
'classifies unsafe example fixture as $expected',
|
|
({ expected, source }): void => {
|
|
expect(exampleSafetyViolationKinds(source)).toContain(expected);
|
|
},
|
|
);
|
|
|
|
it('extracts tilde-fenced and unclosed examples without exposing their contents', (): void => {
|
|
const sensitiveFixture = ['sk-ant-api03-', 'a'.repeat(80)].join('');
|
|
const blocks = fencedCodeBlocks(
|
|
`~~~sh\n${sensitiveFixture}\n~~~\n\n\`\`\`sh\n${sensitiveFixture}`,
|
|
);
|
|
|
|
expect(blocks).toHaveLength(2);
|
|
for (const block of blocks) {
|
|
const kinds = exampleSafetyViolationKinds(block);
|
|
expect(kinds).toContain('credential-format');
|
|
expect(JSON.stringify(kinds)).not.toContain(sensitiveFixture);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('fleet operator documentation', (): void => {
|
|
it('ships every accepted information-architecture page', async (): Promise<void> => {
|
|
await expect(
|
|
Promise.all(REQUIRED_FLEET_PAGES.map((path) => readFile(join(fleetDocs, path), 'utf8'))),
|
|
).resolves.toHaveLength(REQUIRED_FLEET_PAGES.length);
|
|
});
|
|
|
|
it('resolves every local Markdown link and heading fragment in the fleet book and sitemap', async (): Promise<void> => {
|
|
const files = [...(await markdownFiles(fleetDocs)), join(repositoryRoot, 'docs', 'SITEMAP.md')];
|
|
const documents: Record<string, string> = {};
|
|
for (const file of files) {
|
|
const relative = file.slice(repositoryRoot.length + 1);
|
|
documents[relative] = await readFile(file, 'utf8');
|
|
}
|
|
|
|
const violations: string[] = [];
|
|
for (const [sourcePath, source] of Object.entries(documents)) {
|
|
for (const target of localMarkdownTargets(source)) {
|
|
const encodedPath = target.split('#', 1)[0] ?? '';
|
|
const targetPath = resolve(
|
|
dirname(join(repositoryRoot, sourcePath)),
|
|
decodeURIComponent(encodedPath),
|
|
);
|
|
const relativeTarget = targetPath.slice(repositoryRoot.length + 1);
|
|
if (documents[relativeTarget] === undefined) {
|
|
try {
|
|
documents[relativeTarget] = await readFile(targetPath, 'utf8');
|
|
} catch {
|
|
// The deterministic validator below records the missing target without exposing content.
|
|
}
|
|
}
|
|
}
|
|
violations.push(...markdownLinkViolations(sourcePath, source, documents));
|
|
}
|
|
expect(violations).toEqual([]);
|
|
});
|
|
|
|
it('validates the canonical documentation example through the production compiler and resolver', async (): Promise<void> => {
|
|
const source = await readFile(join(fleetDocs, 'examples', 'roster-v2.yaml'), 'utf8');
|
|
const roster = parseRosterV2(source, 'yaml');
|
|
const validated = await validateRosterV2Semantics(roster, {
|
|
rolesDir: join(frameworkFleet, 'roles'),
|
|
overrideDir: join(fleetDocs, 'examples', 'roles.local'),
|
|
});
|
|
|
|
expect(validated.generation).toBe(1);
|
|
expect(validated.agents.map((agent) => agent.canonicalClass)).toEqual([
|
|
'code',
|
|
'interaction',
|
|
'validator',
|
|
]);
|
|
});
|
|
|
|
it('keeps every fenced fleet example free of sensitive values, privileged commands, arbitrary command overrides, and product-hardcoded identities', async (): Promise<void> => {
|
|
const violations: string[] = [];
|
|
for (const file of await markdownFiles(fleetDocs)) {
|
|
const source = await readFile(file, 'utf8');
|
|
for (const [index, block] of fencedCodeBlocks(source).entries()) {
|
|
for (const kind of exampleSafetyViolationKinds(block)) {
|
|
violations.push(`${file.slice(repositoryRoot.length + 1)}#block-${index + 1}: ${kind}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const rosterSource = await readFile(join(fleetDocs, 'examples', 'roster-v2.yaml'), 'utf8');
|
|
for (const kind of exampleSafetyViolationKinds(rosterSource)) {
|
|
violations.push(`docs/fleet/examples/roster-v2.yaml: ${kind}`);
|
|
}
|
|
expect(violations).toEqual([]);
|
|
});
|
|
});
|