diff --git a/test/helpers/qa-functional-eval.ts b/test/helpers/qa-functional-eval.ts index 4e881c819..84e75c512 100644 --- a/test/helpers/qa-functional-eval.ts +++ b/test/helpers/qa-functional-eval.ts @@ -103,7 +103,7 @@ export async function runQAFunctionalCase(entry: { id: string; family: QAFamily; fixtureGit(fixture.root, ['commit', '-m', 'Bind current QA instructions to fixture']); fixture.revision = fixtureGit(fixture.root, ['rev-parse', 'HEAD']); if (fixtureGit(fixture.root, ['status', '--porcelain'])) throw new Error('QA fixture must start clean'); - observer = await observeQAWrites(fixture.root, { evidenceProducer: true }); + observer = await observeQAWrites(fixture.root, { evidenceProducer: true, atomicWriteMode: entry.mode }); await runRecordedOfficeHoursAttempt({ collector, name: entry.id, suite: 'Functional QA native E2E', model: process.env.EVALS_MODEL ?? resolveEvalModel('capture'), diff --git a/test/helpers/qa-functional-observer.ts b/test/helpers/qa-functional-observer.ts index 866c232a3..22e513e66 100644 --- a/test/helpers/qa-functional-observer.ts +++ b/test/helpers/qa-functional-observer.ts @@ -72,7 +72,7 @@ export interface QAWriteObservation { limits: string[]; } -export async function observeQAWrites(root: string, options: { reportDirectory?: string; evidenceProducer?: boolean; atomicTargets?: string[] } = {}) { +export async function observeQAWrites(root: string, options: { reportDirectory?: string; evidenceProducer?: boolean; atomicTargets?: string[]; atomicWriteMode?: QAMode } = {}) { if (process.platform !== 'linux') throw new Error('QA write observer unavailable: Linux inotify required'); if (fs.realpathSync(root) !== root) throw new Error('Observer root must be canonical'); let reportDirectory: string | undefined; @@ -83,7 +83,9 @@ export async function observeQAWrites(root: string, options: { reportDirectory?: } const transientFile = (relative: string) => qaWriteAllowed(relative, 'qa-only') || (reportDirectory !== undefined && relative.startsWith(reportDirectory + path.sep)) - || (options.atomicTargets ?? []).some(target => relative.startsWith(target + '.tmp.') && /^\.tmp\.[1-9]\d*\.[0-9a-f]{12}$/.test(relative.slice(target.length))); + || (options.atomicTargets ?? []).some(target => relative.startsWith(target + '.tmp.') && /^\.tmp\.[1-9]\d*\.[0-9a-f]{12}$/.test(relative.slice(target.length))) + || (options.atomicWriteMode !== undefined && /\.tmp\.[1-9]\d*\.[0-9a-f]{12}$/.test(relative) + && qaWriteAllowed(relative.replace(/\.tmp\.[1-9]\d*\.[0-9a-f]{12}$/, ''), options.atomicWriteMode)); const before = qaTreeSnapshot(root); const { dlopen, FFIType, ptr } = await import('bun:ffi'); const libc = dlopen('libc.so.6', { diff --git a/test/qa-caller-report-observer.test.ts b/test/qa-caller-report-observer.test.ts index 68004e864..ae9df1c4c 100644 --- a/test/qa-caller-report-observer.test.ts +++ b/test/qa-caller-report-observer.test.ts @@ -14,11 +14,11 @@ function fixture() { return root; } -async function atomicPublication(directory: string, declared?: string, atomicTargets?: string[]) { +async function atomicPublication(directory: string, declared?: string, atomicTargets?: string[], atomicWriteMode?: 'qa' | 'qa-only') { const root = fixture(); const temporary = path.join(root, directory, 'exploration-004.json.tmp.2644.340bb6ad0afe'); const target = path.join(root, directory, 'exploration-004.json'); - const observer = await observeQAWrites(root, { reportDirectory: declared, atomicTargets }); + const observer = await observeQAWrites(root, { reportDirectory: declared, atomicTargets, atomicWriteMode }); const stat = fs.lstatSync; let renamedAtWatch = false; let stopped = false; @@ -83,6 +83,15 @@ async function atomicPublication(directory: string, declared?: string, atomicTar expect(result.observation.events).toContainEqual(expect.objectContaining({ path: result.target, mask: 0x80 })); }); + test('fix mode treats an atomic temp for an authorized source/test write as transient; report-only mode stays strict', async () => { + const fix = await atomicPublication('src', undefined, undefined, 'qa'); + expect(fix.renamedAtWatch).toBe(false); + expect(fix.observation.failures).toEqual([]); + const reportOnly = await atomicPublication('src', undefined, undefined, 'qa-only'); + expect(reportOnly.renamedAtWatch).toBe(true); + expect(reportOnly.observation.complete).toBe(false); + }); + test('supports an explicitly selected nested report directory, not an implicit reports name', async () => { const result = await atomicPublication('foreign/reports', 'foreign/reports'); expect(result.observation.complete).toBe(true);