Skip to content

Commit a897933

Browse files
committed
feat: 新增端到端烟雾测试脚本,增强项目连接和任务初始化验证
- 在 scripts 目录下新增 e2e-smoke.mjs 和 e2e-smoke.ts 文件,提供端到端烟雾测试功能 - 测试项目连接和任务初始化,确保文档正确落在连接的项目 .wave 目录下 - 更新现有测试用例,确保路径和面板内容的正确性 - 所有相关测试通过,确保代码质量和功能一致性
1 parent b4221a0 commit a897933

9 files changed

Lines changed: 785 additions & 48 deletions

scripts/e2e-smoke.mjs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import fs from 'fs-extra';
2+
import path from 'path';
3+
import { fileURLToPath } from 'url';
4+
import { ProjectManager } from '../dist/esm/core/project-manager.js';
5+
import { TaskManager } from '../dist/esm/core/task-manager.js';
6+
import { ConnectProjectTool } from '../dist/esm/tools/handshake-tools.js';
7+
import { CurrentTaskInitTool } from '../dist/esm/tools/task-tools.js';
8+
9+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
10+
11+
async function main() {
12+
const base = path.join(process.cwd(), 'test-temp', 'e2e-smoke-js');
13+
const projectDir = path.join(base, 'my-project');
14+
await fs.remove(base).catch(() => {});
15+
await fs.ensureDir(projectDir);
16+
17+
const pm = new ProjectManager();
18+
const tm = new TaskManager(path.join(projectDir, '.wave'), pm);
19+
20+
const connect = new ConnectProjectTool(pm, tm);
21+
const res = await connect.handle({ project_path: projectDir });
22+
const conn = JSON.parse(res.content[0].text);
23+
if (!conn.success || !conn.data.connected) {
24+
console.error('Connect failed:', conn);
25+
process.exit(1);
26+
}
27+
28+
const init = new CurrentTaskInitTool(tm);
29+
const initRes = await init.handle({
30+
title: 'E2E Smoke Task',
31+
goal: 'Ensure docs land under connected project .wave',
32+
overall_plan: [],
33+
knowledge_refs: [],
34+
});
35+
const initPayload = JSON.parse(initRes.content[0].text);
36+
if (!initPayload.success) {
37+
console.error('Init task failed:', initPayload);
38+
process.exit(1);
39+
}
40+
41+
const waveDir = path.join(projectDir, '.wave');
42+
const jsonPath = path.join(waveDir, 'current-task.json');
43+
const mdPath = path.join(waveDir, 'current-task.md');
44+
45+
const exists = {
46+
waveDir: await fs.pathExists(waveDir),
47+
currentTaskJson: await fs.pathExists(jsonPath),
48+
currentTaskMd: await fs.pathExists(mdPath),
49+
};
50+
51+
const tasksDir = path.join(waveDir, 'tasks');
52+
const hasTasksDir = await fs.pathExists(tasksDir);
53+
54+
console.log(JSON.stringify({ projectDir, waveDir, exists, hasTasksDir }, null, 2));
55+
}
56+
57+
main().catch((e) => {
58+
console.error('E2E smoke failed:', e);
59+
process.exit(1);
60+
});
61+

scripts/e2e-smoke.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import * as fs from 'fs-extra';
2+
import * as path from 'path';
3+
import { ProjectManager } from '../src/core/project-manager.js';
4+
import { TaskManager } from '../src/core/task-manager.js';
5+
import { ConnectProjectTool } from '../src/tools/handshake-tools.js';
6+
import { CurrentTaskInitTool } from '../src/tools/task-tools.js';
7+
8+
async function main() {
9+
const base = path.join(process.cwd(), 'test-temp', 'e2e-smoke');
10+
const projectDir = path.join(base, 'my-project');
11+
await fs.remove(base).catch(() => {});
12+
await fs.ensureDir(projectDir);
13+
14+
const projectManager = new ProjectManager();
15+
const taskManager = new TaskManager(path.join(projectDir, '.wave'), projectManager);
16+
17+
const connect = new ConnectProjectTool(projectManager, taskManager);
18+
const res = await connect.handle({ project_path: projectDir });
19+
const conn = JSON.parse(res.content[0].text);
20+
if (!conn.success || !conn.data.connected) {
21+
console.error('Connect failed:', conn);
22+
process.exit(1);
23+
}
24+
25+
const init = new CurrentTaskInitTool(taskManager);
26+
const initRes = await init.handle({
27+
title: 'E2E Smoke Task',
28+
goal: 'Ensure docs land under connected project .wave',
29+
overall_plan: [],
30+
knowledge_refs: [],
31+
});
32+
const initPayload = JSON.parse(initRes.content[0].text);
33+
if (!initPayload.success) {
34+
console.error('Init task failed:', initPayload);
35+
process.exit(1);
36+
}
37+
38+
const waveDir = path.join(projectDir, '.wave');
39+
const jsonPath = path.join(waveDir, 'current-task.json');
40+
const mdPath = path.join(waveDir, 'current-task.md');
41+
42+
const exists = {
43+
waveDir: await fs.pathExists(waveDir),
44+
currentTaskJson: await fs.pathExists(jsonPath),
45+
currentTaskMd: await fs.pathExists(mdPath),
46+
};
47+
48+
// Also verify multi-task directory
49+
const tasksDir = path.join(waveDir, 'tasks');
50+
const hasTasksDir = await fs.pathExists(tasksDir);
51+
let taskSubDirCount = 0;
52+
if (hasTasksDir) {
53+
const years = (await fs.readdir(tasksDir)).filter(async (y) => (await fs.stat(path.join(tasksDir, y))).isDirectory());
54+
// Best-effort: count nested directories
55+
taskSubDirCount = (await fs.readdir(tasksDir)).length;
56+
}
57+
58+
console.log(JSON.stringify({
59+
projectDir,
60+
waveDir,
61+
exists,
62+
hasTasksDir,
63+
taskSubDirCount,
64+
}, null, 2));
65+
}
66+
67+
main().catch((e) => {
68+
console.error('E2E smoke failed:', e);
69+
process.exit(1);
70+
});
71+

src/core/project-manager.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,30 @@ export class ProjectManager {
110110

111111
try {
112112
// 验证项目是否仍然有效
113-
const projectRecord = await this.projectRegistry.resolveProject(
113+
let projectRecord = await this.projectRegistry.resolveProject(
114114
this.activeBinding.project_id
115115
);
116116

117+
// 当全局注册表不可用或未持久化时,回退到本地 .wave/project.json 验证
118+
if (!projectRecord && this.activeBinding.root) {
119+
try {
120+
const localInfo = await this.projectRegistry.loadByPath(
121+
this.activeBinding.root
122+
);
123+
if (localInfo && localInfo.id === this.activeBinding.project_id) {
124+
projectRecord = {
125+
id: localInfo.id,
126+
root: this.activeBinding.root,
127+
slug: localInfo.slug,
128+
origin: localInfo.origin,
129+
last_seen: new Date().toISOString(),
130+
};
131+
}
132+
} catch {
133+
// ignore and keep projectRecord as null
134+
}
135+
}
136+
117137
if (!projectRecord) {
118138
// 项目已失效,清除绑定
119139
this.activeBinding = null;

src/core/project-registry.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,19 @@ export class ProjectRegistry {
213213
error: error instanceof Error ? error.message : String(error),
214214
});
215215

216-
// 如果是超时错误,不抛出异常,只记录警告
217-
if (error instanceof Error && error.message.includes('超时')) {
216+
// 以下错误视为非致命:超时、权限不足、只读文件系统
217+
const msg = error instanceof Error ? error.message : String(error);
218+
if (
219+
msg.includes('超时') ||
220+
msg.includes('EPERM') ||
221+
msg.includes('EACCES') ||
222+
msg.toLowerCase().includes('permission') ||
223+
msg.toLowerCase().includes('read-only')
224+
) {
218225
logger.warning(
219226
LogCategory.Task,
220227
LogAction.Update,
221-
'全局注册表更新超时,跳过此操作',
228+
'全局注册表不可写,跳过更新(不影响主要功能)',
222229
{
223230
projectId: record.id,
224231
}

src/core/task-manager.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,14 @@ export class TaskManager {
206206
*/
207207
getCurrentTaskPanelPath(): string | null {
208208
try {
209-
// current-task.md 应该在 .wave 目录下
210-
const panelPath = path.join(this.docsPath, 'current-task.md');
209+
// 如果有活跃项目,优先使用该项目的 .wave 目录
210+
const active = this.projectManager?.getActiveProject();
211+
if (active?.root) {
212+
return path.join(active.root, '.wave', 'current-task.md');
213+
}
211214

212-
// 不检查文件是否存在,直接返回路径
213-
// 这样在同步检测时可以正确找到路径
214-
return panelPath;
215+
// 回退到初始化时的 docsPath
216+
return path.join(this.docsPath, 'current-task.md');
215217
} catch (error) {
216218
return null;
217219
}
@@ -286,7 +288,8 @@ export class TaskManager {
286288
if (!effectiveProjectId) {
287289
const activeProject = this.projectManager.getActiveProject();
288290
if (activeProject) {
289-
effectiveProjectId = activeProject.root;
291+
// 使用项目ID而不是root路径以兼容 resolveProject 要求
292+
effectiveProjectId = activeProject.project_id;
290293
} else {
291294
// 没有活动项目,使用默认路径
292295
return this.docsPath;
@@ -456,7 +459,8 @@ export class TaskManager {
456459
expectedResults: task.expectedResults || [],
457460
createdAt: task.created_at,
458461
updatedAt: task.updated_at,
459-
projectId: this.projectManager?.getActiveProject()?.root || '',
462+
// 使用活动项目的ID作为 projectId,避免与路径混淆
463+
projectId: this.projectManager?.getActiveProject()?.project_id || '',
460464
panelModTime, // 传递面板修改时间
461465
};
462466

@@ -809,7 +813,7 @@ export class TaskManager {
809813

810814
// 如果没有传入 projectId,尝试从当前活动项目获取
811815
const effectiveProjectId =
812-
projectId || this.projectManager?.getActiveProject()?.root;
816+
projectId || this.projectManager?.getActiveProject()?.project_id;
813817

814818
const taskPath = await this.resolveTaskPath(effectiveProjectId);
815819

@@ -1168,19 +1172,19 @@ export class TaskManager {
11681172

11691173
// 同时保存到多任务目录结构
11701174
try {
1175+
// 使用当前项目的 .wave 路径初始化目录管理器,避免写入服务器启动目录
1176+
const docsPathForProject = await this.resolveProjectPath(projectId);
1177+
const projectDirManager = new MultiTaskDirectoryManager(docsPathForProject);
1178+
11711179
// 检查任务是否已经存在于多任务目录中
1172-
const existingTaskDir =
1173-
await this.multiTaskDirectoryManager.findTaskDirectory(task.id);
1180+
const existingTaskDir = await projectDirManager.findTaskDirectory(task.id);
11741181

11751182
if (existingTaskDir) {
11761183
// 更新现有任务
1177-
await this.multiTaskDirectoryManager.updateTaskInDirectory(
1178-
task,
1179-
existingTaskDir
1180-
);
1184+
await projectDirManager.updateTaskInDirectory(task, existingTaskDir);
11811185
} else {
11821186
// 创建新的任务目录
1183-
await this.multiTaskDirectoryManager.saveTaskToDirectory(task);
1187+
await projectDirManager.saveTaskToDirectory(task);
11841188
}
11851189
} catch (error) {
11861190
// 多任务目录保存失败不应该影响主要功能
@@ -1787,7 +1791,18 @@ export class TaskManager {
17871791
*/
17881792
async getTaskHistory(): Promise<any[]> {
17891793
try {
1790-
const historyDir = path.join(this.docsPath, 'history');
1794+
// 优先使用当前绑定项目的 .wave 路径
1795+
let activeProjectId: string | undefined;
1796+
try {
1797+
// 访问活跃项目(存在则使用其 project_id)
1798+
const active = this.projectManager?.getActiveProject();
1799+
if (active?.project_id) {
1800+
activeProjectId = active.project_id;
1801+
}
1802+
} catch {}
1803+
1804+
const docsPath = await this.resolveProjectPath(activeProjectId);
1805+
const historyDir = path.join(docsPath, 'history');
17911806

17921807
if (!(await fs.pathExists(historyDir))) {
17931808
return [];

src/tests/deep-e2e-test.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ describe('深度端到端测试 - 设计文档符合性验证', () => {
5959

6060
// 辅助函数:读取并显示面板内容
6161
async function readAndLogPanel(stepName: string): Promise<string> {
62-
const panelPath = path.join(tempDir, '.wave', 'current-task.md');
62+
// 文档现按绑定项目落在 testProjectPath/.wave 下
63+
const panelPath = path.join(testProjectPath, '.wave', 'current-task.md');
6364
if (await fs.pathExists(panelPath)) {
6465
const content = await fs.readFile(panelPath, 'utf-8');
6566
console.log(`\n========== ${stepName} - current-task.md 内容 ==========`);
@@ -73,7 +74,7 @@ describe('深度端到端测试 - 设计文档符合性验证', () => {
7374

7475
// 辅助函数:手动编辑面板
7576
async function editPanel(modifier: (content: string) => string): Promise<void> {
76-
const panelPath = path.join(tempDir, '.wave', 'current-task.md');
77+
const panelPath = path.join(testProjectPath, '.wave', 'current-task.md');
7778
const content = await fs.readFile(panelPath, 'utf-8');
7879
const modified = modifier(content);
7980
await fs.writeFile(panelPath, modified, 'utf-8');

0 commit comments

Comments
 (0)