@sleep2agi/commhub-server 0.9.0-preview.14 → 0.9.0-preview.140
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-department-http.test.ts +148 -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 +423 -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 +604 -51
- 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/chain-attempt-dedup.test.ts +447 -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 +49 -0
- package/src/create-node-unicode-name.test.ts +120 -0
- package/src/create-node-validate.test.ts +199 -7
- package/src/create-node-validate.ts +232 -15
- package/src/create-node-workdir.test.ts +289 -0
- package/src/create-node.ts +88 -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 +109 -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 +1111 -42
- 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 +276 -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/env-transport-gate-http.test.ts +146 -0
- package/src/env-transport-gate.ts +74 -0
- package/src/env-transport-policy.ts +21 -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/host-supervisor-identity-http.test.ts +152 -0
- package/src/host-supervisor-identity.ts +92 -0
- package/src/http-gzip.test.ts +94 -0
- package/src/http-gzip.ts +112 -0
- package/src/hub-catalog-read-http.test.ts +187 -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/human-inbox.ts +42 -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/list-host-supervisors.test.ts +5 -4
- 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 +1083 -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 +135 -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-secret-rest-http.test.ts +190 -0
- package/src/node-skills-transport.test.ts +209 -0
- package/src/node-token-identity.ts +35 -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 +442 -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/password-network-tokens.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 +38 -10
- 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 +184 -0
- package/src/report-status-secret-keys.test.ts +140 -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 +131 -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 +357 -0
- package/src/requirements-stats.ts +195 -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 +130 -0
- package/src/rest-identity.ts +141 -0
- package/src/rest-projections.ts +70 -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-backoff-http.test.ts +193 -0
- package/src/scheduled-backoff.ts +64 -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-outcome-guidance-http.test.ts +139 -0
- package/src/scheduled-outcome-guidance.ts +9 -0
- package/src/scheduled-reply-unread-http.test.ts +212 -0
- package/src/scheduled-requirement-wake-http.test.ts +264 -0
- package/src/scheduled-requirement-wake.ts +125 -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 +312 -0
- package/src/scheduled-stuck.ts +186 -0
- package/src/scheduled-tasks-http.test.ts +638 -0
- package/src/scheduled-tasks.ts +819 -0
- package/src/scheduled-work-outcome-http.test.ts +154 -0
- package/src/scheduled-work-outcome.ts +50 -0
- package/src/scoped-secrets.ts +139 -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 +5398 -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 -2
- package/src/task-access-http.test.ts +402 -0
- package/src/task-access.ts +420 -0
- package/src/task-activity.ts +60 -0
- package/src/task-auth-origin-http.test.ts +93 -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 +140 -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-sender-http.test.ts +143 -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 +4008 -355
- 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,252 @@
|
|
|
1
|
+
// #461 — unit tests for the network observer SSE channel (push.ts).
|
|
2
|
+
//
|
|
3
|
+
// These tests read REAL bytes off the observer stream, so they are a
|
|
4
|
+
// live gate: stub pushNetworkObserverEvent out to a no-op and every
|
|
5
|
+
// "receives …" test below times out red. (Per 通信龙 2026-07-28 rule:
|
|
6
|
+
// a test that stays green when the core function is emptied is a fake
|
|
7
|
+
// gate.)
|
|
8
|
+
|
|
9
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
__resetSSEClientsForTest,
|
|
12
|
+
createNetworkObserverStream,
|
|
13
|
+
createSSEStream,
|
|
14
|
+
createUserEventStream,
|
|
15
|
+
pushEvent,
|
|
16
|
+
pushNetworkObserverEvent,
|
|
17
|
+
pushUserEvent,
|
|
18
|
+
} from "./push";
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
__resetSSEClientsForTest();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type Frame = Record<string, any>;
|
|
25
|
+
|
|
26
|
+
/** Read the next SSE data frame (skipping keepalive comments), with a
|
|
27
|
+
* hard timeout so a silent stream fails the test instead of hanging. */
|
|
28
|
+
async function readFrame(
|
|
29
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
30
|
+
timeoutMs = 2_000,
|
|
31
|
+
): Promise<Frame> {
|
|
32
|
+
const decoder = new TextDecoder();
|
|
33
|
+
let buf = "";
|
|
34
|
+
const deadline = Date.now() + timeoutMs;
|
|
35
|
+
while (Date.now() < deadline) {
|
|
36
|
+
const remaining = deadline - Date.now();
|
|
37
|
+
const result = await Promise.race([
|
|
38
|
+
reader.read(),
|
|
39
|
+
new Promise<never>((_, rej) =>
|
|
40
|
+
setTimeout(() => rej(new Error(`no SSE frame within ${timeoutMs}ms`)), remaining),
|
|
41
|
+
),
|
|
42
|
+
]);
|
|
43
|
+
if (result.done) throw new Error("stream ended before a data frame arrived");
|
|
44
|
+
buf += decoder.decode(result.value, { stream: true });
|
|
45
|
+
const sep = buf.indexOf("\n\n");
|
|
46
|
+
if (sep === -1) continue;
|
|
47
|
+
const rawFrame = buf.slice(0, sep);
|
|
48
|
+
buf = buf.slice(sep + 2);
|
|
49
|
+
const dataLine = rawFrame.split("\n").find((l) => l.startsWith("data: "));
|
|
50
|
+
if (!dataLine) continue; // keepalive comment frame — keep reading
|
|
51
|
+
return JSON.parse(dataLine.slice(6));
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`no SSE frame within ${timeoutMs}ms`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("#461 network observer stream", () => {
|
|
57
|
+
test("observer receives connected frame, then new_task summary with metadata only", async () => {
|
|
58
|
+
const res = createNetworkObserverStream("net_obs_a");
|
|
59
|
+
const reader = res.body!.getReader();
|
|
60
|
+
|
|
61
|
+
const connected = await readFrame(reader);
|
|
62
|
+
expect(connected.type).toBe("connected");
|
|
63
|
+
expect(connected.observer).toBe(true);
|
|
64
|
+
expect(connected.network_id).toBe("net_obs_a");
|
|
65
|
+
|
|
66
|
+
pushNetworkObserverEvent("net_obs_a", {
|
|
67
|
+
type: "new_task",
|
|
68
|
+
task_id: "task-123",
|
|
69
|
+
from: "sender-agent",
|
|
70
|
+
to: "receiver-agent",
|
|
71
|
+
status: "delivered",
|
|
72
|
+
priority: "high",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const evt = await readFrame(reader);
|
|
76
|
+
expect(evt.type).toBe("new_task");
|
|
77
|
+
expect(evt.task_id).toBe("task-123");
|
|
78
|
+
expect(evt.from).toBe("sender-agent");
|
|
79
|
+
expect(evt.to).toBe("receiver-agent");
|
|
80
|
+
expect(evt.status).toBe("delivered");
|
|
81
|
+
expect(evt.priority).toBe("high");
|
|
82
|
+
expect(evt.network_id).toBe("net_obs_a");
|
|
83
|
+
expect(evt.scope).toBe("network");
|
|
84
|
+
// Summary contract: NO content field, ever.
|
|
85
|
+
expect("content" in evt).toBe(false);
|
|
86
|
+
expect("task" in evt).toBe(false);
|
|
87
|
+
await reader.cancel();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("observer receives new_reply summary", async () => {
|
|
91
|
+
const res = createNetworkObserverStream("net_obs_reply");
|
|
92
|
+
const reader = res.body!.getReader();
|
|
93
|
+
await readFrame(reader); // connected
|
|
94
|
+
|
|
95
|
+
pushNetworkObserverEvent("net_obs_reply", {
|
|
96
|
+
type: "new_reply",
|
|
97
|
+
task_id: "parent-task",
|
|
98
|
+
message_id: "msg-9",
|
|
99
|
+
from: "worker",
|
|
100
|
+
to: "boss",
|
|
101
|
+
status: "completed",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const evt = await readFrame(reader);
|
|
105
|
+
expect(evt.type).toBe("new_reply");
|
|
106
|
+
expect(evt.task_id).toBe("parent-task");
|
|
107
|
+
expect(evt.message_id).toBe("msg-9");
|
|
108
|
+
expect(evt.status).toBe("completed");
|
|
109
|
+
expect("content" in evt).toBe(false);
|
|
110
|
+
expect("text" in evt).toBe(false);
|
|
111
|
+
await reader.cancel();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("events are network-isolated: observer on net B never sees net A traffic", async () => {
|
|
115
|
+
const resB = createNetworkObserverStream("net_obs_b");
|
|
116
|
+
const readerB = resB.body!.getReader();
|
|
117
|
+
await readFrame(readerB); // connected
|
|
118
|
+
|
|
119
|
+
// Push to A first, then a marker to B. Delivery per key is
|
|
120
|
+
// synchronous and ordered, so if B's next frame is the marker,
|
|
121
|
+
// the A event provably never reached B.
|
|
122
|
+
pushNetworkObserverEvent("net_obs_a2", { type: "new_task", task_id: "leaked" });
|
|
123
|
+
pushNetworkObserverEvent("net_obs_b", { type: "new_task", task_id: "marker-b" });
|
|
124
|
+
|
|
125
|
+
const evt = await readFrame(readerB);
|
|
126
|
+
expect(evt.task_id).toBe("marker-b");
|
|
127
|
+
await readerB.cancel();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("observer stream is isolated from session channels (both directions)", async () => {
|
|
131
|
+
const sessionRes = createSSEStream("agent-x", "net_obs_c");
|
|
132
|
+
const sessionReader = sessionRes.body!.getReader();
|
|
133
|
+
await readFrame(sessionReader); // connected
|
|
134
|
+
|
|
135
|
+
const obsRes = createNetworkObserverStream("net_obs_c");
|
|
136
|
+
const obsReader = obsRes.body!.getReader();
|
|
137
|
+
await readFrame(obsReader); // connected
|
|
138
|
+
|
|
139
|
+
// Session push must NOT hit the observer; observer push must NOT
|
|
140
|
+
// hit the session. Marker technique as above.
|
|
141
|
+
pushEvent("agent-x", { type: "new_task", inbox_count: 1, marker: "to-session" }, "net_obs_c");
|
|
142
|
+
pushNetworkObserverEvent("net_obs_c", { type: "new_task", task_id: "to-observer" });
|
|
143
|
+
|
|
144
|
+
const sessionEvt = await readFrame(sessionReader);
|
|
145
|
+
expect(sessionEvt.marker).toBe("to-session");
|
|
146
|
+
expect("task_id" in sessionEvt).toBe(false);
|
|
147
|
+
|
|
148
|
+
const obsEvt = await readFrame(obsReader);
|
|
149
|
+
expect(obsEvt.task_id).toBe("to-observer");
|
|
150
|
+
expect("marker" in obsEvt).toBe(false);
|
|
151
|
+
|
|
152
|
+
await sessionReader.cancel();
|
|
153
|
+
await obsReader.cancel();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("null / undefined networkId is a silent no-op", () => {
|
|
157
|
+
// Must not throw — legacy null-network traffic has no observer
|
|
158
|
+
// stream by design (documented in #461 / the PR).
|
|
159
|
+
expect(() => pushNetworkObserverEvent(null, { type: "new_task" })).not.toThrow();
|
|
160
|
+
expect(() => pushNetworkObserverEvent(undefined, { type: "new_task" })).not.toThrow();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("multiple observers on the same network all receive the event", async () => {
|
|
164
|
+
const r1 = createNetworkObserverStream("net_obs_multi");
|
|
165
|
+
const r2 = createNetworkObserverStream("net_obs_multi");
|
|
166
|
+
const reader1 = r1.body!.getReader();
|
|
167
|
+
const reader2 = r2.body!.getReader();
|
|
168
|
+
await readFrame(reader1);
|
|
169
|
+
await readFrame(reader2);
|
|
170
|
+
|
|
171
|
+
pushNetworkObserverEvent("net_obs_multi", { type: "new_task", task_id: "fanout" });
|
|
172
|
+
|
|
173
|
+
expect((await readFrame(reader1)).task_id).toBe("fanout");
|
|
174
|
+
expect((await readFrame(reader2)).task_id).toBe("fanout");
|
|
175
|
+
await reader1.cancel();
|
|
176
|
+
await reader2.cancel();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("Desktop user SSE stream", () => {
|
|
181
|
+
test("user stream receives connected frame, then desktop_message", async () => {
|
|
182
|
+
const res = createUserEventStream("net_user_a", "u_user_a");
|
|
183
|
+
const reader = res.body!.getReader();
|
|
184
|
+
|
|
185
|
+
const connected = await readFrame(reader);
|
|
186
|
+
expect(connected.type).toBe("connected");
|
|
187
|
+
expect(connected.user).toBe(true);
|
|
188
|
+
expect(connected.network_id).toBe("net_user_a");
|
|
189
|
+
expect(connected.user_id).toBe("u_user_a");
|
|
190
|
+
|
|
191
|
+
pushUserEvent("net_user_a", "u_user_a", {
|
|
192
|
+
type: "desktop_message",
|
|
193
|
+
message_id: "dm_one",
|
|
194
|
+
from: "agent-a",
|
|
195
|
+
message: "hello",
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const evt = await readFrame(reader);
|
|
199
|
+
expect(evt.type).toBe("desktop_message");
|
|
200
|
+
expect(evt.message_id).toBe("dm_one");
|
|
201
|
+
expect(evt.from).toBe("agent-a");
|
|
202
|
+
expect(evt.message).toBe("hello");
|
|
203
|
+
expect(evt.network_id).toBe("net_user_a");
|
|
204
|
+
expect(evt.user_id).toBe("u_user_a");
|
|
205
|
+
expect(evt.scope).toBe("user");
|
|
206
|
+
await reader.cancel();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("user stream is isolated from alias and observer key spaces", async () => {
|
|
210
|
+
const userRes = createUserEventStream("net_user_iso", "u_iso");
|
|
211
|
+
const userReader = userRes.body!.getReader();
|
|
212
|
+
await readFrame(userReader);
|
|
213
|
+
|
|
214
|
+
const sessionRes = createSSEStream("u_iso", "net_user_iso");
|
|
215
|
+
const sessionReader = sessionRes.body!.getReader();
|
|
216
|
+
await readFrame(sessionReader);
|
|
217
|
+
|
|
218
|
+
const observerRes = createNetworkObserverStream("net_user_iso");
|
|
219
|
+
const observerReader = observerRes.body!.getReader();
|
|
220
|
+
await readFrame(observerReader);
|
|
221
|
+
|
|
222
|
+
pushEvent("u_iso", { type: "alias_marker", marker: "alias" }, "net_user_iso");
|
|
223
|
+
pushNetworkObserverEvent("net_user_iso", { type: "observer_marker", marker: "observer" });
|
|
224
|
+
pushUserEvent("net_user_iso", "u_iso", { type: "desktop_message", marker: "user" });
|
|
225
|
+
|
|
226
|
+
expect((await readFrame(sessionReader)).marker).toBe("alias");
|
|
227
|
+
expect((await readFrame(observerReader)).marker).toBe("observer");
|
|
228
|
+
expect((await readFrame(userReader)).marker).toBe("user");
|
|
229
|
+
|
|
230
|
+
await userReader.cancel();
|
|
231
|
+
await sessionReader.cancel();
|
|
232
|
+
await observerReader.cancel();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("same user and network fan out to multiple live clients; cancelling one leaves the other live", async () => {
|
|
236
|
+
const r1 = createUserEventStream("net_user_multi", "u_multi");
|
|
237
|
+
const r2 = createUserEventStream("net_user_multi", "u_multi");
|
|
238
|
+
const reader1 = r1.body!.getReader();
|
|
239
|
+
const reader2 = r2.body!.getReader();
|
|
240
|
+
await readFrame(reader1);
|
|
241
|
+
await readFrame(reader2);
|
|
242
|
+
|
|
243
|
+
pushUserEvent("net_user_multi", "u_multi", { type: "desktop_message", message_id: "dm_fanout_1" });
|
|
244
|
+
expect((await readFrame(reader1)).message_id).toBe("dm_fanout_1");
|
|
245
|
+
expect((await readFrame(reader2)).message_id).toBe("dm_fanout_1");
|
|
246
|
+
|
|
247
|
+
await reader1.cancel();
|
|
248
|
+
pushUserEvent("net_user_multi", "u_multi", { type: "desktop_message", message_id: "dm_fanout_2" });
|
|
249
|
+
expect((await readFrame(reader2)).message_id).toBe("dm_fanout_2");
|
|
250
|
+
await reader2.cancel();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import "./require-explicit-test-db.js";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { db } from "./db.js";
|
|
8
|
+
import { registerTools } from "./tools.js";
|
|
9
|
+
import {
|
|
10
|
+
ackOpenCodeCatalog,
|
|
11
|
+
expireOpenCodeCatalogForTest,
|
|
12
|
+
getOpenCodeCatalogResult,
|
|
13
|
+
queueOpenCodeCatalogRead,
|
|
14
|
+
resetOpenCodeCatalogForTest,
|
|
15
|
+
takeOpenCodeCatalogRequest,
|
|
16
|
+
} from "./opencode-v2-catalog.js";
|
|
17
|
+
import { readOwnedOpenCodeV2Catalog } from "../../agent-node/src/runtime/opencode-v2-catalog-daemon.ts";
|
|
18
|
+
import { readOpenCodeV2CatalogEndpoint, writeOpenCodeV2CatalogEndpoint } from "../../agent-node/src/runtime/opencode-v2-catalog-endpoint.ts";
|
|
19
|
+
|
|
20
|
+
const NET = "net_oc_catalog";
|
|
21
|
+
const OTHER = "net_oc_catalog_other";
|
|
22
|
+
const USER = "u_oc_catalog";
|
|
23
|
+
const VIEWER = "u_oc_catalog_viewer";
|
|
24
|
+
const OUTSIDER = "u_oc_catalog_out";
|
|
25
|
+
const DAEMON = "node_oc_daemon";
|
|
26
|
+
const DAEMON_ALIAS = "oc-daemon";
|
|
27
|
+
const DAEMON_TOK = "tok_oc_daemon";
|
|
28
|
+
const V2_REQ = "cr_ocv2child";
|
|
29
|
+
const V2 = "node_ocv2child";
|
|
30
|
+
const V1 = "node_ocv1child";
|
|
31
|
+
const UNSAFE = "node_ocunsafechild";
|
|
32
|
+
const NULLDIR = "node_ocnullchild";
|
|
33
|
+
const PROJECT = resolve(mkdtempSync(join(tmpdir(), "oc-hub-proj-")));
|
|
34
|
+
|
|
35
|
+
function cleanup() {
|
|
36
|
+
resetOpenCodeCatalogForTest();
|
|
37
|
+
for (const net of [NET, OTHER]) {
|
|
38
|
+
for (const table of ["node_create_requests", "audit_log", "nodes", "sessions", "api_tokens", "network_members"]) {
|
|
39
|
+
try { db.run(`DELETE FROM ${table} WHERE network_id = ?1`, [net]); } catch {}
|
|
40
|
+
}
|
|
41
|
+
try { db.run("DELETE FROM networks WHERE network_id = ?1", [net]); } catch {}
|
|
42
|
+
}
|
|
43
|
+
for (const user of [USER, VIEWER, OUTSIDER]) {
|
|
44
|
+
try { db.run("DELETE FROM users WHERE user_id = ?1", [user]); } catch {}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
beforeEach(cleanup);
|
|
48
|
+
afterAll(() => { cleanup(); rmSync(PROJECT, { recursive: true, force: true }); });
|
|
49
|
+
|
|
50
|
+
function userRow(id: string) {
|
|
51
|
+
db.run(`INSERT INTO users (user_id, username, password_hash, role, created_at) VALUES (?1, ?1, 'x', 'user', datetime('now'))`, [id]);
|
|
52
|
+
}
|
|
53
|
+
function member(id: string, net: string, role: string) {
|
|
54
|
+
db.run(`INSERT INTO network_members (user_id, network_id, role, joined_at) VALUES (?1, ?2, ?3, datetime('now'))`, [id, net, role]);
|
|
55
|
+
}
|
|
56
|
+
function node(id: string, alias: string, snapshot: unknown) {
|
|
57
|
+
db.run(
|
|
58
|
+
`INSERT INTO nodes (node_id, node_name, alias, network_id, config_snapshot, hostname, created_at, updated_at, lifecycle_state)
|
|
59
|
+
VALUES (?1, ?2, ?2, ?3, ?4, 'host-x', datetime('now'), datetime('now'), 'active')`,
|
|
60
|
+
[id, alias, NET, JSON.stringify(snapshot)],
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
function created(requestId: string, nodeId: string, flags: unknown, workdir: string | null) {
|
|
64
|
+
node(nodeId, nodeId, {});
|
|
65
|
+
db.run(
|
|
66
|
+
`INSERT INTO node_create_requests
|
|
67
|
+
(request_id, daemon_node_id, child_name, network_id, runtime, model, flags_json, env_keys, status, created_at, created_by_token, workdir)
|
|
68
|
+
VALUES (?1, ?2, ?3, ?4, 'opencode-cli', 'fixture/fixture-model', ?5, '[]', 'succeeded', ?6, 'tok', ?7)`,
|
|
69
|
+
[requestId, DAEMON, nodeId, NET, JSON.stringify(flags), Date.now(), workdir],
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function seed() {
|
|
74
|
+
userRow(USER); userRow(VIEWER); userRow(OUTSIDER);
|
|
75
|
+
db.run(`INSERT OR REPLACE INTO networks (network_id, network_name, owner_id, created_at) VALUES (?1, ?1, ?2, datetime('now'))`, [NET, USER]);
|
|
76
|
+
db.run(`INSERT OR REPLACE INTO networks (network_id, network_name, owner_id, created_at) VALUES (?1, ?1, ?2, datetime('now'))`, [OTHER, OUTSIDER]);
|
|
77
|
+
member(USER, NET, "admin");
|
|
78
|
+
member(VIEWER, NET, "viewer");
|
|
79
|
+
member(OUTSIDER, OTHER, "admin");
|
|
80
|
+
node(DAEMON, DAEMON_ALIAS, { role: "host_supervisor" });
|
|
81
|
+
db.run(
|
|
82
|
+
`INSERT INTO api_tokens (token_id, user_id, network_id, scope, name, token_hash, expires_at, revoked_at)
|
|
83
|
+
VALUES (?1, ?2, ?3, 'network', ?4, ?5, NULL, NULL)`,
|
|
84
|
+
[DAEMON_TOK, USER, NET, `node:${DAEMON_ALIAS}`, `hash_${DAEMON_TOK}`],
|
|
85
|
+
);
|
|
86
|
+
created(V2_REQ, V2, { opencodeGeneration: "v2", opencodeUnsafeTools: true }, PROJECT);
|
|
87
|
+
created("cr_ocv1child", V1, { opencodeGeneration: "v1" }, PROJECT);
|
|
88
|
+
created("cr_ocunsafechild", UNSAFE, { opencodeGeneration: "v2", opencodeUnsafeTools: false }, PROJECT);
|
|
89
|
+
created("cr_ocnullchild", NULLDIR, { opencodeGeneration: "v2", opencodeUnsafeTools: true }, null);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function queue(userId: string, nodeId: string, networkId: string | null = NET, callerIsNode = false) {
|
|
93
|
+
return queueOpenCodeCatalogRead({ callerUserId: userId, callerIsNode, networkId, daemonNodeId: DAEMON, nodeId });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handlers(userId: string, isNode: boolean, tokenId: string | null) {
|
|
97
|
+
const server = new McpServer({ name: "oc-catalog-test", version: "0" }) as any;
|
|
98
|
+
const out: Record<string, (args: any) => Promise<{ content: Array<{ text: string }> }>> = {};
|
|
99
|
+
const origTool = server.tool.bind(server);
|
|
100
|
+
server.tool = (name: string, d: string, schema: any, handler: any) => { out[name] = handler; return origTool(name, d, schema, handler); };
|
|
101
|
+
const origRegister = server.registerTool.bind(server);
|
|
102
|
+
server.registerTool = (name: string, cfg: any, handler: any) => { out[name] = handler; return origRegister(name, cfg, handler); };
|
|
103
|
+
registerTools(server, undefined, NET, userId, isNode ? DAEMON_ALIAS : null, isNode, tokenId);
|
|
104
|
+
return { out, server };
|
|
105
|
+
}
|
|
106
|
+
async function call(handler: (args: any) => Promise<{ content: Array<{ text: string }> }>, args: any) {
|
|
107
|
+
return JSON.parse((await handler(args)).content[0]!.text);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function snapshot(directory = PROJECT, overrides: Record<string, unknown> = {}) {
|
|
111
|
+
return {
|
|
112
|
+
scope: { networkId: NET, daemonId: DAEMON, directory },
|
|
113
|
+
source: "opencode-v2-api",
|
|
114
|
+
state: "snapshot",
|
|
115
|
+
invocationVerified: false,
|
|
116
|
+
completeness: "unknown",
|
|
117
|
+
observedAt: "2026-10-11T00:00:00.000Z",
|
|
118
|
+
providers: [{ id: "custom", activation: "enabled" }],
|
|
119
|
+
models: [{ providerId: "custom", id: "nested/model" }],
|
|
120
|
+
defaultModel: { providerId: "custom", id: "nested/model" },
|
|
121
|
+
...overrides,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe("hub authorizes an owned OpenCode V2 catalog read", () => {
|
|
126
|
+
test("member queues, daemon reads the owned loopback, ready snapshot stays unverified", async () => {
|
|
127
|
+
seed();
|
|
128
|
+
const user = handlers(USER, false, null);
|
|
129
|
+
const queued = await call(user.out.read_opencode_catalog!, { daemon_node_id: DAEMON, node_id: V2 });
|
|
130
|
+
expect(queued).toEqual({ ok: true, status: "pending", request_id: queued.request_id });
|
|
131
|
+
expect(JSON.stringify(queued)).not.toContain("daemon_alias");
|
|
132
|
+
const strict = await (user.server as any)._registeredTools.read_opencode_catalog.inputSchema.safeParseAsync({
|
|
133
|
+
daemon_node_id: DAEMON, node_id: V2, settings: { apiKey: "sk-FAKE" }, headers: {}, body: {},
|
|
134
|
+
});
|
|
135
|
+
expect(strict.success).toBe(false);
|
|
136
|
+
|
|
137
|
+
expect(takeOpenCodeCatalogRequest(queued.request_id, "node_other")).toEqual({ ok: false, error: "catalog_request_unavailable" });
|
|
138
|
+
const daemon = handlers(USER, true, DAEMON_TOK);
|
|
139
|
+
const pulled = await call(daemon.out.get_opencode_catalog_request!, { request_id: queued.request_id });
|
|
140
|
+
expect(pulled.ok).toBe(true);
|
|
141
|
+
expect(pulled.directory).toBe(PROJECT);
|
|
142
|
+
for (const key of ["url", "authorization", "settings", "headers", "body"]) expect(pulled[key]).toBeUndefined();
|
|
143
|
+
|
|
144
|
+
let requests = 0;
|
|
145
|
+
const server = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch(req) {
|
|
146
|
+
requests++;
|
|
147
|
+
const url = new URL(req.url);
|
|
148
|
+
const row = url.pathname === "/api/provider"
|
|
149
|
+
? [{ id: "custom", activation: "enabled", settings: { apiKey: "sk-FAKE" } }]
|
|
150
|
+
: [{ providerID: "custom", id: "nested/model", headers: { Authorization: "sk-FAKE" } }];
|
|
151
|
+
return Response.json({ location: { directory: PROJECT }, data: url.pathname === "/api/model/default" ? row[0] : row });
|
|
152
|
+
} });
|
|
153
|
+
const nodeDir = mkdtempSync(join(tmpdir(), "oc-hub-node-"));
|
|
154
|
+
try {
|
|
155
|
+
writeOpenCodeV2CatalogEndpoint(nodeDir, { url: `http://127.0.0.1:${server.port}`, authorization: "Basic fake", directory: PROJECT });
|
|
156
|
+
const read = await readOwnedOpenCodeV2Catalog({
|
|
157
|
+
networkId: NET, daemonId: DAEMON, authorizedDirectory: pulled.directory,
|
|
158
|
+
ownedProjectDirectory: PROJECT, endpoint: readOpenCodeV2CatalogEndpoint(nodeDir),
|
|
159
|
+
});
|
|
160
|
+
expect(read.ok).toBe(true);
|
|
161
|
+
if (!read.ok) return;
|
|
162
|
+
const acked = await call(daemon.out.ack_opencode_catalog!, { request_id: queued.request_id, snapshot: read.snapshot });
|
|
163
|
+
expect(acked).toEqual({ ok: true, status: "ready" });
|
|
164
|
+
const got = await call(user.out.get_opencode_catalog!, { request_id: queued.request_id });
|
|
165
|
+
expect(got.status).toBe("ready");
|
|
166
|
+
expect(got.snapshot.invocationVerified).toBe(false);
|
|
167
|
+
expect(got.snapshot.completeness).toBe("unknown");
|
|
168
|
+
expect(JSON.stringify(got.snapshot)).not.toContain("sk-FAKE");
|
|
169
|
+
expect(requests).toBe(3);
|
|
170
|
+
} finally {
|
|
171
|
+
server.stop(true);
|
|
172
|
+
rmSync(nodeDir, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("viewer may queue; outsider, node token, and non-v2 children may not", () => {
|
|
177
|
+
seed();
|
|
178
|
+
expect(queue(VIEWER, V2).ok).toBe(true);
|
|
179
|
+
expect(queue(OUTSIDER, V2, NET)).toEqual({ ok: false, error: "access_denied" });
|
|
180
|
+
expect(queue(OUTSIDER, V2, OTHER)).toEqual({ ok: false, error: "daemon_not_found" });
|
|
181
|
+
expect(queue(USER, V2, NET, true)).toEqual({ ok: false, error: "user_token_required" });
|
|
182
|
+
expect(queue(USER, V1)).toEqual({ ok: false, error: "not_opencode_v2" });
|
|
183
|
+
expect(queue(USER, UNSAFE)).toEqual({ ok: false, error: "not_opencode_v2" });
|
|
184
|
+
expect(queue(USER, "node_missing")).toEqual({ ok: false, error: "node_not_found" });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("pending is not a snapshot; expiry, bad acks, and a missing endpoint stay closed", async () => {
|
|
188
|
+
seed();
|
|
189
|
+
const queued = queue(USER, V2);
|
|
190
|
+
expect(queued.ok).toBe(true);
|
|
191
|
+
if (!queued.ok) return;
|
|
192
|
+
expect(getOpenCodeCatalogResult({ callerUserId: USER, callerIsNode: false, networkId: NET, requestId: queued.request_id })).toEqual({
|
|
193
|
+
ok: true, status: "pending", request_id: queued.request_id,
|
|
194
|
+
});
|
|
195
|
+
const pulled = takeOpenCodeCatalogRequest(queued.request_id, DAEMON);
|
|
196
|
+
expect(pulled.ok).toBe(true);
|
|
197
|
+
|
|
198
|
+
expect(ackOpenCodeCatalog(DAEMON, { request_id: queued.request_id, snapshot: snapshot(), settings: { apiKey: "sk-FAKE" } })).toEqual({ ok: false, error: "invalid_ack" });
|
|
199
|
+
expect(ackOpenCodeCatalog(DAEMON, { request_id: queued.request_id, snapshot: snapshot(PROJECT, { invocationVerified: true }) })).toEqual({ ok: false, error: "invalid_ack" });
|
|
200
|
+
expect(getOpenCodeCatalogResult({ callerUserId: USER, callerIsNode: false, networkId: NET, requestId: queued.request_id }).status).toBe("pending");
|
|
201
|
+
|
|
202
|
+
const wrongDir = ackOpenCodeCatalog(DAEMON, { request_id: queued.request_id, snapshot: snapshot("/tmp/other-project") });
|
|
203
|
+
expect(wrongDir).toEqual({ ok: true, status: "failed", error: "invalid_response" });
|
|
204
|
+
expect(getOpenCodeCatalogResult({ callerUserId: USER, callerIsNode: false, networkId: NET, requestId: queued.request_id })).toMatchObject({
|
|
205
|
+
ok: false, status: "failed", error: "invalid_response",
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const again = queue(USER, V2);
|
|
209
|
+
expect(again.ok).toBe(true);
|
|
210
|
+
if (!again.ok) return;
|
|
211
|
+
takeOpenCodeCatalogRequest(again.request_id, DAEMON);
|
|
212
|
+
expect(ackOpenCodeCatalog(DAEMON, { request_id: again.request_id, error: "unavailable" })).toEqual({ ok: true, status: "failed", error: "unavailable" });
|
|
213
|
+
expect(getOpenCodeCatalogResult({ callerUserId: USER, callerIsNode: false, networkId: NET, requestId: again.request_id })).toMatchObject({
|
|
214
|
+
ok: false, status: "failed", error: "unavailable",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const waiting = queue(USER, NULLDIR);
|
|
218
|
+
expect(waiting.ok).toBe(true);
|
|
219
|
+
if (!waiting.ok) return;
|
|
220
|
+
expireOpenCodeCatalogForTest(waiting.request_id);
|
|
221
|
+
expect(getOpenCodeCatalogResult({ callerUserId: USER, callerIsNode: false, networkId: NET, requestId: waiting.request_id })).toMatchObject({
|
|
222
|
+
ok: false, status: "expired", error: "unavailable",
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("acks a Windows absolute directory without Hub path.resolve", () => {
|
|
227
|
+
seed();
|
|
228
|
+
db.run(`UPDATE node_create_requests SET workdir = ?1 WHERE request_id = ?2`, ["C:\\project", V2_REQ]);
|
|
229
|
+
const queued = queue(USER, V2);
|
|
230
|
+
expect(queued.ok).toBe(true);
|
|
231
|
+
if (!queued.ok) return;
|
|
232
|
+
takeOpenCodeCatalogRequest(queued.request_id, DAEMON);
|
|
233
|
+
expect(ackOpenCodeCatalog(DAEMON, { request_id: queued.request_id, snapshot: snapshot("project") })).toEqual({ ok: false, error: "invalid_ack" });
|
|
234
|
+
expect(ackOpenCodeCatalog(DAEMON, { request_id: queued.request_id, snapshot: snapshot("C:relative") })).toEqual({ ok: false, error: "invalid_ack" });
|
|
235
|
+
expect(getOpenCodeCatalogResult({ callerUserId: USER, callerIsNode: false, networkId: NET, requestId: queued.request_id }).status).toBe("pending");
|
|
236
|
+
expect(ackOpenCodeCatalog(DAEMON, { request_id: queued.request_id, snapshot: snapshot("C:/project") })).toEqual({ ok: true, status: "ready" });
|
|
237
|
+
const got = getOpenCodeCatalogResult({ callerUserId: USER, callerIsNode: false, networkId: NET, requestId: queued.request_id });
|
|
238
|
+
expect(got.ok).toBe(true);
|
|
239
|
+
if (!got.ok || got.status !== "ready") return;
|
|
240
|
+
expect(got.snapshot.scope.directory).toBe("C:/project");
|
|
241
|
+
|
|
242
|
+
const other = queue(USER, V2);
|
|
243
|
+
expect(other.ok).toBe(true);
|
|
244
|
+
if (!other.ok) return;
|
|
245
|
+
takeOpenCodeCatalogRequest(other.request_id, DAEMON);
|
|
246
|
+
expect(ackOpenCodeCatalog(DAEMON, { request_id: other.request_id, snapshot: snapshot("D:\\other") })).toEqual({
|
|
247
|
+
ok: true, status: "failed", error: "invalid_response",
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("a tilde workdir matches a native absolute report, and lexical .. collapses", () => {
|
|
252
|
+
seed();
|
|
253
|
+
db.run(`UPDATE node_create_requests SET workdir = ?1 WHERE request_id = ?2`, ["~/project", V2_REQ]);
|
|
254
|
+
const queued = queue(USER, V2);
|
|
255
|
+
expect(queued.ok).toBe(true);
|
|
256
|
+
if (!queued.ok) return;
|
|
257
|
+
takeOpenCodeCatalogRequest(queued.request_id, DAEMON);
|
|
258
|
+
expect(ackOpenCodeCatalog(DAEMON, { request_id: queued.request_id, snapshot: snapshot("/home/user/project") })).toEqual({ ok: true, status: "ready" });
|
|
259
|
+
|
|
260
|
+
db.run(`UPDATE node_create_requests SET workdir = ?1 WHERE request_id = ?2`, ["/tmp/foo/../bar", V2_REQ]);
|
|
261
|
+
const lexical = queue(USER, V2);
|
|
262
|
+
expect(lexical.ok).toBe(true);
|
|
263
|
+
if (!lexical.ok) return;
|
|
264
|
+
takeOpenCodeCatalogRequest(lexical.request_id, DAEMON);
|
|
265
|
+
expect(ackOpenCodeCatalog(DAEMON, { request_id: lexical.request_id, snapshot: snapshot("/tmp/foo/../bar") })).toEqual({ ok: false, error: "invalid_ack" });
|
|
266
|
+
expect(ackOpenCodeCatalog(DAEMON, { request_id: lexical.request_id, snapshot: snapshot("/tmp/bar") })).toEqual({ ok: true, status: "ready" });
|
|
267
|
+
});
|
|
268
|
+
});
|