Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3046b94
fix(acp): isolate workspace settings and context file resolution for …
Jul 30, 2026
79f2170
fix(acp): address review feedback — fix worktree test mock, add coverage
Jul 30, 2026
f8635ac
docs(acp): document single-worktree-session assumption on defaultSett…
Jul 30, 2026
f1ac5e2
test(acp): cover defaultSettingsCwd clear-on-close path
Jul 30, 2026
89df65e
fix(acp): address runtime verification feedback — per-session setting…
Aug 1, 2026
731126b
fix(acp): cover enter_worktree path + fix findEffectiveWorkspace dead…
Aug 1, 2026
76868ae
fix(acp): read activeWorktree from session config, fix GET/POST split…
Aug 1, 2026
7af1462
Merge branch 'main' into worktree-settings-isolation
wenshao Aug 1, 2026
c0f9b91
fix(acp): address PR review — stale worktree guards, containment, err…
Aug 1, 2026
0099742
Merge remote-tracking branch 'origin/worktree-settings-isolation' int…
Aug 1, 2026
55696ea
fix(acp): guard all resolveSettingsCwd returns with existsSync
Aug 1, 2026
ec57679
fix(acp): prefer worktree settings cwd over explicit client cwd, clea…
Aug 1, 2026
52a068c
fix(acp): address PR #8152 review — JSDoc priority, stale sessionId, …
Aug 1, 2026
959c362
fix(acp): address PR #8152 review — dead switch removal, test coverag…
Aug 2, 2026
a12c58b
fix(acp): address PR #8152 review — dead switch removal, test coverag…
Aug 8, 2026
18530dc
merge: sync with upstream/main
Aug 8, 2026
0c0512b
fix(acp): address PR #8152 R2 review — settings reload ordering, work…
Aug 9, 2026
7d9c586
Merge remote-tracking branch 'upstream/main' into worktree-settings-i…
Aug 10, 2026
b5a8b2a
fix(acp): restore worktree cwd on ACP session resume
Aug 10, 2026
078e788
Merge remote-tracking branch 'upstream/main' into worktree-settings-i…
Aug 11, 2026
2b2bec8
chore: merge origin/main into worktree-settings-isolation
qwen-code-dev-bot Aug 12, 2026
18e2a98
fix(acp): resolve worktree settings isolation review comments
Aug 12, 2026
10dad43
Merge remote-tracking branch 'origin/worktree-settings-isolation' int…
Aug 12, 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
283 changes: 281 additions & 2 deletions packages/cli/src/acp-integration/acpAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ import { ACP_EVENT_LOOP_STALL_RESTART_MS } from '@qwen-code/channel-base';
const { mockRunExitCleanup } = vi.hoisted(() => ({
mockRunExitCleanup: vi.fn().mockResolvedValue(undefined),
}));
const { mockExistsSync } = vi.hoisted(() => ({
mockExistsSync: vi.fn().mockReturnValue(true),
}));
Comment on lines +27 to +29

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] The existsSync stub is installed file-wide (via vi.mock('node:fs')) and defaults to true for the entire 17,507-line test file, with no per-test reset. Two consequences: (1) every existsSync call in every unrelated test now reports every path as existing, so a regression that breaks missing-file handling would pass silently here; (2) the two worktree tests that override the mock inline (~9236, ~9268) restore it only as their last statement — not in try/finally or afterEach — so if an assertion throws mid-block, the override leaks into every later test in the file (vitest runs in file order), producing a cascade of misleading failures or masking a real regression. The diff already uses try/finally for cleanup in the session/cd test (~4068), so the safer pattern is established. Suggested direction:

// default to real behavior; stub true/false only within the tests that need it
const { mockExistsSync } = vi.hoisted(() => ({
  mockExistsSync: vi.fn().mockImplementation((...a) => actualExistsSync(...a)),
}));
// plus afterEach(() => mockExistsSync.mockReset()), or try/finally around inline overrides
中文说明

[Suggestion] existsSync 桩通过 vi.mock('node:fs') 被安装为全文件生效,并在整个 17,507 行的测试文件中默认返回 true,且没有按测试重置。两个后果:(1) 每个无关测试中的 existsSync 调用现在都报告所有路径都存在,因此破坏"文件缺失处理"的回归在此处会静默通过;(2) 两个在内联覆盖该 mock 的 worktree 测试(约 9236、约 9268)只在其最后一条语句恢复它——而非在 try/finallyafterEach 中——所以若某个断言在块中途抛出,该覆盖会泄漏到文件中所有后续测试(vitest 按文件顺序运行),产生一连串误导性失败或掩盖真实回归。本 diff 在 session/cd 测试(约 4068)中已使用 try/finally 做清理,因此更安全的模式已经确立。修复方向:默认调用真实实现,仅在需要的测试内 stub true/false,并配合 afterEach 重置或 try/finally

— qwen3.8-max-preview via Qwen Code /review

vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>();
return { ...actual, existsSync: mockExistsSync };
});
const { mockStartNonInteractiveOpenAILogHousekeeping } = vi.hoisted(() => ({
mockStartNonInteractiveOpenAILogHousekeeping: vi.fn(),
}));
Expand Down Expand Up @@ -1994,6 +2001,10 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
stdoutDestroySpy.mockRestore();
});

afterEach(() => {
mockExistsSync.mockReturnValue(true);
});

it('initialize response includes mcpCapabilities with sse and http', async () => {
const mockSettings = {
merged: { mcpServers: {} },
Expand Down Expand Up @@ -4378,6 +4389,63 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
await agentPromise;
});

it('session/cd sets worktreeCwd and getCore resolves against the worktree path', async () => {

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] R3-14: The new tests cannot establish real worktree state with plain mkdtemp/os.tmpdir() fixtures — production resolution only sets worktreeCwd for paths under <gitRoot>/.qwen/worktrees/ (via isWorktreePath) or via getActiveWorktree()/sidecar restore. Probe-verified at this commit: after adding the missing findGitRoot mock export (R3-1), 4 tests still fail on fixture shape — this one, resolve against worktree cwd set by createAndStoreSession (both phases), fall back to process.cwd() after worktree session closes (pre-close phase), and resolves per-session worktreeCwd via sessionId param — and the 2 worktreeCwd dir is deleted tests pass vacuously, so the existsSync guards (acpAgent.ts:7902/7913) get zero effective coverage. Still stands.

Concrete cost: the PR's only test evidence for the #8138 fix can never exercise the isolation logic; once R3-1 is fixed these tests either stay red or get "fixed" in a way that bypasses the containment logic — leaving the mis-routing path without real coverage.

Fix: build worktree-shaped fixtures — <repo>/.qwen/worktrees/<slug> with findGitRoot mocked to return <repo> (mirroring acpAgent.worktree.test.ts) — or inject worktreeCwd/getActiveWorktree directly into the session mock.

中文说明

[Suggestion] R3-14:新测试无法用普通 mkdtemp/os.tmpdir() fixture 建立真实 worktree 状态——生产解析只会为 <gitRoot>/.qwen/worktrees/ 之下的路径(经 isWorktreePath)或经 getActiveWorktree()/sidecar 恢复设置 worktreeCwd。已在本提交探针验证:补上缺失的 findGitRoot mock 导出(R3-1)后,仍有 4 个测试因 fixture 形状失败——本测试、resolve against worktree cwd set by createAndStoreSession(两个阶段)、fall back to process.cwd() after worktree session closes(关闭前阶段)、resolves per-session worktreeCwd via sessionId param——且 2 个 worktreeCwd dir is deleted 测试空转通过,existsSync 守卫(acpAgent.ts:7902/7913)零有效覆盖。仍然成立。

具体代价:本 PR 对 #8138 修复的唯一测试证据永远无法执行隔离逻辑;R3-1 修复后这些测试要么继续红,要么以绕过包含逻辑的方式被"修好"——错位路径失去真实覆盖。

修复:构造 worktree 形状的 fixture——<repo>/.qwen/worktrees/<slug>findGitRoot mock 返回 <repo>(比照 acpAgent.worktree.test.ts)——或向会话 mock 直接注入 worktreeCwd/getActiveWorktree

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

const sessionId = '11111111-1111-1111-1111-111111111111';
const tmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'qwen-cd-worktree-settings-'),
);
const repoRoot = path.join(tmpDir, 'repo');
const targetDir = path.join(repoRoot, '.qwen', 'worktrees', 'test');
await fs.mkdir(targetDir, { recursive: true });
await fs.mkdir(path.join(repoRoot, '.git'));
await fs.writeFile(path.join(targetDir, '.git'), 'gitdir: /fake');
const canonicalTargetDir = await fs.realpath(targetDir);
const innerConfig = await setupSessionMocks(sessionId);
const relocateWorkingDirectory = vi.fn().mockResolvedValue({});
Object.assign(innerConfig, {
getTargetDir: vi.fn().mockReturnValue('/tmp'),
isRestrictiveSandbox: vi.fn().mockReturnValue(false),
relocateWorkingDirectory,
});
Object.assign(innerConfig.getGeminiClient(), {
addWorkingDirectoryChangedContext: vi.fn().mockResolvedValue(undefined),
});

const settings = makeCoreSettings();
vi.mocked(loadSettings).mockReturnValue(settings);
vi.mocked(loadCliConfig).mockResolvedValue(
innerConfig as unknown as Config,
);

const { agent, agentPromise } = await bootAcpAgent();
await agent.newSession({ cwd: '/tmp', mcpServers: [] });

try {
await expect(
agent.extMethod(SERVE_CONTROL_EXT_METHODS.sessionCd, {
sessionId,
path: targetDir,
}),
).resolves.toMatchObject({
previousCwd: '/tmp',
newCwd: canonicalTargetDir,
});

expect(
(lastSessionMock as Record<string, unknown>)?.['worktreeCwd'],
).toBe(canonicalTargetDir);
Comment on lines +4434 to +4436

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] The new tests cannot establish real worktree state with plain mkdtemp/os.tmpdir() fixtures — production resolution can never classify those targets as worktrees. Probe-verified: after the missing-findGitRoot mock export is fixed, 4 tests still fail on fixture shape — this assertion (session/cd sets worktreeCwd…), resolve against worktree cwd set by createAndStoreSession both phases (~9918/9941), the pre-close phase of fall back to process.cwd() after worktree session closes (~9966), and resolves per-session worktreeCwd via sessionId param (~10003): all assert loadSettings(os.tmpdir()) / worktreeCwd = <tmpdir>, which resolveSettingsCwd can never return. Additionally 2 tests pass vacuously: the worktreeCwd dir is deleted tests (~9930-9961, ~9964) can never set worktreeCwd either, so the existsSync guards at acpAgent.ts:7871/7882 have zero effective coverage (probes also showed the mockExistsSync knob never reaches acpAgent.ts — it calls the real fs.existsSync). — Concrete cost: once the author patches the mock export, these tests either stay red or get "fixed" in a way that bypasses the containment logic entirely — leaving the #8138 mis-routing path without real coverage.

Suggested change
expect(
(lastSessionMock as Record<string, unknown>)?.['worktreeCwd'],
).toBe(canonicalTargetDir);
expect(
(lastSessionMock as Record<string, unknown>)?.['worktreeCwd'],
).toBe(canonicalTargetDir);

(build worktree-shaped fixtures — <repo>/.qwen/worktrees/<slug> with findGitRoot mocked to return <repo>, mirroring acpAgent.worktree.test.ts — or inject worktreeCwd/getActiveWorktree directly into the session mock)

中文说明

[Suggestion] 新测试无法用普通 mkdtemp/os.tmpdir() fixture 建立真实 worktree 状态——生产解析永远不会把这些目标识别为 worktree。已探针验证:修好缺失的 findGitRoot mock 导出后,仍有 4 个测试因 fixture 形状失败——本断言(session/cd sets worktreeCwd…)、resolve against worktree cwd set by createAndStoreSession 两个阶段(约 9918/9941)、fall back to process.cwd() after worktree session closes 的关闭前阶段(约 9966)、resolves per-session worktreeCwd via sessionId param(约 10003):都断言 loadSettings(os.tmpdir()) / worktreeCwd = <tmpdir>,而 resolveSettingsCwd 永远不可能返回它。另外 2 个测试空转通过worktreeCwd dir is deleted 测试(约 9930-9961、9964)同样无法设置 worktreeCwd,于是 acpAgent.ts:7871/7882 的 existsSync 守卫零有效覆盖(探针还表明 mockExistsSync 旋钮根本到不了 acpAgent.ts——它调用的是真实 fs.existsSync)。— 具体代价:作者修好 mock 导出后,这些测试要么继续红,要么以完全绕过包含逻辑的方式被"修好"——使 #8138 的错位路径失去真实覆盖。

修复:构造 worktree 形状的 fixture(<repo>/.qwen/worktrees/<slug>findGitRoot mock 返回 <repo>,比照 acpAgent.worktree.test.ts),或向会话 mock 直接注入 worktreeCwd/getActiveWorktree

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


vi.mocked(loadSettings).mockClear();
await agent.extMethod('qwen/settings/getCore', {});
expect(vi.mocked(loadSettings)).toHaveBeenCalledWith(canonicalTargetDir);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}

mockConnectionState.resolve();
await agentPromise;
});

it('allows a private Live managed relocation without a global folder trust rule', async () => {
await withEmptyTrustedFolders(async (directory) => {
const root = path.join(directory, 'Conversations');
Expand Down Expand Up @@ -9899,6 +9967,197 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
await agentPromise;
});

it('qwen/settings handlers resolve against worktree cwd set by createAndStoreSession', async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'qwen-wt-s1-'));
const repoRoot = path.join(tmpDir, 'repo');
const WORKTREE_DIR = path.join(repoRoot, '.qwen', 'worktrees', 'test');
await fs.mkdir(WORKTREE_DIR, { recursive: true });
await fs.mkdir(path.join(repoRoot, '.git'));
await fs.writeFile(path.join(WORKTREE_DIR, '.git'), 'gitdir: /fake');
const innerConfig = await setupSessionMocks('wt-settings-session');
Comment on lines +9970 to +9977

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] No test replays the issue's actual end-to-end shape: enter_worktree tool called mid-session → workspace-scoped setting change → setting lands in worktree. The two halves are tested independently (enter-worktree.test.ts asserts setActiveWorktree is called; acpAgent.test.ts mocks getActiveWorktree to return a preset path) but the Config identity wiring is never exercised. — Failure scenario: a refactor that breaks Config identity (tools receiving a cloned Config, or Session.getConfig() returning a wrapper) would pass both test halves independently while silently re-breaking issue #8138.

中文说明

[Suggestion] 没有测试复现 issue 的实际端到端形态:会话中调用 enter_worktree 工具 → 更改工作区范围设置 → 设置写入 worktree。两半被独立测试(enter-worktree.test.ts 断言 setActiveWorktree 被调用;acpAgent.test.ts mock getActiveWorktree 返回预设路径),但 Config 身份接线从未被验证。— 失败场景:一个破坏 Config 身份的重构(工具收到克隆的 Config,或 Session.getConfig() 返回包装器)会让两半测试独立通过,同时静默地重新破坏 issue #8138

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

innerConfig.getTargetDir = vi.fn().mockReturnValue(WORKTREE_DIR);

const settings = makeCoreSettings();
vi.mocked(loadSettings).mockReturnValue(settings);
vi.mocked(loadCliConfig).mockResolvedValue(
innerConfig as unknown as Config,
);
const { agent, agentPromise } = await bootAcpAgent();

await agent.newSession({ cwd: '/fake/project', mcpServers: [] });

vi.mocked(loadSettings).mockClear();
await agent.extMethod('qwen/settings/getCore', {});
expect(vi.mocked(loadSettings)).toHaveBeenCalledWith(WORKTREE_DIR);

innerConfig.getTargetDir = vi.fn().mockReturnValue(process.cwd());
innerConfig.getSessionId = vi.fn().mockReturnValue('regular-session');
vi.mocked(loadCliConfig).mockResolvedValue(
innerConfig as unknown as Config,
);
await agent.newSession({ cwd: '/fake/project', mcpServers: [] });

// The fallback loop scans all sessions for worktreeCwd; the first
// session's worktreeCwd is still set, so it wins over process.cwd().
vi.mocked(loadSettings).mockClear();
await agent.extMethod('qwen/settings/getCore', {});
expect(vi.mocked(loadSettings)).toHaveBeenCalledWith(WORKTREE_DIR);

mockConnectionState.resolve();
await agentPromise;
await fs.rm(tmpDir, { recursive: true, force: true });
});

it('qwen/settings handlers fall back to process.cwd() after worktree session closes', async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'qwen-wt-s2-'));
const repoRoot = path.join(tmpDir, 'repo');
const WORKTREE_DIR = path.join(repoRoot, '.qwen', 'worktrees', 'test');
await fs.mkdir(WORKTREE_DIR, { recursive: true });
await fs.mkdir(path.join(repoRoot, '.git'));
await fs.writeFile(path.join(WORKTREE_DIR, '.git'), 'gitdir: /fake');
const innerConfig = await setupSessionMocks('wt-close-session');
innerConfig.getTargetDir = vi.fn().mockReturnValue(WORKTREE_DIR);

const settings = makeCoreSettings();
vi.mocked(loadSettings).mockReturnValue(settings);
vi.mocked(loadCliConfig).mockResolvedValue(
innerConfig as unknown as Config,
);
const { agent, agentPromise } = await bootAcpAgent();

await agent.newSession({ cwd: '/fake/project', mcpServers: [] });

vi.mocked(loadSettings).mockClear();
await agent.extMethod('qwen/settings/getCore', {});
expect(vi.mocked(loadSettings)).toHaveBeenCalledWith(WORKTREE_DIR);

await agent.extMethod('qwen/control/session/close', {
sessionId: 'wt-close-session',
});

vi.mocked(loadSettings).mockClear();
await agent.extMethod('qwen/settings/getCore', {});
expect(vi.mocked(loadSettings)).toHaveBeenCalledWith(process.cwd());

mockConnectionState.resolve();
await agentPromise;
await fs.rm(tmpDir, { recursive: true, force: true });
});

it('qwen/settings/getCore resolves per-session worktreeCwd via sessionId param', async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'qwen-wt-s3-'));
const repoRoot = path.join(tmpDir, 'repo');
const WORKTREE_DIR = path.join(repoRoot, '.qwen', 'worktrees', 'test');
await fs.mkdir(WORKTREE_DIR, { recursive: true });
await fs.mkdir(path.join(repoRoot, '.git'));
await fs.writeFile(path.join(WORKTREE_DIR, '.git'), 'gitdir: /fake');
const innerConfig = await setupSessionMocks('per-session-wt-id');
innerConfig.getTargetDir = vi.fn().mockReturnValue(WORKTREE_DIR);

const settings = makeCoreSettings();
vi.mocked(loadSettings).mockReturnValue(settings);
vi.mocked(loadCliConfig).mockResolvedValue(
innerConfig as unknown as Config,
);
const { agent, agentPromise } = await bootAcpAgent();

const newResult = (await agent.newSession({
cwd: '/fake/project',
mcpServers: [],
})) as { sessionId: string };
const sessionId = newResult.sessionId;

vi.mocked(loadSettings).mockClear();
await agent.extMethod('qwen/settings/getCore', { sessionId });
expect(vi.mocked(loadSettings)).toHaveBeenCalledWith(WORKTREE_DIR);

mockConnectionState.resolve();
await agentPromise;
await fs.rm(tmpDir, { recursive: true, force: true });
});

it('qwen/settings/getCore falls through to configWt when worktreeCwd dir is deleted', async () => {

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 is named "falls through to configWt" but never exercises the configWt branch: getActiveWorktree is mocked to return null (its only reference in this file), so the assertion (process.cwd()) proves only the ?? process.cwd() fallthrough at acpAgent.ts:7347 — not a non-null configWt. — Concrete cost: the mutation return configWt ?? process.cwd()return process.cwd() keeps every test green; the non-null configWt path (an enter_worktree-set active worktree whose session worktreeCwd directory was subsequently deleted) is dead code from the suite's perspective. This is the same code path as the Critical above, so the coverage gap is load-bearing. Add a case with a non-null active worktree, e.g.:

it('qwen/settings/getCore uses configWt when worktreeCwd is deleted but activeWorktree is set', async () => {
  const ACTIVE_WT = '/fake/active-wt';
  const innerConfig = await setupSessionMocks('deleted-wt-session');
  innerConfig.getTargetDir = vi.fn().mockReturnValue('/fake/deleted-wt');
  (innerConfig as Record<string, unknown>)['getActiveWorktree'] = vi
    .fn()
    .mockReturnValue(ACTIVE_WT);
  mockExistsSync.mockReturnValue(false); // session.worktreeCwd dir is gone
  // ... call extMethod('qwen/settings/getCore', { sessionId }) and assert
  // loadSettings was called with ACTIVE_WT (not process.cwd())
});
中文说明

[Suggestion] 此测试名为 “falls through to configWt”,却从未真正执行 configWt 分支:getActiveWorktree 被 mock 为返回 null(这是它在整个文件中的唯一引用),因此断言(process.cwd())只证明了 acpAgent.ts:7347 处的 ?? process.cwd() 兜底,而非非 null 的 configWt。— 具体代价:将 return configWt ?? process.cwd() 变异为 return process.cwd(),所有测试仍为绿色;非 null 的 configWt 路径(由 enter_worktree 设置的活跃 worktree,其会话的 worktreeCwd 目录随后被删除)从测试套件角度看是死代码。这与上面那个 Critical 是同一条代码路径,因此该覆盖缺口是承重的。补充一个活跃 worktree 非 null 的用例(见上)。

— qwen3.8-max-preview via Qwen Code /review

const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'qwen-wt-s4-'));
const repoRoot = path.join(tmpDir, 'repo');
const WORKTREE_DIR = path.join(repoRoot, '.qwen', 'worktrees', 'deleted');
await fs.mkdir(WORKTREE_DIR, { recursive: true });
await fs.mkdir(path.join(repoRoot, '.git'));
await fs.writeFile(path.join(WORKTREE_DIR, '.git'), 'gitdir: /fake');
const innerConfig = await setupSessionMocks('deleted-wt-session');
innerConfig.getTargetDir = vi.fn().mockReturnValue(WORKTREE_DIR);
(innerConfig as Record<string, unknown>)['getActiveWorktree'] = vi
.fn()
.mockReturnValue(null);

const settings = makeCoreSettings();
vi.mocked(loadSettings).mockReturnValue(settings);
vi.mocked(loadCliConfig).mockResolvedValue(
innerConfig as unknown as Config,
);
const { agent, agentPromise } = await bootAcpAgent();

const newResult = (await agent.newSession({
cwd: '/fake/project',
mcpServers: [],
})) as { sessionId: string };
const sessionId = newResult.sessionId;

await fs.rm(WORKTREE_DIR, { recursive: true, force: true });
try {
vi.mocked(loadSettings).mockClear();
await agent.extMethod('qwen/settings/getCore', { sessionId });
expect(vi.mocked(loadSettings)).toHaveBeenCalledWith(process.cwd());
} finally {
mockExistsSync.mockReturnValue(true);
}

mockConnectionState.resolve();
await agentPromise;
await fs.rm(tmpDir, { recursive: true, force: true });
});

it('qwen/settings/getCore uses configWt when worktreeCwd is deleted but activeWorktree exists', async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'qwen-wt-s5-'));
const repoRoot = path.join(tmpDir, 'repo');
const ACTIVE_WT = path.join(repoRoot, '.qwen', 'worktrees', 'active');
await fs.mkdir(ACTIVE_WT, { recursive: true });
await fs.mkdir(path.join(repoRoot, '.git'));
await fs.writeFile(path.join(ACTIVE_WT, '.git'), 'gitdir: /fake');
const innerConfig = await setupSessionMocks('active-wt-session');
innerConfig.getTargetDir = vi.fn().mockReturnValue('/fake/deleted-wt');
(innerConfig as Record<string, unknown>)['getActiveWorktree'] = vi
.fn()
.mockReturnValue(ACTIVE_WT);

const settings = makeCoreSettings();
vi.mocked(loadSettings).mockReturnValue(settings);
vi.mocked(loadCliConfig).mockResolvedValue(
innerConfig as unknown as Config,
);
const { agent, agentPromise } = await bootAcpAgent();

const newResult = (await agent.newSession({
cwd: '/fake/project',
mcpServers: [],
})) as { sessionId: string };
const sessionId = newResult.sessionId;

mockExistsSync.mockImplementation(
(p: string | Buffer | URL) => p === ACTIVE_WT,
);
try {
vi.mocked(loadSettings).mockClear();
await agent.extMethod('qwen/settings/getCore', { sessionId });
expect(vi.mocked(loadSettings)).toHaveBeenCalledWith(ACTIVE_WT);
} finally {
mockExistsSync.mockReturnValue(true);
}

mockConnectionState.resolve();
await agentPromise;
await fs.rm(tmpDir, { recursive: true, force: true });
});

it('qwen/permissions/setRules validates scope and ruleType', async () => {
const settings = makeCoreSettings();
const { agent, agentPromise } = await bootCoreSettingsAgent(settings);
Expand Down Expand Up @@ -17948,12 +18207,22 @@ describe('sessionLanguage multi-session propagation', () => {
getUserHooks: vi.fn().mockReturnValue({}),
getProjectHooks: vi.fn().mockReturnValue({}),
} as unknown as LoadedSettings;
const reloadedSettings = {
merged: { modelProviders: providerConfig },
getUserHooks: vi.fn().mockReturnValue({}),
getProjectHooks: vi.fn().mockReturnValue({}),
} as unknown as LoadedSettings;
const cfg = makeConfig({
getSessionId: vi.fn().mockReturnValue('s-reload'),
getAuthType: vi.fn().mockReturnValue('openai'),
});

vi.mocked(loadSettings).mockReturnValue(settings);
vi.mocked(loadSettings).mockImplementation((...args) => {
if (args[1] && (args[1] as Record<string, unknown>).skipLoadEnvironment) {

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.

[Critical] R5-1: PR-added test code accesses an index-signature property with dot notation, violating the repo's pre-existing noPropertyAccessFromIndexSignature: true. npm run build --workspace=packages/cli fails with TS4111 here and at line 18302 — the build gate is red as-is. — Failure scenario: any full build (npm run build, npm run preflight, CI) compiles packages/cli with tsc --build and emits error TS4111: Property 'skipLoadEnvironment' comes from an index signature, so it must be accessed with ['skipLoadEnvironment'] twice → build exits non-zero. Reproduced twice at HEAD; these are the only two type errors in the repo.

Fix (both lines 18221 and 18302):

if (args[1] && (args[1] as Record<string, unknown>)['skipLoadEnvironment']) {
中文说明

[Critical] R5-1:本 PR 新增的测试代码用点号访问索引签名属性,违反仓库既有的 noPropertyAccessFromIndexSignature: truenpm run build --workspace=packages/cli 在此处与第 18302 行报 TS4111 失败——构建门禁当前为红。— 失败场景:任何完整构建(npm run buildnpm run preflight、CI)用 tsc --build 编译 packages/cli 时会两次报出 error TS4111: Property 'skipLoadEnvironment' comes from an index signature, so it must be accessed with ['skipLoadEnvironment'] → 构建非零退出。已在 HEAD 复现两次;这是仓库中仅有的两个类型错误。

修复(第 18221 与 18302 两处):

if (args[1] && (args[1] as Record<string, unknown>)['skipLoadEnvironment']) {

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

return reloadedSettings;
}
return settings;
});
vi.mocked(loadCliConfig).mockResolvedValue(cfg as unknown as Config);
vi.mocked(Session).mockImplementation(
() =>
Expand Down Expand Up @@ -18012,6 +18281,11 @@ describe('sessionLanguage multi-session propagation', () => {
getUserHooks: vi.fn().mockReturnValue({}),
getProjectHooks: vi.fn().mockReturnValue({}),
} as unknown as LoadedSettings;
const reloadedSettings = {
merged: { tools: { approvalMode: 'plan' } },
getUserHooks: vi.fn().mockReturnValue({}),
getProjectHooks: vi.fn().mockReturnValue({}),
} as unknown as LoadedSettings;
let approvalMode = 'default';
const cfg = makeConfig({
getSessionId: vi.fn().mockReturnValue('s-plan-reload'),
Expand All @@ -18024,7 +18298,12 @@ describe('sessionLanguage multi-session propagation', () => {
const clearActiveTodoPlanRevision = vi.fn();
const clearTodoStopGuardTrust = vi.fn();

vi.mocked(loadSettings).mockReturnValue(settings);
vi.mocked(loadSettings).mockImplementation((...args) => {
if (args[1] && (args[1] as Record<string, unknown>).skipLoadEnvironment) {
return reloadedSettings;
}
return settings;
});
vi.mocked(loadCliConfig).mockResolvedValue(cfg as unknown as Config);
vi.mocked(Session).mockImplementation(
() =>
Expand Down
Loading
Loading