@baique/pi-web-sky 0.1.56 → 0.1.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +5 -5
- package/.next/build-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +2 -2
- package/.next/required-server-files.js +3 -2
- package/.next/required-server-files.json +3 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/running/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/board-assets/[...path]/route.js +1 -1
- package/.next/server/app/api/board-assets/route.js +1 -1
- package/.next/server/app/api/boards/[id]/add-session/route.js +1 -1
- package/.next/server/app/api/boards/[id]/reconcile/route.js +1 -1
- package/.next/server/app/api/boards/[id]/route.js +1 -1
- package/.next/server/app/api/boards/reorder/route.js +1 -1
- package/.next/server/app/api/boards/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +1 -1
- package/.next/server/app/api/file-index/route.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +2 -2
- package/.next/server/app/api/mcp/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/search/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/export/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/minimap/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/summary/route.js +1 -1
- package/.next/server/app/api/skills/install/route.js +1 -1
- package/.next/server/app/api/subagents/[id]/route.js +1 -1
- package/.next/server/app/api/subagents/profiles/route.js +1 -1
- package/.next/server/app/api/task-card-questions/[id]/answer/route.js +1 -1
- package/.next/server/app/api/task-card-questions/route.js +1 -1
- package/.next/server/app/api/task-cards/[id]/route.js +1 -1
- package/.next/server/app/api/task-cards/route.js +1 -1
- package/.next/server/app/api/task-cards/unbind/route.js +1 -1
- package/.next/server/app/api/task-scheduler/status/route.js +1 -1
- package/.next/server/app/api/tasks/[id]/assign-session/route.js +2 -12
- package/.next/server/app/api/tasks/[id]/board/route.js +1 -1
- package/.next/server/app/api/tasks/[id]/route.js +1 -1
- package/.next/server/app/api/tasks/reorder/route.js +2 -12
- package/.next/server/app/api/tasks/route.js +2 -12
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +10 -10
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +5 -5
- package/.next/server/chunks/1937.js +14 -13
- package/.next/server/chunks/2524.js +2 -2
- package/.next/server/chunks/3500.js +1 -1
- package/.next/server/chunks/402.js +21 -10
- package/.next/server/chunks/4457.js +3 -13
- package/.next/server/chunks/542.js +8 -8
- package/.next/server/chunks/5794.js +1 -1
- package/.next/server/chunks/5940.js +1 -1
- package/.next/server/chunks/6429.js +4 -4
- package/.next/server/chunks/6525.js +18 -7
- package/.next/server/chunks/6583.js +12 -11
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/752.bafff304e6b00987.js +1 -0
- package/.next/static/chunks/8088.39866dc5f1d15868.js +1 -0
- package/.next/static/chunks/app/{layout-87087b955e0c9507.js → layout-fae2b226b3c78448.js} +1 -1
- package/.next/static/chunks/app/page-d5af3bd535589daf.js +256 -0
- package/.next/static/chunks/{webpack-4de8e8dd9f2f3173.js → webpack-1ce35e1249cfd931.js} +1 -1
- package/.next/static/css/{c6d32b6031181be7.css → c7d127e07b1a786c.css} +1 -1
- package/.next/trace +8 -7
- package/.next/trace-build +1 -1
- package/lib/board-reconcile.test.mjs +26 -8
- package/lib/board-reconcile.ts +3 -1
- package/lib/board-store.test.mjs +2 -2
- package/lib/ime-guard.test.mjs +52 -0
- package/lib/ime-guard.ts +54 -0
- package/lib/rpc-manager.activity.test.mjs +223 -0
- package/lib/rpc-manager.test.mjs +22 -0
- package/lib/rpc-manager.title.test.mjs +152 -0
- package/lib/rpc-manager.ts +95 -8
- package/lib/session-activity.test.mjs +123 -0
- package/lib/session-activity.ts +77 -0
- package/lib/session-delete.test.mjs +45 -41
- package/lib/session-delete.ts +8 -34
- package/lib/session-index-scanner.test.mjs +364 -2
- package/lib/session-index-scanner.ts +162 -57
- package/lib/session-reader.noscan.test.mjs +371 -0
- package/lib/session-reader.project.test.mjs +30 -52
- package/lib/session-reader.test.mjs +2 -65
- package/lib/session-reader.ts +177 -213
- package/lib/session-scanner.test.mjs +151 -38
- package/lib/session-scanner.ts +140 -61
- package/lib/session-search.ts +6 -5
- package/lib/sqlite-db.test.mjs +26 -0
- package/lib/sqlite-db.ts +10 -1
- package/lib/subagent-runtime.ts +42 -0
- package/lib/subagent-session-title.test.mjs +120 -0
- package/lib/task-scheduler.test.mjs +27 -0
- package/lib/task-scheduler.ts +8 -1
- package/lib/task-store.activity.test.mjs +141 -0
- package/lib/task-store.meta-row.test.mjs +25 -5
- package/lib/task-store.partial-row.test.mjs +198 -0
- package/lib/task-store.subtree.test.mjs +172 -0
- package/lib/task-store.test.mjs +46 -20
- package/lib/task-store.ts +344 -68
- package/next.config.ts +3 -1
- package/package.json +1 -1
- package/.next/static/chunks/752.adce8bc3ebbaf4b6.js +0 -1
- package/.next/static/chunks/8088.0cad1ff2e09fd8ff.js +0 -1
- package/.next/static/chunks/app/page-efca6bf51a5f8ccb.js +0 -256
- /package/.next/static/{k8ZFqwz7d-bnK0ZiUVgCc → MPBMmQqmDG8zo8tM_bkDy}/_buildManifest.js +0 -0
- /package/.next/static/{k8ZFqwz7d-bnK0ZiUVgCc → MPBMmQqmDG8zo8tM_bkDy}/_ssgManifest.js +0 -0
- /package/lib/{subagent-isolation.test.mjs → worktree-isolation.test.mjs} +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import test, { after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { createJiti } from "jiti";
|
|
8
|
+
|
|
9
|
+
// getAgentDir() 每次调用都读环境变量 → 指向临时树,测试绝不碰 ~/.pi/agent。
|
|
10
|
+
// sessions 子目录先建好:无行会话补行时会按 id 在这里找文件,miss 不回落扫开发机真实目录。
|
|
11
|
+
const agentDir = mkdtempSync(join(tmpdir(), "pi-web-subtree-"));
|
|
12
|
+
mkdirSync(join(agentDir, "sessions"), { recursive: true });
|
|
13
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
14
|
+
after(() => rmSync(agentDir, { recursive: true, force: true }));
|
|
15
|
+
|
|
16
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false, alias: { "@": process.cwd() } });
|
|
17
|
+
const { setDbForTesting } = await jiti.import("./sqlite-db.ts");
|
|
18
|
+
const store = await jiti.import("./task-store.ts");
|
|
19
|
+
|
|
20
|
+
function freshDb() {
|
|
21
|
+
const db = new DatabaseSync(":memory:");
|
|
22
|
+
setDbForTesting(db);
|
|
23
|
+
return db;
|
|
24
|
+
}
|
|
25
|
+
// 造一个「任务 T + 根 A + 子树 A<-B<-C」的库内结构
|
|
26
|
+
function seedTree(db) {
|
|
27
|
+
const task = store.createTask("/p", "T");
|
|
28
|
+
const row = (id, taskId, parentId) => db.prepare(
|
|
29
|
+
"INSERT INTO session_meta (session_id, task_id, updated, pinned, path, cwd, project_key, created, modified, parent_id) VALUES (?,?,?,0,?,?,?,?,?,?)",
|
|
30
|
+
).run(id, taskId, Date.now(), `/s/${id}.jsonl`, "/p", "/p", Date.now(), Date.now(), parentId);
|
|
31
|
+
row("A", null, null); row("B", null, "A"); row("C", null, "B");
|
|
32
|
+
return task;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("listDescendantIds 按 parent_id 递归(不含自身,含多层)", () => {
|
|
36
|
+
const db = freshDb(); seedTree(db);
|
|
37
|
+
assert.deepEqual(store.listDescendantIds("A").sort(), ["B", "C"]);
|
|
38
|
+
assert.deepEqual(store.listDescendantIds("C"), []);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("agent/new 之外的归属:assignSessionSubtreeToTask 连带子树", async () => {
|
|
42
|
+
const db = freshDb(); const task = seedTree(db);
|
|
43
|
+
assert.equal(await store.assignSessionSubtreeToTask("A", task.id), true);
|
|
44
|
+
const ids = db.prepare("SELECT session_id, task_id FROM session_meta ORDER BY session_id").all();
|
|
45
|
+
assert.deepEqual(ids.map((r) => [r.session_id, r.task_id]), [["A", task.id], ["B", task.id], ["C", task.id]]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("归属守卫:祖先属于别的任务 → 拒绝且不写任何行(子树/单节点入口一致)", async () => {
|
|
49
|
+
const db = freshDb(); const task = seedTree(db);
|
|
50
|
+
const other = store.createTask("/p", "Other");
|
|
51
|
+
// A 属于另一个任务 → C 的祖先链(C←B←A)上有非空且 ≠ 目标任务的 task_id
|
|
52
|
+
db.prepare("UPDATE session_meta SET task_id = ? WHERE session_id = 'A'").run(other.id);
|
|
53
|
+
const snapshot = () => db.prepare("SELECT session_id, task_id, updated FROM session_meta ORDER BY session_id").all()
|
|
54
|
+
.map((r) => [r.session_id, r.task_id, r.updated]);
|
|
55
|
+
const before = snapshot();
|
|
56
|
+
const errors = [];
|
|
57
|
+
const original = console.error;
|
|
58
|
+
console.error = (...args) => errors.push(args.map(String).join(" "));
|
|
59
|
+
try {
|
|
60
|
+
assert.equal(await store.assignSessionSubtreeToTask("C", task.id), false, "子树入口拒绝");
|
|
61
|
+
assert.equal(await store.assignSessionToTask("C", task.id), false, "单节点入口拒绝");
|
|
62
|
+
} finally {
|
|
63
|
+
console.error = original;
|
|
64
|
+
}
|
|
65
|
+
assert.deepEqual(snapshot(), before, "不写任何行(归属与 updated 都不变)");
|
|
66
|
+
assert.equal(errors.length, 2, "拒绝必须 console.error 可见");
|
|
67
|
+
assert.deepEqual(
|
|
68
|
+
errors.map((e) => (/子树归属被拒绝/.test(e) ? "subtree" : "single")),
|
|
69
|
+
["subtree", "single"],
|
|
70
|
+
"子树入口与单节点入口各自给出可见日志",
|
|
71
|
+
);
|
|
72
|
+
// 悬空祖先(行已不在库)不算冲突:C 就是根
|
|
73
|
+
db.prepare("DELETE FROM session_meta WHERE session_id = 'A'").run();
|
|
74
|
+
assert.equal(await store.assignSessionSubtreeToTask("C", task.id), true, "祖先行已不存在 → 允许(C 是根)");
|
|
75
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("归属守卫:祖先只是聊天区会话(task_id NULL)→ 正常归属", async () => {
|
|
79
|
+
const db = freshDb(); const task = seedTree(db);
|
|
80
|
+
assert.equal(await store.assignSessionSubtreeToTask("C", task.id), true, "聊天区祖先不拦");
|
|
81
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
82
|
+
assert.equal(store.taskForSession("A"), null, "只归属自身 + 子树,祖先不动");
|
|
83
|
+
assert.equal(store.taskForSession("B"), null);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("归属守卫:祖先就在同一任务 → 正常(幂等,不误拦)", async () => {
|
|
87
|
+
const db = freshDb(); const task = seedTree(db);
|
|
88
|
+
db.prepare("UPDATE session_meta SET task_id = ? WHERE session_id IN ('A','B')").run(task.id);
|
|
89
|
+
assert.equal(await store.assignSessionSubtreeToTask("C", task.id), true, "祖先同任务");
|
|
90
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
91
|
+
assert.equal(await store.assignSessionToTask("B", task.id), true, "重复归属幂等");
|
|
92
|
+
assert.equal(store.taskForSession("B"), task.id);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("reparentSessionChildren:子行 parent_id 同请求改写为祖父(或 NULL)", () => {
|
|
96
|
+
const db = freshDb(); seedTree(db);
|
|
97
|
+
store.reparentSessionChildren("B", "A");
|
|
98
|
+
assert.equal(db.prepare("SELECT parent_id FROM session_meta WHERE session_id='C'").get().parent_id, "A", "子行接到祖父");
|
|
99
|
+
store.reparentSessionChildren("A", null);
|
|
100
|
+
assert.equal(db.prepare("SELECT parent_id FROM session_meta WHERE session_id='B'").get().parent_id, null, "祖父拿不到 → NULL");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("updateTask 的 sessionIds 全量替换按子树闭包展开", () => {
|
|
104
|
+
const db = freshDb(); const task = seedTree(db);
|
|
105
|
+
store.updateTask(task.id, { sessionIds: ["A"] }); // 只给根,服务端要连带子树
|
|
106
|
+
assert.equal(store.taskForSession("B"), task.id);
|
|
107
|
+
store.updateTask(task.id, { sessionIds: [] }); // 清空 → 整棵子树回到聊天区
|
|
108
|
+
assert.equal(store.taskForSession("A"), null);
|
|
109
|
+
assert.equal(store.taskForSession("C"), null);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("移出带子树的父会话:前端提交剩余成员列表(含子 id)也整树离开", () => {
|
|
113
|
+
const db = freshDb(); const task = seedTree(db);
|
|
114
|
+
store.updateTask(task.id, { sessionIds: ["A"] }); // A/B/C 全在任务
|
|
115
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
116
|
+
// 前端拖出父 A 时提交的是「剩余成员列表」(flat sessionIds 去掉 A,仍含子 id)
|
|
117
|
+
store.updateTask(task.id, { sessionIds: ["B", "C"] });
|
|
118
|
+
assert.equal(store.taskForSession("A"), null, "父离开任务");
|
|
119
|
+
assert.equal(store.taskForSession("B"), null, "B 的祖先 A 不在成员集合里 → B 一起离开");
|
|
120
|
+
assert.equal(store.taskForSession("C"), null, "C 跟随 B 一起离开(多层)");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("祖先行已不存在(断链)时不剪:子会话仍可留在任务里当根", async () => {
|
|
124
|
+
const db = freshDb(); const task = seedTree(db);
|
|
125
|
+
await store.assignSessionSubtreeToTask("A", task.id);
|
|
126
|
+
// A 已被删(B 的 parent_id 悬空)——此窗口内不得把 B/C 踢出任务(它们就是本任务的根)
|
|
127
|
+
db.prepare("DELETE FROM session_meta WHERE session_id = 'A'").run();
|
|
128
|
+
store.updateTask(task.id, { sessionIds: ["B", "C"] });
|
|
129
|
+
assert.equal(store.taskForSession("B"), task.id);
|
|
130
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("子会话不能单独移出:祖先仍是成员时子会话保持在任务内", () => {
|
|
134
|
+
const db = freshDb(); const task = seedTree(db);
|
|
135
|
+
store.updateTask(task.id, { sessionIds: ["A"] }); // A/B/C 全在任务
|
|
136
|
+
// 前端拖出子会话 B 时提交的是 「flat 剩余成员列表」= filter(≠B) = ["A", "C"](含祖先 A),
|
|
137
|
+
// 闭包再把 B 纳入 → 成员集合不变。这是有意的不变量:子会话不能被单独移出任务。
|
|
138
|
+
store.updateTask(task.id, { sessionIds: ["A", "C"] });
|
|
139
|
+
assert.equal(store.taskForSession("A"), task.id, "祖先仍在任务");
|
|
140
|
+
assert.equal(store.taskForSession("B"), task.id, "B 不能单飞(否则聊天区出现孤儿根)");
|
|
141
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("ensureSessionMetaRow 传 taskId 时落归属,不传时保持 NULL", () => {
|
|
145
|
+
freshDb();
|
|
146
|
+
store.ensureSessionMetaRow("X", { path: "/s/X.jsonl", cwd: "/p", projectKey: "/p", taskId: "T1" });
|
|
147
|
+
assert.equal(store.taskForSession("X"), "T1");
|
|
148
|
+
store.ensureSessionMetaRow("Y", { path: "/s/Y.jsonl", cwd: "/p", projectKey: "/p" });
|
|
149
|
+
assert.equal(store.taskForSession("Y"), null);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("ensureSessionMetaRow 冲突路径不覆盖已有归属(第二次传别的 taskId 仍保留首个)", () => {
|
|
153
|
+
const db = freshDb();
|
|
154
|
+
store.ensureSessionMetaRow("X", { path: "/s/X.jsonl", cwd: "/p", projectKey: "/p", taskId: "T1" });
|
|
155
|
+
store.ensureSessionMetaRow("X", { path: "/s/X2.jsonl", cwd: "/p2", projectKey: "/p2", taskId: "T2" });
|
|
156
|
+
assert.equal(store.taskForSession("X"), "T1", "归属各自管:upsert 不覆盖");
|
|
157
|
+
assert.equal(
|
|
158
|
+
db.prepare("SELECT path FROM session_meta WHERE session_id = 'X'").get().path,
|
|
159
|
+
"/s/X2.jsonl",
|
|
160
|
+
"索引列照常刷新",
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("ensureSessionMetaRow:不传 firstMessage 的 upsert 不擦已有首条消息(COALESCE)", () => {
|
|
165
|
+
const db = freshDb();
|
|
166
|
+
store.ensureSessionMetaRow("F", { path: "/s/F.jsonl", cwd: "/p", projectKey: "/p", firstMessage: "首条消息" });
|
|
167
|
+
// 同 id 再 upsert(如 fork/重新落盘)但不带 firstMessage:旧写法会把首条消息擦成 NULL
|
|
168
|
+
store.ensureSessionMetaRow("F", { path: "/s/F2.jsonl", cwd: "/p", projectKey: "/p" });
|
|
169
|
+
const row = db.prepare("SELECT first_message, path FROM session_meta WHERE session_id = 'F'").get();
|
|
170
|
+
assert.equal(row.first_message, "首条消息", "已有首条消息保留");
|
|
171
|
+
assert.equal(row.path, "/s/F2.jsonl", "索引列照常刷新");
|
|
172
|
+
});
|
package/lib/task-store.test.mjs
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test, { after } from "node:test";
|
|
3
6
|
import { DatabaseSync } from "node:sqlite";
|
|
4
7
|
import { createJiti } from "jiti";
|
|
5
8
|
|
|
9
|
+
// getAgentDir() 每次调用都读环境变量 → 指向临时树,测试绝不碰 ~/.pi/agent。
|
|
10
|
+
// sessions 子目录先建好:无行会话补行时会按 id 在这里找文件,miss 不回落扫开发机真实目录。
|
|
11
|
+
const agentDir = mkdtempSync(join(tmpdir(), "pi-web-task-store-"));
|
|
12
|
+
mkdirSync(join(agentDir, "sessions"), { recursive: true });
|
|
13
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
14
|
+
after(() => rmSync(agentDir, { recursive: true, force: true }));
|
|
15
|
+
|
|
6
16
|
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
7
17
|
const { getDb, setDbForTesting } = await jiti.import("./sqlite-db.ts");
|
|
8
18
|
const { createTask, deleteTask, listTasks, updateTask, taskForSession, assignSessionToTask, reorderTasks, setSessionTitle } = await jiti.import("./task-store.ts");
|
|
@@ -112,26 +122,26 @@ test("updateTask returns null for unknown id and rejects empty rename", () => {
|
|
|
112
122
|
assert.throws(() => updateTask(a.id, { name: " " }), /name must not be empty/);
|
|
113
123
|
});
|
|
114
124
|
|
|
115
|
-
test("assignSessionToTask attaches a session atomically and bumps the task's updated", () => {
|
|
125
|
+
test("assignSessionToTask attaches a session atomically and bumps the task's updated", async () => {
|
|
116
126
|
freshDb();
|
|
117
127
|
const a = createTask("p", "A");
|
|
118
128
|
seedSession("s1", null); // temp
|
|
119
129
|
seedSession("s2", a.id); // already a member
|
|
120
130
|
|
|
121
|
-
assert.equal(assignSessionToTask("s1", a.id), true);
|
|
131
|
+
assert.equal(await assignSessionToTask("s1", a.id), true);
|
|
122
132
|
assert.equal(taskForSession("s1"), a.id);
|
|
123
133
|
// 重复归属幂等:已是成员再归一次仍成功且不重复。
|
|
124
|
-
assert.equal(assignSessionToTask("s2", a.id), true);
|
|
134
|
+
assert.equal(await assignSessionToTask("s2", a.id), true);
|
|
125
135
|
|
|
126
136
|
const task = listTasks("p")[0];
|
|
127
137
|
assert.deepEqual([...task.sessionIds].sort(), ["s1", "s2"]);
|
|
128
138
|
assert.ok(task.updated >= task.created, "归属会刷新任务的 updated(排序用)");
|
|
129
139
|
});
|
|
130
140
|
|
|
131
|
-
test("assignSessionToTask returns false for a missing task without writing anything", () => {
|
|
141
|
+
test("assignSessionToTask returns false for a missing task without writing anything", async () => {
|
|
132
142
|
freshDb();
|
|
133
143
|
seedSession("s1", null);
|
|
134
|
-
assert.equal(assignSessionToTask("s1", "no-such-task"), false);
|
|
144
|
+
assert.equal(await assignSessionToTask("s1", "no-such-task"), false);
|
|
135
145
|
assert.equal(taskForSession("s1"), null);
|
|
136
146
|
assert.equal(listTasks("p").length, 0);
|
|
137
147
|
});
|
|
@@ -177,18 +187,38 @@ test("updateTask supports sortOrder patch", () => {
|
|
|
177
187
|
assert.equal(b.sortOrder < 0, true);
|
|
178
188
|
});
|
|
179
189
|
|
|
180
|
-
test("assignSessionToTask moves a session out of its previous task", () => {
|
|
190
|
+
test("assignSessionToTask moves a session out of its previous task", async () => {
|
|
181
191
|
freshDb();
|
|
182
192
|
const a = createTask("p", "A");
|
|
183
193
|
const b = createTask("p", "B");
|
|
184
194
|
seedSession("s1", null);
|
|
185
|
-
assignSessionToTask("s1", a.id);
|
|
186
|
-
assignSessionToTask("s1", b.id); // 移动到 B
|
|
195
|
+
await assignSessionToTask("s1", a.id);
|
|
196
|
+
await assignSessionToTask("s1", b.id); // 移动到 B
|
|
187
197
|
assert.equal(taskForSession("s1"), b.id);
|
|
188
198
|
const taskA = listTasks("p").find((t) => t.id === a.id);
|
|
189
199
|
assert.deepEqual(taskA.sessionIds, []);
|
|
190
200
|
});
|
|
191
201
|
|
|
202
|
+
test("updateTask 不再为无行会话插缺列局部行(加侧只 UPDATE)", () => {
|
|
203
|
+
freshDb();
|
|
204
|
+
const a = createTask("p", "A");
|
|
205
|
+
updateTask(a.id, { sessionIds: ["ghost-no-row"] });
|
|
206
|
+
// 行由路由层 updateTask 之前调 ensureSessionRows 建(那里才能 await 文件 I/O)。
|
|
207
|
+
assert.equal(
|
|
208
|
+
getDb().prepare("SELECT COUNT(*) c FROM session_meta WHERE session_id='ghost-no-row'").get().c,
|
|
209
|
+
0,
|
|
210
|
+
);
|
|
211
|
+
assert.deepEqual(listTasks("p")[0].sessionIds, []);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("updateTask 对已有行的成员照常写归属(加侧 UPDATE 不丢写)", () => {
|
|
215
|
+
freshDb();
|
|
216
|
+
const a = createTask("p", "A");
|
|
217
|
+
seedSession("s1", null);
|
|
218
|
+
updateTask(a.id, { sessionIds: ["s1"] });
|
|
219
|
+
assert.equal(taskForSession("s1"), a.id);
|
|
220
|
+
});
|
|
221
|
+
|
|
192
222
|
test("deleteTask cascades to its task board (nodes/edges/view gone)", () => {
|
|
193
223
|
freshDb();
|
|
194
224
|
const a = createTask("p", "A");
|
|
@@ -218,21 +248,17 @@ test("updateTask rename syncs task board name", () => {
|
|
|
218
248
|
updateTask(b.id, { name: "B2" });
|
|
219
249
|
});
|
|
220
250
|
|
|
221
|
-
test("setSessionTitle
|
|
251
|
+
test("setSessionTitle:已有行只改名(不覆盖归属/索引列)", async () => {
|
|
222
252
|
freshDb();
|
|
223
253
|
// 场景1:已有归属行(任务会话),改名写 title 保留 task_id
|
|
224
254
|
seedSession("s-task", "task-1");
|
|
225
|
-
setSessionTitle("s-task", "新名字");
|
|
255
|
+
await setSessionTitle("s-task", "新名字");
|
|
226
256
|
let row = getDb().prepare("SELECT title, task_id FROM session_meta WHERE session_id='s-task'").get();
|
|
227
257
|
assert.equal(row.title, "新名字");
|
|
228
258
|
assert.equal(row.task_id, "task-1", "不覆盖已有归属");
|
|
229
|
-
// 场景2
|
|
230
|
-
setSessionTitle("s-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
assert.equal(
|
|
234
|
-
assert.equal(row.pinned, 0);
|
|
235
|
-
// 场景3:再次改名覆盖
|
|
236
|
-
setSessionTitle("s-orphan", "再改");
|
|
237
|
-
assert.equal(getDb().prepare("SELECT title FROM session_meta WHERE session_id='s-orphan'").get().title, "再改");
|
|
259
|
+
// 场景2:再次改名覆盖
|
|
260
|
+
await setSessionTitle("s-task", "再改");
|
|
261
|
+
assert.equal(getDb().prepare("SELECT title FROM session_meta WHERE session_id='s-task'").get().title, "再改");
|
|
262
|
+
// 场景3(无行 + 磁盘无文件 → 不写缺列局部行)在 task-store.partial-row.test.mjs 覆盖。
|
|
263
|
+
assert.equal(getDb().prepare("SELECT COUNT(*) c FROM session_meta").get().c, 1);
|
|
238
264
|
});
|