@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.
Files changed (138) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +5 -5
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/react-loadable-manifest.json +2 -2
  5. package/.next/required-server-files.js +3 -2
  6. package/.next/required-server-files.json +3 -2
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +1 -1
  15. package/.next/server/app/_not-found.rsc +3 -3
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  19. package/.next/server/app/api/agent/[id]/bash-output/route.js +2 -2
  20. package/.next/server/app/api/agent/[id]/events/route.js +1 -1
  21. package/.next/server/app/api/agent/[id]/route.js +1 -1
  22. package/.next/server/app/api/agent/new/route.js +1 -1
  23. package/.next/server/app/api/agent/running/route.js +1 -1
  24. package/.next/server/app/api/app-update/route.js +1 -1
  25. package/.next/server/app/api/board-assets/[...path]/route.js +1 -1
  26. package/.next/server/app/api/board-assets/route.js +1 -1
  27. package/.next/server/app/api/boards/[id]/add-session/route.js +1 -1
  28. package/.next/server/app/api/boards/[id]/reconcile/route.js +1 -1
  29. package/.next/server/app/api/boards/[id]/route.js +1 -1
  30. package/.next/server/app/api/boards/reorder/route.js +1 -1
  31. package/.next/server/app/api/boards/route.js +1 -1
  32. package/.next/server/app/api/cwd/validate/route.js +1 -1
  33. package/.next/server/app/api/default-cwd/route.js +1 -1
  34. package/.next/server/app/api/file-index/route.js +1 -1
  35. package/.next/server/app/api/files/[...path]/route.js +2 -2
  36. package/.next/server/app/api/mcp/route.js +1 -1
  37. package/.next/server/app/api/models/route.js +1 -1
  38. package/.next/server/app/api/plugins/route.js +1 -1
  39. package/.next/server/app/api/project-trust/route.js +1 -1
  40. package/.next/server/app/api/search/route.js +2 -2
  41. package/.next/server/app/api/sessions/[id]/auto-name/route.js +2 -2
  42. package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
  43. package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +2 -2
  44. package/.next/server/app/api/sessions/[id]/export/route.js +2 -2
  45. package/.next/server/app/api/sessions/[id]/minimap/route.js +1 -1
  46. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  47. package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
  48. package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
  49. package/.next/server/app/api/sessions/route.js +1 -1
  50. package/.next/server/app/api/sessions/summary/route.js +1 -1
  51. package/.next/server/app/api/skills/install/route.js +1 -1
  52. package/.next/server/app/api/subagents/[id]/route.js +1 -1
  53. package/.next/server/app/api/subagents/profiles/route.js +1 -1
  54. package/.next/server/app/api/task-card-questions/[id]/answer/route.js +1 -1
  55. package/.next/server/app/api/task-card-questions/route.js +1 -1
  56. package/.next/server/app/api/task-cards/[id]/route.js +1 -1
  57. package/.next/server/app/api/task-cards/route.js +1 -1
  58. package/.next/server/app/api/task-cards/unbind/route.js +1 -1
  59. package/.next/server/app/api/task-scheduler/status/route.js +1 -1
  60. package/.next/server/app/api/tasks/[id]/assign-session/route.js +2 -12
  61. package/.next/server/app/api/tasks/[id]/board/route.js +1 -1
  62. package/.next/server/app/api/tasks/[id]/route.js +1 -1
  63. package/.next/server/app/api/tasks/reorder/route.js +2 -12
  64. package/.next/server/app/api/tasks/route.js +2 -12
  65. package/.next/server/app/api/worktrees/route.js +1 -1
  66. package/.next/server/app/index.html +1 -1
  67. package/.next/server/app/index.rsc +5 -5
  68. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  69. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  70. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  71. package/.next/server/app/page.js +10 -10
  72. package/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/.next/server/app-paths-manifest.json +5 -5
  74. package/.next/server/chunks/1937.js +14 -13
  75. package/.next/server/chunks/2524.js +2 -2
  76. package/.next/server/chunks/3500.js +1 -1
  77. package/.next/server/chunks/402.js +21 -10
  78. package/.next/server/chunks/4457.js +3 -13
  79. package/.next/server/chunks/542.js +8 -8
  80. package/.next/server/chunks/5794.js +1 -1
  81. package/.next/server/chunks/5940.js +1 -1
  82. package/.next/server/chunks/6429.js +4 -4
  83. package/.next/server/chunks/6525.js +18 -7
  84. package/.next/server/chunks/6583.js +12 -11
  85. package/.next/server/middleware-build-manifest.js +1 -1
  86. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  87. package/.next/server/pages/404.html +1 -1
  88. package/.next/server/pages/500.html +1 -1
  89. package/.next/static/chunks/752.bafff304e6b00987.js +1 -0
  90. package/.next/static/chunks/8088.39866dc5f1d15868.js +1 -0
  91. package/.next/static/chunks/app/{layout-87087b955e0c9507.js → layout-fae2b226b3c78448.js} +1 -1
  92. package/.next/static/chunks/app/page-d5af3bd535589daf.js +256 -0
  93. package/.next/static/chunks/{webpack-4de8e8dd9f2f3173.js → webpack-1ce35e1249cfd931.js} +1 -1
  94. package/.next/static/css/{c6d32b6031181be7.css → c7d127e07b1a786c.css} +1 -1
  95. package/.next/trace +8 -7
  96. package/.next/trace-build +1 -1
  97. package/lib/board-reconcile.test.mjs +26 -8
  98. package/lib/board-reconcile.ts +3 -1
  99. package/lib/board-store.test.mjs +2 -2
  100. package/lib/ime-guard.test.mjs +52 -0
  101. package/lib/ime-guard.ts +54 -0
  102. package/lib/rpc-manager.activity.test.mjs +223 -0
  103. package/lib/rpc-manager.test.mjs +22 -0
  104. package/lib/rpc-manager.title.test.mjs +152 -0
  105. package/lib/rpc-manager.ts +95 -8
  106. package/lib/session-activity.test.mjs +123 -0
  107. package/lib/session-activity.ts +77 -0
  108. package/lib/session-delete.test.mjs +45 -41
  109. package/lib/session-delete.ts +8 -34
  110. package/lib/session-index-scanner.test.mjs +364 -2
  111. package/lib/session-index-scanner.ts +162 -57
  112. package/lib/session-reader.noscan.test.mjs +371 -0
  113. package/lib/session-reader.project.test.mjs +30 -52
  114. package/lib/session-reader.test.mjs +2 -65
  115. package/lib/session-reader.ts +177 -213
  116. package/lib/session-scanner.test.mjs +151 -38
  117. package/lib/session-scanner.ts +140 -61
  118. package/lib/session-search.ts +6 -5
  119. package/lib/sqlite-db.test.mjs +26 -0
  120. package/lib/sqlite-db.ts +10 -1
  121. package/lib/subagent-runtime.ts +42 -0
  122. package/lib/subagent-session-title.test.mjs +120 -0
  123. package/lib/task-scheduler.test.mjs +27 -0
  124. package/lib/task-scheduler.ts +8 -1
  125. package/lib/task-store.activity.test.mjs +141 -0
  126. package/lib/task-store.meta-row.test.mjs +25 -5
  127. package/lib/task-store.partial-row.test.mjs +198 -0
  128. package/lib/task-store.subtree.test.mjs +172 -0
  129. package/lib/task-store.test.mjs +46 -20
  130. package/lib/task-store.ts +344 -68
  131. package/next.config.ts +3 -1
  132. package/package.json +1 -1
  133. package/.next/static/chunks/752.adce8bc3ebbaf4b6.js +0 -1
  134. package/.next/static/chunks/8088.0cad1ff2e09fd8ff.js +0 -1
  135. package/.next/static/chunks/app/page-efca6bf51a5f8ccb.js +0 -256
  136. /package/.next/static/{k8ZFqwz7d-bnK0ZiUVgCc → MPBMmQqmDG8zo8tM_bkDy}/_buildManifest.js +0 -0
  137. /package/.next/static/{k8ZFqwz7d-bnK0ZiUVgCc → MPBMmQqmDG8zo8tM_bkDy}/_ssgManifest.js +0 -0
  138. /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
+ });
@@ -1,8 +1,18 @@
1
1
  import assert from "node:assert/strict";
2
- import test from "node:test";
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:写 title;老会话无行则 INSERT 首改,不影响归属/索引列", () => {
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:无行老会话首改 → INSERT,title 写入、task_id NULL
230
- setSessionTitle("s-orphan", "老会话改名");
231
- row = getDb().prepare("SELECT title, task_id, pinned FROM session_meta WHERE session_id='s-orphan'").get();
232
- assert.equal(row.title, "老会话改名");
233
- assert.equal(row.task_id, null);
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
  });