switchroom 0.7.15 โ 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -59
- package/bin/run-hook.sh +27 -11
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +410 -133
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +26937 -5601
- package/dist/host-control/main.js +12702 -0
- package/dist/vault/approvals/kernel-server.js +467 -184
- package/dist/vault/broker/server.js +1430 -724
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +7 -4
- package/profiles/_base/settings.json.hbs +20 -5
- package/profiles/_base/start.sh.hbs +16 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/_shared/telegram-style.md.hbs +20 -90
- package/profiles/_shared/vault-protocol.md.hbs +68 -0
- package/profiles/default/CLAUDE.md +50 -96
- package/profiles/default/CLAUDE.md.hbs +36 -6
- package/profiles/default/workspace/SOUL.md.hbs +12 -5
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +191 -0
- package/skills/switchroom-status/SKILL.md +27 -2
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/token-helpers/SKILL.md +24 -1
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/analytics-posthog.ts +191 -0
- package/telegram-plugin/bridge/bridge.ts +69 -0
- package/telegram-plugin/bridge/ipc-client.ts +4 -1
- package/telegram-plugin/dist/bridge/bridge.js +194 -119
- package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
- package/telegram-plugin/dist/server.js +245 -189
- package/telegram-plugin/first-paint.ts +3 -24
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +169 -40
- package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
- package/telegram-plugin/gateway/boot-probes.ts +166 -123
- package/telegram-plugin/gateway/boot-reason.ts +41 -7
- package/telegram-plugin/gateway/boot-version.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +3499 -1885
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
- package/telegram-plugin/gateway/quarantine.ts +69 -0
- package/telegram-plugin/gateway/quota-cache.ts +9 -4
- package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
- package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
- package/telegram-plugin/gateway/recent-denials.ts +77 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
- package/telegram-plugin/history.ts +91 -0
- package/telegram-plugin/hooks/hooks.json +10 -0
- package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/inbound-classifier.ts +50 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +4 -2
- package/telegram-plugin/permission-rule.ts +51 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/registry/reaper.ts +223 -0
- package/telegram-plugin/retry-api-call.ts +80 -0
- package/telegram-plugin/runtime-metrics.ts +177 -0
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/secret-detect/index.ts +24 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
- package/telegram-plugin/secret-detect/vault-error.ts +78 -11
- package/telegram-plugin/secret-detect/vault-write.ts +14 -2
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/session-tail.ts +6 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/silence-poke.ts +420 -0
- package/telegram-plugin/silent-end.ts +174 -0
- package/telegram-plugin/stream-controller.ts +13 -0
- package/telegram-plugin/stream-reply-handler.ts +7 -0
- package/telegram-plugin/subagent-watcher.ts +213 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
- package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
- package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
- package/telegram-plugin/tests/boot-probes.test.ts +216 -10
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
- package/telegram-plugin/tests/history-reaper.test.ts +378 -0
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
- package/telegram-plugin/tests/issues-card.test.ts +49 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
- package/telegram-plugin/tests/permission-rule.test.ts +80 -1
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/tests/races.test.ts +179 -0
- package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
- package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
- package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
- package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
- package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
- package/telegram-plugin/tests/silence-poke.test.ts +493 -0
- package/telegram-plugin/tests/silent-end.test.ts +206 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
- package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
- package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
- package/telegram-plugin/turn-signal-tracker.ts +100 -24
- package/telegram-plugin/uat/SETUP.md +210 -35
- package/telegram-plugin/uat/assertions.ts +264 -37
- package/telegram-plugin/uat/driver-info.ts +57 -0
- package/telegram-plugin/uat/driver.ts +590 -51
- package/telegram-plugin/uat/harness.ts +140 -94
- package/telegram-plugin/uat/load-env.test.ts +72 -0
- package/telegram-plugin/uat/load-env.ts +48 -0
- package/telegram-plugin/uat/login.ts +96 -53
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
- package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
- package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
- package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
- package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
- package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
- package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
- package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
- package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
- package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
- package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
- package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
- package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
- package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
- package/telegram-plugin/vault-approval-posture.ts +42 -0
- package/telegram-plugin/welcome-text.ts +1 -0
- package/telegram-plugin/active-pins-sweep.ts +0 -204
- package/telegram-plugin/active-pins.ts +0 -146
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/card-event-log.ts +0 -138
- package/telegram-plugin/dist/foreman/foreman.js +0 -31106
- package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/pin-event-log.ts +0 -76
- package/telegram-plugin/progress-card-driver.ts +0 -2886
- package/telegram-plugin/progress-card-pin-manager.ts +0 -589
- package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
- package/telegram-plugin/progress-card.ts +0 -1409
- package/telegram-plugin/tests/HARNESS.md +0 -340
- package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
- package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
- package/telegram-plugin/tests/active-pins.test.ts +0 -187
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/card-event-log.test.ts +0 -145
- package/telegram-plugin/tests/first-paint.test.ts +0 -257
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
- package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
- package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
- package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
- package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- package/telegram-plugin/tests/setup-state.test.ts +0 -146
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
- package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
- package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
- package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
- package/telegram-plugin/two-zone-card.ts +0 -269
- package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the boot-issue dedup module (`boot-issue-cache.ts`).
|
|
3
|
+
*
|
|
4
|
+
* Covers the four canonical lifecycles a probe can move through across
|
|
5
|
+
* consecutive boots:
|
|
6
|
+
*
|
|
7
|
+
* 1. novel โ first boot a fingerprint is seen โ not snoozed, not resolved
|
|
8
|
+
* 2. repeated โ fingerprint matches prior boot โ counter increments
|
|
9
|
+
* 3. snoozed โ same fingerprint past snoozeBoots / snoozeMs โ hidden
|
|
10
|
+
* 4. resolved โ was degraded/fail last boot, ok this boot โ resolved=true
|
|
11
|
+
*
|
|
12
|
+
* Plus persistence guardrails (corrupt cache, schema mismatch, GC).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
16
|
+
import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync } from 'fs'
|
|
17
|
+
import { tmpdir } from 'os'
|
|
18
|
+
import { join } from 'path'
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
fingerprintProbe,
|
|
22
|
+
diffProbes,
|
|
23
|
+
loadCache,
|
|
24
|
+
applyAndSave,
|
|
25
|
+
DEFAULT_SNOOZE_BOOTS,
|
|
26
|
+
type BootIssueCacheFile,
|
|
27
|
+
} from '../gateway/boot-issue-cache.js'
|
|
28
|
+
import type { ProbeMap } from '../gateway/boot-card.js'
|
|
29
|
+
|
|
30
|
+
let tmp: string
|
|
31
|
+
beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), 'boot-issue-')) })
|
|
32
|
+
afterEach(() => { rmSync(tmp, { recursive: true, force: true }) })
|
|
33
|
+
|
|
34
|
+
describe('fingerprintProbe โ per-probe fold policy', () => {
|
|
35
|
+
it('skills folds across dangling count and named entries', () => {
|
|
36
|
+
const a = fingerprintProbe('skills', {
|
|
37
|
+
status: 'degraded', label: 'Skills',
|
|
38
|
+
detail: '3/12 dangling: alpha, beta, gamma',
|
|
39
|
+
})
|
|
40
|
+
const b = fingerprintProbe('skills', {
|
|
41
|
+
status: 'degraded', label: 'Skills',
|
|
42
|
+
detail: '5/12 dangling: alpha, beta, gamma, delta, epsilon',
|
|
43
|
+
})
|
|
44
|
+
expect(a).toBe(b)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('account folds by status_kind (signed-out vs token-expiring vs token-expired)', () => {
|
|
48
|
+
const signedOut1 = fingerprintProbe('account', { status: 'degraded', label: 'Account', detail: 'not signed in' })
|
|
49
|
+
const signedOut2 = fingerprintProbe('account', { status: 'degraded', label: 'Account', detail: 'Not Signed In' })
|
|
50
|
+
expect(signedOut1).toBe(signedOut2)
|
|
51
|
+
|
|
52
|
+
const exp1 = fingerprintProbe('account', { status: 'degraded', label: 'Account', detail: 'a@b ยท Pro ยท token 4d' })
|
|
53
|
+
const exp2 = fingerprintProbe('account', { status: 'degraded', label: 'Account', detail: 'a@b ยท Pro ยท token 6d' })
|
|
54
|
+
expect(exp1).toBe(exp2)
|
|
55
|
+
expect(exp1).not.toBe(signedOut1)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('agent folds by raw systemd state string', () => {
|
|
59
|
+
const a = fingerprintProbe('agent', { status: 'fail', label: 'Agent', detail: 'service failed' })
|
|
60
|
+
const b = fingerprintProbe('agent', { status: 'fail', label: 'Agent', detail: 'service failed' })
|
|
61
|
+
expect(a).toBe(b)
|
|
62
|
+
const c = fingerprintProbe('agent', { status: 'degraded', label: 'Agent', detail: 'service activating' })
|
|
63
|
+
expect(a).not.toBe(c)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('broker / kernel / hindsight use literal detail (normalized)', () => {
|
|
67
|
+
const broker1 = fingerprintProbe('broker', { status: 'fail', label: 'Broker', detail: 'socket missing' })
|
|
68
|
+
const broker2 = fingerprintProbe('broker', { status: 'fail', label: 'Broker', detail: 'socket missing' })
|
|
69
|
+
expect(broker1).toBe(broker2)
|
|
70
|
+
const broker3 = fingerprintProbe('broker', { status: 'fail', label: 'Broker', detail: 'connection refused' })
|
|
71
|
+
expect(broker1).not.toBe(broker3)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('ok results all share a single per-probe fingerprint', () => {
|
|
75
|
+
const a = fingerprintProbe('skills', { status: 'ok', label: 'Skills', detail: '12 resolved' })
|
|
76
|
+
const b = fingerprintProbe('skills', { status: 'ok', label: 'Skills', detail: '8 resolved' })
|
|
77
|
+
expect(a).toBe(b)
|
|
78
|
+
expect(a).toBe('skills:ok')
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('diffProbes โ lifecycle: novel โ repeated โ snoozed โ resolved', () => {
|
|
83
|
+
it('novel: first sighting โ not snoozed, not resolved, counter=1', () => {
|
|
84
|
+
const probes: ProbeMap = { broker: { status: 'fail', label: 'Broker', detail: 'socket missing' } }
|
|
85
|
+
const diff = diffProbes(probes, { schema: 1, probes: {} }, { now: () => 1000 })
|
|
86
|
+
expect(diff.broker?.snoozed).toBe(false)
|
|
87
|
+
expect(diff.broker?.resolved).toBe(false)
|
|
88
|
+
expect(diff.broker?.firstSighting).toBe(true)
|
|
89
|
+
expect(diff.broker?.nextEntry?.consecutiveBoots).toBe(1)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('repeated: same fingerprint โ counter increments, still surfaced (not snoozed) below threshold', () => {
|
|
93
|
+
const probes: ProbeMap = { broker: { status: 'fail', label: 'Broker', detail: 'socket missing' } }
|
|
94
|
+
const cache: BootIssueCacheFile = {
|
|
95
|
+
schema: 1,
|
|
96
|
+
probes: {
|
|
97
|
+
broker: {
|
|
98
|
+
fingerprint: 'broker:fail:socket missing',
|
|
99
|
+
consecutiveBoots: 3,
|
|
100
|
+
firstSeenMs: 1000,
|
|
101
|
+
lastSeenMs: 2000,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
const diff = diffProbes(probes, cache, { now: () => 3000, snoozeBoots: 10, snoozeMs: 1_000_000 })
|
|
106
|
+
expect(diff.broker?.snoozed).toBe(false)
|
|
107
|
+
expect(diff.broker?.nextEntry?.consecutiveBoots).toBe(4)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('snoozed: same fingerprint past snoozeBoots โ snoozed=true', () => {
|
|
111
|
+
const probes: ProbeMap = { broker: { status: 'fail', label: 'Broker', detail: 'socket missing' } }
|
|
112
|
+
const cache: BootIssueCacheFile = {
|
|
113
|
+
schema: 1,
|
|
114
|
+
probes: {
|
|
115
|
+
broker: {
|
|
116
|
+
fingerprint: 'broker:fail:socket missing',
|
|
117
|
+
consecutiveBoots: DEFAULT_SNOOZE_BOOTS, // next boot triggers snooze
|
|
118
|
+
firstSeenMs: 1000,
|
|
119
|
+
lastSeenMs: 2000,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
const diff = diffProbes(probes, cache, { now: () => 3000, snoozeMs: 1_000_000_000 })
|
|
124
|
+
expect(diff.broker?.snoozed).toBe(true)
|
|
125
|
+
expect(diff.broker?.nextEntry?.consecutiveBoots).toBe(DEFAULT_SNOOZE_BOOTS + 1)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('snoozed: same fingerprint past snoozeMs โ snoozed=true even if below boot count', () => {
|
|
129
|
+
const probes: ProbeMap = { broker: { status: 'fail', label: 'Broker', detail: 'socket missing' } }
|
|
130
|
+
const cache: BootIssueCacheFile = {
|
|
131
|
+
schema: 1,
|
|
132
|
+
probes: {
|
|
133
|
+
broker: {
|
|
134
|
+
fingerprint: 'broker:fail:socket missing',
|
|
135
|
+
consecutiveBoots: 2,
|
|
136
|
+
firstSeenMs: 1000,
|
|
137
|
+
lastSeenMs: 1500,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
const diff = diffProbes(probes, cache, {
|
|
142
|
+
now: () => 1000 + 4 * 24 * 60 * 60 * 1000, // 4 days later
|
|
143
|
+
snoozeMs: 3 * 24 * 60 * 60 * 1000, // 3-day window
|
|
144
|
+
snoozeBoots: 100,
|
|
145
|
+
})
|
|
146
|
+
expect(diff.broker?.snoozed).toBe(true)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('resolved: was degraded last boot, now ok โ resolved=true, nextEntry=null', () => {
|
|
150
|
+
const probes: ProbeMap = { broker: { status: 'ok', label: 'Broker', detail: 'reachable' } }
|
|
151
|
+
const cache: BootIssueCacheFile = {
|
|
152
|
+
schema: 1,
|
|
153
|
+
probes: {
|
|
154
|
+
broker: {
|
|
155
|
+
fingerprint: 'broker:fail:socket missing',
|
|
156
|
+
consecutiveBoots: 2,
|
|
157
|
+
firstSeenMs: 1000,
|
|
158
|
+
lastSeenMs: 2000,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
const diff = diffProbes(probes, cache, { now: () => 3000 })
|
|
163
|
+
expect(diff.broker?.resolved).toBe(true)
|
|
164
|
+
expect(diff.broker?.snoozed).toBe(false)
|
|
165
|
+
expect(diff.broker?.nextEntry).toBeNull()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('fingerprint change resets counter โ new failure mode shows even if old one was snoozed', () => {
|
|
169
|
+
const probes: ProbeMap = { broker: { status: 'fail', label: 'Broker', detail: 'connection refused' } }
|
|
170
|
+
const cache: BootIssueCacheFile = {
|
|
171
|
+
schema: 1,
|
|
172
|
+
probes: {
|
|
173
|
+
broker: {
|
|
174
|
+
fingerprint: 'broker:fail:socket missing',
|
|
175
|
+
consecutiveBoots: 50,
|
|
176
|
+
firstSeenMs: 1000,
|
|
177
|
+
lastSeenMs: 2000,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
const diff = diffProbes(probes, cache, { now: () => 3000 })
|
|
182
|
+
expect(diff.broker?.snoozed).toBe(false)
|
|
183
|
+
expect(diff.broker?.nextEntry?.consecutiveBoots).toBe(1)
|
|
184
|
+
expect(diff.broker?.firstSighting).toBe(true)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('loadCache / applyAndSave โ persistence', () => {
|
|
189
|
+
it('round-trips a diff: save then load yields the same probe entries', () => {
|
|
190
|
+
const path = join(tmp, 'cache.json')
|
|
191
|
+
const probes: ProbeMap = { broker: { status: 'fail', label: 'Broker', detail: 'socket missing' } }
|
|
192
|
+
const empty: BootIssueCacheFile = { schema: 1, probes: {} }
|
|
193
|
+
const diff = diffProbes(probes, empty, { now: () => 1000 })
|
|
194
|
+
applyAndSave(path, empty, diff)
|
|
195
|
+
expect(existsSync(path)).toBe(true)
|
|
196
|
+
const loaded = loadCache(path, () => 1000) // same clock as the diff
|
|
197
|
+
expect(loaded.probes.broker?.fingerprint).toBe(diff.broker?.fingerprint)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('resolved probe removes its entry from the cache (nextEntry=null)', () => {
|
|
201
|
+
const path = join(tmp, 'cache.json')
|
|
202
|
+
const seed: BootIssueCacheFile = {
|
|
203
|
+
schema: 1,
|
|
204
|
+
probes: {
|
|
205
|
+
broker: { fingerprint: 'broker:fail:x', consecutiveBoots: 2, firstSeenMs: 1, lastSeenMs: 2 },
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
writeFileSync(path, JSON.stringify(seed))
|
|
209
|
+
const loaded = loadCache(path, () => 1000)
|
|
210
|
+
expect(loaded.probes.broker).toBeDefined()
|
|
211
|
+
const probes: ProbeMap = { broker: { status: 'ok', label: 'Broker', detail: 'reachable' } }
|
|
212
|
+
const diff = diffProbes(probes, loaded, { now: () => 1000 })
|
|
213
|
+
applyAndSave(path, loaded, diff)
|
|
214
|
+
const reloaded = loadCache(path, () => 1000)
|
|
215
|
+
expect(reloaded.probes.broker).toBeUndefined()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('corrupt cache file is renamed aside and an empty cache is returned', () => {
|
|
219
|
+
const path = join(tmp, 'cache.json')
|
|
220
|
+
writeFileSync(path, 'not-json-{{{')
|
|
221
|
+
const loaded = loadCache(path, () => 12345)
|
|
222
|
+
expect(loaded.probes).toEqual({})
|
|
223
|
+
// The corrupt file is preserved for forensics.
|
|
224
|
+
expect(existsSync(`${path}.corrupt-12345`)).toBe(true)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('schema mismatch is treated like empty', () => {
|
|
228
|
+
const path = join(tmp, 'cache.json')
|
|
229
|
+
writeFileSync(path, JSON.stringify({ schema: 99, probes: {} }))
|
|
230
|
+
const loaded = loadCache(path)
|
|
231
|
+
expect(loaded.probes).toEqual({})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('GC drops entries older than 30 days on load', () => {
|
|
235
|
+
const path = join(tmp, 'cache.json')
|
|
236
|
+
const seed: BootIssueCacheFile = {
|
|
237
|
+
schema: 1,
|
|
238
|
+
probes: {
|
|
239
|
+
broker: { fingerprint: 'x', consecutiveBoots: 1, firstSeenMs: 0, lastSeenMs: 0 },
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
writeFileSync(path, JSON.stringify(seed))
|
|
243
|
+
// Now = far enough that 0-mtime entry exceeds 30d.
|
|
244
|
+
const loaded = loadCache(path, () => 60 * 24 * 60 * 60 * 1000)
|
|
245
|
+
expect(loaded.probes.broker).toBeUndefined()
|
|
246
|
+
})
|
|
247
|
+
})
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration unit test โ couples `determineRestartReason` to
|
|
3
|
+
* `renderBootCard` so the user-visible output is pinned across the
|
|
4
|
+
* decision boundary.
|
|
5
|
+
*
|
|
6
|
+
* Cause class CC-8 from `docs/status-ask-cause-classes.md`:
|
|
7
|
+
*
|
|
8
|
+
* Boot card silenced on operator update vs. silent on a real crash.
|
|
9
|
+
*
|
|
10
|
+
* Clean-shutdown marker (#1139/#1141/#1142) silences the boot card
|
|
11
|
+
* on operator-driven restarts. If the marker is stamped erroneously
|
|
12
|
+
* (or the 5-min freshness window is too generous on a slow boot),
|
|
13
|
+
* the card stays silent after a real crash โ user sees the agent
|
|
14
|
+
* come back with no acknowledgement โ asks "did you crash?".
|
|
15
|
+
*
|
|
16
|
+
* `boot-card-reason.test.ts` covers the decision in isolation. This
|
|
17
|
+
* file pins the next link: given the decision, what string lands in
|
|
18
|
+
* the user's chat? Specifically, on `'crash'` the card MUST surface
|
|
19
|
+
* the โ ๏ธ row + journalctl `nextStep` hint; on `'graceful'` /
|
|
20
|
+
* `'planned'` it MUST NOT (no crash row, just the ack).
|
|
21
|
+
*
|
|
22
|
+
* Together: a refactor that subtly drops the crash row, swaps the
|
|
23
|
+
* emoji on `'crash'`, or accidentally renders the crash row on
|
|
24
|
+
* `'graceful'`, fails one of these snapshots at test time.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, it, expect } from 'bun:test'
|
|
28
|
+
import { determineRestartReason } from '../gateway/boot-reason.js'
|
|
29
|
+
import { renderBootCard } from '../gateway/boot-card.js'
|
|
30
|
+
|
|
31
|
+
const NOW = 1_700_000_000_000
|
|
32
|
+
const VERSION = 'v0.8.0+106'
|
|
33
|
+
const AGENT = 'test-harness'
|
|
34
|
+
|
|
35
|
+
function rec(offsetMs: number) {
|
|
36
|
+
return { ts: NOW - offsetMs }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clean(offsetMs: number, reason?: string) {
|
|
40
|
+
return { ts: NOW - offsetMs, signal: 'SIGTERM', reason }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const session = { pid: 1234 }
|
|
44
|
+
|
|
45
|
+
describe('boot-card: reason โ user-visible render (CC-8)', () => {
|
|
46
|
+
// โโโ happy path: clean operator restart, recently stamped โโโโโโ
|
|
47
|
+
it('clean operator restart within 5min window renders ack only (no crash row)', () => {
|
|
48
|
+
const reason = determineRestartReason({
|
|
49
|
+
marker: null,
|
|
50
|
+
cleanMarker: clean(97_000, 'operator: switchroom update'),
|
|
51
|
+
sessionMarker: session,
|
|
52
|
+
now: NOW,
|
|
53
|
+
})
|
|
54
|
+
expect(reason).toBe('graceful')
|
|
55
|
+
const card = renderBootCard({
|
|
56
|
+
agentName: AGENT,
|
|
57
|
+
version: VERSION,
|
|
58
|
+
restartReason: reason,
|
|
59
|
+
})
|
|
60
|
+
expect(card).toMatchInlineSnapshot(`"โ
<b>test-harness</b> back up ยท v0.8.0+106"`)
|
|
61
|
+
// Negative assertions on the failure surface CC-8 worries about:
|
|
62
|
+
expect(card).not.toContain('crash recovery')
|
|
63
|
+
expect(card).not.toContain('journalctl')
|
|
64
|
+
expect(card).not.toContain('โ ๏ธ')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// โโโ the worry case: marker erroneously stamped, real crash later โโ
|
|
68
|
+
it('operator marker stale beyond 5min + session marker โ crash card with hint', () => {
|
|
69
|
+
// 6 min after stamping โ operator-extended window has expired,
|
|
70
|
+
// so even a planned-looking marker reads as a crash. This is
|
|
71
|
+
// EXACTLY CC-8's failure shape: if we'd left this case silent,
|
|
72
|
+
// the user would never see the crash recovery row.
|
|
73
|
+
const reason = determineRestartReason({
|
|
74
|
+
marker: null,
|
|
75
|
+
cleanMarker: clean(6 * 60_000, 'operator: switchroom update'),
|
|
76
|
+
sessionMarker: session,
|
|
77
|
+
now: NOW,
|
|
78
|
+
})
|
|
79
|
+
expect(reason).toBe('crash')
|
|
80
|
+
const card = renderBootCard({
|
|
81
|
+
agentName: AGENT,
|
|
82
|
+
version: VERSION,
|
|
83
|
+
restartReason: reason,
|
|
84
|
+
restartAgeMs: 3_400,
|
|
85
|
+
})
|
|
86
|
+
expect(card).toContain('โ ๏ธ <b>test-harness</b> back up')
|
|
87
|
+
expect(card).toContain('โ ๏ธ <b>Restart</b> crash recovery ยท 3.4s ago')
|
|
88
|
+
expect(card).toContain('Tail logs:')
|
|
89
|
+
expect(card).toContain('<code>journalctl --user -u switchroom-test-harness -n 100</code>')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// โโโ canonical crash: no marker at all โโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
93
|
+
it('no markers + session marker โ crash card with hint', () => {
|
|
94
|
+
const reason = determineRestartReason({
|
|
95
|
+
marker: null,
|
|
96
|
+
cleanMarker: null,
|
|
97
|
+
sessionMarker: session,
|
|
98
|
+
now: NOW,
|
|
99
|
+
})
|
|
100
|
+
expect(reason).toBe('crash')
|
|
101
|
+
const card = renderBootCard({
|
|
102
|
+
agentName: AGENT,
|
|
103
|
+
version: VERSION,
|
|
104
|
+
restartReason: reason,
|
|
105
|
+
restartAgeMs: 12_000,
|
|
106
|
+
})
|
|
107
|
+
expect(card).toContain('โ ๏ธ <b>test-harness</b> back up')
|
|
108
|
+
expect(card).toContain('crash recovery ยท 12.0s ago')
|
|
109
|
+
expect(card).toContain('Tail logs:')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// โโโ user /restart (non-operator): tight 60s window applies โโโโ
|
|
113
|
+
it('user: /restart marker stale beyond 60s โ crash card (tight window not extended)', () => {
|
|
114
|
+
// A /restart from chat that takes >60s before its gateway boots
|
|
115
|
+
// is a real crash; the catalog's CC-8 worry includes this path.
|
|
116
|
+
const reason = determineRestartReason({
|
|
117
|
+
marker: null,
|
|
118
|
+
cleanMarker: clean(90_000, 'user: /restart from chat'),
|
|
119
|
+
sessionMarker: session,
|
|
120
|
+
now: NOW,
|
|
121
|
+
})
|
|
122
|
+
expect(reason).toBe('crash')
|
|
123
|
+
const card = renderBootCard({
|
|
124
|
+
agentName: AGENT,
|
|
125
|
+
version: VERSION,
|
|
126
|
+
restartReason: reason,
|
|
127
|
+
restartAgeMs: 1_500,
|
|
128
|
+
})
|
|
129
|
+
expect(card).toContain('crash recovery ยท 1.5s ago')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// โโโ planned restart via switchroom: ack only โโโโโโโโโโโโโโโโโโ
|
|
133
|
+
it('planned restart (marker present, fresh) โ ack only', () => {
|
|
134
|
+
const reason = determineRestartReason({
|
|
135
|
+
marker: rec(10_000),
|
|
136
|
+
cleanMarker: null,
|
|
137
|
+
sessionMarker: session,
|
|
138
|
+
now: NOW,
|
|
139
|
+
})
|
|
140
|
+
expect(reason).toBe('planned')
|
|
141
|
+
const card = renderBootCard({
|
|
142
|
+
agentName: AGENT,
|
|
143
|
+
version: VERSION,
|
|
144
|
+
restartReason: reason,
|
|
145
|
+
})
|
|
146
|
+
expect(card).toMatchInlineSnapshot(`"โ
<b>test-harness</b> back up ยท v0.8.0+106"`)
|
|
147
|
+
expect(card).not.toContain('crash recovery')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// โโโ fresh first start: distinct emoji, ack only โโโโโโโโโโโโโโโ
|
|
151
|
+
it('fresh first start (no markers, no session) โ ๐ ack', () => {
|
|
152
|
+
const reason = determineRestartReason({
|
|
153
|
+
marker: null,
|
|
154
|
+
cleanMarker: null,
|
|
155
|
+
sessionMarker: null,
|
|
156
|
+
now: NOW,
|
|
157
|
+
})
|
|
158
|
+
expect(reason).toBe('fresh')
|
|
159
|
+
const card = renderBootCard({
|
|
160
|
+
agentName: AGENT,
|
|
161
|
+
version: VERSION,
|
|
162
|
+
restartReason: reason,
|
|
163
|
+
})
|
|
164
|
+
expect(card).toMatchInlineSnapshot(`"๐ <b>test-harness</b> back up ยท v0.8.0+106"`)
|
|
165
|
+
expect(card).not.toContain('crash recovery')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// โโโ slug override path: agentSlug used in journalctl, not agentName โโโ
|
|
169
|
+
it('crash card uses agentSlug (not agentName) in the journalctl command', () => {
|
|
170
|
+
// The journalctl row is the user's actionable next step โ if the
|
|
171
|
+
// slug ever drifts (capitalization, special chars), the copy-paste
|
|
172
|
+
// command stops working. Pin the slug pathway explicitly.
|
|
173
|
+
const card = renderBootCard({
|
|
174
|
+
agentName: 'Test Harness Display',
|
|
175
|
+
agentSlug: 'test-harness',
|
|
176
|
+
version: VERSION,
|
|
177
|
+
restartReason: 'crash',
|
|
178
|
+
restartAgeMs: 800,
|
|
179
|
+
})
|
|
180
|
+
expect(card).toContain('<code>journalctl --user -u switchroom-test-harness -n 100</code>')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
@@ -16,8 +16,10 @@ function recentMarker(offsetMs = 0) {
|
|
|
16
16
|
return { ts: NOW - offsetMs }
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function recentCleanMarker(offsetMs = 0) {
|
|
20
|
-
|
|
19
|
+
function recentCleanMarker(offsetMs = 0, reason?: string) {
|
|
20
|
+
// `signal` is part of the CleanShutdownMarker shape; required at the
|
|
21
|
+
// type level but not exercised by determineRestartReason itself.
|
|
22
|
+
return { ts: NOW - offsetMs, signal: 'SIGTERM', reason }
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
function sessionMarker() {
|
|
@@ -89,6 +91,67 @@ describe('determineRestartReason', () => {
|
|
|
89
91
|
expect(result).toBe('planned')
|
|
90
92
|
})
|
|
91
93
|
|
|
94
|
+
it('returns "graceful" when clean-shutdown marker has operator: reason and is within the extended 5-min window (#1141 follow-up: 9-agent fleet recreate can exceed 60s)', () => {
|
|
95
|
+
const result = determineRestartReason({
|
|
96
|
+
marker: null,
|
|
97
|
+
cleanMarker: recentCleanMarker(97_000, 'operator: switchroom update'),
|
|
98
|
+
sessionMarker: sessionMarker(),
|
|
99
|
+
now: NOW,
|
|
100
|
+
})
|
|
101
|
+
expect(result).toBe('graceful')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('still treats operator: marker as stale beyond 5 min (longer window not "silent forever")', () => {
|
|
105
|
+
const result = determineRestartReason({
|
|
106
|
+
marker: null,
|
|
107
|
+
cleanMarker: recentCleanMarker(6 * 60_000, 'operator: switchroom update'),
|
|
108
|
+
sessionMarker: sessionMarker(),
|
|
109
|
+
now: NOW,
|
|
110
|
+
})
|
|
111
|
+
expect(result).toBe('crash')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('non-operator reasons (user:, cli:) keep the tight 60s window โ a /restart that takes >60s before its gateway boots is still a crash', () => {
|
|
115
|
+
const result = determineRestartReason({
|
|
116
|
+
marker: null,
|
|
117
|
+
cleanMarker: recentCleanMarker(90_000, 'user: /restart from chat'),
|
|
118
|
+
sessionMarker: sessionMarker(),
|
|
119
|
+
now: NOW,
|
|
120
|
+
})
|
|
121
|
+
expect(result).toBe('crash')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('cli: reasons also keep the tight 60s window', () => {
|
|
125
|
+
const result = determineRestartReason({
|
|
126
|
+
marker: null,
|
|
127
|
+
cleanMarker: recentCleanMarker(90_000, 'cli: switchroom restart'),
|
|
128
|
+
sessionMarker: sessionMarker(),
|
|
129
|
+
now: NOW,
|
|
130
|
+
})
|
|
131
|
+
expect(result).toBe('crash')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('operator: marker just barely inside the 5-min window still graceful', () => {
|
|
135
|
+
const result = determineRestartReason({
|
|
136
|
+
marker: null,
|
|
137
|
+
cleanMarker: recentCleanMarker(4 * 60_000 + 59_000, 'operator: switchroom update'),
|
|
138
|
+
sessionMarker: sessionMarker(),
|
|
139
|
+
now: NOW,
|
|
140
|
+
})
|
|
141
|
+
expect(result).toBe('graceful')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('respects operatorMaxAgeMs override (tests can tighten the window)', () => {
|
|
145
|
+
const result = determineRestartReason({
|
|
146
|
+
marker: null,
|
|
147
|
+
cleanMarker: recentCleanMarker(120_000, 'operator: switchroom update'),
|
|
148
|
+
sessionMarker: sessionMarker(),
|
|
149
|
+
now: NOW,
|
|
150
|
+
operatorMaxAgeMs: 60_000, // override tightens to 60s
|
|
151
|
+
})
|
|
152
|
+
expect(result).toBe('crash')
|
|
153
|
+
})
|
|
154
|
+
|
|
92
155
|
it('respects custom markerMaxAgeMs โ stale marker does not count as planned', () => {
|
|
93
156
|
const result = determineRestartReason({
|
|
94
157
|
marker: recentMarker(10 * 60_000), // 10 min ago
|
|
@@ -135,6 +135,95 @@ describe('renderBootCard โ degraded conditions', () => {
|
|
|
135
135
|
expect(out).toContain('๐ก <b>Account</b> expiring')
|
|
136
136
|
})
|
|
137
137
|
|
|
138
|
+
it('renders nextStep as an indented continuation line beneath a degraded row', () => {
|
|
139
|
+
// Principle 1 ("If they need the docs, we've failed"): every degraded
|
|
140
|
+
// probe should surface its remediation inline. Plain backticks in the
|
|
141
|
+
// nextStep get translated to <code> spans so the command stays tap-to-
|
|
142
|
+
// copy on mobile.
|
|
143
|
+
const out = renderBootCard({
|
|
144
|
+
agentName: 'lawgpt',
|
|
145
|
+
version: 'v0.7.16',
|
|
146
|
+
probes: {
|
|
147
|
+
skills: {
|
|
148
|
+
status: 'degraded',
|
|
149
|
+
label: 'Skills',
|
|
150
|
+
detail: '10/10 dangling: a, b, c +7 more',
|
|
151
|
+
nextStep: 'Run `switchroom agent reconcile lawgpt` to rebuild symlinks',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
expect(out).toContain('๐ก <b>Skills</b> 10/10 dangling')
|
|
156
|
+
expect(out).toContain(' โณ Run <code>switchroom agent reconcile lawgpt</code> to rebuild symlinks')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('crash row carries a tail-logs next-step', () => {
|
|
160
|
+
const out = renderBootCard({
|
|
161
|
+
agentName: 'lawgpt',
|
|
162
|
+
version: 'v0.7.16',
|
|
163
|
+
restartReason: 'crash',
|
|
164
|
+
restartAgeMs: 6_100,
|
|
165
|
+
})
|
|
166
|
+
expect(out).toContain('โ ๏ธ <b>Restart</b> crash recovery ยท 6.1s ago')
|
|
167
|
+
expect(out).toContain('โณ Tail logs: <code>journalctl --user -u switchroom-lawgpt -n 100</code>')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('crash row uses agentSlug for the systemd unit when provided', () => {
|
|
171
|
+
const out = renderBootCard({
|
|
172
|
+
agentName: 'LawGPT',
|
|
173
|
+
agentSlug: 'lawgpt',
|
|
174
|
+
version: 'v1',
|
|
175
|
+
restartReason: 'crash',
|
|
176
|
+
})
|
|
177
|
+
expect(out).toContain('switchroom-lawgpt')
|
|
178
|
+
expect(out).not.toContain('switchroom-LawGPT')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('renderNextStep escapes HTML inside backtick-quoted commands', () => {
|
|
182
|
+
const out = renderBootCard({
|
|
183
|
+
agentName: 'a',
|
|
184
|
+
version: 'v',
|
|
185
|
+
probes: {
|
|
186
|
+
account: {
|
|
187
|
+
status: 'fail',
|
|
188
|
+
label: 'Account',
|
|
189
|
+
detail: 'expired',
|
|
190
|
+
nextStep: 'Run `foo <bar> & baz` to fix',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
expect(out).toContain('<code>foo <bar> & baz</code>')
|
|
195
|
+
expect(out).not.toContain('<bar>')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('unpaired backticks in nextStep fall back to plain escaped text', () => {
|
|
199
|
+
const out = renderBootCard({
|
|
200
|
+
agentName: 'a',
|
|
201
|
+
version: 'v',
|
|
202
|
+
probes: {
|
|
203
|
+
account: {
|
|
204
|
+
status: 'fail',
|
|
205
|
+
label: 'Account',
|
|
206
|
+
detail: 'expired',
|
|
207
|
+
nextStep: 'Run `switchroom foo to fix',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
expect(out).toContain('โณ Run `switchroom foo to fix')
|
|
212
|
+
expect(out).not.toContain('<code>')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('degraded rows without nextStep render unchanged (backwards compat)', () => {
|
|
216
|
+
const out = renderBootCard({
|
|
217
|
+
agentName: 'a',
|
|
218
|
+
version: 'v',
|
|
219
|
+
probes: {
|
|
220
|
+
quota: { status: 'fail', label: 'Quota', detail: 'rate limited' },
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
expect(out).toContain('๐ด <b>Quota</b> rate limited')
|
|
224
|
+
expect(out).not.toContain('โณ')
|
|
225
|
+
})
|
|
226
|
+
|
|
138
227
|
it('null probe entries are skipped (defensive against partial probe maps)', () => {
|
|
139
228
|
const out = renderBootCard({
|
|
140
229
|
agentName: 'a',
|
|
@@ -218,3 +307,60 @@ describe('resolvePersonaName โ persona name over slug (#169)', () => {
|
|
|
218
307
|
expect(out).not.toContain('<b>finn</b>')
|
|
219
308
|
})
|
|
220
309
|
})
|
|
310
|
+
|
|
311
|
+
// โโ Issue dedup rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
312
|
+
// Resolved rows render at the top of the degraded section; snoozed rows
|
|
313
|
+
// suppress the matching probe row entirely.
|
|
314
|
+
|
|
315
|
+
describe('renderBootCard โ resolved / snooze rendering', () => {
|
|
316
|
+
it('renders a โ
"resolved" row for each entry in resolvedRows above the degraded section', () => {
|
|
317
|
+
const out = renderBootCard({
|
|
318
|
+
agentName: 'k',
|
|
319
|
+
version: 'v',
|
|
320
|
+
probes: {
|
|
321
|
+
broker: { status: 'fail', label: 'Broker', detail: 'socket missing' },
|
|
322
|
+
},
|
|
323
|
+
resolvedRows: ['hindsight'],
|
|
324
|
+
})
|
|
325
|
+
expect(out).toContain('โ
<b>Hindsight</b> resolved')
|
|
326
|
+
expect(out).toContain('๐ด <b>Broker</b> socket missing')
|
|
327
|
+
// Resolved appears BEFORE Broker.
|
|
328
|
+
expect(out.indexOf('Hindsight')).toBeLessThan(out.indexOf('Broker'))
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('skips a degraded row when its probe key is in snoozeRows', () => {
|
|
332
|
+
const out = renderBootCard({
|
|
333
|
+
agentName: 'k',
|
|
334
|
+
version: 'v',
|
|
335
|
+
probes: {
|
|
336
|
+
broker: { status: 'fail', label: 'Broker', detail: 'socket missing' },
|
|
337
|
+
kernel: { status: 'fail', label: 'Kernel', detail: 'socket missing' },
|
|
338
|
+
},
|
|
339
|
+
snoozeRows: ['broker'],
|
|
340
|
+
})
|
|
341
|
+
expect(out).not.toContain('Broker')
|
|
342
|
+
expect(out).toContain('Kernel')
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('snoozed everything โ output is the bare ack line (silent-when-snoozed)', () => {
|
|
346
|
+
const out = renderBootCard({
|
|
347
|
+
agentName: 'k',
|
|
348
|
+
version: 'v0.1',
|
|
349
|
+
probes: {
|
|
350
|
+
broker: { status: 'fail', label: 'Broker', detail: 'socket missing' },
|
|
351
|
+
},
|
|
352
|
+
snoozeRows: ['broker'],
|
|
353
|
+
})
|
|
354
|
+
expect(out).toBe('โ
<b>k</b> back up ยท v0.1')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('resolvedRows alone (no probes degraded) renders the resolved row beneath the ack', () => {
|
|
358
|
+
const out = renderBootCard({
|
|
359
|
+
agentName: 'k',
|
|
360
|
+
version: 'v',
|
|
361
|
+
resolvedRows: ['skills', 'broker'],
|
|
362
|
+
})
|
|
363
|
+
expect(out).toContain('โ
<b>Skills</b> resolved')
|
|
364
|
+
expect(out).toContain('โ
<b>Broker</b> resolved')
|
|
365
|
+
})
|
|
366
|
+
})
|