@sleep2agi/commhub-server 0.9.0-preview.13 → 0.9.0-preview.130

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 (317) hide show
  1. package/README.md +130 -10
  2. package/bin/commhub.ts +99 -11
  3. package/package.json +7 -7
  4. package/src/ack-create-request-transport.test.ts +182 -0
  5. package/src/ack-create-request.test.ts +50 -7
  6. package/src/admin-delete-network-http.test.ts +228 -0
  7. package/src/admin-networks-http.test.ts +88 -0
  8. package/src/adoption-candidates-http.test.ts +137 -0
  9. package/src/adoption-candidates.ts +158 -0
  10. package/src/agent-access-batch.test.ts +210 -0
  11. package/src/agent-access.ts +487 -0
  12. package/src/agent-acl-groups-http.test.ts +247 -0
  13. package/src/agent-acl-http.test.ts +659 -0
  14. package/src/agent-acl-slice3-http.test.ts +267 -0
  15. package/src/agent-groups-perf.test.ts +223 -0
  16. package/src/agent-notice.ts +48 -0
  17. package/src/agent-team-whoami-http.test.ts +98 -0
  18. package/src/agent-teams-http.test.ts +323 -0
  19. package/src/agent-teams.ts +244 -0
  20. package/src/alias-filter.test.ts +72 -0
  21. package/src/alias-filter.ts +42 -0
  22. package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
  23. package/src/api-host-supervisors-fallback.test.ts +271 -0
  24. package/src/api-host-supervisors-runtime-readiness.test.ts +195 -0
  25. package/src/api-nodes-shape.test.ts +26 -10
  26. package/src/auth-password-change-cli-tokens-http.test.ts +294 -0
  27. package/src/auth-sessions-http.test.ts +250 -0
  28. package/src/auth-validate.test.ts +5 -1
  29. package/src/auth-weak-password-login.test.ts +176 -0
  30. package/src/auth.ts +598 -49
  31. package/src/avatar-validate.test.ts +163 -0
  32. package/src/avatar-validate.ts +103 -0
  33. package/src/binding-generation-http.test.ts +121 -0
  34. package/src/blocked-exit-on-reply.test.ts +122 -0
  35. package/src/boot-side-effect.test.ts +124 -0
  36. package/src/clone-node.test.ts +248 -0
  37. package/src/clone-node.ts +289 -0
  38. package/src/codex-config-flags.ts +77 -0
  39. package/src/codex-fork-contract.ts +23 -0
  40. package/src/codex-fork-migration.test.ts +38 -0
  41. package/src/commhub-bin-args.test.ts +85 -0
  42. package/src/config-apply-sec1.test.ts +107 -0
  43. package/src/config-apply-validate.test.ts +134 -1
  44. package/src/config-apply-validate.ts +111 -5
  45. package/src/create-node-blocked-gate.test.ts +356 -0
  46. package/src/create-node-network-id.test.ts +79 -0
  47. package/src/create-node-tool-schema.test.ts +48 -0
  48. package/src/create-node-unicode-name.test.ts +120 -0
  49. package/src/create-node-validate.test.ts +193 -7
  50. package/src/create-node-validate.ts +227 -15
  51. package/src/create-node-workdir.test.ts +289 -0
  52. package/src/create-node.ts +77 -23
  53. package/src/daemon-provider-snapshots.test.ts +421 -0
  54. package/src/daemon-provider-snapshots.ts +327 -0
  55. package/src/daemon-token-owner-http.test.ts +236 -0
  56. package/src/dashboard-slash-routing-http.test.ts +107 -0
  57. package/src/db-adapter-guard.test.ts +148 -0
  58. package/src/db-adapter-translate.test.ts +128 -0
  59. package/src/db-adapter.ts +323 -87
  60. package/src/db-timestamp.test.ts +38 -0
  61. package/src/db-timestamp.ts +15 -0
  62. package/src/db.ts +930 -35
  63. package/src/delivered-without-push.test.ts +140 -0
  64. package/src/department-groups-http.test.ts +209 -0
  65. package/src/department-groups-sync-http.test.ts +328 -0
  66. package/src/department-groups.ts +434 -0
  67. package/src/department-heads-http.test.ts +330 -0
  68. package/src/department-heads.ts +226 -0
  69. package/src/departments-http.test.ts +185 -0
  70. package/src/departments.ts +260 -0
  71. package/src/desktop-message-seam.test.ts +150 -0
  72. package/src/desktop-message.test.ts +368 -0
  73. package/src/dm-file-scope-http.test.ts +178 -0
  74. package/src/ext-token-shared.test.ts +60 -0
  75. package/src/ext-validation-regression.test.ts +131 -0
  76. package/src/external-schedule-edits-http.test.ts +321 -0
  77. package/src/external-schedule-edits-timestamps.test.ts +64 -0
  78. package/src/external-schedule-edits.ts +327 -0
  79. package/src/file-download-authz-dev-open.test.ts +135 -0
  80. package/src/file-download-authz.test.ts +404 -0
  81. package/src/file-download-date-bucket.test.ts +436 -0
  82. package/src/file-network-scope-dev-open.test.ts +97 -0
  83. package/src/file-network-scope.test.ts +732 -0
  84. package/src/group-membership-events-http.test.ts +264 -0
  85. package/src/group-messages-http.test.ts +482 -0
  86. package/src/group-messages.ts +297 -0
  87. package/src/health-redaction.test.ts +642 -0
  88. package/src/http-gzip.test.ts +94 -0
  89. package/src/http-gzip.ts +112 -0
  90. package/src/hub-provider-config.test.ts +139 -0
  91. package/src/hub-provider-config.ts +105 -0
  92. package/src/hub-timestamp.test.ts +38 -0
  93. package/src/hub-timestamp.ts +27 -0
  94. package/src/human-dm.ts +215 -0
  95. package/src/inbox-caller.ts +49 -0
  96. package/src/inbox-count-sse.test.ts +310 -0
  97. package/src/inbox-count.ts +29 -0
  98. package/src/index.ts +15 -2362
  99. package/src/lifecycle-guard.ts +1 -1
  100. package/src/mcp-context-cost-http.test.ts +123 -0
  101. package/src/mcp-projects-events-http.test.ts +162 -0
  102. package/src/mcp-route-label-http.test.ts +120 -0
  103. package/src/mcp-write-network-resolution.test.ts +432 -0
  104. package/src/member-audit-network-id-http.test.ts +93 -0
  105. package/src/member-presence-http.test.ts +210 -0
  106. package/src/messages-ack-by-agent-http.test.ts +142 -0
  107. package/src/migrate-sqlite-to-pg.test.ts +47 -0
  108. package/src/migrate-sqlite-to-pg.ts +263 -0
  109. package/src/model-auth-notify-http.test.ts +181 -0
  110. package/src/model-auth-notify.ts +116 -0
  111. package/src/network-name-http.test.ts +75 -0
  112. package/src/network-scope.ts +276 -0
  113. package/src/network-token-membership-http.test.ts +139 -0
  114. package/src/node-alias-control.ts +12 -0
  115. package/src/node-attrs-validate.ts +88 -0
  116. package/src/node-attrs.test.ts +217 -0
  117. package/src/node-caller-identity-http.test.ts +781 -0
  118. package/src/node-create-request-status-http.test.ts +105 -0
  119. package/src/node-daemon-bindings.test.ts +132 -0
  120. package/src/node-daemon-bindings.ts +130 -0
  121. package/src/node-files-transport.test.ts +344 -0
  122. package/src/node-health-codex-login.test.ts +120 -0
  123. package/src/node-health-dispatch-http.test.ts +279 -0
  124. package/src/node-health-guard.ts +105 -0
  125. package/src/node-health-passthrough.test.ts +115 -0
  126. package/src/node-health-store.ts +101 -0
  127. package/src/node-id-alias.test.ts +190 -0
  128. package/src/node-identity-conflict-http.test.ts +269 -0
  129. package/src/node-identity-conflict.ts +142 -0
  130. package/src/node-identity-mismatch.test.ts +74 -0
  131. package/src/node-lifecycle-controllable.test.ts +64 -0
  132. package/src/node-lifecycle-controllable.ts +53 -0
  133. package/src/node-lifecycle-read-http.test.ts +245 -0
  134. package/src/node-lifecycle-read.ts +121 -0
  135. package/src/node-logs-transport.test.ts +263 -0
  136. package/src/node-permissions-http.test.ts +276 -0
  137. package/src/node-permissions.ts +285 -0
  138. package/src/node-request-retention.test.ts +281 -0
  139. package/src/node-request-retention.ts +120 -0
  140. package/src/node-skills-transport.test.ts +209 -0
  141. package/src/node-token-ownership.ts +51 -0
  142. package/src/node-token-privilege-http.test.ts +158 -0
  143. package/src/observer-avatar-http.test.ts +431 -0
  144. package/src/observer-push.test.ts +252 -0
  145. package/src/opencode-v2-catalog.test.ts +268 -0
  146. package/src/opencode-v2-catalog.ts +352 -0
  147. package/src/package-version-consistency.test.ts +25 -0
  148. package/src/peer-reply-atomic.test.ts +455 -0
  149. package/src/peer-reply-capability.test.ts +70 -0
  150. package/src/pg-worker.ts +87 -0
  151. package/src/project-dir-warn-clear-http.test.ts +132 -0
  152. package/src/project-viewer-can-http.test.ts +114 -0
  153. package/src/push.ts +429 -10
  154. package/src/redact-tokens.test.ts +65 -0
  155. package/src/redact-tokens.ts +40 -0
  156. package/src/rename.ts +22 -2
  157. package/src/reply-target-derivation.test.ts +96 -0
  158. package/src/report-status-handover-capabilities-http.test.ts +124 -0
  159. package/src/report-status-host-schema.test.ts +235 -0
  160. package/src/report-status-resume-id-handover.test.ts +150 -0
  161. package/src/require-explicit-test-db.ts +9 -0
  162. package/src/requirement-due-reminders-http.test.ts +620 -0
  163. package/src/requirement-due-reminders.ts +416 -0
  164. package/src/requirement-events-http.test.ts +268 -0
  165. package/src/requirement-events.test.ts +45 -0
  166. package/src/requirement-events.ts +144 -0
  167. package/src/requirement-notify.ts +165 -0
  168. package/src/requirement-participant-notify-http.test.ts +309 -0
  169. package/src/requirement-tag-ops-http.test.ts +266 -0
  170. package/src/requirement-tags.ts +89 -0
  171. package/src/requirements-abandoned-http.test.ts +236 -0
  172. package/src/requirements-comment-http.test.ts +167 -0
  173. package/src/requirements-completed-at-migration.test.ts +70 -0
  174. package/src/requirements-errors-mcp-http.test.ts +164 -0
  175. package/src/requirements-errors.ts +143 -0
  176. package/src/requirements-etag-http.test.ts +91 -0
  177. package/src/requirements-http.test.ts +709 -0
  178. package/src/requirements-last-event-http.test.ts +349 -0
  179. package/src/requirements-list-mcp-slim-http.test.ts +155 -0
  180. package/src/requirements-list-slim-http.test.ts +319 -0
  181. package/src/requirements-mcp.test.ts +163 -0
  182. package/src/requirements-migrate.ts +259 -0
  183. package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
  184. package/src/requirements-people-http.test.ts +114 -0
  185. package/src/requirements-people-mcp-http.test.ts +225 -0
  186. package/src/requirements-people.ts +127 -0
  187. package/src/requirements-priority-lowest.test.ts +114 -0
  188. package/src/requirements-search-http.test.ts +182 -0
  189. package/src/requirements-search.test.ts +32 -0
  190. package/src/requirements-search.ts +145 -0
  191. package/src/requirements-seq-http.test.ts +193 -0
  192. package/src/requirements-seq-migration.test.ts +63 -0
  193. package/src/requirements-start-date.test.ts +125 -0
  194. package/src/requirements-stats-http.test.ts +288 -0
  195. package/src/requirements-stats.ts +167 -0
  196. package/src/requirements-status-participants-http.test.ts +222 -0
  197. package/src/requirements-upgrade-e2e.test.ts +126 -0
  198. package/src/requirements-upgrade-migration.test.ts +98 -0
  199. package/src/requirements.ts +1806 -0
  200. package/src/resolve-port.test.ts +57 -0
  201. package/src/resolve-port.ts +44 -0
  202. package/src/response-charset.test.ts +130 -0
  203. package/src/rest-explicit-columns-http.test.ts +222 -0
  204. package/src/rest-identity.test.ts +129 -0
  205. package/src/rest-identity.ts +125 -0
  206. package/src/rest-projections.ts +68 -0
  207. package/src/rest-write-network-resolution.test.ts +133 -0
  208. package/src/restricted-files.ts +76 -0
  209. package/src/retention.ts +15 -3
  210. package/src/route-callers-http.test.ts +120 -0
  211. package/src/route-timing.test.ts +211 -0
  212. package/src/route-timing.ts +258 -0
  213. package/src/rules-file-duplicate-alias.test.ts +234 -0
  214. package/src/rules-file-session-target.test.ts +207 -0
  215. package/src/rules-file-transport.test.ts +237 -0
  216. package/src/runtime-label.test.ts +22 -0
  217. package/src/runtime-label.ts +21 -0
  218. package/src/runtime-readiness.ts +90 -0
  219. package/src/schedule-agent-mcp-http.test.ts +381 -0
  220. package/src/schedule-agent.ts +192 -0
  221. package/src/scheduled-doorbell-nodeless-session.test.ts +134 -0
  222. package/src/scheduled-failures-http.test.ts +233 -0
  223. package/src/scheduled-failures.ts +179 -0
  224. package/src/scheduled-offline.test.ts +131 -0
  225. package/src/scheduled-offline.ts +66 -0
  226. package/src/scheduled-reply-unread-http.test.ts +212 -0
  227. package/src/scheduled-run-terminal.test.ts +268 -0
  228. package/src/scheduled-skip-blocking-task.test.ts +120 -0
  229. package/src/scheduled-stuck-http.test.ts +238 -0
  230. package/src/scheduled-stuck.ts +153 -0
  231. package/src/scheduled-tasks-http.test.ts +638 -0
  232. package/src/scheduled-tasks.ts +766 -0
  233. package/src/scoped-secrets.ts +124 -0
  234. package/src/send-dedup-request-id.test.ts +229 -0
  235. package/src/send-reply-agent-warning.test.ts +202 -0
  236. package/src/send-reply-alias-optional.test.ts +131 -0
  237. package/src/send-reply-attachments.test.ts +458 -0
  238. package/src/send_dedup.test.ts +54 -0
  239. package/src/send_dedup.ts +52 -6
  240. package/src/serve-error.test.ts +38 -0
  241. package/src/serve-error.ts +27 -0
  242. package/src/server.ts +5257 -0
  243. package/src/session-task-on-dispatch.ts +14 -0
  244. package/src/session-token-ownership-http.test.ts +122 -0
  245. package/src/shared/codex-inventory.ts +41 -0
  246. package/src/shared/daemon-provider-login.ts +31 -0
  247. package/src/shared/daemon-provider-snapshot.ts +52 -0
  248. package/src/shared/external-schedule-contract.ts +60 -0
  249. package/src/shared/node-name.test.ts +62 -0
  250. package/src/shared/node-name.ts +161 -0
  251. package/src/shared/provider-application.ts +29 -0
  252. package/src/shared/provider-collection.ts +14 -0
  253. package/src/shared/provider-initialization.ts +8 -0
  254. package/src/shared/reserved-env-drift.test.ts +10 -0
  255. package/src/shared/scoped-secret-contract.ts +5 -0
  256. package/src/shared/task-content-limit-drift.test.ts +28 -0
  257. package/src/shared/task-content-limit.ts +25 -0
  258. package/src/side-thread-command-transport.test.ts +312 -0
  259. package/src/side-thread-command-transport.ts +318 -0
  260. package/src/side-thread-http-integration.test.ts +233 -0
  261. package/src/side-thread-http.ts +504 -0
  262. package/src/side-thread-production.test.ts +72 -0
  263. package/src/side-thread-production.ts +118 -0
  264. package/src/side-thread.test.ts +1067 -0
  265. package/src/side-thread.ts +2499 -0
  266. package/src/skillhub-http.test.ts +108 -0
  267. package/src/sqlite-close-checkpoint.test.ts +78 -0
  268. package/src/start-node.test.ts +264 -0
  269. package/src/status-alias-filter-http.test.ts +106 -0
  270. package/src/status-alias-resolver-http.test.ts +163 -0
  271. package/src/status-alias-resolver.ts +19 -0
  272. package/src/status-node-id-filter-http.test.ts +161 -0
  273. package/src/status-read-cache-http.test.ts +229 -0
  274. package/src/status-read-cache.ts +122 -0
  275. package/src/stop-delete-node.test.ts +509 -1
  276. package/src/task-access-http.test.ts +402 -0
  277. package/src/task-access.ts +420 -0
  278. package/src/task-auth-origin-http.test.ts +91 -0
  279. package/src/task-auth-origin.test.ts +24 -0
  280. package/src/task-auth-origin.ts +7 -0
  281. package/src/task-consumption.test.ts +482 -0
  282. package/src/task-diagnostic-http.test.ts +149 -0
  283. package/src/task-diagnostic.test.ts +115 -0
  284. package/src/task-diagnostic.ts +118 -0
  285. package/src/task-expiry-notice-http.test.ts +287 -0
  286. package/src/task-expiry-notice.ts +315 -0
  287. package/src/task-idempotency-mcp.test.ts +142 -0
  288. package/src/task-idempotency.test.ts +60 -0
  289. package/src/task-idempotency.ts +75 -0
  290. package/src/task-late-replies.ts +54 -0
  291. package/src/task-late-reply-http.test.ts +152 -0
  292. package/src/task-lifecycle-watcher.test.ts +204 -0
  293. package/src/task-lifecycle-watcher.ts +49 -0
  294. package/src/task-network-resolution-http.test.ts +141 -0
  295. package/src/task-orphan-http.test.ts +287 -0
  296. package/src/task-orphan.ts +119 -0
  297. package/src/task-queue-ahead-http.test.ts +309 -0
  298. package/src/task-queue-ahead-scale.test.ts +236 -0
  299. package/src/task-queue-ahead.ts +186 -0
  300. package/src/task-rejection-log-http.test.ts +160 -0
  301. package/src/task-rejection-log.ts +49 -0
  302. package/src/task-stale-open-http.test.ts +290 -0
  303. package/src/task-stale-open.ts +103 -0
  304. package/src/tasks-pagination-http.test.ts +141 -0
  305. package/src/tool-audience-http.test.ts +220 -0
  306. package/src/tool-audience.ts +118 -0
  307. package/src/tools-list-json-schema.test.ts +67 -0
  308. package/src/tools.ts +3927 -323
  309. package/src/upload-early-reject-guard.test.ts +155 -0
  310. package/src/uploads-http.test.ts +97 -15
  311. package/src/uploads.ts +103 -6
  312. package/src/user-avatar-http.test.ts +74 -0
  313. package/src/user-avatar-migration.test.ts +25 -0
  314. package/src/user-inbox-by-agent-http.test.ts +161 -0
  315. package/src/user-inbox-netid-notnull.test.ts +122 -0
  316. package/src/user-inbox-read-http.test.ts +223 -0
  317. package/src/user-inbox-schema.test.ts +54 -0
@@ -0,0 +1,132 @@
1
+ // #667 — a restart from the right directory must clear the cwd warning the
2
+ // previous process left on the same resume_id. Hub stores
3
+ // `task = COALESCE(?11, sessions.task)`, so omitting task keeps the warning.
4
+ // Register sends task: "" once. Later idle reports omit task, so the
5
+ // description of a finished task stays. This boots a throwaway hub on
6
+ // port 0 (never 9200).
7
+
8
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
9
+ import { mkdtempSync, rmSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { reportedTask } from "../../agent-node/src/project-dir-mismatch.ts";
13
+
14
+ const DIR = mkdtempSync(join(tmpdir(), "anet-667-clear-"));
15
+ process.env.COMMHUB_DB = join(DIR, "hub.db");
16
+ process.env.COMMHUB_UPLOADS_DIR = join(DIR, "uploads");
17
+ process.env.HOST = "127.0.0.1";
18
+
19
+ const PW = "ProjectDirWarn123!x";
20
+ const stamp = Date.now();
21
+ const ROOT = "/work";
22
+ const NODE_DIR = "/work/.anet/nodes/demo-node";
23
+ const CONFIG = `${NODE_DIR}/config.json`;
24
+
25
+ let db: { get: (sql: string, ...params: unknown[]) => { task: string | null } | null };
26
+ let hub: { port: number; stop: (close?: boolean) => void } | null = null;
27
+ let BASE = "";
28
+ let NET = "";
29
+ let node: { alias: string; nodeId: string; token: string };
30
+
31
+ async function tool(token: string, name: string, args: Record<string, unknown>) {
32
+ const res = await fetch(`${BASE}/mcp`, {
33
+ method: "POST",
34
+ headers: {
35
+ Authorization: `Bearer ${token}`,
36
+ "Content-Type": "application/json",
37
+ Accept: "application/json, text/event-stream",
38
+ "MCP-Protocol-Version": "2025-03-26",
39
+ },
40
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }),
41
+ });
42
+ const raw = await res.text();
43
+ const data = raw.split("\n").filter((line) => line.startsWith("data:"));
44
+ const payload = data.length ? JSON.parse(data.at(-1)!.slice(5).trim()) : JSON.parse(raw);
45
+ return JSON.parse(payload.result.content[0].text);
46
+ }
47
+
48
+ function sessionTask(): string | null {
49
+ return db.get("SELECT task FROM sessions WHERE resume_id = ?1", `sdk-${node.nodeId}`)?.task ?? null;
50
+ }
51
+
52
+ beforeAll(async () => {
53
+ ({ db } = await import("./db.js"));
54
+ const { createNetworkTokenForNode, register } = await import("./auth.js");
55
+ const boss = register(`cwd_boss_${stamp}`, PW);
56
+ expect(boss.ok).toBe(true);
57
+ NET = boss.network_id!;
58
+ const alias = `demo-node-${stamp}`;
59
+ const nodeId = `n_cwd_${stamp}`;
60
+ const minted = createNetworkTokenForNode(boss.user!.user_id, NET, alias, nodeId);
61
+ expect(minted.ok).toBe(true);
62
+ node = { alias, nodeId, token: minted.token! };
63
+ const mod = await import("./server.js");
64
+ hub = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
65
+ expect(hub.port).not.toBe(9200);
66
+ expect(hub.port).toBeGreaterThan(0);
67
+ BASE = `http://127.0.0.1:${hub.port}`;
68
+ }, 30_000);
69
+
70
+ afterAll(() => {
71
+ try { hub?.stop(true); } catch { /* already stopped */ }
72
+ try { rmSync(DIR, { recursive: true, force: true }); } catch { /* temp dir */ }
73
+ });
74
+
75
+ describe("#667 restart from the workspace root clears the cwd warning", () => {
76
+ test("wrong directory writes the warning; the right directory sends \"\" and the hub row is empty", async () => {
77
+ const resume = `sdk-${node.nodeId}`;
78
+ const wrong = reportedTask({ configPath: CONFIG, cwd: NODE_DIR, inFlight: 0, status: "idle" });
79
+ expect(wrong).toContain("目录不一致");
80
+ const started = await tool(node.token, "report_status", {
81
+ resume_id: resume, alias: node.alias, status: "idle", node_id: node.nodeId, network_id: NET, task: wrong,
82
+ });
83
+ expect(started.ok).toBe(true);
84
+ expect(sessionTask()).toBe(wrong);
85
+
86
+ const kept = await tool(node.token, "report_status", {
87
+ resume_id: resume, alias: node.alias, status: "idle", node_id: node.nodeId, network_id: NET,
88
+ });
89
+ expect(kept.ok).toBe(true);
90
+ expect(sessionTask()).toBe(wrong);
91
+
92
+ const idle = reportedTask({ configPath: CONFIG, cwd: ROOT, inFlight: 0, status: "idle" });
93
+ expect(idle).toBeUndefined();
94
+ const cleared = idle ?? "";
95
+ expect(cleared).toBe("");
96
+ const fixed = await tool(node.token, "report_status", {
97
+ resume_id: resume, alias: node.alias, status: "idle", node_id: node.nodeId, network_id: NET, task: cleared,
98
+ });
99
+ expect(fixed.ok).toBe(true);
100
+ expect(sessionTask()).toBe("");
101
+ });
102
+
103
+ test("after startup, a finished task's description stays on the hub", async () => {
104
+ const resume = `sdk-${node.nodeId}`;
105
+ const description = "BOARD667-FINISHED-TASK";
106
+ const startup = reportedTask({ configPath: CONFIG, cwd: ROOT, inFlight: 0, status: "idle" }) ?? "";
107
+ expect(startup).toBe("");
108
+ const registered = await tool(node.token, "report_status", {
109
+ resume_id: resume, alias: node.alias, status: "idle", node_id: node.nodeId, network_id: NET, task: startup,
110
+ });
111
+ expect(registered.ok).toBe(true);
112
+ expect(sessionTask()).toBe("");
113
+
114
+ const working = reportedTask({
115
+ configPath: CONFIG, cwd: ROOT, inFlight: 1, status: "working", task: description,
116
+ });
117
+ expect(working).toBe(description);
118
+ const ran = await tool(node.token, "report_status", {
119
+ resume_id: resume, alias: node.alias, status: "working", node_id: node.nodeId, network_id: NET, task: working,
120
+ });
121
+ expect(ran.ok).toBe(true);
122
+ expect(sessionTask()).toBe(description);
123
+
124
+ const finished = reportedTask({ configPath: CONFIG, cwd: ROOT, inFlight: 0, status: "idle" });
125
+ expect(finished).toBeUndefined();
126
+ const done = await tool(node.token, "report_status", {
127
+ resume_id: resume, alias: node.alias, status: "idle", node_id: node.nodeId, network_id: NET,
128
+ });
129
+ expect(done.ok).toBe(true);
130
+ expect(sessionTask()).toBe(description);
131
+ });
132
+ });
@@ -0,0 +1,114 @@
1
+ // GET /api/requirements/projects 的每行 viewer_can.edit(app 任务页审计 L13)—— HTTP 集成测试(真实 Bun.serve,私有端口,临时库)。
2
+ //
3
+ // 客户端据此只在新建 / 挪卡的项目下拉里列「能编辑」的项目。判据必须与真写卡时一致,所以这里对每个项目
4
+ // 都真建一张卡,断言「viewer_can.edit === 建卡成功」,而不是另抄一份规则来比。
5
+ // admin —— Hub 管理员 + 网络 owner:未归档项目全 true,归档的 false(projectRef 拒归档)
6
+ // alice —— scoped 成员:P1 只看 → false,P2 可改 → true(只列得到这两个)
7
+ // carol —— task_access='all' 的成员:未归档全 true
8
+ // vic —— scoped viewer,P3 只看:false(viewer 本来就不能写卡)
9
+
10
+ import { describe, expect, test, beforeAll, afterAll } from "bun:test";
11
+ import { mkdtempSync, rmSync } from "fs";
12
+ import { join } from "path";
13
+ import { tmpdir } from "os";
14
+ import { register } from "./auth.js";
15
+ import { db } from "./db.js";
16
+
17
+ const DIR = mkdtempSync(join(tmpdir(), "anet-project-viewer-can-"));
18
+ let BASE = "";
19
+ let hub: any = null;
20
+ const PW = "ProjViewerCanPassw0rd!x";
21
+ let NET = "";
22
+ const U: Record<string, { token: string; id: string }> = {};
23
+ const P: Record<string, string> = {};
24
+
25
+ type R = { status: number; body: any };
26
+ async function send(token: string, method: string, path: string, payload?: unknown): Promise<R> {
27
+ const res = await fetch(`${BASE}${path}`, {
28
+ method,
29
+ headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), "Content-Type": "application/json" },
30
+ body: payload === undefined ? undefined : JSON.stringify(payload),
31
+ });
32
+ const text = await res.text();
33
+ let body: any = null;
34
+ try { body = JSON.parse(text); } catch {}
35
+ return { status: res.status, body };
36
+ }
37
+ const projects = async (who: string) => {
38
+ const r = await send(U[who].token, "GET", `/api/requirements/projects?network_id=${NET}`);
39
+ expect(r.status).toBe(200);
40
+ return Object.fromEntries((r.body.projects as any[]).map((p) => [p.name, p]));
41
+ };
42
+
43
+ beforeAll(async () => {
44
+ process.env.COMMHUB_DB ||= join(DIR, "hub.db");
45
+ process.env.COMMHUB_UPLOADS_DIR = join(DIR, "uploads");
46
+ process.env.HOST = "127.0.0.1";
47
+ const a = register(`pvc_admin_${Date.now()}`, PW, undefined, "Admin");
48
+ expect(a.ok).toBe(true);
49
+ U.admin = { token: a.token!, id: a.user!.user_id };
50
+ NET = a.network_id!;
51
+ db.run("UPDATE users SET role = 'admin' WHERE user_id = ?1", [U.admin.id]);
52
+ const mod: any = await import("./server.js");
53
+ hub = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
54
+ BASE = `http://127.0.0.1:${hub.port}`;
55
+
56
+ const mk = async (username: string, role: string) => {
57
+ expect((await send(U.admin.token, "POST", "/api/admin/users", { username, password: PW, network_id: NET, role })).status).toBe(200);
58
+ const login = await send("", "POST", "/api/auth/login", { username, password: PW });
59
+ expect(login.status).toBe(200);
60
+ return { token: login.body.token as string, id: login.body.user.user_id as string };
61
+ };
62
+ const stamp = Date.now();
63
+ U.alice = await mk(`pvc_alice_${stamp}`, "member");
64
+ U.carol = await mk(`pvc_carol_${stamp}`, "member");
65
+ U.vic = await mk(`pvc_vic_${stamp}`, "viewer");
66
+ db.run("UPDATE network_members SET task_access = 'all' WHERE network_id = ?1 AND user_id = ?2", [NET, U.carol.id]);
67
+
68
+ for (const name of ["pvc-P1", "pvc-P2", "pvc-P3", "pvc-P4"]) {
69
+ const r = await send(U.admin.token, "POST", "/api/requirements/projects", { network_id: NET, name });
70
+ expect(r.status).toBe(201);
71
+ P[name] = r.body.project.id;
72
+ }
73
+ expect((await send(U.admin.token, "PATCH", `/api/requirements/projects/${P["pvc-P4"]}?network_id=${NET}`, { archived: true })).status).toBe(200);
74
+ expect((await send(U.admin.token, "PUT", `/api/networks/${NET}/members/${U.alice.id}/task-grants`, { task_access: "scoped", project_grants: [{ project_id: P["pvc-P1"] }, { project_id: P["pvc-P2"], can_edit: true }] })).status).toBe(200);
75
+ expect((await send(U.admin.token, "PUT", `/api/networks/${NET}/members/${U.vic.id}/task-grants`, { task_access: "scoped", project_grants: [P["pvc-P3"]] })).status).toBe(200);
76
+ }, 30_000);
77
+
78
+ afterAll(() => {
79
+ try { hub?.stop?.(true); } catch {}
80
+ try { rmSync(DIR, { recursive: true, force: true }); } catch {}
81
+ });
82
+
83
+ describe("GET /api/requirements/projects 的 viewer_can.edit", () => {
84
+ test("每个调用者的取值", async () => {
85
+ const edit = (rows: Record<string, any>) => Object.fromEntries(Object.entries(rows).map(([n, p]) => [n, p.viewer_can?.edit]));
86
+ expect(edit(await projects("admin"))).toEqual({ "pvc-P1": true, "pvc-P2": true, "pvc-P3": true, "pvc-P4": false });
87
+ expect(edit(await projects("carol"))).toEqual({ "pvc-P1": true, "pvc-P2": true, "pvc-P3": true, "pvc-P4": false });
88
+ expect(edit(await projects("alice"))).toEqual({ "pvc-P1": false, "pvc-P2": true });
89
+ expect(edit(await projects("vic"))).toEqual({ "pvc-P3": false });
90
+ });
91
+
92
+ test("只加字段:其余键与之前逐字相同", async () => {
93
+ const row = (await projects("alice"))["pvc-P2"];
94
+ expect(Object.keys(row).sort()).toEqual(["archived", "color", "createdAt", "id", "name", "sort", "viewer_can"]);
95
+ expect(Object.keys(row.viewer_can)).toEqual(["edit"]);
96
+ });
97
+
98
+ test("与真写卡一致:edit === 往这个项目建卡成功", async () => {
99
+ let checked = 0;
100
+ for (const who of ["admin", "carol", "alice", "vic"]) {
101
+ for (const [name, p] of Object.entries(await projects(who))) {
102
+ const r = await send(U[who].token, "POST", "/api/requirements", { network_id: NET, name: `pvc-card-${who}-${name}`, project_id: p.id });
103
+ expect({ who, name, created: r.status === 201 }).toEqual({ who, name, created: p.viewer_can.edit });
104
+ checked++;
105
+ }
106
+ }
107
+ expect(checked).toBe(11);
108
+ });
109
+
110
+ test("授权改成可编辑后立即反映", async () => {
111
+ expect((await send(U.admin.token, "PUT", `/api/networks/${NET}/members/${U.alice.id}/task-grants`, { project_grants: [{ project_id: P["pvc-P1"], can_edit: true }, { project_id: P["pvc-P2"], can_edit: true }] })).status).toBe(200);
112
+ expect((await projects("alice"))["pvc-P1"].viewer_can.edit).toBe(true);
113
+ });
114
+ });