@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.
- package/README.md +130 -10
- package/bin/commhub.ts +99 -11
- package/package.json +7 -7
- package/src/ack-create-request-transport.test.ts +182 -0
- package/src/ack-create-request.test.ts +50 -7
- package/src/admin-delete-network-http.test.ts +228 -0
- package/src/admin-networks-http.test.ts +88 -0
- package/src/adoption-candidates-http.test.ts +137 -0
- package/src/adoption-candidates.ts +158 -0
- package/src/agent-access-batch.test.ts +210 -0
- package/src/agent-access.ts +487 -0
- package/src/agent-acl-groups-http.test.ts +247 -0
- package/src/agent-acl-http.test.ts +659 -0
- package/src/agent-acl-slice3-http.test.ts +267 -0
- package/src/agent-groups-perf.test.ts +223 -0
- package/src/agent-notice.ts +48 -0
- package/src/agent-team-whoami-http.test.ts +98 -0
- package/src/agent-teams-http.test.ts +323 -0
- package/src/agent-teams.ts +244 -0
- package/src/alias-filter.test.ts +72 -0
- package/src/alias-filter.ts +42 -0
- package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
- package/src/api-host-supervisors-fallback.test.ts +271 -0
- package/src/api-host-supervisors-runtime-readiness.test.ts +195 -0
- package/src/api-nodes-shape.test.ts +26 -10
- package/src/auth-password-change-cli-tokens-http.test.ts +294 -0
- package/src/auth-sessions-http.test.ts +250 -0
- package/src/auth-validate.test.ts +5 -1
- package/src/auth-weak-password-login.test.ts +176 -0
- package/src/auth.ts +598 -49
- package/src/avatar-validate.test.ts +163 -0
- package/src/avatar-validate.ts +103 -0
- package/src/binding-generation-http.test.ts +121 -0
- package/src/blocked-exit-on-reply.test.ts +122 -0
- package/src/boot-side-effect.test.ts +124 -0
- package/src/clone-node.test.ts +248 -0
- package/src/clone-node.ts +289 -0
- package/src/codex-config-flags.ts +77 -0
- package/src/codex-fork-contract.ts +23 -0
- package/src/codex-fork-migration.test.ts +38 -0
- package/src/commhub-bin-args.test.ts +85 -0
- package/src/config-apply-sec1.test.ts +107 -0
- package/src/config-apply-validate.test.ts +134 -1
- package/src/config-apply-validate.ts +111 -5
- package/src/create-node-blocked-gate.test.ts +356 -0
- package/src/create-node-network-id.test.ts +79 -0
- package/src/create-node-tool-schema.test.ts +48 -0
- package/src/create-node-unicode-name.test.ts +120 -0
- package/src/create-node-validate.test.ts +193 -7
- package/src/create-node-validate.ts +227 -15
- package/src/create-node-workdir.test.ts +289 -0
- package/src/create-node.ts +77 -23
- package/src/daemon-provider-snapshots.test.ts +421 -0
- package/src/daemon-provider-snapshots.ts +327 -0
- package/src/daemon-token-owner-http.test.ts +236 -0
- package/src/dashboard-slash-routing-http.test.ts +107 -0
- package/src/db-adapter-guard.test.ts +148 -0
- package/src/db-adapter-translate.test.ts +128 -0
- package/src/db-adapter.ts +323 -87
- package/src/db-timestamp.test.ts +38 -0
- package/src/db-timestamp.ts +15 -0
- package/src/db.ts +930 -35
- package/src/delivered-without-push.test.ts +140 -0
- package/src/department-groups-http.test.ts +209 -0
- package/src/department-groups-sync-http.test.ts +328 -0
- package/src/department-groups.ts +434 -0
- package/src/department-heads-http.test.ts +330 -0
- package/src/department-heads.ts +226 -0
- package/src/departments-http.test.ts +185 -0
- package/src/departments.ts +260 -0
- package/src/desktop-message-seam.test.ts +150 -0
- package/src/desktop-message.test.ts +368 -0
- package/src/dm-file-scope-http.test.ts +178 -0
- package/src/ext-token-shared.test.ts +60 -0
- package/src/ext-validation-regression.test.ts +131 -0
- package/src/external-schedule-edits-http.test.ts +321 -0
- package/src/external-schedule-edits-timestamps.test.ts +64 -0
- package/src/external-schedule-edits.ts +327 -0
- package/src/file-download-authz-dev-open.test.ts +135 -0
- package/src/file-download-authz.test.ts +404 -0
- package/src/file-download-date-bucket.test.ts +436 -0
- package/src/file-network-scope-dev-open.test.ts +97 -0
- package/src/file-network-scope.test.ts +732 -0
- package/src/group-membership-events-http.test.ts +264 -0
- package/src/group-messages-http.test.ts +482 -0
- package/src/group-messages.ts +297 -0
- package/src/health-redaction.test.ts +642 -0
- package/src/http-gzip.test.ts +94 -0
- package/src/http-gzip.ts +112 -0
- package/src/hub-provider-config.test.ts +139 -0
- package/src/hub-provider-config.ts +105 -0
- package/src/hub-timestamp.test.ts +38 -0
- package/src/hub-timestamp.ts +27 -0
- package/src/human-dm.ts +215 -0
- package/src/inbox-caller.ts +49 -0
- package/src/inbox-count-sse.test.ts +310 -0
- package/src/inbox-count.ts +29 -0
- package/src/index.ts +15 -2362
- package/src/lifecycle-guard.ts +1 -1
- package/src/mcp-context-cost-http.test.ts +123 -0
- package/src/mcp-projects-events-http.test.ts +162 -0
- package/src/mcp-route-label-http.test.ts +120 -0
- package/src/mcp-write-network-resolution.test.ts +432 -0
- package/src/member-audit-network-id-http.test.ts +93 -0
- package/src/member-presence-http.test.ts +210 -0
- package/src/messages-ack-by-agent-http.test.ts +142 -0
- package/src/migrate-sqlite-to-pg.test.ts +47 -0
- package/src/migrate-sqlite-to-pg.ts +263 -0
- package/src/model-auth-notify-http.test.ts +181 -0
- package/src/model-auth-notify.ts +116 -0
- package/src/network-name-http.test.ts +75 -0
- package/src/network-scope.ts +276 -0
- package/src/network-token-membership-http.test.ts +139 -0
- package/src/node-alias-control.ts +12 -0
- package/src/node-attrs-validate.ts +88 -0
- package/src/node-attrs.test.ts +217 -0
- package/src/node-caller-identity-http.test.ts +781 -0
- package/src/node-create-request-status-http.test.ts +105 -0
- package/src/node-daemon-bindings.test.ts +132 -0
- package/src/node-daemon-bindings.ts +130 -0
- package/src/node-files-transport.test.ts +344 -0
- package/src/node-health-codex-login.test.ts +120 -0
- package/src/node-health-dispatch-http.test.ts +279 -0
- package/src/node-health-guard.ts +105 -0
- package/src/node-health-passthrough.test.ts +115 -0
- package/src/node-health-store.ts +101 -0
- package/src/node-id-alias.test.ts +190 -0
- package/src/node-identity-conflict-http.test.ts +269 -0
- package/src/node-identity-conflict.ts +142 -0
- package/src/node-identity-mismatch.test.ts +74 -0
- package/src/node-lifecycle-controllable.test.ts +64 -0
- package/src/node-lifecycle-controllable.ts +53 -0
- package/src/node-lifecycle-read-http.test.ts +245 -0
- package/src/node-lifecycle-read.ts +121 -0
- package/src/node-logs-transport.test.ts +263 -0
- package/src/node-permissions-http.test.ts +276 -0
- package/src/node-permissions.ts +285 -0
- package/src/node-request-retention.test.ts +281 -0
- package/src/node-request-retention.ts +120 -0
- package/src/node-skills-transport.test.ts +209 -0
- package/src/node-token-ownership.ts +51 -0
- package/src/node-token-privilege-http.test.ts +158 -0
- package/src/observer-avatar-http.test.ts +431 -0
- package/src/observer-push.test.ts +252 -0
- package/src/opencode-v2-catalog.test.ts +268 -0
- package/src/opencode-v2-catalog.ts +352 -0
- package/src/package-version-consistency.test.ts +25 -0
- package/src/peer-reply-atomic.test.ts +455 -0
- package/src/peer-reply-capability.test.ts +70 -0
- package/src/pg-worker.ts +87 -0
- package/src/project-dir-warn-clear-http.test.ts +132 -0
- package/src/project-viewer-can-http.test.ts +114 -0
- package/src/push.ts +429 -10
- package/src/redact-tokens.test.ts +65 -0
- package/src/redact-tokens.ts +40 -0
- package/src/rename.ts +22 -2
- package/src/reply-target-derivation.test.ts +96 -0
- package/src/report-status-handover-capabilities-http.test.ts +124 -0
- package/src/report-status-host-schema.test.ts +235 -0
- package/src/report-status-resume-id-handover.test.ts +150 -0
- package/src/require-explicit-test-db.ts +9 -0
- package/src/requirement-due-reminders-http.test.ts +620 -0
- package/src/requirement-due-reminders.ts +416 -0
- package/src/requirement-events-http.test.ts +268 -0
- package/src/requirement-events.test.ts +45 -0
- package/src/requirement-events.ts +144 -0
- package/src/requirement-notify.ts +165 -0
- package/src/requirement-participant-notify-http.test.ts +309 -0
- package/src/requirement-tag-ops-http.test.ts +266 -0
- package/src/requirement-tags.ts +89 -0
- package/src/requirements-abandoned-http.test.ts +236 -0
- package/src/requirements-comment-http.test.ts +167 -0
- package/src/requirements-completed-at-migration.test.ts +70 -0
- package/src/requirements-errors-mcp-http.test.ts +164 -0
- package/src/requirements-errors.ts +143 -0
- package/src/requirements-etag-http.test.ts +91 -0
- package/src/requirements-http.test.ts +709 -0
- package/src/requirements-last-event-http.test.ts +349 -0
- package/src/requirements-list-mcp-slim-http.test.ts +155 -0
- package/src/requirements-list-slim-http.test.ts +319 -0
- package/src/requirements-mcp.test.ts +163 -0
- package/src/requirements-migrate.ts +259 -0
- package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
- package/src/requirements-people-http.test.ts +114 -0
- package/src/requirements-people-mcp-http.test.ts +225 -0
- package/src/requirements-people.ts +127 -0
- package/src/requirements-priority-lowest.test.ts +114 -0
- package/src/requirements-search-http.test.ts +182 -0
- package/src/requirements-search.test.ts +32 -0
- package/src/requirements-search.ts +145 -0
- package/src/requirements-seq-http.test.ts +193 -0
- package/src/requirements-seq-migration.test.ts +63 -0
- package/src/requirements-start-date.test.ts +125 -0
- package/src/requirements-stats-http.test.ts +288 -0
- package/src/requirements-stats.ts +167 -0
- package/src/requirements-status-participants-http.test.ts +222 -0
- package/src/requirements-upgrade-e2e.test.ts +126 -0
- package/src/requirements-upgrade-migration.test.ts +98 -0
- package/src/requirements.ts +1806 -0
- package/src/resolve-port.test.ts +57 -0
- package/src/resolve-port.ts +44 -0
- package/src/response-charset.test.ts +130 -0
- package/src/rest-explicit-columns-http.test.ts +222 -0
- package/src/rest-identity.test.ts +129 -0
- package/src/rest-identity.ts +125 -0
- package/src/rest-projections.ts +68 -0
- package/src/rest-write-network-resolution.test.ts +133 -0
- package/src/restricted-files.ts +76 -0
- package/src/retention.ts +15 -3
- package/src/route-callers-http.test.ts +120 -0
- package/src/route-timing.test.ts +211 -0
- package/src/route-timing.ts +258 -0
- package/src/rules-file-duplicate-alias.test.ts +234 -0
- package/src/rules-file-session-target.test.ts +207 -0
- package/src/rules-file-transport.test.ts +237 -0
- package/src/runtime-label.test.ts +22 -0
- package/src/runtime-label.ts +21 -0
- package/src/runtime-readiness.ts +90 -0
- package/src/schedule-agent-mcp-http.test.ts +381 -0
- package/src/schedule-agent.ts +192 -0
- package/src/scheduled-doorbell-nodeless-session.test.ts +134 -0
- package/src/scheduled-failures-http.test.ts +233 -0
- package/src/scheduled-failures.ts +179 -0
- package/src/scheduled-offline.test.ts +131 -0
- package/src/scheduled-offline.ts +66 -0
- package/src/scheduled-reply-unread-http.test.ts +212 -0
- package/src/scheduled-run-terminal.test.ts +268 -0
- package/src/scheduled-skip-blocking-task.test.ts +120 -0
- package/src/scheduled-stuck-http.test.ts +238 -0
- package/src/scheduled-stuck.ts +153 -0
- package/src/scheduled-tasks-http.test.ts +638 -0
- package/src/scheduled-tasks.ts +766 -0
- package/src/scoped-secrets.ts +124 -0
- package/src/send-dedup-request-id.test.ts +229 -0
- package/src/send-reply-agent-warning.test.ts +202 -0
- package/src/send-reply-alias-optional.test.ts +131 -0
- package/src/send-reply-attachments.test.ts +458 -0
- package/src/send_dedup.test.ts +54 -0
- package/src/send_dedup.ts +52 -6
- package/src/serve-error.test.ts +38 -0
- package/src/serve-error.ts +27 -0
- package/src/server.ts +5257 -0
- package/src/session-task-on-dispatch.ts +14 -0
- package/src/session-token-ownership-http.test.ts +122 -0
- package/src/shared/codex-inventory.ts +41 -0
- package/src/shared/daemon-provider-login.ts +31 -0
- package/src/shared/daemon-provider-snapshot.ts +52 -0
- package/src/shared/external-schedule-contract.ts +60 -0
- package/src/shared/node-name.test.ts +62 -0
- package/src/shared/node-name.ts +161 -0
- package/src/shared/provider-application.ts +29 -0
- package/src/shared/provider-collection.ts +14 -0
- package/src/shared/provider-initialization.ts +8 -0
- package/src/shared/reserved-env-drift.test.ts +10 -0
- package/src/shared/scoped-secret-contract.ts +5 -0
- package/src/shared/task-content-limit-drift.test.ts +28 -0
- package/src/shared/task-content-limit.ts +25 -0
- package/src/side-thread-command-transport.test.ts +312 -0
- package/src/side-thread-command-transport.ts +318 -0
- package/src/side-thread-http-integration.test.ts +233 -0
- package/src/side-thread-http.ts +504 -0
- package/src/side-thread-production.test.ts +72 -0
- package/src/side-thread-production.ts +118 -0
- package/src/side-thread.test.ts +1067 -0
- package/src/side-thread.ts +2499 -0
- package/src/skillhub-http.test.ts +108 -0
- package/src/sqlite-close-checkpoint.test.ts +78 -0
- package/src/start-node.test.ts +264 -0
- package/src/status-alias-filter-http.test.ts +106 -0
- package/src/status-alias-resolver-http.test.ts +163 -0
- package/src/status-alias-resolver.ts +19 -0
- package/src/status-node-id-filter-http.test.ts +161 -0
- package/src/status-read-cache-http.test.ts +229 -0
- package/src/status-read-cache.ts +122 -0
- package/src/stop-delete-node.test.ts +509 -1
- package/src/task-access-http.test.ts +402 -0
- package/src/task-access.ts +420 -0
- package/src/task-auth-origin-http.test.ts +91 -0
- package/src/task-auth-origin.test.ts +24 -0
- package/src/task-auth-origin.ts +7 -0
- package/src/task-consumption.test.ts +482 -0
- package/src/task-diagnostic-http.test.ts +149 -0
- package/src/task-diagnostic.test.ts +115 -0
- package/src/task-diagnostic.ts +118 -0
- package/src/task-expiry-notice-http.test.ts +287 -0
- package/src/task-expiry-notice.ts +315 -0
- package/src/task-idempotency-mcp.test.ts +142 -0
- package/src/task-idempotency.test.ts +60 -0
- package/src/task-idempotency.ts +75 -0
- package/src/task-late-replies.ts +54 -0
- package/src/task-late-reply-http.test.ts +152 -0
- package/src/task-lifecycle-watcher.test.ts +204 -0
- package/src/task-lifecycle-watcher.ts +49 -0
- package/src/task-network-resolution-http.test.ts +141 -0
- package/src/task-orphan-http.test.ts +287 -0
- package/src/task-orphan.ts +119 -0
- package/src/task-queue-ahead-http.test.ts +309 -0
- package/src/task-queue-ahead-scale.test.ts +236 -0
- package/src/task-queue-ahead.ts +186 -0
- package/src/task-rejection-log-http.test.ts +160 -0
- package/src/task-rejection-log.ts +49 -0
- package/src/task-stale-open-http.test.ts +290 -0
- package/src/task-stale-open.ts +103 -0
- package/src/tasks-pagination-http.test.ts +141 -0
- package/src/tool-audience-http.test.ts +220 -0
- package/src/tool-audience.ts +118 -0
- package/src/tools-list-json-schema.test.ts +67 -0
- package/src/tools.ts +3927 -323
- package/src/upload-early-reject-guard.test.ts +155 -0
- package/src/uploads-http.test.ts +97 -15
- package/src/uploads.ts +103 -6
- package/src/user-avatar-http.test.ts +74 -0
- package/src/user-avatar-migration.test.ts +25 -0
- package/src/user-inbox-by-agent-http.test.ts +161 -0
- package/src/user-inbox-netid-notnull.test.ts +122 -0
- package/src/user-inbox-read-http.test.ts +223 -0
- package/src/user-inbox-schema.test.ts +54 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Duplicate-alias node rows vs the node pull/ack paths (field report).
|
|
2
|
+
//
|
|
3
|
+
// Re-creating a node under the same alias leaves the old `nodes` row behind, so
|
|
4
|
+
// one alias can own several rows in a network. The pull/ack tools used to
|
|
5
|
+
// resolve the caller by `WHERE alias = ? AND network_id = ?` + `db.get`, i.e.
|
|
6
|
+
// the first row SQLite yields — the stale one. Field shape: the client queued a
|
|
7
|
+
// rules-file read for the live node, the node pulled with its own (bound)
|
|
8
|
+
// token, the hub looked under the stale node_id, answered `request: null`, and
|
|
9
|
+
// the read timed out with only `[rules-file] doorbell received` in the node log.
|
|
10
|
+
//
|
|
11
|
+
// 跑法:cd server && COMMHUB_DB=/tmp/rules-dup.db bun test src/rules-file-duplicate-alias.test.ts
|
|
12
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
14
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { db } from "./db.js";
|
|
17
|
+
import { registerTools } from "./tools.js";
|
|
18
|
+
|
|
19
|
+
const NET = "net_rules_dup";
|
|
20
|
+
const USER = "u_rules_dup_owner";
|
|
21
|
+
const ALIAS = "rules-dup-node";
|
|
22
|
+
// Inserted first → the row a plain `db.get` on the alias returns.
|
|
23
|
+
const STALE_ID = "node_rules_dup_stale";
|
|
24
|
+
const LIVE_ID = "node_rules_dup_live";
|
|
25
|
+
const STALE_TOKEN = "tok_rules_dup_stale";
|
|
26
|
+
const LIVE_TOKEN = "tok_rules_dup_live";
|
|
27
|
+
const UNBOUND_TOKEN = "tok_rules_dup_unbound";
|
|
28
|
+
|
|
29
|
+
function cleanup() {
|
|
30
|
+
try { db.run("DELETE FROM node_rules_requests WHERE network_id = ?1", [NET]); } catch {}
|
|
31
|
+
try { db.run("DELETE FROM node_config_updates WHERE network_id = ?1", [NET]); } catch {}
|
|
32
|
+
try { db.run("DELETE FROM audit_log WHERE network_id = ?1", [NET]); } catch {}
|
|
33
|
+
try { db.run("DELETE FROM sessions WHERE network_id = ?1", [NET]); } catch {}
|
|
34
|
+
try { db.run("DELETE FROM nodes WHERE network_id = ?1", [NET]); } catch {}
|
|
35
|
+
try { db.run("DELETE FROM api_tokens WHERE network_id = ?1", [NET]); } catch {}
|
|
36
|
+
try { db.run("DELETE FROM network_members WHERE network_id = ?1", [NET]); } catch {}
|
|
37
|
+
try { db.run("DELETE FROM networks WHERE network_id = ?1", [NET]); } catch {}
|
|
38
|
+
try { db.run("DELETE FROM users WHERE user_id = ?1", [USER]); } catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function seedWorld(opts: { liveSession?: boolean } = {}) {
|
|
42
|
+
db.run(
|
|
43
|
+
`INSERT INTO users (user_id, username, password_hash, role, created_at) VALUES (?1, ?2, 'x', 'user', datetime('now'))`,
|
|
44
|
+
[USER, USER],
|
|
45
|
+
);
|
|
46
|
+
db.run(`INSERT INTO networks (network_id, network_name, owner_id, created_at) VALUES (?1, ?2, ?3, datetime('now'))`, [NET, NET, USER]);
|
|
47
|
+
db.run(`INSERT INTO network_members (user_id, network_id, role, joined_at) VALUES (?1, ?2, 'owner', datetime('now'))`, [USER, NET]);
|
|
48
|
+
// Stale row first (older created_at AND lower rowid), live row second.
|
|
49
|
+
db.run(
|
|
50
|
+
`INSERT INTO nodes (node_id, node_name, alias, network_id, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, '2026-09-16 01:40:40', '2026-09-16 01:40:40')`,
|
|
51
|
+
[STALE_ID, ALIAS, ALIAS, NET],
|
|
52
|
+
);
|
|
53
|
+
db.run(
|
|
54
|
+
`INSERT INTO nodes (node_id, node_name, alias, network_id, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, '2026-09-16 01:42:46', '2026-09-16 01:42:46')`,
|
|
55
|
+
[LIVE_ID, ALIAS, ALIAS, NET],
|
|
56
|
+
);
|
|
57
|
+
for (const [tok, bound, hash] of [
|
|
58
|
+
[STALE_TOKEN, STALE_ID, "hash_rules_dup_stale"],
|
|
59
|
+
[LIVE_TOKEN, LIVE_ID, "hash_rules_dup_live"],
|
|
60
|
+
[UNBOUND_TOKEN, null, "hash_rules_dup_unbound"],
|
|
61
|
+
] as const) {
|
|
62
|
+
db.run(
|
|
63
|
+
`INSERT INTO api_tokens (token_id, user_id, network_id, scope, name, token_hash, expires_at, revoked_at, bound_node_id) VALUES (?1, ?2, ?3, 'network', ?4, ?5, NULL, NULL, ?6)`,
|
|
64
|
+
[tok, USER, NET, `node:${ALIAS}`, hash, bound],
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (opts.liveSession !== false) {
|
|
68
|
+
db.run(
|
|
69
|
+
`INSERT INTO sessions (resume_id, alias, network_id, node_id, status, updated_at) VALUES (?1, ?2, ?3, ?4, 'idle', datetime('now'))`,
|
|
70
|
+
[`sdk-${LIVE_ID}`, ALIAS, NET, LIVE_ID],
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type Identity = { alias: string; isNetworkToken: boolean; tokenId: string };
|
|
76
|
+
const asUser: Identity = { alias: USER, isNetworkToken: false, tokenId: "tok_rules_dup_user" };
|
|
77
|
+
const asLive: Identity = { alias: ALIAS, isNetworkToken: true, tokenId: LIVE_TOKEN };
|
|
78
|
+
const asStale: Identity = { alias: ALIAS, isNetworkToken: true, tokenId: STALE_TOKEN };
|
|
79
|
+
const asUnbound: Identity = { alias: ALIAS, isNetworkToken: true, tokenId: UNBOUND_TOKEN };
|
|
80
|
+
|
|
81
|
+
async function connect(id: Identity) {
|
|
82
|
+
const server = new McpServer({ name: "rules-dup-test", version: "1" });
|
|
83
|
+
registerTools(server, undefined, NET, USER, id.alias, id.isNetworkToken, id.tokenId);
|
|
84
|
+
const client = new Client({ name: "rules-dup-client", version: "1" });
|
|
85
|
+
const [ct, st] = InMemoryTransport.createLinkedPair();
|
|
86
|
+
await server.connect(st);
|
|
87
|
+
await client.connect(ct);
|
|
88
|
+
const call = async (name: string, args: Record<string, unknown>) => {
|
|
89
|
+
const r: any = await client.callTool({ name, arguments: args });
|
|
90
|
+
const first = r.content?.[0];
|
|
91
|
+
if (!first || first.type !== "text") throw new Error(`no text result from ${name}`);
|
|
92
|
+
return JSON.parse(first.text);
|
|
93
|
+
};
|
|
94
|
+
const close = async () => { await client.close(); await server.close(); };
|
|
95
|
+
return { call, close };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function queuedNodeId(requestId: string): string | undefined {
|
|
99
|
+
return db.get<{ node_id: string }>("SELECT node_id FROM node_rules_requests WHERE request_id = ?1", requestId)?.node_id;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
beforeEach(() => { cleanup(); });
|
|
103
|
+
afterAll(cleanup);
|
|
104
|
+
|
|
105
|
+
describe("duplicate alias — rules file pull/ack", () => {
|
|
106
|
+
test("field case: read queued for the live node_id is pulled and acked by the live node's bound token", async () => {
|
|
107
|
+
seedWorld();
|
|
108
|
+
const u = await connect(asUser);
|
|
109
|
+
const n = await connect(asLive);
|
|
110
|
+
try {
|
|
111
|
+
const enq = await u.call("read_node_rules_file", { node_id: LIVE_ID });
|
|
112
|
+
expect(enq).toMatchObject({ ok: true, op: "read" });
|
|
113
|
+
const pulled = await n.call("get_rules_file_request", {});
|
|
114
|
+
expect(pulled).toEqual({ ok: true, request: { request_id: enq.request_id, op: "read" } });
|
|
115
|
+
const ack = await n.call("ack_rules_file_request", { request_id: enq.request_id, status: "done", file_name: "AGENTS.md", exists: true, content: "# live" });
|
|
116
|
+
expect(ack).toMatchObject({ ok: true, status: "done" });
|
|
117
|
+
expect(await u.call("get_rules_file_result", { request_id: enq.request_id })).toMatchObject({ status: "done", content: "# live" });
|
|
118
|
+
} finally { await u.close(); await n.close(); }
|
|
119
|
+
}, 20_000);
|
|
120
|
+
|
|
121
|
+
test("a token bound to the stale row can neither pull nor ack the live node's request", async () => {
|
|
122
|
+
seedWorld();
|
|
123
|
+
const u = await connect(asUser);
|
|
124
|
+
const stale = await connect(asStale);
|
|
125
|
+
try {
|
|
126
|
+
const enq = await u.call("read_node_rules_file", { node_id: LIVE_ID });
|
|
127
|
+
expect(await stale.call("get_rules_file_request", {})).toEqual({ ok: true, request: null });
|
|
128
|
+
expect(await stale.call("ack_rules_file_request", { request_id: enq.request_id, status: "done", content: "# forged" }))
|
|
129
|
+
.toEqual({ ok: true, ignored: "unknown_or_foreign_request" });
|
|
130
|
+
expect(await u.call("get_rules_file_result", { request_id: enq.request_id })).toMatchObject({ status: "pending" });
|
|
131
|
+
} finally { await u.close(); await stale.close(); }
|
|
132
|
+
}, 20_000);
|
|
133
|
+
|
|
134
|
+
test("the live token does not pull a request queued for the stale row", async () => {
|
|
135
|
+
seedWorld();
|
|
136
|
+
const u = await connect(asUser);
|
|
137
|
+
const n = await connect(asLive);
|
|
138
|
+
try {
|
|
139
|
+
await u.call("read_node_rules_file", { node_id: STALE_ID });
|
|
140
|
+
expect(await n.call("get_rules_file_request", {})).toEqual({ ok: true, request: null });
|
|
141
|
+
} finally { await u.close(); await n.close(); }
|
|
142
|
+
}, 20_000);
|
|
143
|
+
|
|
144
|
+
test("client targeting by alias queues under the row the alias's session points at", async () => {
|
|
145
|
+
seedWorld();
|
|
146
|
+
const u = await connect(asUser);
|
|
147
|
+
try {
|
|
148
|
+
const enq = await u.call("read_node_rules_file", { alias: ALIAS });
|
|
149
|
+
expect(enq).toMatchObject({ ok: true, op: "read" });
|
|
150
|
+
expect(queuedNodeId(enq.request_id)).toBe(LIVE_ID);
|
|
151
|
+
const sk = await u.call("list_node_skills", { alias: ALIAS });
|
|
152
|
+
expect(sk).toMatchObject({ ok: true });
|
|
153
|
+
expect(queuedNodeId(sk.request_id)).toBe(LIVE_ID);
|
|
154
|
+
} finally { await u.close(); }
|
|
155
|
+
}, 20_000);
|
|
156
|
+
|
|
157
|
+
test("without a session row, alias targeting falls back to the newest row", async () => {
|
|
158
|
+
seedWorld({ liveSession: false });
|
|
159
|
+
const u = await connect(asUser);
|
|
160
|
+
try {
|
|
161
|
+
const enq = await u.call("read_node_rules_file", { alias: ALIAS });
|
|
162
|
+
expect(queuedNodeId(enq.request_id)).toBe(LIVE_ID);
|
|
163
|
+
} finally { await u.close(); }
|
|
164
|
+
}, 20_000);
|
|
165
|
+
|
|
166
|
+
test("the session mapping wins over recency: alias targeting picks the older row when the session points at it", async () => {
|
|
167
|
+
seedWorld({ liveSession: false });
|
|
168
|
+
db.run(
|
|
169
|
+
`INSERT INTO sessions (resume_id, alias, network_id, node_id, status, updated_at) VALUES (?1, ?2, ?3, ?4, 'idle', datetime('now'))`,
|
|
170
|
+
[`sdk-${STALE_ID}`, ALIAS, NET, STALE_ID],
|
|
171
|
+
);
|
|
172
|
+
const u = await connect(asUser);
|
|
173
|
+
try {
|
|
174
|
+
const enq = await u.call("read_node_rules_file", { alias: ALIAS });
|
|
175
|
+
expect(queuedNodeId(enq.request_id)).toBe(STALE_ID);
|
|
176
|
+
} finally { await u.close(); }
|
|
177
|
+
}, 20_000);
|
|
178
|
+
|
|
179
|
+
test("a token bound to a node whose row is gone does not fall back to another row with its alias", async () => {
|
|
180
|
+
seedWorld();
|
|
181
|
+
db.run("UPDATE api_tokens SET bound_node_id = 'node_rules_dup_deleted' WHERE token_id = ?1", [STALE_TOKEN]);
|
|
182
|
+
const u = await connect(asUser);
|
|
183
|
+
const stale = await connect(asStale);
|
|
184
|
+
try {
|
|
185
|
+
const enq = await u.call("read_node_rules_file", { node_id: LIVE_ID });
|
|
186
|
+
expect(await stale.call("get_rules_file_request", {})).toEqual({ ok: true, request: null });
|
|
187
|
+
expect(await u.call("get_rules_file_result", { request_id: enq.request_id })).toMatchObject({ status: "pending" });
|
|
188
|
+
} finally { await u.close(); await stale.close(); }
|
|
189
|
+
}, 20_000);
|
|
190
|
+
|
|
191
|
+
test("an unbound legacy token resolves its alias to the session-mapped row", async () => {
|
|
192
|
+
seedWorld();
|
|
193
|
+
const u = await connect(asUser);
|
|
194
|
+
const n = await connect(asUnbound);
|
|
195
|
+
try {
|
|
196
|
+
const enq = await u.call("read_node_rules_file", { node_id: LIVE_ID });
|
|
197
|
+
expect(await n.call("get_rules_file_request", {})).toEqual({ ok: true, request: { request_id: enq.request_id, op: "read" } });
|
|
198
|
+
} finally { await u.close(); await n.close(); }
|
|
199
|
+
}, 20_000);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("duplicate alias — config update pull/ack", () => {
|
|
203
|
+
function queueUpdate(nodeId: string, updateId: string) {
|
|
204
|
+
db.run(
|
|
205
|
+
`INSERT INTO node_config_updates (update_id, node_id, network_id, patch_json, apply_mode, base_revision, status, created_at, created_by_token) VALUES (?1, ?2, ?3, '{"model":"m"}', 'hot', 0, 'pending', ?4, 'tok_rules_dup_user')`,
|
|
206
|
+
[updateId, nodeId, NET, Date.now()],
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
test("the live node's bound token pulls and acks its own pending update", async () => {
|
|
211
|
+
seedWorld();
|
|
212
|
+
queueUpdate(LIVE_ID, "cu_rules_dup_live");
|
|
213
|
+
const n = await connect(asLive);
|
|
214
|
+
try {
|
|
215
|
+
const pulled = await n.call("get_config_update", {});
|
|
216
|
+
expect(pulled.update?.update_id).toBe("cu_rules_dup_live");
|
|
217
|
+
const ack = await n.call("ack_config_update", { update_id: "cu_rules_dup_live", status: "applied", new_revision: 1 });
|
|
218
|
+
expect(ack.ok).toBe(true);
|
|
219
|
+
expect(ack.ignored).toBeUndefined();
|
|
220
|
+
expect(db.get<{ status: string }>("SELECT status FROM node_config_updates WHERE update_id = 'cu_rules_dup_live'")?.status).toBe("applied");
|
|
221
|
+
} finally { await n.close(); }
|
|
222
|
+
}, 20_000);
|
|
223
|
+
|
|
224
|
+
test("a token bound to the stale row cannot pull or ack the live node's update", async () => {
|
|
225
|
+
seedWorld();
|
|
226
|
+
queueUpdate(LIVE_ID, "cu_rules_dup_live2");
|
|
227
|
+
const stale = await connect(asStale);
|
|
228
|
+
try {
|
|
229
|
+
expect((await stale.call("get_config_update", {})).update).toBeNull();
|
|
230
|
+
await stale.call("ack_config_update", { update_id: "cu_rules_dup_live2", status: "applied", new_revision: 1 });
|
|
231
|
+
expect(db.get<{ status: string }>("SELECT status FROM node_config_updates WHERE update_id = 'cu_rules_dup_live2'")?.status).toBe("pending");
|
|
232
|
+
} finally { await stale.close(); }
|
|
233
|
+
}, 20_000);
|
|
234
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// app#225 follow-up — rules-file requests to a SESSION that has no `nodes` row
|
|
2
|
+
// (claude-code sessions launched by `anet node start` + the channel server).
|
|
3
|
+
//
|
|
4
|
+
// Hub-side contract verified here:
|
|
5
|
+
// 1. report_status { rules_file_capable: true } marks the session capable only
|
|
6
|
+
// when the caller is a node token bound to that same alias (a user token or
|
|
7
|
+
// another node's token cannot mark it); omitting the field later does not clear it.
|
|
8
|
+
// 2. read/write_node_rules_file { alias } reach such a session: the node pulls
|
|
9
|
+
// with its own token (queue key `session:<alias>`), acks, the client polls done.
|
|
10
|
+
// 3. An alias that has neither a node row nor a capable session is refused up
|
|
11
|
+
// front (no request row, no 60 s timeout for the user to sit through).
|
|
12
|
+
// 4. Network isolation: a same-named alias in another network neither sees nor
|
|
13
|
+
// acks the request; a caller in the other network cannot enqueue to it.
|
|
14
|
+
// 5. The original node_id path is unchanged (node row wins when both exist).
|
|
15
|
+
//
|
|
16
|
+
// 跑法:cd server && COMMHUB_DB=/tmp/rules-session.db bun test src/rules-file-session-target.test.ts
|
|
17
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
18
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
19
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
20
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
|
+
import { db } from "./db.js";
|
|
22
|
+
import { registerTools } from "./tools.js";
|
|
23
|
+
|
|
24
|
+
const NET = "net_rules_session";
|
|
25
|
+
const OTHER_NET = "net_rules_session_other";
|
|
26
|
+
const USER = "u_rules_session_owner";
|
|
27
|
+
const SESSION_ALIAS = "rules-session-cc";
|
|
28
|
+
const SESSION_RESUME = "cc-rules-session-1";
|
|
29
|
+
const OTHER_ALIAS = "rules-session-peer";
|
|
30
|
+
const NODE_ROW_ALIAS = "rules-session-noderow";
|
|
31
|
+
const NODE_ROW_ID = "node_rules_session_noderow";
|
|
32
|
+
const T_SESSION = "tok_rules_session_cc";
|
|
33
|
+
const T_PEER = "tok_rules_session_peer";
|
|
34
|
+
const T_OTHER_NET_SAME_ALIAS = "tok_rules_session_other_net";
|
|
35
|
+
const T_NODE_ROW = "tok_rules_session_noderow";
|
|
36
|
+
|
|
37
|
+
function cleanup() {
|
|
38
|
+
for (const net of [NET, OTHER_NET]) {
|
|
39
|
+
try { db.run("DELETE FROM node_rules_requests WHERE network_id = ?1", [net]); } catch {}
|
|
40
|
+
try { db.run("DELETE FROM sessions WHERE network_id = ?1", [net]); } catch {}
|
|
41
|
+
try { db.run("DELETE FROM nodes WHERE network_id = ?1", [net]); } catch {}
|
|
42
|
+
try { db.run("DELETE FROM api_tokens WHERE network_id = ?1", [net]); } catch {}
|
|
43
|
+
try { db.run("DELETE FROM network_members WHERE network_id = ?1", [net]); } catch {}
|
|
44
|
+
try { db.run("DELETE FROM networks WHERE network_id = ?1", [net]); } catch {}
|
|
45
|
+
}
|
|
46
|
+
try { db.run("DELETE FROM users WHERE user_id = ?1", [USER]); } catch {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function seedWorld() {
|
|
50
|
+
db.run(`INSERT INTO users (user_id, username, password_hash, role, created_at) VALUES (?1, ?2, 'x', 'user', datetime('now'))`, [USER, USER]);
|
|
51
|
+
for (const net of [NET, OTHER_NET]) {
|
|
52
|
+
db.run(`INSERT INTO networks (network_id, network_name, owner_id, created_at) VALUES (?1, ?2, ?3, datetime('now'))`, [net, net, USER]);
|
|
53
|
+
db.run(`INSERT INTO network_members (user_id, network_id, role, joined_at) VALUES (?1, ?2, 'owner', datetime('now'))`, [USER, net]);
|
|
54
|
+
}
|
|
55
|
+
const tok = (id: string, net: string, alias: string) => db.run(
|
|
56
|
+
`INSERT INTO api_tokens (token_id, user_id, network_id, scope, name, token_hash, expires_at, revoked_at) VALUES (?1, ?2, ?3, 'network', ?4, ?5, NULL, NULL)`,
|
|
57
|
+
[id, USER, net, `node:${alias}`, `hash_${id}`],
|
|
58
|
+
);
|
|
59
|
+
tok(T_SESSION, NET, SESSION_ALIAS);
|
|
60
|
+
tok(T_PEER, NET, OTHER_ALIAS);
|
|
61
|
+
tok(T_OTHER_NET_SAME_ALIAS, OTHER_NET, SESSION_ALIAS);
|
|
62
|
+
tok(T_NODE_ROW, NET, NODE_ROW_ALIAS);
|
|
63
|
+
db.run(
|
|
64
|
+
`INSERT INTO nodes (node_id, node_name, alias, network_id, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, datetime('now'), datetime('now'))`,
|
|
65
|
+
[NODE_ROW_ID, NODE_ROW_ALIAS, NODE_ROW_ALIAS, NET],
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type Identity = { net: string; alias: string; isNetworkToken: boolean; tokenId: string };
|
|
70
|
+
const asUser: Identity = { net: NET, alias: USER, isNetworkToken: false, tokenId: "tok_rules_session_user" };
|
|
71
|
+
const asOtherNetUser: Identity = { net: OTHER_NET, alias: USER, isNetworkToken: false, tokenId: "tok_rules_session_user2" };
|
|
72
|
+
const asSession: Identity = { net: NET, alias: SESSION_ALIAS, isNetworkToken: true, tokenId: T_SESSION };
|
|
73
|
+
const asPeer: Identity = { net: NET, alias: OTHER_ALIAS, isNetworkToken: true, tokenId: T_PEER };
|
|
74
|
+
const asOtherNetSameAlias: Identity = { net: OTHER_NET, alias: SESSION_ALIAS, isNetworkToken: true, tokenId: T_OTHER_NET_SAME_ALIAS };
|
|
75
|
+
const asNodeRow: Identity = { net: NET, alias: NODE_ROW_ALIAS, isNetworkToken: true, tokenId: T_NODE_ROW };
|
|
76
|
+
|
|
77
|
+
async function connect(id: Identity) {
|
|
78
|
+
const server = new McpServer({ name: "rules-session-test", version: "1" });
|
|
79
|
+
registerTools(server, undefined, id.net, USER, id.alias, id.isNetworkToken, id.tokenId);
|
|
80
|
+
const client = new Client({ name: "rules-session-client", version: "1" });
|
|
81
|
+
const [ct, st] = InMemoryTransport.createLinkedPair();
|
|
82
|
+
await server.connect(st);
|
|
83
|
+
await client.connect(ct);
|
|
84
|
+
const call = async (name: string, args: Record<string, unknown>) => {
|
|
85
|
+
const r: any = await client.callTool({ name, arguments: args });
|
|
86
|
+
const first = r.content?.[0];
|
|
87
|
+
if (!first || first.type !== "text") throw new Error(`no text result from ${name}: ${JSON.stringify(r).slice(0, 300)}`);
|
|
88
|
+
return JSON.parse(first.text);
|
|
89
|
+
};
|
|
90
|
+
const close = async () => { await client.close(); await server.close(); };
|
|
91
|
+
return { call, close };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const capable = (alias: string, net = NET) =>
|
|
95
|
+
db.get<{ c: number }>("SELECT rules_file_capable AS c FROM sessions WHERE alias = ?1 AND network_id = ?2", alias, net)?.c;
|
|
96
|
+
|
|
97
|
+
async function registerSession(id: Identity, alias: string, resume: string, flag: boolean) {
|
|
98
|
+
const c = await connect(id);
|
|
99
|
+
try {
|
|
100
|
+
return await c.call("report_status", { resume_id: resume, alias, status: "idle", agent: "claude-code", ...(flag ? { rules_file_capable: true } : {}) });
|
|
101
|
+
} finally { await c.close(); }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
beforeEach(() => { cleanup(); seedWorld(); });
|
|
105
|
+
afterAll(cleanup);
|
|
106
|
+
|
|
107
|
+
describe("rules_file_capable is set only by the session's own node token", () => {
|
|
108
|
+
test("bound node token sets it; a later report without the field does not clear it", async () => {
|
|
109
|
+
await registerSession(asSession, SESSION_ALIAS, SESSION_RESUME, true);
|
|
110
|
+
expect(capable(SESSION_ALIAS)).toBe(1);
|
|
111
|
+
await registerSession(asSession, SESSION_ALIAS, SESSION_RESUME, false);
|
|
112
|
+
expect(capable(SESSION_ALIAS)).toBe(1);
|
|
113
|
+
}, 20_000);
|
|
114
|
+
|
|
115
|
+
// The hub already refuses these reports outright (network_token_required /
|
|
116
|
+
// alias_identity_mismatch); the bound-token check on the UPDATE is defence in
|
|
117
|
+
// depth. The real session row is registered first so "stays 0" is not vacuous.
|
|
118
|
+
test("a user token reporting for the alias is refused and cannot mark it capable", async () => {
|
|
119
|
+
await registerSession(asSession, SESSION_ALIAS, SESSION_RESUME, false);
|
|
120
|
+
expect(capable(SESSION_ALIAS)).toBe(0);
|
|
121
|
+
const r = await registerSession(asUser, SESSION_ALIAS, SESSION_RESUME, true);
|
|
122
|
+
expect(r).toMatchObject({ ok: false, error: "network_token_required" });
|
|
123
|
+
expect(capable(SESSION_ALIAS)).toBe(0);
|
|
124
|
+
}, 20_000);
|
|
125
|
+
|
|
126
|
+
test("another node's token is refused and cannot mark someone else's session capable", async () => {
|
|
127
|
+
await registerSession(asSession, SESSION_ALIAS, SESSION_RESUME, false);
|
|
128
|
+
const r = await registerSession(asPeer, SESSION_ALIAS, SESSION_RESUME, true);
|
|
129
|
+
expect(r).toMatchObject({ ok: false, error: "alias_identity_mismatch" });
|
|
130
|
+
expect(capable(SESSION_ALIAS)).toBe(0);
|
|
131
|
+
}, 20_000);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("read/write by alias reach a session with no nodes row", () => {
|
|
135
|
+
test("read round trip: enqueue by alias → session pulls with its own token → ack → done", async () => {
|
|
136
|
+
await registerSession(asSession, SESSION_ALIAS, SESSION_RESUME, true);
|
|
137
|
+
const u = await connect(asUser);
|
|
138
|
+
const s = await connect(asSession);
|
|
139
|
+
try {
|
|
140
|
+
const enq = await u.call("read_node_rules_file", { alias: SESSION_ALIAS });
|
|
141
|
+
expect(enq).toMatchObject({ ok: true, op: "read" });
|
|
142
|
+
expect(db.get<{ node_id: string }>("SELECT node_id FROM node_rules_requests WHERE request_id = ?1", enq.request_id)?.node_id).toBe(`session:${SESSION_ALIAS}`);
|
|
143
|
+
const pulled = await s.call("get_rules_file_request", {});
|
|
144
|
+
expect(pulled.request).toEqual({ request_id: enq.request_id, op: "read" });
|
|
145
|
+
expect(await s.call("ack_rules_file_request", { request_id: enq.request_id, status: "done", file_name: "CLAUDE.md", exists: true, content: "# rules\n" })).toMatchObject({ ok: true, status: "done" });
|
|
146
|
+
expect(await u.call("get_rules_file_result", { request_id: enq.request_id })).toMatchObject({ ok: true, status: "done", file_name: "CLAUDE.md", exists: true, content: "# rules\n" });
|
|
147
|
+
} finally { await u.close(); await s.close(); }
|
|
148
|
+
}, 20_000);
|
|
149
|
+
|
|
150
|
+
test("write carries content to the session", async () => {
|
|
151
|
+
await registerSession(asSession, SESSION_ALIAS, SESSION_RESUME, true);
|
|
152
|
+
const u = await connect(asUser);
|
|
153
|
+
const s = await connect(asSession);
|
|
154
|
+
try {
|
|
155
|
+
const enq = await u.call("write_node_rules_file", { alias: SESSION_ALIAS, content: "new body\n" });
|
|
156
|
+
expect(enq).toMatchObject({ ok: true, op: "write" });
|
|
157
|
+
const pulled = await s.call("get_rules_file_request", {});
|
|
158
|
+
expect(pulled.request).toEqual({ request_id: enq.request_id, op: "write", content: "new body\n" });
|
|
159
|
+
} finally { await u.close(); await s.close(); }
|
|
160
|
+
}, 20_000);
|
|
161
|
+
|
|
162
|
+
test("alias with neither a node row nor a capable session is refused before any row is written", async () => {
|
|
163
|
+
await registerSession(asSession, SESSION_ALIAS, SESSION_RESUME, false);
|
|
164
|
+
const u = await connect(asUser);
|
|
165
|
+
try {
|
|
166
|
+
expect(await u.call("read_node_rules_file", { alias: SESSION_ALIAS })).toMatchObject({ ok: false, error: "rules_file_target_not_found" });
|
|
167
|
+
expect(db.get<{ c: number }>("SELECT COUNT(*) AS c FROM node_rules_requests WHERE network_id = ?1", NET)?.c).toBe(0);
|
|
168
|
+
} finally { await u.close(); }
|
|
169
|
+
}, 20_000);
|
|
170
|
+
|
|
171
|
+
test("network isolation: same alias in another network neither pulls nor acks; other-network user cannot enqueue", async () => {
|
|
172
|
+
await registerSession(asSession, SESSION_ALIAS, SESSION_RESUME, true);
|
|
173
|
+
const u = await connect(asUser);
|
|
174
|
+
const x = await connect(asOtherNetSameAlias);
|
|
175
|
+
const ou = await connect(asOtherNetUser);
|
|
176
|
+
try {
|
|
177
|
+
const enq = await u.call("read_node_rules_file", { alias: SESSION_ALIAS });
|
|
178
|
+
expect(enq.ok).toBe(true);
|
|
179
|
+
expect(await x.call("get_rules_file_request", {})).toEqual({ ok: true, request: null });
|
|
180
|
+
expect(await x.call("ack_rules_file_request", { request_id: enq.request_id, status: "done", content: "stolen" })).toMatchObject({ ok: true, ignored: "unknown_or_foreign_request" });
|
|
181
|
+
expect(db.get<{ s: string }>("SELECT status AS s FROM node_rules_requests WHERE request_id = ?1", enq.request_id)?.s).toBe("pending");
|
|
182
|
+
expect(await ou.call("read_node_rules_file", { alias: SESSION_ALIAS })).toMatchObject({ ok: false, error: "rules_file_target_not_found" });
|
|
183
|
+
} finally { await u.close(); await x.close(); await ou.close(); }
|
|
184
|
+
}, 20_000);
|
|
185
|
+
|
|
186
|
+
test("a peer node in the same network cannot pull or ack the session's request", async () => {
|
|
187
|
+
await registerSession(asSession, SESSION_ALIAS, SESSION_RESUME, true);
|
|
188
|
+
const u = await connect(asUser);
|
|
189
|
+
const p = await connect(asPeer);
|
|
190
|
+
try {
|
|
191
|
+
const enq = await u.call("read_node_rules_file", { alias: SESSION_ALIAS });
|
|
192
|
+
expect(await p.call("get_rules_file_request", {})).toEqual({ ok: true, request: null });
|
|
193
|
+
expect(await p.call("ack_rules_file_request", { request_id: enq.request_id, status: "done", content: "x" })).toMatchObject({ ok: true, ignored: "unknown_or_foreign_request" });
|
|
194
|
+
} finally { await u.close(); await p.close(); }
|
|
195
|
+
}, 20_000);
|
|
196
|
+
|
|
197
|
+
test("alias that has a nodes row resolves to that node_id (original queue key)", async () => {
|
|
198
|
+
const u = await connect(asUser);
|
|
199
|
+
const n = await connect(asNodeRow);
|
|
200
|
+
try {
|
|
201
|
+
const enq = await u.call("read_node_rules_file", { alias: NODE_ROW_ALIAS });
|
|
202
|
+
expect(enq.ok).toBe(true);
|
|
203
|
+
expect(db.get<{ node_id: string }>("SELECT node_id FROM node_rules_requests WHERE request_id = ?1", enq.request_id)?.node_id).toBe(NODE_ROW_ID);
|
|
204
|
+
expect((await n.call("get_rules_file_request", {})).request?.request_id).toBe(enq.request_id);
|
|
205
|
+
} finally { await u.close(); await n.close(); }
|
|
206
|
+
}, 20_000);
|
|
207
|
+
});
|