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
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Phase 3b foreman write operations:
|
|
3
|
-
* - handleRestartCommand (mocked execFileSync)
|
|
4
|
-
* - handleDeleteCommand + executeDeleteAgent (mocked exec + FS)
|
|
5
|
-
* - handleUpdateCommand (mocked combined exec)
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
9
|
-
import {
|
|
10
|
-
handleRestartCommand,
|
|
11
|
-
handleDeleteCommand,
|
|
12
|
-
executeDeleteAgent,
|
|
13
|
-
handleUpdateCommand,
|
|
14
|
-
type SwitchroomExecFn,
|
|
15
|
-
} from '../foreman/foreman-handlers.js'
|
|
16
|
-
import type { execFileSync } from 'child_process'
|
|
17
|
-
|
|
18
|
-
// ─── /restart ─────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
describe('foreman: handleRestartCommand — input validation', () => {
|
|
21
|
-
it('returns usage when no agent specified', () => {
|
|
22
|
-
const result = handleRestartCommand('')
|
|
23
|
-
expect(result.ok).toBe(false)
|
|
24
|
-
expect(result.text).toContain('Usage')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('rejects invalid agent name', () => {
|
|
28
|
-
const execFile = vi.fn()
|
|
29
|
-
// 'BadName' has uppercase — first token is invalid
|
|
30
|
-
const result = handleRestartCommand('BadName', execFile as never)
|
|
31
|
-
expect(result.ok).toBe(false)
|
|
32
|
-
expect(result.text).toBe('Invalid agent name.')
|
|
33
|
-
expect(execFile).not.toHaveBeenCalled()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('rejects path traversal', () => {
|
|
37
|
-
const execFile = vi.fn()
|
|
38
|
-
const result = handleRestartCommand('../etc/passwd', execFile as never)
|
|
39
|
-
expect(result.ok).toBe(false)
|
|
40
|
-
expect(execFile).not.toHaveBeenCalled()
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('rejects agent name with colon', () => {
|
|
44
|
-
const execFile = vi.fn()
|
|
45
|
-
const result = handleRestartCommand('bad:name', execFile as never)
|
|
46
|
-
expect(result.ok).toBe(false)
|
|
47
|
-
expect(execFile).not.toHaveBeenCalled()
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe('foreman: handleRestartCommand — execFileSync calls', () => {
|
|
52
|
-
it('calls systemctl --user restart with correct unit name', () => {
|
|
53
|
-
const execFile = vi.fn().mockReturnValue('')
|
|
54
|
-
const result = handleRestartCommand('gymbro', execFile as never)
|
|
55
|
-
|
|
56
|
-
expect(result.ok).toBe(true)
|
|
57
|
-
expect(execFile).toHaveBeenCalledOnce()
|
|
58
|
-
const [cmd, args] = execFile.mock.calls[0] as [string, string[]]
|
|
59
|
-
expect(cmd).toBe('systemctl')
|
|
60
|
-
expect(args).toContain('--user')
|
|
61
|
-
expect(args).toContain('restart')
|
|
62
|
-
expect(args).toContain('switchroom-gymbro')
|
|
63
|
-
// Must NOT be a shell string — second arg must be an array
|
|
64
|
-
expect(Array.isArray(args)).toBe(true)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('uses execFileSync not execSync (no shell)', () => {
|
|
68
|
-
const execFile = vi.fn().mockReturnValue('')
|
|
69
|
-
handleRestartCommand('gymbro', execFile as never)
|
|
70
|
-
// The function is called with 3 args (cmd, args[], opts) not a shell string
|
|
71
|
-
const [, args] = execFile.mock.calls[0] as [string, string[], object]
|
|
72
|
-
expect(Array.isArray(args)).toBe(true)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('includes agent name in success reply', () => {
|
|
76
|
-
const execFile = vi.fn().mockReturnValue('')
|
|
77
|
-
const result = handleRestartCommand('gymbro', execFile as never)
|
|
78
|
-
expect(result.ok).toBe(true)
|
|
79
|
-
expect(result.text).toContain('gymbro')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('returns error text when systemctl fails', () => {
|
|
83
|
-
const execFile = vi.fn().mockImplementation(() => {
|
|
84
|
-
throw Object.assign(new Error('unit not found'), { stderr: 'Unit switchroom-gymbro.service not found.' })
|
|
85
|
-
})
|
|
86
|
-
const result = handleRestartCommand('gymbro', execFile as never)
|
|
87
|
-
expect(result.ok).toBe(false)
|
|
88
|
-
expect(result.text).toContain('restart failed')
|
|
89
|
-
expect(result.text).toContain('gymbro')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('handles agent name with hyphen', () => {
|
|
93
|
-
const execFile = vi.fn().mockReturnValue('')
|
|
94
|
-
handleRestartCommand('my-agent', execFile as never)
|
|
95
|
-
const [, args] = execFile.mock.calls[0] as [string, string[]]
|
|
96
|
-
expect(args).toContain('switchroom-my-agent')
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('escapes HTML in error output', () => {
|
|
100
|
-
const execFile = vi.fn().mockImplementation(() => {
|
|
101
|
-
throw Object.assign(new Error('err'), { stderr: 'Error: <unit> not found' })
|
|
102
|
-
})
|
|
103
|
-
const result = handleRestartCommand('gymbro', execFile as never)
|
|
104
|
-
expect(result.text).not.toContain('<unit>')
|
|
105
|
-
})
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
// ─── /delete first step ───────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
describe('foreman: handleDeleteCommand — prompt', () => {
|
|
111
|
-
it('returns usage when no agent specified', () => {
|
|
112
|
-
const result = handleDeleteCommand('')
|
|
113
|
-
expect(result.replies[0].text).toContain('Usage')
|
|
114
|
-
expect(result.needsConfirm).toBeFalsy()
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('rejects invalid agent name', () => {
|
|
118
|
-
// 'BadAgent' has uppercase — invalid
|
|
119
|
-
const result = handleDeleteCommand('BadAgent')
|
|
120
|
-
expect(result.replies[0].text).toBe('Invalid agent name.')
|
|
121
|
-
expect(result.needsConfirm).toBeFalsy()
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('returns confirmation prompt for valid agent', () => {
|
|
125
|
-
const result = handleDeleteCommand('gymbro')
|
|
126
|
-
expect(result.replies[0].text).toContain('gymbro')
|
|
127
|
-
expect(result.replies[0].text).toContain('YES')
|
|
128
|
-
expect(result.needsConfirm).toBe(true)
|
|
129
|
-
expect(result.agentForConfirm).toBe('gymbro')
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('escapes HTML in agent name in prompt', () => {
|
|
133
|
-
// valid name, but let's use one that could trip HTML — actually all valid names
|
|
134
|
-
// are alphanumeric so no HTML risk; just verify the name appears
|
|
135
|
-
const result = handleDeleteCommand('my-agent')
|
|
136
|
-
expect(result.replies[0].text).toContain('my-agent')
|
|
137
|
-
expect(result.replies[0].html).toBe(true)
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
// ─── executeDeleteAgent ───────────────────────────────────────────────────
|
|
142
|
-
|
|
143
|
-
describe('foreman: executeDeleteAgent — execution', () => {
|
|
144
|
-
it('runs CLI destroy with --yes flag', () => {
|
|
145
|
-
const switchroomExec: SwitchroomExecFn = vi.fn().mockReturnValue('Removed unit.')
|
|
146
|
-
const execFile = vi.fn().mockReturnValue('')
|
|
147
|
-
const tmpDir = '/tmp/fake-agents-dir'
|
|
148
|
-
|
|
149
|
-
const result = executeDeleteAgent('gymbro', switchroomExec, execFile as never, tmpDir)
|
|
150
|
-
expect(switchroomExec).toHaveBeenCalledWith(['agent', 'destroy', '--yes', 'gymbro'])
|
|
151
|
-
expect(result.replies[0].text).toContain('gymbro')
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('reports success without archive when dir does not exist', () => {
|
|
155
|
-
const switchroomExec: SwitchroomExecFn = vi.fn().mockReturnValue('done')
|
|
156
|
-
const result = executeDeleteAgent('gymbro', switchroomExec, vi.fn() as never, '/nonexistent-agents-dir')
|
|
157
|
-
// No archive reported (dir didn't exist)
|
|
158
|
-
expect(result.replies[0].text).not.toContain('Archived')
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('rejects invalid agent name without calling CLI', () => {
|
|
162
|
-
const switchroomExec: SwitchroomExecFn = vi.fn()
|
|
163
|
-
const result = executeDeleteAgent('bad name!', switchroomExec, vi.fn() as never, '/tmp')
|
|
164
|
-
expect(result.replies[0].text).toBe('Invalid agent name.')
|
|
165
|
-
expect(switchroomExec).not.toHaveBeenCalled()
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('reports CLI error but notes archive succeeded', () => {
|
|
169
|
-
const switchroomExec: SwitchroomExecFn = vi.fn().mockImplementation(() => {
|
|
170
|
-
throw Object.assign(new Error('destroy failed'), { stderr: 'switchroom error' })
|
|
171
|
-
})
|
|
172
|
-
// Use a dir that definitely doesn't exist so no actual rename
|
|
173
|
-
const result = executeDeleteAgent('gymbro', switchroomExec, vi.fn() as never, '/nonexistent-agents-12345')
|
|
174
|
-
expect(result.replies[0].text).toContain('CLI destroy failed')
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
// ─── /update ──────────────────────────────────────────────────────────────
|
|
179
|
-
|
|
180
|
-
describe('foreman: handleUpdateCommand', () => {
|
|
181
|
-
it('calls switchroomExec with ["update"]', () => {
|
|
182
|
-
const exec: SwitchroomExecFn = vi.fn().mockReturnValue('Updated successfully.')
|
|
183
|
-
handleUpdateCommand(exec)
|
|
184
|
-
expect(exec).toHaveBeenCalledWith(['update'])
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('returns output in a pre block', () => {
|
|
188
|
-
const exec: SwitchroomExecFn = vi.fn().mockReturnValue('Pulled abc123. Reconciled 2 agents.')
|
|
189
|
-
const result = handleUpdateCommand(exec)
|
|
190
|
-
expect(result.replies[0].text).toContain('Pulled abc123')
|
|
191
|
-
expect(result.replies[0].html).toBe(true)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('returns error message when CLI throws', () => {
|
|
195
|
-
const exec: SwitchroomExecFn = vi.fn().mockImplementation(() => {
|
|
196
|
-
throw Object.assign(new Error('not a git repo'), { stderr: 'fatal: not a git repository' })
|
|
197
|
-
})
|
|
198
|
-
const result = handleUpdateCommand(exec)
|
|
199
|
-
expect(result.replies[0].text).toContain('update failed')
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('returns no-output message when CLI returns empty', () => {
|
|
203
|
-
const exec: SwitchroomExecFn = vi.fn().mockReturnValue(' \n')
|
|
204
|
-
const result = handleUpdateCommand(exec)
|
|
205
|
-
expect(result.replies[0].text).toContain('Update complete')
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('paginates output > 3 KB', () => {
|
|
209
|
-
const bigOutput = 'x'.repeat(4000)
|
|
210
|
-
const exec: SwitchroomExecFn = vi.fn().mockReturnValue(bigOutput)
|
|
211
|
-
const result = handleUpdateCommand(exec)
|
|
212
|
-
expect(result.replies.length).toBeGreaterThan(1)
|
|
213
|
-
})
|
|
214
|
-
})
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Harness ordering invariants — table-driven scenarios.
|
|
3
|
-
*
|
|
4
|
-
* These tests assert cross-cutting properties that hold across a range
|
|
5
|
-
* of turn shapes (Class A/B/C, retry, replay-dup, error-cascade). The
|
|
6
|
-
* scenarios are deterministic and named, not random — see
|
|
7
|
-
* HARNESS_UPGRADE_PLAN.md "scenario fuzzer" decision (skeptic finding 7:
|
|
8
|
-
* property-based fuzzing without shrinking is worse than no fuzzing).
|
|
9
|
-
*
|
|
10
|
-
* Each invariant carries a `// fails when:` comment indicating the
|
|
11
|
-
* production change that would break it. The test author should mentally
|
|
12
|
-
* `git stash` that change and confirm the test fails — see plan
|
|
13
|
-
* "Validation rule for each new test."
|
|
14
|
-
*
|
|
15
|
-
* Invariants:
|
|
16
|
-
*
|
|
17
|
-
* INV-1 — Terminal reaction (👍) fires AT-OR-AFTER the last user-
|
|
18
|
-
* visible answer text. NEVER before. The Bug D/Z contract
|
|
19
|
-
* generalized to all turn shapes.
|
|
20
|
-
*
|
|
21
|
-
* INV-2 — Exactly one terminal reaction fires per logical turn
|
|
22
|
-
* (regardless of how many tool emoji ladder steps occurred
|
|
23
|
-
* in between). Catches a future regression where setDone
|
|
24
|
-
* fires twice.
|
|
25
|
-
*
|
|
26
|
-
* INV-3 — Editing a deleted message always errors. Catches a
|
|
27
|
-
* regression in the fake's delete-vs-edit ordering, which
|
|
28
|
-
* would let a buggy production module silently miss the
|
|
29
|
-
* "edit-to-deleted-message" failure mode in tests.
|
|
30
|
-
*
|
|
31
|
-
* INV-4 — Outbound dedup window holds for the full TTL once the
|
|
32
|
-
* cache is wired in.
|
|
33
|
-
*
|
|
34
|
-
* INV-5 — Hold-and-release ordering: an event fired while a
|
|
35
|
-
* sendMessage/editMessageText is parked at `holdNext`
|
|
36
|
-
* observes the world as it was BEFORE the held call landed.
|
|
37
|
-
* This pins the harness's own contract — necessary for
|
|
38
|
-
* future Bug-D-class tests to be expressible.
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
42
|
-
import { createRealGatewayHarness } from './real-gateway-harness.js'
|
|
43
|
-
import { createFakeBotApi } from './fake-bot-api.js'
|
|
44
|
-
|
|
45
|
-
const CHAT = '8248703757'
|
|
46
|
-
const INBOUND_MSG = 100
|
|
47
|
-
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
vi.useFakeTimers()
|
|
50
|
-
})
|
|
51
|
-
afterEach(() => {
|
|
52
|
-
vi.useRealTimers()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
interface TurnShape {
|
|
56
|
-
name: string
|
|
57
|
-
/** Drive the harness through one logical turn. Resolves when turn ended. */
|
|
58
|
-
drive: (h: ReturnType<typeof createRealGatewayHarness>) => Promise<void>
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const turnShapes: TurnShape[] = [
|
|
62
|
-
{
|
|
63
|
-
name: 'class-A reply (sub-2s, no tools)',
|
|
64
|
-
drive: async (h) => {
|
|
65
|
-
h.inbound({ chatId: CHAT, messageId: INBOUND_MSG, text: 'hi' })
|
|
66
|
-
h.feedSessionEvent({
|
|
67
|
-
kind: 'enqueue',
|
|
68
|
-
chatId: CHAT,
|
|
69
|
-
messageId: '1',
|
|
70
|
-
threadId: null,
|
|
71
|
-
rawContent: 'hi',
|
|
72
|
-
})
|
|
73
|
-
await h.clock.advance(20)
|
|
74
|
-
await h.streamReply({ chat_id: CHAT, text: 'Hello back!', done: true })
|
|
75
|
-
await h.clock.advance(20)
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
name: 'class-B with-tools (1 tool, ~3s)',
|
|
80
|
-
drive: async (h) => {
|
|
81
|
-
h.inbound({ chatId: CHAT, messageId: INBOUND_MSG, text: 'work' })
|
|
82
|
-
h.feedSessionEvent({
|
|
83
|
-
kind: 'enqueue',
|
|
84
|
-
chatId: CHAT,
|
|
85
|
-
messageId: '1',
|
|
86
|
-
threadId: null,
|
|
87
|
-
rawContent: 'work',
|
|
88
|
-
})
|
|
89
|
-
await h.clock.advance(50)
|
|
90
|
-
h.feedSessionEvent({ kind: 'thinking' })
|
|
91
|
-
await h.clock.advance(500)
|
|
92
|
-
h.feedSessionEvent({ kind: 'tool_use', toolName: 'Read' })
|
|
93
|
-
await h.clock.advance(2500)
|
|
94
|
-
await h.streamReply({ chat_id: CHAT, text: 'Here is the answer.', done: true })
|
|
95
|
-
await h.clock.advance(20)
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
name: 'class-C subagent (long, ~10s, sub-agent emit)',
|
|
100
|
-
drive: async (h) => {
|
|
101
|
-
h.inbound({ chatId: CHAT, messageId: INBOUND_MSG, text: 'big task' })
|
|
102
|
-
h.feedSessionEvent({
|
|
103
|
-
kind: 'enqueue',
|
|
104
|
-
chatId: CHAT,
|
|
105
|
-
messageId: '1',
|
|
106
|
-
threadId: null,
|
|
107
|
-
rawContent: 'big task',
|
|
108
|
-
})
|
|
109
|
-
await h.clock.advance(50)
|
|
110
|
-
h.feedSessionEvent({ kind: 'thinking' })
|
|
111
|
-
await h.clock.advance(500)
|
|
112
|
-
h.feedSessionEvent({ kind: 'tool_use', toolName: 'Read' })
|
|
113
|
-
await h.clock.advance(2000)
|
|
114
|
-
h.feedSessionEvent({ kind: 'tool_use', toolName: 'Grep' })
|
|
115
|
-
await h.clock.advance(2000)
|
|
116
|
-
h.feedSessionEvent({ kind: 'tool_use', toolName: 'Edit' })
|
|
117
|
-
await h.clock.advance(5000)
|
|
118
|
-
await h.streamReply({ chat_id: CHAT, text: 'Done — see the diff above.', done: true })
|
|
119
|
-
await h.clock.advance(20)
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
]
|
|
123
|
-
|
|
124
|
-
describe('INV-1 — terminal reaction fires AT-OR-AFTER last delivery (Bug D/Z generalized)', () => {
|
|
125
|
-
for (const shape of turnShapes) {
|
|
126
|
-
it(`${shape.name}: lastReactionEmojiAt >= lastAnswerTextDeliveredAt`, async () => {
|
|
127
|
-
// fails when: a future refactor moves setDone() from the streamReply
|
|
128
|
-
// post-await branch back to the JSONL turn_end handler — exactly
|
|
129
|
-
// Bug D's failure mode, generalized over turn shapes.
|
|
130
|
-
const h = createRealGatewayHarness({ gapMs: 0 })
|
|
131
|
-
await shape.drive(h)
|
|
132
|
-
const deliveredAt = h.lastAnswerTextDeliveredAt(CHAT)
|
|
133
|
-
const reactionAt = h.lastReactionEmojiAt(CHAT)
|
|
134
|
-
expect(deliveredAt, `no answer text delivered for ${shape.name}`).not.toBeNull()
|
|
135
|
-
expect(reactionAt, `no reaction emitted for ${shape.name}`).not.toBeNull()
|
|
136
|
-
expect(reactionAt!).toBeGreaterThanOrEqual(deliveredAt!)
|
|
137
|
-
h.finalize()
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
describe('INV-2 — terminal 👍 fires exactly once per turn (Bug Z generalized)', () => {
|
|
143
|
-
for (const shape of turnShapes) {
|
|
144
|
-
it(`${shape.name}: 👍 appears exactly once across the full reaction sequence`, async () => {
|
|
145
|
-
// fails when: a future change fires setDone() more than once
|
|
146
|
-
// for the same turn (e.g. both the streamReply post-await branch
|
|
147
|
-
// AND the turn_end JSONL handler call it). Specifically asserting
|
|
148
|
-
// the COUNT of 👍 in the full sequence — not just "the last
|
|
149
|
-
// emoji is unique" (which would be a tautology).
|
|
150
|
-
const h = createRealGatewayHarness({ gapMs: 0 })
|
|
151
|
-
await shape.drive(h)
|
|
152
|
-
const seq = h.recorder.reactionSequence()
|
|
153
|
-
expect(seq.length, `no reactions for ${shape.name}`).toBeGreaterThan(0)
|
|
154
|
-
const thumbsUpCount = seq.filter((e) => e === '👍').length
|
|
155
|
-
expect(thumbsUpCount).toBe(1)
|
|
156
|
-
// And the 👍 is the LAST reaction — terminal contract.
|
|
157
|
-
expect(seq[seq.length - 1]).toBe('👍')
|
|
158
|
-
h.finalize()
|
|
159
|
-
})
|
|
160
|
-
}
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
describe('INV-3 — editing a deleted message always errors', () => {
|
|
164
|
-
it('fake throws messageToEditNotFound after the message is deleted', async () => {
|
|
165
|
-
// fails when: someone changes fake-bot-api's editMessageText to
|
|
166
|
-
// silently succeed for deleted messages — production modules
|
|
167
|
-
// would then look correct in tests but error in real Telegram.
|
|
168
|
-
const bot = createFakeBotApi()
|
|
169
|
-
const r = (await bot.api.sendMessage('c1', 'long enough text content here ok', {})) as {
|
|
170
|
-
message_id: number
|
|
171
|
-
}
|
|
172
|
-
await bot.api.deleteMessage('c1', r.message_id)
|
|
173
|
-
await expect(
|
|
174
|
-
bot.api.editMessageText('c1', r.message_id, 'updated text content here ok', {}),
|
|
175
|
-
).rejects.toMatchObject({ error_code: 400 })
|
|
176
|
-
})
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
describe('INV-4 — outbound dedup window holds for the full TTL', () => {
|
|
180
|
-
// Span of "now" offsets within the TTL that should all be deduped.
|
|
181
|
-
const inWindowOffsets = [0, 1000, 30_000, 59_000]
|
|
182
|
-
// Span outside the TTL that should NOT be deduped.
|
|
183
|
-
const outOfWindowOffsets = [60_001, 120_000]
|
|
184
|
-
|
|
185
|
-
for (const ms of inWindowOffsets) {
|
|
186
|
-
it(`same content at +${ms}ms is suppressed (within TTL=60s)`, async () => {
|
|
187
|
-
// fails when: TTL is silently shortened, or the cache's eviction
|
|
188
|
-
// sweep evicts entries before their TTL expires.
|
|
189
|
-
const h = createRealGatewayHarness({ gapMs: 0, withDedup: true })
|
|
190
|
-
const text = 'A long enough message to clear the 24-char dedup floor by a wide margin.'
|
|
191
|
-
await h.send({ chat_id: CHAT, text })
|
|
192
|
-
await h.clock.advance(ms)
|
|
193
|
-
const id2 = await h.send({ chat_id: CHAT, text })
|
|
194
|
-
expect(id2).toBeNull()
|
|
195
|
-
h.finalize()
|
|
196
|
-
})
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
for (const ms of outOfWindowOffsets) {
|
|
200
|
-
it(`same content at +${ms}ms is allowed (outside TTL)`, async () => {
|
|
201
|
-
// fails when: TTL eviction breaks (entries linger past their TTL).
|
|
202
|
-
const h = createRealGatewayHarness({ gapMs: 0, withDedup: true })
|
|
203
|
-
const text = 'A long enough message to clear the 24-char dedup floor by a wide margin.'
|
|
204
|
-
await h.send({ chat_id: CHAT, text })
|
|
205
|
-
await h.clock.advance(ms)
|
|
206
|
-
const id2 = await h.send({ chat_id: CHAT, text })
|
|
207
|
-
expect(id2).not.toBeNull()
|
|
208
|
-
h.finalize()
|
|
209
|
-
})
|
|
210
|
-
}
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
describe('INV-5 — holdNext: events fired during a held call observe pre-held state', () => {
|
|
214
|
-
it('a setMessageReaction parked at holdNext lets unrelated state mutate before its release', async () => {
|
|
215
|
-
// fails when: a future fake-bot refactor makes holdNext block
|
|
216
|
-
// mutations on OTHER methods, or makes release() synchronous when
|
|
217
|
-
// it should be async.
|
|
218
|
-
//
|
|
219
|
-
// This is the foundational seam that makes Bug-D-class tests
|
|
220
|
-
// expressible: "while editMessageText is pending, fire the 👍" —
|
|
221
|
-
// the harness must let the 👍 land while the edit is parked.
|
|
222
|
-
const bot = createFakeBotApi()
|
|
223
|
-
const r = (await bot.api.sendMessage('c1', 'long enough text content here ok', {})) as {
|
|
224
|
-
message_id: number
|
|
225
|
-
}
|
|
226
|
-
const hold = bot.holdNext('editMessageText', 'c1')
|
|
227
|
-
// Start the edit; it parks at the gate.
|
|
228
|
-
const editPromise = bot.api.editMessageText('c1', r.message_id, 'edited content here ok', {})
|
|
229
|
-
// Yield a microtask so the held call enters its await.
|
|
230
|
-
await Promise.resolve()
|
|
231
|
-
expect(hold.triggered()).toBe(true)
|
|
232
|
-
// Fire something else — setMessageReaction — and confirm it lands
|
|
233
|
-
// independently while the edit is still parked.
|
|
234
|
-
await bot.api.setMessageReaction('c1', r.message_id, [{ type: 'emoji', emoji: '👍' }])
|
|
235
|
-
expect(bot.state.reactions.length).toBe(1)
|
|
236
|
-
// The edit's text hasn't landed yet — the message is still original.
|
|
237
|
-
expect(bot.textOf(r.message_id)).toBe('long enough text content here ok')
|
|
238
|
-
// Release the edit — now the text updates.
|
|
239
|
-
hold.release()
|
|
240
|
-
await editPromise
|
|
241
|
-
expect(bot.textOf(r.message_id)).toBe('edited content here ok')
|
|
242
|
-
})
|
|
243
|
-
})
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the structured pin/unpin event logger — the module that
|
|
3
|
-
* emits one JSON line per pin API interaction so operators can audit
|
|
4
|
-
* the pin lifecycle without parsing free-form log text.
|
|
5
|
-
*
|
|
6
|
-
* Covers spec `docs/pinned-progress-card-reliability.md` §6.1 and T9.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from "vitest";
|
|
10
|
-
import {
|
|
11
|
-
logPinEvent,
|
|
12
|
-
classifyPinError,
|
|
13
|
-
errorMessage,
|
|
14
|
-
type PinEvent,
|
|
15
|
-
} from "../pin-event-log.js";
|
|
16
|
-
|
|
17
|
-
describe("logPinEvent", () => {
|
|
18
|
-
it("writes one JSON line prefixed with 'pin-event: '", () => {
|
|
19
|
-
const lines: string[] = [];
|
|
20
|
-
const ev: PinEvent = {
|
|
21
|
-
event: "pin",
|
|
22
|
-
chatId: "100",
|
|
23
|
-
messageId: 42,
|
|
24
|
-
turnKey: "100::1",
|
|
25
|
-
outcome: "ok",
|
|
26
|
-
durationMs: 123,
|
|
27
|
-
};
|
|
28
|
-
logPinEvent(ev, (line) => lines.push(line));
|
|
29
|
-
expect(lines).toHaveLength(1);
|
|
30
|
-
expect(lines[0]).toMatch(/^pin-event: /);
|
|
31
|
-
expect(lines[0].endsWith("\n")).toBe(true);
|
|
32
|
-
const payload = JSON.parse(lines[0].replace(/^pin-event: /, "").trimEnd());
|
|
33
|
-
expect(payload).toEqual(ev);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("preserves all optional fields that are set", () => {
|
|
37
|
-
const lines: string[] = [];
|
|
38
|
-
logPinEvent(
|
|
39
|
-
{
|
|
40
|
-
event: "unpin-retry",
|
|
41
|
-
chatId: "c",
|
|
42
|
-
messageId: 7,
|
|
43
|
-
turnKey: "c::2",
|
|
44
|
-
outcome: "rate-limited",
|
|
45
|
-
error: "Too Many Requests",
|
|
46
|
-
durationMs: 1050,
|
|
47
|
-
},
|
|
48
|
-
(line) => lines.push(line),
|
|
49
|
-
);
|
|
50
|
-
const payload = JSON.parse(lines[0].replace(/^pin-event: /, "").trimEnd());
|
|
51
|
-
expect(payload.error).toBe("Too Many Requests");
|
|
52
|
-
expect(payload.outcome).toBe("rate-limited");
|
|
53
|
-
expect(payload.event).toBe("unpin-retry");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("omits undefined optional fields cleanly", () => {
|
|
57
|
-
const lines: string[] = [];
|
|
58
|
-
logPinEvent(
|
|
59
|
-
{
|
|
60
|
-
event: "sweep-pin",
|
|
61
|
-
chatId: "c",
|
|
62
|
-
outcome: "ok",
|
|
63
|
-
},
|
|
64
|
-
(line) => lines.push(line),
|
|
65
|
-
);
|
|
66
|
-
const raw = lines[0].replace(/^pin-event: /, "").trimEnd();
|
|
67
|
-
expect(raw).not.toContain("undefined");
|
|
68
|
-
const payload = JSON.parse(raw);
|
|
69
|
-
expect(payload.error).toBeUndefined();
|
|
70
|
-
expect(payload.messageId).toBeUndefined();
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe("classifyPinError", () => {
|
|
75
|
-
it("returns 'rate-limited' for Telegram 429", () => {
|
|
76
|
-
expect(
|
|
77
|
-
classifyPinError({ error_code: 429, description: "Too Many Requests: retry after 2" }),
|
|
78
|
-
).toBe("rate-limited");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("returns 'forbidden' for Telegram 403", () => {
|
|
82
|
-
expect(classifyPinError({ error_code: 403, description: "Forbidden: bot was kicked" })).toBe(
|
|
83
|
-
"forbidden",
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("falls back to message substring match when error_code is absent", () => {
|
|
88
|
-
expect(classifyPinError(new Error("Bad Request: not enough rights to manage pins"))).toBe(
|
|
89
|
-
"forbidden",
|
|
90
|
-
);
|
|
91
|
-
expect(classifyPinError(new Error("connect ETIMEDOUT 1.2.3.4:443"))).toBe("timeout");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("returns 'fail' for any unrecognised error shape", () => {
|
|
95
|
-
expect(classifyPinError(new Error("weird unexpected failure"))).toBe("fail");
|
|
96
|
-
expect(classifyPinError("random string")).toBe("fail");
|
|
97
|
-
expect(classifyPinError({})).toBe("fail");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("returns 'fail' for null/undefined", () => {
|
|
101
|
-
expect(classifyPinError(null)).toBe("fail");
|
|
102
|
-
expect(classifyPinError(undefined)).toBe("fail");
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe("errorMessage", () => {
|
|
107
|
-
it("extracts message from Error instance", () => {
|
|
108
|
-
expect(errorMessage(new Error("boom"))).toBe("boom");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("prefers description over message on grammy-shaped objects", () => {
|
|
112
|
-
expect(errorMessage({ description: "Telegram says no", message: "generic" })).toBe(
|
|
113
|
-
"Telegram says no",
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("returns empty string for null", () => {
|
|
118
|
-
expect(errorMessage(null)).toBe("");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("stringifies unknown shapes", () => {
|
|
122
|
-
expect(errorMessage(42)).toBe("42");
|
|
123
|
-
});
|
|
124
|
-
});
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PR-C2 — reportApiFailure crossing its threshold while a chat is in
|
|
3
|
-
* `pendingCompletion` state must not corrupt the deferred-completion
|
|
4
|
-
* resolution path.
|
|
5
|
-
*
|
|
6
|
-
* Setup: parent turn_end while a bg sub-agent is still running →
|
|
7
|
-
* chatState.pendingCompletion=true. Then `maxConsecutive4xx` permanent
|
|
8
|
-
* 4xx failures arrive (the card is being abandoned locally). We then
|
|
9
|
-
* resolve the bg sub-agent via sub_agent_turn_end. The driver must:
|
|
10
|
-
*
|
|
11
|
-
* - Fire onTurnComplete exactly once for the originating turnKey.
|
|
12
|
-
* - Not double-flush.
|
|
13
|
-
*
|
|
14
|
-
* fails when: the terminal-apiFailure branch races with the
|
|
15
|
-
* pendingCompletion resolution path and either swallows or duplicates
|
|
16
|
-
* the completion callback.
|
|
17
|
-
*/
|
|
18
|
-
import { describe, it, expect } from 'vitest'
|
|
19
|
-
import { makeHarness, enqueue } from './_progress-card-harness.js'
|
|
20
|
-
|
|
21
|
-
describe('PR-C2: API failure crossing threshold during pendingCompletion', () => {
|
|
22
|
-
it('deferred completion still resolves exactly once; no double-flush', () => {
|
|
23
|
-
const completions: string[] = []
|
|
24
|
-
const { driver } = makeHarness({
|
|
25
|
-
minIntervalMs: 0,
|
|
26
|
-
coalesceMs: 0,
|
|
27
|
-
heartbeatMs: 999_999, // keep heartbeat from racing the test
|
|
28
|
-
maxConsecutive4xx: 3,
|
|
29
|
-
promoteAfterMs: 999_999,
|
|
30
|
-
onTurnComplete: (s) => completions.push(s.turnKey),
|
|
31
|
-
})
|
|
32
|
-
const maps = driver._debugGetMaps!()
|
|
33
|
-
const CHAT = 'cA'
|
|
34
|
-
|
|
35
|
-
driver.ingest(enqueue(CHAT), null)
|
|
36
|
-
driver.ingest(
|
|
37
|
-
{
|
|
38
|
-
kind: 'tool_use',
|
|
39
|
-
toolName: 'Agent',
|
|
40
|
-
toolUseId: 'tu1',
|
|
41
|
-
input: { prompt: 'bg', run_in_background: true },
|
|
42
|
-
},
|
|
43
|
-
CHAT,
|
|
44
|
-
)
|
|
45
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'saBG', firstPromptText: 'bg' }, CHAT)
|
|
46
|
-
driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
|
|
47
|
-
driver.recordOutboundDelivered(CHAT)
|
|
48
|
-
driver.ingest({ kind: 'turn_end', durationMs: 100 }, CHAT)
|
|
49
|
-
|
|
50
|
-
expect(maps.chats.size).toBe(1)
|
|
51
|
-
const turnKey = [...maps.chats.keys()][0]
|
|
52
|
-
const cs = maps.chats.get(turnKey) as { pendingCompletion: boolean; apiFailures: { terminal: boolean } }
|
|
53
|
-
expect(cs.pendingCompletion).toBe(true)
|
|
54
|
-
|
|
55
|
-
// Hammer reportApiFailure past the threshold (3).
|
|
56
|
-
for (let i = 0; i < 5; i++) {
|
|
57
|
-
driver.reportApiFailure(turnKey, {
|
|
58
|
-
kind: 'permanent_4xx',
|
|
59
|
-
code: 400,
|
|
60
|
-
description: 'bad request',
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
expect(cs.apiFailures.terminal).toBe(true)
|
|
64
|
-
// No completion fired yet — bg still running.
|
|
65
|
-
expect(completions.length).toBe(0)
|
|
66
|
-
|
|
67
|
-
// Resolve the bg sub-agent. The originating turn must complete
|
|
68
|
-
// exactly once.
|
|
69
|
-
driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'saBG' }, CHAT)
|
|
70
|
-
expect(completions.length).toBe(1)
|
|
71
|
-
expect(completions[0]).toBe(turnKey)
|
|
72
|
-
})
|
|
73
|
-
})
|