@sleep2agi/commhub-server 0.9.0-preview.2 → 0.9.0-preview.21

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.
@@ -0,0 +1,354 @@
1
+ // RFC-026 v4 P1 — unit tests for state + sweeper (covers H + J live-
2
+ // test deferment per 通信牛 PR #299 review). These exercise the same
3
+ // pure functions the daemon-facing MCP tools call into; the e2e
4
+ // scenarios H/J would only re-verify these same code paths through
5
+ // multi-daemon or crash-sim plumbing, which is Phase 3 work.
6
+
7
+ import { describe, expect, test, beforeEach, afterAll } from "bun:test";
8
+ import { db } from "./db.js";
9
+ import {
10
+ putPendingEnvBlob,
11
+ takePendingEnvBlob,
12
+ peekPendingEnvBlob,
13
+ runOrphanSweepOnce,
14
+ evictExpired,
15
+ finalizeCreateOnFirstRegister,
16
+ newRequestId,
17
+ stopBackgroundTimersForTest,
18
+ resolveCallerDaemonTokenBound,
19
+ } from "./create-node.js";
20
+ import { hashToken } from "./db.js";
21
+
22
+ // ── helpers ────────────────────────────────────────────────────────
23
+ function freshFixtures() {
24
+ // Wipe RFC-026 state between tests. Other suites use the same DB;
25
+ // we only delete from tables RFC-026 owns.
26
+ db.run("DELETE FROM node_create_requests");
27
+ db.run("DELETE FROM api_tokens WHERE role = 'child' OR role = 'test_child' OR (request_id IS NOT NULL AND request_id LIKE 'cr_test_%')");
28
+ stopBackgroundTimersForTest();
29
+ }
30
+
31
+ function insertRequestRow(
32
+ request_id: string,
33
+ daemon_node_id: string,
34
+ child_name: string,
35
+ status: "pending" | "delivered" | "succeeded" | "failed" | "expired",
36
+ created_at_ms: number,
37
+ child_token_id: string | null = null,
38
+ network_id: string = "test_net",
39
+ ) {
40
+ db.run(
41
+ `INSERT INTO node_create_requests
42
+ (request_id, daemon_node_id, child_name, network_id, runtime, model, flags_json, env_keys, status, child_token_id, created_at, created_by_token)
43
+ VALUES (?1, ?2, ?3, ?4, 'claude-agent-sdk', 'x', '{}', '[]', ?5, ?6, ?7, 'tok_test')`,
44
+ [request_id, daemon_node_id, child_name, network_id, status, child_token_id, created_at_ms],
45
+ );
46
+ }
47
+
48
+ function insertChildTokenRow(token_id: string, request_id: string) {
49
+ db.run(
50
+ `INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name, scope, role, request_id)
51
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'child', ?7)`,
52
+ [token_id, `hash_${token_id}`, "u_test_creator", "test_net", `node:test_child_${token_id}`, "network", request_id],
53
+ );
54
+ }
55
+
56
+ // ── tests ──────────────────────────────────────────────────────────
57
+
58
+ beforeEach(() => freshFixtures());
59
+ afterAll(() => stopBackgroundTimersForTest());
60
+
61
+ describe("§4.4 F1 mint-stream-evict — pendingEnvBlobs Map (covers H C2)", () => {
62
+ test("takePendingEnvBlob with WRONG daemon_node_id returns null (C2 cross-daemon guard)", () => {
63
+ const id = newRequestId();
64
+ putPendingEnvBlob({
65
+ request_id: id,
66
+ daemon_node_id: "node_daemon_A",
67
+ env_blob: { ANTHROPIC_API_KEY: "secret-A" },
68
+ child_token: "ntok_test_child_A",
69
+ child_token_id: "tok_test_child_A",
70
+ });
71
+ // daemonB attempts to take daemonA's request → null
72
+ expect(takePendingEnvBlob(id, "node_daemon_B")).toBeNull();
73
+ // But the entry IS NOT consumed by the wrong-caller attempt — daemonA
74
+ // can still take it. (Otherwise an attacker could DoS legit dispatch
75
+ // by spamming wrong-daemon takes.)
76
+ const blob = takePendingEnvBlob(id, "node_daemon_A");
77
+ expect(blob).not.toBeNull();
78
+ expect(blob!.env_blob).toEqual({ ANTHROPIC_API_KEY: "secret-A" });
79
+ expect(blob!.child_token).toBe("ntok_test_child_A");
80
+ });
81
+
82
+ test("takePendingEnvBlob with RIGHT daemon_node_id evicts immediately (F1 one-shot consume)", () => {
83
+ const id = newRequestId();
84
+ putPendingEnvBlob({
85
+ request_id: id,
86
+ daemon_node_id: "node_daemon_X",
87
+ env_blob: { K1: "v1" },
88
+ child_token: "ntok_x",
89
+ child_token_id: "tok_x",
90
+ });
91
+ expect(peekPendingEnvBlob(id)).not.toBeNull();
92
+ expect(takePendingEnvBlob(id, "node_daemon_X")).not.toBeNull();
93
+ // Second take is null — entry was evicted on first successful take
94
+ expect(takePendingEnvBlob(id, "node_daemon_X")).toBeNull();
95
+ expect(peekPendingEnvBlob(id)).toBeNull();
96
+ });
97
+
98
+ test("takePendingEnvBlob with unknown id returns null", () => {
99
+ expect(takePendingEnvBlob("cr_never_existed", "node_anything")).toBeNull();
100
+ });
101
+
102
+ test("evictExpired drops entries past TTL", () => {
103
+ const id = newRequestId();
104
+ putPendingEnvBlob({
105
+ request_id: id,
106
+ daemon_node_id: "node_z",
107
+ env_blob: {},
108
+ child_token: "ntok_z",
109
+ child_token_id: "tok_z",
110
+ });
111
+ // Fast-forward past 60s TTL
112
+ const past = Date.now() + 60_001;
113
+ expect(evictExpired(past)).toBeGreaterThan(0);
114
+ expect(takePendingEnvBlob(id, "node_z")).toBeNull();
115
+ });
116
+ });
117
+
118
+ describe("§4.4.8 C4 orphan sweeper — runOrphanSweepOnce (covers J F-1 + F-2)", () => {
119
+ test("F-1: status='pending' age > TTL → revoke child-ntok + mark status='failed'", () => {
120
+ const reqId = `cr_test_${Date.now()}_F1`;
121
+ const tokId = `tok_test_${Date.now()}_F1`;
122
+ const longAgo = Date.now() - 90_000; // 90s past — well > 60s TTL
123
+ insertChildTokenRow(tokId, reqId);
124
+ insertRequestRow(reqId, "node_daemon_F1", "child_F1", "pending", longAgo, tokId);
125
+ const result = runOrphanSweepOnce();
126
+ expect(result.swept).toBeGreaterThanOrEqual(1);
127
+ expect(result.revoked).toBeGreaterThanOrEqual(1);
128
+ // Verify api_tokens.revoked_at is now set
129
+ const tokRow = db.get<{ revoked_at: string | null }>(
130
+ "SELECT revoked_at FROM api_tokens WHERE token_id = ?1", tokId,
131
+ );
132
+ expect(tokRow?.revoked_at).not.toBeNull();
133
+ // Verify request status flipped to 'failed'
134
+ const reqRow = db.get<{ status: string; error: string | null }>(
135
+ "SELECT status, error FROM node_create_requests WHERE request_id = ?1", reqId,
136
+ );
137
+ expect(reqRow?.status).toBe("failed");
138
+ expect(reqRow?.error).toContain("sweeper_revoked_before_delivery");
139
+ });
140
+
141
+ test("F-2: status='delivered' age > TTL → revoke child-ntok + mark status='expired'", () => {
142
+ const reqId = `cr_test_${Date.now()}_F2`;
143
+ const tokId = `tok_test_${Date.now()}_F2`;
144
+ const longAgo = Date.now() - 120_000; // 120s past
145
+ insertChildTokenRow(tokId, reqId);
146
+ insertRequestRow(reqId, "node_daemon_F2", "child_F2", "delivered", longAgo, tokId);
147
+ const result = runOrphanSweepOnce();
148
+ expect(result.swept).toBeGreaterThanOrEqual(1);
149
+ expect(result.revoked).toBeGreaterThanOrEqual(1);
150
+ const tokRow = db.get<{ revoked_at: string | null }>(
151
+ "SELECT revoked_at FROM api_tokens WHERE token_id = ?1", tokId,
152
+ );
153
+ expect(tokRow?.revoked_at).not.toBeNull();
154
+ const reqRow = db.get<{ status: string; error: string | null }>(
155
+ "SELECT status, error FROM node_create_requests WHERE request_id = ?1", reqId,
156
+ );
157
+ expect(reqRow?.status).toBe("expired");
158
+ expect(reqRow?.error).toContain("sweeper_revoked_after_delivery_no_ack");
159
+ });
160
+
161
+ test("does NOT sweep recent pending/delivered rows (within TTL)", () => {
162
+ const reqId = `cr_test_${Date.now()}_fresh`;
163
+ const tokId = `tok_test_${Date.now()}_fresh`;
164
+ insertChildTokenRow(tokId, reqId);
165
+ // created 5s ago — well within 60s TTL
166
+ insertRequestRow(reqId, "node_daemon_fresh", "child_fresh", "pending", Date.now() - 5_000, tokId);
167
+ const result = runOrphanSweepOnce();
168
+ expect(result.swept).toBe(0);
169
+ expect(result.revoked).toBe(0);
170
+ const tokRow = db.get<{ revoked_at: string | null }>(
171
+ "SELECT revoked_at FROM api_tokens WHERE token_id = ?1", tokId,
172
+ );
173
+ expect(tokRow?.revoked_at).toBeNull();
174
+ const reqRow = db.get<{ status: string }>(
175
+ "SELECT status FROM node_create_requests WHERE request_id = ?1", reqId,
176
+ );
177
+ expect(reqRow?.status).toBe("pending");
178
+ });
179
+
180
+ test("does NOT sweep terminal rows (succeeded/failed/expired)", () => {
181
+ const reqId = `cr_test_${Date.now()}_terminal`;
182
+ const tokId = `tok_test_${Date.now()}_terminal`;
183
+ insertChildTokenRow(tokId, reqId);
184
+ insertRequestRow(reqId, "node_daemon_t", "child_t", "succeeded", Date.now() - 120_000, tokId);
185
+ const result = runOrphanSweepOnce();
186
+ expect(result.swept).toBe(0);
187
+ expect(result.revoked).toBe(0);
188
+ const tokRow = db.get<{ revoked_at: string | null }>(
189
+ "SELECT revoked_at FROM api_tokens WHERE token_id = ?1", tokId,
190
+ );
191
+ expect(tokRow?.revoked_at).toBeNull();
192
+ });
193
+
194
+ test("sweeper is idempotent — second run on same stale row is a no-op", () => {
195
+ const reqId = `cr_test_${Date.now()}_idem`;
196
+ const tokId = `tok_test_${Date.now()}_idem`;
197
+ insertChildTokenRow(tokId, reqId);
198
+ insertRequestRow(reqId, "node_daemon_i", "child_i", "pending", Date.now() - 120_000, tokId);
199
+ const r1 = runOrphanSweepOnce();
200
+ expect(r1.swept).toBe(1);
201
+ const r2 = runOrphanSweepOnce();
202
+ expect(r2.swept).toBe(0); // already terminal after first sweep
203
+ });
204
+ });
205
+
206
+ describe("§4.1.4 C2 token-bound daemon resolution regression (PR #299 BLOCKER #1)", () => {
207
+ // Calls the REAL resolveCallerDaemonTokenBound helper exported from
208
+ // create-node.ts — same code path the daemon-facing MCP tools take
209
+ // (per 通信龙 PR #299 nit 1, no inline-mirror SQL: anti-pattern that
210
+ // would let helper-drift slip past). Setup: two daemons same alias
211
+ // in different networks; two ntoks bound to each; confirm each
212
+ // ntok's resolution returns ITS OWN node row (not the other).
213
+ test("two daemons same alias different networks: helper resolves token to correct node", () => {
214
+ db.run("DELETE FROM nodes WHERE alias = 'daemon-shared'");
215
+ db.run("DELETE FROM api_tokens WHERE token_id IN ('tok_test_A', 'tok_test_B')");
216
+ // daemonA in netA
217
+ db.run(
218
+ `INSERT INTO nodes (node_id, node_name, alias, runtime, model, network_id, updated_at)
219
+ VALUES ('node_test_daemonA', 'daemon-shared', 'daemon-shared', 'claude-agent-sdk', 'm', 'net_test_A', datetime('now'))`,
220
+ );
221
+ // daemonB in netB — SAME alias
222
+ db.run(
223
+ `INSERT INTO nodes (node_id, node_name, alias, runtime, model, network_id, updated_at)
224
+ VALUES ('node_test_daemonB', 'daemon-shared', 'daemon-shared', 'claude-agent-sdk', 'm', 'net_test_B', datetime('now'))`,
225
+ );
226
+ // ntok rows bound to each daemon (name='node:daemon-shared'; network_id distinguishes)
227
+ db.run(
228
+ `INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name, scope)
229
+ VALUES ('tok_test_A', ?1, 'u_test_creator', 'net_test_A', 'node:daemon-shared', 'network')`,
230
+ [hashToken("ntok_test_A")],
231
+ );
232
+ db.run(
233
+ `INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name, scope)
234
+ VALUES ('tok_test_B', ?1, 'u_test_creator', 'net_test_B', 'node:daemon-shared', 'network')`,
235
+ [hashToken("ntok_test_B")],
236
+ );
237
+
238
+ // ntok A → must resolve to node_test_daemonA (NOT daemonB)
239
+ const resA = resolveCallerDaemonTokenBound({
240
+ callerTokenIsNetwork: true,
241
+ callerTokenId: "tok_test_A",
242
+ enforceNetworkId: "net_test_A",
243
+ });
244
+ expect(resA.ok).toBe(true);
245
+ if (resA.ok) {
246
+ expect(resA.daemonNodeId).toBe("node_test_daemonA");
247
+ expect(resA.networkId).toBe("net_test_A");
248
+ }
249
+
250
+ // ntok B → must resolve to node_test_daemonB
251
+ const resB = resolveCallerDaemonTokenBound({
252
+ callerTokenIsNetwork: true,
253
+ callerTokenId: "tok_test_B",
254
+ enforceNetworkId: "net_test_B",
255
+ });
256
+ expect(resB.ok).toBe(true);
257
+ if (resB.ok) expect(resB.daemonNodeId).toBe("node_test_daemonB");
258
+
259
+ // Pre-blocker buggy form (alias only) would be ambiguous —
260
+ // confirm there really ARE 2 rows so the SQL was un-scoped before.
261
+ const all = db.all<{ node_id: string }>(
262
+ `SELECT node_id FROM nodes WHERE alias = ?1`, "daemon-shared",
263
+ );
264
+ expect(all.length).toBe(2);
265
+
266
+ // Mismatched scope: tok_test_A presented with enforceNetworkId=B
267
+ // (compromised caller trying to pivot to wrong net) → reject.
268
+ const resCross = resolveCallerDaemonTokenBound({
269
+ callerTokenIsNetwork: true,
270
+ callerTokenId: "tok_test_A",
271
+ enforceNetworkId: "net_test_B",
272
+ });
273
+ expect(resCross.ok).toBe(false);
274
+
275
+ // utok (callerTokenIsNetwork=false) → reject as not-a-daemon
276
+ const resUtok = resolveCallerDaemonTokenBound({
277
+ callerTokenIsNetwork: false,
278
+ callerTokenId: "tok_test_A",
279
+ enforceNetworkId: "net_test_A",
280
+ });
281
+ expect(resUtok.ok).toBe(false);
282
+
283
+ // Revoked token → reject
284
+ db.run("UPDATE api_tokens SET revoked_at = datetime('now') WHERE token_id = 'tok_test_A'");
285
+ const resRevoked = resolveCallerDaemonTokenBound({
286
+ callerTokenIsNetwork: true,
287
+ callerTokenId: "tok_test_A",
288
+ enforceNetworkId: "net_test_A",
289
+ });
290
+ expect(resRevoked.ok).toBe(false);
291
+
292
+ db.run("DELETE FROM nodes WHERE node_id IN ('node_test_daemonA', 'node_test_daemonB')");
293
+ db.run("DELETE FROM api_tokens WHERE token_id IN ('tok_test_A', 'tok_test_B')");
294
+ });
295
+
296
+ test("daemon B cannot take/ack daemon A's request even with same alias (Map-level guard)", () => {
297
+ const id = newRequestId();
298
+ putPendingEnvBlob({
299
+ request_id: id,
300
+ daemon_node_id: "node_legit_daemonA",
301
+ env_blob: { ANTHROPIC_API_KEY: "s" },
302
+ child_token: "ntok_a",
303
+ child_token_id: "tok_a",
304
+ });
305
+ // daemonB-token resolves to node_id = node_attacker (different node_id)
306
+ expect(takePendingEnvBlob(id, "node_attacker")).toBeNull();
307
+ // Original entry still there for daemonA
308
+ expect(takePendingEnvBlob(id, "node_legit_daemonA")).not.toBeNull();
309
+ });
310
+ });
311
+
312
+ describe("finalizeCreateOnFirstRegister — content-match on child register", () => {
313
+ test("matching child_name + network_id flips status='succeeded' + stamps child_node_id", () => {
314
+ const reqId = `cr_test_${Date.now()}_finalize`;
315
+ insertRequestRow(reqId, "node_daemon_F", "child_finalize_test", "delivered", Date.now() - 1_000, null, "test_net_finalize");
316
+ const r = finalizeCreateOnFirstRegister({
317
+ node_id: "node_child_real_F",
318
+ alias: "child_finalize_test",
319
+ network_id: "test_net_finalize",
320
+ });
321
+ expect(r.finalized).toBe(1);
322
+ const row = db.get<{ status: string; child_node_id: string | null }>(
323
+ "SELECT status, child_node_id FROM node_create_requests WHERE request_id = ?1", reqId,
324
+ );
325
+ expect(row?.status).toBe("succeeded");
326
+ expect(row?.child_node_id).toBe("node_child_real_F");
327
+ });
328
+
329
+ test("non-matching alias is a no-op", () => {
330
+ const reqId = `cr_test_${Date.now()}_nomatch`;
331
+ insertRequestRow(reqId, "node_daemon_nm", "expected_alias", "pending", Date.now(), null, "test_net_nm");
332
+ const r = finalizeCreateOnFirstRegister({
333
+ node_id: "node_intruder",
334
+ alias: "different_alias",
335
+ network_id: "test_net_nm",
336
+ });
337
+ expect(r.finalized).toBe(0);
338
+ const row = db.get<{ status: string }>(
339
+ "SELECT status FROM node_create_requests WHERE request_id = ?1", reqId,
340
+ );
341
+ expect(row?.status).toBe("pending");
342
+ });
343
+
344
+ test("cross-network: same child_name in different network is a no-op (network_id mismatch)", () => {
345
+ const reqId = `cr_test_${Date.now()}_crossnet`;
346
+ insertRequestRow(reqId, "node_daemon_xn", "shared_name", "delivered", Date.now(), null, "test_net_A");
347
+ const r = finalizeCreateOnFirstRegister({
348
+ node_id: "node_evil",
349
+ alias: "shared_name",
350
+ network_id: "test_net_B", // different network
351
+ });
352
+ expect(r.finalized).toBe(0);
353
+ });
354
+ });