From ca3608c065569d32465b0a8c52fe85b43bd071ed Mon Sep 17 00:00:00 2001
From: phantom5099 <1011668688@qq.com>
Date: Sun, 4 Oct 2026 01:36:15 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E7=90=86=E6=B5=8B=E8=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
删除不验证行为的测试(提示词文案断言、类型形状、存在性/导出、常量钉死、测试内重实现、源码文本扫描等),并在 AGENTS.md / CLAUDE.md 补充测试规则;新增 desktop/test/diff-and-store.test.ts 承接原性能测试中有效的行为用例。
---
AGENTS.md | 29 +
CLAUDE.md | 29 +
.../test/agent/agent-cache-stability.test.ts | 64 ---
.../test/agent/agent-profile-filter.test.ts | 13 -
.../test/agent/build-system-prompt.test.ts | 67 ---
.../test/agent/memory-snapshot.test.ts | 27 +-
.../test/agent/message-tool-calls.test.ts | 35 --
.../test/agent/stop-decision-type.test.ts | 54 --
.../test/checkpoint/checkpoint-diff.test.ts | 35 --
.../test/checkpoint/checkpoint-undo.test.ts | 8 -
.../test/context/compressor/l5-prompt.test.ts | 35 --
.../codingcode/test/context/organizer.test.ts | 60 ---
packages/codingcode/test/core/path.test.ts | 13 +-
.../test/plan/is-plan-profile.test.ts | 6 +-
packages/codingcode/test/plan/policy.test.ts | 6 -
.../test/prompts/system-prompt.test.ts | 132 -----
.../test/session/facade-surface.test.ts | 30 --
.../test/session/types-export.test.ts | 26 -
.../test/subagent/builtin-profiles.test.ts | 20 -
.../test/types/type-collapse.test.ts | 30 +-
packages/desktop/test/diff-and-store.test.ts | 234 ++++++++
packages/desktop/test/global.store.test.ts | 180 -------
.../test/performance-optimization.test.ts | 508 ------------------
23 files changed, 304 insertions(+), 1337 deletions(-)
delete mode 100644 packages/codingcode/test/agent/agent-cache-stability.test.ts
delete mode 100644 packages/codingcode/test/agent/agent-profile-filter.test.ts
delete mode 100644 packages/codingcode/test/agent/build-system-prompt.test.ts
delete mode 100644 packages/codingcode/test/agent/message-tool-calls.test.ts
delete mode 100644 packages/codingcode/test/agent/stop-decision-type.test.ts
delete mode 100644 packages/codingcode/test/context/compressor/l5-prompt.test.ts
delete mode 100644 packages/codingcode/test/context/organizer.test.ts
delete mode 100644 packages/codingcode/test/prompts/system-prompt.test.ts
delete mode 100644 packages/codingcode/test/session/types-export.test.ts
delete mode 100644 packages/codingcode/test/subagent/builtin-profiles.test.ts
create mode 100644 packages/desktop/test/diff-and-store.test.ts
delete mode 100644 packages/desktop/test/global.store.test.ts
delete mode 100644 packages/desktop/test/performance-optimization.test.ts
diff --git a/AGENTS.md b/AGENTS.md
index 2df137bd..0271de79 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -80,6 +80,35 @@ Coding Code 是 AI 编程助手。
- 修改过程中发现错误,如果是本次范围就修改(包括测试),否则要在最后指出
- 仅允许使用简短注释
+## 测试规则
+
+**唯一判据**:改生产代码的逻辑,测试必须失败;改不影响行为的文案、命名、注释、格式,测试不应失败。不满足此判据的用例一律不写。
+
+**该怎么写**:
+
+- 只断言可观察的行为:给定输入 → 返回值、副作用、状态变化、抛出的错误。断言对象是生产代码的结果,不是测试里自造的数据。
+- 采用 Arrange-Act-Assert:一段准备、一段触发、一段断言,一个用例只验证一个概念。
+- 用例名描述行为与预期,不描述实现细节。
+- 覆盖边界与失败路径(边界值、空集合、错误分支),这里才是缺陷高发区。
+- 按层级选择:纯函数用单元测试;跨模块链路用集成测试(mock 掉外部依赖如 LLM),验证真实装配后的端到端行为。
+
+**禁止写的类型**:
+
+1. 文案 / 提示词断言——断言提示词、系统说明、注释、UI 文案包含或不再包含某个字符串。提示词是产品内容,其变更由人工评审守护,不写成测试。
+2. 类型形状断言——运行时断言某字面量有哪些字段、字段是什么类型。类型正确性由 `tsc` 保证,这类断言恒真且零信息量;确需钉住类型时用编译期的 `@ts-expect-error` / `expectTypeOf`,不要写成运行时 `expect`。
+3. 存在性 / 导出断言——断言文件存在、类 / 函数 / layer 已导出、方法存在。真断裂时编译与上层用例会同时失败,此类用例不提供额外信息。
+4. 常量钉死——断言常量等于它自身的字面量。需要守护的是行为,不是常量的副本。
+5. 测试内重新实现逻辑——在测试里重写一遍待测算法再断言自己写的副本,只证明写法一致,发现不了实现缺陷。
+6. 源码文本 / 结构扫描——用字符串或正则解析源码断言导入、布局或写法。这属于静态分析:类型与导入可解析性由 `tsc` 保证,无需重复;架构分层等 `tsc` 无法表达的约束,改用 ESLint 规则或 AST 解析,而不是正则匹配源码文本(现有 `test/architecture/boundaries.test.ts`、`test/hooks/points-coverage.test.ts` 应按此方向迁移)。
+7. 占位 / 恒真断言——`expect(true).toBe(true)`、只 `toBeDefined()` 不校验值、以及无论如何都会通过的断言。
+8. 重复用例——与已有用例覆盖同一行为的副本;发现重复应合并,不要并存。
+
+**其他**:
+
+- 测试必须独立、确定、可任意顺序执行,不依赖执行顺序或共享可变状态(每个用例自备数据并自行清理)。
+- 不为「将来可能的变化」预留用例,只覆盖当前真实存在的行为。
+- 新增 / 删除测试前先取得用户许可(见「开发规则」)。
+
## 其他规则
- 用户要求回答问题时,必须清晰回答每一点问题,不得遗漏
diff --git a/CLAUDE.md b/CLAUDE.md
index 2df137bd..0271de79 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -80,6 +80,35 @@ Coding Code 是 AI 编程助手。
- 修改过程中发现错误,如果是本次范围就修改(包括测试),否则要在最后指出
- 仅允许使用简短注释
+## 测试规则
+
+**唯一判据**:改生产代码的逻辑,测试必须失败;改不影响行为的文案、命名、注释、格式,测试不应失败。不满足此判据的用例一律不写。
+
+**该怎么写**:
+
+- 只断言可观察的行为:给定输入 → 返回值、副作用、状态变化、抛出的错误。断言对象是生产代码的结果,不是测试里自造的数据。
+- 采用 Arrange-Act-Assert:一段准备、一段触发、一段断言,一个用例只验证一个概念。
+- 用例名描述行为与预期,不描述实现细节。
+- 覆盖边界与失败路径(边界值、空集合、错误分支),这里才是缺陷高发区。
+- 按层级选择:纯函数用单元测试;跨模块链路用集成测试(mock 掉外部依赖如 LLM),验证真实装配后的端到端行为。
+
+**禁止写的类型**:
+
+1. 文案 / 提示词断言——断言提示词、系统说明、注释、UI 文案包含或不再包含某个字符串。提示词是产品内容,其变更由人工评审守护,不写成测试。
+2. 类型形状断言——运行时断言某字面量有哪些字段、字段是什么类型。类型正确性由 `tsc` 保证,这类断言恒真且零信息量;确需钉住类型时用编译期的 `@ts-expect-error` / `expectTypeOf`,不要写成运行时 `expect`。
+3. 存在性 / 导出断言——断言文件存在、类 / 函数 / layer 已导出、方法存在。真断裂时编译与上层用例会同时失败,此类用例不提供额外信息。
+4. 常量钉死——断言常量等于它自身的字面量。需要守护的是行为,不是常量的副本。
+5. 测试内重新实现逻辑——在测试里重写一遍待测算法再断言自己写的副本,只证明写法一致,发现不了实现缺陷。
+6. 源码文本 / 结构扫描——用字符串或正则解析源码断言导入、布局或写法。这属于静态分析:类型与导入可解析性由 `tsc` 保证,无需重复;架构分层等 `tsc` 无法表达的约束,改用 ESLint 规则或 AST 解析,而不是正则匹配源码文本(现有 `test/architecture/boundaries.test.ts`、`test/hooks/points-coverage.test.ts` 应按此方向迁移)。
+7. 占位 / 恒真断言——`expect(true).toBe(true)`、只 `toBeDefined()` 不校验值、以及无论如何都会通过的断言。
+8. 重复用例——与已有用例覆盖同一行为的副本;发现重复应合并,不要并存。
+
+**其他**:
+
+- 测试必须独立、确定、可任意顺序执行,不依赖执行顺序或共享可变状态(每个用例自备数据并自行清理)。
+- 不为「将来可能的变化」预留用例,只覆盖当前真实存在的行为。
+- 新增 / 删除测试前先取得用户许可(见「开发规则」)。
+
## 其他规则
- 用户要求回答问题时,必须清晰回答每一点问题,不得遗漏
diff --git a/packages/codingcode/test/agent/agent-cache-stability.test.ts b/packages/codingcode/test/agent/agent-cache-stability.test.ts
deleted file mode 100644
index 6e83c82a..00000000
--- a/packages/codingcode/test/agent/agent-cache-stability.test.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { describe, it, expect, vi } from 'vitest';
-import { makeState, runAgentTurn, llmStream, pEnd } from '../helpers/agent-harness.js';
-
-vi.mock('../../src/infra/config.js', () => ({
- loadConfig: () => ({
- maxSteps: 5,
- maxStopContinuations: 2,
- context: {
- compactionModel: '',
- },
- memory: {
- enabled: false,
- model: '',
- maxBytes: 16384,
- promptMaxBytes: 8192,
- },
- server: { port: 8080 },
- }),
-}));
-
-const mockState = makeState({
- sessionId: 'cache-test-sid',
- cwd: '/tmp/cache-test',
- title: 'cache-stability',
-});
-
-function makeCapturingLlm() {
- const captured: { system?: string } = {};
- const llm = {
- completeStream: vi.fn((params: any) => {
- captured.system = params.system;
- return llmStream(pEnd());
- }),
- modelInfo: { maxTokens: 1000 },
- } as any;
- return { llm, captured };
-}
-
-async function runOnce(llm: any) {
- return runAgentTurn(
- { llm, state: mockState },
- { sessionId: 'cache-test-sid', cwd: '/tmp/cache-test' }
- );
-}
-
-describe('LLM prompt cache stability', () => {
- it('system prompt does not include deferred tools catalog', async () => {
- const { llm, captured } = makeCapturingLlm();
- await runOnce(llm);
- expect(captured.system).toBeDefined();
- expect(captured.system).not.toContain('');
- expect(captured.system).not.toContain('');
- });
-
- it('system prompt is byte-identical across consecutive turns', async () => {
- const { llm, captured } = makeCapturingLlm();
- await runOnce(llm);
- const first = captured.system;
- expect(first).toBeDefined();
- await runOnce(llm);
- const second = captured.system;
- expect(second).toBe(first);
- });
-});
diff --git a/packages/codingcode/test/agent/agent-profile-filter.test.ts b/packages/codingcode/test/agent/agent-profile-filter.test.ts
deleted file mode 100644
index 3b162944..00000000
--- a/packages/codingcode/test/agent/agent-profile-filter.test.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import { buildSystemPrompt } from '../../src/agent/prompt.js';
-
-describe('system prompt', () => {
- it('does not advertise a subagent catalog', () => {
- const prompt = buildSystemPrompt({
- cwd: '/x',
- platform: 'linux',
- shell: 'bash',
- });
- expect(prompt).not.toContain('Available Subagents');
- });
-});
diff --git a/packages/codingcode/test/agent/build-system-prompt.test.ts b/packages/codingcode/test/agent/build-system-prompt.test.ts
deleted file mode 100644
index 24e83b2c..00000000
--- a/packages/codingcode/test/agent/build-system-prompt.test.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { BUILD_PROMPT, PLAN_PROMPT, PLAN_PROFILE } from '../../src/agent/profile.js';
-import { buildSystemPrompt } from '../../src/agent/prompt.js';
-
-describe('buildSystemPrompt', () => {
- it('uses the build prompt when profileSystemPrompt is not provided', () => {
- const prompt = buildSystemPrompt({
- cwd: '/test',
- platform: 'linux',
- shell: 'bash',
- });
- expect(prompt).toContain(BUILD_PROMPT);
- expect(prompt).toContain('## How you work');
- expect(prompt).toContain('## Environment');
- expect(prompt).toContain('Working directory: /test');
- });
-
- it('uses the plan prompt when profileSystemPrompt is provided', () => {
- const prompt = buildSystemPrompt({
- cwd: '/test',
- platform: 'linux',
- shell: 'bash',
- profileSystemPrompt: PLAN_PROMPT,
- });
- expect(prompt).toContain('You are a planning agent');
- expect(prompt).toContain('## Environment');
- expect(prompt).toContain('Working directory: /test');
- expect(prompt).not.toContain('You are a coding assistant');
- expect(prompt).not.toContain('## How you work');
- });
-
- it('emits env segment with cwd/platform/shell replaced', () => {
- const prompt = buildSystemPrompt({
- cwd: '/projects/foo',
- platform: 'darwin',
- shell: 'zsh',
- });
- expect(prompt).toContain('Working directory: /projects/foo');
- expect(prompt).toContain('Operating system: darwin');
- expect(prompt).toContain('Shell: zsh');
- expect(prompt).not.toContain('{{cwd}}');
- expect(prompt).not.toContain('{{platform}}');
- expect(prompt).not.toContain('{{shell}}');
- });
-
- it('appends user-defined rules when provided', () => {
- const prompt = buildSystemPrompt({
- cwd: '/x',
- platform: 'linux',
- shell: 'bash',
- rules: 'Always use TypeScript strict mode.',
- });
- expect(prompt).toContain('## User-defined Rules');
- expect(prompt).toContain('Always use TypeScript strict mode.');
- });
-
- it('plan profile prompt limits implementation work to submit_plan', () => {
- const prompt = buildSystemPrompt({
- cwd: '/x',
- platform: 'linux',
- shell: 'bash',
- profileSystemPrompt: PLAN_PROFILE.systemPrompt,
- });
- expect(prompt).toContain('submit_plan');
- expect(prompt).toContain('write_file / edit_file / execute_command are denied');
- });
-});
diff --git a/packages/codingcode/test/agent/memory-snapshot.test.ts b/packages/codingcode/test/agent/memory-snapshot.test.ts
index 6a46d531..14e2f0d3 100644
--- a/packages/codingcode/test/agent/memory-snapshot.test.ts
+++ b/packages/codingcode/test/agent/memory-snapshot.test.ts
@@ -25,11 +25,10 @@ function makeStateForMemory() {
}
function makeCapturingLlm() {
- const captured: { system?: string; messages?: any[] } = {};
+ const captured: { system?: string } = {};
const llm = {
completeStream: vi.fn((params: any) => {
captured.system = params.system;
- captured.messages = params.messages;
return llmStream(pEnd());
}),
modelInfo: { maxTokens: 1000 },
@@ -45,31 +44,9 @@ async function runOnce(llm: any, memorySnapshot: string = '') {
}
describe('Memory snapshot semantics', () => {
- it('loads memory via MemoryPort and includes it in the system prompt', async () => {
+ it('injects the provided memory snapshot into the system prompt', async () => {
const { llm, captured } = makeCapturingLlm();
await runOnce(llm, MEMORY);
- expect(captured.system).toContain('## Session Memory');
expect(captured.system).toContain('Frozen content');
});
-
- it('system prompt is byte-identical across consecutive turns with the same memory snapshot', async () => {
- const { llm, captured } = makeCapturingLlm();
- await runOnce(llm, MEMORY);
- const first = captured.system;
- expect(first).toBeDefined();
- await runOnce(llm, MEMORY);
- const second = captured.system;
- expect(second).toBe(first);
- });
-
- it('appends memory verbatim and does not inject into messages', async () => {
- const { llm, captured } = makeCapturingLlm();
- await runOnce(llm, MEMORY);
- // memory 块原样拼在 "## Session Memory" 标题之后,中间无注入的 reminder 包装
- expect(captured.system).toContain('## Session Memory\n\n## Long-term Memory\n\nFrozen content');
- const allContents = (captured.messages ?? [])
- .map((m: any) => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content)))
- .join('\n');
- expect(allContents).not.toContain('');
- });
});
diff --git a/packages/codingcode/test/agent/message-tool-calls.test.ts b/packages/codingcode/test/agent/message-tool-calls.test.ts
deleted file mode 100644
index 43fd5091..00000000
--- a/packages/codingcode/test/agent/message-tool-calls.test.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import type { Message } from '../../src/contracts/types.js';
-
-describe('Message tool_calls assignment', () => {
- it('should allow direct assignment without as any cast', () => {
- const toolCalls = [{ id: 'tc1', name: 'read_file', arguments: { path: '/foo.ts' } }];
- const assistantMsg: Message = { role: 'assistant', content: 'Reading file' };
-
- // This must compile and work without `as any` because Message already
- // declares the optional `tool_calls` field.
- assistantMsg.tool_calls = toolCalls;
-
- expect(assistantMsg.tool_calls).toBe(toolCalls);
- expect(assistantMsg.tool_calls!).toHaveLength(1);
- expect(assistantMsg.tool_calls![0]!.name).toBe('read_file');
- });
-
- it('should keep tool_calls undefined when not assigned', () => {
- const assistantMsg: Message = { role: 'assistant', content: 'Hello' };
- expect(assistantMsg.tool_calls).toBeUndefined();
- });
-
- it('should round-trip through messages array', () => {
- const messages: Message[] = [];
- const assistantMsg: Message = { role: 'assistant', content: '' };
- assistantMsg.tool_calls = [
- { id: 'a', name: 'tool1', arguments: {} },
- { id: 'b', name: 'tool2', arguments: { x: 1 } },
- ];
- messages.push(assistantMsg);
-
- expect(messages[0]!.tool_calls!).toHaveLength(2);
- expect(messages[0]!.tool_calls![1]!.id).toBe('b');
- });
-});
diff --git a/packages/codingcode/test/agent/stop-decision-type.test.ts b/packages/codingcode/test/agent/stop-decision-type.test.ts
deleted file mode 100644
index 702abf37..00000000
--- a/packages/codingcode/test/agent/stop-decision-type.test.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-// 必须排在 hooks.js 前面:vi.mock 工厂要引用 fakeSpawn
-import { resetFakeSpawn, whenCommand, fakeSpawn } from '../hooks/fake-spawn.js';
-import { Effect } from 'effect';
-import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
-import { join, resolve } from 'path';
-import { tmpdir } from 'os';
-import { HookService } from '../../src/hooks/port.js';
-import { HookLayer } from '../../src/hooks/hooks.js';
-import { useTempHome } from '../helpers/temp-home.js';
-
-vi.mock('child_process', () => ({
- spawn: (command: string, args: string[]) => fakeSpawn(command, args),
-}));
-
-describe('agent.turn.stop 的决策类型推断', () => {
- const testDir = resolve(tmpdir(), 'codingcode-test-stop-decision');
- // 全局层落在临时 home 里,避免读到开发机上的 ~/.codingcode(全局配置目录不可指定)
- useTempHome('codingcode-test-stop-decision-');
-
- beforeEach(() => {
- resetFakeSpawn();
- if (existsSync(testDir)) rmSync(testDir, { recursive: true, force: true });
- mkdirSync(join(testDir, '.codingcode'), { recursive: true });
- });
-
- afterEach(() => {
- if (existsSync(testDir)) rmSync(testDir, { recursive: true, force: true });
- });
-
- it('emitDecision 直接给出 HookDecision,读 decision / injection 不需要 any', async () => {
- writeFileSync(
- join(testDir, '.codingcode', 'hooks.yaml'),
- 'hooks:\n - name: loop\n point: agent.turn.stop\n type: decision\n command: cmd-loop\n args: []\n'
- );
- whenCommand('cmd-loop', { stdout: '{"decision":"continue","injection":"(test continue)"}' });
-
- const program = Effect.gen(function* () {
- const hooks = yield* HookService;
- yield* hooks.reloadUserHooks(testDir);
- const stopDecision = yield* hooks.emitDecision('agent.turn.stop', {
- projectPath: testDir,
- sessionId: 'test-sid',
- content: 'hello',
- turnId: 1,
- });
- // 类型级检查:stopDecision 是 HookDecision | null,下面两行必须无 `as any` 通过编译
- return stopDecision?.decision === 'continue' ? stopDecision.injection : null;
- });
-
- const result = await Effect.runPromise(program.pipe(Effect.provide(HookLayer) as any));
- expect(result).toBe('(test continue)');
- });
-});
diff --git a/packages/codingcode/test/checkpoint/checkpoint-diff.test.ts b/packages/codingcode/test/checkpoint/checkpoint-diff.test.ts
index e09f64a9..4e09da0f 100644
--- a/packages/codingcode/test/checkpoint/checkpoint-diff.test.ts
+++ b/packages/codingcode/test/checkpoint/checkpoint-diff.test.ts
@@ -1,6 +1,5 @@
import { describe, it, expect } from 'vitest';
import { useTempProjectBase } from '../helpers/project-base.js';
-import { CheckpointLayer } from '../../src/checkpoint/checkpoint.js';
useTempProjectBase();
@@ -18,37 +17,3 @@ describe('toGitPath', () => {
});
});
-describe('CheckpointService class', () => {
- it('CheckpointService class is exported', async () => {
- const mod = await import('../../src/checkpoint/port.js');
- expect(mod.CheckpointService).toBeDefined();
- }, 60000);
-});
-
-describe('CheckpointDiff type with insertions/deletions', () => {
- it('CheckpointDiff type includes insertions and deletions fields', async () => {
- // Verify the type structure by creating a mock object
- const diff: import('../../src/checkpoint/types.js').CheckpointDiff = {
- turnId: 1,
- files: [
- {
- path: 'test.ts',
- status: 'M',
- diff: '--- a/test.ts\n+++ b/test.ts\n@@ -1 +1 @@\n-old\n+new',
- insertions: 1,
- deletions: 1,
- },
- ],
- };
- expect(diff.files[0]!.insertions).toBe(1);
- expect(diff.files[0]!.deletions).toBe(1);
- });
-});
-
-describe('CheckpointService', () => {
- it('should export a Default layer', async () => {
- const { CheckpointService } = await import('../../src/checkpoint/port.js');
- expect(CheckpointService).toBeDefined();
- expect((CheckpointLayer as any)).toBeDefined();
- });
-});
diff --git a/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts b/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts
index 6ce9f093..fceda626 100644
--- a/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts
+++ b/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts
@@ -1,6 +1,5 @@
import { describe, it, expect } from 'vitest';
import { useTempProjectBase } from '../helpers/project-base.js';
-import { CheckpointLayer } from '../../src/checkpoint/checkpoint.js';
useTempProjectBase();
@@ -47,10 +46,3 @@ describe('toGitPath preserves original casing for git paths', () => {
});
});
-describe('CheckpointService', () => {
- it('should export a Default layer', async () => {
- const { CheckpointService } = await import('../../src/checkpoint/port.js');
- expect(CheckpointService).toBeDefined();
- expect((CheckpointLayer as any)).toBeDefined();
- });
-});
diff --git a/packages/codingcode/test/context/compressor/l5-prompt.test.ts b/packages/codingcode/test/context/compressor/l5-prompt.test.ts
deleted file mode 100644
index e076353f..00000000
--- a/packages/codingcode/test/context/compressor/l5-prompt.test.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { COMPACTION_SYSTEM_PROMPT } from '../../../src/context/compaction-prompt.js';
-
-// 断言直接读真实提示词常量,避免测试与实现各写一份而悄悄漂移。
-describe('L5 compaction prompt contract', () => {
- const SECTIONS = [
- '## 1. Primary Request and Intent',
- '## 2. Key Technical Concepts',
- '## 3. Files and Code Sections',
- '## 4. Errors and Fixes',
- '## 5. Problem Solving',
- '## 6. Decision Rationale and Rejected Approaches',
- '## 7. All User Messages',
- '## 8. Pending Tasks',
- '## 9. Current Work',
- '## 10. Optional Next Step',
- ];
-
- it('requests all ten sections in order', () => {
- let cursor = -1;
- for (const section of SECTIONS) {
- const at = COMPACTION_SYSTEM_PROMPT.indexOf(section);
- expect(at, `missing or out-of-order section: ${section}`).toBeGreaterThan(cursor);
- cursor = at;
- }
- });
-
- it('no longer asks for a separate analysis block', () => {
- expect(COMPACTION_SYSTEM_PROMPT).not.toMatch(/<\/?analysis>/);
- });
-
- it('no longer wraps the summary in tags', () => {
- expect(COMPACTION_SYSTEM_PROMPT).not.toMatch(/<\/?summary>/);
- });
-});
diff --git a/packages/codingcode/test/context/organizer.test.ts b/packages/codingcode/test/context/organizer.test.ts
deleted file mode 100644
index c712c4bb..00000000
--- a/packages/codingcode/test/context/organizer.test.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { Effect, Layer } from 'effect';
-import { ContextService } from '../../src/context/port.js';
-import { SessionService } from '../../src/session/port.js';
-import { SessionLayer } from '../../src/session/session.js';
-import { LLMService } from '../../src/llm/port.js';
-import type { SessionEvent, ToolResultEvent } from '../../src/contracts/session.js';
-import { ContextLayer } from '../../src/context/context.js';
-
-const baseConfig = {
- compactionModel: '',
-};
-
-function makeUserEvent(content: string, turnId: number): SessionEvent {
- return { type: 'user', content, turnId };
-}
-
-function makeAssistant(content: string, turnId: number): SessionEvent {
- return {
- type: 'assistant',
- content,
- turnId,
- toolCalls: [],
- };
-}
-
-function makeToolResult(
- toolName: string,
- output: string,
- turnId: number,
- toolCallId: string
-): ToolResultEvent {
- return {
- type: 'tool_result',
- toolName,
- toolCallId,
- output,
- turnId,
- };
-}
-
-const TestLayer = Layer.merge(
- SessionLayer,
- Layer.succeed(LLMService, {
- complete: () => Effect.fail(new Error('no llm')),
- completeStream: () => (async function* () {})(),
- } as any)
-);
-
-describe('assemblePayload', () => {
- it('is importable and exists as a method on ContextService', async () => {
- const svc = await Effect.runPromise(
- Effect.gen(function* () {
- const ctx = yield* ContextService;
- return ctx;
- }).pipe(Effect.provide(ContextLayer), Effect.provide(TestLayer))
- );
- expect(typeof svc.assemblePayload).toBe('function');
- });
-});
diff --git a/packages/codingcode/test/core/path.test.ts b/packages/codingcode/test/core/path.test.ts
index 33db2804..d0454d59 100644
--- a/packages/codingcode/test/core/path.test.ts
+++ b/packages/codingcode/test/core/path.test.ts
@@ -1,12 +1,7 @@
import { describe, it, expect } from 'vitest';
import { join } from 'path';
import { homedir, tmpdir } from 'os';
-import {
- normalizePath,
- encodeProjectPath,
- CODINGCODE_DIRNAME,
- getGlobalDir,
-} from '../../src/core/path.js';
+import { normalizePath, encodeProjectPath, getGlobalDir } from '../../src/core/path.js';
import { ShadowGit } from '../../src/checkpoint/shadow-git.js';
import { setFakeHome, restoreHome } from '../helpers/temp-home.js';
@@ -41,11 +36,7 @@ describe('core/path:纯格式化函数', () => {
});
});
-describe('core/path:唯一被允许的常量', () => {
- it('目录名字面量被钉住:改它必须显式改这里', () => {
- expect(CODINGCODE_DIRNAME).toBe('.codingcode');
- });
-
+describe('core/path:全局目录', () => {
it('全局目录落在用户目录下', () => {
expect(getGlobalDir()).toBe(join(homedir(), '.codingcode'));
});
diff --git a/packages/codingcode/test/plan/is-plan-profile.test.ts b/packages/codingcode/test/plan/is-plan-profile.test.ts
index 77c2cdfd..6633c3d5 100644
--- a/packages/codingcode/test/plan/is-plan-profile.test.ts
+++ b/packages/codingcode/test/plan/is-plan-profile.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { isPlanProfile, PLAN_PROFILE_NAME, BUILD_PROFILE_NAME } from '../../src/agent/profile.js';
+import { isPlanProfile } from '../../src/agent/profile.js';
describe('isPlanProfile', () => {
it('returns true for a profile named "plan"', () => {
@@ -19,8 +19,4 @@ describe('isPlanProfile', () => {
expect(isPlanProfile(undefined)).toBe(false);
});
- it('exposes the canonical plan/build profile name constants', () => {
- expect(PLAN_PROFILE_NAME).toBe('plan');
- expect(BUILD_PROFILE_NAME).toBe('build');
- });
});
diff --git a/packages/codingcode/test/plan/policy.test.ts b/packages/codingcode/test/plan/policy.test.ts
index 2e09926b..6c38c4eb 100644
--- a/packages/codingcode/test/plan/policy.test.ts
+++ b/packages/codingcode/test/plan/policy.test.ts
@@ -2,12 +2,6 @@ import { describe, expect, it } from 'vitest';
import { PLAN_ALLOWED_TOOLS } from '../../src/contracts/permission.js';
describe('PLAN_ALLOWED_TOOLS', () => {
- it('contains only read tools and submit_plan', () => {
- expect(PLAN_ALLOWED_TOOLS).toEqual(
- new Set(['read_file', 'search_files', 'search_code', 'fetch_url', 'submit_plan'])
- );
- });
-
it('does not expose write tools', () => {
expect(PLAN_ALLOWED_TOOLS.has('write_file')).toBe(false);
expect(PLAN_ALLOWED_TOOLS.has('edit_file')).toBe(false);
diff --git a/packages/codingcode/test/prompts/system-prompt.test.ts b/packages/codingcode/test/prompts/system-prompt.test.ts
deleted file mode 100644
index 671eaffe..00000000
--- a/packages/codingcode/test/prompts/system-prompt.test.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { buildSystemPrompt, SYSTEM_NOTES } from '../../src/agent/prompt.js';
-
-const baseOpts = { cwd: '/test', platform: 'linux', shell: 'bash' };
-
-describe('buildSystemPrompt', () => {
- it('replaces cwd, platform, shell placeholders', () => {
- const prompt = buildSystemPrompt({ cwd: '/my/proj', platform: 'darwin', shell: 'zsh' });
- expect(prompt).toContain('/my/proj');
- expect(prompt).toContain('darwin');
- expect(prompt).toContain('zsh');
- });
-
- it('includes identity definition', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).toContain('coding assistant');
- expect(prompt).toContain('software engineering tasks');
- });
-
- it('includes How you work section', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).toContain('How you work');
- expect(prompt).toContain('permission system');
- expect(prompt).toContain('system-reminder');
- });
-
- it('Rule 7 guides assessment-first then optional delegation', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).toContain('assess the task scope');
- expect(prompt).toContain('dispatch_agent');
- });
-
- it('Rule 7 delegation does not point at a removed subagent catalog', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).not.toContain('runtime-configured subagent');
- expect(prompt).not.toContain('Available Subagents');
- expect(prompt).toContain('short agentName');
- expect(prompt).toContain('keep the delegated write set disjoint');
- });
-
- it('includes Using your tools section', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).toContain('Using your tools');
- expect(prompt).toContain('Prefer dedicated tools over shell commands');
- expect(prompt).toContain('Call multiple tools in parallel');
- expect(prompt).toContain('read_file instead of cat');
- });
-
- it('includes Executing actions with care section', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).toContain('Executing actions with care');
- expect(prompt).toContain('reversibility and blast radius');
- expect(prompt).toContain('destructive commands');
- expect(prompt).toContain('rm -rf');
- });
-
- it('includes Git operations section', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).toContain('Git operations');
- expect(prompt).toContain('Do NOT commit changes unless the user explicitly asks');
- expect(prompt).toContain('git reset --hard');
- expect(prompt).toContain('git push --force');
- });
-
- it('includes Professional objectivity section', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).toContain('Professional objectivity');
- expect(prompt).toContain('technical accuracy');
- expect(prompt).toContain('Do not begin responses with conversational interjections');
- });
-
- it('includes Follow existing conventions section with expanded guidance', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).toContain('Follow existing conventions');
- expect(prompt).toContain('Never assume a library is available');
- expect(prompt).toContain('package.json');
- expect(prompt).toContain('Comments');
- expect(prompt).toContain('WHY is non-obvious');
- });
-
- it('includes Code references section', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).toContain('Code references');
- expect(prompt).toContain('file_path:line_number');
- });
-
- it('includes Output efficiency section', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).toContain('Output efficiency');
- expect(prompt).toContain('Lead with the answer');
- expect(prompt).toContain('one-to-two sentence summary');
- expect(prompt).toContain('Match the response to the question');
- });
-
- it('SYSTEM_NOTES explains compression, memory, and todo', () => {
- expect(SYSTEM_NOTES).toContain('automatically compressed');
- expect(SYSTEM_NOTES).toContain('Session Memory');
- expect(SYSTEM_NOTES).toContain('todo_write');
- });
-
- it('includes SYSTEM_NOTES in prompt', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).toContain('System Notes');
- expect(prompt).toContain('automatically compressed');
- });
-
- it('includes user-defined rules section when rules exist', () => {
- const prompt = buildSystemPrompt({ ...baseOpts, rules: 'Always use TypeScript strict mode' });
- expect(prompt).toContain('User-defined Rules');
- expect(prompt).toContain('Always use TypeScript strict mode');
- });
-
- it('omits user-defined rules section when rules is undefined', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).not.toContain('User-defined Rules');
- });
-
- it('omits available subagents section when no profiles are provided', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).not.toContain('Available Subagents');
- });
-
- it('does not contain old Rule 3 (verify with read_file after writing)', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).not.toContain('verify with read_file');
- });
-
- it('does not contain DEFERRED_TOOLS_GUIDELINES as separate section', () => {
- const prompt = buildSystemPrompt(baseOpts);
- expect(prompt).not.toContain('Deferred tools');
- });
-});
diff --git a/packages/codingcode/test/session/facade-surface.test.ts b/packages/codingcode/test/session/facade-surface.test.ts
index b4593489..ea099dbe 100644
--- a/packages/codingcode/test/session/facade-surface.test.ts
+++ b/packages/codingcode/test/session/facade-surface.test.ts
@@ -4,36 +4,6 @@ import { SessionService } from '../../src/session/port.js';
import { SessionLayer } from '../../src/session/session.js';
describe('session service surface', () => {
- it('service shape exposes exactly the contract method set', async () => {
- const service = await Effect.runPromise(
- Effect.gen(function* () {
- return yield* SessionService;
- }).pipe(Effect.provide(SessionLayer))
- );
- const methods = Object.keys(service).sort();
- expect(methods).toEqual([
- 'appendEvent',
- 'appendSummary',
- 'create',
- 'deleteSession',
- 'forkSession',
- 'listSessions',
- 'load',
- 'readEvents',
- 'readHistory',
- 'readUITurns',
- 'recordAssistant',
- 'recordSystem',
- 'recordToolResult',
- 'recordUser',
- 'renameSession',
- 'rollbackToTurn',
- 'setActiveProfile',
- 'setModel',
- 'setPermissionMode',
- ]);
- });
-
it('does not leak file-level operations through the service', async () => {
const service = await Effect.runPromise(
Effect.gen(function* () {
diff --git a/packages/codingcode/test/session/types-export.test.ts b/packages/codingcode/test/session/types-export.test.ts
deleted file mode 100644
index cf9b248e..00000000
--- a/packages/codingcode/test/session/types-export.test.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import type { SessionStoreState } from '../../src/contracts/session.js';
-
-describe('SessionStoreState export', () => {
- it('contains only cwd as its path source', () => {
- const state: SessionStoreState = {
- type: 'session_meta',
- sessionId: 'test-sid',
- cwd: '/tmp',
- createdAt: '2026-01-01T00:00:00.000Z',
- model: 'gpt-4',
- title: '',
- activeProfile: 'build',
- permissionMode: 'ask',
- currentTurnId: 0,
- usage: undefined,
- memorySnapshot: '',
- };
-
- expect(state.cwd).toBe('/tmp');
- expect(state).not.toHaveProperty('projectPath');
- expect(state).not.toHaveProperty('transcriptPath');
- expect(state).not.toHaveProperty('indexPath');
- expect(state).not.toHaveProperty('mode');
- });
-});
diff --git a/packages/codingcode/test/subagent/builtin-profiles.test.ts b/packages/codingcode/test/subagent/builtin-profiles.test.ts
deleted file mode 100644
index 06b5c233..00000000
--- a/packages/codingcode/test/subagent/builtin-profiles.test.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import { PLAN_PROFILE, BUILD_PROFILE, PLAN_TOOL_NAMES } from '../../src/agent/profile.js';
-
-describe('built-in subagent profiles', () => {
- it('keeps only build and plan as built-in names', () => {
- expect([BUILD_PROFILE.name, PLAN_PROFILE.name].sort()).toEqual(['build', 'plan']);
- });
-
- it('keeps plan tools independent from profile tool lists', () => {
- expect('tools' in PLAN_PROFILE).toBe(false);
- expect('tools' in BUILD_PROFILE).toBe(false);
- expect(PLAN_TOOL_NAMES).toEqual([
- 'read_file',
- 'search_files',
- 'search_code',
- 'fetch_url',
- 'submit_plan',
- ]);
- });
-});
diff --git a/packages/codingcode/test/types/type-collapse.test.ts b/packages/codingcode/test/types/type-collapse.test.ts
index bdf24e20..0f4dee8f 100644
--- a/packages/codingcode/test/types/type-collapse.test.ts
+++ b/packages/codingcode/test/types/type-collapse.test.ts
@@ -1,20 +1,13 @@
-import { describe, expect, it } from 'vitest';
+import { describe, it } from 'vitest';
import type { UITurn, UITurnItem } from '../../src/contracts/session.js';
-import type { TodoItem, TokenUsage } from '../../src/contracts/types.js';
-
-type AssertNotAny = 0 extends 1 & T ? never : T;
-
-type _UITurnNotAny = AssertNotAny;
-type _TodoNotAny = AssertNotAny;
-type _UsageNotAny = AssertNotAny;
+import type { TodoItem } from '../../src/contracts/types.js';
+// 本文件只做编译期类型约束(tsc 执行 @ts-expect-error 校验),不含运行时断言。
describe('类型收口', () => {
- it('UITurn.status 不再退化为 string', () => {
- const turn: UITurn = { id: '1', items: [], status: 'completed' };
- expect(turn.status).toBe('completed');
+ it('UITurn.status 不接受任意字符串', () => {
// @ts-expect-error status 只能是三个字面量之一
const bad: UITurn = { id: '1', items: [], status: 'nope' };
- expect(bad).toBeDefined();
+ void bad;
});
it('UITurnItem 覆盖 session 产出的全部变体', () => {
@@ -26,21 +19,12 @@ describe('类型收口', () => {
{ id: 'e', type: 'reasoning', content: 'r', isVisible: false },
{ id: 'f', type: 'error', message: 'e' },
];
- expect(items.map((i) => i.type)).toEqual([
- 'message',
- 'tool_call',
- 'tool_result',
- 'summary',
- 'reasoning',
- 'error',
- ]);
+ void items;
});
it('TodoItem.status 是字面量联合而非 string', () => {
- const todo: TodoItem = { step: 'do it', status: 'in_progress' };
- expect(todo.status).toBe('in_progress');
// @ts-expect-error status 只能是 pending / in_progress / completed
const bad: TodoItem = { step: 'x', status: 'whatever' };
- expect(bad).toBeDefined();
+ void bad;
});
});
diff --git a/packages/desktop/test/diff-and-store.test.ts b/packages/desktop/test/diff-and-store.test.ts
new file mode 100644
index 00000000..7c64e73d
--- /dev/null
+++ b/packages/desktop/test/diff-and-store.test.ts
@@ -0,0 +1,234 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { computeDiff } from '../src/lib/diff-compute';
+import { useAgentStore } from '../src/stores/agent.store';
+import type { Item } from '../shared/types';
+
+// ─── diff-compute: large file protection ─────────────────────────────────
+
+describe('computeDiff - large file protection', () => {
+ it('uses LCS diff for files under 500 lines', () => {
+ const oldContent = Array(100).fill('line').join('\n');
+ const newContent = Array(100).fill('line').join('\n');
+ newContent.replace('line', 'changed');
+
+ const result = computeDiff(oldContent, newContent);
+ // LCS diff should produce contextual diff, not all-delete+all-insert
+ expect(result.diff).toContain(' line');
+ });
+
+ it('falls back to simplified diff for files over 500 lines', () => {
+ const oldLines = Array(600).fill('old line');
+ const newLines = Array(600).fill('new line');
+ // Only change 1 line — LCS would show 1 deletion + 1 insertion
+ oldLines[300] = 'changed';
+ newLines[300] = 'changed';
+
+ const result = computeDiff(oldLines.join('\n'), newLines.join('\n'));
+ // Simplified diff shows ALL old lines as deletions, ALL new lines as insertions
+ expect(result.deletions).toBe(600);
+ expect(result.insertions).toBe(600);
+ });
+
+ it('simplified diff still produces valid diff output', () => {
+ const oldContent = Array(501).fill('a').join('\n');
+ const newContent = Array(501).fill('b').join('\n');
+
+ const result = computeDiff(oldContent, newContent);
+ expect(result.diff).toContain('-a');
+ expect(result.diff).toContain('+b');
+ expect(result.insertions).toBe(501);
+ expect(result.deletions).toBe(501);
+ });
+
+ it('new file shortcut still works regardless of line count', () => {
+ const newContent = Array(1000).fill('line').join('\n');
+ const result = computeDiff('', newContent);
+ expect(result.insertions).toBe(1000);
+ expect(result.deletions).toBe(0);
+ expect(result.diff).toContain('+line');
+ });
+});
+
+// ─── agent.store: applyChunk tool_result 定位与插入位置 ───────────────────
+
+describe('global store - applyChunk tool_result searches current turn first', () => {
+ beforeEach(() => {
+ useAgentStore.setState({
+ currentThreadId: null,
+ threads: {},
+ profile: 'build', permissionMode: 'ask',
+ model: '',
+ models: [],
+ contextUsage: null,
+ todoByThreadId: {},
+ pendingInput: null,
+ usageByThreadId: {},
+ isCompressing: false,
+ });
+ });
+
+ it('finds tool_call in current turn first', () => {
+ const threadId = 't1';
+
+ // Turn 1 with a tool_call
+ useAgentStore.getState().startTurn(threadId, {
+ id: 'turn-1',
+ items: [
+ { id: 'call-1', type: 'tool_call', name: 'read_file', args: {}, status: 'running' } as Item,
+ ],
+ status: 'completed',
+ });
+ useAgentStore.getState().completeTurn(threadId, 'turn-1', 'completed');
+
+ // Turn 2 with a tool_call of same name but different id
+ useAgentStore.getState().startTurn(threadId, {
+ id: 'turn-2',
+ items: [
+ { id: 'call-2', type: 'tool_call', name: 'read_file', args: {}, status: 'running' } as Item,
+ ],
+ status: 'running',
+ });
+
+ // Apply tool_result for call-2 (should find it in turn-2 first)
+ useAgentStore.getState().applyChunk(threadId, 'turn-2', {
+ id: 'res-2',
+ type: 'tool_result',
+ callId: 'call-2',
+ name: 'read_file',
+ output: 'ok',
+ exitCode: 0,
+ } as Item);
+
+ const turn2 = useAgentStore.getState().threads[threadId].turns[1];
+ const call = turn2.items.find((i) => i.id === 'call-2') as any;
+ expect(call.status).toBe('approved');
+ expect(turn2.items).toHaveLength(2); // call + result
+ });
+
+ it('falls back to other turns when callId not in current turn', () => {
+ const threadId = 't1';
+
+ // Turn 1 with tool_call
+ useAgentStore.getState().startTurn(threadId, {
+ id: 'turn-1',
+ items: [
+ { id: 'call-1', type: 'tool_call', name: 'read_file', args: {}, status: 'running' } as Item,
+ ],
+ status: 'completed',
+ });
+ useAgentStore.getState().completeTurn(threadId, 'turn-1', 'completed');
+
+ // Turn 2 with no tool_call
+ useAgentStore.getState().startTurn(threadId, {
+ id: 'turn-2',
+ items: [],
+ status: 'running',
+ });
+
+ // Apply tool_result for call-1 (should find it in turn-1 via fallback)
+ useAgentStore.getState().applyChunk(threadId, 'turn-2', {
+ id: 'res-1',
+ type: 'tool_result',
+ callId: 'call-1',
+ name: 'read_file',
+ output: 'ok',
+ exitCode: 0,
+ } as Item);
+
+ const turn1 = useAgentStore.getState().threads[threadId].turns[0];
+ const call = turn1.items.find((i) => i.id === 'call-1') as any;
+ expect(call.status).toBe('approved');
+ });
+});
+
+describe('global store - applyChunk tool_result uses push', () => {
+ beforeEach(() => {
+ useAgentStore.setState({
+ currentThreadId: null,
+ threads: {},
+ profile: 'build', permissionMode: 'ask',
+ model: '',
+ models: [],
+ contextUsage: null,
+ todoByThreadId: {},
+ pendingInput: null,
+ usageByThreadId: {},
+ isCompressing: false,
+ });
+ });
+
+ it('tool_result is pushed to end, not spliced after tool_call', () => {
+ const threadId = 't1';
+
+ useAgentStore.getState().startTurn(threadId, {
+ id: 'turn-1',
+ items: [
+ { id: 'msg-1', type: 'message', role: 'user', content: 'hi' } as Item,
+ { id: 'call-1', type: 'tool_call', name: 'read_file', args: {}, status: 'running' } as Item,
+ {
+ id: 'msg-2',
+ type: 'message',
+ role: 'assistant',
+ content: 'done',
+ partial: false,
+ } as Item,
+ ],
+ status: 'running',
+ });
+
+ // Apply tool_result for call-1
+ useAgentStore.getState().applyChunk(threadId, 'turn-1', {
+ id: 'res-1',
+ type: 'tool_result',
+ callId: 'call-1',
+ name: 'read_file',
+ output: 'ok',
+ exitCode: 0,
+ } as Item);
+
+ const turn = useAgentStore.getState().threads[threadId].turns[0];
+ // tool_result should be at the end, not between call-1 and msg-2
+ const lastItem = turn.items[turn.items.length - 1];
+ expect(lastItem.type).toBe('tool_result');
+ // msg-2 should still be at index 2 (not shifted)
+ expect(turn.items[2].id).toBe('msg-2');
+ });
+
+ it('existing item indices are not shifted when tool_result is pushed', () => {
+ const threadId = 't1';
+
+ useAgentStore.getState().startTurn(threadId, {
+ id: 'turn-1',
+ items: [
+ { id: 'call-1', type: 'tool_call', name: 'edit', args: {}, status: 'running' } as Item,
+ {
+ id: 'msg-1',
+ type: 'message',
+ role: 'assistant',
+ content: 'editing',
+ partial: true,
+ } as Item,
+ ],
+ status: 'running',
+ });
+
+ // Record index of msg-1 before tool_result
+ const beforeTurn = useAgentStore.getState().threads[threadId].turns[0];
+ const msgIndexBefore = beforeTurn.items.findIndex((i) => i.id === 'msg-1');
+ expect(msgIndexBefore).toBe(1);
+
+ useAgentStore.getState().applyChunk(threadId, 'turn-1', {
+ id: 'res-1',
+ type: 'tool_result',
+ callId: 'call-1',
+ name: 'edit',
+ output: 'ok',
+ exitCode: 0,
+ } as Item);
+
+ const afterTurn = useAgentStore.getState().threads[threadId].turns[0];
+ const msgIndexAfter = afterTurn.items.findIndex((i) => i.id === 'msg-1');
+ // msg-1 should still be at the same index
+ expect(msgIndexAfter).toBe(msgIndexBefore);
+ });
+});
diff --git a/packages/desktop/test/global.store.test.ts b/packages/desktop/test/global.store.test.ts
deleted file mode 100644
index a1d2ae6b..00000000
--- a/packages/desktop/test/global.store.test.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest';
-import { create } from 'zustand';
-import { immer } from 'zustand/middleware/immer';
-
-// Minimal store for testing core logic without persist
-interface TestThread {
- id: string;
- projectId: string;
- title: string;
- cwd: string;
- turns: any[];
- createdAt: number;
- updatedAt: number;
-}
-
-interface TestState {
- threads: Record;
- currentThreadId: string | null;
-}
-
-interface TestActions {
- upsertThread: (thread: TestThread) => void;
- loadThreads: (threads: TestThread[]) => void;
- setCurrentThread: (id: string | null) => void;
- startTurn: (threadId: string, turn: any, meta?: { cwd?: string; title?: string }) => void;
-}
-
-const testStore = create()(
- immer((set) => ({
- threads: {},
- currentThreadId: null,
- upsertThread: (thread) =>
- set((s) => {
- s.threads[thread.id] = thread;
- }),
- loadThreads: (threads) =>
- set((s) => {
- const next: Record = {};
- for (const t of threads) {
- const existing = s.threads[t.id];
- next[t.id] = existing ? { ...t, turns: existing.turns } : t;
- }
- s.threads = next;
- }),
- setCurrentThread: (id) =>
- set((s) => {
- s.currentThreadId = id;
- }),
- startTurn: (threadId, turn, meta) =>
- set((s) => {
- if (!s.threads[threadId]) {
- s.threads[threadId] = {
- id: threadId,
- projectId: '',
- title: meta?.title ?? 'New Conversation',
- cwd: meta?.cwd ?? '',
- turns: [turn],
- createdAt: Date.now(),
- updatedAt: Date.now(),
- };
- } else {
- s.threads[threadId].turns.push(turn);
- s.threads[threadId].updatedAt = Date.now();
- }
- }),
- }))
-);
-
-function resetStore() {
- testStore.setState({ threads: {}, currentThreadId: null });
-}
-
-describe('loadThreads simplified', () => {
- beforeEach(resetStore);
-
- it('replaces threads and preserves existing turns', () => {
- testStore.getState().upsertThread({
- id: 'sess-1',
- projectId: '',
- title: 'Old Title',
- cwd: '/path',
- turns: [{ id: 'turn-1', items: [], status: 'completed' }],
- createdAt: 1000,
- updatedAt: 1000,
- });
-
- testStore.getState().loadThreads([
- {
- id: 'sess-1',
- projectId: '',
- title: 'New Title',
- cwd: '/new-path',
- turns: [],
- createdAt: 2000,
- updatedAt: 2000,
- },
- ]);
-
- const thread = testStore.getState().threads['sess-1'];
- expect(thread).toBeDefined();
- expect(thread!.title).toBe('New Title');
- expect(thread!.cwd).toBe('/new-path');
- expect(thread!.turns).toHaveLength(1);
- expect(thread!.turns[0]!.id).toBe('turn-1');
- });
-
- it('does not preserve orphan threads', () => {
- testStore.getState().upsertThread({
- id: 'orphan-1',
- projectId: '',
- title: 'Orphan',
- cwd: '/path',
- turns: [{ id: 't1', items: [], status: 'running' }],
- createdAt: 1000,
- updatedAt: 1000,
- });
-
- testStore.getState().loadThreads([
- {
- id: 'sess-1',
- projectId: '',
- title: 'Only Session',
- cwd: '/path',
- turns: [],
- createdAt: 2000,
- updatedAt: 2000,
- },
- ]);
-
- expect(testStore.getState().threads['orphan-1']).toBeUndefined();
- expect(testStore.getState().threads['sess-1']).toBeDefined();
- });
-
- it('creates new threads for unknown ids', () => {
- testStore.getState().loadThreads([
- {
- id: 'sess-new',
- projectId: '',
- title: 'New',
- cwd: '/path',
- turns: [],
- createdAt: 3000,
- updatedAt: 3000,
- },
- ]);
-
- expect(testStore.getState().threads['sess-new']).toBeDefined();
- expect(testStore.getState().threads['sess-new']!.title).toBe('New');
- });
-});
-
-describe('Thread type without backendSessionId', () => {
- it('backendSessionId is not present on Thread', () => {
- const thread = {
- id: 'sess-1',
- projectId: '',
- title: 'Test',
- cwd: '/',
- turns: [],
- createdAt: 0,
- updatedAt: 0,
- };
- expect(thread).toBeDefined();
- });
-});
-
-describe('sendMessage uses sessionId as threadId', () => {
- beforeEach(resetStore);
-
- it('startTurn creates a thread with given threadId', () => {
- testStore.getState().startTurn('sess-abc', {
- id: 'turn-1',
- items: [{ id: 'u1', type: 'message', role: 'user', content: 'hi' }],
- status: 'running',
- });
-
- expect(testStore.getState().threads['sess-abc']).toBeDefined();
- expect(testStore.getState().threads['sess-abc']!.id).toBe('sess-abc');
- });
-});
diff --git a/packages/desktop/test/performance-optimization.test.ts b/packages/desktop/test/performance-optimization.test.ts
deleted file mode 100644
index 5952959b..00000000
--- a/packages/desktop/test/performance-optimization.test.ts
+++ /dev/null
@@ -1,508 +0,0 @@
-import { describe, it, expect, beforeEach, vi } from 'vitest';
-import { computeDiff } from '../src/lib/diff-compute';
-import { parseUnifiedDiff } from '../src/lib/diff-parser';
-import { useAgentStore } from '../src/stores/agent.store';
-import type { Item, Turn } from '../shared/types';
-
-// ─── diff-compute: large file protection ─────────────────────────────────
-
-describe('computeDiff - large file protection', () => {
- it('uses LCS diff for files under 500 lines', () => {
- const oldContent = Array(100).fill('line').join('\n');
- const newContent = Array(100).fill('line').join('\n');
- newContent.replace('line', 'changed');
-
- const result = computeDiff(oldContent, newContent);
- // LCS diff should produce contextual diff, not all-delete+all-insert
- expect(result.diff).toContain(' line');
- });
-
- it('falls back to simplified diff for files over 500 lines', () => {
- const oldLines = Array(600).fill('old line');
- const newLines = Array(600).fill('new line');
- // Only change 1 line — LCS would show 1 deletion + 1 insertion
- oldLines[300] = 'changed';
- newLines[300] = 'changed';
-
- const result = computeDiff(oldLines.join('\n'), newLines.join('\n'));
- // Simplified diff shows ALL old lines as deletions, ALL new lines as insertions
- expect(result.deletions).toBe(600);
- expect(result.insertions).toBe(600);
- });
-
- it('simplified diff still produces valid diff output', () => {
- const oldContent = Array(501).fill('a').join('\n');
- const newContent = Array(501).fill('b').join('\n');
-
- const result = computeDiff(oldContent, newContent);
- expect(result.diff).toContain('-a');
- expect(result.diff).toContain('+b');
- expect(result.insertions).toBe(501);
- expect(result.deletions).toBe(501);
- });
-
- it('new file shortcut still works regardless of line count', () => {
- const newContent = Array(1000).fill('line').join('\n');
- const result = computeDiff('', newContent);
- expect(result.insertions).toBe(1000);
- expect(result.deletions).toBe(0);
- expect(result.diff).toContain('+line');
- });
-});
-
-// ─── global.store: applyChunk tool_result priority ──────────────────────
-
-describe('global store - applyChunk tool_result searches current turn first', () => {
- beforeEach(() => {
- useAgentStore.setState({
- currentThreadId: null,
- threads: {},
- profile: 'build', permissionMode: 'ask',
- model: '',
- models: [],
- contextUsage: null,
- todoByThreadId: {},
- pendingInput: null,
- usageByThreadId: {},
- isCompressing: false,
- });
- });
-
- it('finds tool_call in current turn first', () => {
- const threadId = 't1';
-
- // Turn 1 with a tool_call
- useAgentStore.getState().startTurn(threadId, {
- id: 'turn-1',
- items: [
- { id: 'call-1', type: 'tool_call', name: 'read_file', args: {}, status: 'running' } as Item,
- ],
- status: 'completed',
- });
- useAgentStore.getState().completeTurn(threadId, 'turn-1', 'completed');
-
- // Turn 2 with a tool_call of same name but different id
- useAgentStore.getState().startTurn(threadId, {
- id: 'turn-2',
- items: [
- { id: 'call-2', type: 'tool_call', name: 'read_file', args: {}, status: 'running' } as Item,
- ],
- status: 'running',
- });
-
- // Apply tool_result for call-2 (should find it in turn-2 first)
- useAgentStore.getState().applyChunk(threadId, 'turn-2', {
- id: 'res-2',
- type: 'tool_result',
- callId: 'call-2',
- name: 'read_file',
- output: 'ok',
- exitCode: 0,
- } as Item);
-
- const turn2 = useAgentStore.getState().threads[threadId].turns[1];
- const call = turn2.items.find((i) => i.id === 'call-2') as any;
- expect(call.status).toBe('approved');
- expect(turn2.items).toHaveLength(2); // call + result
- });
-
- it('falls back to other turns when callId not in current turn', () => {
- const threadId = 't1';
-
- // Turn 1 with tool_call
- useAgentStore.getState().startTurn(threadId, {
- id: 'turn-1',
- items: [
- { id: 'call-1', type: 'tool_call', name: 'read_file', args: {}, status: 'running' } as Item,
- ],
- status: 'completed',
- });
- useAgentStore.getState().completeTurn(threadId, 'turn-1', 'completed');
-
- // Turn 2 with no tool_call
- useAgentStore.getState().startTurn(threadId, {
- id: 'turn-2',
- items: [],
- status: 'running',
- });
-
- // Apply tool_result for call-1 (should find it in turn-1 via fallback)
- useAgentStore.getState().applyChunk(threadId, 'turn-2', {
- id: 'res-1',
- type: 'tool_result',
- callId: 'call-1',
- name: 'read_file',
- output: 'ok',
- exitCode: 0,
- } as Item);
-
- const turn1 = useAgentStore.getState().threads[threadId].turns[0];
- const call = turn1.items.find((i) => i.id === 'call-1') as any;
- expect(call.status).toBe('approved');
- });
-});
-
-// ─── agent.store: 不再挂 persist ─────────────────────────────────────────
-
-describe('agent store - 不持久化任何状态', () => {
- it('has no persist middleware attached', () => {
- // 模型、会话、用量、新建会话默认值全部以服务端为准,前端不留副本到磁盘
- const store: any = useAgentStore;
- expect(store.persist).toBeUndefined();
- });
-});
-
-// ─── App.tsx: event listener cleanup ─────────────────────────────────────
-
-describe('App.tsx - event listener cleanup', () => {
- it('handler reference is preserved for removeEventListener', () => {
- // Verify the pattern: store handler ref, pass to both add and remove
- const handlers: EventListener[] = [];
- const fakeAdd = (type: string, handler: EventListener) => {
- handlers.push(handler);
- };
- const fakeRemove = (type: string, handler: EventListener) => {
- const idx = handlers.indexOf(handler);
- expect(idx).toBeGreaterThanOrEqual(0);
- handlers.splice(idx, 1);
- };
-
- const handler = (() => {}) as EventListener;
- fakeAdd('menu:switchProfile', handler);
- expect(handlers).toHaveLength(1);
- fakeRemove('menu:switchProfile', handler);
- expect(handlers).toHaveLength(0);
- });
-});
-
-// ─── main.ts: stopPolling on window close ────────────────────────────────
-
-describe('main.ts - resource cleanup', () => {
- it('stopPolling is exported from git.service', async () => {
- const { stopPolling } = await import('../electron/core/git.service');
- expect(typeof stopPolling).toBe('function');
- });
-});
-
-// ─── tool_result push instead of splice ──────────────────────────────────
-
-describe('global store - applyChunk tool_result uses push', () => {
- beforeEach(() => {
- useAgentStore.setState({
- currentThreadId: null,
- threads: {},
- profile: 'build', permissionMode: 'ask',
- model: '',
- models: [],
- contextUsage: null,
- todoByThreadId: {},
- pendingInput: null,
- usageByThreadId: {},
- isCompressing: false,
- });
- });
-
- it('tool_result is pushed to end, not spliced after tool_call', () => {
- const threadId = 't1';
-
- useAgentStore.getState().startTurn(threadId, {
- id: 'turn-1',
- items: [
- { id: 'msg-1', type: 'message', role: 'user', content: 'hi' } as Item,
- { id: 'call-1', type: 'tool_call', name: 'read_file', args: {}, status: 'running' } as Item,
- {
- id: 'msg-2',
- type: 'message',
- role: 'assistant',
- content: 'done',
- partial: false,
- } as Item,
- ],
- status: 'running',
- });
-
- // Apply tool_result for call-1
- useAgentStore.getState().applyChunk(threadId, 'turn-1', {
- id: 'res-1',
- type: 'tool_result',
- callId: 'call-1',
- name: 'read_file',
- output: 'ok',
- exitCode: 0,
- } as Item);
-
- const turn = useAgentStore.getState().threads[threadId].turns[0];
- // tool_result should be at the end, not between call-1 and msg-2
- const lastItem = turn.items[turn.items.length - 1];
- expect(lastItem.type).toBe('tool_result');
- // msg-2 should still be at index 2 (not shifted)
- expect(turn.items[2].id).toBe('msg-2');
- });
-
- it('existing item indices are not shifted when tool_result is pushed', () => {
- const threadId = 't1';
-
- useAgentStore.getState().startTurn(threadId, {
- id: 'turn-1',
- items: [
- { id: 'call-1', type: 'tool_call', name: 'edit', args: {}, status: 'running' } as Item,
- {
- id: 'msg-1',
- type: 'message',
- role: 'assistant',
- content: 'editing',
- partial: true,
- } as Item,
- ],
- status: 'running',
- });
-
- // Record index of msg-1 before tool_result
- const beforeTurn = useAgentStore.getState().threads[threadId].turns[0];
- const msgIndexBefore = beforeTurn.items.findIndex((i) => i.id === 'msg-1');
- expect(msgIndexBefore).toBe(1);
-
- useAgentStore.getState().applyChunk(threadId, 'turn-1', {
- id: 'res-1',
- type: 'tool_result',
- callId: 'call-1',
- name: 'edit',
- output: 'ok',
- exitCode: 0,
- } as Item);
-
- const afterTurn = useAgentStore.getState().threads[threadId].turns[0];
- const msgIndexAfter = afterTurn.items.findIndex((i) => i.id === 'msg-1');
- // msg-1 should still be at the same index
- expect(msgIndexAfter).toBe(msgIndexBefore);
- });
-});
-
-// ─── turnsStructureKey stability ─────────────────────────────────────────
-
-describe('turnsStructureKey - content changes do not affect structure', () => {
- it('structure key includes item types and ids but not content', () => {
- const turns1 = [
- {
- id: 'turn-1',
- status: 'running',
- items: [{ id: 'msg-1', type: 'message', role: 'assistant', content: 'hello' }],
- },
- ];
- const turns2 = [
- {
- id: 'turn-1',
- status: 'running',
- items: [
- { id: 'msg-1', type: 'message', role: 'assistant', content: 'hello world more text' },
- ],
- },
- ];
-
- const buildKey = (turns: any[]) =>
- turns
- .map(
- (t) =>
- `${t.id}:${t.status}:${t.items.length}:${t.items.map((i: any) => `${i.type}:${i.id}`).join(',')}`
- )
- .join('|');
-
- // Same structure: same id, status, item count, item types and ids
- expect(buildKey(turns1)).toBe(buildKey(turns2));
- });
-
- it('structure key changes when item is added', () => {
- const turns1 = [
- {
- id: 'turn-1',
- status: 'running',
- items: [{ id: 'msg-1', type: 'message', role: 'assistant', content: 'hello' }],
- },
- ];
- const turns2 = [
- {
- id: 'turn-1',
- status: 'running',
- items: [
- { id: 'msg-1', type: 'message', role: 'assistant', content: 'hello' },
- { id: 'call-1', type: 'tool_call', name: 'edit', args: {}, status: 'running' },
- ],
- },
- ];
-
- const buildKey = (turns: any[]) =>
- turns
- .map(
- (t) =>
- `${t.id}:${t.status}:${t.items.length}:${t.items.map((i: any) => `${i.type}:${i.id}`).join(',')}`
- )
- .join('|');
-
- expect(buildKey(turns1)).not.toBe(buildKey(turns2));
- });
-
- it('structure key changes when turn status changes', () => {
- const turns1 = [{ id: 'turn-1', status: 'running', items: [{ id: 'msg-1', type: 'message' }] }];
- const turns2 = [
- { id: 'turn-1', status: 'completed', items: [{ id: 'msg-1', type: 'message' }] },
- ];
-
- const buildKey = (turns: any[]) =>
- turns
- .map(
- (t) =>
- `${t.id}:${t.status}:${t.items.length}:${t.items.map((i: any) => `${i.type}:${i.id}`).join(',')}`
- )
- .join('|');
-
- expect(buildKey(turns1)).not.toBe(buildKey(turns2));
- });
-});
-
-// ─── entryCountByTurnId correctness ──────────────────────────────────────
-
-describe('entryCountByTurnId - correct counts for multiple turns', () => {
- it('counts entries per turn correctly', () => {
- const turns = [
- {
- id: 'turn-1',
- items: [
- { id: 'msg-1', type: 'message', role: 'user', content: 'hi' },
- { id: 'msg-2', type: 'message', role: 'assistant', content: 'hello' },
- ],
- },
- {
- id: 'turn-2',
- items: [
- { id: 'msg-3', type: 'message', role: 'user', content: 'do it' },
- { id: 'call-1', type: 'tool_call', name: 'edit', args: {}, status: 'approved' },
- {
- id: 'res-1',
- type: 'tool_result',
- callId: 'call-1',
- name: 'edit',
- output: 'ok',
- exitCode: 0,
- },
- { id: 'msg-4', type: 'message', role: 'assistant', content: 'done' },
- ],
- },
- ];
-
- // Simulate renderEntries logic: skip tool_result, count per turn
- const countMap = new Map();
- for (const turn of turns) {
- for (const item of turn.items) {
- if (item.type === 'tool_result') continue;
- countMap.set(turn.id, (countMap.get(turn.id) ?? 0) + 1);
- }
- }
-
- // turn-1: msg-1, msg-2 = 2 entries
- expect(countMap.get('turn-1')).toBe(2);
- // turn-2: msg-3, call-1 (with toolResult), msg-4 = 3 entries (tool_result skipped)
- expect(countMap.get('turn-2')).toBe(3);
- });
-});
-
-// ─── turnById Map correctness ────────────────────────────────────────────
-
-describe('turnById - O(1) lookup', () => {
- it('finds turn by id in Map', () => {
- const turns = [
- { id: 'turn-1', status: 'completed', items: [] },
- { id: 'turn-2', status: 'running', items: [] },
- { id: 'turn-3', status: 'error', items: [] },
- ];
-
- const turnById = new Map(turns.map((t) => [t.id, t]));
- expect(turnById.get('turn-2')?.status).toBe('running');
- expect(turnById.get('turn-99')).toBeUndefined();
- });
-});
-
-// ─── ApprovalPanel pendingKey stability ──────────────────────────────────
-
-describe('ApprovalPanel - pendingKey stability', () => {
- it('pendingKey only includes pending tool_call IDs', () => {
- const thread = {
- id: 't1',
- turns: [
- {
- id: 'turn-1',
- items: [
- { id: 'msg-1', type: 'message', role: 'user', content: 'hi' },
- { id: 'call-1', type: 'tool_call', name: 'edit', args: {}, status: 'approved' },
- { id: 'call-2', type: 'tool_call', name: 'read', args: {}, status: 'pending' },
- { id: 'msg-2', type: 'message', role: 'assistant', content: 'working...' },
- ],
- },
- ],
- };
-
- const pendingIds = thread.turns
- .flatMap((t: any) => t.items)
- .filter((i: any) => i.type === 'tool_call' && i.status === 'pending')
- .map((i: any) => i.id)
- .join(',');
-
- expect(pendingIds).toBe('call-2');
- });
-
- it('pendingKey is empty string when no pending items', () => {
- const thread = {
- id: 't1',
- turns: [
- {
- id: 'turn-1',
- items: [{ id: 'call-1', type: 'tool_call', name: 'edit', args: {}, status: 'approved' }],
- },
- ],
- };
-
- const pendingIds = thread.turns
- .flatMap((t: any) => t.items)
- .filter((i: any) => i.type === 'tool_call' && i.status === 'pending')
- .map((i: any) => i.id)
- .join(',');
-
- expect(pendingIds).toBe('');
- });
-});
-
-// ─── parseUnifiedDiff useMemo caching ────────────────────────────────────
-
-describe('parseUnifiedDiff - caching behavior', () => {
- it('parseUnifiedDiff returns consistent results for same input', () => {
- const diff = `diff --git a/foo.ts b/foo.ts
---- a/foo.ts
-+++ b/foo.ts
-@@ -1,3 +1,3 @@
- line1
--old
-+new
- line3`;
-
- const result1 = parseUnifiedDiff(diff);
- const result2 = parseUnifiedDiff(diff);
- expect(result1).toEqual(result2);
- expect(result1).toHaveLength(1);
- expect(result1[0].fileName).toBe('foo.ts');
- });
-});
-
-// ─── Virtuoso unified path ───────────────────────────────────────────────
-
-describe('Virtuoso unified path - no isLargeList threshold', () => {
- it('rendering path does not switch based on item count', () => {
- // The old code had: isLargeList = totalCount > 100
- // The new code always uses Virtuoso regardless of count
- // This test verifies the threshold is removed
- const totalCounts = [1, 50, 99, 100, 101, 500];
- for (const count of totalCounts) {
- // All counts should use the same rendering path (Virtuoso)
- // No isLargeList check needed
- expect(true).toBe(true); // Placeholder — actual behavior verified by typecheck
- }
- });
-});