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,309 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for sweepActivePins — the shared helper called by both the
|
|
3
|
-
* startup failsafe and the /restart, /reconcile --restart, and /update
|
|
4
|
-
* command handlers to unpin any still-pinned progress cards and clear
|
|
5
|
-
* the sidecar.
|
|
6
|
-
*
|
|
7
|
-
* The real unpin path goes through `lockedBot.api.unpinChatMessage`.
|
|
8
|
-
* These tests inject a fake unpin callback so behavior is verified
|
|
9
|
-
* without a Telegram stub — the shape under test is:
|
|
10
|
-
*
|
|
11
|
-
* 1. Reads the sidecar
|
|
12
|
-
* 2. Calls unpinFn(chatId, messageId) for each entry
|
|
13
|
-
* 3. Swallows unpin errors (best-effort — the message may already be
|
|
14
|
-
* gone, or the bot may have lost admin rights)
|
|
15
|
-
* 4. Clears the sidecar regardless of success/failure
|
|
16
|
-
* 5. Bounded by timeoutMs so a hung Telegram API can't block a
|
|
17
|
-
* restart indefinitely
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
21
|
-
import { mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
22
|
-
import { tmpdir } from "node:os";
|
|
23
|
-
import { join } from "node:path";
|
|
24
|
-
import { addActivePin, readActivePins, ACTIVE_PINS_FILENAME, type ActivePin } from "../active-pins.js";
|
|
25
|
-
import {
|
|
26
|
-
sweepActivePins,
|
|
27
|
-
sweepBotAuthoredPins,
|
|
28
|
-
type PinnedMessageInfo,
|
|
29
|
-
} from "../active-pins-sweep.js";
|
|
30
|
-
|
|
31
|
-
describe("sweepActivePins", () => {
|
|
32
|
-
let tmp: string;
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
tmp = mkdtempSync(join(tmpdir(), "active-pins-sweep-"));
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
afterEach(() => {
|
|
39
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const makePin = (overrides: Partial<ActivePin> = {}): ActivePin => ({
|
|
43
|
-
chatId: "100",
|
|
44
|
-
messageId: 42,
|
|
45
|
-
turnKey: "100:0:1",
|
|
46
|
-
pinnedAt: 1_700_000_000_000,
|
|
47
|
-
...overrides,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("is a no-op when the sidecar is empty", async () => {
|
|
51
|
-
const calls: Array<[string, number]> = [];
|
|
52
|
-
const result = await sweepActivePins(tmp, async (chatId, messageId) => {
|
|
53
|
-
calls.push([chatId, messageId]);
|
|
54
|
-
});
|
|
55
|
-
expect(calls).toEqual([]);
|
|
56
|
-
expect(result.swept).toEqual([]);
|
|
57
|
-
expect(result.timedOut).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("calls unpin for each sidecar entry and clears the file", async () => {
|
|
61
|
-
addActivePin(tmp, makePin({ chatId: "A", messageId: 1, turnKey: "A:0:1" }));
|
|
62
|
-
addActivePin(tmp, makePin({ chatId: "B", messageId: 2, turnKey: "B:0:1" }));
|
|
63
|
-
addActivePin(tmp, makePin({ chatId: "A", messageId: 3, turnKey: "A:0:2" }));
|
|
64
|
-
|
|
65
|
-
const calls: Array<[string, number]> = [];
|
|
66
|
-
const result = await sweepActivePins(tmp, async (chatId, messageId) => {
|
|
67
|
-
calls.push([chatId, messageId]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(calls.sort()).toEqual([
|
|
71
|
-
["A", 1],
|
|
72
|
-
["A", 3],
|
|
73
|
-
["B", 2],
|
|
74
|
-
]);
|
|
75
|
-
expect(result.swept).toHaveLength(3);
|
|
76
|
-
expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
|
|
77
|
-
expect(readActivePins(tmp)).toEqual([]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("still clears the sidecar when unpin throws for every entry", async () => {
|
|
81
|
-
addActivePin(tmp, makePin({ chatId: "A", messageId: 1 }));
|
|
82
|
-
addActivePin(tmp, makePin({ chatId: "B", messageId: 2, turnKey: "B:0:1" }));
|
|
83
|
-
|
|
84
|
-
const errors: string[] = [];
|
|
85
|
-
await sweepActivePins(
|
|
86
|
-
tmp,
|
|
87
|
-
async () => {
|
|
88
|
-
throw new Error("message to unpin not found");
|
|
89
|
-
},
|
|
90
|
-
{ log: (msg) => errors.push(msg) },
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
|
|
94
|
-
expect(readActivePins(tmp)).toEqual([]);
|
|
95
|
-
// Each failure is logged — log receives one "sweeping N" line plus
|
|
96
|
-
// one "unpin failed" line per entry.
|
|
97
|
-
expect(errors.some((e) => e.includes("sweeping 2"))).toBe(true);
|
|
98
|
-
expect(errors.filter((e) => e.includes("unpin failed"))).toHaveLength(2);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("tolerates a mix of successful and failing unpins", async () => {
|
|
102
|
-
addActivePin(tmp, makePin({ chatId: "A", messageId: 1 }));
|
|
103
|
-
addActivePin(tmp, makePin({ chatId: "A", messageId: 2, turnKey: "A:0:2" }));
|
|
104
|
-
|
|
105
|
-
const unpinned: Array<[string, number]> = [];
|
|
106
|
-
await sweepActivePins(tmp, async (chatId, messageId) => {
|
|
107
|
-
if (messageId === 2) throw new Error("nope");
|
|
108
|
-
unpinned.push([chatId, messageId]);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
expect(unpinned).toEqual([["A", 1]]);
|
|
112
|
-
expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("returns after timeoutMs even if unpin never resolves", async () => {
|
|
116
|
-
addActivePin(tmp, makePin({ chatId: "A", messageId: 1 }));
|
|
117
|
-
|
|
118
|
-
const start = Date.now();
|
|
119
|
-
const result = await sweepActivePins(
|
|
120
|
-
tmp,
|
|
121
|
-
() => new Promise(() => { /* never resolves */ }),
|
|
122
|
-
{ timeoutMs: 50 },
|
|
123
|
-
);
|
|
124
|
-
const elapsed = Date.now() - start;
|
|
125
|
-
|
|
126
|
-
expect(result.timedOut).toBe(true);
|
|
127
|
-
expect(elapsed).toBeLessThan(500);
|
|
128
|
-
// Sidecar is cleared even though the unpin never landed — stale
|
|
129
|
-
// entries get retried from Telegram's side on next boot but we
|
|
130
|
-
// don't want the sweep to keep re-firing them forever.
|
|
131
|
-
expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("passes the caller-provided log hook", async () => {
|
|
135
|
-
addActivePin(tmp, makePin({ chatId: "A", messageId: 1 }));
|
|
136
|
-
const lines: string[] = [];
|
|
137
|
-
await sweepActivePins(
|
|
138
|
-
tmp,
|
|
139
|
-
async () => { /* succeed silently */ },
|
|
140
|
-
{ log: (msg) => lines.push(msg) },
|
|
141
|
-
);
|
|
142
|
-
expect(lines).toContain("sweeping 1 active pin(s)");
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe("sweepBotAuthoredPins", () => {
|
|
147
|
-
const BOT_ID = 1000;
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Build a fake getTopPin backed by a map of { chatId: queue }.
|
|
151
|
-
* Each unpin pops the head of the queue — which mimics Telegram's
|
|
152
|
-
* "unpinning the top reveals the next most recent pin" behavior.
|
|
153
|
-
*/
|
|
154
|
-
function fakeChats(
|
|
155
|
-
state: Record<string, Array<{ messageId: number; fromId: number | null }>>,
|
|
156
|
-
) {
|
|
157
|
-
const getTopPin = async (chatId: string): Promise<PinnedMessageInfo | null> => {
|
|
158
|
-
const q = state[chatId] ?? [];
|
|
159
|
-
return q.length === 0 ? null : { ...q[0] };
|
|
160
|
-
};
|
|
161
|
-
const unpinCalls: Array<[string, number]> = [];
|
|
162
|
-
const unpin = async (chatId: string, messageId: number): Promise<void> => {
|
|
163
|
-
unpinCalls.push([chatId, messageId]);
|
|
164
|
-
const q = state[chatId] ?? [];
|
|
165
|
-
if (q[0]?.messageId === messageId) q.shift();
|
|
166
|
-
};
|
|
167
|
-
return { getTopPin, unpin, unpinCalls };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
it("is a no-op when no chats are provided", async () => {
|
|
171
|
-
const { getTopPin, unpin, unpinCalls } = fakeChats({});
|
|
172
|
-
const result = await sweepBotAuthoredPins([], BOT_ID, getTopPin, unpin);
|
|
173
|
-
expect(unpinCalls).toEqual([]);
|
|
174
|
-
expect(result.total).toBe(0);
|
|
175
|
-
expect(result.perChat).toEqual({});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("is a no-op when a chat has no pinned message", async () => {
|
|
179
|
-
const { getTopPin, unpin, unpinCalls } = fakeChats({ A: [] });
|
|
180
|
-
const result = await sweepBotAuthoredPins(["A"], BOT_ID, getTopPin, unpin);
|
|
181
|
-
expect(unpinCalls).toEqual([]);
|
|
182
|
-
expect(result.total).toBe(0);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("unpins consecutive bot-authored pins until a foreign pin is reached", async () => {
|
|
186
|
-
const { getTopPin, unpin, unpinCalls } = fakeChats({
|
|
187
|
-
A: [
|
|
188
|
-
{ messageId: 10, fromId: BOT_ID },
|
|
189
|
-
{ messageId: 9, fromId: BOT_ID },
|
|
190
|
-
{ messageId: 8, fromId: 42 }, // user pin — barrier
|
|
191
|
-
{ messageId: 7, fromId: BOT_ID }, // would be unpinned if barrier weren't there
|
|
192
|
-
],
|
|
193
|
-
});
|
|
194
|
-
const result = await sweepBotAuthoredPins(["A"], BOT_ID, getTopPin, unpin);
|
|
195
|
-
expect(unpinCalls).toEqual([
|
|
196
|
-
["A", 10],
|
|
197
|
-
["A", 9],
|
|
198
|
-
]);
|
|
199
|
-
expect(result.total).toBe(2);
|
|
200
|
-
expect(result.perChat).toEqual({ A: 2 });
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("stops immediately when the top pin belongs to someone else", async () => {
|
|
204
|
-
const { getTopPin, unpin, unpinCalls } = fakeChats({
|
|
205
|
-
A: [
|
|
206
|
-
{ messageId: 5, fromId: 999 },
|
|
207
|
-
{ messageId: 4, fromId: BOT_ID },
|
|
208
|
-
],
|
|
209
|
-
});
|
|
210
|
-
const result = await sweepBotAuthoredPins(["A"], BOT_ID, getTopPin, unpin);
|
|
211
|
-
expect(unpinCalls).toEqual([]);
|
|
212
|
-
expect(result.total).toBe(0);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("treats pins with no from.id as foreign (anonymous channel post)", async () => {
|
|
216
|
-
const { getTopPin, unpin, unpinCalls } = fakeChats({
|
|
217
|
-
A: [
|
|
218
|
-
{ messageId: 5, fromId: null },
|
|
219
|
-
{ messageId: 4, fromId: BOT_ID },
|
|
220
|
-
],
|
|
221
|
-
});
|
|
222
|
-
const result = await sweepBotAuthoredPins(["A"], BOT_ID, getTopPin, unpin);
|
|
223
|
-
expect(unpinCalls).toEqual([]);
|
|
224
|
-
expect(result.total).toBe(0);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("iterates across multiple chats independently", async () => {
|
|
228
|
-
const { getTopPin, unpin, unpinCalls } = fakeChats({
|
|
229
|
-
A: [
|
|
230
|
-
{ messageId: 1, fromId: BOT_ID },
|
|
231
|
-
{ messageId: 2, fromId: BOT_ID },
|
|
232
|
-
],
|
|
233
|
-
B: [{ messageId: 3, fromId: 777 }],
|
|
234
|
-
C: [{ messageId: 4, fromId: BOT_ID }],
|
|
235
|
-
});
|
|
236
|
-
const result = await sweepBotAuthoredPins(
|
|
237
|
-
["A", "B", "C"],
|
|
238
|
-
BOT_ID,
|
|
239
|
-
getTopPin,
|
|
240
|
-
unpin,
|
|
241
|
-
);
|
|
242
|
-
expect(unpinCalls).toEqual([
|
|
243
|
-
["A", 1],
|
|
244
|
-
["A", 2],
|
|
245
|
-
["C", 4],
|
|
246
|
-
]);
|
|
247
|
-
expect(result.total).toBe(3);
|
|
248
|
-
expect(result.perChat).toEqual({ A: 2, C: 1 });
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("advances to the next chat when getTopPin throws", async () => {
|
|
252
|
-
const unpinCalls: Array<[string, number]> = [];
|
|
253
|
-
const errors: string[] = [];
|
|
254
|
-
const result = await sweepBotAuthoredPins(
|
|
255
|
-
["A", "B"],
|
|
256
|
-
BOT_ID,
|
|
257
|
-
async (chatId) => {
|
|
258
|
-
if (chatId === "A") throw new Error("boom");
|
|
259
|
-
return { messageId: 7, fromId: BOT_ID };
|
|
260
|
-
},
|
|
261
|
-
async (chatId, messageId) => {
|
|
262
|
-
unpinCalls.push([chatId, messageId]);
|
|
263
|
-
// Simulate "pin is gone after unpin" so the B loop terminates
|
|
264
|
-
throw new Error("already gone");
|
|
265
|
-
},
|
|
266
|
-
{ log: (msg) => errors.push(msg) },
|
|
267
|
-
);
|
|
268
|
-
// A errored on getChat; B attempted one unpin (which threw) then stopped
|
|
269
|
-
expect(unpinCalls).toEqual([["B", 7]]);
|
|
270
|
-
expect(result.total).toBe(0);
|
|
271
|
-
expect(errors.some((e) => e.includes("getChat failed for A"))).toBe(true);
|
|
272
|
-
expect(errors.some((e) => e.includes("unpin failed for B/7"))).toBe(true);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("breaks out of a chat on unpin failure to avoid infinite loops", async () => {
|
|
276
|
-
const calls: Array<[string, number]> = [];
|
|
277
|
-
const result = await sweepBotAuthoredPins(
|
|
278
|
-
["A"],
|
|
279
|
-
BOT_ID,
|
|
280
|
-
async () => ({ messageId: 1, fromId: BOT_ID }),
|
|
281
|
-
async (chatId, messageId) => {
|
|
282
|
-
calls.push([chatId, messageId]);
|
|
283
|
-
throw new Error("permission denied");
|
|
284
|
-
},
|
|
285
|
-
);
|
|
286
|
-
// Without the break, the fake getTopPin would keep returning the same
|
|
287
|
-
// pin and we'd loop until maxPerChat. With the break, we see exactly
|
|
288
|
-
// one call and the sweep moves on.
|
|
289
|
-
expect(calls).toEqual([["A", 1]]);
|
|
290
|
-
expect(result.total).toBe(0);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it("is bounded by maxPerChat when pins keep re-appearing", async () => {
|
|
294
|
-
// A pathological case: getTopPin always returns a bot-authored pin,
|
|
295
|
-
// and unpin silently "succeeds" without removing it. Without the
|
|
296
|
-
// maxPerChat bound the loop would be infinite.
|
|
297
|
-
let calls = 0;
|
|
298
|
-
await sweepBotAuthoredPins(
|
|
299
|
-
["A"],
|
|
300
|
-
BOT_ID,
|
|
301
|
-
async () => ({ messageId: 1, fromId: BOT_ID }),
|
|
302
|
-
async () => {
|
|
303
|
-
calls++;
|
|
304
|
-
},
|
|
305
|
-
{ maxPerChat: 5 },
|
|
306
|
-
);
|
|
307
|
-
expect(calls).toBe(5);
|
|
308
|
-
});
|
|
309
|
-
});
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
readActivePins,
|
|
7
|
-
writeActivePins,
|
|
8
|
-
addActivePin,
|
|
9
|
-
removeActivePin,
|
|
10
|
-
clearActivePins,
|
|
11
|
-
ACTIVE_PINS_FILENAME,
|
|
12
|
-
type ActivePin,
|
|
13
|
-
} from "../active-pins.js";
|
|
14
|
-
|
|
15
|
-
describe("active-pins sidecar", () => {
|
|
16
|
-
let tmp: string;
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
tmp = mkdtempSync(join(tmpdir(), "active-pins-"));
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const makePin = (overrides: Partial<ActivePin> = {}): ActivePin => ({
|
|
27
|
-
chatId: "100",
|
|
28
|
-
messageId: 42,
|
|
29
|
-
turnKey: "100:0:1",
|
|
30
|
-
pinnedAt: 1_700_000_000_000,
|
|
31
|
-
...overrides,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe("readActivePins", () => {
|
|
35
|
-
it("returns [] when the file is missing", () => {
|
|
36
|
-
expect(readActivePins(tmp)).toEqual([]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("returns [] when the file is empty", () => {
|
|
40
|
-
writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), "");
|
|
41
|
-
expect(readActivePins(tmp)).toEqual([]);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("returns [] when the file is not valid JSON", () => {
|
|
45
|
-
writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), "{not json");
|
|
46
|
-
expect(readActivePins(tmp)).toEqual([]);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("returns [] when the JSON root is not an array", () => {
|
|
50
|
-
writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), '{"oops": true}');
|
|
51
|
-
expect(readActivePins(tmp)).toEqual([]);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("round-trips valid pin entries", () => {
|
|
55
|
-
const pin = makePin();
|
|
56
|
-
writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), JSON.stringify([pin]));
|
|
57
|
-
expect(readActivePins(tmp)).toEqual([pin]);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("drops entries with missing or wrong-typed fields", () => {
|
|
61
|
-
const good = makePin({ messageId: 7 });
|
|
62
|
-
const bad = [
|
|
63
|
-
good,
|
|
64
|
-
{ chatId: "x", messageId: "not-a-number", turnKey: "k", pinnedAt: 1 },
|
|
65
|
-
{ chatId: "y", messageId: 1, turnKey: "k" }, // missing pinnedAt
|
|
66
|
-
null,
|
|
67
|
-
"nope",
|
|
68
|
-
];
|
|
69
|
-
writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), JSON.stringify(bad));
|
|
70
|
-
expect(readActivePins(tmp)).toEqual([good]);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe("writeActivePins", () => {
|
|
75
|
-
it("creates the sidecar with JSON content", () => {
|
|
76
|
-
const pins = [makePin(), makePin({ messageId: 43, turnKey: "100:0:2" })];
|
|
77
|
-
writeActivePins(tmp, pins);
|
|
78
|
-
const raw = readFileSync(join(tmp, ACTIVE_PINS_FILENAME), "utf-8");
|
|
79
|
-
expect(JSON.parse(raw)).toEqual(pins);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("deletes the sidecar when writing an empty list", () => {
|
|
83
|
-
writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), JSON.stringify([makePin()]));
|
|
84
|
-
writeActivePins(tmp, []);
|
|
85
|
-
expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("is idempotent — deleting an already-missing file is a no-op", () => {
|
|
89
|
-
writeActivePins(tmp, []);
|
|
90
|
-
expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("addActivePin", () => {
|
|
95
|
-
it("creates the sidecar on first add", () => {
|
|
96
|
-
addActivePin(tmp, makePin());
|
|
97
|
-
expect(readActivePins(tmp)).toHaveLength(1);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("appends distinct pins", () => {
|
|
101
|
-
addActivePin(tmp, makePin({ messageId: 1, turnKey: "a" }));
|
|
102
|
-
addActivePin(tmp, makePin({ messageId: 2, turnKey: "b" }));
|
|
103
|
-
const pins = readActivePins(tmp);
|
|
104
|
-
expect(pins).toHaveLength(2);
|
|
105
|
-
expect(pins.map((p) => p.messageId).sort()).toEqual([1, 2]);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("replaces an existing entry with the same (chatId, messageId)", () => {
|
|
109
|
-
addActivePin(tmp, makePin({ pinnedAt: 100 }));
|
|
110
|
-
addActivePin(tmp, makePin({ pinnedAt: 200 }));
|
|
111
|
-
const pins = readActivePins(tmp);
|
|
112
|
-
expect(pins).toHaveLength(1);
|
|
113
|
-
expect(pins[0].pinnedAt).toBe(200);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("treats different chatIds as distinct even with matching messageIds", () => {
|
|
117
|
-
addActivePin(tmp, makePin({ chatId: "A", messageId: 1 }));
|
|
118
|
-
addActivePin(tmp, makePin({ chatId: "B", messageId: 1 }));
|
|
119
|
-
expect(readActivePins(tmp)).toHaveLength(2);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe("removeActivePin", () => {
|
|
124
|
-
it("removes the matching entry", () => {
|
|
125
|
-
addActivePin(tmp, makePin({ messageId: 1, turnKey: "a" }));
|
|
126
|
-
addActivePin(tmp, makePin({ messageId: 2, turnKey: "b" }));
|
|
127
|
-
removeActivePin(tmp, "100", 1);
|
|
128
|
-
const pins = readActivePins(tmp);
|
|
129
|
-
expect(pins).toHaveLength(1);
|
|
130
|
-
expect(pins[0].messageId).toBe(2);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("deletes the sidecar when the last entry is removed", () => {
|
|
134
|
-
addActivePin(tmp, makePin());
|
|
135
|
-
removeActivePin(tmp, "100", 42);
|
|
136
|
-
expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("is a no-op when the file is missing", () => {
|
|
140
|
-
removeActivePin(tmp, "100", 42);
|
|
141
|
-
expect(readActivePins(tmp)).toEqual([]);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("is a no-op when no entry matches", () => {
|
|
145
|
-
addActivePin(tmp, makePin({ messageId: 1 }));
|
|
146
|
-
removeActivePin(tmp, "100", 999);
|
|
147
|
-
expect(readActivePins(tmp)).toHaveLength(1);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("only matches on (chatId, messageId) — turnKey/pinnedAt are ignored", () => {
|
|
151
|
-
addActivePin(tmp, makePin({ chatId: "A", messageId: 1, turnKey: "x" }));
|
|
152
|
-
removeActivePin(tmp, "A", 1);
|
|
153
|
-
expect(readActivePins(tmp)).toEqual([]);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe("clearActivePins", () => {
|
|
158
|
-
it("deletes the sidecar outright", () => {
|
|
159
|
-
addActivePin(tmp, makePin());
|
|
160
|
-
addActivePin(tmp, makePin({ messageId: 43, turnKey: "b" }));
|
|
161
|
-
clearActivePins(tmp);
|
|
162
|
-
expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("is a no-op when the file is missing", () => {
|
|
166
|
-
clearActivePins(tmp);
|
|
167
|
-
expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
describe("crash → restart simulation", () => {
|
|
172
|
-
it("sidecar survives to be read by a fresh process", () => {
|
|
173
|
-
addActivePin(tmp, makePin({ chatId: "C1", messageId: 10, turnKey: "C1:0:1" }));
|
|
174
|
-
addActivePin(tmp, makePin({ chatId: "C2", messageId: 20, turnKey: "C2:0:1" }));
|
|
175
|
-
// simulated restart — only thing a fresh process has is agentDir
|
|
176
|
-
const recovered = readActivePins(tmp);
|
|
177
|
-
expect(recovered).toHaveLength(2);
|
|
178
|
-
expect(recovered.map((p) => `${p.chatId}/${p.messageId}`).sort()).toEqual([
|
|
179
|
-
"C1/10",
|
|
180
|
-
"C2/20",
|
|
181
|
-
]);
|
|
182
|
-
// sweep: unpin each then clear
|
|
183
|
-
clearActivePins(tmp);
|
|
184
|
-
expect(readActivePins(tmp)).toEqual([]);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
});
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
buildDashboardText,
|
|
4
|
-
formatRateLimitTier,
|
|
5
|
-
type DashboardState,
|
|
6
|
-
type DashboardSlot,
|
|
7
|
-
} from "../auth-dashboard";
|
|
8
|
-
|
|
9
|
-
function slot(o: Partial<DashboardSlot> = {}): DashboardSlot {
|
|
10
|
-
return { slot: "default", active: false, health: "healthy", quotaExhaustedUntil: null, fiveHourPct: null, sevenDayPct: null, ...o };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 2026-04-22 — account-identity surface.
|
|
15
|
-
*
|
|
16
|
-
* Context: a user reauths an agent onto their Max 20x account, but the
|
|
17
|
-
* OAuth browser flow gets hijacked by Telegram's in-app WebView (which
|
|
18
|
-
* uses a separate cookie jar from their main browser) and the saved
|
|
19
|
-
* token ends up for a different account (e.g. a Max 5x) instead. The
|
|
20
|
-
* dashboard header showed 'Plan: max' \u2014 indistinguishable between
|
|
21
|
-
* 5x and 20x \u2014 so the mismatch was silent until the user hit a quota wall
|
|
22
|
-
* hours later.
|
|
23
|
-
*
|
|
24
|
-
* Fix: surface the full `rateLimitTier` string on the dashboard so a
|
|
25
|
-
* wrong-account reauth is IMMEDIATELY visible. User expected max_20x,
|
|
26
|
-
* sees max_5x, acts.
|
|
27
|
-
*
|
|
28
|
-
* Pair fixes (out of scope for these tests but covered in the PR):
|
|
29
|
-
* - Auth response now includes a \ud83d\udccb Copy URL button so the user can
|
|
30
|
-
* paste into their main browser instead of Telegram's WebView.
|
|
31
|
-
* - Auth response text includes a tip about the in-app-browser pitfall.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
describe("formatRateLimitTier", () => {
|
|
35
|
-
it("shortens default_claude_max_5x to max_5x", () => {
|
|
36
|
-
expect(formatRateLimitTier("default_claude_max_5x")).toBe("max_5x");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("shortens default_claude_max_20x to max_20x", () => {
|
|
40
|
-
expect(formatRateLimitTier("default_claude_max_20x")).toBe("max_20x");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("shortens default_claude_pro to pro", () => {
|
|
44
|
-
expect(formatRateLimitTier("default_claude_pro")).toBe("pro");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("passes unknown tiers through unchanged", () => {
|
|
48
|
-
// We don't pretend to understand every future tier string. Passthrough
|
|
49
|
-
// means a new Anthropic tier name is visible verbatim until we
|
|
50
|
-
// update the formatter.
|
|
51
|
-
expect(formatRateLimitTier("team_custom_42")).toBe("team_custom_42");
|
|
52
|
-
expect(formatRateLimitTier("enterprise_unlimited")).toBe("enterprise_unlimited");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("handles empty/null-ish input gracefully", () => {
|
|
56
|
-
expect(formatRateLimitTier("")).toBe("");
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe("dashboard header surfaces rateLimitTier when present", () => {
|
|
61
|
-
const base: DashboardState = {
|
|
62
|
-
agent: "lawgpt",
|
|
63
|
-
bankId: "lawgpt",
|
|
64
|
-
plan: "max",
|
|
65
|
-
slots: [slot({ active: true })],
|
|
66
|
-
quotaHot: false,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
it("shows max_20x when on the bigger plan", () => {
|
|
70
|
-
const text = buildDashboardText({ ...base, rateLimitTier: "default_claude_max_20x" });
|
|
71
|
-
expect(text).toContain("Plan: <b>max_20x</b>");
|
|
72
|
-
// Should NOT just say 'max' \u2014 that's the ambiguous label that
|
|
73
|
-
// hid the account mismatch in the incident.
|
|
74
|
-
expect(text).not.toContain("Plan: <b>max</b>");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("shows max_5x when on the smaller plan", () => {
|
|
78
|
-
const text = buildDashboardText({ ...base, rateLimitTier: "default_claude_max_5x" });
|
|
79
|
-
expect(text).toContain("Plan: <b>max_5x</b>");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("falls back to plan label when rateLimitTier missing", () => {
|
|
83
|
-
const text = buildDashboardText({ ...base, rateLimitTier: null });
|
|
84
|
-
expect(text).toContain("Plan: <b>max</b>");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("falls back to plan label when rateLimitTier undefined", () => {
|
|
88
|
-
const text = buildDashboardText({ ...base });
|
|
89
|
-
expect(text).toContain("Plan: <b>max</b>");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("omits Plan: when neither tier nor plan are known", () => {
|
|
93
|
-
const text = buildDashboardText({ ...base, plan: null, rateLimitTier: null });
|
|
94
|
-
expect(text).not.toContain("Plan:");
|
|
95
|
-
expect(text).toContain("Bank: <code>lawgpt</code>");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("escapes HTML in tier (injection guard)", () => {
|
|
99
|
-
const text = buildDashboardText({
|
|
100
|
-
...base,
|
|
101
|
-
rateLimitTier: "<script>alert(1)</script>",
|
|
102
|
-
});
|
|
103
|
-
expect(text).not.toContain("<script>");
|
|
104
|
-
expect(text).toContain("<script>");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("pair assertion: user can distinguish 5x from 20x without hunting", () => {
|
|
108
|
-
// Regression anchor: this was the exact confusion in the incident.
|
|
109
|
-
// The user saw 'Plan: max' for both accounts and couldn't tell
|
|
110
|
-
// which got authorized. With the tier string present, 5x and 20x
|
|
111
|
-
// look different in a glance.
|
|
112
|
-
const fivex = buildDashboardText({ ...base, rateLimitTier: "default_claude_max_5x" });
|
|
113
|
-
const twentyx = buildDashboardText({ ...base, rateLimitTier: "default_claude_max_20x" });
|
|
114
|
-
expect(fivex).not.toBe(twentyx);
|
|
115
|
-
expect(fivex).toContain("5x");
|
|
116
|
-
expect(twentyx).toContain("20x");
|
|
117
|
-
});
|
|
118
|
-
});
|