@cohortapp/agent-sdk 2.16.0 → 2.18.4
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/.claude/settings.json +18 -0
- package/.env.example +23 -7
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/poller-daemon-setup.md +4 -1
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/context/budget.mjs +327 -0
- package/lib/context/history-scope.mjs +138 -0
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/economics.mjs +9 -0
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +95 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +82 -4
- package/lib/org/inbound/hydrate.mjs +555 -51
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +15 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +104 -59
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +804 -157
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/lib/session-router.mjs +310 -42
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +121 -12
- package/scripts/daemon/responder.mjs +315 -146
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -453
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -300
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -295
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -344
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -185
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* registry.test.mjs — tests for the file-backed org registry.
|
|
3
|
-
* Run: node --test lib/org/registry.test.mjs
|
|
4
|
-
*/
|
|
5
|
-
"use strict";
|
|
6
|
-
|
|
7
|
-
import { test } from "node:test";
|
|
8
|
-
import assert from "node:assert/strict";
|
|
9
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
buildSelfEntry, currentFocus, writeSelfEntry, readSelfEntry, readDirectory,
|
|
15
|
-
} from "./registry.mjs";
|
|
16
|
-
|
|
17
|
-
function tmp() { return mkdtempSync(join(tmpdir(), "org-registry-")); }
|
|
18
|
-
function repo() {
|
|
19
|
-
const root = tmp();
|
|
20
|
-
mkdirSync(join(root, "config"), { recursive: true });
|
|
21
|
-
return root;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test("buildSelfEntry: maps identity + company + profile + focus", () => {
|
|
25
|
-
const entry = buildSelfEntry({
|
|
26
|
-
identity: {
|
|
27
|
-
firstName: "Alex", fullName: "Alex Chen", title: "Chief of Staff",
|
|
28
|
-
email: "alex@example.com", slackMemberId: "U1", repoSlug: "alex-ai",
|
|
29
|
-
function: "executive-operator", altitude: "c-suite",
|
|
30
|
-
principal: { fullName: "Sam Smith", title: "CEO" },
|
|
31
|
-
},
|
|
32
|
-
company: { name: "Northwind", domain: "northwind.example.com" },
|
|
33
|
-
profile: { label: "C-Suite Executive Operator", towers: [{ id: "t1", name: "Strategic Initiatives", focus: "x" }] },
|
|
34
|
-
focus: [{ title: "Ship Q3 plan", queue: "action-stack", priority: "high" }],
|
|
35
|
-
nowMs: Date.parse("2026-06-03T12:00:00Z"),
|
|
36
|
-
});
|
|
37
|
-
assert.equal(entry.id, "alex-ai");
|
|
38
|
-
assert.equal(entry.fullName, "Alex Chen");
|
|
39
|
-
assert.equal(entry.archetype.function, "executive-operator");
|
|
40
|
-
assert.equal(entry.archetype.label, "C-Suite Executive Operator");
|
|
41
|
-
assert.equal(entry.towers.length, 1);
|
|
42
|
-
assert.equal(entry.towers[0].name, "Strategic Initiatives");
|
|
43
|
-
assert.equal(entry.focus[0].title, "Ship Q3 plan");
|
|
44
|
-
assert.equal(entry.principal.fullName, "Sam Smith");
|
|
45
|
-
assert.equal(entry.company.name, "Northwind");
|
|
46
|
-
assert.equal(entry.updatedAt, "2026-06-03T12:00:00.000Z");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("buildSelfEntry: derives repo + id from firstName when no repoSlug", () => {
|
|
50
|
-
const entry = buildSelfEntry({ identity: { firstName: "Robin" } });
|
|
51
|
-
assert.equal(entry.repo, "robin-ai");
|
|
52
|
-
assert.equal(entry.id, "robin-ai");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("currentFocus: uses injected items, capped", async () => {
|
|
56
|
-
const items = Array.from({ length: 9 }, (_, i) => ({ title: `t${i}`, queue: "q", priority: "normal" }));
|
|
57
|
-
const focus = await currentFocus("/x", { items, limit: 3 });
|
|
58
|
-
assert.equal(focus.length, 3);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("currentFocus: reads + filters queue YAML (open only)", async () => {
|
|
62
|
-
const root = repo();
|
|
63
|
-
try {
|
|
64
|
-
mkdirSync(join(root, "state", "queues"), { recursive: true });
|
|
65
|
-
writeFileSync(join(root, "state", "queues", "action-stack.yaml"),
|
|
66
|
-
"items:\n - title: Open one\n status: open\n priority: high\n - title: Done one\n status: done\n");
|
|
67
|
-
const focus = await currentFocus(root, { queues: ["action-stack"], limit: 5 });
|
|
68
|
-
assert.equal(focus.length, 1);
|
|
69
|
-
assert.equal(focus[0].title, "Open one");
|
|
70
|
-
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("writeSelfEntry/readSelfEntry: round-trip", () => {
|
|
74
|
-
const root = repo();
|
|
75
|
-
try {
|
|
76
|
-
const e = buildSelfEntry({ identity: { firstName: "Alex", repoSlug: "alex-ai" } });
|
|
77
|
-
assert.equal(writeSelfEntry(root, e), true);
|
|
78
|
-
assert.equal(readSelfEntry(root).id, "alex-ai");
|
|
79
|
-
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("readDirectory: merges known-agents peers + self; self + synced override", () => {
|
|
83
|
-
const root = repo();
|
|
84
|
-
try {
|
|
85
|
-
writeFileSync(join(root, "config", "known-agents.json"), JSON.stringify({
|
|
86
|
-
agents: [{ name: "Bob", fullName: "Bob Lee", slackId: "U2", role: "Head of Eng", repo: "bob-ai" }],
|
|
87
|
-
}));
|
|
88
|
-
// synced richer peer overrides the static known-agents entry
|
|
89
|
-
mkdirSync(join(root, "state", "org", "peers"), { recursive: true });
|
|
90
|
-
writeFileSync(join(root, "state", "org", "peers", "bob.json"), JSON.stringify({ id: "bob-ai", name: "Bob", title: "VP Engineering", source: "cohort" }));
|
|
91
|
-
writeSelfEntry(root, buildSelfEntry({ identity: { firstName: "Alex", repoSlug: "alex-ai", title: "CoS" } }));
|
|
92
|
-
const dir = readDirectory(root);
|
|
93
|
-
const bob = dir.find((d) => d.id === "bob-ai");
|
|
94
|
-
const alex = dir.find((d) => d.id === "alex-ai");
|
|
95
|
-
assert.equal(bob.title, "VP Engineering", "synced peer overrides known-agents role");
|
|
96
|
-
assert.equal(bob.source, "cohort");
|
|
97
|
-
assert.equal(alex.self, true, "self entry flagged");
|
|
98
|
-
assert.equal(dir.length, 2);
|
|
99
|
-
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
100
|
-
});
|
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* resource-tools.test.mjs — the venture-deliverable desk, and the parity that
|
|
3
|
-
* keeps it from drifting the way every other desk already has.
|
|
4
|
-
*
|
|
5
|
-
* WHAT THIS FILE IS FOR. hq exposes 239 responder ops across 11 families;
|
|
6
|
-
* maestro's curated table exposed 69 desk tools across 9 — files 29 vs 7, crm
|
|
7
|
-
* 44 vs 8. One cause: both tool tables are hand-written, so the moment one side
|
|
8
|
-
* moves, the other is stale and NOTHING is red. The `resource_*` block is
|
|
9
|
-
* generated from the vendored protocol declaration instead, and the assertions
|
|
10
|
-
* below are what make "generated" a guarantee rather than a claim: they read
|
|
11
|
-
* the protocol table at run time and demand the tool block match it exactly, in
|
|
12
|
-
* BOTH directions.
|
|
13
|
-
*
|
|
14
|
-
* It also pins the access-level rule the wave was called on: reads at "read"
|
|
15
|
-
* (org.read → every tier including least-privilege "default"), writes at
|
|
16
|
-
* "write" (org.write → ceo + leadership), and NOTHING at "admin", which is the
|
|
17
|
-
* CEO-only escape-hatch tier where a capability is exactly as unreachable as
|
|
18
|
-
* the org_rpc route a curated tool exists to replace.
|
|
19
|
-
*
|
|
20
|
-
* Hermetic: no network, no env dependence beyond the snapshot below.
|
|
21
|
-
* Run: node --test lib/org/resource-tools.test.mjs
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
"use strict";
|
|
25
|
-
|
|
26
|
-
import { test, before, after } from "node:test";
|
|
27
|
-
import assert from "node:assert/strict";
|
|
28
|
-
|
|
29
|
-
import { METHODS, methodDef } from "./protocol.mjs";
|
|
30
|
-
import { ADMIN_TOOLS, expectedAccessFor } from "./tool-access.mjs";
|
|
31
|
-
import {
|
|
32
|
-
RESOURCE_FAMILY,
|
|
33
|
-
RESOURCE_DESK,
|
|
34
|
-
RESOURCE_PRESENTATION,
|
|
35
|
-
RESOURCE_TOOLS,
|
|
36
|
-
buildResourceTools,
|
|
37
|
-
toolNameForMethod,
|
|
38
|
-
} from "./resource-tools.mjs";
|
|
39
|
-
import { ORG_TOOLS, getOrgTools, orgToolDef, executeOrgTool, deskFamilyAvailable } from "./tool-surface.mjs";
|
|
40
|
-
import { getToolNamesForAccessLevel } from "../tool-definitions.js";
|
|
41
|
-
|
|
42
|
-
// ── env hygiene ────────────────────────────────────────────────────────────
|
|
43
|
-
const ENV_KEYS = [
|
|
44
|
-
"COHORT_API_TOKEN", "COHORT_TOKEN", "COHORT_API_KEY", "COHORT_ORG_ID",
|
|
45
|
-
"COHORT_BASE", "COHORT_API_URL", "COHORT_AGENT_ROOT", "AGENT_ROOT",
|
|
46
|
-
];
|
|
47
|
-
const saved = {};
|
|
48
|
-
before(() => {
|
|
49
|
-
for (const k of ENV_KEYS) { saved[k] = process.env[k]; delete process.env[k]; }
|
|
50
|
-
});
|
|
51
|
-
after(() => {
|
|
52
|
-
for (const k of ENV_KEYS) {
|
|
53
|
-
if (saved[k] === undefined) delete process.env[k];
|
|
54
|
-
else process.env[k] = saved[k];
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const CFG = { org: { cohort: { enabled: true, base: "https://org.example", orgId: "acme", token: "nlk_secret" } } };
|
|
59
|
-
|
|
60
|
-
/** fetch stub: records calls, returns a res-frame body. */
|
|
61
|
-
function fakeFetch(body = { ok: true, result: { fine: true } }, { status = 200, ok = true } = {}) {
|
|
62
|
-
const calls = [];
|
|
63
|
-
const fn = async (url, init) => {
|
|
64
|
-
calls.push({ url: String(url), init: init || {} });
|
|
65
|
-
return { ok, status, json: async () => body, headers: { get: () => undefined } };
|
|
66
|
-
};
|
|
67
|
-
fn.calls = calls;
|
|
68
|
-
return fn;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** The family, straight from the vendored protocol — the ONLY authority here. */
|
|
72
|
-
const PROTOCOL_OPS = Object.entries(METHODS)
|
|
73
|
-
.filter(([, d]) => d.family === RESOURCE_FAMILY)
|
|
74
|
-
.map(([name]) => name);
|
|
75
|
-
|
|
76
|
-
// ── parity: the block IS the protocol declaration ──────────────────────────
|
|
77
|
-
|
|
78
|
-
test("parity: one curated tool per registered resource op, and NO extras", () => {
|
|
79
|
-
// Eight is not a magic number typed here — it is whatever the vendored
|
|
80
|
-
// protocol declares, which is what makes this an anti-drift test rather than
|
|
81
|
-
// a second hand-list. It is asserted non-empty so a protocol that lost the
|
|
82
|
-
// family cannot make this file pass vacuously.
|
|
83
|
-
assert.ok(PROTOCOL_OPS.length > 0, "the vendored protocol carries a resource family");
|
|
84
|
-
assert.equal(PROTOCOL_OPS.length, 8, "eight resource methods (list/get + 6 writes)");
|
|
85
|
-
|
|
86
|
-
const toolMethods = RESOURCE_TOOLS.map((t) => t.binding.method).sort();
|
|
87
|
-
assert.deepEqual(
|
|
88
|
-
toolMethods,
|
|
89
|
-
[...PROTOCOL_OPS].sort(),
|
|
90
|
-
"every resource method has exactly one curated tool, and no tool names a method that is not one",
|
|
91
|
-
);
|
|
92
|
-
assert.equal(
|
|
93
|
-
new Set(RESOURCE_TOOLS.map((t) => t.name)).size,
|
|
94
|
-
RESOURCE_TOOLS.length,
|
|
95
|
-
"tool names are unique",
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("parity: every binding names a REAL protocol method, resolved through methodDef", () => {
|
|
100
|
-
for (const t of RESOURCE_TOOLS) {
|
|
101
|
-
assert.equal(t.binding.kind, "rpc", `${t.name} is rpc-bound (no local shim to drift)`);
|
|
102
|
-
const def = methodDef(t.binding.method);
|
|
103
|
-
assert.ok(def, `${t.name} binds a method the frozen table carries (${t.binding.method})`);
|
|
104
|
-
assert.equal(def.family, RESOURCE_FAMILY, `${t.name} stays inside its own family`);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("parity: access MIRRORS the declaration — sideEffecting false→read, true→write", () => {
|
|
109
|
-
for (const t of RESOURCE_TOOLS) {
|
|
110
|
-
const def = methodDef(t.binding.method);
|
|
111
|
-
assert.equal(
|
|
112
|
-
t.access,
|
|
113
|
-
def.sideEffecting ? "write" : "read",
|
|
114
|
-
`${t.name} access must mirror sideEffecting (${t.binding.method})`,
|
|
115
|
-
);
|
|
116
|
-
// Same answer from the scope lane, which is what hq actually gates on. For
|
|
117
|
-
// this family the two rules coincide exactly (org.read ↔ not side-effecting,
|
|
118
|
-
// org.write ↔ side-effecting), so asserting both pins the coincidence: a
|
|
119
|
-
// future resource method that broke it would fail here rather than ship a
|
|
120
|
-
// tool at a tier hq will refuse.
|
|
121
|
-
assert.equal(t.access, expectedAccessFor(t.binding.method), `${t.name} access mirrors its scope lane`);
|
|
122
|
-
assert.match(def.scope, /^org\.(read|write)$/, `${t.binding.method} rides org.read/org.write — no new scope minted`);
|
|
123
|
-
}
|
|
124
|
-
assert.deepEqual(
|
|
125
|
-
RESOURCE_TOOLS.filter((t) => t.access === "read").map((t) => t.name),
|
|
126
|
-
["resource_list", "resource_get"],
|
|
127
|
-
"exactly the two reads",
|
|
128
|
-
);
|
|
129
|
-
assert.equal(RESOURCE_TOOLS.filter((t) => t.access === "write").length, 6, "exactly six writes");
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("parity: NOTHING at access:\"admin\", and nothing carries `outbound`", () => {
|
|
133
|
-
for (const t of RESOURCE_TOOLS) {
|
|
134
|
-
// access:"admin" is the CEO-only escape-hatch tier. A venture's own
|
|
135
|
-
// colleagues are default- and leadership-tier agents, so a deliverable verb
|
|
136
|
-
// parked there would be exactly as dark as the org_rpc route it replaces —
|
|
137
|
-
// i.e. this whole wave would ship as a no-op for the seats it is for.
|
|
138
|
-
assert.notEqual(t.access, "admin", `${t.name} must not be admin-tier`);
|
|
139
|
-
assert.ok(!ADMIN_TOOLS.has(t.name), `${t.name} is not an escape hatch`);
|
|
140
|
-
// No free text a human reads leaves the org through this desk: a card is
|
|
141
|
-
// org-internal metadata. The send-gate lane stays messaging_send /
|
|
142
|
-
// email_send / email_draft_send / org_call_share_step.
|
|
143
|
-
assert.equal(t.outbound, undefined, `${t.name} is not an outbound-content verb`);
|
|
144
|
-
assert.equal(t.email, undefined, `${t.name} is not an email tool`);
|
|
145
|
-
assert.equal(t.integration, undefined, `${t.name} is not an integration proxy`);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// ── generation, not transcription ──────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
test("the block is GENERATED: a protocol method with no presentation still gets a tool", () => {
|
|
152
|
-
// The point of the generator, proved against a synthetic table. A ninth
|
|
153
|
-
// resource method must not be able to arrive and be silently absent from the
|
|
154
|
-
// tool surface — that silence is the 239-vs-69 mechanism. It appears with a
|
|
155
|
-
// derived description (an under-described capability is a documentation bug;
|
|
156
|
-
// a MISSING one is the failure this wave exists to fix), and the coverage
|
|
157
|
-
// test below is what goes red.
|
|
158
|
-
const synthetic = {
|
|
159
|
-
...METHODS,
|
|
160
|
-
"resource.archive": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
161
|
-
};
|
|
162
|
-
const built = buildResourceTools(synthetic);
|
|
163
|
-
const archive = built.find((t) => t.binding.method === "resource.archive");
|
|
164
|
-
assert.ok(archive, "a brand-new family method produces a tool with no edit to the tool file");
|
|
165
|
-
assert.equal(archive.name, "resource_archive");
|
|
166
|
-
assert.equal(archive.access, "write", "its tier is derived from its scope, not typed by hand");
|
|
167
|
-
assert.equal(archive.desk, RESOURCE_DESK);
|
|
168
|
-
assert.match(archive.description, /add a presentation entry/i, "and it says out loud that it is undescribed");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("the block is GENERATED: a presentation entry for a NON-family method is never emitted", () => {
|
|
172
|
-
// The reverse drift: a description left behind after a method was removed
|
|
173
|
-
// from the protocol must not resurrect a tool that binds a method the frozen
|
|
174
|
-
// table no longer carries.
|
|
175
|
-
const withoutDetach = Object.fromEntries(
|
|
176
|
-
Object.entries(METHODS).filter(([m]) => m !== "resource.detachFile"),
|
|
177
|
-
);
|
|
178
|
-
const built = buildResourceTools(withoutDetach);
|
|
179
|
-
assert.equal(
|
|
180
|
-
built.find((t) => t.binding.method === "resource.detachFile"),
|
|
181
|
-
undefined,
|
|
182
|
-
"a stale presentation entry cannot emit a tool for a method the protocol dropped",
|
|
183
|
-
);
|
|
184
|
-
assert.equal(built.length, PROTOCOL_OPS.length - 1);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("the block is GENERATED: an older vendored protocol degrades to an empty block", () => {
|
|
188
|
-
const noFamily = Object.fromEntries(
|
|
189
|
-
Object.entries(METHODS).filter(([, d]) => d.family !== RESOURCE_FAMILY),
|
|
190
|
-
);
|
|
191
|
-
assert.deepEqual(buildResourceTools(noFamily), [], "no family → no tools, never a broken binding");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("presentation covers the family EXACTLY — this is the assertion that goes red on drift", () => {
|
|
195
|
-
assert.deepEqual(
|
|
196
|
-
Object.keys(RESOURCE_PRESENTATION).sort(),
|
|
197
|
-
[...PROTOCOL_OPS].sort(),
|
|
198
|
-
"every resource method is described, and no description outlives its method",
|
|
199
|
-
);
|
|
200
|
-
for (const [method, p] of Object.entries(RESOURCE_PRESENTATION)) {
|
|
201
|
-
assert.ok(p.title && p.title.length > 3, `${method} titled`);
|
|
202
|
-
assert.ok(p.description && p.description.length > 80, `${method} described in earnest`);
|
|
203
|
-
assert.equal(p.input_schema.type, "object", `${method} schema is an object`);
|
|
204
|
-
assert.ok(p.input_schema.properties, `${method} schema has properties`);
|
|
205
|
-
assert.ok(Array.isArray(p.input_schema.required), `${method} schema declares required[]`);
|
|
206
|
-
// Presentation is presentation. A `name`/`access`/`binding` literal here
|
|
207
|
-
// would be the hand-copy the generator exists to prevent.
|
|
208
|
-
for (const forbidden of ["name", "access", "binding", "desk"]) {
|
|
209
|
-
assert.equal(p[forbidden], undefined, `${method} presentation must not hand-write ${forbidden}`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test("tool names are a pure function of the method name", () => {
|
|
215
|
-
assert.equal(toolNameForMethod("resource.list"), "resource_list");
|
|
216
|
-
assert.equal(toolNameForMethod("resource.attachFile"), "resource_attach_file");
|
|
217
|
-
assert.equal(toolNameForMethod("resource.detachFile"), "resource_detach_file");
|
|
218
|
-
for (const t of RESOURCE_TOOLS) {
|
|
219
|
-
assert.equal(t.name, toolNameForMethod(t.binding.method), `${t.name} derives from its method`);
|
|
220
|
-
assert.match(t.name, /^[a-z0-9_]+$/, `${t.name} snake_case`);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// ── the schemas the model actually sees ────────────────────────────────────
|
|
225
|
-
|
|
226
|
-
test("the schemas require what hq requires, and nothing hq refuses", () => {
|
|
227
|
-
// .strict() on the hq side means an unknown key is a BAD_REQUEST, so every
|
|
228
|
-
// property advertised here has to be one the handler accepts.
|
|
229
|
-
const req = (n) => orgToolDef(n).input_schema.required;
|
|
230
|
-
const props = (n) => Object.keys(orgToolDef(n).input_schema.properties);
|
|
231
|
-
|
|
232
|
-
// Ref-addressed verbs take EITHER slug OR resourceId, so neither can be
|
|
233
|
-
// schema-`required` — the "exactly one" rule is a refine, and the
|
|
234
|
-
// description carries it.
|
|
235
|
-
for (const n of ["resource_get", "resource_update", "resource_delete", "resource_detach_file"]) {
|
|
236
|
-
assert.deepEqual(req(n), [], `${n} cannot mark either ref required`);
|
|
237
|
-
assert.ok(props(n).includes("slug") && props(n).includes("resourceId"), `${n} offers both refs`);
|
|
238
|
-
}
|
|
239
|
-
assert.deepEqual(req("resource_create"), ["kind", "title"]);
|
|
240
|
-
assert.deepEqual(req("resource_reorder"), ["slugs"]);
|
|
241
|
-
assert.deepEqual(req("resource_attach_file"), ["fileId"]);
|
|
242
|
-
|
|
243
|
-
// `source` is server-owned: an agent must never be able to relabel its own
|
|
244
|
-
// card as "ftlab-portal" and inherit the portal's provenance.
|
|
245
|
-
assert.ok(!props("resource_create").includes("source"), "create cannot set source");
|
|
246
|
-
assert.ok(!props("resource_update").includes("source"), "update cannot set source");
|
|
247
|
-
// slug is the ref AND the portal's re-sync key — renaming it would orphan the
|
|
248
|
-
// upsert and duplicate the card on the next push.
|
|
249
|
-
assert.ok(!props("resource_update").includes("slug_new"), "update has no slug rename");
|
|
250
|
-
// Re-pointing a card at a different drive row is a separately-audited act.
|
|
251
|
-
assert.ok(!props("resource_update").includes("fileId"), "update cannot re-point the file");
|
|
252
|
-
assert.equal(orgToolDef("resource_reorder").input_schema.properties.slugs.type, "array");
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
test("the descriptions carry the semantics the SCHEMA cannot", () => {
|
|
256
|
-
// A tool schema can say `metadata: object`. It cannot say that a null value
|
|
257
|
-
// deletes a key, that reorder is total, or that the file survives a delete —
|
|
258
|
-
// and an agent that does not know those three will destroy delivery
|
|
259
|
-
// provenance, scramble a catalogue, or tell a human their deck is gone.
|
|
260
|
-
assert.match(orgToolDef("resource_update").description, /shallow merge/i);
|
|
261
|
-
assert.match(orgToolDef("resource_update").description, /null is DELETED|null is deleted/i);
|
|
262
|
-
assert.match(orgToolDef("resource_reorder").description, /TOTAL, not partial/i);
|
|
263
|
-
assert.match(orgToolDef("resource_reorder").description, /CONFLICT/);
|
|
264
|
-
assert.match(orgToolDef("resource_delete").description, /fileRetained/);
|
|
265
|
-
assert.match(orgToolDef("resource_delete").description, /ftlab-portal/);
|
|
266
|
-
assert.match(orgToolDef("resource_create").description, /CONFLICT/);
|
|
267
|
-
assert.match(orgToolDef("resource_attach_file").description, /setUrl` defaults to FALSE|defaults to FALSE/i);
|
|
268
|
-
assert.match(orgToolDef("resource_detach_file").description, /CONFLICT/);
|
|
269
|
-
// The files bridge is a pointer: say where bytes actually come from.
|
|
270
|
-
assert.match(orgToolDef("resource_list").description, /files_get|files_doc_read/);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// ── the desk gate + the surrounding table ──────────────────────────────────
|
|
274
|
-
|
|
275
|
-
test("the desk registers only when the vendored protocol carries the family", () => {
|
|
276
|
-
assert.equal(deskFamilyAvailable(RESOURCE_DESK), true, "the vendored protocol carries resource.list");
|
|
277
|
-
for (const t of RESOURCE_TOOLS) assert.equal(t.desk, RESOURCE_DESK, `${t.name} is desk-gated`);
|
|
278
|
-
// desksAvailable:false is the older-protocol shape — the block disappears
|
|
279
|
-
// cleanly rather than binding methods the server does not implement.
|
|
280
|
-
const off = getOrgTools({ desksAvailable: false }).filter((t) => t.desk === RESOURCE_DESK);
|
|
281
|
-
assert.equal(off.length, 0, "an older protocol drops the block whole");
|
|
282
|
-
const on = getOrgTools({}).filter((t) => t.desk === RESOURCE_DESK);
|
|
283
|
-
assert.equal(on.length, PROTOCOL_OPS.length, "and carries all of it when present");
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
test("the generated block is spliced into ORG_TOOLS itself (both planes see it)", () => {
|
|
287
|
-
for (const t of RESOURCE_TOOLS) {
|
|
288
|
-
assert.ok(orgToolDef(t.name), `${t.name} is resolvable by name`);
|
|
289
|
-
assert.equal(orgToolDef(t.name), ORG_TOOLS.find((x) => x.name === t.name));
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// ── the access-level bug, at its consumer ──────────────────────────────────
|
|
294
|
-
|
|
295
|
-
test("ACCESS LEVELS: the reads reach EVERY tier; the writes reach ceo + leadership; none is CEO-only", () => {
|
|
296
|
-
// This is the assertion the wave turns on. lib/tool-definitions.js routes
|
|
297
|
-
// access:"read" into lookupTools (every tier, including least-privilege
|
|
298
|
-
// "default"), access:"write" into writeToolsCeo + writeToolsLeadership, and
|
|
299
|
-
// access:"admin" into writeToolsCeo ALONE. A venture's own colleagues are
|
|
300
|
-
// default- and leadership-tier agents, so the family is only actually shipped
|
|
301
|
-
// if it lands in those two surfaces.
|
|
302
|
-
const ceo = new Set(getToolNamesForAccessLevel("ceo"));
|
|
303
|
-
const leadership = new Set(getToolNamesForAccessLevel("leadership"));
|
|
304
|
-
const dflt = new Set(getToolNamesForAccessLevel("default"));
|
|
305
|
-
|
|
306
|
-
for (const t of RESOURCE_TOOLS) {
|
|
307
|
-
assert.ok(ceo.has(t.name), `${t.name} reaches ceo`);
|
|
308
|
-
assert.ok(leadership.has(t.name), `${t.name} reaches leadership — the tier a venture's colleagues run at`);
|
|
309
|
-
}
|
|
310
|
-
assert.ok(dflt.has("resource_list"), "a least-privilege seat can SEE its venture's deliverables");
|
|
311
|
-
assert.ok(dflt.has("resource_get"), "and open one");
|
|
312
|
-
for (const t of RESOURCE_TOOLS.filter((x) => x.access === "write")) {
|
|
313
|
-
assert.ok(!dflt.has(t.name), `${t.name} does not leak into the least-privilege tier`);
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
// ── dispatch ───────────────────────────────────────────────────────────────
|
|
318
|
-
|
|
319
|
-
test("every tool POSTS its protocol method with the params verbatim", async () => {
|
|
320
|
-
for (const t of RESOURCE_TOOLS) {
|
|
321
|
-
const fetchImpl = fakeFetch({ ok: true, result: { resources: [] } });
|
|
322
|
-
const frame = await executeOrgTool(t.name, { probe: 1 }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
323
|
-
assert.equal(frame.ok, true, `${t.name} dispatches`);
|
|
324
|
-
assert.equal(
|
|
325
|
-
fetchImpl.calls[0].url,
|
|
326
|
-
`https://org.example/api/v1/${t.binding.method}`,
|
|
327
|
-
`${t.name} hits its own method`,
|
|
328
|
-
);
|
|
329
|
-
const body = JSON.parse(fetchImpl.calls[0].init.body);
|
|
330
|
-
assert.equal(body.probe, 1, `${t.name} passes params through untouched`);
|
|
331
|
-
// sideEffecting:true at the dispatcher → a fresh idempotency header per
|
|
332
|
-
// invocation; the reads must not carry one.
|
|
333
|
-
const hasKey = Boolean(fetchImpl.calls[0].init.headers["x-idempotency-key"]);
|
|
334
|
-
assert.equal(hasKey, methodDef(t.binding.method).sideEffecting, `${t.name} idempotency header matches the declaration`);
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("the desk FAILS CLOSED with no credential — a refusal, never a silent success", async () => {
|
|
339
|
-
const fetchImpl = fakeFetch();
|
|
340
|
-
for (const t of RESOURCE_TOOLS) {
|
|
341
|
-
const frame = await executeOrgTool(t.name, {}, { orgConfig: {}, agentRoot: "/tmp/none", fetchImpl });
|
|
342
|
-
assert.equal(frame.ok, false, `${t.name} refuses`);
|
|
343
|
-
assert.equal(frame.error.code, "UNAUTHORIZED");
|
|
344
|
-
}
|
|
345
|
-
assert.equal(fetchImpl.calls.length, 0, "nothing dispatched without a credential");
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
test("a FORBIDDEN from hq surfaces verbatim — the scope gate is the server's answer, not ours", async () => {
|
|
349
|
-
const fetchImpl = fakeFetch(
|
|
350
|
-
{ ok: false, error: { code: "FORBIDDEN", message: "scope org.write required" } },
|
|
351
|
-
{ ok: false, status: 403 },
|
|
352
|
-
);
|
|
353
|
-
const frame = await executeOrgTool(
|
|
354
|
-
"resource_create",
|
|
355
|
-
{ kind: "deck", title: "Seed deck", url: "https://example.com/deck" },
|
|
356
|
-
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
|
|
357
|
-
);
|
|
358
|
-
assert.equal(frame.ok, false);
|
|
359
|
-
assert.equal(frame.error.code, "FORBIDDEN");
|
|
360
|
-
assert.match(frame.error.message, /org\.write/);
|
|
361
|
-
});
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tool-access.test.mjs — the access-tier mechanism, and the two silent failures
|
|
3
|
-
* it exists to close.
|
|
4
|
-
*
|
|
5
|
-
* REGRESSION 1 — THE ADMIN TRAPDOOR. `access:"admin"` is the CEO-only tier
|
|
6
|
-
* (lib/tool-definitions.js withholds it from writeToolsLeadership AND from the
|
|
7
|
-
* default lookup set). A CAPABILITY parked there is shipped and dark for every
|
|
8
|
-
* seat below the operator's console — the exact state tool-surface.mjs's header
|
|
9
|
-
* describes when it says "for a leadership or default agent, 'the long tail is
|
|
10
|
-
* reachable via org_rpc' is false". The rule against it used to live in
|
|
11
|
-
* per-delta hand-listed name arrays, so it guarded the names someone
|
|
12
|
-
* remembered. ADMIN_TOOLS makes the tier a closed set instead.
|
|
13
|
-
*
|
|
14
|
-
* REGRESSION 2 — THE TYPO TRAPDOOR. The tiers were bucketed by exact string
|
|
15
|
-
* equality, so an entry whose `access` was missing or mis-cased matched no
|
|
16
|
-
* bucket and reached NO native tier at all, while the MCP plane kept publishing
|
|
17
|
-
* it. That is the tool-side twin of the caller-side footgun `normalizeAccessLevel`
|
|
18
|
-
* already fixes ("'CEO' yielded zero tools, a silent UNDER-privilege footgun").
|
|
19
|
-
* The test below drives the OLD comparison and the NEW one over the same bad
|
|
20
|
-
* row so the difference is on the record.
|
|
21
|
-
*
|
|
22
|
-
* Hermetic, no network, no env. Run: node --test lib/org/tool-access.test.mjs
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
"use strict";
|
|
26
|
-
|
|
27
|
-
import { test } from "node:test";
|
|
28
|
-
import assert from "node:assert/strict";
|
|
29
|
-
|
|
30
|
-
import { METHODS } from "./protocol.mjs";
|
|
31
|
-
import {
|
|
32
|
-
TOOL_ACCESS_LEVELS,
|
|
33
|
-
MOST_RESTRICTED_ACCESS,
|
|
34
|
-
ADMIN_TOOLS,
|
|
35
|
-
expectedAccessFor,
|
|
36
|
-
normalizeToolAccess,
|
|
37
|
-
partitionByAccess,
|
|
38
|
-
} from "./tool-access.mjs";
|
|
39
|
-
|
|
40
|
-
// ── the derivation ─────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
test("expectedAccessFor derives the tier from the protocol SCOPE, not from sideEffecting", () => {
|
|
43
|
-
assert.equal(expectedAccessFor("resource.list"), "read");
|
|
44
|
-
assert.equal(expectedAccessFor("resource.create"), "write");
|
|
45
|
-
|
|
46
|
-
// The five audited reads: side-effecting (they land on the chain or the AI
|
|
47
|
-
// meter) yet `.read`-scoped on purpose, because a VIEWER seat may run them —
|
|
48
|
-
// exactly the ability its human seat has. hq keeps the same list under
|
|
49
|
-
// AUDITED_READ_SCOPE_WRITES. A sideEffecting-derived rule would demote every
|
|
50
|
-
// one of them out of the lookup set that every tier gets.
|
|
51
|
-
for (const m of [
|
|
52
|
-
"knowledge.search",
|
|
53
|
-
"directory.ask",
|
|
54
|
-
"branding.renderTemplate",
|
|
55
|
-
"branding.rewriteInVoice",
|
|
56
|
-
"branding.exportKit",
|
|
57
|
-
]) {
|
|
58
|
-
assert.equal(METHODS[m].sideEffecting, true, `${m} is side-effecting`);
|
|
59
|
-
assert.equal(expectedAccessFor(m), "read", `${m} is nonetheless a read-tier tool`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// The mirror case: not side-effecting, but write-scoped.
|
|
63
|
-
assert.equal(METHODS["artifact.create"].sideEffecting, false);
|
|
64
|
-
assert.equal(expectedAccessFor("artifact.create"), "write");
|
|
65
|
-
|
|
66
|
-
// Scopes that are neither `.read` nor `.write` (board.work, approval.request,
|
|
67
|
-
// mandate.propose) are writes — they are not callable by a read-only seat.
|
|
68
|
-
assert.equal(expectedAccessFor("board.claim"), "write");
|
|
69
|
-
assert.equal(expectedAccessFor("approval.request"), "write");
|
|
70
|
-
assert.equal(expectedAccessFor("mandate.propose"), "write");
|
|
71
|
-
|
|
72
|
-
// An unknown method is null, not a guess — the caller decides how loud to be.
|
|
73
|
-
assert.equal(expectedAccessFor("nope.nothing"), null);
|
|
74
|
-
assert.equal(expectedAccessFor(""), null);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// ── the admin trapdoor ─────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
test("ADMIN_TOOLS is the closed set of escape hatches — two names, both consoles", () => {
|
|
80
|
-
assert.deepEqual([...ADMIN_TOOLS].sort(), ["org_read", "org_rpc"]);
|
|
81
|
-
assert.equal(ADMIN_TOOLS.has("resource_create"), false);
|
|
82
|
-
// Frozen: a runtime widening would be a privilege change with no diff.
|
|
83
|
-
assert.throws(() => ADMIN_TOOLS.add("resource_create"));
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// ── the typo trapdoor ──────────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
test("normalizeToolAccess forgives casing/whitespace — 'Read' must not mean 'nowhere'", () => {
|
|
89
|
-
for (const good of TOOL_ACCESS_LEVELS) assert.equal(normalizeToolAccess(good, "t", () => {}), good);
|
|
90
|
-
assert.equal(normalizeToolAccess("Read", "t", () => {}), "read");
|
|
91
|
-
assert.equal(normalizeToolAccess(" WRITE ", "t", () => {}), "write");
|
|
92
|
-
assert.equal(normalizeToolAccess("Admin", "t", () => {}), "admin");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("an unrecognised tier fails CLOSED and NOISY, never invisible", () => {
|
|
96
|
-
for (const bad of [undefined, null, "", "readonly", 7, {}, []]) {
|
|
97
|
-
const warnings = [];
|
|
98
|
-
const got = normalizeToolAccess(bad, "broken_tool", (m) => warnings.push(m));
|
|
99
|
-
assert.equal(got, MOST_RESTRICTED_ACCESS, `${JSON.stringify(bad)} → most-restricted`);
|
|
100
|
-
assert.equal(warnings.length, 1, `${JSON.stringify(bad)} warns exactly once`);
|
|
101
|
-
assert.match(warnings[0], /broken_tool/, "the warning names the bad row so it is findable");
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("a throwing warn sink cannot take the tool table down", () => {
|
|
106
|
-
assert.equal(
|
|
107
|
-
normalizeToolAccess("nonsense", "t", () => {
|
|
108
|
-
throw new Error("logger exploded");
|
|
109
|
-
}),
|
|
110
|
-
MOST_RESTRICTED_ACCESS,
|
|
111
|
-
);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("REGRESSION: the old equality bucketing dropped a bad row from EVERY tier", () => {
|
|
115
|
-
const table = [
|
|
116
|
-
{ name: "good_read", access: "read" },
|
|
117
|
-
{ name: "good_write", access: "write" },
|
|
118
|
-
{ name: "mis_cased", access: "Read" }, // a plausible table typo
|
|
119
|
-
{ name: "no_access" }, // an entry someone forgot to tier
|
|
120
|
-
];
|
|
121
|
-
|
|
122
|
-
// What the code used to do, verbatim: three independent equality filters.
|
|
123
|
-
const oldBuckets = ["read", "write", "admin"].map((a) => table.filter((t) => t.access === a));
|
|
124
|
-
const oldReach = new Set(oldBuckets.flat().map((t) => t.name));
|
|
125
|
-
assert.equal(oldReach.has("mis_cased"), false, "the old filter lost a mis-cased row…");
|
|
126
|
-
assert.equal(oldReach.has("no_access"), false, "…and an untiered one");
|
|
127
|
-
assert.equal(oldReach.size, 2, "two of four tools reached no access level at all, silently");
|
|
128
|
-
|
|
129
|
-
// What it does now: an exhaustive partition. Nothing is dropped.
|
|
130
|
-
const warnings = [];
|
|
131
|
-
const now = partitionByAccess(table, (m) => warnings.push(m));
|
|
132
|
-
assert.equal(now.read.length + now.write.length + now.admin.length, table.length, "exhaustive");
|
|
133
|
-
assert.deepEqual(now.read.map((t) => t.name), ["good_read", "mis_cased"], "casing recovered into its real tier");
|
|
134
|
-
assert.deepEqual(now.admin.map((t) => t.name), ["no_access"], "the untiered row fails closed, not open");
|
|
135
|
-
assert.equal(warnings.length, 1, "and exactly the untiered row is reported");
|
|
136
|
-
assert.match(warnings[0], /no_access/);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("partitionByAccess survives a null/empty table (fail-open, like the rest of the client)", () => {
|
|
140
|
-
const empty = partitionByAccess(null, () => {});
|
|
141
|
-
assert.deepEqual(empty, { read: [], write: [], admin: [] });
|
|
142
|
-
const nullish = partitionByAccess([null, undefined], () => {});
|
|
143
|
-
assert.equal(nullish.admin.length, 2, "a null row is tiered, not thrown on");
|
|
144
|
-
});
|