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,510 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the /setup wizard state machine (setup-flow.ts).
|
|
3
|
-
*
|
|
4
|
-
* Pure function tests — no grammY, no SQLite, no network.
|
|
5
|
-
*
|
|
6
|
-
* Covers:
|
|
7
|
-
* - startSetupFlow: no slug, valid slug, invalid slug
|
|
8
|
-
* - handleSetupText: full happy-path step transitions
|
|
9
|
-
* - Validator helpers: isValidSlug, isValidPersonaName, isValidModel, isValidEmoji
|
|
10
|
-
* - Skip / cancel / error paths at each step
|
|
11
|
-
* - makeSetupInitialState / advanceSetupState / setupStepLabel helpers
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect } from 'vitest'
|
|
15
|
-
import {
|
|
16
|
-
startSetupFlow,
|
|
17
|
-
handleSetupText,
|
|
18
|
-
makeSetupInitialState,
|
|
19
|
-
advanceSetupState,
|
|
20
|
-
setupStepLabel,
|
|
21
|
-
isValidSlug,
|
|
22
|
-
isValidPersonaName,
|
|
23
|
-
isValidModel,
|
|
24
|
-
isValidEmoji,
|
|
25
|
-
isSkip,
|
|
26
|
-
isCancel,
|
|
27
|
-
} from '../foreman/setup-flow.js'
|
|
28
|
-
import type { SetupFlowState } from '../foreman/setup-state.js'
|
|
29
|
-
|
|
30
|
-
const CALLER = '12345678'
|
|
31
|
-
|
|
32
|
-
// #190: setup-flow now needs the operator's profile list. Tests that don't
|
|
33
|
-
// care about the profile validation just pass `default` so the asked-profile
|
|
34
|
-
// step accepts any input that maps to one of these.
|
|
35
|
-
const PROFILES = ['default', 'coding', 'health-coach', 'executive-assistant']
|
|
36
|
-
|
|
37
|
-
function makeState(overrides: Partial<SetupFlowState> = {}): SetupFlowState {
|
|
38
|
-
return {
|
|
39
|
-
chatId: 'chat1',
|
|
40
|
-
step: 'asked-slug',
|
|
41
|
-
slug: null,
|
|
42
|
-
persona: null,
|
|
43
|
-
model: null,
|
|
44
|
-
emoji: null,
|
|
45
|
-
profile: null,
|
|
46
|
-
botToken: null,
|
|
47
|
-
allowedUserId: null,
|
|
48
|
-
authSessionName: null,
|
|
49
|
-
loginUrl: null,
|
|
50
|
-
startedAt: 1000,
|
|
51
|
-
updatedAt: 1000,
|
|
52
|
-
...overrides,
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Test helper — wraps handleSetupText with a default profiles list so the
|
|
58
|
-
* 30+ existing call sites don't need to be rewritten one-by-one. Tests that
|
|
59
|
-
* specifically exercise the asked-profile validation pass their own
|
|
60
|
-
* profiles via the `opts` argument.
|
|
61
|
-
*/
|
|
62
|
-
function call(
|
|
63
|
-
state: SetupFlowState | null,
|
|
64
|
-
text: string,
|
|
65
|
-
opts: { callerId?: string; profiles?: string[] } = {},
|
|
66
|
-
) {
|
|
67
|
-
return handleSetupText({
|
|
68
|
-
state,
|
|
69
|
-
text,
|
|
70
|
-
callerId: opts.callerId ?? CALLER,
|
|
71
|
-
profiles: opts.profiles ?? PROFILES,
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ─── isValidSlug ─────────────────────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
describe('isValidSlug', () => {
|
|
78
|
-
it('accepts simple lowercase', () => expect(isValidSlug('gymbro')).toBe(true))
|
|
79
|
-
it('accepts hyphens', () => expect(isValidSlug('gym-bro')).toBe(true))
|
|
80
|
-
it('accepts underscores', () => expect(isValidSlug('gym_bro')).toBe(true))
|
|
81
|
-
it('accepts leading digit', () => expect(isValidSlug('1agent')).toBe(true))
|
|
82
|
-
it('rejects uppercase', () => expect(isValidSlug('GymBro')).toBe(false))
|
|
83
|
-
it('rejects spaces', () => expect(isValidSlug('gym bro')).toBe(false))
|
|
84
|
-
it('rejects empty', () => expect(isValidSlug('')).toBe(false))
|
|
85
|
-
it('accepts 51-char slug', () => expect(isValidSlug('a'.repeat(51))).toBe(true))
|
|
86
|
-
it('rejects 52-char slug', () => expect(isValidSlug('a'.repeat(52))).toBe(false))
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
// ─── isValidPersonaName ───────────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
describe('isValidPersonaName', () => {
|
|
92
|
-
it('accepts normal name', () => expect(isValidPersonaName('Gym Bro')).toBe(true))
|
|
93
|
-
it('accepts emoji in name', () => expect(isValidPersonaName('Clerk 💼')).toBe(true))
|
|
94
|
-
it('rejects empty string', () => expect(isValidPersonaName('')).toBe(false))
|
|
95
|
-
it('rejects control char', () => expect(isValidPersonaName('bad\x00name')).toBe(false))
|
|
96
|
-
it('rejects 81-char name', () => expect(isValidPersonaName('a'.repeat(81))).toBe(false))
|
|
97
|
-
it('accepts 80-char name', () => expect(isValidPersonaName('a'.repeat(80))).toBe(true))
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
// ─── isValidModel ─────────────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
describe('isValidModel', () => {
|
|
103
|
-
it('accepts sonnet alias', () => expect(isValidModel('sonnet')).toBe(true))
|
|
104
|
-
it('accepts opus alias', () => expect(isValidModel('opus')).toBe(true))
|
|
105
|
-
it('accepts haiku alias', () => expect(isValidModel('haiku')).toBe(true))
|
|
106
|
-
it('accepts inherit alias', () => expect(isValidModel('inherit')).toBe(true))
|
|
107
|
-
it('accepts full model ID', () => expect(isValidModel('claude-sonnet-4-5')).toBe(true))
|
|
108
|
-
it('rejects spaces', () => expect(isValidModel('bad model')).toBe(false))
|
|
109
|
-
it('rejects empty', () => expect(isValidModel('')).toBe(false))
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
// ─── isValidEmoji ─────────────────────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
describe('isValidEmoji', () => {
|
|
115
|
-
it('accepts single emoji', () => expect(isValidEmoji('🏋️')).toBe(true))
|
|
116
|
-
it('accepts simple ascii (single char)', () => expect(isValidEmoji('x')).toBe(true))
|
|
117
|
-
it('rejects empty string', () => expect(isValidEmoji('')).toBe(false))
|
|
118
|
-
it('rejects only whitespace', () => expect(isValidEmoji(' ')).toBe(false))
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
// ─── isSkip / isCancel ────────────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
describe('isSkip', () => {
|
|
124
|
-
it('matches "skip"', () => expect(isSkip('skip')).toBe(true))
|
|
125
|
-
it('matches "s"', () => expect(isSkip('s')).toBe(true))
|
|
126
|
-
it('matches "-"', () => expect(isSkip('-')).toBe(true))
|
|
127
|
-
it('ignores case', () => expect(isSkip('SKIP')).toBe(true))
|
|
128
|
-
it('does not match other words', () => expect(isSkip('no')).toBe(false))
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
describe('isCancel', () => {
|
|
132
|
-
it('matches "cancel"', () => expect(isCancel('cancel')).toBe(true))
|
|
133
|
-
it('matches "/cancel"', () => expect(isCancel('/cancel')).toBe(true))
|
|
134
|
-
it('matches "abort"', () => expect(isCancel('abort')).toBe(true))
|
|
135
|
-
it('ignores case', () => expect(isCancel('CANCEL')).toBe(true))
|
|
136
|
-
it('does not match "yes"', () => expect(isCancel('yes')).toBe(false))
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
// ─── startSetupFlow ───────────────────────────────────────────────────────
|
|
140
|
-
|
|
141
|
-
describe('startSetupFlow', () => {
|
|
142
|
-
it('asks for slug when no inline arg', () => {
|
|
143
|
-
const action = startSetupFlow(null)
|
|
144
|
-
expect(action.kind).toBe('ask-slug')
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('asks for persona when valid inline slug given', () => {
|
|
148
|
-
const action = startSetupFlow('gymbro')
|
|
149
|
-
expect(action.kind).toBe('ask-persona')
|
|
150
|
-
if (action.kind === 'ask-persona') expect(action.slug).toBe('gymbro')
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
it('returns error for invalid inline slug', () => {
|
|
154
|
-
const action = startSetupFlow('INVALID SLUG!')
|
|
155
|
-
expect(action.kind).toBe('error')
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
// ─── handleSetupText: cancel at any step ─────────────────────────────────
|
|
160
|
-
|
|
161
|
-
describe('handleSetupText: cancel', () => {
|
|
162
|
-
const steps = [
|
|
163
|
-
'asked-slug', 'asked-persona', 'asked-model', 'asked-emoji',
|
|
164
|
-
'asked-bot-token', 'confirming-allowlist',
|
|
165
|
-
] as const
|
|
166
|
-
|
|
167
|
-
for (const step of steps) {
|
|
168
|
-
it(`cancels at step ${step}`, () => {
|
|
169
|
-
const state = makeState({ step, slug: 'gymbro', persona: 'Gym Bro' })
|
|
170
|
-
const action = handleSetupText({ state, text: 'cancel', callerId: CALLER })
|
|
171
|
-
expect(action.kind).toBe('cancel')
|
|
172
|
-
if (action.kind === 'cancel') expect(action.reason).toBe('user-cancelled')
|
|
173
|
-
})
|
|
174
|
-
}
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
// ─── handleSetupText: null state ──────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
describe('handleSetupText: null state', () => {
|
|
180
|
-
it('returns cancel with no-active-flow reason', () => {
|
|
181
|
-
const action = handleSetupText({ state: null, text: 'gymbro', callerId: CALLER })
|
|
182
|
-
expect(action.kind).toBe('cancel')
|
|
183
|
-
if (action.kind === 'cancel') expect(action.reason).toBe('no-active-flow')
|
|
184
|
-
})
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
// ─── handleSetupText: step asked-slug ────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
describe('handleSetupText: asked-slug', () => {
|
|
190
|
-
it('advances to ask-persona on valid slug', () => {
|
|
191
|
-
const state = makeState({ step: 'asked-slug' })
|
|
192
|
-
const action = handleSetupText({ state, text: 'gymbro', callerId: CALLER })
|
|
193
|
-
expect(action.kind).toBe('ask-persona')
|
|
194
|
-
if (action.kind === 'ask-persona') expect(action.slug).toBe('gymbro')
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('returns error on invalid slug', () => {
|
|
198
|
-
const state = makeState({ step: 'asked-slug' })
|
|
199
|
-
const action = handleSetupText({ state, text: 'BAD SLUG', callerId: CALLER })
|
|
200
|
-
expect(action.kind).toBe('error')
|
|
201
|
-
if (action.kind === 'error') expect(action.stayInStep).toBe(true)
|
|
202
|
-
})
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
// ─── handleSetupText: step asked-persona ─────────────────────────────────
|
|
206
|
-
|
|
207
|
-
describe('handleSetupText: asked-persona', () => {
|
|
208
|
-
it('advances to ask-model on valid persona', () => {
|
|
209
|
-
const state = makeState({ step: 'asked-persona', slug: 'gymbro' })
|
|
210
|
-
const action = handleSetupText({ state, text: 'Gym Bro', callerId: CALLER })
|
|
211
|
-
expect(action.kind).toBe('ask-model')
|
|
212
|
-
if (action.kind === 'ask-model') {
|
|
213
|
-
expect(action.slug).toBe('gymbro')
|
|
214
|
-
expect(action.persona).toBe('Gym Bro')
|
|
215
|
-
}
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
it('returns error on empty persona', () => {
|
|
219
|
-
const state = makeState({ step: 'asked-persona', slug: 'gymbro' })
|
|
220
|
-
const action = handleSetupText({ state, text: '', callerId: CALLER })
|
|
221
|
-
expect(action.kind).toBe('error')
|
|
222
|
-
})
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
// ─── handleSetupText: step asked-model ───────────────────────────────────
|
|
226
|
-
|
|
227
|
-
describe('handleSetupText: asked-model', () => {
|
|
228
|
-
it('advances to ask-emoji with skip', () => {
|
|
229
|
-
const state = makeState({ step: 'asked-model', slug: 'gymbro', persona: 'Gym Bro' })
|
|
230
|
-
const action = handleSetupText({ state, text: 'skip', callerId: CALLER })
|
|
231
|
-
expect(action.kind).toBe('ask-emoji')
|
|
232
|
-
if (action.kind === 'ask-emoji') expect(action.model).toBeNull()
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
it('advances to ask-emoji with valid model', () => {
|
|
236
|
-
const state = makeState({ step: 'asked-model', slug: 'gymbro', persona: 'Gym Bro' })
|
|
237
|
-
const action = handleSetupText({ state, text: 'sonnet', callerId: CALLER })
|
|
238
|
-
expect(action.kind).toBe('ask-emoji')
|
|
239
|
-
if (action.kind === 'ask-emoji') expect(action.model).toBe('sonnet')
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('returns error on model string with spaces', () => {
|
|
243
|
-
const state = makeState({ step: 'asked-model', slug: 'gymbro', persona: 'Gym Bro' })
|
|
244
|
-
// Spaces are not allowed in model IDs
|
|
245
|
-
const action = handleSetupText({ state, text: 'bad model name', callerId: CALLER })
|
|
246
|
-
expect(action.kind).toBe('error')
|
|
247
|
-
if (action.kind === 'error') expect(action.stayInStep).toBe(true)
|
|
248
|
-
})
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
// ─── handleSetupText: step asked-emoji ───────────────────────────────────
|
|
252
|
-
|
|
253
|
-
describe('handleSetupText: asked-emoji (#190 — now transitions to ask-profile)', () => {
|
|
254
|
-
it('advances to ask-profile with skip (no emoji)', () => {
|
|
255
|
-
const state = makeState({ step: 'asked-emoji', slug: 'gymbro', persona: 'Gym Bro', model: 'sonnet' })
|
|
256
|
-
const action = call(state, 'skip')
|
|
257
|
-
expect(action.kind).toBe('ask-profile')
|
|
258
|
-
if (action.kind === 'ask-profile') {
|
|
259
|
-
expect(action.emoji).toBeNull()
|
|
260
|
-
expect(action.profiles).toEqual(PROFILES)
|
|
261
|
-
}
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
it('advances to ask-profile carrying the emoji', () => {
|
|
265
|
-
const state = makeState({ step: 'asked-emoji', slug: 'gymbro', persona: 'Gym Bro', model: null })
|
|
266
|
-
const action = call(state, '🏋️')
|
|
267
|
-
expect(action.kind).toBe('ask-profile')
|
|
268
|
-
if (action.kind === 'ask-profile') expect(action.emoji).toBe('🏋️')
|
|
269
|
-
})
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
// ─── handleSetupText: step asked-profile (#190) ──────────────────────────
|
|
273
|
-
|
|
274
|
-
describe('handleSetupText: asked-profile (#190)', () => {
|
|
275
|
-
function profileState(): SetupFlowState {
|
|
276
|
-
return makeState({
|
|
277
|
-
step: 'asked-profile',
|
|
278
|
-
slug: 'gymbro',
|
|
279
|
-
persona: 'Gym Bro',
|
|
280
|
-
model: 'sonnet',
|
|
281
|
-
emoji: '🏋️',
|
|
282
|
-
})
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
it('advances to ask-bot-token when profile is in the live list', () => {
|
|
286
|
-
const action = call(profileState(), 'health-coach')
|
|
287
|
-
expect(action.kind).toBe('ask-bot-token')
|
|
288
|
-
if (action.kind === 'ask-bot-token') {
|
|
289
|
-
expect(action.profile).toBe('health-coach')
|
|
290
|
-
expect(action.slug).toBe('gymbro')
|
|
291
|
-
expect(action.emoji).toBe('🏋️')
|
|
292
|
-
}
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
it('returns error stayInStep when profile is unknown', () => {
|
|
296
|
-
const action = call(profileState(), 'nonexistent')
|
|
297
|
-
expect(action.kind).toBe('error')
|
|
298
|
-
if (action.kind === 'error') {
|
|
299
|
-
expect(action.stayInStep).toBe(true)
|
|
300
|
-
expect(action.message).toContain('nonexistent')
|
|
301
|
-
}
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
it('lists valid profiles in error message', () => {
|
|
305
|
-
const action = call(profileState(), 'bogus')
|
|
306
|
-
if (action.kind === 'error') {
|
|
307
|
-
for (const p of PROFILES) expect(action.message).toContain(p)
|
|
308
|
-
}
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
it('cancels when slug or persona missing', () => {
|
|
312
|
-
const action = call(makeState({ step: 'asked-profile' }), 'default')
|
|
313
|
-
expect(action.kind).toBe('cancel')
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
it('respects a different profiles list per call', () => {
|
|
317
|
-
const action = call(profileState(), 'tiny-bundle', { profiles: ['tiny-bundle'] })
|
|
318
|
-
expect(action.kind).toBe('ask-bot-token')
|
|
319
|
-
})
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
// ─── handleSetupText: step asked-bot-token ───────────────────────────────
|
|
323
|
-
|
|
324
|
-
describe('handleSetupText: asked-bot-token', () => {
|
|
325
|
-
it('advances to confirm-allowlist with valid token shape', () => {
|
|
326
|
-
const state = makeState({
|
|
327
|
-
step: 'asked-bot-token',
|
|
328
|
-
slug: 'gymbro',
|
|
329
|
-
persona: 'Gym Bro',
|
|
330
|
-
model: null,
|
|
331
|
-
emoji: null,
|
|
332
|
-
})
|
|
333
|
-
const action = handleSetupText({ state, text: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx', callerId: CALLER })
|
|
334
|
-
expect(action.kind).toBe('confirm-allowlist')
|
|
335
|
-
if (action.kind === 'confirm-allowlist') expect(action.callerId).toBe(CALLER)
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
it('returns error on bad token shape', () => {
|
|
339
|
-
const state = makeState({
|
|
340
|
-
step: 'asked-bot-token',
|
|
341
|
-
slug: 'gymbro',
|
|
342
|
-
persona: 'Gym Bro',
|
|
343
|
-
})
|
|
344
|
-
const action = handleSetupText({ state, text: 'notavalidtoken', callerId: CALLER })
|
|
345
|
-
expect(action.kind).toBe('error')
|
|
346
|
-
if (action.kind === 'error') expect(action.stayInStep).toBe(true)
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
it('returns cancel when slug or persona is missing', () => {
|
|
350
|
-
const state = makeState({
|
|
351
|
-
step: 'asked-bot-token',
|
|
352
|
-
slug: null,
|
|
353
|
-
persona: null,
|
|
354
|
-
})
|
|
355
|
-
const action = handleSetupText({ state, text: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx', callerId: CALLER })
|
|
356
|
-
expect(action.kind).toBe('cancel')
|
|
357
|
-
})
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
// ─── handleSetupText: step confirming-allowlist ───────────────────────────
|
|
361
|
-
|
|
362
|
-
describe('handleSetupText: confirming-allowlist (#189 — now transitions to call-create-agent)', () => {
|
|
363
|
-
const baseState = makeState({
|
|
364
|
-
step: 'confirming-allowlist',
|
|
365
|
-
slug: 'gymbro',
|
|
366
|
-
persona: 'Gym Bro',
|
|
367
|
-
model: null,
|
|
368
|
-
emoji: null,
|
|
369
|
-
profile: 'default',
|
|
370
|
-
botToken: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx',
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
it('advances to call-create-agent on "yes"', () => {
|
|
374
|
-
const action = call(baseState, 'yes')
|
|
375
|
-
expect(action.kind).toBe('call-create-agent')
|
|
376
|
-
if (action.kind === 'call-create-agent') {
|
|
377
|
-
expect(action.allowedUserId).toBe(CALLER)
|
|
378
|
-
expect(action.slug).toBe('gymbro')
|
|
379
|
-
expect(action.persona).toBe('Gym Bro')
|
|
380
|
-
expect(action.profile).toBe('default')
|
|
381
|
-
}
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
it('advances to call-create-agent on "y"', () => {
|
|
385
|
-
const action = call(baseState, 'y')
|
|
386
|
-
expect(action.kind).toBe('call-create-agent')
|
|
387
|
-
if (action.kind === 'call-create-agent') expect(action.allowedUserId).toBe(CALLER)
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
it('uses custom user_id when not "yes"', () => {
|
|
391
|
-
const action = call(baseState, '99999999')
|
|
392
|
-
expect(action.kind).toBe('call-create-agent')
|
|
393
|
-
if (action.kind === 'call-create-agent') expect(action.allowedUserId).toBe('99999999')
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
it('falls back to "default" profile for legacy in-flight flows where profile is null', () => {
|
|
397
|
-
// Simulates a flow that started before the #190 schema migration —
|
|
398
|
-
// SQLite returns NULL for the new `profile` column, the wizard
|
|
399
|
-
// shouldn't break.
|
|
400
|
-
const legacy = makeState({
|
|
401
|
-
step: 'confirming-allowlist',
|
|
402
|
-
slug: 'oldgymbro',
|
|
403
|
-
persona: 'Old Gym',
|
|
404
|
-
botToken: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx',
|
|
405
|
-
profile: null,
|
|
406
|
-
})
|
|
407
|
-
const action = call(legacy, 'yes')
|
|
408
|
-
expect(action.kind).toBe('call-create-agent')
|
|
409
|
-
if (action.kind === 'call-create-agent') expect(action.profile).toBe('default')
|
|
410
|
-
})
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
// ─── handleSetupText: step asked-oauth-code (#189) ───────────────────────
|
|
414
|
-
|
|
415
|
-
describe('handleSetupText: asked-oauth-code (#189)', () => {
|
|
416
|
-
function oauthState(): SetupFlowState {
|
|
417
|
-
return makeState({
|
|
418
|
-
step: 'asked-oauth-code',
|
|
419
|
-
slug: 'gymbro',
|
|
420
|
-
persona: 'Gym Bro',
|
|
421
|
-
profile: 'default',
|
|
422
|
-
botToken: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx',
|
|
423
|
-
authSessionName: 'gymbro-foreman',
|
|
424
|
-
loginUrl: 'https://example.com/login',
|
|
425
|
-
})
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
it('advances to call-complete-creation with a valid-shape code', () => {
|
|
429
|
-
const action = call(oauthState(), 'a1b2c3d4e5')
|
|
430
|
-
expect(action.kind).toBe('call-complete-creation')
|
|
431
|
-
if (action.kind === 'call-complete-creation') {
|
|
432
|
-
expect(action.slug).toBe('gymbro')
|
|
433
|
-
expect(action.code).toBe('a1b2c3d4e5')
|
|
434
|
-
}
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
it('returns error stayInStep on too-short code', () => {
|
|
438
|
-
const action = call(oauthState(), 'abc')
|
|
439
|
-
expect(action.kind).toBe('error')
|
|
440
|
-
if (action.kind === 'error') expect(action.stayInStep).toBe(true)
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
it('cancels when slug missing (corrupt state)', () => {
|
|
444
|
-
const action = call(makeState({ step: 'asked-oauth-code' }), 'a1b2c3d4e5')
|
|
445
|
-
expect(action.kind).toBe('cancel')
|
|
446
|
-
})
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
// ─── handleSetupText: terminal steps ─────────────────────────────────────
|
|
450
|
-
|
|
451
|
-
describe('handleSetupText: terminal steps', () => {
|
|
452
|
-
it('returns cancel for reconciling step', () => {
|
|
453
|
-
const state = makeState({ step: 'reconciling' })
|
|
454
|
-
const action = handleSetupText({ state, text: 'anything', callerId: CALLER })
|
|
455
|
-
expect(action.kind).toBe('cancel')
|
|
456
|
-
})
|
|
457
|
-
|
|
458
|
-
it('returns cancel for done step', () => {
|
|
459
|
-
const state = makeState({ step: 'done' })
|
|
460
|
-
const action = handleSetupText({ state, text: 'anything', callerId: CALLER })
|
|
461
|
-
expect(action.kind).toBe('cancel')
|
|
462
|
-
})
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
// ─── makeSetupInitialState ────────────────────────────────────────────────
|
|
466
|
-
|
|
467
|
-
describe('makeSetupInitialState', () => {
|
|
468
|
-
it('sets step to asked-slug when no slug', () => {
|
|
469
|
-
const s = makeSetupInitialState('chat1', null)
|
|
470
|
-
expect(s.step).toBe('asked-slug')
|
|
471
|
-
expect(s.slug).toBeNull()
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
it('sets step to asked-persona when slug provided', () => {
|
|
475
|
-
const s = makeSetupInitialState('chat1', 'gymbro')
|
|
476
|
-
expect(s.step).toBe('asked-persona')
|
|
477
|
-
expect(s.slug).toBe('gymbro')
|
|
478
|
-
})
|
|
479
|
-
})
|
|
480
|
-
|
|
481
|
-
// ─── advanceSetupState ────────────────────────────────────────────────────
|
|
482
|
-
|
|
483
|
-
describe('advanceSetupState', () => {
|
|
484
|
-
it('merges updates and bumps updatedAt', () => {
|
|
485
|
-
const original = makeState({ updatedAt: 1000 })
|
|
486
|
-
const advanced = advanceSetupState(original, { step: 'asked-persona', slug: 'gymbro' })
|
|
487
|
-
expect(advanced.step).toBe('asked-persona')
|
|
488
|
-
expect(advanced.slug).toBe('gymbro')
|
|
489
|
-
expect(advanced.chatId).toBe(original.chatId)
|
|
490
|
-
expect(advanced.updatedAt).toBeGreaterThanOrEqual(original.updatedAt)
|
|
491
|
-
})
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
// ─── setupStepLabel ───────────────────────────────────────────────────────
|
|
495
|
-
|
|
496
|
-
describe('setupStepLabel', () => {
|
|
497
|
-
const cases: [import('../foreman/setup-state.js').SetupFlowStep, string][] = [
|
|
498
|
-
['asked-slug', 'waiting for agent slug'],
|
|
499
|
-
['asked-persona', 'waiting for persona name'],
|
|
500
|
-
['asked-model', 'waiting for model choice'],
|
|
501
|
-
['asked-emoji', 'waiting for emoji'],
|
|
502
|
-
['asked-bot-token', 'waiting for BotFather token'],
|
|
503
|
-
['confirming-allowlist', 'waiting for allowlist confirmation'],
|
|
504
|
-
['reconciling', 'provisioning agent'],
|
|
505
|
-
['done', 'done'],
|
|
506
|
-
]
|
|
507
|
-
for (const [step, expected] of cases) {
|
|
508
|
-
it(`labels ${step} correctly`, () => expect(setupStepLabel(step)).toBe(expected))
|
|
509
|
-
}
|
|
510
|
-
})
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for /setup wizard SQLite state (setup-state.ts).
|
|
3
|
-
*
|
|
4
|
-
* Uses bun:test (not vitest) because setup-state.ts imports bun:sqlite.
|
|
5
|
-
* Run with: bun test telegram-plugin/tests/setup-state.test.ts
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
|
|
9
|
-
import { mkdtempSync, rmSync } from 'fs'
|
|
10
|
-
import { tmpdir } from 'os'
|
|
11
|
-
|
|
12
|
-
let tmpDir: string
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
tmpDir = mkdtempSync(tmpdir() + '/setup-state-test-')
|
|
16
|
-
process.env.SWITCHROOM_FOREMAN_DIR = tmpDir
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
const { _resetSetupDbForTest } = await import('../foreman/setup-state.js')
|
|
21
|
-
_resetSetupDbForTest()
|
|
22
|
-
delete process.env.SWITCHROOM_FOREMAN_DIR
|
|
23
|
-
try { rmSync(tmpDir, { recursive: true, force: true }) } catch { /* ignore */ }
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
function makeState(chatId = 'chat1') {
|
|
27
|
-
const now = Date.now()
|
|
28
|
-
return {
|
|
29
|
-
chatId,
|
|
30
|
-
step: 'asked-slug' as const,
|
|
31
|
-
slug: null,
|
|
32
|
-
persona: null,
|
|
33
|
-
model: null,
|
|
34
|
-
emoji: null,
|
|
35
|
-
botToken: null,
|
|
36
|
-
allowedUserId: null,
|
|
37
|
-
startedAt: now,
|
|
38
|
-
updatedAt: now,
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ─── Round-trip: setSetupState + getSetupState ────────────────────────────
|
|
43
|
-
|
|
44
|
-
describe('setup-state: round-trip', () => {
|
|
45
|
-
it('stores and retrieves initial state', async () => {
|
|
46
|
-
const { setSetupState, getSetupState } = await import('../foreman/setup-state.js')
|
|
47
|
-
const state = makeState()
|
|
48
|
-
setSetupState(state)
|
|
49
|
-
const retrieved = getSetupState('chat1')
|
|
50
|
-
expect(retrieved).not.toBeNull()
|
|
51
|
-
expect(retrieved?.step).toBe('asked-slug')
|
|
52
|
-
expect(retrieved?.slug).toBeNull()
|
|
53
|
-
expect(retrieved?.chatId).toBe('chat1')
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('returns null for unknown chatId', async () => {
|
|
57
|
-
const { getSetupState } = await import('../foreman/setup-state.js')
|
|
58
|
-
expect(getSetupState('nonexistent')).toBeNull()
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('upserts state on repeat setSetupState', async () => {
|
|
62
|
-
const { setSetupState, getSetupState } = await import('../foreman/setup-state.js')
|
|
63
|
-
const state = makeState()
|
|
64
|
-
setSetupState(state)
|
|
65
|
-
setSetupState({ ...state, step: 'asked-persona', slug: 'gymbro' })
|
|
66
|
-
const retrieved = getSetupState('chat1')
|
|
67
|
-
expect(retrieved?.step).toBe('asked-persona')
|
|
68
|
-
expect(retrieved?.slug).toBe('gymbro')
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('stores all fields', async () => {
|
|
72
|
-
const { setSetupState, getSetupState } = await import('../foreman/setup-state.js')
|
|
73
|
-
const now = Date.now()
|
|
74
|
-
setSetupState({
|
|
75
|
-
chatId: 'chat2',
|
|
76
|
-
step: 'asked-bot-token',
|
|
77
|
-
slug: 'myagent',
|
|
78
|
-
persona: 'My Agent',
|
|
79
|
-
model: 'sonnet',
|
|
80
|
-
emoji: '🤖',
|
|
81
|
-
botToken: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx',
|
|
82
|
-
allowedUserId: '99999999',
|
|
83
|
-
startedAt: now - 1000,
|
|
84
|
-
updatedAt: now,
|
|
85
|
-
})
|
|
86
|
-
const retrieved = getSetupState('chat2')
|
|
87
|
-
expect(retrieved?.slug).toBe('myagent')
|
|
88
|
-
expect(retrieved?.persona).toBe('My Agent')
|
|
89
|
-
expect(retrieved?.model).toBe('sonnet')
|
|
90
|
-
expect(retrieved?.emoji).toBe('🤖')
|
|
91
|
-
expect(retrieved?.botToken).toBe('1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx')
|
|
92
|
-
expect(retrieved?.allowedUserId).toBe('99999999')
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
// ─── clearSetupState ──────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
describe('setup-state: clearSetupState', () => {
|
|
99
|
-
it('removes state for given chat', async () => {
|
|
100
|
-
const { setSetupState, clearSetupState, getSetupState } = await import('../foreman/setup-state.js')
|
|
101
|
-
setSetupState(makeState('chatA'))
|
|
102
|
-
setSetupState(makeState('chatB'))
|
|
103
|
-
clearSetupState('chatA')
|
|
104
|
-
expect(getSetupState('chatA')).toBeNull()
|
|
105
|
-
expect(getSetupState('chatB')).not.toBeNull()
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('is a no-op when chat has no state', async () => {
|
|
109
|
-
const { clearSetupState, getSetupState } = await import('../foreman/setup-state.js')
|
|
110
|
-
// Should not throw
|
|
111
|
-
clearSetupState('nobody')
|
|
112
|
-
expect(getSetupState('nobody')).toBeNull()
|
|
113
|
-
})
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
// ─── listActiveSetupFlows ─────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
describe('setup-state: listActiveSetupFlows', () => {
|
|
119
|
-
it('returns only non-done flows within maxAge', async () => {
|
|
120
|
-
const { setSetupState, listActiveSetupFlows } = await import('../foreman/setup-state.js')
|
|
121
|
-
const now = Date.now()
|
|
122
|
-
|
|
123
|
-
setSetupState({ ...makeState('chat1'), step: 'asked-slug', updatedAt: now })
|
|
124
|
-
setSetupState({ ...makeState('chat2'), step: 'done', updatedAt: now })
|
|
125
|
-
setSetupState({ ...makeState('chat3'), step: 'asked-persona', updatedAt: now - 2 * 60 * 60 * 1000 }) // 2 hours old
|
|
126
|
-
|
|
127
|
-
const active = listActiveSetupFlows(60 * 60 * 1000) // 1 hour
|
|
128
|
-
expect(active.length).toBe(1)
|
|
129
|
-
expect(active[0].chatId).toBe('chat1')
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('returns empty list when nothing active', async () => {
|
|
133
|
-
const { listActiveSetupFlows } = await import('../foreman/setup-state.js')
|
|
134
|
-
const active = listActiveSetupFlows()
|
|
135
|
-
expect(active).toEqual([])
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('returns multiple active flows', async () => {
|
|
139
|
-
const { setSetupState, listActiveSetupFlows } = await import('../foreman/setup-state.js')
|
|
140
|
-
const now = Date.now()
|
|
141
|
-
setSetupState({ ...makeState('c1'), updatedAt: now })
|
|
142
|
-
setSetupState({ ...makeState('c2'), step: 'asked-persona', updatedAt: now })
|
|
143
|
-
const active = listActiveSetupFlows()
|
|
144
|
-
expect(active.length).toBe(2)
|
|
145
|
-
})
|
|
146
|
-
})
|