Files
gstack/test/agents-digest.test.ts
T
Garry TanandClaude Fable 5 760554d0ac test(digest): pin the script-anchored explainer path; catch declaration-prefixed writers
- Asserts $SOURCE_GSTACK_DIR/agents-digest path and forbids $(pwd)/agents-digest
  (the cycle-1 fix was revertible without failing anything).
- The laundered-assignment arm now matches local/export/declare/readonly/typeset
  prefixed assignments — the likeliest in-function writer shape in setup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 05:42:57 +00:00

107 lines
4.9 KiB
TypeScript

/**
* agents-digest/gstack-AGENTS.md — the instruction-only tier artifact.
*
* Freshness (committed file matches the generator, same pattern as
* llms-txt-shape.test.ts), a HARD byte budget (every rules-reading host
* loads the whole file every session), and the delivery-safety invariant
* (setup prints the path; it must never copy onto a user's AGENTS.md).
*/
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import {
generateAgentsDigest,
DIGEST_RELPATH,
DIGEST_BYTE_BUDGET,
} from '../scripts/gen-agents-digest';
const ROOT = path.resolve(import.meta.dir, '..');
const digestPath = path.join(ROOT, DIGEST_RELPATH);
describe('agents-digest', () => {
test('committed digest is fresh (matches generator output)', () => {
const { content } = generateAgentsDigest({ root: ROOT });
const committed = fs.readFileSync(digestPath, 'utf-8');
expect(committed).toBe(content);
});
test(`digest stays within its ${DIGEST_BYTE_BUDGET}-byte budget`, () => {
const { bytes } = generateAgentsDigest({ root: ROOT });
if (bytes > DIGEST_BYTE_BUDGET) {
throw new Error(
`agents-digest is ${bytes} bytes, over the ${DIGEST_BYTE_BUDGET}-byte budget ` +
`(over by ${bytes - DIGEST_BYTE_BUDGET}).\n` +
`This file is always-on context for every instruction-tier host — trim, don't grow.\n` +
`Trim protocol: cut prose from scripts/gen-agents-digest.ts (shorten explanations, ` +
`never drop a section outright), rerun 'bun run gen:skill-docs', and re-check. ` +
`Raising the budget requires the same conscious-decision treatment as the ` +
`context-budget ratchet: justify it in the PR that does it.`,
);
}
expect(bytes).toBeLessThanOrEqual(DIGEST_BYTE_BUDGET);
});
test('digest carries its version header and the load-bearing sections', () => {
const version = fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf-8').trim();
const content = fs.readFileSync(digestPath, 'utf-8');
expect(content.startsWith(`# gstack digest v${version}`)).toBe(true);
expect(content).toContain('re-copy after upgrading');
expect(content).toContain('## Ethos');
expect(content).toContain('## The reuse ladder');
expect(content).toContain('## Voice');
expect(content).toContain('## Full gstack');
// The ladder must keep the completeness reconciliation clause.
expect(content).toContain('build the complete version of what remains');
});
test('setup never copies the digest onto a user AGENTS.md (print-path only)', () => {
const setup = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8');
// The explainer arms print the digest path, anchored to the script's own
// directory — a $(pwd)-relative path prints a nonexistent file whenever
// setup is invoked from anywhere but the repo root.
expect(setup).toContain('$SOURCE_GSTACK_DIR/agents-digest/gstack-AGENTS.md');
expect(setup).not.toMatch(/\$\(pwd\)\/agents-digest/);
// …and no line may write to an AGENTS.md destination. Covers direct
// write verbs (cp/ln/mv/tee/install/rsync/dd/truncate), > and >>
// redirects, and the laundered form (dest="$proj/AGENTS.md"; … > "$dest")
// by flagging any variable assigned from an AGENTS.md path. echo/printf
// lines that merely PRINT the path stay legal.
const writers = setup
.split('\n')
.filter((l) => !/^\s*#/.test(l))
.filter((l) =>
/(^|\s|\|)(cp|ln|mv|tee|install|rsync|dd|truncate)\s[^#]*AGENTS\.md/.test(l)
|| />{1,2}\s*"?[^"\s]*AGENTS\.md/.test(l)
|| /^\s*(?:(?:local|export|declare|readonly|typeset)\s+)?\w+=[^#]*\/AGENTS\.md/.test(l));
expect(writers).toEqual([]);
});
test('the digest reuse-ladder text stays in lockstep with the preamble resolver', () => {
// The ladder and root-cause rules are hand-rendered into the digest (it
// ships to hosts that never load the preamble). The freshness test above
// pins digest-vs-generator; this pins generator-vs-resolver so an edit to
// scripts/resolvers/preamble/generate-search-before-building.ts fails CI
// instead of silently shipping a stale digest.
const { content } = generateAgentsDigest();
const resolver = fs.readFileSync(
path.join(ROOT, 'scripts', 'resolvers', 'preamble', 'generate-search-before-building.ts'),
'utf-8',
);
for (const shared of [
'stop at the first rung that holds',
'Then build the complete version of what remains',
'one guard in the shared function beats a guard in every caller',
]) {
expect(content).toContain(shared);
expect(resolver).toContain(shared);
}
});
test('instruction-tier hosts declare the digest in host config', () => {
for (const host of ['openclaw', 'hermes']) {
const src = fs.readFileSync(path.join(ROOT, 'hosts', `${host}.ts`), 'utf-8');
expect(src).toContain("instructionTier: { rulesFile: 'agents-digest/gstack-AGENTS.md' }");
}
});
});