mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-12 19:44:48 +01:00
* feat(ship): review loop converges (frozen finding scope, 3-pass cap, NO FINDINGS first-class) and receipts carry across conflict-only merges
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(ship): keep the Step 9 first-entry and Fix-First phrasing the skip/requeue contract asserts; fill the review record's pass, review_scope and range_base placeholders in the caller template test; narrow a test type
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes (pass 1) and the review-loop budget wording
Two blocking findings from /ship's own pass 1, both reproduced in the QA smoke before the fix:
gstack-ship-receipt carry hashed the product diff with git patch-id --stable, which strips
whitespace, so a space moved in a reviewed shell line carried as "no product change"; and it
classed package.json and every lockfile as release metadata, so a dependency swap carried too.
The patch-id is now --verbatim, package.json and VERSION are metadata only while the change is a
version stamp (lib/pregate/stamp-only.ts), lockfiles are product unless the policy names them,
the test globs name code extensions, a dirty working tree refuses before anything is compared,
and a receipt carried twice keeps the reviewed head as carry.from.
gstack-review-scope: an unresolvable base is POLICY_SOURCE_UNAVAILABLE instead of a silent
working-tree read; --range-base is validated; git runs with core.quotePath=false; range_base is
null on pass 1; a blocking finding with no path is fix; category "testing" is a test finding
below the floor; defer searches once, files nothing on a failed search and exits 0 unfiled;
summary refuses zero matching passes and the receipt write is chained to it with &&; a finding
the diff causes elsewhere names the changed line in `cause` and classify follows it.
Review-loop budget wording (ideas from tstack's sol-loop and pstack's interrogate, rewritten;
NOTICE.md): the blocker / should / nit glossary and two stop rules in Step 9.4, the checklist
and docs/review-policy.md; a lead-judgment stage in the finding merge both /review and /ship
render. The core read diffs $RANGE_BASE from pass 2 on. Caps and pins re-measured with reasons.
Step 7 coverage audit: four tests added in existing files, the --review-loop case moved to its
own test (the shared test tripped bun's 5 s default).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pin the renumbered review-army stages and point the wording test at product code
CI at c3e27fc failed three free-tier tests, all this branch's own: the
workflow-required-steps fixture and shared-libs-rendering still pinned the
pre-Lead-judgment stage numbers, and test-of-test-ratchet counted
review-loop-budget-wording.test.ts as test-only code because it read only
generated markdown. The fixture and rendering test now pin stages 1-8, and the
wording test renders both armies through generateReviewArmy and reads the
authored ship template as well as the generated sections.
Pass-2 maintainability notes folded in: docs/troubleshooting.md names the two
new RECEIPT_CARRY_REFUSED causes (dirty tree, unreadable status) and the two new
REVIEW_SCOPE_INPUT causes (non-hex --range-base, summary with no matching
pass); gstack-review-scope's help states that classify reads the base branch's
policy and refuses an unresolvable base unless --policy is passed.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: date a review-log row from its binding start when it has no timestamp
The Step 9.4 template asks the writer to fill `timestamp`; the logger stamps
`review_binding.started_at` but never `timestamp`. `gstack-review-scope
summary` dropped such a row silently, so this PR's own pass 2 was missing from
REVIEW_LOOP (passes=1 for a two-pass loop). A row now dates from `timestamp`,
else from its binding start; a row with neither is still not counted.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pass-4 review fixes (carry stamp rule, deleted-file locus, scanned defer, diff prefixes, row time)
Five findings at the blocking floor from the Step 11 outside voice (gpt-6.1-sol)
and the native adversarial pass, fixed as a fourth pass approved by GStack
Minder under the three-pass policy:
- carry: a path the policy names under stamp_files, stamp_paths or
release_outputs is metadata only while its change is a stamp (versionStampOnly
for a manifest kind, stampOnlyText otherwise), the rule the tree receipt
applies; a logic change in a stamped source file is product. Every
metadata-listed path is read at both heads and a non-stamp change drops its
globs from the product exclusion.
- review-scope: parseHunks keeps a deleted file as a whole changed location, so
a cause anchored in a removed export is the diff's to fix, not deferred.
- defer: issues are filed through lib/gstack-post's publication scan; a HIGH
finding in the title or body refuses and a MEDIUM one needs a human, both
recorded as unfiled with the reason; --repo names the destination.
- classify: both diffs pin --src-prefix=a/ --dst-prefix=b/ --no-textconv, so
diff.mnemonicPrefix cannot send every finding outside.
- summary: a row dates from review_binding.started_at before the writer-typed
timestamp, so a date-only value cannot drop a real pass.
Docs: ship-policy.md#receipt-carry, review-policy.md, the Step 9.4 template
and the CHANGELOG gate-integrity note record the rule and the pass.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(review-army): assert the Red Team branch the run took in the N+1 case
Under the blocking floor the Performance specialist rates a 12-line N+1
INFORMATIONAL, so the conditional Red Team does not activate; the case
encoded the old severity (N+1 is CRITICAL, a Red Team always follows). It
now asserts the branch the run took: a Red Team that ran must follow the
Performance specialist; no Red Team is correct only when the report's own
count says 0 critical and its Red Team line names that reason. The N+1
detection assertion is unchanged. The free contract test gains the
informational-not-activated and informational-claimed controls and the
budget test's synthetic report carries the count line.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* v1.91.256.0 stamp
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
293 lines
17 KiB
TypeScript
293 lines
17 KiB
TypeScript
/**
|
||
* #3339: /ship Step 9 and /review may re-review only the fix range, but a fix-range pass
|
||
* can never be the last review. The fix base comes from the review log (never
|
||
* the working tree or the caller) and is the commit whose tree the fixing pass
|
||
* started from, a narrowed row is never CURRENT and carries no
|
||
* start_wtree/end_wtree for Step 11.5 to bind, and narrowed dispatches stay out
|
||
* of specialist hit rates.
|
||
*/
|
||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||
import { spawnSync } from 'node:child_process';
|
||
import * as fs from 'node:fs';
|
||
import * as os from 'node:os';
|
||
import * as path from 'node:path';
|
||
import { canReuseSharedLibsAdvisory, reviewFreshness, sharedLibsFingerprint } from '../lib/review-evidence';
|
||
import { gitIn } from './helpers/scratch-repo';
|
||
|
||
const ROOT = path.resolve(import.meta.dir, '..');
|
||
const BIN = path.join(ROOT, 'bin');
|
||
const W = 'a'.repeat(40);
|
||
|
||
let root: string, repo: string, env: NodeJS.ProcessEnv;
|
||
beforeEach(() => {
|
||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-fix-range-'));
|
||
repo = path.join(root, 'repo');
|
||
fs.mkdirSync(repo);
|
||
env = { ...process.env, GSTACK_STATE_ROOT: path.join(root, 'state') };
|
||
delete env.GSTACK_HOME;
|
||
gitIn(repo, 'init -q -b feature');
|
||
fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 1;\n');
|
||
gitIn(repo, 'add a.ts');
|
||
gitIn(repo, 'commit -q -m init');
|
||
});
|
||
afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); });
|
||
|
||
function bin(name: string, args: string[]) {
|
||
const r = spawnSync(path.join(BIN, name), args, { cwd: repo, encoding: 'utf8', timeout: 60_000, env });
|
||
return { code: r.status, out: r.stdout.trim(), err: r.stderr };
|
||
}
|
||
const head = () => gitIn(repo, 'rev-parse HEAD').trim();
|
||
const edit = (text: string) => fs.writeFileSync(path.join(repo, 'a.ts'), text);
|
||
const commit = () => gitIn(repo, 'commit -q -am fix');
|
||
const row = (fields: Record<string, unknown>) => JSON.stringify({ skill: 'review', via: 'ship', status: 'clean', issues_found: 0, critical: 0, informational: 0, ...fields });
|
||
const fixing = { status: 'issues_found', issues_found: 1, completed: true, converged: false };
|
||
const clean = { completed: true, converged: true };
|
||
|
||
// Step 9.4's order: start, review, apply fixes, log with the original token, then commit the fixes.
|
||
function pass(fields: Record<string, unknown>, start: string[] = ['--start', 'review'], fixes?: string, commitFixes = true) {
|
||
const started = bin('gstack-review-log', start);
|
||
expect(started.code).toBe(0);
|
||
const [token, base] = started.out.split('\n');
|
||
if (fixes) edit(fixes);
|
||
expect(bin('gstack-review-log', [row(fields), '--finish', token!]).code).toBe(0);
|
||
if (fixes && commitFixes) commit();
|
||
return base;
|
||
}
|
||
function rows(): any[] {
|
||
return bin('gstack-review-read', []).out.split('\n---CONFIG---')[0]!.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line));
|
||
}
|
||
const narrowStart = () => bin('gstack-review-log', ['--start', 'review', '--fix-range']);
|
||
|
||
describe('fix-range start', () => {
|
||
test('after a completed fixing pass it prints the token and the reviewed commit as FIX_BASE', () => {
|
||
const reviewed = head();
|
||
pass(fixing, undefined, 'export const a = 2;\n');
|
||
expect(pass(clean, ['--start', 'review', '--fix-range'])).toBe(`FIX_BASE=${reviewed}`);
|
||
expect(gitIn(repo, `diff --name-only ${reviewed}`).trim()).toBe('a.ts');
|
||
});
|
||
|
||
test('fixes still uncommitted at the repeat are inside the range', () => {
|
||
const reviewed = head();
|
||
pass(fixing, undefined, 'export const a = 2;\n', false);
|
||
expect(pass(clean, ['--start', 'review', '--fix-range'])).toBe(`FIX_BASE=${reviewed}`);
|
||
expect(gitIn(repo, `diff --name-only ${reviewed}`).trim()).toBe('a.ts');
|
||
});
|
||
|
||
test('chained fix-range passes take the latest fixing pass as base', () => {
|
||
pass(fixing, undefined, 'export const a = 2;\n');
|
||
const second = head();
|
||
pass(fixing, ['--start', 'review', '--fix-range'], 'export const a = 3;\n');
|
||
expect(pass(clean, ['--start', 'review', '--fix-range'])).toBe(`FIX_BASE=${second}`);
|
||
});
|
||
|
||
test.each([
|
||
['no earlier pass', () => {}, 'no earlier pass on this branch'],
|
||
['a clean latest pass', () => pass(clean), 'the latest pass made no fixes'],
|
||
['an incomplete latest pass (coverage restoration stays full)', () => pass({ ...fixing, status: 'unavailable', completed: false }, undefined, 'export const a = 2;\n'), 'the latest pass did not complete'],
|
||
['a Step 11 review after the latest pass', () => {
|
||
pass(fixing, undefined, 'export const a = 2;\n');
|
||
pass({ skill: 'adversarial-review', ...clean }, ['--start', 'adversarial-review']);
|
||
}, 'a Step 11 review followed the latest pass'],
|
||
['a latest pass logged without a start capture', () => {
|
||
edit('export const a = 2;\n');
|
||
expect(bin('gstack-review-log', [row(fixing)]).code).toBe(0);
|
||
}, 'the latest pass has no captured start'],
|
||
['a pass that started from uncommitted content (a reverted dirty hunk would fall outside the range)', () => {
|
||
edit('export const a = 1; // dirty\n');
|
||
pass(fixing, undefined, 'export const a = 1;\n', false);
|
||
}, 'started from uncommitted content'],
|
||
['a pass that started with an untracked file', () => {
|
||
fs.writeFileSync(path.join(repo, 'b.ts'), 'export const b = 1;\n');
|
||
pass(fixing, undefined, 'export const a = 2;\n');
|
||
}, 'started from uncommitted content'],
|
||
['a base that is no longer an ancestor of HEAD', () => {
|
||
edit('export const a = 2;\n');
|
||
commit();
|
||
pass(fixing, undefined, 'export const a = 3;\n');
|
||
gitIn(repo, 'reset -q --hard HEAD~2');
|
||
edit('export const a = 4;\n');
|
||
commit();
|
||
}, 'is not an ancestor of HEAD'],
|
||
])('refuses %s', (_, setup, reason) => {
|
||
setup();
|
||
const r = narrowStart();
|
||
expect(r.code).toBe(1);
|
||
expect(r.out).toBe('');
|
||
expect(r.err).toContain('fix-range refused: ');
|
||
expect(r.err).toContain(reason);
|
||
expect(r.err).toContain('run a full pass (gstack-review-log --start review)');
|
||
});
|
||
|
||
test('rejects an unknown start flag with usage', () => {
|
||
expect(bin('gstack-review-log', ['--start', 'review', '--narrow']).code).toBe(2);
|
||
});
|
||
});
|
||
|
||
describe('a fix-range pass is never the last review', () => {
|
||
test('its row is fix-range scoped whatever the caller claims, unbindable at Step 11.5 and never CURRENT', () => {
|
||
const reviewed = head();
|
||
pass(fixing, undefined, 'export const a = 2;\n');
|
||
pass({ ...clean, scope: 'full', fix_base: 'f'.repeat(40), review_binding: { state: 'verified', start_wtree: W, end_wtree: W }, wtree: W },
|
||
['--start', 'review', '--fix-range']);
|
||
const last = rows().at(-1);
|
||
expect(last.scope).toBe('fix-range');
|
||
expect(last.fix_base).toBe(reviewed);
|
||
expect(last.review_binding.state).toBe('fix-range');
|
||
expect(last.review_binding.range_start_wtree).toBe(last.review_binding.range_end_wtree);
|
||
expect(last.review_binding).not.toHaveProperty('start_wtree');
|
||
expect(last.review_binding).not.toHaveProperty('end_wtree');
|
||
expect(last).not.toHaveProperty('wtree');
|
||
expect(last.review_freshness).toEqual({ status: 'UNVERIFIED', reason: 'fix-range pass; only a later full pass can clear review' });
|
||
expect(narrowStart().err).toContain('the latest pass made no fixes');
|
||
pass(clean);
|
||
expect(rows().at(-1).review_freshness.status).toBe('CURRENT');
|
||
});
|
||
|
||
test('a fixing fix-range pass is not CURRENT either', () => {
|
||
pass(fixing, undefined, 'export const a = 2;\n');
|
||
pass(fixing, ['--start', 'review', '--fix-range'], 'export const a = 3;\n');
|
||
const last = rows().at(-1);
|
||
expect(last.scope).toBe('fix-range');
|
||
expect(last.review_freshness.status).not.toBe('CURRENT');
|
||
});
|
||
|
||
test('a caller cannot narrow a full pass by claiming fix-range', () => {
|
||
pass({ ...clean, scope: 'fix-range', fix_base: W });
|
||
const last = rows().at(-1);
|
||
expect(last).not.toHaveProperty('scope');
|
||
expect(last).not.toHaveProperty('fix_base');
|
||
expect(last.review_freshness.status).toBe('CURRENT');
|
||
});
|
||
|
||
test('a start token that still names a fix base stays narrowed even if its scope is rewritten', () => {
|
||
pass(fixing, undefined, 'export const a = 2;\n');
|
||
const [token] = narrowStart().out.split('\n');
|
||
const slug = bin('gstack-slug', []).out.match(/^SLUG=(.+)$/m)![1]!;
|
||
const file = path.join(root, 'state', 'projects', slug, '.review-starts', `${token}.json`);
|
||
fs.writeFileSync(file, JSON.stringify({ ...JSON.parse(fs.readFileSync(file, 'utf8')), scope: 'full', fix_base: 'not-a-commit' }));
|
||
expect(bin('gstack-review-log', [row(clean), '--finish', token!]).code).toBe(0);
|
||
const last = rows().at(-1);
|
||
expect(last.scope).toBe('fix-range');
|
||
expect(last).not.toHaveProperty('fix_base');
|
||
expect(last.review_freshness.status).toBe('UNVERIFIED');
|
||
});
|
||
|
||
test('freshness rejects a fix-range row even with a verified-looking binding', () => {
|
||
const rec = { skill: 'review', scope: 'fix-range', status: 'clean', issues_found: 0, critical: 0, completed: true, converged: true, wtree: W,
|
||
review_binding: { state: 'verified', start_wtree: W, end_wtree: W } };
|
||
expect(reviewFreshness(rec, W)?.status).toBe('UNVERIFIED');
|
||
expect(reviewFreshness({ ...rec, scope: undefined }, W)?.status).toBe('CURRENT');
|
||
});
|
||
|
||
test('shared-libs advisory reuse never trusts a fix-range row', () => {
|
||
const finding = { severity: 'INFORMATIONAL', advisory: true, action: 'skipped', evidence_paths: ['a.ts'], helper_target: { path: 'b.ts', symbol: 'b' } };
|
||
const prior = { ...finding, fingerprint: sharedLibsFingerprint(finding), snapshot_covered_paths: ['a.ts'] };
|
||
const snapshot = { wtree: W, branch_id: 'd'.repeat(64), covered_paths: ['a.ts'] };
|
||
const full = { skill: 'review', completed: true, converged: true, wtree: W, review_binding: { state: 'verified', start_wtree: W, end_wtree: W, branch_id: 'd'.repeat(64) } };
|
||
expect(canReuseSharedLibsAdvisory(prior, finding, full, snapshot)).toBe(true);
|
||
const narrowed = { ...full, scope: 'fix-range', review_binding: { state: 'fix-range', range_start_wtree: W, range_end_wtree: W, branch_id: 'd'.repeat(64) } };
|
||
expect(canReuseSharedLibsAdvisory(prior, finding, narrowed, snapshot)).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('gstack-specialist-stats', () => {
|
||
test('fix-range passes do not count toward hit rates or gate candidacy', () => {
|
||
const entry = (scope?: string) => JSON.stringify({ ...(scope ? { scope } : {}), specialists: { testing: { dispatched: true, findings: 0 } } });
|
||
const slug = bin('gstack-slug', []).out.match(/^SLUG=(.+)$/m)![1]!;
|
||
const dir = path.join(root, 'state', 'projects', slug);
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
fs.writeFileSync(path.join(dir, 'feature-reviews.jsonl'), [...Array(9).fill(entry()), ...Array(5).fill(entry('fix-range'))].join('\n') + '\n');
|
||
const out = bin('gstack-specialist-stats', []).out;
|
||
expect(out).toContain('SPECIALIST_STATS: 9 reviews analyzed');
|
||
expect(out).toContain('testing: 9/9 dispatched, 0 findings (0%)');
|
||
expect(out).not.toContain('GATE_CANDIDATE');
|
||
});
|
||
});
|
||
|
||
describe('ship Step 9 instructions', () => {
|
||
// Harness lane E (issue row 1): the fix-range token's mandatory final full pass re-read the
|
||
// whole tree and found new neighbours every time. /ship now reads only the range since the
|
||
// previous pass (RANGE_BASE) under a full token, so the final record still binds the tree.
|
||
const army = fs.readFileSync(path.join(ROOT, 'ship/sections/review-army.md'), 'utf8').replace(/\s+/g, ' ');
|
||
test('pass 2+ reads the range since the previous pass under a full token; the fix-range token is /review\'s alone', () => {
|
||
expect(army).toContain('Every entry saves `PASS_HEAD=$(git rev-parse HEAD)`; from pass 2 on, `RANGE_BASE` is the previous pass\'s PASS_HEAD.');
|
||
expect(army).toContain('use `DIFF_BASE=$RANGE_BASE` instead, so every reviewer reads what changed since the previous pass.');
|
||
expect(army).toContain('Every pass captures a full token (no `--fix-range`), so the record binds the whole tree even when reviewers read only the range.');
|
||
expect(army).not.toContain('--start review --fix-range');
|
||
expect(army).toContain('gstack-review-scope classify --base <base> --pass $PASS --range-base $RANGE_BASE');
|
||
expect(army).toContain('A pass whose only findings were deferred or noted applied no edits and continues here: the loop converges below the floor.');
|
||
});
|
||
});
|
||
|
||
describe('review Step 3 and Step 5.8 instructions', () => {
|
||
const skill = fs.readFileSync(path.join(ROOT, 'review/SKILL.md'), 'utf8');
|
||
const step3 = skill.slice(skill.indexOf('## Step 3: Get the diff'), skill.indexOf('## Step 3.4:'));
|
||
const step58 = skill.slice(skill.indexOf('## Step 5.8: Persist Eng Review result'), skill.indexOf('### Report the final review'));
|
||
const flat3 = step3.replace(/\s+/g, ' '), flat58 = step58.replace(/\s+/g, ' ');
|
||
|
||
test('the first pass starts full and only the pass after an edited pass narrows its DIFF_BASE, never Step 4.8 or other base uses', () => {
|
||
const block = step3.slice(step3.indexOf('```bash'), step3.indexOf('```', step3.indexOf('```bash') + 7));
|
||
expect(block).toContain('gstack-review-log --start review\ngit diff "$DIFF_BASE"');
|
||
expect(block).not.toContain('--fix-range');
|
||
expect(flat3).toContain('After an edited pass, start the next with `--start review --fix-range`: it prints REVIEW_START, then `FIX_BASE=<sha>`. Wherever this pass sets DIFF_BASE outside Step 4.8, use FIX_BASE instead; Step 4.8 and every other `<base>` use stay full. On refusal, start a full pass.');
|
||
expect(skill.match(/--fix-range/g)).toHaveLength(1);
|
||
});
|
||
|
||
test('an edited pass is logged before the repeat, kept out of Step 5.0, and a fix-range pass is never the final review', () => {
|
||
const edited = flat58.indexOf("Edited: increment CYCLES once, then fill and save the record below with `converged:false`. Step 5.0 never treats this invocation's records as prior reviews.");
|
||
const repeat = flat58.indexOf('Below 3, repeat Steps 3–5 with a new REVIEW_START.');
|
||
const none = flat58.indexOf('No edits: after a full pass, fill the record below. A fix-range pass is never final: repeat Steps 3–5 with a full start.');
|
||
expect(edited).toBeGreaterThan(-1);
|
||
expect(repeat).toBeGreaterThan(edited);
|
||
expect(none).toBeGreaterThan(repeat);
|
||
expect(flat58).toContain("finish only this pass's own token");
|
||
});
|
||
});
|
||
|
||
describe('review Step 3/5.8 order against the real logger', () => {
|
||
// /review's order on every pass: core start, Step 4.8 attempt start and finish, fixes in the working tree, core finish; never a commit.
|
||
const reviewRow = (fields: Record<string, unknown>) => JSON.stringify({ skill: 'review', status: 'clean', issues_found: 0, critical: 0, informational: 0, ...fields });
|
||
function reviewPass(fields: Record<string, unknown>, start: string[], fixes?: string) {
|
||
const started = bin('gstack-review-log', start);
|
||
expect(started.code).toBe(0);
|
||
const [token, base] = started.out.split('\n');
|
||
// Step 4.8: each pass's adversarial attempt saves its row before any fix and before the core row.
|
||
const attempt = bin('gstack-review-log', ['--start', 'adversarial-review']);
|
||
expect(attempt.code).toBe(0);
|
||
expect(bin('gstack-review-log', [JSON.stringify({ skill: 'adversarial-review', status: 'clean', source: 'claude', ...clean }), '--finish', attempt.out]).code).toBe(0);
|
||
if (fixes) edit(fixes);
|
||
expect(bin('gstack-review-log', [reviewRow(fields), '--finish', token!]).code).toBe(0);
|
||
return base;
|
||
}
|
||
|
||
test('a committed first pass narrows the next repeat to the uncommitted fixes, then the chain returns to a full pass', () => {
|
||
const reviewed = head();
|
||
reviewPass(fixing, ['--start', 'review'], 'export const a = 2;\n');
|
||
expect(reviewPass(fixing, ['--start', 'review', '--fix-range'], 'export const a = 3;\n')).toBe(`FIX_BASE=${reviewed}`);
|
||
expect(gitIn(repo, `diff --name-only ${reviewed}`).trim()).toBe('a.ts');
|
||
const third = narrowStart();
|
||
expect(third.code).toBe(1);
|
||
expect(third.err).toContain('started from uncommitted content');
|
||
reviewPass(clean, ['--start', 'review']);
|
||
const last = rows().at(-1)!;
|
||
expect(last.scope).toBeUndefined();
|
||
expect(reviewFreshness(last, last.wtree)?.status).toBe('CURRENT');
|
||
}, 30_000);
|
||
|
||
test('a zero-edit fix-range pass is never CURRENT even if it were logged, so the final record needs a full pass', () => {
|
||
reviewPass(fixing, ['--start', 'review'], 'export const a = 2;\n');
|
||
reviewPass(clean, ['--start', 'review', '--fix-range']);
|
||
const narrowed = rows().at(-1)!;
|
||
expect(narrowed.scope).toBe('fix-range');
|
||
expect(reviewFreshness(narrowed, narrowed.review_binding.range_end_wtree)?.status).toBe('UNVERIFIED');
|
||
}, 30_000);
|
||
|
||
test('a first pass over uncommitted work cannot be narrowed', () => {
|
||
edit('export const a = 1; // uncommitted\n');
|
||
reviewPass(fixing, ['--start', 'review'], 'export const a = 2;\n');
|
||
const r = narrowStart();
|
||
expect(r.code).toBe(1);
|
||
expect(r.err).toContain('started from uncommitted content');
|
||
}, 30_000);
|
||
});
|