Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
dff2b78
feat(review): content-anchored incremental rounds for the local revie…
wenshao Aug 21, 2026
ee54f8b
fix(review): close nine Criticals, most of them my own last round's
wenshao Aug 21, 2026
0810d06
fix(review): close round 2 — the cache rename's fallout, and the stop…
wenshao Aug 22, 2026
91d93cb
fix(review): close round 3 — the cache records the gate's identity, a…
github-actions[bot] Aug 22, 2026
87b769b
fix(review): close round 4 — the named-path flow's six dead ends
github-actions[bot] Aug 22, 2026
c661806
fix(review): close round 5 — including a gate of mine that could neve…
wenshao Aug 22, 2026
feb55b5
fix(review): close round 6 — the five Criticals the re-read of HEAD s…
github-actions[bot] Aug 22, 2026
b295232
fix(review): close round 7 — and stop enumerating the fail-closed list
wenshao Aug 23, 2026
ecf5fea
merge: keep both sets of SKILL guards
wenshao Aug 23, 2026
af708af
fix(review): close round 8 — the four shapes the last fixes left open
wenshao Aug 23, 2026
575d136
fix(review): close round 9 — date a stop's blockers; key the local ca…
wenshao Aug 23, 2026
8192762
fix(review): close round 10 — date each stop blocker against its own …
wenshao Aug 23, 2026
df533ff
fix(review): close round 11 — date blockers on bytes; certify vanishe…
wenshao Aug 23, 2026
3b916d6
fix(review): close round 12 — attribute landed fixes, date by regime,…
github-actions[bot] Aug 24, 2026
4142065
Merge origin/main into review-incremental-anchor
wenshao Aug 24, 2026
e6aa683
fix(review): close round 13 — pay the isolation test's import cost at…
github-actions[bot] Aug 24, 2026
99fff42
Merge branch 'review/local-content-anchor' of https://github.com/Qwen…
qwen-code-dev-bot Aug 24, 2026
1f0fb94
refactor(review): drop the stop-round blocker-dating chain
wenshao Aug 24, 2026
7774eb3
Merge remote round-12/13 autofix commits, keeping the chain deleted
wenshao Aug 24, 2026
b13eb13
Merge remote-tracking branch 'origin/main' into p1
wenshao Aug 24, 2026
4a24ca5
fix(review): close round 14 — certify what git diff cannot see, split…
github-actions[bot] Aug 24, 2026
5dba419
fix(review): close round 15 — withhold the candidate under visibility…
github-actions[bot] Aug 25, 2026
dc74e83
fix(review): close round 15 review — bracket the visibility oracle, g…
wenshao Aug 25, 2026
c723d5f
fix(review): close round 16 — withhold the incremental stops under --…
github-actions[bot] Aug 25, 2026
af521c5
fix(review): gate decided stops on a composed re-rule verdict (#9941)
wenshao Aug 25, 2026
0d11241
Revert "fix(review): gate decided stops on a composed re-rule verdict…
wenshao Aug 25, 2026
2f050a6
Merge remote-tracking branch 'origin/main' into p1
wenshao Aug 25, 2026
0cf1b07
fix(cli): drop duplicate getCurrentCustomTitle key that breaks npm ci
wenshao Aug 25, 2026
8b81762
fix(cli): drop the second duplicate key too (getSessionDisplayName)
wenshao Aug 25, 2026
2e03ecd
Merge branch 'main' into review/local-content-anchor
qwen-code-dev-bot Aug 25, 2026
9443811
test(review): pin the visibility oracle's sample count
wenshao Aug 25, 2026
49c2db3
Merge remote-tracking branch 'origin/review/local-content-anchor' int…
wenshao Aug 25, 2026
79dfd58
fix(review): close round 17 — restore the merged-away fake, publish t…
wenshao Aug 26, 2026
eef3c34
fix(review): close round 18 — ask git for the cone, make the cap idem…
wenshao Aug 26, 2026
0f70270
fix(review): close round 19 — only ENOENT is absence, undecodable is …
wenshao Aug 26, 2026
518d26f
fix(review): close round 20 — bool-typed fileMode, no directory in th…
wenshao Aug 26, 2026
146ab25
fix(review): close round 21 — measure the pointer, ride the skipped l…
wenshao Aug 27, 2026
60bb3ce
fix(review): close round 22 — a gitlink's identity is real, on both s…
wenshao Aug 27, 2026
e94892e
Merge remote-tracking branch 'origin/main' into p1
wenshao Aug 27, 2026
f1d9c62
fix(review): close round 23 — withhold over the dropped-out set, excl…
wenshao Aug 27, 2026
f425a8c
fix(review): keep this run's stop sidecar across its own cleanup
wenshao Aug 27, 2026
917e1d4
Merge remote-tracking branch 'origin/main' into p1
wenshao Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,076 changes: 1,076 additions & 0 deletions packages/cli/src/commands/review/capture-local.incremental.test.ts

Large diffs are not rendered by default.

284 changes: 284 additions & 0 deletions packages/cli/src/commands/review/capture-local.toctou.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

// The TOCTOU withhold branch, in isolation: when the re-capture after hashing
// returns different bytes, the candidate must NOT be written and the refusal
// must be said out loud — the one uncertainty in the anchor module that used
// to fail open. The capture layer is mocked with a stateful fake so the two
// captures can disagree deterministically; everything downstream is real.

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
mkdtempSync,
rmSync,
writeFileSync,
readFileSync,
existsSync,
realpathSync,
} from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const stderrLines: string[] = [];
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: vi.fn(),
writeStderrLine: vi.fn((line: string) => {
stderrLines.push(line);
}),
writeStderrLineSafe: vi.fn(),
}));

const captures: Array<{ diff: Buffer }> = [];
/**
* Successive `hashWorktreeFiles` answers, when a test needs the passes to
* disagree. Empty means "use the real one" — every other test in this file
* hashes for real. A list shorter than the number of passes REPEATS its last
* entry, which reads as "the tree stopped moving": a fixture says what it is
* about and the guard's extra samples see a settled tree.
*/
const hashPasses: Array<Record<string, string>> = [];
vi.mock('./lib/local-anchor.js', async (importOriginal) => {
const real = await importOriginal<typeof import('./lib/local-anchor.js')>();
return {
...real,
hashWorktreeFiles: (...args: Parameters<typeof real.hashWorktreeFiles>) =>
hashPasses.length > 0
? ((hashPasses.length > 1
? hashPasses.shift()
: hashPasses[0]) as Record<string, string>)
: real.hashWorktreeFiles(...args),
};
});
vi.mock('./lib/local-diff.js', async (importOriginal) => {
const real = await importOriginal<typeof import('./lib/local-diff.js')>();
return {
...real,
captureLocalDiff: vi.fn(() => {
const next = captures.length > 1 ? captures.shift() : captures[0];
if (!next) throw new Error('fixture exhausted');
return {
diff: next.diff,
untracked: [],
skipped: [],
unbornHead: false,
repoRoot: repo,
};
}),
};
});

import { captureLocalCommand } from './capture-local.js';
import { isolateHostGitConfig } from './lib/test-utils.js';

let repo: string;
let cwd: string;
let gitIsolation: ReturnType<typeof isolateHostGitConfig>;

beforeEach(() => {
stderrLines.length = 0;
captures.length = 0;
hashPasses.length = 0;
repo = realpathSync(mkdtempSync(join(tmpdir(), 'review-toctou-')));
cwd = process.cwd();
process.chdir(repo);
gitIsolation = isolateHostGitConfig();
const git = (...args: string[]) =>
execFileSync('git', args, { cwd: repo, encoding: 'utf8' });
git('init', '-q', '--template=', '.');
git('config', 'user.email', 'a@b');
git('config', 'user.name', 'a');
git('config', 'commit.gpgsign', 'false');
writeFileSync(join(repo, 'a.ts'), 'export const a = 1;\n');
git('add', '-A');
git('commit', '-q', '--no-verify', '-m', 'base');
});

afterEach(() => {
process.chdir(cwd);
rmSync(repo, { recursive: true, force: true });
gitIsolation.dispose();
});

const DIFF_A = Buffer.from(
'diff --git a/a.ts b/a.ts\nindex 000..111 100644\n--- a/a.ts\n+++ b/a.ts\n@@ -1,1 +1,1 @@\n-export const a = 1;\n+export const a = 2;\n',
'utf8',
);

function run(extra: Record<string, unknown> = {}): void {
(captureLocalCommand.handler as (argv: unknown) => void)({
out: join(repo, 'plan.json'),
target: 'local',
untracked: true,
...extra,
});
}

/** Read the plan report `run()` just wrote. */
function report(): { incremental?: unknown; diffPath: string } {
return JSON.parse(readFileSync(join(repo, 'plan.json'), 'utf8')) as {
incremental?: unknown;
diffPath: string;
};
}

describe('capture-local — TOCTOU candidate withholding', () => {
it('a tree that moved between capture and hash withholds the candidate, out loud', () => {
captures.push(
{ diff: DIFF_A },
{ diff: Buffer.from('changed mid-hash\n') },
);
run();
expect(
existsSync(
join(repo, '.qwen/tmp/qwen-review-local-cache-candidate.json'),
),
).toBe(false);
expect(stderrLines.join('\n')).toContain(
'working tree changed while the capture was being hashed',
);
});

it('a moved tree refuses THIS round\u2019s scoping too, not just the candidate', () => {
// Withholding only the candidate protects the NEXT round and leaves this
// one wrong: the scoping compares the very hashes the guard just proved
// may not describe the capture under review. A file edited during the
// hash pass and reverted before it is hashed reads as unchanged,
// `changedSince` reports nothing, and its diff section is sliced out —
// the round then says "nothing to re-review" over a capture no agent
// read. Promote a real candidate first, so the anchor is otherwise
// valid and the refusal can only come from the guard.
captures.push({ diff: DIFF_A }, { diff: Buffer.from(DIFF_A) });
run({ model: 'model-a' });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-5: Still stands — model in argv remains a dead fixture argument: CaptureLocalArgs has no model field, the yargs builder defines none, and identity comes exclusively from roundModelIdFrom(process.env) — this file never sets QWEN_CODE_MODEL_IDENTITY, so the comment's isolation claim ("the anchor is otherwise valid and the refusal can only come from the guard") is false: the same-model gate also refuses this anchor. Witness (mutation arm): reordering the gates (model before treeHeldStill, guard intact) makes this test fail with a model-mismatch refusal instead of the TOCTOU refusal — a false regression signal pointing at the guard. The sibling harness solves this explicitly (strips model from argv, sets the env identity). Fix: mirror it here, or drop the no-op keys and the claim.

中文说明

[Suggestion] 仍然成立 —— argv 里的 model 依旧是死夹具参数:CaptureLocalArgs 没有 model 字段,yargs builder 也未定义,身份完全来自 roundModelIdFrom(process.env) —— 本文件又从不设置 QWEN_CODE_MODEL_IDENTITY,因此注释的隔离声明(「锚点在其他方面有效,拒绝只能来自守卫」)不成立:同模型门同样会拒绝这个锚点。实测(变异臂):调整门顺序(model 提到 treeHeldStill 之前、守卫不动)会让本测试以模型不匹配拒绝而非 TOCTOU 拒绝失败 —— 一个把矛头指向守卫的假回归信号。兄弟夹具已明确解决(从 argv 剥掉 model、设置环境身份)。修复:在此照做,或删掉无效键与该声明。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to the next round — this round's batch was bounded: it implemented the two still-open Criticals (the PR cache's recorded identity — {{model}} → the fetch report's reviewModelId — and the stale <target>.json naming prose) plus six Suggestions. This finding is acknowledged and queued; no code was changed for it in this round. The toctou test's model argv key is dead and the env identity is unset there.

中文说明

延迟到下一轮——本轮批次有上限:本轮实现了两个仍未关闭的 Critical(PR 缓存记录的身份——{{model}} → fetch 报告的 reviewModelId——以及过时的 <target>.json 命名文案),另加六条 Suggestion。该发现已确认并排队;本轮未对其改动代码。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to the next round. This round's batch was capped at the six round-3 Criticals, all fixed in 87b769b (comma-driver fold, directory plumbing targets, ignore-rules exemption, directory-target tracked drop, cap-before-parse, file-review prose). R1-5 the toctou suite's unused model argument remains. It stays queued for the next round.

中文说明

延迟到下一轮处理。本轮批次上限为六个 round-3 Critical,已在 87b769b 中全部修复(逗号驱动名折叠、目录形 plumbing 目标、ignore 规则豁免、目录目标的 tracked 剔除、限额先于解析、文件评审措辞)。R1-5:toctou 套件中未使用的 model 参数仍在。该条保留在队列中,下一轮处理。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-5 (carried): model: 'model-a' passed through run()'s argv is dead input — CaptureLocalArgs has no model field; the round identity comes exclusively from roundModelIdFrom(process.env). So this test's isolation claim ("the anchor is otherwise valid and the refusal can only come from the guard") is false: isolateHostGitConfig never blanks QWEN_CODE_MODEL*, the running identity is the host's (ordinarily ''), certifierMatchesRound('model-a', '') is false, and the same-model gate ALSO refuses. The test passes only because anchorRefusalReason orders the treeHeldStill clause first and the assertion pins that clause's exact text. A maintainer extending this file with the complementary case (still tree + cache, expecting the anchor to be HONOURED) copies run({ model, cache }), gets a model-gate refusal instead of incremental scope, and diagnoses the production gate rather than the fixture; under a Qwen-Code-spawned environment the host identity is machine-dependent. Fix: mirror the sibling harness — destructure model out of extra and set process.env['QWEN_CODE_MODEL_IDENTITY'] around the handler call (restore in finally); or drop the model keys and correct the "otherwise valid" comment.

中文说明

R1-5(沿用):经 run() argv 传入的 model: 'model-a' 是死输入——CaptureLocalArgs 没有 model 字段,轮次身份只来自 roundModelIdFrom(process.env)。因此本测试的隔离声明(「锚点在其余方面有效,拒绝只能来自守卫」)是假的:isolateHostGitConfig 不清空 QWEN_CODE_MODEL*,运行身份取宿主值(通常为 ''),certifierMatchesRound('model-a', '') 为 false,同模型门禁同样会拒绝。测试之所以通过,仅因 anchorRefusalReasontreeHeldStill 子句排在最前、断言钉的又是该子句的确切文案。维护者若要在此文件补一个互补用例(静止树+缓存、期望锚点被接受),照抄 run({ model, cache }) 会得到模型门禁的拒绝而非增量范围,从而误诊生产门禁而不是 fixture;在 Qwen Code 派生的环境里宿主身份还是机器相关的。修复:仿照姊妹测试的 harness——把 modelextra 解构出来,在 handler 调用前后设置/恢复 process.env['QWEN_CODE_MODEL_IDENTITY'];或删掉 model 键并更正「其余方面有效」的注释。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to the next round. This round's batch was capped at the six round-3 Criticals, all fixed in 87b769b (comma-driver fold, directory plumbing targets, ignore-rules exemption, directory-target tracked drop, cap-before-parse, file-review prose). R1-5 the toctou suite's unused model argument remains. It stays queued for the next round.

中文说明

延迟到下一轮处理。本轮批次上限为六个 round-3 Critical,已在 87b769b 中全部修复(逗号驱动名折叠、目录形 plumbing 目标、ignore 规则豁免、目录目标的 tracked 剔除、限额先于解析、文件评审措辞)。R1-5:toctou 套件中未使用的 model 参数仍在。该条保留在队列中,下一轮处理。

Comment on lines +181 to +182

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-5: (carried from round 1, re-confirmed): model: 'model-a' passed through run()'s argv is dead input — CaptureLocalArgs has no model field; round identity comes exclusively from roundModelIdFrom(process.env), which the suite never sets.

When it bites: certifierMatchesRound('model-a', '') is false by construction, so the cached anchor is NOT 'otherwise valid' as the comment claims — the model gate would refuse it too; the test passes only because anchorRefusalReason checks !treeHeldStill before the model clause. A maintainer copying run({ model: ... }) into a test where the model gate must PASS gets a silent refusal; the inaccurate comment hides why the fixture never established the isolation it advertises. The sibling incremental harness routes model into the env correctly.

Suggested fix: Route model into QWEN_CODE_MODEL_IDENTITY in run() like the sibling harness, or drop the inert key and reword the comment to 'the guard's clause is checked first, so the emitted refusal can only be the guard's'.

中文说明

[Suggestion] R1-5: (沿用第 1 轮编号,本轮重新确认)run() argv 里的 model: 'model-a' 是死输入——CaptureLocalArgs 没有 model 字段,轮次身份只来自 roundModelIdFrom(process.env),而本套件从不设置它。certifierMatchesRound('model-a', '') 按构造为假,所以缓存锚点并不像注释声称的那样「otherwise valid」——模型门同样会拒绝它;测试能通过仅因 anchorRefusalReason 先检查 !treeHeldStill。把这个 run({ model: ... }) 模式抄进「模型门必须通过」的测试会得到静默拒绝;不准确的注释掩盖了 fixture 从未建立其宣称的隔离。修复:像兄弟增量脚手架那样把 model 路由进 QWEN_CODE_MODEL_IDENTITY,或删掉该惰性键并把注释改写为「守卫子句先被检查,所以打印的拒绝只能是守卫的」。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +181 to +182

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-5: (carried from round 1, re-confirmed at HEAD) model: 'model-a' in run()'s argv is dead input — CaptureLocalArgs has no model field; identity comes exclusively from roundModelIdFrom(process.env). Unlike the sibling incremental harness, this file's run() never sets QWEN_CODE_MODEL_IDENTITY, so the test's isolation premise ("the anchor is otherwise valid and the refusal can only come from the guard") is not established: the model gate would also refuse (certifierMatchesRound('model-a', '') mismatches by construction), and the guard wins only because treeHeldStill is the first clause of anchorRefusalReason.

Fix: mirror the sibling harness — translate model into QWEN_CODE_MODEL_IDENTITY in run() with a try/finally restore.

中文说明

(自第 1 轮携带,已在 HEAD 重新确认)run() argv 中的 model: 'model-a' 是死输入——CaptureLocalArgs 没有 model 字段,身份只来自 roundModelIdFrom(process.env)。与姊妹的 incremental 测试支架不同,本文件的 run() 从不设置 QWEN_CODE_MODEL_IDENTITY,因此测试的隔离前提(「锚点在其他方面有效,拒绝只能来自守卫」)并不成立:模型门同样会拒绝(certifierMatchesRound('model-a', '') 按构造即失配),守卫之所以胜出仅因 treeHeldStill 是 anchorRefusalReason 的第一子句。修复:对齐姊妹支架——在 run() 中把 model 转设为 QWEN_CODE_MODEL_IDENTITY 并在 try/finally 中恢复。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to the next round. This round's batch is capped at the 5 fresh Criticals the round-5 re-read of HEAD raised (Criticals first); this finding — “R1-5: (carried from round 1, re-confirmed at HEAD) model: 'model-a' in run()'s argv is dead input” — stays open and is queued for the next round. It was not declined, only sequenced.

中文说明

延迟到下一轮处理。本轮的批次上限是第 5 轮对 HEAD 重新审查提出的 5 个新 Critical(Critical 优先);本条发现——“R1-5: (carried from round 1, re-confirmed at HEAD) model: 'model-a' in run()'s argv is dead input”——保持打开状态并排入下一轮队列。它没有被拒绝,只是排序靠后。

const cachePath = join(repo, 'cache.json');
const promoted = JSON.parse(
readFileSync(
join(repo, '.qwen/tmp/qwen-review-local-cache-candidate.json'),
'utf8',
),
) as Record<string, unknown>;
writeFileSync(
cachePath,
JSON.stringify({ ...promoted, lastModelId: 'model-a' }),
);

// Round 2: same anchor, but the tree moves under the hash pass.
stderrLines.length = 0;
captures.push(
{ diff: DIFF_A },
{ diff: Buffer.from('changed mid-hash\n') },
);
run({ model: 'model-a', cache: cachePath });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This test's stated isolation — "the anchor is otherwise valid and the refusal can only come from the guard" — is not achieved: model in argv is inert (CaptureLocalArgs has no model field; the round identity is read only from QWEN_CODE_MODEL_IDENTITY via roundModelIdFrom), so round 2's running identity is '' and the model gate (certifierMatchesRound('model-a', '')) would refuse the anchor even if the tree held still. Mutation-verified: moving the treeHeldStill clause below the model clause fails the test with the MODEL-gate message ("reviewed by model-a, not …") — the refusal comes from the model gate, proving the anchor is not otherwise valid and the test passes only because treeHeldStill is the first clause in anchorRefusalReason. If the clauses are ever reordered, this test fails although the guard still works — and the obvious "fix" (updating the expected substring to the model-mismatch message) leaves a test named for the guard that no longer exercises it. Set/restore process.env['QWEN_CODE_MODEL_IDENTITY'] = 'model-a' around run() (as the sibling suite's capture() does) and drop the dead model keys.

中文说明

[Suggestion] 该测试声明的隔离性——「锚点在其他方面有效,拒绝只能来自守卫」——并未成立:argv 里的 model 是惰性的(CaptureLocalArgs 没有 model 字段;轮次身份只经 roundModelIdFromQWEN_CODE_MODEL_IDENTITY 读取),所以第 2 轮的运行身份是 '',即使树保持静止,模型门(certifierMatchesRound('model-a', ''))也会拒绝锚点。变异实测:把 treeHeldStill 条款移到模型条款之下,测试以模型门消息("reviewed by model-a, not …")失败——拒绝来自模型门,证明锚点在其他方面并不有效,测试通过仅仅因为 treeHeldStillanchorRefusalReason 的第一条款。若条款顺序被重排,守卫仍工作时本测试却会失败——而显眼的「修复」(把期望子串改成模型不匹配消息)会留下一个以守卫命名却不再检验守卫的测试。建议在 run() 前后设置/恢复 process.env['QWEN_CODE_MODEL_IDENTITY'] = 'model-a'(如兄弟套件 capture() 所做),并删掉惰性的 model 键。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same finding as the round-2 re-post rc:3833436424 — deferred to the next round together with it; see that thread for the specifics.

中文说明

与第 2 轮重发的 rc:3833436424 是同一发现——与它一起延迟到下一轮;具体情况见该线程。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to the next round. This round's batch was capped at the six round-3 Criticals, all fixed in 87b769b (comma-driver fold, directory plumbing targets, ignore-rules exemption, directory-target tracked drop, cap-before-parse, file-review prose). R1-5 the toctou suite's unused model argument remains. It stays queued for the next round.

中文说明

延迟到下一轮处理。本轮批次上限为六个 round-3 Critical,已在 87b769b 中全部修复(逗号驱动名折叠、目录形 plumbing 目标、ignore 规则豁免、目录目标的 tracked 剔除、限额先于解析、文件评审措辞)。R1-5:toctou 套件中未使用的 model 参数仍在。该条保留在队列中,下一轮处理。


expect(report().incremental).toBeUndefined();
expect(stderrLines.join('\n')).toContain(
'Incremental anchor not used — the working tree changed while the ' +
'capture was being hashed',
);
// The full capture is what the plan reviews.
expect(readFileSync(report().diffPath).equals(DIFF_A)).toBe(true);
});

it('does not call a MOVED tree clean, even with an empty capture', () => {
// The two stops contradicted each other. A review starting on an empty
// tree, with an autosave landing inside the capture window, withholds the
// candidate and refuses the anchor — and then wrote `clean-tree` anyway,
// because capture 0's diff is empty. stderr printed both lines back to
// back, the round stopped on the second, and the just-written change went
// unreviewed while the run was recorded as clean.
captures.push(
{ diff: Buffer.from('') },
{ diff: DIFF_A },
{ diff: DIFF_A },
);
run();
const plan = JSON.parse(
readFileSync(join(repo, 'plan.json'), 'utf8'),
) as Record<string, unknown>;
expect(plan['nothingToReview']).toBeUndefined();
const err = stderrLines.join('\n');
expect(err).toContain(
'working tree changed while the capture was being hashed',
);
// …and the PROSE must not contradict it either. The orchestrator branches
// on these sentences, and the round printed "the working tree is clean"
// right after the line above until this was gated too.
expect(err).not.toContain('the working tree is clean');
expect(err).toContain('this is NOT a clean tree');
});

it('catches a PHASE-ALIGNED write the pairwise guard let through', () => {
// Two samples of each kind, compared pairwise, never tied a capture to
// the hashes recorded beside it. Three timed writes defeat it: X→Y
// before the hash pass, Y→X before the re-capture, X→Y after it. The two
// captures agree (X, X) and the two hash passes agree (Y, Y), so
// `treeHeldStill` is true — and the candidate certifies Y's identity for
// a round that reviewed X. Promoted, the next round compares cache Y
// against tree Y, finds no delta and says "No changes" over bytes no
// round ever read.
//
// Interleaving a third sample of each kind means the write pattern has
// to keep alternating; this one stops, and the third hash pass reads
// what the captures did.
captures.push(
{ diff: DIFF_A },
{ diff: Buffer.from(DIFF_A) },
{ diff: Buffer.from(DIFF_A) },
);
hashPasses.push(
{ 'a.ts': '100644:oid-Y' },
{ 'a.ts': '100644:oid-Y' },
{ 'a.ts': '100644:oid-X' },
);
run();
expect(
existsSync(
join(repo, '.qwen/tmp/qwen-review-local-cache-candidate.json'),
),
).toBe(false);
expect(stderrLines.join('\n')).toContain(
'working tree changed while the capture was being hashed',
);
});

it('catches a same-bytes revert that STRADDLES the hash pass', () => {
// The hash pass sits BETWEEN the two diff snapshots, so a write that
// straddles it is invisible to the diffs alone: capture B0 → autosave
// writes B1 → the hashes read B1 → undo restores B0 → the re-capture
// reads B0. Both diffs agree and the candidate certifies B1's identity
// for a round that reviewed B0.
//
// The note here used to call that shape harmless and a
// different-bytes revert the uncatchable one — backwards: a
// different-bytes revert moves the endpoints and IS caught by the
// diffs. Re-hashing after the re-capture is what sees this one.
captures.push({ diff: DIFF_A }, { diff: Buffer.from(DIFF_A) });
// The two diffs AGREE — that is the point. What disagrees is the pair of
// hash passes that bracket the re-capture: the first read B1, the second
// reads B0.
hashPasses.push({ 'a.ts': '100644:oid-B1' }, { 'a.ts': '100644:oid-B0' });
run();
expect(
existsSync(
join(repo, '.qwen/tmp/qwen-review-local-cache-candidate.json'),
),
).toBe(false);
expect(stderrLines.join('\n')).toContain(
'working tree changed while the capture was being hashed',
);
});

it('a tree that held still writes the candidate and no warning', () => {
captures.push({ diff: DIFF_A }, { diff: Buffer.from(DIFF_A) });
run();
expect(
existsSync(
join(repo, '.qwen/tmp/qwen-review-local-cache-candidate.json'),
),
).toBe(true);
expect(stderrLines.join('\n')).not.toContain('candidate is withheld');
});
});
Loading
Loading