-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(review): lock the PR review worktree lease against concurrent sessions #9211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
baa828e
95db313
6f3a68e
2a7d9a5
80e69da
e865fbc
0da76ba
2473f80
5c852cd
01f6755
b4d4f1d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| // SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { join } from 'node:path'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const mocks = vi.hoisted(() => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| execFileSync: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -16,6 +17,8 @@ const mocks = vi.hoisted(() => ({ | |||||||||||||||||||||||||||||||||||||||||||||||
| writeStdoutLine: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| writeStderrLine: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearReviewWorktreeLease: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| readReviewWorktreeLease: vi.fn((): unknown => null), | ||||||||||||||||||||||||||||||||||||||||||||||||
| reviewLeaseHeldByAnotherSession: vi.fn((_lease: unknown): boolean => false), | ||||||||||||||||||||||||||||||||||||||||||||||||
| refExists: vi.fn(() => true), | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The parameter is declared so `mock.calls` is typed `[string][]` rather than | ||||||||||||||||||||||||||||||||||||||||||||||||
| // `[][]` — the paths it was asked to free are the assertion in the sweep test. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -64,6 +67,12 @@ vi.mock('../../utils/stdioHelpers.js', () => ({ | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| vi.mock('../../services/review-worktree-lease.js', () => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearReviewWorktreeLease: mocks.clearReviewWorktreeLease, | ||||||||||||||||||||||||||||||||||||||||||||||||
| readReviewWorktreeLease: mocks.readReviewWorktreeLease, | ||||||||||||||||||||||||||||||||||||||||||||||||
| reviewLeaseHeldByAnotherSession: mocks.reviewLeaseHeldByAnotherSession, | ||||||||||||||||||||||||||||||||||||||||||||||||
| reviewLeasePath: (repositoryRoot: string, target: string) => | ||||||||||||||||||||||||||||||||||||||||||||||||
| `${repositoryRoot}/.qwen/tmp/qwen-review-lease-${target}.json`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| isReviewLeaseFile: (fileName: string) => | ||||||||||||||||||||||||||||||||||||||||||||||||
| /^qwen-review-lease-pr-\d+\.json$/.test(fileName), | ||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| vi.mock('./lib/git.js', () => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -83,6 +92,7 @@ vi.mock('./lib/paths.js', () => ({ | |||||||||||||||||||||||||||||||||||||||||||||||
| probeWorktreePath: (path: string) => `${path}-probe`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| baseWorktreePath: (path: string) => `${path}-base`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| reviewBranch: (prNumber: string) => `qwen-review/pr-${prNumber}`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| LEASE_PREFIX: 'qwen-review-lease-', | ||||||||||||||||||||||||||||||||||||||||||||||||
| REVIEW_TMP_DIR: '/repo/.qwen/tmp', | ||||||||||||||||||||||||||||||||||||||||||||||||
| tmpFile: (target: string, suffix: string) => | ||||||||||||||||||||||||||||||||||||||||||||||||
| `/repo/.qwen/tmp/qwen-review-${target}-${suffix}`, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -107,6 +117,9 @@ describe('runCleanup', () => { | |||||||||||||||||||||||||||||||||||||||||||||||
| freed: false, | ||||||||||||||||||||||||||||||||||||||||||||||||
| reason: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // clearAllMocks keeps implementations a prior test set — drop them so a | ||||||||||||||||||||||||||||||||||||||||||||||||
| // throwing rmSync cannot leak into tests that expect deletion to work. | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.rmSync.mockReset(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it('keeps the lease when branch deletion fails', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -138,6 +151,163 @@ describe('runCleanup', () => { | |||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it('clears the lease when only a side file fails to delete', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The lease guards the worktree and branch, not side files: once those | ||||||||||||||||||||||||||||||||||||||||||||||||
| // are freed, a residue a later sweep retries must not keep the lock held | ||||||||||||||||||||||||||||||||||||||||||||||||
| // — a leftover lease refuses every later fetch-pr of this PR and skips | ||||||||||||||||||||||||||||||||||||||||||||||||
| // every later cleanup, and nothing sweeps it automatically. | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.execFileSync.mockReturnValue(Buffer.from('')); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.existsSync.mockReturnValue(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.readdirSync.mockReturnValue(['qwen-review-pr-123-diff.txt']); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.rmSync.mockImplementation(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| throw Object.assign(new Error('EACCES'), { code: 'EACCES' }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| runCleanup('pr-123'); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.writeStderrLine).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect.stringContaining('Failed to remove'), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.clearReviewWorktreeLease).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.cwd(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it('skips the whole target when another session holds the lease (#9205)', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The incident shape: session B cleans up while session A is mid-review. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Nothing of A's may be touched — worktree, siblings, branch, side files, | ||||||||||||||||||||||||||||||||||||||||||||||||
| // audit window, or the lease itself. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+178
to
+180
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R4-3 (round-4 re-review — still stands): the lease-skip test's side-file safety assertion is vacuous — Witness: probe — mutant adding a pre-gate side-file sweep → 34/34 green (the suite cannot see the holder's side files being swept).
Suggested change
中文说明[Suggestion] R4-3(第 4 轮复审——仍然成立):租约跳过测试的附属文件安全断言是空洞的—— 证据:探针——在门禁前加入附属文件清扫的变异体 → 34/34 全绿(套件看不到持有者的附属文件被清扫)。 建议修复:在测试中填充 tmp 目录使清扫真正运行,从而让跳过逻辑的附属文件一侧可观察,见上方 suggestion。 — qwen3.8-max via Qwen Code /review (v0.21.12) |
||||||||||||||||||||||||||||||||||||||||||||||||
| const lease = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| sessionId: 'session-a', | ||||||||||||||||||||||||||||||||||||||||||||||||
| promptId: 'prompt-a', | ||||||||||||||||||||||||||||||||||||||||||||||||
| target: 'pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| repositoryRoot: '/repo', | ||||||||||||||||||||||||||||||||||||||||||||||||
| worktreePath: '/repo/.qwen/tmp/review-pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| branch: 'qwen-review/pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.readReviewWorktreeLease.mockReturnValueOnce(lease); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.reviewLeaseHeldByAnotherSession.mockImplementationOnce( | ||||||||||||||||||||||||||||||||||||||||||||||||
| (l: unknown) => l === lease, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Populate the tmp dir so the per-target side-file sweep actually runs | ||||||||||||||||||||||||||||||||||||||||||||||||
| // once past the skip gate: a refactor that moves the sweep above the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // gate would reach for the holder's side files and trip the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // rmSync-not-called assertion below. | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.existsSync.mockReturnValue(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.readdirSync.mockReturnValue(['qwen-review-pr-123-diff.txt']); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| runCleanup('pr-123'); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // The skip must key on THIS target's lease: mockReturnValueOnce is | ||||||||||||||||||||||||||||||||||||||||||||||||
| // argument-blind, so an unwired read consults another PR's lease. | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.readReviewWorktreeLease).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.cwd(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.releaseWorktree).not.toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.execFileSync).not.toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.rmSync).not.toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R4-3 (round-4 ledger finding, re-asserted — still stands): this test's side-file safety assertion ( Suggested fix — arm the sweep in this test so the assertion discriminates: mocks.existsSync.mockReturnValue(true);
mocks.readdirSync.mockReturnValue(['qwen-review-pr-123-diff.txt']);中文说明[Suggestion] R4-3(第 4 轮账本发现,重新断言——仍然存在):该测试的附属文件安全断言( — qwen3.8-max via Qwen Code /review (v0.21.12) |
||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.ghApiAll).not.toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+210
to
+211
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The lease-skip test's side-file safety assertion is vacuous: Fix — make the sweep reachable in the skip test and assert it does not run: mocks.existsSync.mockReturnValue(true);
mocks.readdirSync.mockReturnValue(['qwen-review-pr-123-diff.txt']);
// keep, now meaningful:
expect(mocks.rmSync).not.toHaveBeenCalled();中文说明[Suggestion] lease 跳过测试中"附属文件安全"的断言是空的: 修复:在跳过测试中让清扫可达并断言其未运行(见上方代码),使部分跳过回归变红。 — qwen3.8-max via Qwen Code /review (v0.21.12) |
||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.clearReviewWorktreeLease).not.toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+211
to
+212
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The lease-skip test does not pin the gate-before-audit ordering: it runs with no fetch report (the default Fix — give the skip test a fetch report so a reordered gate reaches // in the skip test, as the audit tests do:
mocks.readFileSync.mockReturnValue(fetchReport);
// the existing assertion then turns red under the mutation:
expect(mocks.ghApiAll).not.toHaveBeenCalled();中文说明[Suggestion] lease 跳过测试没有钉住"门禁先于审计"的顺序:该测试未提供 fetch 报告( 修复:在跳过测试中提供 fetch 报告(与审计测试相同),使被降位的门禁真的触达 — qwen3.8-max via Qwen Code /review (v0.21.12) |
||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.writeStdoutLine).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect.stringContaining('skipped cleanup for "pr-123"'), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.writeStdoutLine).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect.stringContaining('session-a'), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+213
to
+218
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The skip-note test does not pin the lease path interpolation. The skip note interpolates Concrete cost: a future edit dropping or mangling the
Suggested change
中文说明[Suggestion] skip-note 测试没有钉住租约路径插值。skip note 通过 具体代价:未来删除或破坏 建议修复:补充对 — qwen3.8-max via Qwen Code /review (v0.21.12) |
||||||||||||||||||||||||||||||||||||||||||||||||
| // The note must name the lease file itself — the operator cannot act on | ||||||||||||||||||||||||||||||||||||||||||||||||
| // "delete the lease file" without knowing which file that is. | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.writeStdoutLine).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect.stringContaining('qwen-review-lease-pr-123.json'), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+219
to
+223
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The operator-facing skip note's Failure scenario: a refactor replacing Witness: changed only the skip note's root arg to
Suggested change
中文说明面向操作者的跳过提示中, — qwen3.8-max via Qwen Code /review (v0.21.12) |
||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it('proceeds when the lease belongs to this session', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const lease = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| sessionId: 'session-b', | ||||||||||||||||||||||||||||||||||||||||||||||||
| promptId: 'prompt-b', | ||||||||||||||||||||||||||||||||||||||||||||||||
| target: 'pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| repositoryRoot: '/repo', | ||||||||||||||||||||||||||||||||||||||||||||||||
| worktreePath: '/repo/.qwen/tmp/review-pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| branch: 'qwen-review/pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.readReviewWorktreeLease.mockReturnValueOnce(lease); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.reviewLeaseHeldByAnotherSession.mockReturnValueOnce(false); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.execFileSync.mockReturnValue(Buffer.from('')); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| runCleanup('pr-123'); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.releaseWorktree).toHaveBeenCalledTimes(3); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.clearReviewWorktreeLease).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.cwd(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it('re-checks the lease after the network-bound audit and skips if a session moved in during it (#9205)', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The gate above reads the lease BEFORE the audit, but the audit spawns | ||||||||||||||||||||||||||||||||||||||||||||||||
| // network-bound gh processes (seconds-scale). A review of the same PR that | ||||||||||||||||||||||||||||||||||||||||||||||||
| // starts inside that window — reading no lease, then writing its own — | ||||||||||||||||||||||||||||||||||||||||||||||||
| // must not be destroyed by this cleanup: re-read the lease after the audit, | ||||||||||||||||||||||||||||||||||||||||||||||||
| // before any destructive step, and take the same skip path. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const lease = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| sessionId: 'session-b', | ||||||||||||||||||||||||||||||||||||||||||||||||
| promptId: 'prompt-b', | ||||||||||||||||||||||||||||||||||||||||||||||||
| target: 'pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| repositoryRoot: '/repo', | ||||||||||||||||||||||||||||||||||||||||||||||||
| worktreePath: '/repo/.qwen/tmp/review-pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| branch: 'qwen-review/pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| // First read (the gate): no lease yet. Second read (post-audit): session B | ||||||||||||||||||||||||||||||||||||||||||||||||
| // has acquired one. | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.readReviewWorktreeLease | ||||||||||||||||||||||||||||||||||||||||||||||||
| .mockReturnValueOnce(null) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .mockReturnValueOnce(lease); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.reviewLeaseHeldByAnotherSession | ||||||||||||||||||||||||||||||||||||||||||||||||
| .mockReturnValueOnce(false) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .mockReturnValueOnce(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+267
to
+269
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This post-audit re-check test pins both lease READS' arguments (
Suggested change
Argument-sensitive once-mocks (production semantics: 中文说明[Suggestion] 这个审计后重查测试钉住了两次租约读取的参数( 建议修复:把两个一次性 mock 换成参数敏感的实现(生产语义: — qwen3.8-max via Qwen Code /review (v0.21.12) |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| runCleanup('pr-123'); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.readReviewWorktreeLease).toHaveBeenCalledTimes(2); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This re-check test proves a second lease read exists but not that it happens after the audit — in the mocked environment
Suggested change
中文说明这个重查测试只证明了存在第二次租约读取,没有证明它发生在审计之后——在 mock 环境中 — qwen3.8-max via Qwen Code /review (v0.21.12)
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This re-check test never pins the ARGUMENTS of either lease read;
Suggested change
中文说明这个重查测试从未钉住两次租约读取的参数; — qwen3.8-max via Qwen Code /review (v0.21.12) |
||||||||||||||||||||||||||||||||||||||||||||||||
| // Pin the ARGUMENTS of both reads: mockReturnValueOnce is argument-blind, | ||||||||||||||||||||||||||||||||||||||||||||||||
| // so a re-check that reads a malformed target stays green here while | ||||||||||||||||||||||||||||||||||||||||||||||||
| // failing open in production (validTarget rejects it -> null -> not held). | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.readReviewWorktreeLease).toHaveBeenNthCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| 1, | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.cwd(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.readReviewWorktreeLease).toHaveBeenNthCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| 2, | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.cwd(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'pr-123', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // And the second read must come AFTER the audit, not merely exist: | ||||||||||||||||||||||||||||||||||||||||||||||||
| // hoisting it above auditPrWrites keeps every other assertion green while | ||||||||||||||||||||||||||||||||||||||||||||||||
| // the seconds-long audit again runs after the last lease check (#9205). | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Here the audit no-ops on the missing fetch report and names that skip | ||||||||||||||||||||||||||||||||||||||||||||||||
| // on stderr — the note's position pins the audit inside the window. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const auditNoteIndex = mocks.writeStderrLine.mock.calls.findIndex((c) => | ||||||||||||||||||||||||||||||||||||||||||||||||
| String(c[0]).includes('bypass audit skipped'), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(auditNoteIndex).toBeGreaterThanOrEqual(0); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect( | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.readReviewWorktreeLease.mock.invocationCallOrder[1]!, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ).toBeGreaterThan( | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.writeStderrLine.mock.invocationCallOrder[auditNoteIndex]!, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Nothing of B's may be touched. | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.releaseWorktree).not.toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.execFileSync).not.toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.rmSync).not.toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.clearReviewWorktreeLease).not.toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.writeStdoutLine).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect.stringContaining('acquired the lease'), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it('releases the review worktree AND both disposable siblings', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // `base-tree` deliberately leaves its tree standing for the whole review | ||||||||||||||||||||||||||||||||||||||||||||||||
| // (a later verifier may need it, and a base that failed to build is kept as | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -170,6 +340,73 @@ describe('runCleanup', () => { | |||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it('never sweeps lease files, even for a target whose name collides with the lease prefix (#9205)', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // `safeTarget` flattens `lease` (and `./lease`) to `lease`, so a | ||||||||||||||||||||||||||||||||||||||||||||||||
| // file-review target with that name sweeps with a prefix that IS the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // lease prefix: unguarded, the rmSync below deletes every live PR lease | ||||||||||||||||||||||||||||||||||||||||||||||||
| // — including another session's — and defeats the lock this PR adds. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Lease removal belongs to `clearReviewWorktreeLease` alone. | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.execFileSync.mockReturnValue(Buffer.from('')); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.existsSync.mockReturnValue(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.readdirSync.mockReturnValue(['qwen-review-lease-pr-123.json']); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| runCleanup('lease'); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.rmSync).not.toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| join('/repo/.qwen/tmp', 'qwen-review-lease-pr-123.json'), | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect.anything(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect( | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.writeStdoutLine.mock.calls.map((c) => String(c[0])).join('\n'), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ).not.toContain('qwen-review-lease-pr-123.json'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it('sweeps the side files of a lease-named target that share the lease prefix', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The guard keys on the real lease shape, not the bare prefix: a | ||||||||||||||||||||||||||||||||||||||||||||||||
| // file-review target named `lease` flattens to exactly the lease prefix, | ||||||||||||||||||||||||||||||||||||||||||||||||
| // so keying on the prefix alone skips its OWN side files and nothing else | ||||||||||||||||||||||||||||||||||||||||||||||||
| // ever removes them (`clearReviewWorktreeLease` no-ops off `pr-\d+`) — | ||||||||||||||||||||||||||||||||||||||||||||||||
| // permanent residue. Only files shaped `…-pr-<n>.json` are real leases. | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.execFileSync.mockReturnValue(Buffer.from('')); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.existsSync.mockReturnValue(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.readdirSync.mockReturnValue([ | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'qwen-review-lease-diff.txt', | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'qwen-review-lease-pr-999.json', | ||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| runCleanup('lease'); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const sideFile = join('/repo/.qwen/tmp', 'qwen-review-lease-diff.txt'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.rmSync).toHaveBeenCalledWith(sideFile, { | ||||||||||||||||||||||||||||||||||||||||||||||||
| recursive: true, | ||||||||||||||||||||||||||||||||||||||||||||||||
| force: true, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // A live foreign lease survives the very same sweep. | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.rmSync).not.toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| join('/repo/.qwen/tmp', 'qwen-review-lease-pr-999.json'), | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect.anything(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it('still sweeps side files that match the target prefix', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The positive control for the lease guard: the skip keys on the lease | ||||||||||||||||||||||||||||||||||||||||||||||||
| // prefix, not on the sweep itself. | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.execFileSync.mockReturnValue(Buffer.from('')); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.existsSync.mockReturnValue(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mocks.readdirSync.mockReturnValue(['qwen-review-local-diff.txt']); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| runCleanup('local'); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const sideFile = join('/repo/.qwen/tmp', 'qwen-review-local-diff.txt'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.rmSync).toHaveBeenCalledWith(sideFile, { | ||||||||||||||||||||||||||||||||||||||||||||||||
| recursive: true, | ||||||||||||||||||||||||||||||||||||||||||||||||
| force: true, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mocks.writeStdoutLine).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| `Removed temp file: ${sideFile}`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it('keeps the record directory of a NON-CONVERGED reverse audit (#9206)', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The loop writes its stop marker inside the record directory when it | ||||||||||||||||||||||||||||||||||||||||||||||||
| // runs to the round cap (or the budget) without converging, and clears | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] This
beforeEachresets onlymocks.rmSync, but this PR's new tests also set persistent implementations onmocks.readdirSync(mockReturnValue([...])in the four sweep tests) and a throwing implementation onmocks.execFileSync; under the installed Vitest 3.2.4,vi.clearAllMocks()is mockClear-only — implementations survive — so they leak into every later test in this describe. Today the leak is masked becausebeforeEachre-pinsexistsSyncto false, short-circuiting the sweep beforereaddirSyncis consulted. — Concrete cost: probe — an appended test that setsexistsSynctrue without re-declaringreaddirSyncswept the phantom entry queued by a preceding test (rmSyncwas called withqwen-review-local-diff.txtit never declared) — future assertions pass or fail by test ORDER rather than the code under test. Baseline plus four shuffled-seed runs confirm no CURRENT test is affected (34/34 green in every order); the author's own comment names this hazard class and patches one of the three overridden mocks.Witness: probe — appended leak test FAILS as-is (
rmSynccalled 1× with the phantom entry); adding the twomockReset()lines below → 35/35 green.中文说明
[Suggestion] 这个
beforeEach只重置了mocks.rmSync,但本 PR 的新测试还给mocks.readdirSync设置了持久实现(四个清扫测试中的mockReturnValue([...])),并给mocks.execFileSync设置了抛错实现;在安装的 Vitest 3.2.4 下,vi.clearAllMocks()只做 mockClear——实现会保留——因此它们会泄漏进该 describe 中所有后续测试。目前泄漏被掩盖,是因为beforeEach把existsSync重新固定为 false,使清扫在查询readdirSync之前就被短路。 — 具体代价:探针——追加一个把existsSync设为 true 且不重新声明readdirSync的测试,会清扫到前一个测试排队的幽灵条目(rmSync被调用时带着它从未声明过的qwen-review-local-diff.txt)——未来的断言将按测试顺序而非被测代码决定成败。基线加四次随机种子乱序运行确认当前没有测试受影响(每种顺序下均 34/34 全绿);作者自己的注释已经点名了这一隐患类别,并修补了三个被覆盖 mock 中的一个。证据:探针——追加的泄漏测试现状下失败(
rmSync以幽灵条目被调用 1 次);加入下方两行mockReset()后 → 35/35 全绿。建议修复:在同一个
beforeEach中补上另外两个 mock 的mockReset(),见上方 suggestion。— qwen3.8-max via Qwen Code /review (v0.21.12)
There was a problem hiding this comment.
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, not dropped. This round ran in critical-only mode with the ~8-finding cap, and the batch went to the five Criticals (atomic lease acquire, guarded rollback, fail-closed identity, platform-safe assertion, win32-gated ENOTDIR test) plus the three cheapest coherent Suggestions. R5-3 is pure test-order hygiene, and the finding's own probe confirms no current test is affected (34/34 green across four shuffled-seed orders), so it deferred safely. The
beforeEachincleanup.test.tsalready resetsrmSyncfor exactly this hazard class; extending it toreaddirSync/execFileSyncremains a small, agreed change for the follow-up round this thread stays open for.中文说明
延后到下一轮,不会丢弃。本轮处于仅处理 Critical 的模式并受约 8 个发现的上限约束,批次给了五个 Critical(原子租约获取、有守卫的回滚、fail-closed 身份校验、平台安全断言、win32 门控的 ENOTDIR 测试)以及三个最廉价且与主线一致的 Suggestion。R5-3 是纯测试顺序卫生,且该发现自己的探针确认当前无测试受影响(四次乱序种子下均 34/34 全绿),因此可以安全延后。
cleanup.test.ts的beforeEach已为同一隐患类别重置了rmSync;把它扩展到readdirSync/execFileSync仍是本线程保持开放所对应的小幅后续改动。