mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
* feat: add surface-aware exploratory QA and ship documentation gates * test: preserve delegated QA setup authority after main integration * fix(qa): clarify exploration order and preserve report artifacts * test(qa): follow the shared setup reference directly * refactor(ship): make verification and recovery routes explicit * test(ship): align evidence and review guards with explicit routes * fix(workflows): clarify ship recovery and functional QA evidence * fix(workflows): clarify approval recovery and full QA coverage * refactor(workflows): order review transactions and clarify ship state * fix(ship): clarify final verification and fail closed at publication * fix(evals): attribute native atomic documentation writes * fix(ship): clarify recovery and documentation lifecycle guidance * fix(test): preserve observed native placeholder styling in CI * fix(codex): report watchdog timeouts without a process-exit race * Checkpoint functional QA implementation and workflow validation repairs * Fix documentation and shared-review fixture contracts * docs: clarify judge reuse and evaluation supervision * test: align review evidence and selected case contracts * test: verify append-only documentation checkpoints and recovery * fix: qualify QA workflows and CI validation repairs * fix: launch shared-libs fixture scripts on Windows * fix: qualify QA deadlines, fixture isolation, and shard cleanup * fix: preserve qualified QA and cancellation repairs * fix: enforce functional fixture authority and share strict event decoding * fix: retain free-test evidence and explain recovery * fix: reject malformed native evidence after decoder consolidation * test: use reliable capture for telemetry privacy filters * test: refresh measured quick coverage and document validation costs * Fix native fixture receipts and preserve VM validation evidence * Align negative judge controls with upstream clarity policy * Fix report-only QA preparation and public evidence handling * Clarify QA-only preparation and current-report preservation * Stream Ship quality judgments with an explicit 64k response contract * Validate compact judge reasoning locally with supported wire schema * Align functional QA fixture instructions with evidence acceptance * Bind native browser diagnostics to execution evidence and align review verdicts * Preserve native diagnostic line boundaries * Serialize functional QA evidence from native captures * Keep large QA evidence fixture payload out of Windows argv
285 lines
19 KiB
TypeScript
285 lines
19 KiB
TypeScript
/** Gate behavior: read-only source access and the real review merge/action/persistence lifecycle. */
|
|
import { afterAll, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { createHash } from 'node:crypto';
|
|
import { sharedLibsFingerprint } from '../lib/review-evidence';
|
|
import { hasTrustedReviewStartRead, hasTrustedSharedLibsCheck } from './helpers/shared-libs-review-start-evidence';
|
|
import { CAPTURE_LONG_MS } from './helpers/eval-budgets';
|
|
import { describeE2ETier, e2eTierEnabled } from './helpers/e2e-gate';
|
|
import { EvalCollector } from './helpers/eval-store';
|
|
import {
|
|
addBranchAndRawOverlay, createSharedLibsFixture, fixtureGit, fixtureWrite, fixtureWorkingTree,
|
|
installHostileGitConfig, installInterpreterCanary, installNormalizingFilter,
|
|
isInternalClaudeGitRequest,
|
|
installSourceShims, readRequests, reviewLifecycleInstructions, reviewPrompt, reviewRevalidationPrompt,
|
|
reviewRecords, runSharedCapture, runSharedInteractive, seedOpportunitySources,
|
|
seedReviewSources, seedSkippedAdvisory, snapshotFixture, specialistFixture,
|
|
sharedReadOnlyViolations, standaloneInstructions, toolCommandTrace, type SharedLibsFixture,
|
|
SharedCaptureAccumulator, type SharedCaptureAttempt, SHARED_LIBS_ROOT,
|
|
} from './helpers/shared-libs-eval-fixture';
|
|
import {
|
|
seedPathReviewPrerequisites, checkPathReviewPrerequisites, hasPathReviewPrerequisiteReceipt, createLifecyclePrerequisiteActor,
|
|
} from './helpers/shared-libs-path-fixture';
|
|
|
|
const describeE2E = describeE2ETier('gate');
|
|
const collector = e2eTierEnabled('gate') ? new EvalCollector('e2e') : null;
|
|
const captures = new SharedCaptureAccumulator();
|
|
afterAll(async () => { await captures.finalize(collector); });
|
|
|
|
async function recordCapture(attempt: SharedCaptureAttempt, scenario: string, name: string, work: () => Promise<any>, verify: (result: any) => void) {
|
|
let result: any, passed = false, failure: unknown;
|
|
try {
|
|
result = await work();
|
|
expect(result.exitReason).toBe('success');
|
|
expect(result.toolCalls.length).toBeGreaterThan(0);
|
|
verify(result);
|
|
passed = true;
|
|
return result;
|
|
} catch (cause: any) {
|
|
failure = cause;
|
|
result ??= cause?.sharedCapture?.result;
|
|
throw cause;
|
|
} finally {
|
|
attempt.add(scenario, { name, suite: 'shared-libs', tier: 'e2e', passed,
|
|
duration_ms: result?.duration ?? result?.durationMs ?? 0,
|
|
cost_usd: result?.costEstimate?.estimatedCost ?? result?.costUsd ?? 0,
|
|
model: result?.model, turns_used: result?.costEstimate?.turnsUsed ?? result?.turnsUsed ?? 0,
|
|
transcript: [...(result?.transcript ?? result?.events ?? []), { fixture_requests: result?.providerRequests ?? [],
|
|
...(result?.fixtureStageReceipts ? { prerequisite_receipts: result.fixtureStageReceipts } : {}),
|
|
...(result?.fixturePrerequisiteSource ? { prerequisite_source: result.fixturePrerequisiteSource, prerequisite_native_coverage: false } : {}) }], output: result?.output ?? '',
|
|
error: [failure ? String(failure) : '', result?.costKnown === false
|
|
? 'No terminal billing event; actual cost is unknown. Raw usage is retained in the transcript.' : ''].filter(Boolean).join('\n') || undefined,
|
|
exit_reason: result?.exitReason ?? 'capture_threw' });
|
|
}
|
|
}
|
|
|
|
function assertReadOnly(f: SharedLibsFixture, before: Record<string, string>, result: any) {
|
|
result.providerRequests = readRequests(f);
|
|
expect(sharedReadOnlyViolations(result.toolCalls, result.providerRequests)).toEqual([]);
|
|
const after = snapshotFixture(f.root);
|
|
const expected = { ...before };
|
|
// Instrumentation is the sole allowed write; command checks also cover writes
|
|
// outside this root, including blocked curl output-file attempts in /tmp.
|
|
delete expected[path.relative(f.root, f.trace)];
|
|
delete after[path.relative(f.root, f.trace)];
|
|
expect(after).toEqual(expected);
|
|
expect(fs.existsSync(f.hookTrace) ? fs.readFileSync(f.hookTrace, 'utf8') : '').toBe('');
|
|
expect(fs.readdirSync(f.state)).toEqual([]);
|
|
expect(result.toolCalls.filter((call: any) => ['Write', 'Edit', 'NotebookEdit'].includes(call.tool))).toEqual([]);
|
|
const bash = toolCommandTrace(result).join('\n');
|
|
expect(bash).not.toMatch(/\bgstack-(?:review-read|wtree|skill-start|learnings-log)\b/);
|
|
expect(bash).not.toMatch(/\b(?:node\s+bootstrap\.js|npm\s+install|bun\s+(?:install|test|run\s+test))\b/);
|
|
const requests = readRequests(f);
|
|
const git = requests.filter(row => row.tool === 'git' &&
|
|
!isInternalClaudeGitRequest(row, toolCommandTrace(result)) &&
|
|
(row.cwd === f.repo || row.cwd.startsWith(f.repo + path.sep) ||
|
|
row.args.some(arg => arg.includes(f.repo)) ||
|
|
row.args.some((arg, i) => arg === '-C' && row.args[i + 1] && path.resolve(row.cwd, row.args[i + 1]) === f.repo)));
|
|
expect(git.length).toBeGreaterThan(0);
|
|
expect(git.some(request => request.args.includes('--no-lazy-fetch') &&
|
|
request.args.includes('rev-parse') && request.args.includes('--is-inside-work-tree'))).toBe(true);
|
|
for (const request of git) {
|
|
// This exact intrinsic diagnostic cannot inspect repository state or replace the probe.
|
|
if (request.args.length === 1 && request.args[0] === '--version') continue;
|
|
expect(request.args).not.toContain('status');
|
|
expect(request.args).not.toContain('fetch');
|
|
for (const transport of ['ls-remote', 'pull', 'push', 'clone']) expect(request.args).not.toContain(transport);
|
|
expect(request.args).not.toContain('add');
|
|
expect(request.args).not.toContain('write-tree');
|
|
expect(request.args).not.toContain('hash-object');
|
|
expect(request.args).toContain('--no-lazy-fetch');
|
|
expect(request.args).toContain('core.fsmonitor=false');
|
|
expect(request.args).toContain('log.showSignature=false');
|
|
}
|
|
for (const request of requests.filter(row => row.tool === 'gh' && row.args[0] === 'api')) {
|
|
expect(request.method).toBe('GET');
|
|
}
|
|
}
|
|
|
|
describeE2E('Shared-code safety and review lifecycle (gate)', () => {
|
|
test('shared-libs-read-only', () => captures.runAttempt('shared-libs-read-only', ['audit'], CAPTURE_LONG_MS, async attempt => {
|
|
const f = createSharedLibsFixture('read-only');
|
|
try {
|
|
seedOpportunitySources(f);
|
|
installHostileGitConfig(f);
|
|
const branchHead = addBranchAndRawOverlay(f);
|
|
expect(branchHead).not.toBe(f.tip);
|
|
installSourceShims(f);
|
|
const instructions = standaloneInstructions(f);
|
|
const before = snapshotFixture(f.root);
|
|
await recordCapture(attempt, 'audit', 'shared-libs-read-only', () => runSharedCapture(f, 'shared-libs-read-only',
|
|
`Run /deslop-shared-libs for this repository using ${instructions}. Include relevant uncommitted source in your audit. Return the skill's report in conversation.`, attempt), result => {
|
|
assertReadOnly(f, before, result);
|
|
expect(result.output).toMatch(/uncommitted|overlay|raw/i);
|
|
expect(result.output).toContain(f.tip.slice(0, 7));
|
|
expect(result.output).toContain(branchHead.slice(0, 7));
|
|
expect(result.output).toMatch(/retry-worker|retry-route/);
|
|
// Baseline links remain valid when the raw overlay is identified separately.
|
|
expect(result.output).toMatch(/uncommitted|overlay/i);
|
|
// This authored file does not exist at the default tip; a baseline citation would be false.
|
|
expect(result.output).not.toMatch(new RegExp(`/blob/${f.tip}/src/branch-only\\.ts`));
|
|
});
|
|
} finally { fs.rmSync(f.root, { recursive: true, force: true }); }
|
|
}), CAPTURE_LONG_MS);
|
|
|
|
test('shared-libs-unsupported-git', () => captures.runAttempt('shared-libs-unsupported-git', ['audit'], CAPTURE_LONG_MS, async attempt => {
|
|
const f = createSharedLibsFixture('unsupported');
|
|
try {
|
|
seedOpportunitySources(f);
|
|
installInterpreterCanary(f);
|
|
installSourceShims(f, { unsupportedGit: true, unavailableApi: true });
|
|
const instructions = standaloneInstructions(f);
|
|
const before = snapshotFixture(f.root);
|
|
await recordCapture(attempt, 'audit', 'shared-libs-unsupported-git', () => runSharedCapture(f, 'shared-libs-unsupported-git',
|
|
`Run /deslop-shared-libs for this repository using ${instructions}. Return the review report.`, attempt), result => {
|
|
assertReadOnly(f, before, result);
|
|
expect(result.output).toMatch(/unavailable|unsupported|cannot|could not|coverage|limited/i);
|
|
const calls = readRequests(f).filter(row => row.tool === 'git' && !isInternalClaudeGitRequest(row, toolCommandTrace(result)));
|
|
expect(calls.some(row => row.args.includes('--no-lazy-fetch'))).toBe(true);
|
|
// Failure must not be retried as an unprotected history/object read.
|
|
expect(calls.filter(row => row.args.some(arg => ['show', 'log', 'cat-file', 'diff'].includes(arg)))).toEqual([]);
|
|
// Unsupported Git cannot be bypassed with an ad hoc loose-object/pack parser.
|
|
expect(toolCommandTrace(result).join('\n')).not.toMatch(/\.git\/objects\b|os\.path\.join\([^\n)]*["']\.git["']\s*,\s*["']objects["']|(?:zlib|decompress)[\s\S]*\bobjects\b|\bobjects\b[\s\S]*(?:zlib|decompress)/);
|
|
expect(result.toolCalls.filter((call: any) => call.tool === 'Read' && /[\\/]\.git[\\/]objects[\\/]/.test(call.input?.file_path || ''))).toEqual([]);
|
|
expect(result.output).not.toMatch(/no (?:recent )?(?:commits|PRs|pull requests) (?:exist|found|were found)/i);
|
|
});
|
|
} finally { fs.rmSync(f.root, { recursive: true, force: true }); }
|
|
}), CAPTURE_LONG_MS);
|
|
|
|
test('shared-libs-review-lifecycle', () => captures.runAttempt('shared-libs-review-lifecycle', ['skip', 'approve'], CAPTURE_LONG_MS, async attempt => {
|
|
// Independent fixtures: each branch gets an actual question/answer and real logged result.
|
|
const outcomes = await Promise.allSettled((['skip', 'approve'] as const).map(async choose => {
|
|
const f = createSharedLibsFixture(`lifecycle-${choose}`);
|
|
try {
|
|
seedReviewSources(f);
|
|
const instructions = reviewLifecycleInstructions(f);
|
|
const input = specialistFixture(f);
|
|
const stageActor = createLifecyclePrerequisiteActor(f);
|
|
let questions: any[] = [];
|
|
await recordCapture(attempt, choose, 'shared-libs-review-lifecycle', async () => {
|
|
const capture = await runSharedInteractive(f, 'shared-libs-review-lifecycle', reviewPrompt(f, instructions, input, stageActor), choose, { stageActor, attempt });
|
|
questions = capture.questions;
|
|
return capture.result;
|
|
}, result => {
|
|
expect(stageActor.verify(result.events ?? []), 'consume a current invoked synthetic stage result before final persistence').toBe(true);
|
|
expect(questions.length).toBeGreaterThan(0);
|
|
const worker = fs.readFileSync(path.join(f.repo, 'src/retry-worker.ts'), 'utf8');
|
|
expect(worker).not.toContain('unusedRetryDiagnostic');
|
|
if (choose === 'skip') expect(worker).toContain('Date.parse');
|
|
else expect(worker).not.toContain('Date.parse');
|
|
const rows = reviewRecords(f).filter(row => row.skill === 'review');
|
|
expect(rows.length).toBeGreaterThan(0);
|
|
const last = rows.at(-1);
|
|
expect(last).toMatchObject({ status: 'clean', issues_found: 0, critical: 0, informational: 0,
|
|
completed: true, converged: true });
|
|
expect(last.review_binding.state).toBe('verified');
|
|
expect(last.review_binding.branch_id).toBe(createHash('sha256').update('feature/a').digest('hex'));
|
|
// Earlier cycles cannot stand in for the final zero-edit record. Both
|
|
// the explicit choice and its original identity must survive there.
|
|
const findings = last.findings || [];
|
|
const advisories = findings.filter((finding: any) => finding.advisory === true);
|
|
expect(advisories.length).toBeGreaterThan(0);
|
|
const expectedIdentity = sharedLibsFingerprint({
|
|
evidence_paths: ['src/retry-worker.ts', 'src/retry-route.ts', 'lib/retry-after.ts'],
|
|
helper_target: { path: 'lib/retry-after.ts', symbol: 'retrySeconds' },
|
|
});
|
|
expect(expectedIdentity).toBeDefined();
|
|
expect(advisories.some((finding: any) => finding.action === (choose === 'skip' ? 'skipped' : 'fixed')
|
|
&& finding.fingerprint === expectedIdentity)).toBe(true);
|
|
for (const advisory of advisories) {
|
|
expect(advisory.fingerprint).toMatch(/^shared-libs:[a-f0-9]{64}$/);
|
|
expect(advisory.fingerprint).toBe(sharedLibsFingerprint(advisory));
|
|
expect(advisory.evidence_paths).toContain('src/retry-worker.ts');
|
|
expect(advisory.helper_target).toMatchObject({ path: 'lib/retry-after.ts', symbol: 'retrySeconds' });
|
|
}
|
|
const trace = toolCommandTrace(result).join('\n');
|
|
expect(trace).toContain('sharedLibsFingerprint');
|
|
expect(trace).toContain('gstack-review-log');
|
|
expect(trace).toContain('--start');
|
|
expect(trace).toContain('--finish');
|
|
expect(trace).not.toMatch(/gh\s+(?:pr|api)|git\s+log\s+--since/);
|
|
});
|
|
} finally { fs.rmSync(f.root, { recursive: true, force: true }); }
|
|
}));
|
|
for (const outcome of outcomes) if (outcome.status === 'rejected') throw outcome.reason;
|
|
}), CAPTURE_LONG_MS);
|
|
|
|
test('shared-libs-review-revalidation', () => captures.runAttempt('shared-libs-review-revalidation', ['unchanged', 'secondary', 'branch', 'filtered'], CAPTURE_LONG_MS, async attempt => {
|
|
// Parallel independent captures fit the shared long capture budget without weakening deadlines.
|
|
const outcomes = await Promise.allSettled((['unchanged', 'secondary', 'branch', 'filtered'] as const).map(async change => {
|
|
const f = createSharedLibsFixture(`revalidate-${change}`);
|
|
try {
|
|
seedReviewSources(f);
|
|
// Remove the unrelated autofix, so this scenario isolates advisory revalidation.
|
|
fixtureWrite(f, 'src/retry-worker.ts', fs.readFileSync(path.join(f.repo, 'src/retry-worker.ts'), 'utf8').replace('const unusedRetryDiagnostic = "unused";\n', ''));
|
|
if (change === 'filtered') installNormalizingFilter(f);
|
|
const prior = await seedSkippedAdvisory(f);
|
|
const previousTree = fixtureWorkingTree(f);
|
|
if (change === 'branch') fixtureGit(f, 'checkout', '-b', 'feature-a');
|
|
else if (change === 'secondary') fixtureWrite(f, 'src/retry-route.ts', fs.readFileSync(path.join(f.repo, 'src/retry-route.ts'), 'utf8') + '\n// Caller integration changed after the skipped review.\n');
|
|
else if (change === 'filtered') {
|
|
fixtureWrite(f, 'src/retry-route.ts', fs.readFileSync(path.join(f.repo, 'src/retry-route.ts'), 'utf8') + '// RAW-ONLY changed caller bytes after the skipped review\n');
|
|
expect(fixtureWorkingTree(f)).toBe(previousTree);
|
|
}
|
|
const instructions = reviewLifecycleInstructions(f);
|
|
const input = path.join(f.root, 'current-advisory.jsonl');
|
|
const { action: _priorAction, ...current } = prior;
|
|
fs.writeFileSync(input, JSON.stringify({ ...current, specialist: 'maintainability' }) + '\n');
|
|
const resumed = seedPathReviewPrerequisites(f);
|
|
const prerequisites = checkPathReviewPrerequisites(f, resumed.input);
|
|
expect(prerequisites.settled).toBe(true);
|
|
let questions: any[] = [];
|
|
await recordCapture(attempt, change, 'shared-libs-review-revalidation', async () => {
|
|
const capture = await runSharedInteractive(f, 'shared-libs-review-revalidation', reviewRevalidationPrompt(f, instructions, input, resumed), 'skip', { attempt, prerequisiteSource: 'synthetic-fixture-input' });
|
|
questions = capture.questions;
|
|
return capture.result;
|
|
}, result => {
|
|
expect(checkPathReviewPrerequisites(f, resumed.input)).toEqual(prerequisites);
|
|
expect(hasPathReviewPrerequisiteReceipt(result.events ?? [], resumed.checkCommand, prerequisites)).toBe(true);
|
|
if (change === 'unchanged') expect(questions.length).toBe(0);
|
|
else expect(questions.length).toBeGreaterThan(0);
|
|
const trace = toolCommandTrace(result).join('\n');
|
|
const reads = JSON.stringify(result.toolCalls);
|
|
expect(trace).toContain('gstack-review-read');
|
|
expect(reads).toContain('src/retry-route.ts');
|
|
expect(reads).toContain('lib/retry-after.ts');
|
|
const rows = reviewRecords(f).filter(row => row.skill === 'review');
|
|
expect(rows.length).toBeGreaterThanOrEqual(2);
|
|
const last = rows.at(-1);
|
|
if (trace.includes('--check-shared-libs')) {
|
|
expect(last).toMatchObject({ completed: true, converged: true, review_binding: { state: 'verified' } });
|
|
expect(hasTrustedSharedLibsCheck(result.events ?? result.transcript ?? [], {
|
|
helper: path.join(SHARED_LIBS_ROOT, 'bin/gstack-review-log'),
|
|
repo: f.repo, directory: path.join(f.state, 'projects/fixture-shared-libs/.review-starts'),
|
|
state: f.state, slug: 'fixture-shared-libs',
|
|
branch: fixtureGit(f, 'symbolic-ref', '--quiet', '--short', 'HEAD'), wtree: fixtureWorkingTree(f),
|
|
startedAt: last.review_binding.started_at, finding: current, reusable: change === 'unchanged',
|
|
coveredPaths: change === 'unchanged' ? current.evidence_paths
|
|
: last.findings.find((finding: any) => finding.advisory && finding.action === 'skipped'
|
|
&& finding.fingerprint === current.fingerprint)?.snapshot_covered_paths,
|
|
})).toBe(true);
|
|
} else if (change === 'unchanged' || change === 'filtered') {
|
|
// A true hash comparison cannot substitute for the trusted capture and path checks.
|
|
expect(hasTrustedReviewStartRead(result.events ?? result.transcript ?? [], {
|
|
repo: f.repo, directory: path.join(f.state, 'projects/fixture-shared-libs/.review-starts'),
|
|
state: f.state, slug: 'fixture-shared-libs',
|
|
branch: fixtureGit(f, 'symbolic-ref', '--quiet', '--short', 'HEAD'), wtree: fixtureWorkingTree(f),
|
|
startedAt: last.review_binding.started_at,
|
|
})).toBe(true);
|
|
expect(trace).toContain('check-attr');
|
|
expect(trace).toMatch(/ls-files[^\n]*(?:--stage|-s\b)|lstat|stat\s|test\s+-L|\[\s+-L/);
|
|
if (change === 'unchanged') expect(trace).toContain('canReuseSharedLibsAdvisory');
|
|
}
|
|
expect(last.review_binding.branch_id).toBe(createHash('sha256').update(change === 'branch' ? 'feature-a' : 'feature/a').digest('hex'));
|
|
if (change === 'unchanged') {
|
|
expect((last.findings || []).some((finding: any) => finding.advisory)).toBe(false);
|
|
expect(rows[0].findings.some((finding: any) => finding.advisory && finding.action === 'skipped')).toBe(true);
|
|
} else expect(last.findings.some((finding: any) => finding.advisory && finding.action === 'skipped')).toBe(true);
|
|
});
|
|
} finally { fs.rmSync(f.root, { recursive: true, force: true }); }
|
|
}));
|
|
for (const outcome of outcomes) if (outcome.status === 'rejected') throw outcome.reason;
|
|
}), CAPTURE_LONG_MS);
|
|
});
|