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,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the sandbox-hint-posttool hook (Layer 2 of the sandbox UX work).
|
|
3
|
+
*
|
|
4
|
+
* Hook contract:
|
|
5
|
+
* stdin: PostToolUse JSON event { tool_name, tool_response, ... }
|
|
6
|
+
* stdout: optional JSON
|
|
7
|
+
* {"hookSpecificOutput":{"hookEventName":"PostToolUse",
|
|
8
|
+
* "additionalContext":"..."}}
|
|
9
|
+
* exit: 0 always.
|
|
10
|
+
*
|
|
11
|
+
* Tests spawn the hook as a subprocess (mirroring how Claude Code invokes
|
|
12
|
+
* it), feed a tool_response, and assert whether additionalContext was
|
|
13
|
+
* emitted and that it carries the load-bearing strings the agent needs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'bun:test'
|
|
17
|
+
import { join } from 'path'
|
|
18
|
+
import { spawnSync } from 'child_process'
|
|
19
|
+
|
|
20
|
+
const HOOK_SCRIPT = join(import.meta.dir, '..', 'hooks', 'sandbox-hint-posttool.mjs')
|
|
21
|
+
|
|
22
|
+
function runHook(event: object) {
|
|
23
|
+
const result = spawnSync(process.execPath, [HOOK_SCRIPT], {
|
|
24
|
+
input: JSON.stringify(event),
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
env: process.env,
|
|
27
|
+
timeout: 5_000,
|
|
28
|
+
})
|
|
29
|
+
return result
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseContext(stdout: string): string | null {
|
|
33
|
+
if (!stdout.trim()) return null
|
|
34
|
+
const parsed = JSON.parse(stdout)
|
|
35
|
+
return parsed?.hookSpecificOutput?.additionalContext ?? null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('sandbox-hint-posttool', () => {
|
|
39
|
+
it('emits sandbox hint when tool_response contains EROFS', () => {
|
|
40
|
+
const result = runHook({
|
|
41
|
+
tool_name: 'Write',
|
|
42
|
+
tool_use_id: 'toolu_001',
|
|
43
|
+
tool_response: {
|
|
44
|
+
error: "EROFS: read-only file system, open '/opt/switchroom/skills/foo.md'",
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(result.status).toBe(0)
|
|
49
|
+
const ctx = parseContext(result.stdout)
|
|
50
|
+
expect(ctx).not.toBeNull()
|
|
51
|
+
expect(ctx).toContain('Sandbox boundary hit')
|
|
52
|
+
expect(ctx).toContain('operator action')
|
|
53
|
+
expect(ctx).toContain('Writable paths')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('emits sandbox hint when tool_response contains "Read-only file system"', () => {
|
|
57
|
+
const result = runHook({
|
|
58
|
+
tool_name: 'Edit',
|
|
59
|
+
tool_use_id: 'toolu_002',
|
|
60
|
+
tool_response: 'mkdir: cannot create directory: Read-only file system',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(result.status).toBe(0)
|
|
64
|
+
const ctx = parseContext(result.stdout)
|
|
65
|
+
expect(ctx).toContain('Sandbox boundary hit')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('emits an apt-specific hint when tool_response shows dpkg permission denied', () => {
|
|
69
|
+
const result = runHook({
|
|
70
|
+
tool_name: 'Bash',
|
|
71
|
+
tool_use_id: 'toolu_003',
|
|
72
|
+
tool_response: {
|
|
73
|
+
stderr:
|
|
74
|
+
'E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?',
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(result.status).toBe(0)
|
|
79
|
+
const ctx = parseContext(result.stdout)
|
|
80
|
+
expect(ctx).toContain('docker/Dockerfile.agent')
|
|
81
|
+
expect(ctx).toContain('rebuild')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('emits a hint for EACCES on a rootfs path', () => {
|
|
85
|
+
const result = runHook({
|
|
86
|
+
tool_name: 'Bash',
|
|
87
|
+
tool_use_id: 'toolu_004',
|
|
88
|
+
tool_response: 'npm ERR! EACCES: permission denied, mkdir "/usr/lib/node_modules/foo"',
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(result.status).toBe(0)
|
|
92
|
+
const ctx = parseContext(result.stdout)
|
|
93
|
+
expect(ctx).toContain('Sandbox boundary hit')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('emits nothing when tool_response is a normal success', () => {
|
|
97
|
+
const result = runHook({
|
|
98
|
+
tool_name: 'Bash',
|
|
99
|
+
tool_use_id: 'toolu_005',
|
|
100
|
+
tool_response: { stdout: 'hello world\n', exit_code: 0 },
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(result.status).toBe(0)
|
|
104
|
+
expect(result.stdout.trim()).toBe('')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('emits nothing when tool_response merely mentions /usr but is not a sandbox error', () => {
|
|
108
|
+
// Guard against false positives — the agent may legitimately discuss
|
|
109
|
+
// paths under /usr in normal output (e.g. `which node` returning
|
|
110
|
+
// /usr/local/bin/node). Only EACCES / EROFS patterns should trigger.
|
|
111
|
+
const result = runHook({
|
|
112
|
+
tool_name: 'Bash',
|
|
113
|
+
tool_use_id: 'toolu_006',
|
|
114
|
+
tool_response: { stdout: '/usr/local/bin/node\n' },
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
expect(result.status).toBe(0)
|
|
118
|
+
expect(result.stdout.trim()).toBe('')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('exits 0 on malformed stdin without crashing', () => {
|
|
122
|
+
const result = spawnSync(process.execPath, [HOOK_SCRIPT], {
|
|
123
|
+
input: 'not json at all',
|
|
124
|
+
encoding: 'utf8',
|
|
125
|
+
timeout: 5_000,
|
|
126
|
+
})
|
|
127
|
+
expect(result.status).toBe(0)
|
|
128
|
+
expect(result.stdout.trim()).toBe('')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('exits 0 on empty stdin', () => {
|
|
132
|
+
const result = spawnSync(process.execPath, [HOOK_SCRIPT], {
|
|
133
|
+
input: '',
|
|
134
|
+
encoding: 'utf8',
|
|
135
|
+
timeout: 5_000,
|
|
136
|
+
})
|
|
137
|
+
expect(result.status).toBe(0)
|
|
138
|
+
expect(result.stdout.trim()).toBe('')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('caps the scan window for huge tool_response payloads', () => {
|
|
142
|
+
// 100 KiB of harmless output followed by an EROFS — we cap at 64 KiB
|
|
143
|
+
// so this should NOT match. Keeps a runaway tool_response from
|
|
144
|
+
// pinning the hook on a regex scan.
|
|
145
|
+
const huge = 'x'.repeat(100 * 1024) + ' EROFS happened'
|
|
146
|
+
const result = runHook({
|
|
147
|
+
tool_name: 'Bash',
|
|
148
|
+
tool_use_id: 'toolu_007',
|
|
149
|
+
tool_response: { stdout: huge },
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(result.status).toBe(0)
|
|
153
|
+
expect(result.stdout.trim()).toBe('')
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD-RED-first contract for the silent-delete-failure class the
|
|
3
|
+
* operator reported on 2026-05-12.
|
|
4
|
+
*
|
|
5
|
+
* Symptom: "🔒 captured a secret. we deleted it from chat" lands as
|
|
6
|
+
* a reply, but the raw secret-bearing message remains visible in
|
|
7
|
+
* chat history. The operator only finds out by scrolling, after
|
|
8
|
+
* the secret has already been screen-shot / cached / synced to
|
|
9
|
+
* other devices.
|
|
10
|
+
*
|
|
11
|
+
* Root cause: every secret-detect call site that invokes
|
|
12
|
+
* `bot.api.deleteMessage(chat_id, msgId)` does it via a raw
|
|
13
|
+
* `try { … } catch { … }` block that silently swallows the error
|
|
14
|
+
* (or, at best, logs to stderr — invisible to the operator). The
|
|
15
|
+
* gateway already has a `deleteSensitiveMessage(chat_id, msgId,
|
|
16
|
+
* reason)` helper that does the right thing — try delete, surface
|
|
17
|
+
* loudly on failure with an in-chat warning naming the message id
|
|
18
|
+
* the operator must delete manually. The four secret-detect sites
|
|
19
|
+
* weren't migrated when that helper was added (#44).
|
|
20
|
+
*
|
|
21
|
+
* The fix is mechanical: replace each `try { bot.api.deleteMessage
|
|
22
|
+
* } catch { }` in the secret-detect path with
|
|
23
|
+
* `deleteSensitiveMessage`. That's what this test pins.
|
|
24
|
+
*
|
|
25
|
+
* Failing means: at least one secret-detect path still has a raw
|
|
26
|
+
* `bot.api.deleteMessage` call wrapped in a swallow-catch — a
|
|
27
|
+
* regression of the silent-failure bug.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { describe, it, expect } from "vitest";
|
|
31
|
+
import { readFileSync } from "node:fs";
|
|
32
|
+
import { resolve } from "node:path";
|
|
33
|
+
|
|
34
|
+
const gatewaySrc = readFileSync(
|
|
35
|
+
resolve(__dirname, "..", "gateway", "gateway.ts"),
|
|
36
|
+
"utf-8",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/** Slice the gateway source between two anchor strings. */
|
|
40
|
+
function sliceBetween(src: string, from: string, to: string): string {
|
|
41
|
+
const start = src.indexOf(from);
|
|
42
|
+
if (start < 0) return "";
|
|
43
|
+
const end = src.indexOf(to, start);
|
|
44
|
+
return src.slice(start, end > 0 ? end : src.length);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("secret-detect — delete failures must NOT be silently swallowed (2026-05-12)", () => {
|
|
48
|
+
// The secret-detect block lives inside `handleInbound`. It runs
|
|
49
|
+
// through several branches:
|
|
50
|
+
// - passphrase cached + high-confidence hit (stored path)
|
|
51
|
+
// - passphrase cached + Channel-B auth-flow fallback
|
|
52
|
+
// - no-passphrase deferred path
|
|
53
|
+
// - pipeline-error fail-closed path
|
|
54
|
+
//
|
|
55
|
+
// Each branch deletes the raw message. None of them may use the
|
|
56
|
+
// raw `bot.api.deleteMessage` pattern wrapped in a swallow-catch
|
|
57
|
+
// — they must all route through `deleteSensitiveMessage` which
|
|
58
|
+
// surfaces failures via stderr + in-chat warning.
|
|
59
|
+
|
|
60
|
+
const secretDetectBlock = sliceBetween(
|
|
61
|
+
gatewaySrc,
|
|
62
|
+
"FAIL-CLOSED: if the pipeline throws",
|
|
63
|
+
"Status reaction controller",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
it("the secret-detect block exists and is non-trivial (anchor sanity)", () => {
|
|
67
|
+
// fails when: the anchors above are renamed/moved. If this fails,
|
|
68
|
+
// update the anchors AND audit the other assertions in this file
|
|
69
|
+
// to make sure they still target the secret-detect path.
|
|
70
|
+
expect(secretDetectBlock.length).toBeGreaterThan(500);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("no raw `bot.api.deleteMessage` calls inside the secret-detect block", () => {
|
|
74
|
+
// fails when: a code site reverts to the raw API call (which
|
|
75
|
+
// swallows on failure). All deletes here MUST route through the
|
|
76
|
+
// shared helper.
|
|
77
|
+
expect(secretDetectBlock).not.toMatch(/bot\.api\.deleteMessage/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("every delete in the secret-detect block goes through deleteSensitiveMessage", () => {
|
|
81
|
+
// The block currently performs deletes for four distinct cases
|
|
82
|
+
// (stored / auth-flow-fallback / deferred / pipeline-error). All
|
|
83
|
+
// four MUST land here.
|
|
84
|
+
//
|
|
85
|
+
// fails when: a refactor extracts one of the branches into a
|
|
86
|
+
// helper that doesn't use deleteSensitiveMessage, OR when a new
|
|
87
|
+
// branch is added without the helper. Either way the contract
|
|
88
|
+
// breaks.
|
|
89
|
+
const callMatches = secretDetectBlock.match(/deleteSensitiveMessage\s*\(/g) ?? [];
|
|
90
|
+
expect(
|
|
91
|
+
callMatches.length,
|
|
92
|
+
`expected ≥3 deleteSensitiveMessage calls inside the secret-detect block; got ${callMatches.length}`,
|
|
93
|
+
).toBeGreaterThanOrEqual(3);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("no swallow-catch pattern around delete calls in the secret-detect block", () => {
|
|
97
|
+
// The legacy pattern: `try { await bot.api.deleteMessage(...) } catch {}`
|
|
98
|
+
// OR `try { ... } catch { /* swallow */ }`. The helper handles
|
|
99
|
+
// failure surfacing, so call sites should NOT wrap the helper
|
|
100
|
+
// in another silencing catch.
|
|
101
|
+
//
|
|
102
|
+
// fails when: a refactor wraps the helper call in `try { ... } catch {}`
|
|
103
|
+
// for "robustness" — which would re-introduce the silent-failure
|
|
104
|
+
// class this PR exists to close.
|
|
105
|
+
expect(secretDetectBlock).not.toMatch(/try\s*\{[^}]*deleteSensitiveMessage[^}]*\}\s*catch\s*\{\s*\}/s);
|
|
106
|
+
// Also forbid the raw `try { bot.api.deleteMessage ... } catch {}` shape.
|
|
107
|
+
expect(secretDetectBlock).not.toMatch(/try\s*\{[^}]*bot\.api\.deleteMessage[^}]*\}\s*catch/s);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("secret-detect — deleteSensitiveMessage helper retains its 'surface failures' contract", () => {
|
|
112
|
+
// The fix only works if the helper itself surfaces failures.
|
|
113
|
+
// Pin the helper's load-bearing behavior so a future refactor
|
|
114
|
+
// can't quietly turn it into a silent-catch.
|
|
115
|
+
const helperBody = sliceBetween(
|
|
116
|
+
gatewaySrc,
|
|
117
|
+
"async function deleteSensitiveMessage",
|
|
118
|
+
"function getCommandArgs",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
it("helper logs to stderr on delete failure", () => {
|
|
122
|
+
expect(helperBody).toMatch(/process\.stderr\.write/);
|
|
123
|
+
expect(helperBody).toMatch(/SECURITY:.*FAILED/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("helper posts an in-chat warning naming the leaked message id", () => {
|
|
127
|
+
// The warning is the only signal a mobile-only operator gets —
|
|
128
|
+
// stderr is invisible to them. Pinning the in-chat surface as
|
|
129
|
+
// the load-bearing piece.
|
|
130
|
+
expect(helperBody).toMatch(/sendMessage/);
|
|
131
|
+
expect(helperBody).toMatch(/delete message.*manually|delete it manually|manually|delete message <code>/i);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD-RED-first contract tests for the false-positive class the
|
|
3
|
+
* operator reported on 2026-05-12.
|
|
4
|
+
*
|
|
5
|
+
* Symptom: casual chat that MENTIONS the words "secret", "token",
|
|
6
|
+
* "password", or an ALLCAPS *_KEY/_TOKEN/_SECRET identifier triggers
|
|
7
|
+
* the redaction pipeline as if the user just pasted a real credential
|
|
8
|
+
* — original message gets deleted, ambiguous-card lands, the operator
|
|
9
|
+
* has to dismiss it. Worst case: the operator is asking the agent
|
|
10
|
+
* *about* a secret ("delete the secret I sent yesterday") and gets
|
|
11
|
+
* stuck in a redaction-of-the-question loop.
|
|
12
|
+
*
|
|
13
|
+
* The pre-fix `env_key_value` pattern in patterns.ts:71 is the
|
|
14
|
+
* load-bearing culprit:
|
|
15
|
+
*
|
|
16
|
+
* /\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD))\b\s*[=:]\s*
|
|
17
|
+
* (["']?)([^\s"'\\]+)\2/g
|
|
18
|
+
*
|
|
19
|
+
* It matches ANY value after `SECRET=` / `TOKEN=` with no entropy
|
|
20
|
+
* gate, no length floor, no shape check on the value. Trivially fires
|
|
21
|
+
* on `SECRET=foo`, `MY_KEY=bar`, even `FATSECRET=hello`.
|
|
22
|
+
*
|
|
23
|
+
* The pre-fix `kv_entropy` pattern in kv-scanner.ts:30 has a 4.0
|
|
24
|
+
* entropy gate which catches some — but `kv_entropy` is the
|
|
25
|
+
* lower-confidence layer and doesn't run when the higher-confidence
|
|
26
|
+
* `env_key_value` already fired. Tightening env_key_value is the
|
|
27
|
+
* right place to land the fix.
|
|
28
|
+
*
|
|
29
|
+
* Each test is the contract: detection MUST NOT fire on the listed
|
|
30
|
+
* input. Failing means the pipeline still flags the input as a hit.
|
|
31
|
+
* The fix adds an entropy + length floor to env_key_value to match
|
|
32
|
+
* the existing kv_entropy precedent.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { describe, it, expect } from "vitest";
|
|
36
|
+
import { detectSecrets } from "../secret-detect/index.js";
|
|
37
|
+
|
|
38
|
+
describe("secret-detect — does NOT fire on casual mentions of 'secret' / 'token' / 'password'", () => {
|
|
39
|
+
// The "operator asking the agent about a secret" cases.
|
|
40
|
+
// Pre-fix: env_key_value matches "SECRET=" anywhere; these were
|
|
41
|
+
// tripping. Post-fix: entropy + length gate on the value.
|
|
42
|
+
it.each([
|
|
43
|
+
[
|
|
44
|
+
"operator asks for a secret by name",
|
|
45
|
+
"what's my fatsecret token?",
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
"operator references a deleted prior message",
|
|
49
|
+
"please delete that secret you sent earlier",
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
"agent name contains 'secret' as a substring (FatSecret API)",
|
|
53
|
+
"the FatSecret API needs an OAuth token — can you wire it up?",
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"human language sentence with 'password' as a noun",
|
|
57
|
+
"I keep forgetting my password again",
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
"fragment that mentions an env var by name but no value",
|
|
61
|
+
"the FATSECRET_TOKEN env var is missing",
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"code-shaped placeholder, value is human-readable English",
|
|
65
|
+
"set FOO_SECRET=hello and try again",
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"shell example with placeholder value (test fixture style)",
|
|
69
|
+
"run: export OPENAI_API_KEY=sk-yourkey",
|
|
70
|
+
],
|
|
71
|
+
])("%s — %j", (_label, text) => {
|
|
72
|
+
const hits = detectSecrets(text);
|
|
73
|
+
expect(
|
|
74
|
+
hits,
|
|
75
|
+
`false positive on ${JSON.stringify(text)} — hits=${JSON.stringify(hits.map((h) => h.matched_text))}`,
|
|
76
|
+
).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("secret-detect — DOES still fire on actually-shaped secrets (regression guard)", () => {
|
|
81
|
+
// After tightening env_key_value, these MUST still be caught.
|
|
82
|
+
// Locking in so a future regex-tightening doesn't over-shoot.
|
|
83
|
+
// Values constructed at runtime so the source file doesn't trip
|
|
84
|
+
// GitHub Push Protection — same pattern as
|
|
85
|
+
// secret-detect-secretlint.test.ts:1.
|
|
86
|
+
const fakeApiKey = `sk-ant-${"a1b2c3d4".repeat(4)}XYZ987`; // sk-ant- + 32 chars
|
|
87
|
+
const fakeBearer = `${"abc123".repeat(8)}.${"def456".repeat(4)}`;
|
|
88
|
+
const fakeRandom = `${"x9zM4kP3qR7sT2vW".repeat(2)}`; // 32 chars, high entropy
|
|
89
|
+
|
|
90
|
+
it.each([
|
|
91
|
+
[
|
|
92
|
+
"real-shaped Anthropic API key (anchored prefix path)",
|
|
93
|
+
`export ANTHROPIC_API_KEY=${fakeApiKey}`,
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
"real-shaped Bearer token (anchored Bearer path)",
|
|
97
|
+
`Authorization: Bearer ${fakeBearer}`,
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
"uppercase env_key_value with high-entropy value (must still match)",
|
|
101
|
+
`MYAPP_API_KEY=${fakeRandom}`,
|
|
102
|
+
],
|
|
103
|
+
])("%s — %j", (_label, text) => {
|
|
104
|
+
const hits = detectSecrets(text);
|
|
105
|
+
expect(
|
|
106
|
+
hits.length,
|
|
107
|
+
`regression: expected a hit on ${JSON.stringify(text)} but pipeline returned 0`,
|
|
108
|
+
).toBeGreaterThanOrEqual(1);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("secret-detect — boundary cases that pin the fix's shape", () => {
|
|
113
|
+
it("short low-entropy value after KEY= is NOT flagged", () => {
|
|
114
|
+
// pre-fix: matches; post-fix: filtered by entropy/length gate.
|
|
115
|
+
expect(detectSecrets("MY_API_KEY=foo")).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("English word after KEY= is NOT flagged", () => {
|
|
119
|
+
expect(detectSecrets("MY_TOKEN=hello")).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("long but low-entropy value after KEY= is NOT flagged (repeating chars)", () => {
|
|
123
|
+
// 32 chars but all 'a' — Shannon entropy ~0. Real secrets are
|
|
124
|
+
// dense; this is template/placeholder shape.
|
|
125
|
+
expect(detectSecrets(`MY_KEY=${"a".repeat(32)}`)).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("high-entropy value after KEY= IS flagged (the fix doesn't break detection)", () => {
|
|
129
|
+
// 32 chars of base64-ish noise. Should match.
|
|
130
|
+
const hits = detectSecrets(
|
|
131
|
+
// Build the value via concat to avoid Push-Protection trip on a
|
|
132
|
+
// contiguous secret-shaped literal.
|
|
133
|
+
`MY_API_KEY=${"k" + "9zMpQrT2vBxYuFnGwL8cHj"}`,
|
|
134
|
+
);
|
|
135
|
+
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
136
|
+
});
|
|
137
|
+
});
|