mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 06:24:41 +01:00
fix(docsync): the receipt record is housekeeping for read-only audits and the fault cases too
Pass 9's outside voice found two gaps in the fixture change (2524ec4): a
read-only audit (ship-docsync-current) must still write .gstack/tmp/docs-audit.json,
but docsToolFailures flagged every product write under readOnly before the
record allowance; and the unseeded fault cases (missing-asset, legacy-completion)
run the Entry too, so their kernel-write check saw .git/info/exclude as a
forbidden write because only the lifecycle test named the housekeeping paths.
- docsToolFailures: the record write is permitted under readOnly; the document
and every other product file stay refused.
- docsWriteFailures: DOCS_AUDIT_RECORD_PATHS are permitted for every caller, so
the fault evaluator needs no per-scenario list; anything else under .git or
.gstack stays forbidden.
- test/docsync-command-grammar.test.ts pins both: the record under readOnly, the
document refused there, the observed housekeeping paths accepted with an empty
allowance and two neighbours refused.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
2524ec4475
commit
8aa3578753
3 files changed
+17
-10
No files matched your search
@@ -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<typeof fixtureDocs>;
|
||||
@@ -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'));
|
||||
|
||||
@@ -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<ty
|
||||
const file = path.resolve(fixture.repo, call.input?.file_path ?? '');
|
||||
const actual = actualWritePath(file);
|
||||
const productWrite = within(file, fixture.repo) || (actual !== null && within(actual, repo));
|
||||
if (readOnly && productWrite) failures.push('read-only docs write attempt');
|
||||
const allowedDoc = (file === path.join(fixture.repo, DOC_PATH) && actual === authoredDoc) ||
|
||||
(file === path.join(fixture.repo, DOCS_AUDIT_RECORD) && actual === path.join(repo, DOCS_AUDIT_RECORD));
|
||||
// Step 14.5's receipt record is housekeeping every scenario may write, a read-only audit included; the document stays read-only.
|
||||
const recordWrite = file === path.join(fixture.repo, DOCS_AUDIT_RECORD) && actual === path.join(repo, DOCS_AUDIT_RECORD);
|
||||
if (readOnly && productWrite && !recordWrite) failures.push('read-only docs write attempt');
|
||||
const allowedDoc = (file === path.join(fixture.repo, DOC_PATH) && actual === authoredDoc) || recordWrite;
|
||||
if (productWrite && !allowedDoc) {
|
||||
failures.push('non-document product write attempt');
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { describeE2ETier } from './helpers/e2e-gate';
|
||||
import { docsDispatchIndex, parseDocsCompletion, vetDocsCompletion } from './helpers/docsync-contract';
|
||||
import { fixtureDocs, repoSnapshot, changedFiles, DOC_PATH, preserveDocsEvidence, sawSpawnedMarker, childReportedSpawnedMarker, type DocsScenario } from './helpers/docsync-fixture';
|
||||
import { observeDocsWrites, docsWriteFailures, docsShipPhase, docsToolFailures, docsCompletedRead, docsSessionOptions } from './helpers/docsync-observer';
|
||||
import { DOCS_AUDIT_RECORD_PATHS } from './helpers/docsync-interface';
|
||||
import type { SkillTestResult } from './helpers/session-runner';
|
||||
import { runShipDocsFault } from './helpers/docsync-fault-eval';
|
||||
|
||||
@@ -60,7 +59,7 @@ async function runShipDocs(testName: string, scenario: DocsScenario) {
|
||||
expect(calls[dispatch].input.run_in_background).toBe(false);
|
||||
if (publishCall >= 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([]);
|
||||
|
||||
Reference in new issue
Block a user