@sleep2agi/commhub-server 0.9.0-preview.12 → 0.9.0-preview.121
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 +92 -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/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-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 +217 -15
- package/src/create-node-workdir.test.ts +289 -0
- package/src/create-node.ts +77 -23
- package/src/daemon-provider-snapshots.test.ts +256 -0
- package/src/daemon-provider-snapshots.ts +226 -0
- package/src/daemon-token-owner-http.test.ts +231 -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 +903 -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-timestamp.test.ts +38 -0
- package/src/hub-timestamp.ts +27 -0
- package/src/human-dm.ts +215 -0
- package/src/inbox-count-sse.test.ts +310 -0
- package/src/inbox-count.ts +29 -0
- package/src/index.ts +15 -2356
- package/src/lifecycle-guard.ts +1 -1
- package/src/mcp-context-cost-http.test.ts +120 -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 +429 -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 +714 -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 +283 -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 +36 -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/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/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 +5253 -0
- package/src/session-task-on-dispatch.ts +14 -0
- package/src/shared/codex-inventory.ts +34 -0
- package/src/shared/daemon-provider-snapshot.ts +49 -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 +24 -0
- package/src/shared/reserved-env-drift.test.ts +10 -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 +469 -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 +222 -0
- package/src/tool-audience.ts +116 -0
- package/src/tools-list-json-schema.test.ts +67 -0
- package/src/tools.ts +3598 -313
- package/src/upload-early-reject-guard.test.ts +155 -0
- package/src/uploads-http.test.ts +97 -15
- package/src/uploads.test.ts +66 -0
- package/src/uploads.ts +138 -5
- 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,77 @@
|
|
|
1
|
+
/** Shared Codex node `flags.*` contract (create_node + update_node_config). */
|
|
2
|
+
|
|
3
|
+
export const REASONING_EFFORT_VALUES = [
|
|
4
|
+
"none",
|
|
5
|
+
"minimal",
|
|
6
|
+
"low",
|
|
7
|
+
"medium",
|
|
8
|
+
"high",
|
|
9
|
+
"xhigh",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export type ModelReasoningEffort = (typeof REASONING_EFFORT_VALUES)[number];
|
|
13
|
+
|
|
14
|
+
export const CODEX_APPROVAL_POLICIES = [
|
|
15
|
+
"untrusted",
|
|
16
|
+
"on-failure",
|
|
17
|
+
"on-request",
|
|
18
|
+
"never",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export const CODEX_SANDBOX_MODES = [
|
|
22
|
+
"read-only",
|
|
23
|
+
"workspace-write",
|
|
24
|
+
"danger-full-access",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
/** Runtimes that may carry Codex posture / reasoning flags. */
|
|
28
|
+
export const CODEX_CONFIG_FLAG_RUNTIMES = new Set<string>(["codex-sdk", "codex-app-server"]);
|
|
29
|
+
|
|
30
|
+
export const CODEX_CONFIG_ONLY_FLAG_KEYS = new Set<string>([
|
|
31
|
+
"approvalPolicy",
|
|
32
|
+
"sandboxMode",
|
|
33
|
+
"skipGitRepoCheck",
|
|
34
|
+
"copresenceFullAccess",
|
|
35
|
+
"modelReasoningEffort",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export function isModelReasoningEffort(v: unknown): v is ModelReasoningEffort {
|
|
39
|
+
return typeof v === "string" && (REASONING_EFFORT_VALUES as readonly string[]).includes(v);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateCodexConfigFlag(
|
|
43
|
+
key: string,
|
|
44
|
+
val: unknown,
|
|
45
|
+
runtime: string | null | undefined,
|
|
46
|
+
): { field: string; reason: string } | null {
|
|
47
|
+
if (!CODEX_CONFIG_ONLY_FLAG_KEYS.has(key)) return null;
|
|
48
|
+
if (runtime && !CODEX_CONFIG_FLAG_RUNTIMES.has(runtime)) {
|
|
49
|
+
return { field: `flags.${key}`, reason: `only supported for codex-sdk / codex-app-server runtimes` };
|
|
50
|
+
}
|
|
51
|
+
if (key === "copresenceFullAccess" && runtime && runtime !== "codex-app-server") {
|
|
52
|
+
return { field: "flags.copresenceFullAccess", reason: "only supported when node runtime is codex-app-server" };
|
|
53
|
+
}
|
|
54
|
+
switch (key) {
|
|
55
|
+
case "approvalPolicy":
|
|
56
|
+
if (typeof val !== "string" || !(CODEX_APPROVAL_POLICIES as readonly string[]).includes(val)) {
|
|
57
|
+
return { field: "flags.approvalPolicy", reason: `must be one of ${CODEX_APPROVAL_POLICIES.join("/")}` };
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
case "sandboxMode":
|
|
61
|
+
if (typeof val !== "string" || !(CODEX_SANDBOX_MODES as readonly string[]).includes(val)) {
|
|
62
|
+
return { field: "flags.sandboxMode", reason: `must be one of ${CODEX_SANDBOX_MODES.join("/")}` };
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
case "skipGitRepoCheck":
|
|
66
|
+
case "copresenceFullAccess":
|
|
67
|
+
if (typeof val !== "boolean") return { field: `flags.${key}`, reason: "must be boolean" };
|
|
68
|
+
return null;
|
|
69
|
+
case "modelReasoningEffort":
|
|
70
|
+
if (!isModelReasoningEffort(val)) {
|
|
71
|
+
return { field: "flags.modelReasoningEffort", reason: `must be one of ${REASONING_EFFORT_VALUES.join("/")}` };
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
default:
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
export const forkRecoveryRequestSchema = z.object({
|
|
4
|
+
kind: z.literal("fork_on_missing_ordinal"), confirmed: z.literal(true),
|
|
5
|
+
}).strict();
|
|
6
|
+
const threadId = z.string().length(36).regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
7
|
+
export const forkRecoveryResultSchema = z.discriminatedUnion("state", [
|
|
8
|
+
z.object({ state: z.literal("forked"), old_thread_id: threadId, new_thread_id: threadId }).strict(),
|
|
9
|
+
z.object({ state: z.literal("not_observed") }).strict(),
|
|
10
|
+
z.object({ state: z.literal("unknown"), reason: z.enum([
|
|
11
|
+
"invalid_request_id", "history_unreadable", "history_invalid", "request_ambiguous", "mapping_invalid",
|
|
12
|
+
"result_not_reported", "result_invalid",
|
|
13
|
+
]) }).strict(),
|
|
14
|
+
]).refine(r => r.state !== "forked" || r.old_thread_id !== r.new_thread_id);
|
|
15
|
+
|
|
16
|
+
// Read paths re-validate stored data, not just the write-time MCP schema.
|
|
17
|
+
export function publicForkResult(raw: unknown) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = forkRecoveryResultSchema.safeParse(typeof raw === "string" ? JSON.parse(raw) : raw);
|
|
20
|
+
if (parsed.success) return parsed.data;
|
|
21
|
+
} catch { /* corrupted/legacy storage must not expose arbitrary fields */ }
|
|
22
|
+
return { state: "unknown", reason: raw == null ? "result_not_reported" : "result_invalid" } as const;
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
test("legacy start table upgrades twice without changing ordinary rows or losing fork receipts", () => {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), "fork-migration-"));
|
|
11
|
+
const path = join(root, "legacy.db");
|
|
12
|
+
try {
|
|
13
|
+
let db = new Database(path);
|
|
14
|
+
db.exec(`CREATE TABLE node_start_requests (
|
|
15
|
+
request_id TEXT PRIMARY KEY, network_id TEXT NOT NULL, daemon_node_id TEXT NOT NULL,
|
|
16
|
+
child_node_id TEXT NOT NULL, child_alias TEXT NOT NULL, created_by_token TEXT NOT NULL,
|
|
17
|
+
status TEXT NOT NULL DEFAULT 'pending', error TEXT, child_pid INTEGER,
|
|
18
|
+
created_at INTEGER NOT NULL, delivered_at INTEGER, acked_at INTEGER);
|
|
19
|
+
INSERT INTO node_start_requests(request_id,network_id,daemon_node_id,child_node_id,child_alias,created_by_token,status,created_at)
|
|
20
|
+
VALUES('str_legacy','net_fixture','node_daemon','node_child','fixture','fixture','started',1);`);
|
|
21
|
+
db.close();
|
|
22
|
+
const migrate = () => execFileSync(process.execPath, ["-e",
|
|
23
|
+
`await import(${JSON.stringify(fileURLToPath(new URL("./db.ts", import.meta.url)))}); process.exit(0);`],
|
|
24
|
+
{ env: { ...process.env, COMMHUB_DB: path }, timeout: 15_000, stdio: "pipe" });
|
|
25
|
+
migrate();
|
|
26
|
+
db = new Database(path);
|
|
27
|
+
expect(db.query("SELECT status,fork_recovery_json,fork_result_json FROM node_start_requests WHERE request_id='str_legacy'").get())
|
|
28
|
+
.toEqual({ status: "started", fork_recovery_json: null, fork_result_json: null });
|
|
29
|
+
const receipt = JSON.stringify({ state: "not_observed" });
|
|
30
|
+
db.query("UPDATE node_start_requests SET fork_result_json=? WHERE request_id='str_legacy'").run(receipt);
|
|
31
|
+
db.close();
|
|
32
|
+
migrate();
|
|
33
|
+
db = new Database(path);
|
|
34
|
+
expect(db.query("SELECT fork_result_json FROM node_start_requests WHERE request_id='str_legacy'").get())
|
|
35
|
+
.toEqual({ fork_result_json: receipt });
|
|
36
|
+
db.close();
|
|
37
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
38
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
// commhub-server must refuse what it does not understand instead of starting a
|
|
8
|
+
// Hub on the defaults (the live ~/.commhub/commhub.db). Every child gets a
|
|
9
|
+
// throwaway HOME and a throwaway PORT, so even a regression binds nothing real.
|
|
10
|
+
const BIN = join(dirname(fileURLToPath(import.meta.url)), "..", "bin", "commhub.ts");
|
|
11
|
+
const PKG_VERSION = JSON.parse(readFileSync(join(dirname(BIN), "..", "package.json"), "utf8")).version as string;
|
|
12
|
+
const home = mkdtempSync(join(tmpdir(), "anet-bin-args-"));
|
|
13
|
+
const PORT = String(26000 + Math.floor(Math.random() * 3000));
|
|
14
|
+
|
|
15
|
+
afterAll(() => rmSync(home, { recursive: true, force: true }));
|
|
16
|
+
|
|
17
|
+
function run(args: string[], extraEnv: Record<string, string> = {}, timeoutMs = 15_000) {
|
|
18
|
+
const proc = Bun.spawnSync(["bun", BIN, ...args], {
|
|
19
|
+
env: { PATH: process.env.PATH ?? "", HOME: home, PORT, HOST: "127.0.0.1", ...extraEnv },
|
|
20
|
+
stdout: "pipe", stderr: "pipe", timeout: timeoutMs,
|
|
21
|
+
});
|
|
22
|
+
return { code: proc.exitCode, stdout: proc.stdout.toString(), stderr: proc.stderr.toString() };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function listening(port: string): Promise<boolean> {
|
|
26
|
+
try { await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(500) }); return true; } catch { return false; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("commhub-server argument handling", () => {
|
|
30
|
+
for (const [args, message] of [
|
|
31
|
+
[["bogus"], "unknown command bogus"],
|
|
32
|
+
[["--port=29999"], "unknown option --port=29999"],
|
|
33
|
+
[["--bogus-flag"], "unknown option --bogus-flag"],
|
|
34
|
+
[["--port"], "--port needs a value"],
|
|
35
|
+
[["--db", "--dev-open"], "--db needs a value"],
|
|
36
|
+
] as const) {
|
|
37
|
+
test(`${args.join(" ")} → exit 2, no database, nothing listening`, async () => {
|
|
38
|
+
const r = run([...args]);
|
|
39
|
+
expect(r.code).toBe(2);
|
|
40
|
+
expect(r.stderr).toContain(message);
|
|
41
|
+
expect(r.stderr).toContain("commhub-server --help");
|
|
42
|
+
expect(existsSync(join(home, ".commhub"))).toBe(false);
|
|
43
|
+
expect(await listening(PORT)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test("migrate-to-pg with missing or unknown arguments exits 2 and starts nothing", async () => {
|
|
48
|
+
const missing = run(["migrate-to-pg", "--from", "/x.db"]);
|
|
49
|
+
expect(missing.code).toBe(2);
|
|
50
|
+
expect(missing.stdout).toContain("commhub-server migrate-to-pg --from <sqlite file> --to <postgres url>");
|
|
51
|
+
const unknown = run(["migrate-to-pg", "--from", "/x.db", "--to", "postgres://u@127.0.0.1/anet_x_test", "--bogus"]);
|
|
52
|
+
expect(unknown.code).toBe(2);
|
|
53
|
+
expect(unknown.stderr).toContain("unknown argument --bogus");
|
|
54
|
+
expect(existsSync(join(home, ".commhub"))).toBe(false);
|
|
55
|
+
expect(await listening(PORT)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("--help and --version exit 0 without starting", async () => {
|
|
59
|
+
const help = run(["--help"]);
|
|
60
|
+
expect(help.code).toBe(0);
|
|
61
|
+
expect(help.stdout).toContain("--port, -p <port>");
|
|
62
|
+
const version = run(["--version"]);
|
|
63
|
+
expect(version.code).toBe(0);
|
|
64
|
+
expect(version.stdout.trim()).toBe(PKG_VERSION);
|
|
65
|
+
expect(existsSync(join(home, ".commhub"))).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("the known forms still start a Hub", async () => {
|
|
69
|
+
const db = join(home, "known.db");
|
|
70
|
+
const port = String(Number(PORT) + 1);
|
|
71
|
+
const child = Bun.spawn(["bun", BIN, "--port", port, "--host", "127.0.0.1", "--db", db, "--dev-open"], {
|
|
72
|
+
env: { PATH: process.env.PATH ?? "", HOME: home }, stdout: "pipe", stderr: "pipe",
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
let up = false;
|
|
76
|
+
for (let i = 0; i < 100 && !up; i++) { up = await listening(port); if (!up) await Bun.sleep(100); }
|
|
77
|
+
expect(up).toBe(true);
|
|
78
|
+
expect(existsSync(db)).toBe(true);
|
|
79
|
+
expect(existsSync(join(home, ".commhub", "commhub.db"))).toBe(false);
|
|
80
|
+
} finally {
|
|
81
|
+
child.kill();
|
|
82
|
+
await child.exited;
|
|
83
|
+
}
|
|
84
|
+
}, 30_000);
|
|
85
|
+
});
|
|
@@ -721,3 +721,110 @@ describe("SEC-1 — update lifecycle: single-flight + cross-network pending look
|
|
|
721
721
|
expect(inFlight).toBeDefined();
|
|
722
722
|
});
|
|
723
723
|
});
|
|
724
|
+
|
|
725
|
+
// ── #260 P5 codex-catch regression: channels finalize ─────────────
|
|
726
|
+
describe("finalizePendingMatchingUpdates — #260 P5 channels content-match", () => {
|
|
727
|
+
function insertPendingUpdate(opts: {
|
|
728
|
+
update_id?: string;
|
|
729
|
+
node_id: string;
|
|
730
|
+
network_id: string;
|
|
731
|
+
patch: any;
|
|
732
|
+
apply_mode: "hot" | "restart" | "restart_only";
|
|
733
|
+
status?: "pending" | "restarting";
|
|
734
|
+
}): string {
|
|
735
|
+
const id = opts.update_id ?? `cu_${uuidv4()}`;
|
|
736
|
+
db.run(
|
|
737
|
+
`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, ?4, ?5, 0, ?6, ?7, 'test')`,
|
|
738
|
+
[id, opts.node_id, opts.network_id, JSON.stringify(opts.patch), opts.apply_mode, opts.status ?? "pending", Date.now()],
|
|
739
|
+
);
|
|
740
|
+
return id;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
test("channels-only patch: snapshot BEFORE apply (first boot) must NOT finalize", () => {
|
|
744
|
+
const nid = insertNode({ alias: "n-chan-1", network_id: "netA" });
|
|
745
|
+
const uid = insertPendingUpdate({
|
|
746
|
+
node_id: nid, network_id: "netA",
|
|
747
|
+
// Note the empty flags object — the pre-fix bug was that this
|
|
748
|
+
// trivially matched the flags loop and hub falsely finalized.
|
|
749
|
+
patch: { flags: {}, channels: ["feishu"] },
|
|
750
|
+
apply_mode: "restart",
|
|
751
|
+
});
|
|
752
|
+
// Node's startup snapshot BEFORE it consumed the update — no
|
|
753
|
+
// channels forked yet, so buildConfigSnapshot emits channels=[].
|
|
754
|
+
const r = finalizePendingMatchingUpdates(nid, { model: "x", flags: {}, channels: [] });
|
|
755
|
+
expect(r.finalizedCount).toBe(0);
|
|
756
|
+
expect(db.get<any>("SELECT status FROM node_config_updates WHERE update_id = ?1", uid).status).toBe("pending");
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test("channels-only patch: snapshot AFTER apply (restart complete) finalizes", () => {
|
|
760
|
+
const nid = insertNode({ alias: "n-chan-2", network_id: "netA" });
|
|
761
|
+
const uid = insertPendingUpdate({
|
|
762
|
+
node_id: nid, network_id: "netA",
|
|
763
|
+
patch: { flags: {}, channels: ["feishu"] },
|
|
764
|
+
apply_mode: "restart",
|
|
765
|
+
});
|
|
766
|
+
const r = finalizePendingMatchingUpdates(nid, { model: "x", flags: {}, channels: ["feishu"] });
|
|
767
|
+
expect(r.finalizedCount).toBe(1);
|
|
768
|
+
expect(r.finalizedIds).toContain(uid);
|
|
769
|
+
expect(db.get<any>("SELECT status FROM node_config_updates WHERE update_id = ?1", uid).status).toBe("applied");
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
test("channels set-equality: order doesn't matter", () => {
|
|
773
|
+
const nid = insertNode({ alias: "n-chan-3", network_id: "netA" });
|
|
774
|
+
insertPendingUpdate({
|
|
775
|
+
node_id: nid, network_id: "netA",
|
|
776
|
+
patch: { flags: {}, channels: ["telegram", "feishu"] },
|
|
777
|
+
apply_mode: "restart",
|
|
778
|
+
});
|
|
779
|
+
// Snapshot order swapped — still matches.
|
|
780
|
+
const r = finalizePendingMatchingUpdates(nid, { model: "x", flags: {}, channels: ["feishu", "telegram"] });
|
|
781
|
+
expect(r.finalizedCount).toBe(1);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test("channels: [] disable-all — finalize when snapshot also [] (post-restart)", () => {
|
|
785
|
+
const nid = insertNode({ alias: "n-chan-4", network_id: "netA" });
|
|
786
|
+
insertPendingUpdate({
|
|
787
|
+
node_id: nid, network_id: "netA",
|
|
788
|
+
patch: { flags: {}, channels: [] },
|
|
789
|
+
apply_mode: "restart",
|
|
790
|
+
});
|
|
791
|
+
const r = finalizePendingMatchingUpdates(nid, { model: "x", flags: {}, channels: [] });
|
|
792
|
+
expect(r.finalizedCount).toBe(1);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("channels: [] disable-all — do NOT finalize when snapshot still has channels", () => {
|
|
796
|
+
const nid = insertNode({ alias: "n-chan-5", network_id: "netA" });
|
|
797
|
+
insertPendingUpdate({
|
|
798
|
+
node_id: nid, network_id: "netA",
|
|
799
|
+
patch: { flags: {}, channels: [] },
|
|
800
|
+
apply_mode: "restart",
|
|
801
|
+
});
|
|
802
|
+
// Node hasn't restarted yet — old telegram worker still forked.
|
|
803
|
+
const r = finalizePendingMatchingUpdates(nid, { model: "x", flags: {}, channels: ["telegram"] });
|
|
804
|
+
expect(r.finalizedCount).toBe(0);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("snapshot omits channels field entirely (pre-P5 agent-node) → NOT finalized on channels-carrying patch", () => {
|
|
808
|
+
const nid = insertNode({ alias: "n-chan-6", network_id: "netA" });
|
|
809
|
+
insertPendingUpdate({
|
|
810
|
+
node_id: nid, network_id: "netA",
|
|
811
|
+
patch: { flags: {}, channels: ["feishu"] },
|
|
812
|
+
apply_mode: "restart",
|
|
813
|
+
});
|
|
814
|
+
const r = finalizePendingMatchingUpdates(nid, { model: "x", flags: {} }); // no channels key
|
|
815
|
+
expect(r.finalizedCount).toBe(0);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test("model + channels compound match: both must satisfy", () => {
|
|
819
|
+
const nid = insertNode({ alias: "n-chan-7", network_id: "netA" });
|
|
820
|
+
insertPendingUpdate({
|
|
821
|
+
node_id: nid, network_id: "netA",
|
|
822
|
+
patch: { model: "opus-new", flags: {}, channels: ["telegram"] },
|
|
823
|
+
apply_mode: "restart",
|
|
824
|
+
});
|
|
825
|
+
// Wrong channels → not finalized even though model matches.
|
|
826
|
+
expect(finalizePendingMatchingUpdates(nid, { model: "opus-new", flags: {}, channels: [] }).finalizedCount).toBe(0);
|
|
827
|
+
// Both match → finalized.
|
|
828
|
+
expect(finalizePendingMatchingUpdates(nid, { model: "opus-new", flags: {}, channels: ["telegram"] }).finalizedCount).toBe(1);
|
|
829
|
+
});
|
|
830
|
+
});
|
|
@@ -9,9 +9,11 @@ import { describe, expect, test } from "bun:test";
|
|
|
9
9
|
import {
|
|
10
10
|
ALLOWED_FLAGS,
|
|
11
11
|
SECURITY_SENSITIVE_FLAGS,
|
|
12
|
+
EDITABLE_CHANNELS,
|
|
12
13
|
isAllowedToChangeFlag,
|
|
13
14
|
computeApplyMode,
|
|
14
15
|
validatePatch,
|
|
16
|
+
narrowChannelsPatch,
|
|
15
17
|
} from "./config-apply-validate";
|
|
16
18
|
|
|
17
19
|
describe("ALLOWED_FLAGS — exact contract (no silent extension)", () => {
|
|
@@ -24,20 +26,28 @@ describe("ALLOWED_FLAGS — exact contract (no silent extension)", () => {
|
|
|
24
26
|
// the BLOCKER 2 schema-mismatch issue. P2 to add a claude-code-cli
|
|
25
27
|
// config-apply path.
|
|
26
28
|
expect([...ALLOWED_FLAGS].sort()).toEqual([
|
|
29
|
+
"approvalPolicy",
|
|
27
30
|
"budget",
|
|
31
|
+
"copresenceFullAccess",
|
|
28
32
|
"dangerouslySkipPermissions",
|
|
29
33
|
"maxTurns",
|
|
34
|
+
"modelReasoningEffort",
|
|
30
35
|
"permissionMode",
|
|
36
|
+
"sandboxMode",
|
|
37
|
+
"skipGitRepoCheck",
|
|
31
38
|
"timeout",
|
|
32
39
|
]);
|
|
33
40
|
});
|
|
34
41
|
});
|
|
35
42
|
|
|
36
43
|
describe("SECURITY_SENSITIVE_FLAGS — privilege-elevation surface", () => {
|
|
37
|
-
test("contains
|
|
44
|
+
test("contains privilege-elevation flags (teammateMode dropped — see ALLOWED_FLAGS comment)", () => {
|
|
38
45
|
expect([...SECURITY_SENSITIVE_FLAGS].sort()).toEqual([
|
|
46
|
+
"approvalPolicy",
|
|
47
|
+
"copresenceFullAccess",
|
|
39
48
|
"dangerouslySkipPermissions",
|
|
40
49
|
"permissionMode",
|
|
50
|
+
"sandboxMode",
|
|
41
51
|
]);
|
|
42
52
|
});
|
|
43
53
|
|
|
@@ -159,6 +169,10 @@ describe("computeApplyMode — tier classifier (RFC-024 §4)", () => {
|
|
|
159
169
|
expect(computeApplyMode("gpt-5", { maxTurns: 50 })).toBe("restart");
|
|
160
170
|
});
|
|
161
171
|
|
|
172
|
+
test("modelReasoningEffort flag alone → restart tier", () => {
|
|
173
|
+
expect(computeApplyMode(undefined, { modelReasoningEffort: "medium" })).toBe("restart");
|
|
174
|
+
});
|
|
175
|
+
|
|
162
176
|
test("mixed (only hot flags) → hot", () => {
|
|
163
177
|
expect(computeApplyMode(undefined, { maxTurns: 50, budget: 100 })).toBe("hot");
|
|
164
178
|
});
|
|
@@ -247,6 +261,15 @@ describe("validatePatch — per-flag enum / range", () => {
|
|
|
247
261
|
expect(validatePatch(undefined, { timeout: 3_600_001 })?.field).toBe("flags.timeout");
|
|
248
262
|
expect(validatePatch(undefined, { timeout: 60.5 })?.field).toBe("flags.timeout");
|
|
249
263
|
});
|
|
264
|
+
test("timeout: 0 = no limit; 1..999 is seconds-shaped and rejected (#605)", () => {
|
|
265
|
+
expect(validatePatch(undefined, { timeout: 0 })).toBeNull();
|
|
266
|
+
expect(validatePatch(undefined, { timeout: 1000 })).toBeNull();
|
|
267
|
+
for (const v of [1, 600, 999]) {
|
|
268
|
+
const r = validatePatch(undefined, { timeout: v });
|
|
269
|
+
expect(r?.field).toBe("flags.timeout");
|
|
270
|
+
expect(r?.reason).toContain("milliseconds");
|
|
271
|
+
}
|
|
272
|
+
});
|
|
250
273
|
});
|
|
251
274
|
|
|
252
275
|
describe("validatePatch — combinations", () => {
|
|
@@ -258,3 +281,113 @@ describe("validatePatch — combinations", () => {
|
|
|
258
281
|
expect(r?.field).toBe("flags.maxTurns");
|
|
259
282
|
});
|
|
260
283
|
});
|
|
284
|
+
|
|
285
|
+
// ── #260 P5 — channels support (restart-tier, non-security-sensitive) ─
|
|
286
|
+
describe("EDITABLE_CHANNELS — mirrors agent-node runtime capability (cli.ts:671-676)", () => {
|
|
287
|
+
test("contains exactly telegram + feishu (channels agent-node actually forks workers for)", () => {
|
|
288
|
+
expect(EDITABLE_CHANNELS.size).toBe(2);
|
|
289
|
+
expect(EDITABLE_CHANNELS.has("telegram")).toBe(true);
|
|
290
|
+
expect(EDITABLE_CHANNELS.has("feishu")).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
test("commhub is NOT editable — it's the RPC transport, not a per-node channel worker", () => {
|
|
293
|
+
expect(EDITABLE_CHANNELS.has("commhub")).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
test("wechat is NOT editable — roadmap only on dashboard", () => {
|
|
296
|
+
expect(EDITABLE_CHANNELS.has("wechat")).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("narrowChannelsPatch — hostile input hygiene at the wire boundary", () => {
|
|
301
|
+
test("straight allow-listed input passes verbatim", () => {
|
|
302
|
+
expect(narrowChannelsPatch(["telegram", "feishu"])).toEqual(["telegram", "feishu"]);
|
|
303
|
+
});
|
|
304
|
+
test("empty array preserved — 'disable all editable channels' is a real state", () => {
|
|
305
|
+
expect(narrowChannelsPatch([])).toEqual([]);
|
|
306
|
+
});
|
|
307
|
+
test("non-array returns null (patch didn't touch channels)", () => {
|
|
308
|
+
expect(narrowChannelsPatch(undefined)).toBeNull();
|
|
309
|
+
expect(narrowChannelsPatch("telegram" as any)).toBeNull();
|
|
310
|
+
expect(narrowChannelsPatch({} as any)).toBeNull();
|
|
311
|
+
});
|
|
312
|
+
test("case-folded to lower", () => {
|
|
313
|
+
expect(narrowChannelsPatch(["FEISHU", "Telegram"])).toEqual(["feishu", "telegram"]);
|
|
314
|
+
});
|
|
315
|
+
test("duplicates deduped (Set)", () => {
|
|
316
|
+
expect(narrowChannelsPatch(["telegram", "telegram", "feishu", "TELEGRAM"])).toEqual(["telegram", "feishu"]);
|
|
317
|
+
});
|
|
318
|
+
test("unknown channel keys dropped silently (roadmap wechat, transport commhub, evil keys)", () => {
|
|
319
|
+
expect(narrowChannelsPatch(["wechat", "telegram", "evil-hacker", "commhub"])).toEqual(["telegram"]);
|
|
320
|
+
});
|
|
321
|
+
test("non-string entries dropped (numbers, objects, nulls)", () => {
|
|
322
|
+
expect(narrowChannelsPatch(["telegram", 42, {}, null, "feishu"] as any)).toEqual(["telegram", "feishu"]);
|
|
323
|
+
});
|
|
324
|
+
test("SQL-injection-shaped value dropped (not exact key match)", () => {
|
|
325
|
+
expect(narrowChannelsPatch(["telegram; drop table users;", "feishu"])).toEqual(["feishu"]);
|
|
326
|
+
});
|
|
327
|
+
test("whitespace around a key still trims to the key", () => {
|
|
328
|
+
expect(narrowChannelsPatch([" feishu "])).toEqual(["feishu"]);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe("computeApplyMode — channels tier (restart)", () => {
|
|
333
|
+
test("channels-only patch → restart (boot forks per-channel workers)", () => {
|
|
334
|
+
expect(computeApplyMode(undefined, {}, ["feishu"])).toBe("restart");
|
|
335
|
+
});
|
|
336
|
+
test("empty-array channels → restart (still a state change)", () => {
|
|
337
|
+
expect(computeApplyMode(undefined, {}, [])).toBe("restart");
|
|
338
|
+
});
|
|
339
|
+
test("no channels key + no flags + no model → restart_only", () => {
|
|
340
|
+
expect(computeApplyMode(undefined, {}, undefined)).toBe("restart_only");
|
|
341
|
+
});
|
|
342
|
+
test("channels + hot-tier flag together stays restart (upgrade rules)", () => {
|
|
343
|
+
expect(computeApplyMode(undefined, { maxTurns: 5 }, ["telegram"])).toBe("restart");
|
|
344
|
+
});
|
|
345
|
+
test("channels + model → restart (model was already restart)", () => {
|
|
346
|
+
expect(computeApplyMode("gpt-5", {}, ["feishu"])).toBe("restart");
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe("validatePatch — channels defensive gate", () => {
|
|
351
|
+
test("valid channels array passes", () => {
|
|
352
|
+
expect(validatePatch(undefined, {}, ["telegram", "feishu"])).toBeNull();
|
|
353
|
+
expect(validatePatch(undefined, {}, [])).toBeNull();
|
|
354
|
+
expect(validatePatch(undefined, {}, ["telegram"])).toBeNull();
|
|
355
|
+
});
|
|
356
|
+
test("commhub rejected — RPC transport, not a per-node channel", () => {
|
|
357
|
+
const r = validatePatch(undefined, {}, ["commhub"]);
|
|
358
|
+
expect(r?.field).toBe("channels.commhub");
|
|
359
|
+
});
|
|
360
|
+
test("out-of-allowlist channel rejected (caller bypassed narrowing)", () => {
|
|
361
|
+
const r = validatePatch(undefined, {}, ["wechat"]);
|
|
362
|
+
expect(r?.field).toBe("channels.wechat");
|
|
363
|
+
expect(r?.reason).toMatch(/allowlist/);
|
|
364
|
+
});
|
|
365
|
+
test("non-string entry rejected", () => {
|
|
366
|
+
const r = validatePatch(undefined, {}, [42] as any);
|
|
367
|
+
expect(r?.field).toBe("channels");
|
|
368
|
+
});
|
|
369
|
+
test("non-array rejected", () => {
|
|
370
|
+
const r = validatePatch(undefined, {}, "telegram" as any);
|
|
371
|
+
expect(r?.field).toBe("channels");
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe("Codex config flags (#815)", () => {
|
|
376
|
+
test("yolo bundle on codex-app-server passes", () => {
|
|
377
|
+
expect(validatePatch(undefined, {
|
|
378
|
+
approvalPolicy: "never",
|
|
379
|
+
sandboxMode: "danger-full-access",
|
|
380
|
+
skipGitRepoCheck: true,
|
|
381
|
+
copresenceFullAccess: true,
|
|
382
|
+
modelReasoningEffort: "low",
|
|
383
|
+
}, undefined, "codex-app-server")).toBeNull();
|
|
384
|
+
});
|
|
385
|
+
test("modelReasoningEffort invalid enum rejected", () => {
|
|
386
|
+
expect(validatePatch(undefined, { modelReasoningEffort: "max" }, undefined, "codex-app-server")?.field)
|
|
387
|
+
.toBe("flags.modelReasoningEffort");
|
|
388
|
+
});
|
|
389
|
+
test("codex flags on claude runtime rejected", () => {
|
|
390
|
+
expect(validatePatch(undefined, { modelReasoningEffort: "low" }, undefined, "claude-agent-sdk")?.field)
|
|
391
|
+
.toBe("flags.modelReasoningEffort");
|
|
392
|
+
});
|
|
393
|
+
});
|