switchroom 0.7.15 → 0.10.0
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 +51 -59
- package/bin/run-hook.sh +27 -11
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +410 -133
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +26937 -5601
- package/dist/host-control/main.js +12702 -0
- package/dist/vault/approvals/kernel-server.js +467 -184
- package/dist/vault/broker/server.js +1430 -724
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +7 -4
- package/profiles/_base/settings.json.hbs +20 -5
- package/profiles/_base/start.sh.hbs +16 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/_shared/telegram-style.md.hbs +20 -90
- package/profiles/_shared/vault-protocol.md.hbs +68 -0
- package/profiles/default/CLAUDE.md +50 -96
- package/profiles/default/CLAUDE.md.hbs +36 -6
- package/profiles/default/workspace/SOUL.md.hbs +12 -5
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +191 -0
- package/skills/switchroom-status/SKILL.md +27 -2
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/token-helpers/SKILL.md +24 -1
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/analytics-posthog.ts +191 -0
- package/telegram-plugin/bridge/bridge.ts +69 -0
- package/telegram-plugin/bridge/ipc-client.ts +4 -1
- package/telegram-plugin/dist/bridge/bridge.js +194 -119
- package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
- package/telegram-plugin/dist/server.js +245 -189
- package/telegram-plugin/first-paint.ts +3 -24
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +169 -40
- package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
- package/telegram-plugin/gateway/boot-probes.ts +166 -123
- package/telegram-plugin/gateway/boot-reason.ts +41 -7
- package/telegram-plugin/gateway/boot-version.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +3499 -1885
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
- package/telegram-plugin/gateway/quarantine.ts +69 -0
- package/telegram-plugin/gateway/quota-cache.ts +9 -4
- package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
- package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
- package/telegram-plugin/gateway/recent-denials.ts +77 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
- package/telegram-plugin/history.ts +91 -0
- package/telegram-plugin/hooks/hooks.json +10 -0
- package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/inbound-classifier.ts +50 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +4 -2
- package/telegram-plugin/permission-rule.ts +51 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/registry/reaper.ts +223 -0
- package/telegram-plugin/retry-api-call.ts +80 -0
- package/telegram-plugin/runtime-metrics.ts +177 -0
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/secret-detect/index.ts +24 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
- package/telegram-plugin/secret-detect/vault-error.ts +78 -11
- package/telegram-plugin/secret-detect/vault-write.ts +14 -2
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/session-tail.ts +6 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/silence-poke.ts +420 -0
- package/telegram-plugin/silent-end.ts +174 -0
- package/telegram-plugin/stream-controller.ts +13 -0
- package/telegram-plugin/stream-reply-handler.ts +7 -0
- package/telegram-plugin/subagent-watcher.ts +213 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
- package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
- package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
- package/telegram-plugin/tests/boot-probes.test.ts +216 -10
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
- package/telegram-plugin/tests/history-reaper.test.ts +378 -0
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
- package/telegram-plugin/tests/issues-card.test.ts +49 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
- package/telegram-plugin/tests/permission-rule.test.ts +80 -1
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/tests/races.test.ts +179 -0
- package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
- package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
- package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
- package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
- package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
- package/telegram-plugin/tests/silence-poke.test.ts +493 -0
- package/telegram-plugin/tests/silent-end.test.ts +206 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
- package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
- package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
- package/telegram-plugin/turn-signal-tracker.ts +100 -24
- package/telegram-plugin/uat/SETUP.md +210 -35
- package/telegram-plugin/uat/assertions.ts +264 -37
- package/telegram-plugin/uat/driver-info.ts +57 -0
- package/telegram-plugin/uat/driver.ts +590 -51
- package/telegram-plugin/uat/harness.ts +140 -94
- package/telegram-plugin/uat/load-env.test.ts +72 -0
- package/telegram-plugin/uat/load-env.ts +48 -0
- package/telegram-plugin/uat/login.ts +96 -53
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
- package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
- package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
- package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
- package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
- package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
- package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
- package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
- package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
- package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
- package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
- package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
- package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
- package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
- package/telegram-plugin/vault-approval-posture.ts +42 -0
- package/telegram-plugin/welcome-text.ts +1 -0
- package/telegram-plugin/active-pins-sweep.ts +0 -204
- package/telegram-plugin/active-pins.ts +0 -146
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/card-event-log.ts +0 -138
- package/telegram-plugin/dist/foreman/foreman.js +0 -31106
- package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/pin-event-log.ts +0 -76
- package/telegram-plugin/progress-card-driver.ts +0 -2886
- package/telegram-plugin/progress-card-pin-manager.ts +0 -589
- package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
- package/telegram-plugin/progress-card.ts +0 -1409
- package/telegram-plugin/tests/HARNESS.md +0 -340
- package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
- package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
- package/telegram-plugin/tests/active-pins.test.ts +0 -187
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/card-event-log.test.ts +0 -145
- package/telegram-plugin/tests/first-paint.test.ts +0 -257
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
- package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
- package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
- package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
- package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- package/telegram-plugin/tests/setup-state.test.ts +0 -146
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
- package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
- package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
- package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
- package/telegram-plugin/two-zone-card.ts +0 -269
- package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { classifyInbound } from '../inbound-classifier.js'
|
|
3
|
+
|
|
4
|
+
describe('inbound-classifier — status query', () => {
|
|
5
|
+
describe('positive matches (status_query=true)', () => {
|
|
6
|
+
const positives = [
|
|
7
|
+
'?',
|
|
8
|
+
'??',
|
|
9
|
+
'???',
|
|
10
|
+
'status',
|
|
11
|
+
'Status',
|
|
12
|
+
'STATUS',
|
|
13
|
+
'status?',
|
|
14
|
+
'status ?',
|
|
15
|
+
'update',
|
|
16
|
+
'update?',
|
|
17
|
+
'any update',
|
|
18
|
+
'any update?',
|
|
19
|
+
'still there',
|
|
20
|
+
'still there?',
|
|
21
|
+
'Still There?',
|
|
22
|
+
'still working',
|
|
23
|
+
'still working?',
|
|
24
|
+
'are you there',
|
|
25
|
+
'are you there?',
|
|
26
|
+
'you there',
|
|
27
|
+
'you there?',
|
|
28
|
+
'hello?',
|
|
29
|
+
'Hello??',
|
|
30
|
+
'hey?',
|
|
31
|
+
// surrounding whitespace
|
|
32
|
+
' status? ',
|
|
33
|
+
'\nstill there?\n',
|
|
34
|
+
]
|
|
35
|
+
for (const text of positives) {
|
|
36
|
+
it(`matches: ${JSON.stringify(text)}`, () => {
|
|
37
|
+
expect(classifyInbound(text).isStatusQuery).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('negative matches (status_query=false)', () => {
|
|
43
|
+
const negatives = [
|
|
44
|
+
'',
|
|
45
|
+
' ',
|
|
46
|
+
'hello',
|
|
47
|
+
'hi',
|
|
48
|
+
'what is the status of the deploy',
|
|
49
|
+
'status of the deploy?',
|
|
50
|
+
'are you there with the report',
|
|
51
|
+
'what update did you see',
|
|
52
|
+
'i need an update on the metrics',
|
|
53
|
+
// Plausible but rejected — message too long to be a standalone ping
|
|
54
|
+
'status? also can you check the deployment script for the lint errors please',
|
|
55
|
+
// Punctuation-shaped but not a query
|
|
56
|
+
'.',
|
|
57
|
+
'!',
|
|
58
|
+
'!?',
|
|
59
|
+
]
|
|
60
|
+
for (const text of negatives) {
|
|
61
|
+
it(`does not match: ${JSON.stringify(text)}`, () => {
|
|
62
|
+
expect(classifyInbound(text).isStatusQuery).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('handles null/undefined safely', () => {
|
|
68
|
+
expect(classifyInbound(null).isStatusQuery).toBe(false)
|
|
69
|
+
expect(classifyInbound(undefined).isStatusQuery).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('does not match messages over 40 chars even if they start with a status word', () => {
|
|
73
|
+
const longPretendStatusQuery = 'status? but actually i wanted to ask about deploys'
|
|
74
|
+
expect(classifyInbound(longPretendStatusQuery).isStatusQuery).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural tests for the 13 previously-silent inbound message types
|
|
3
|
+
* registered on `bot.on('message:<type>')` in the gateway (#1077).
|
|
4
|
+
*
|
|
5
|
+
* Why structural: gateway/gateway.ts wires every handler inline against
|
|
6
|
+
* the live `bot` instance — none of these closures are exported, so a
|
|
7
|
+
* functional invocation would require booting the full grammy runtime
|
|
8
|
+
* against a real or mocked Bot API. The existing gateway test suite
|
|
9
|
+
* settled on file-level grep assertions (see
|
|
10
|
+
* `gateway-secret-detect.test.ts`) for exactly this reason: cheap,
|
|
11
|
+
* deterministic, and they catch the regression we actually care about
|
|
12
|
+
* — a future hand mistakenly deleting a handler or skipping the
|
|
13
|
+
* gate/ack call that gives the user feedback.
|
|
14
|
+
*
|
|
15
|
+
* Decision matrix being enforced here (issue #1077):
|
|
16
|
+
*
|
|
17
|
+
* forward → contact, location, venue, poll, web_app_data,
|
|
18
|
+
* users_shared, chat_shared
|
|
19
|
+
* ack-only → dice, game, story, paid_media, successful_payment
|
|
20
|
+
* refuse (DENY) → passport_data
|
|
21
|
+
*
|
|
22
|
+
* The contract per-type:
|
|
23
|
+
* - A `bot.on('message:<type>', …)` registration exists.
|
|
24
|
+
* - Forwarding handlers call `handleInbound(`.
|
|
25
|
+
* - Ack-only handlers call `handleAckOnly(`.
|
|
26
|
+
* - The refusal handler calls `handleRefusal(` and does NOT call
|
|
27
|
+
* `handleInbound(` (passport data must never reach the agent).
|
|
28
|
+
* - Every handler logs a stderr line so operators can see the
|
|
29
|
+
* event landed.
|
|
30
|
+
* - Every handler is wrapped in try/catch (or delegates to a helper
|
|
31
|
+
* that is) so a malformed payload cannot tear down the dispatcher.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { describe, it, expect } from 'vitest'
|
|
35
|
+
import { readFileSync } from 'node:fs'
|
|
36
|
+
|
|
37
|
+
const SRC = readFileSync(
|
|
38
|
+
new URL('../gateway/gateway.ts', import.meta.url),
|
|
39
|
+
'utf8',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract the body of a `bot.on('message:<kind>', …)` handler. Returns
|
|
46
|
+
* the substring from the `bot.on(` line up to the matching closing
|
|
47
|
+
* `})` at the outer scope. Good enough for grepping — not a full
|
|
48
|
+
* AST parse.
|
|
49
|
+
*/
|
|
50
|
+
function handlerBody(kind: string): string {
|
|
51
|
+
const needle = `bot.on('message:${kind}'`
|
|
52
|
+
const start = SRC.indexOf(needle)
|
|
53
|
+
expect(start, `handler bot.on('message:${kind}') not found`).toBeGreaterThan(0)
|
|
54
|
+
// Find the matching close — naive depth count of {/} from the first `{`.
|
|
55
|
+
const firstBrace = SRC.indexOf('{', start)
|
|
56
|
+
let depth = 0
|
|
57
|
+
for (let i = firstBrace; i < SRC.length; i++) {
|
|
58
|
+
const c = SRC[i]
|
|
59
|
+
if (c === '{') depth++
|
|
60
|
+
else if (c === '}') {
|
|
61
|
+
depth--
|
|
62
|
+
if (depth === 0) return SRC.slice(start, i + 1)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`could not find end of handler ${kind}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Registration completeness ───────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
const ALL_KINDS = [
|
|
71
|
+
'contact',
|
|
72
|
+
'location',
|
|
73
|
+
'venue',
|
|
74
|
+
'dice',
|
|
75
|
+
'poll',
|
|
76
|
+
'game',
|
|
77
|
+
'story',
|
|
78
|
+
'paid_media',
|
|
79
|
+
'successful_payment',
|
|
80
|
+
'passport_data',
|
|
81
|
+
'web_app_data',
|
|
82
|
+
'users_shared',
|
|
83
|
+
'chat_shared',
|
|
84
|
+
] as const
|
|
85
|
+
|
|
86
|
+
describe('inbound message-type handlers: registration', () => {
|
|
87
|
+
for (const kind of ALL_KINDS) {
|
|
88
|
+
it(`registers a bot.on('message:${kind}') handler`, () => {
|
|
89
|
+
expect(SRC).toContain(`bot.on('message:${kind}'`)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ─── Forwarding handlers ─────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
const FORWARDING = [
|
|
97
|
+
'contact',
|
|
98
|
+
'location',
|
|
99
|
+
'venue',
|
|
100
|
+
'poll',
|
|
101
|
+
'web_app_data',
|
|
102
|
+
'users_shared',
|
|
103
|
+
'chat_shared',
|
|
104
|
+
] as const
|
|
105
|
+
|
|
106
|
+
describe('inbound message-type handlers: forwarding decisions', () => {
|
|
107
|
+
for (const kind of FORWARDING) {
|
|
108
|
+
it(`${kind} forwards via handleInbound and logs to stderr`, () => {
|
|
109
|
+
const body = handlerBody(kind)
|
|
110
|
+
expect(body).toMatch(/handleInbound\(ctx,/)
|
|
111
|
+
expect(body).toMatch(/process\.stderr\.write/)
|
|
112
|
+
// Must not divert to the ack-only path — that would silently
|
|
113
|
+
// hide the payload from the agent.
|
|
114
|
+
expect(body).not.toMatch(/handleAckOnly\(/)
|
|
115
|
+
expect(body).not.toMatch(/handleRefusal\(/)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
it('forwarding envelopes describe the payload kind in the text', () => {
|
|
120
|
+
// Each handler builds a `(<kind>: …)` text envelope so the agent
|
|
121
|
+
// sees what category of payload arrived without having to decode
|
|
122
|
+
// the meta block.
|
|
123
|
+
expect(handlerBody('contact')).toContain('(contact:')
|
|
124
|
+
expect(handlerBody('location')).toContain('(location:')
|
|
125
|
+
expect(handlerBody('venue')).toContain('(venue:')
|
|
126
|
+
expect(handlerBody('poll')).toContain('(poll:')
|
|
127
|
+
expect(handlerBody('web_app_data')).toContain('(web_app_data:')
|
|
128
|
+
expect(handlerBody('users_shared')).toContain('(users_shared:')
|
|
129
|
+
expect(handlerBody('chat_shared')).toContain('(chat_shared:')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('web_app_data caps untrusted payload length before forwarding', () => {
|
|
133
|
+
// web_app_data.data is arbitrary mini-app output — a malicious
|
|
134
|
+
// mini-app could otherwise flood the agent. Same defence as #553
|
|
135
|
+
// applied to text coalescing.
|
|
136
|
+
const body = handlerBody('web_app_data')
|
|
137
|
+
expect(body).toMatch(/slice\(0,\s*4096\)/)
|
|
138
|
+
expect(body).toMatch(/truncated/)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// ─── Ack-only handlers ───────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
const ACK_ONLY = ['dice', 'game', 'story', 'paid_media', 'successful_payment'] as const
|
|
145
|
+
|
|
146
|
+
describe('inbound message-type handlers: ack-only decisions', () => {
|
|
147
|
+
for (const kind of ACK_ONLY) {
|
|
148
|
+
it(`${kind} uses handleAckOnly (no forward to agent)`, () => {
|
|
149
|
+
const body = handlerBody(kind)
|
|
150
|
+
expect(body).toMatch(/handleAckOnly\(/)
|
|
151
|
+
expect(body).toMatch(/process\.stderr\.write/)
|
|
152
|
+
// Ack-only must NOT call handleInbound — the whole point is
|
|
153
|
+
// we don't bother the agent for these.
|
|
154
|
+
expect(body).not.toMatch(/handleInbound\(/)
|
|
155
|
+
// Nor should it pretend to refuse.
|
|
156
|
+
expect(body).not.toMatch(/handleRefusal\(/)
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
it('dice uses a 🎲 reaction for situational feedback', () => {
|
|
161
|
+
expect(handlerBody('dice')).toContain("emoji: '🎲'")
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('paid_media and successful_payment are marked warn:true', () => {
|
|
165
|
+
expect(handlerBody('paid_media')).toMatch(/warn:\s*true/)
|
|
166
|
+
expect(handlerBody('successful_payment')).toMatch(/warn:\s*true/)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('successful_payment logs the structured payment fields', () => {
|
|
170
|
+
// Money-flow events need a reconciliation-friendly stderr line.
|
|
171
|
+
const body = handlerBody('successful_payment')
|
|
172
|
+
expect(body).toContain('currency=')
|
|
173
|
+
expect(body).toContain('total_amount=')
|
|
174
|
+
expect(body).toContain('telegram_charge=')
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// ─── Refusal handler ─────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
describe('inbound message-type handlers: passport_data refusal', () => {
|
|
181
|
+
it('passport_data uses handleRefusal and NEVER calls handleInbound', () => {
|
|
182
|
+
const body = handlerBody('passport_data')
|
|
183
|
+
expect(body).toMatch(/handleRefusal\(/)
|
|
184
|
+
// Critical: passport data is regulated identity material. Even a
|
|
185
|
+
// diagnostic forward path would leak it onto the agent's wire.
|
|
186
|
+
expect(body).not.toMatch(/handleInbound\(/)
|
|
187
|
+
expect(body).not.toMatch(/ipcServer\.broadcast/)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('passport_data refusal text mentions Telegram Passport', () => {
|
|
191
|
+
// The user gets a polite explanation so they don't think the
|
|
192
|
+
// message was simply dropped on the floor.
|
|
193
|
+
const body = handlerBody('passport_data')
|
|
194
|
+
expect(body).toMatch(/Telegram Passport/)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// ─── Shared helper invariants ────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
describe('inbound message-type helpers: handleAckOnly + handleRefusal', () => {
|
|
201
|
+
it('handleAckOnly is declared and gates before reacting', () => {
|
|
202
|
+
// The function must consult gate() so non-allowlisted senders
|
|
203
|
+
// don't get a reaction (which would confirm the bot exists to
|
|
204
|
+
// a stranger).
|
|
205
|
+
expect(SRC).toMatch(/async function handleAckOnly\(/)
|
|
206
|
+
const fnStart = SRC.indexOf('async function handleAckOnly(')
|
|
207
|
+
const fnSlice = SRC.slice(fnStart, fnStart + 2000)
|
|
208
|
+
expect(fnSlice).toContain('gate(ctx)')
|
|
209
|
+
expect(fnSlice).toContain('setMessageReaction')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('handleAckOnly drops non-allowlisted senders silently', () => {
|
|
213
|
+
const fnStart = SRC.indexOf('async function handleAckOnly(')
|
|
214
|
+
const fnSlice = SRC.slice(fnStart, fnStart + 2000)
|
|
215
|
+
expect(fnSlice).toMatch(/action === 'drop'/)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('handleAckOnly is wrapped in try/catch', () => {
|
|
219
|
+
const fnStart = SRC.indexOf('async function handleAckOnly(')
|
|
220
|
+
const fnSlice = SRC.slice(fnStart, fnStart + 2000)
|
|
221
|
+
expect(fnSlice).toMatch(/try\s*\{/)
|
|
222
|
+
expect(fnSlice).toMatch(/catch\s*\(/)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('handleRefusal is declared and sends a reply via sendMessage', () => {
|
|
226
|
+
expect(SRC).toMatch(/async function handleRefusal\(/)
|
|
227
|
+
const fnStart = SRC.indexOf('async function handleRefusal(')
|
|
228
|
+
const fnSlice = SRC.slice(fnStart, fnStart + 2000)
|
|
229
|
+
expect(fnSlice).toContain('gate(ctx)')
|
|
230
|
+
expect(fnSlice).toContain('sendMessage')
|
|
231
|
+
// SECURITY-tagged log line so operators see the refusal in
|
|
232
|
+
// their stderr scrape.
|
|
233
|
+
expect(fnSlice).toMatch(/SECURITY/)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('handleRefusal sits behind the gate (no leak to strangers)', () => {
|
|
237
|
+
// Mirrors handleAckOnly — we don't even confirm the bot exists
|
|
238
|
+
// to a sender who isn't allowlisted.
|
|
239
|
+
const fnStart = SRC.indexOf('async function handleRefusal(')
|
|
240
|
+
const fnSlice = SRC.slice(fnStart, fnStart + 2000)
|
|
241
|
+
expect(fnSlice).toMatch(/action === 'drop'/)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// ─── Decision-matrix completeness ────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
describe('inbound message-type decisions cover all 13 types from #1077', () => {
|
|
248
|
+
it('every kind in the matrix has exactly one decision', () => {
|
|
249
|
+
const forwardSet = new Set<string>(FORWARDING)
|
|
250
|
+
const ackSet = new Set<string>(ACK_ONLY)
|
|
251
|
+
const refusalSet = new Set<string>(['passport_data'])
|
|
252
|
+
for (const kind of ALL_KINDS) {
|
|
253
|
+
const inForward = forwardSet.has(kind)
|
|
254
|
+
const inAck = ackSet.has(kind)
|
|
255
|
+
const inRefusal = refusalSet.has(kind)
|
|
256
|
+
const count = Number(inForward) + Number(inAck) + Number(inRefusal)
|
|
257
|
+
expect(count, `${kind} should appear in exactly one bucket, got ${count}`).toBe(1)
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('matrix totals: 7 forward, 5 ack-only, 1 refuse', () => {
|
|
262
|
+
expect(FORWARDING.length).toBe(7)
|
|
263
|
+
expect(ACK_ONLY.length).toBe(5)
|
|
264
|
+
// All 13 covered, no extras dropped.
|
|
265
|
+
expect(FORWARDING.length + ACK_ONLY.length + 1).toBe(ALL_KINDS.length)
|
|
266
|
+
})
|
|
267
|
+
})
|
|
@@ -493,3 +493,52 @@ describe("createIssuesCardHandle — persistence (#472 #19)", () => {
|
|
|
493
493
|
}
|
|
494
494
|
});
|
|
495
495
|
});
|
|
496
|
+
|
|
497
|
+
// ─── #1075 — THREAD_NOT_FOUND propagation ───────────────────────────────────
|
|
498
|
+
//
|
|
499
|
+
// In production the gateway wires the issues-card's `bot` adapter through
|
|
500
|
+
// `robustApiCall`, which throws a wrapped error with `message ==
|
|
501
|
+
// 'THREAD_NOT_FOUND'` when the underlying GrammyError matches "thread not
|
|
502
|
+
// found". The card module's job is to NOT crash — it should log and
|
|
503
|
+
// re-post on the next refresh (the same path it uses for "message not
|
|
504
|
+
// found" stale-edit recovery). Both shapes are tested here.
|
|
505
|
+
|
|
506
|
+
describe("createIssuesCardHandle — #1075 thread-deleted resilience", () => {
|
|
507
|
+
it("re-posts the card when an edit throws THREAD_NOT_FOUND", async () => {
|
|
508
|
+
const bot = makeFakeBot();
|
|
509
|
+
const handle = createIssuesCardHandle({
|
|
510
|
+
agentName: "klanker",
|
|
511
|
+
chatId: "1",
|
|
512
|
+
bot,
|
|
513
|
+
now: () => 1_000_000,
|
|
514
|
+
});
|
|
515
|
+
await handle.refresh([makeEvent({ summary: "first" })]);
|
|
516
|
+
expect(bot.sent).toHaveLength(1);
|
|
517
|
+
|
|
518
|
+
// Mid-flight, the topic gets deleted. The wrapped adapter throws
|
|
519
|
+
// the special THREAD_NOT_FOUND error on the next edit.
|
|
520
|
+
bot.editMessageText = async () => {
|
|
521
|
+
throw Object.assign(new Error("THREAD_NOT_FOUND"), { original: null });
|
|
522
|
+
};
|
|
523
|
+
await handle.refresh([makeEvent({ summary: "after thread delete" })]);
|
|
524
|
+
// Card was forced to re-post (sent twice).
|
|
525
|
+
expect(bot.sent).toHaveLength(2);
|
|
526
|
+
// And it should NOT have crashed — handle still operates.
|
|
527
|
+
expect(handle.messageId()).not.toBeNull();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("does not crash when sendMessage itself throws THREAD_NOT_FOUND", async () => {
|
|
531
|
+
const bot = makeFakeBot();
|
|
532
|
+
bot.sendMessage = async () => {
|
|
533
|
+
throw Object.assign(new Error("THREAD_NOT_FOUND"), { original: null });
|
|
534
|
+
};
|
|
535
|
+
const handle = createIssuesCardHandle({
|
|
536
|
+
agentName: "klanker",
|
|
537
|
+
chatId: "1",
|
|
538
|
+
bot,
|
|
539
|
+
});
|
|
540
|
+
// First refresh — should swallow the THREAD_NOT_FOUND, no crash.
|
|
541
|
+
await expect(handle.refresh([makeEvent({})])).resolves.toBeUndefined();
|
|
542
|
+
expect(handle.messageId()).toBeNull();
|
|
543
|
+
});
|
|
544
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pin the per-agent inbound buffer that closes the #1150 root cause:
|
|
3
|
+
* if the gateway tries to deliver a synthetic inbound while the agent's
|
|
4
|
+
* bridge isn't connected (mid-reconnect, claude-session bouncing, etc),
|
|
5
|
+
* the inbound used to be silently dropped. Now it's buffered and
|
|
6
|
+
* drained on the next bridge-register.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest'
|
|
10
|
+
import { createPendingInboundBuffer, DEFAULT_PENDING_INBOUND_CAP } from '../gateway/pending-inbound-buffer.js'
|
|
11
|
+
import type { InboundMessage } from '../gateway/ipc-protocol.js'
|
|
12
|
+
|
|
13
|
+
function inbound(source: string, ts = Date.now()): InboundMessage {
|
|
14
|
+
return {
|
|
15
|
+
type: 'inbound',
|
|
16
|
+
chatId: 'c1',
|
|
17
|
+
messageId: ts,
|
|
18
|
+
user: 'vault-broker',
|
|
19
|
+
userId: 0,
|
|
20
|
+
ts,
|
|
21
|
+
text: `synthetic ${source}`,
|
|
22
|
+
meta: { source },
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('pending-inbound-buffer', () => {
|
|
27
|
+
it('push + drain — FIFO order per agent', () => {
|
|
28
|
+
const buf = createPendingInboundBuffer({ log: () => {} })
|
|
29
|
+
buf.push('a', inbound('vault_grant_approved', 1))
|
|
30
|
+
buf.push('a', inbound('cron', 2))
|
|
31
|
+
buf.push('a', inbound('reaction', 3))
|
|
32
|
+
const drained = buf.drain('a')
|
|
33
|
+
expect(drained.map((m) => m.meta?.source)).toEqual([
|
|
34
|
+
'vault_grant_approved',
|
|
35
|
+
'cron',
|
|
36
|
+
'reaction',
|
|
37
|
+
])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('drain is idempotent — second call returns empty', () => {
|
|
41
|
+
const buf = createPendingInboundBuffer({ log: () => {} })
|
|
42
|
+
buf.push('a', inbound('x'))
|
|
43
|
+
expect(buf.drain('a')).toHaveLength(1)
|
|
44
|
+
expect(buf.drain('a')).toHaveLength(0)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('drain only affects the named agent', () => {
|
|
48
|
+
const buf = createPendingInboundBuffer({ log: () => {} })
|
|
49
|
+
buf.push('a', inbound('x'))
|
|
50
|
+
buf.push('b', inbound('y'))
|
|
51
|
+
expect(buf.drain('a').map((m) => m.meta?.source)).toEqual(['x'])
|
|
52
|
+
expect(buf.depth('b')).toBe(1)
|
|
53
|
+
expect(buf.drain('b').map((m) => m.meta?.source)).toEqual(['y'])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('respects per-agent cap — oldest evicted when full', () => {
|
|
57
|
+
const buf = createPendingInboundBuffer({ capPerAgent: 3, log: () => {} })
|
|
58
|
+
// Push 1 .. 5; cap is 3 so 1, 2 should be evicted.
|
|
59
|
+
buf.push('a', inbound('m1', 1))
|
|
60
|
+
buf.push('a', inbound('m2', 2))
|
|
61
|
+
buf.push('a', inbound('m3', 3))
|
|
62
|
+
buf.push('a', inbound('m4', 4))
|
|
63
|
+
buf.push('a', inbound('m5', 5))
|
|
64
|
+
expect(buf.depth('a')).toBe(3)
|
|
65
|
+
const drained = buf.drain('a')
|
|
66
|
+
expect(drained.map((m) => m.meta?.source)).toEqual(['m3', 'm4', 'm5'])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('push returns false when eviction occurred', () => {
|
|
70
|
+
const buf = createPendingInboundBuffer({ capPerAgent: 2, log: () => {} })
|
|
71
|
+
expect(buf.push('a', inbound('m1'))).toBe(true)
|
|
72
|
+
expect(buf.push('a', inbound('m2'))).toBe(true)
|
|
73
|
+
expect(buf.push('a', inbound('m3'))).toBe(false) // evicted m1
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('default cap is 32', () => {
|
|
77
|
+
expect(DEFAULT_PENDING_INBOUND_CAP).toBe(32)
|
|
78
|
+
const buf = createPendingInboundBuffer({ log: () => {} })
|
|
79
|
+
for (let i = 0; i < 32; i++) buf.push('a', inbound(`m${i}`, i))
|
|
80
|
+
expect(buf.depth('a')).toBe(32)
|
|
81
|
+
buf.push('a', inbound('m33', 33))
|
|
82
|
+
expect(buf.depth('a')).toBe(32) // still at cap
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('logs on eviction', () => {
|
|
86
|
+
const logs: string[] = []
|
|
87
|
+
const buf = createPendingInboundBuffer({ capPerAgent: 1, log: (l) => logs.push(l) })
|
|
88
|
+
buf.push('a', inbound('m1', 1))
|
|
89
|
+
buf.push('a', inbound('m2', 2)) // evicts m1
|
|
90
|
+
expect(logs.some((l) => l.includes('cap=1') && l.includes('dropped oldest'))).toBe(true)
|
|
91
|
+
expect(logs.some((l) => l.includes('m1'))).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('logs on push (depth tracking visibility)', () => {
|
|
95
|
+
const logs: string[] = []
|
|
96
|
+
const buf = createPendingInboundBuffer({ log: (l) => logs.push(l) })
|
|
97
|
+
buf.push('a', inbound('vault_grant_approved'))
|
|
98
|
+
expect(logs.some((l) => l.includes('agent=a buffered source=vault_grant_approved depth_after=1'))).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('logs on drain with source listing', () => {
|
|
102
|
+
const logs: string[] = []
|
|
103
|
+
const buf = createPendingInboundBuffer({ log: (l) => logs.push(l) })
|
|
104
|
+
buf.push('a', inbound('vault_grant_approved'))
|
|
105
|
+
buf.push('a', inbound('cron'))
|
|
106
|
+
logs.length = 0
|
|
107
|
+
buf.drain('a')
|
|
108
|
+
expect(logs.some((l) => l.includes('drained agent=a count=2'))).toBe(true)
|
|
109
|
+
expect(logs.some((l) => l.includes('sources=[vault_grant_approved,cron]'))).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('drain on empty agent does not log', () => {
|
|
113
|
+
const logs: string[] = []
|
|
114
|
+
const buf = createPendingInboundBuffer({ log: (l) => logs.push(l) })
|
|
115
|
+
expect(buf.drain('never-pushed')).toEqual([])
|
|
116
|
+
expect(logs).toEqual([])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('depth and totalDepth track correctly across agents', () => {
|
|
120
|
+
const buf = createPendingInboundBuffer({ log: () => {} })
|
|
121
|
+
expect(buf.totalDepth()).toBe(0)
|
|
122
|
+
buf.push('a', inbound('x'))
|
|
123
|
+
buf.push('a', inbound('y'))
|
|
124
|
+
buf.push('b', inbound('z'))
|
|
125
|
+
expect(buf.depth('a')).toBe(2)
|
|
126
|
+
expect(buf.depth('b')).toBe(1)
|
|
127
|
+
expect(buf.depth('c')).toBe(0)
|
|
128
|
+
expect(buf.totalDepth()).toBe(3)
|
|
129
|
+
buf.drain('a')
|
|
130
|
+
expect(buf.totalDepth()).toBe(1)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { describe, it, expect } from 'vitest'
|
|
15
|
-
import { resolveAlwaysAllowRule } from '../permission-rule.js'
|
|
15
|
+
import { resolveAlwaysAllowRule, matchesAllowRule } from '../permission-rule.js'
|
|
16
16
|
|
|
17
17
|
describe('resolveAlwaysAllowRule — Skill', () => {
|
|
18
18
|
it('returns Skill(name) for a typical skill input', () => {
|
|
@@ -119,3 +119,82 @@ describe('resolveAlwaysAllowRule — fallback', () => {
|
|
|
119
119
|
expect(resolveAlwaysAllowRule('', undefined)).toBeNull()
|
|
120
120
|
})
|
|
121
121
|
})
|
|
122
|
+
|
|
123
|
+
describe('matchesAllowRule — bare tool names', () => {
|
|
124
|
+
// The whole point of #1138: a cached `Edit` rule covers every Edit
|
|
125
|
+
// call from the parent claude AND from sub-agents dispatched via the
|
|
126
|
+
// Task tool, no matter the file path.
|
|
127
|
+
it('matches any invocation of the same tool', () => {
|
|
128
|
+
expect(matchesAllowRule('Edit', 'Edit', undefined)).toBe(true)
|
|
129
|
+
expect(matchesAllowRule('Edit', 'Edit', JSON.stringify({ file_path: '/tmp/a' }))).toBe(true)
|
|
130
|
+
expect(matchesAllowRule('Edit', 'Edit', JSON.stringify({ file_path: '/etc/passwd' }))).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('does not bleed into other tools', () => {
|
|
134
|
+
expect(matchesAllowRule('Edit', 'Write', undefined)).toBe(false)
|
|
135
|
+
expect(matchesAllowRule('Read', 'Edit', undefined)).toBe(false)
|
|
136
|
+
expect(matchesAllowRule('Bash', 'BashOutput', undefined)).toBe(false)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it.each(['Bash', 'Read', 'Write', 'MultiEdit', 'Glob', 'Grep', 'WebFetch', 'TodoWrite'])(
|
|
140
|
+
'roundtrips through resolve → match for %s',
|
|
141
|
+
(tool) => {
|
|
142
|
+
const resolved = resolveAlwaysAllowRule(tool, undefined)
|
|
143
|
+
expect(resolved).not.toBeNull()
|
|
144
|
+
expect(matchesAllowRule(resolved!.rule, tool, undefined)).toBe(true)
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('matchesAllowRule — Skill(name)', () => {
|
|
150
|
+
it('matches only the specific skill', () => {
|
|
151
|
+
expect(matchesAllowRule('Skill(mail)', 'Skill', JSON.stringify({ skill: 'mail' }))).toBe(true)
|
|
152
|
+
expect(matchesAllowRule('Skill(mail)', 'Skill', JSON.stringify({ skill: 'calendar' }))).toBe(false)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('uses the same field fallback chain as the resolver', () => {
|
|
156
|
+
expect(matchesAllowRule('Skill(mail)', 'Skill', JSON.stringify({ skill_name: 'mail' }))).toBe(true)
|
|
157
|
+
expect(matchesAllowRule('Skill(mail)', 'Skill', JSON.stringify({ skillName: 'mail' }))).toBe(true)
|
|
158
|
+
expect(matchesAllowRule('Skill(mail)', 'Skill', JSON.stringify({ name: 'mail' }))).toBe(true)
|
|
159
|
+
expect(matchesAllowRule(
|
|
160
|
+
'Skill(coolify)',
|
|
161
|
+
'Skill',
|
|
162
|
+
JSON.stringify({ path: 'skills/coolify/SKILL.md' }),
|
|
163
|
+
)).toBe(true)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('does not match a different tool with the same arg', () => {
|
|
167
|
+
expect(matchesAllowRule('Skill(mail)', 'Bash', JSON.stringify({ skill: 'mail' }))).toBe(false)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('returns false on malformed Skill input', () => {
|
|
171
|
+
expect(matchesAllowRule('Skill(mail)', 'Skill', undefined)).toBe(false)
|
|
172
|
+
expect(matchesAllowRule('Skill(mail)', 'Skill', 'not-json')).toBe(false)
|
|
173
|
+
expect(matchesAllowRule('Skill(mail)', 'Skill', JSON.stringify({ unrelated: 'x' }))).toBe(false)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('matchesAllowRule — MCP tools', () => {
|
|
178
|
+
it('matches the exact namespaced tool', () => {
|
|
179
|
+
expect(matchesAllowRule(
|
|
180
|
+
'mcp__switchroom-telegram__reply',
|
|
181
|
+
'mcp__switchroom-telegram__reply',
|
|
182
|
+
undefined,
|
|
183
|
+
)).toBe(true)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('does not match a different MCP tool on the same server', () => {
|
|
187
|
+
expect(matchesAllowRule(
|
|
188
|
+
'mcp__switchroom-telegram__reply',
|
|
189
|
+
'mcp__switchroom-telegram__stream_reply',
|
|
190
|
+
undefined,
|
|
191
|
+
)).toBe(false)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('matchesAllowRule — defensive', () => {
|
|
196
|
+
it('returns false for empty inputs', () => {
|
|
197
|
+
expect(matchesAllowRule('', 'Edit', undefined)).toBe(false)
|
|
198
|
+
expect(matchesAllowRule('Edit', '', undefined)).toBe(false)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
@@ -103,4 +103,35 @@ describe('summarizeToolForTitle (#186)', () => {
|
|
|
103
103
|
const input = JSON.stringify({ skill: 'mail', name: 'wrong' })
|
|
104
104
|
expect(summarizeToolForTitle('Skill', input)).toBe('Skill (mail)')
|
|
105
105
|
})
|
|
106
|
+
|
|
107
|
+
test('MCP curated: agent-config tools render as human verb-phrases (#1215)', () => {
|
|
108
|
+
expect(summarizeToolForTitle('mcp__agent-config__skill_list', undefined)).toBe(
|
|
109
|
+
'List its own installed skills',
|
|
110
|
+
)
|
|
111
|
+
expect(summarizeToolForTitle('mcp__agent-config__cron_list', undefined)).toBe(
|
|
112
|
+
'List its own scheduled tasks',
|
|
113
|
+
)
|
|
114
|
+
expect(summarizeToolForTitle('mcp__agent-config__peers_list', undefined)).toBe(
|
|
115
|
+
'List the other agents on this instance',
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('MCP curated: hostd tools render as human verb-phrases (#1215)', () => {
|
|
120
|
+
expect(summarizeToolForTitle('mcp__hostd__agent_logs', undefined)).toBe(
|
|
121
|
+
"Read another agent's container logs",
|
|
122
|
+
)
|
|
123
|
+
expect(summarizeToolForTitle('mcp__hostd__agent_exec', undefined)).toBe(
|
|
124
|
+
'Run a read-only inspection inside another agent',
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('MCP fallback: unknown mcp tool renders as `<server>: <verb with spaces>`', () => {
|
|
129
|
+
expect(summarizeToolForTitle('mcp__some-server__do_thing', undefined)).toBe(
|
|
130
|
+
'some-server: do thing',
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('MCP malformed: bare mcp__ prefix without __<server>__<verb> shape is left alone', () => {
|
|
135
|
+
expect(summarizeToolForTitle('mcp__bad', undefined)).toBe('mcp__bad')
|
|
136
|
+
})
|
|
106
137
|
})
|