diff --git a/test/docsync-command-grammar.test.ts b/test/docsync-command-grammar.test.ts index 8c18d8fcd..ca1cb60d3 100644 --- a/test/docsync-command-grammar.test.ts +++ b/test/docsync-command-grammar.test.ts @@ -3,7 +3,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { spawnSync } from 'node:child_process'; import { DOC_PATH, fixtureDocs, gitAt, repoSnapshot } from './helpers/docsync-fixture'; -import { docsCommandAllowed, docsCompletedRead, docsNativeInterface, docsSessionOptions, docsToolFailures } from './helpers/docsync-observer'; +import { docsCommandAllowed, docsCompletedRead, docsNativeInterface, docsSessionOptions, docsToolFailures, docsWriteFailures } from './helpers/docsync-observer'; import { parseNDJSON, type SkillTestResult } from './helpers/session-runner'; let fixture: ReturnType; @@ -507,7 +507,7 @@ test('the docs interface names the Claude config dir and private actor state as // generated section, and nothing adjacent: a reworded exclude line, a bare rm, another receipt subcommand or another // write under .gstack/ stay outside it (gate: ship-docsync-completion/current red twice at 775d9804 before they were declared). test('Step 14.5 housekeeping commands and the docs record are declared exactly as the generated section writes them', () => { - const { DOCS_AUDIT_RECORD, DOCS_AUDIT_RESET, DOCS_TMP_EXCLUDE, docsCapsCommand } = require('./helpers/docsync-interface') as typeof import('./helpers/docsync-interface'); + const { DOCS_AUDIT_RECORD, DOCS_AUDIT_RECORD_PATHS, DOCS_AUDIT_RESET, DOCS_TMP_EXCLUDE, docsCapsCommand } = require('./helpers/docsync-interface') as typeof import('./helpers/docsync-interface'); const section = fs.readFileSync(path.resolve(import.meta.dir, '..', 'ship/sections/documentation.md'), 'utf8'); expect(section).toContain(DOCS_TMP_EXCLUDE); expect(section).toContain(DOCS_AUDIT_RESET); @@ -532,7 +532,14 @@ test('Step 14.5 housekeeping commands and the docs record are declared exactly a return { ...parsed, exitReason: 'success' } as unknown as SkillTestResult; }; expect(docsToolFailures(write(path.join(fixture.repo, DOCS_AUDIT_RECORD)), fixture, [], false)).toEqual([]); - expect(docsToolFailures(write(path.join(fixture.repo, DOCS_AUDIT_RECORD)), fixture, [], true)).toEqual(['read-only docs write attempt']); + expect(docsToolFailures(write(path.join(fixture.repo, DOCS_AUDIT_RECORD)), fixture, [], true)).toEqual([]); + expect(docsToolFailures(write(path.join(fixture.repo, DOC_PATH)), fixture, [], true)).toEqual(['read-only docs write attempt']); + // The kernel observer sees the exclude file and the record's directories in every scenario that runs the Entry, the fault cases' + // unseeded first attempts included, so the shared list is permitted without each caller naming it; anything else under .git or .gstack is not. + const observed = (paths: string[]) => ({ complete: true, failures: [], events: paths.map(p => ({ path: p, mask: 0x2, cookie: 0, at: 0 })), changed: paths, before: {}, after: {}, limits: [] }); + expect(docsWriteFailures(observed(['.qa-state/.observer-check', ...DOCS_AUDIT_RECORD_PATHS]), [])).toEqual([]); + expect(docsWriteFailures(observed(['.git/info/attributes']), [])).toEqual(['forbidden docs write: .git/info/attributes']); + expect(docsWriteFailures(observed(['.gstack/tmp/review-loop.json']), [DOC_PATH])).toEqual(['forbidden docs write: .gstack/tmp/review-loop.json']); expect(docsToolFailures(write(path.join(fixture.repo, '.gstack/tmp/review-loop.json')), fixture, [], false)).toEqual(['non-document product write attempt', 'write outside docs fixture authority']); expect(docsToolFailures(write(path.join(fixture.repo, '.gstack/ship-policy.json')), fixture, [], false)).toEqual(['non-document product write attempt', 'write outside docs fixture authority']); expect(gitAt(fixture.repo, 'rev-parse', '--verify', 'origin/main')).toBe(gitAt(fixture.repo, 'rev-parse', '--verify', 'main')); diff --git a/test/helpers/docsync-observer.ts b/test/helpers/docsync-observer.ts index 153aea284..d87514927 100644 --- a/test/helpers/docsync-observer.ts +++ b/test/helpers/docsync-observer.ts @@ -5,7 +5,7 @@ import { observeQAWrites, type QAWriteObservation } from './qa-functional-observ import { nativeCalls } from './qa-checkpoint-evidence'; import type { SkillTestResult } from './session-runner'; import { DOC_PATH, type DocsScenario, type fixtureDocs } from './docsync-fixture'; -import { docsNativeInterface, DOCS_AUDIT_RECORD, DOCS_AUDIT_RESET, DOCS_TMP_EXCLUDE, docsCapsCommand } from './docsync-interface'; +import { docsNativeInterface, DOCS_AUDIT_RECORD, DOCS_AUDIT_RECORD_PATHS, DOCS_AUDIT_RESET, DOCS_TMP_EXCLUDE, docsCapsCommand } from './docsync-interface'; import { sliceBetween } from './skill-fixture'; import { readOnlyCommandAllowed } from './read-only-commands'; @@ -137,7 +137,7 @@ export function docsWriteFailures(observation: QAWriteObservation, allowed: stri if (!observation.complete) failures.push('incomplete docs write observation'); const atomicSources = docsAtomicSources(observation, allowed, context); for (const file of new Set([...observation.events.map(e => e.path), ...observation.changed])) { - if (file !== '.qa-state/.observer-check' && !allowed.includes(file) && !atomicSources.has(file)) + if (file !== '.qa-state/.observer-check' && !allowed.includes(file) && !DOCS_AUDIT_RECORD_PATHS.includes(file) && !atomicSources.has(file)) failures.push(`forbidden docs write: ${file}${atomicSources.deniedAt && path.dirname(file) === path.dirname(DOC_PATH) ? ` (atomic replacement unproven at ${atomicSources.deniedAt})` : ''}`); if (allowed.includes(file) && observation.before[file] && observation.after[file] && observation.before[file].split(':')[0] !== observation.after[file].split(':')[0]) failures.push(`document mode changed: ${file}`); @@ -458,9 +458,10 @@ export function docsToolFailures(result: SkillTestResult, fixture: ReturnType= 0) expect(dispatch).toBeLessThan(publishCall); expect(result.exitReason).toBe('success'); - expect(docsWriteFailures(observation!, scenario === 'store' ? [] : [...(scenario === 'current' ? [] : [DOC_PATH]), ...DOCS_AUDIT_RECORD_PATHS], { + expect(docsWriteFailures(observation!, scenario === 'current' || scenario === 'store' ? [] : [DOC_PATH], { result, fixture, scripts: [publish], readOnly: scenario === 'current' || scenario === 'store', })).toEqual([]); expect(docsToolFailures(result, fixture, [publish], scenario === 'current' || scenario === 'store')).toEqual([]);