switchroom 0.17.6 → 0.18.3
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/bin/workspace-dynamic-hook.sh +12 -13
- package/dist/agent-scheduler/index.js +65 -5
- package/dist/auth-broker/index.js +6623 -514
- package/dist/cli/notion-write-pretool.mjs +64 -4
- package/dist/cli/switchroom.js +1888 -1162
- package/dist/host-control/main.js +6306 -162
- package/dist/vault/approvals/kernel-server.js +6014 -202
- package/dist/vault/broker/server.js +6741 -940
- package/package.json +1 -1
- package/profiles/_base/settings.json.hbs +2 -2
- package/profiles/_base/start.sh.hbs +218 -25
- package/profiles/coding/CLAUDE.md.hbs +1 -1
- package/profiles/default/CLAUDE.md +116 -0
- package/profiles/default/CLAUDE.md.hbs +2 -2
- package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
- package/profiles/health-coach/CLAUDE.md.hbs +1 -1
- package/skills/mental-model-curator/SKILL.md +162 -0
- package/telegram-plugin/auth-snapshot-format.ts +22 -24
- package/telegram-plugin/bridge/bridge.ts +80 -1
- package/telegram-plugin/bridge/ipc-client.ts +19 -0
- package/telegram-plugin/bridge/permission-ledger.ts +61 -0
- package/telegram-plugin/consolidation-legibility.ts +279 -0
- package/telegram-plugin/context-exhaustion.ts +124 -0
- package/telegram-plugin/dist/bridge/bridge.js +85 -1
- package/telegram-plugin/dist/gateway/gateway.js +25802 -8488
- package/telegram-plugin/dist/server.js +86 -2
- package/telegram-plugin/feed-heartbeat-climb.ts +206 -0
- package/telegram-plugin/gateway/activity-card-store.ts +369 -0
- package/telegram-plugin/gateway/gateway.ts +1861 -172
- package/telegram-plugin/gateway/inbound-delivery-gate.ts +26 -0
- package/telegram-plugin/gateway/inbound-spool.ts +22 -0
- package/telegram-plugin/gateway/mental-model-propose-card.ts +69 -0
- package/telegram-plugin/gateway/mental-model-propose-diff.ts +171 -0
- package/telegram-plugin/gateway/mental-model-propose-inbound-builders.ts +147 -0
- package/telegram-plugin/gateway/mental-model-propose-resolve.ts +201 -0
- package/telegram-plugin/gateway/missed-approvals-card.ts +161 -0
- package/telegram-plugin/gateway/missed-approvals-store.ts +167 -0
- package/telegram-plugin/gateway/model-command.ts +70 -10
- package/telegram-plugin/gateway/permission-rearm.ts +115 -0
- package/telegram-plugin/gateway/scoped-grant-store.ts +89 -0
- package/telegram-plugin/memory-legibility.ts +217 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +6 -0
- package/telegram-plugin/quota-watch.ts +4 -6
- package/telegram-plugin/registry/turns-schema.test.ts +97 -0
- package/telegram-plugin/registry/turns-schema.ts +78 -0
- package/telegram-plugin/render/ir.ts +209 -0
- package/telegram-plugin/render/parse.ts +363 -0
- package/telegram-plugin/render/render.ts +440 -0
- package/telegram-plugin/render/rich-render.ts +72 -0
- package/telegram-plugin/scoped-approval.ts +59 -0
- package/telegram-plugin/silent-end.ts +78 -0
- package/telegram-plugin/stream-controller.ts +14 -3
- package/telegram-plugin/subagent-watcher.ts +60 -6
- package/telegram-plugin/tests/activity-card-store.test.ts +530 -0
- package/telegram-plugin/tests/activity-card-wiring.test.ts +88 -0
- package/telegram-plugin/tests/auth-command-format2.test.ts +1 -1
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +30 -16
- package/telegram-plugin/tests/claude-code-event-contract.test.ts +48 -0
- package/telegram-plugin/tests/consolidation-legibility.test.ts +224 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +25 -10
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +44 -9
- package/telegram-plugin/tests/feed-survival.test.ts +39 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +81 -0
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +82 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +105 -0
- package/telegram-plugin/tests/liveness-tracker.test.ts +228 -0
- package/telegram-plugin/tests/memory-legibility.test.ts +216 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +67 -0
- package/telegram-plugin/tests/mental-model-propose-card.test.ts +56 -0
- package/telegram-plugin/tests/mental-model-propose-diff.test.ts +201 -0
- package/telegram-plugin/tests/mental-model-propose-inbound-builders.test.ts +68 -0
- package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +157 -0
- package/telegram-plugin/tests/missed-approvals-card.test.ts +145 -0
- package/telegram-plugin/tests/missed-approvals-store.test.ts +147 -0
- package/telegram-plugin/tests/missed-approvals-wiring.test.ts +89 -0
- package/telegram-plugin/tests/model-command.test.ts +193 -16
- package/telegram-plugin/tests/narrative-render.test.ts +125 -0
- package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +123 -163
- package/telegram-plugin/tests/permission-ledger.test.ts +166 -0
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +1 -1
- package/telegram-plugin/tests/permission-rearm-wiring.test.ts +175 -0
- package/telegram-plugin/tests/permission-rearm.test.ts +126 -0
- package/telegram-plugin/tests/quota-watch.test.ts +1 -4
- package/telegram-plugin/tests/rapid-fire-delivery-ordering.test.ts +149 -0
- package/telegram-plugin/tests/render/parse-torture.test.ts +136 -0
- package/telegram-plugin/tests/render/parse.test.ts +393 -0
- package/telegram-plugin/tests/render/render.test.ts +436 -0
- package/telegram-plugin/tests/render/rich-render.test.ts +85 -0
- package/telegram-plugin/tests/scoped-grant-persist.test.ts +223 -0
- package/telegram-plugin/tests/silent-end-transport.test.ts +290 -0
- package/telegram-plugin/tests/silent-turn-climb-transport.test.ts +337 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +139 -0
- package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +155 -1
- package/telegram-plugin/tests/worktree-watch-cwds.test.ts +198 -0
- package/telegram-plugin/turn-liveness-floor.ts +35 -1
- package/telegram-plugin/uat/assertions.ts +88 -4
- package/telegram-plugin/uat/feed-matcher.test.ts +69 -0
- package/telegram-plugin/uat/scenarios/fuzz-liveness-climb-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/jtbd-directive-capture-nudge-dm.test.ts +185 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-channel.test.ts +192 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-dm.test.ts +220 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +137 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +148 -0
- package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-channel.test.ts +66 -0
- package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-dm.test.ts +61 -0
- package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +99 -7
- package/telegram-plugin/uat/scenarios/silent-end-recovery-channel.test.ts +136 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +24 -2
- package/telegram-plugin/worktree-watch-cwds.ts +135 -0
- package/vendor/hindsight-memory/hooks/hooks.json +9 -0
- package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +445 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/client.py +11 -1
- package/vendor/hindsight-memory/scripts/lib/config.py +46 -2
- package/vendor/hindsight-memory/scripts/lib/directives.py +88 -0
- package/vendor/hindsight-memory/scripts/lib/switchroom_envelope.py +77 -0
- package/vendor/hindsight-memory/scripts/recall.py +217 -10
- package/vendor/hindsight-memory/scripts/retain.py +17 -0
- package/vendor/hindsight-memory/scripts/setup_hooks.py +9 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_capture_nudge.py +185 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +516 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +49 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +1 -0
- package/vendor/hindsight-memory/scripts/tests/test_retain_window.py +66 -1
- package/vendor/hindsight-memory/scripts/tests/test_switchroom_envelope.py +69 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/test_client.py +43 -0
- package/vendor/hindsight-memory/tests/test_recall_exit_codes.py +49 -2
- package/vendor/hindsight-memory/tests/test_recall_precision.py +114 -0
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for the
|
|
2
|
+
* Unit tests for the orphaned-reply backstop, repointed at the REAL
|
|
3
|
+
* LivenessTracker (the thinking-pause fix seam) — no replica decision helpers.
|
|
3
4
|
*
|
|
4
5
|
* Root cause: the orphaned-reply backstop fired a synthetic turn_end
|
|
5
|
-
* (`durationMs: -1`) after 30 s of silence, even mid-
|
|
6
|
+
* (`durationMs: -1`) after 30 s of silence, even mid-work. That nulled
|
|
6
7
|
* `currentTurn` and dropped every subsequent `tool_label`, darkening the live
|
|
7
8
|
* activity feed for the rest of the turn.
|
|
8
9
|
*
|
|
9
|
-
* Fix
|
|
10
|
-
* PRIMARY — fuse fires mid-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
10
|
+
* Fix layers (all now expressed through `LivenessTracker`):
|
|
11
|
+
* PRIMARY — the fuse fires mid-work → re-arm instead. `decideOnExpiry`
|
|
12
|
+
* re-arms while working OR recently-streaming OR human-waiting,
|
|
13
|
+
* bounded by ORPHANED_REPLY_MAX_REARMS for the working /
|
|
14
|
+
* recently-streaming cases (human-wait is uncapped).
|
|
15
|
+
* SECONDARY — a genuine stream event (`onStreamEvent`) re-stamps liveness and
|
|
16
|
+
* zeroes the rearm counter, so an actively-progressing turn never
|
|
17
|
+
* hits the cap.
|
|
18
|
+
* DEFENSIVE — the gateway rejects a synthetic turn_end while the turn is
|
|
19
|
+
* legitimately working OR recently streaming (`recentlyStreaming`).
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
import { describe, it, expect } from 'vitest'
|
|
@@ -23,37 +24,24 @@ import {
|
|
|
23
24
|
shouldArmOrphanedReplyTimeout,
|
|
24
25
|
ORPHANED_REPLY_TIMEOUT_MS,
|
|
25
26
|
ORPHANED_REPLY_MAX_REARMS,
|
|
27
|
+
ORPHANED_REPLY_STREAM_WINDOW_MS,
|
|
28
|
+
LivenessTracker,
|
|
26
29
|
} from '../context-exhaustion.js'
|
|
27
30
|
import { ToolFlightTracker } from '../gateway/interrupt-defer.js'
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// These extract the discriminable parts of the fix so they are unit-testable
|
|
32
|
-
// without instantiating the full gateway.
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Mirrors the PRIMARY fix decision inside the setTimeout callback:
|
|
37
|
-
* should the backstop re-arm (true) or fire turn_end (false)?
|
|
38
|
-
*/
|
|
39
|
-
function shouldRearmInsteadOfFire(opts: {
|
|
40
|
-
midToolCall: boolean
|
|
41
|
-
rearmCount: number
|
|
42
|
-
maxRearms: number
|
|
43
|
-
}): boolean {
|
|
44
|
-
return opts.midToolCall && opts.rearmCount < opts.maxRearms
|
|
45
|
-
}
|
|
32
|
+
const W = ORPHANED_REPLY_STREAM_WINDOW_MS
|
|
33
|
+
const MAX = ORPHANED_REPLY_MAX_REARMS
|
|
46
34
|
|
|
47
35
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
36
|
+
* A tracker whose last stream event is far enough in the past that
|
|
37
|
+
* `recentlyStreaming` is false at `now`, so the ONLY thing keeping the turn
|
|
38
|
+
* alive in a `decideOnExpiry` call is the `working` / `humanWaiting` inputs.
|
|
39
|
+
* Lets us test the "no liveness except the tool" decisions in isolation.
|
|
50
40
|
*/
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
midToolCall: boolean
|
|
54
|
-
}): boolean {
|
|
55
|
-
return opts.durationMs === -1 && opts.midToolCall
|
|
41
|
+
function staleTracker(): LivenessTracker {
|
|
42
|
+
return new LivenessTracker(0)
|
|
56
43
|
}
|
|
44
|
+
const STALE_NOW = W + 10_000 // gap > window → recentlyStreaming false
|
|
57
45
|
|
|
58
46
|
// ---------------------------------------------------------------------------
|
|
59
47
|
// Tests: ORPHANED_REPLY_MAX_REARMS constant
|
|
@@ -66,7 +54,6 @@ describe('ORPHANED_REPLY_MAX_REARMS', () => {
|
|
|
66
54
|
|
|
67
55
|
it('combined with ORPHANED_REPLY_TIMEOUT_MS covers at least 10 min of tool activity', () => {
|
|
68
56
|
const coverageMs = ORPHANED_REPLY_MAX_REARMS * ORPHANED_REPLY_TIMEOUT_MS
|
|
69
|
-
// 20 × 30 000 ms = 600 000 ms = 10 min
|
|
70
57
|
expect(coverageMs).toBeGreaterThanOrEqual(10 * 60 * 1000)
|
|
71
58
|
})
|
|
72
59
|
|
|
@@ -76,165 +63,138 @@ describe('ORPHANED_REPLY_MAX_REARMS', () => {
|
|
|
76
63
|
})
|
|
77
64
|
|
|
78
65
|
// ---------------------------------------------------------------------------
|
|
79
|
-
// Tests: PRIMARY fix — re-arm
|
|
66
|
+
// Tests: PRIMARY fix — re-arm via decideOnExpiry (REAL tracker)
|
|
80
67
|
// ---------------------------------------------------------------------------
|
|
81
68
|
|
|
82
|
-
describe('PRIMARY fix: re-arm guard (
|
|
83
|
-
it('re-arms
|
|
84
|
-
|
|
85
|
-
|
|
69
|
+
describe('PRIMARY fix: re-arm guard (LivenessTracker.decideOnExpiry)', () => {
|
|
70
|
+
it('re-arms while working and under the cap (recentlyStreaming false)', () => {
|
|
71
|
+
const t = staleTracker()
|
|
72
|
+
const d = t.decideOnExpiry({ working: true, humanWaiting: false, now: STALE_NOW, windowMs: W, maxRearms: MAX })
|
|
73
|
+
expect(d).toEqual({ rearm: true, countsAgainstCap: true })
|
|
86
74
|
})
|
|
87
75
|
|
|
88
|
-
it('fires once rearm count reaches the cap
|
|
89
|
-
|
|
90
|
-
|
|
76
|
+
it('fires once the working rearm count reaches the cap (recentlyStreaming false)', () => {
|
|
77
|
+
const t = staleTracker()
|
|
78
|
+
let last: { rearm: boolean } | undefined
|
|
79
|
+
for (let k = 0; k <= MAX; k++) {
|
|
80
|
+
last = t.decideOnExpiry({ working: true, humanWaiting: false, now: STALE_NOW, windowMs: W, maxRearms: MAX })
|
|
81
|
+
}
|
|
82
|
+
// MAX re-arms proceeded (counter 1..MAX); the (MAX+1)th fires.
|
|
83
|
+
expect(last!.rearm).toBe(false)
|
|
84
|
+
expect(t.orphanedReplyRearmCount).toBe(MAX)
|
|
91
85
|
})
|
|
92
86
|
|
|
93
|
-
it('fires immediately when
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
it('fires immediately when nothing keeps the turn alive (idle, not recently streaming)', () => {
|
|
88
|
+
const t = staleTracker()
|
|
89
|
+
const d = t.decideOnExpiry({ working: false, humanWaiting: false, now: STALE_NOW, windowMs: W, maxRearms: MAX })
|
|
90
|
+
expect(d).toEqual({ rearm: false, countsAgainstCap: false })
|
|
96
91
|
})
|
|
97
92
|
|
|
98
|
-
it('
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
// At exactly the cap: fire
|
|
104
|
-
expect(shouldRearmInsteadOfFire({ midToolCall: true, rearmCount: max, maxRearms: max })).toBe(false)
|
|
93
|
+
it('re-arms while recently streaming even when NOT working (thinking-pause survival)', () => {
|
|
94
|
+
const t = new LivenessTracker(0)
|
|
95
|
+
t.onStreamEvent('text', undefined, 0)
|
|
96
|
+
const d = t.decideOnExpiry({ working: false, humanWaiting: false, now: 30_000, windowMs: W, maxRearms: MAX })
|
|
97
|
+
expect(d).toEqual({ rearm: true, countsAgainstCap: true })
|
|
105
98
|
})
|
|
106
99
|
})
|
|
107
100
|
|
|
108
101
|
// ---------------------------------------------------------------------------
|
|
109
|
-
// Tests: DEFENSIVE fix — synthetic turn_end
|
|
102
|
+
// Tests: DEFENSIVE fix — recentlyStreaming keeps the synthetic turn_end at bay
|
|
110
103
|
// ---------------------------------------------------------------------------
|
|
111
104
|
|
|
112
|
-
describe('DEFENSIVE fix:
|
|
113
|
-
it('
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// No tools → the backstop should fire normally (turn is genuinely orphaned)
|
|
119
|
-
expect(shouldSuppressSyntheticTurnEnd({ durationMs: -1, midToolCall: false })).toBe(false)
|
|
105
|
+
describe('DEFENSIVE fix: recentlyStreaming suppression window', () => {
|
|
106
|
+
it('is true within the window of the last genuine stream event', () => {
|
|
107
|
+
const t = new LivenessTracker(0)
|
|
108
|
+
t.onStreamEvent('tool_use', undefined, 5_000)
|
|
109
|
+
expect(t.recentlyStreaming(5_000, W)).toBe(true)
|
|
110
|
+
expect(t.recentlyStreaming(5_000 + W - 1, W)).toBe(true)
|
|
120
111
|
})
|
|
121
112
|
|
|
122
|
-
it('
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
expect(
|
|
126
|
-
expect(shouldSuppressSyntheticTurnEnd({ durationMs: 0, midToolCall: false })).toBe(false)
|
|
113
|
+
it('is false once the window lapses (genuine hang, teardown proceeds)', () => {
|
|
114
|
+
const t = new LivenessTracker(0)
|
|
115
|
+
t.onStreamEvent('tool_use', undefined, 5_000)
|
|
116
|
+
expect(t.recentlyStreaming(5_000 + W, W)).toBe(false)
|
|
127
117
|
})
|
|
128
118
|
|
|
129
|
-
it('
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
119
|
+
it('a synthetic turn_end (-1) does not extend the window', () => {
|
|
120
|
+
const t = new LivenessTracker(0)
|
|
121
|
+
t.onStreamEvent('tool_use', undefined, 5_000)
|
|
122
|
+
t.onStreamEvent('turn_end', -1, 5_500) // ignored
|
|
123
|
+
expect(t.lastStreamEventAt).toBe(5_000)
|
|
124
|
+
expect(t.recentlyStreaming(5_000 + W, W)).toBe(false)
|
|
133
125
|
})
|
|
134
126
|
})
|
|
135
127
|
|
|
136
128
|
// ---------------------------------------------------------------------------
|
|
137
|
-
// Tests: ToolFlightTracker
|
|
129
|
+
// Tests: ToolFlightTracker → working input → decideOnExpiry
|
|
138
130
|
// ---------------------------------------------------------------------------
|
|
139
131
|
|
|
140
|
-
describe('ToolFlightTracker
|
|
141
|
-
it('re-arm fires when a Bash tool is in flight', () => {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
expect(shouldSuppressSyntheticTurnEnd({
|
|
169
|
-
durationMs: -1,
|
|
170
|
-
midToolCall: tracker.isMidToolCall(),
|
|
171
|
-
})).toBe(true)
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('defensive guard allows synthetic turn_end after all tools complete', () => {
|
|
175
|
-
const tracker = new ToolFlightTracker()
|
|
176
|
-
tracker.onEvent({ kind: 'tool_use', toolUseId: 'bash_2' })
|
|
177
|
-
tracker.onEvent({ kind: 'tool_result', toolUseId: 'bash_2' })
|
|
178
|
-
|
|
179
|
-
expect(shouldSuppressSyntheticTurnEnd({
|
|
180
|
-
durationMs: -1,
|
|
181
|
-
midToolCall: tracker.isMidToolCall(),
|
|
182
|
-
})).toBe(false)
|
|
132
|
+
describe('ToolFlightTracker drives the `working` input', () => {
|
|
133
|
+
it('re-arm fires when a Bash tool is in flight (working=true)', () => {
|
|
134
|
+
const flight = new ToolFlightTracker()
|
|
135
|
+
flight.onEvent({ kind: 'tool_use', toolUseId: 'bash_1' })
|
|
136
|
+
const t = staleTracker()
|
|
137
|
+
const d = t.decideOnExpiry({
|
|
138
|
+
working: flight.isMidToolCall(),
|
|
139
|
+
humanWaiting: false,
|
|
140
|
+
now: STALE_NOW,
|
|
141
|
+
windowMs: W,
|
|
142
|
+
maxRearms: MAX,
|
|
143
|
+
})
|
|
144
|
+
expect(d.rearm).toBe(true)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('fires normally after tool_result completes the tool (working=false, not recently streaming)', () => {
|
|
148
|
+
const flight = new ToolFlightTracker()
|
|
149
|
+
flight.onEvent({ kind: 'tool_use', toolUseId: 'bash_1' })
|
|
150
|
+
flight.onEvent({ kind: 'tool_result', toolUseId: 'bash_1' })
|
|
151
|
+
const t = staleTracker()
|
|
152
|
+
const d = t.decideOnExpiry({
|
|
153
|
+
working: flight.isMidToolCall(),
|
|
154
|
+
humanWaiting: false,
|
|
155
|
+
now: STALE_NOW,
|
|
156
|
+
windowMs: W,
|
|
157
|
+
maxRearms: MAX,
|
|
158
|
+
})
|
|
159
|
+
expect(d.rearm).toBe(false)
|
|
183
160
|
})
|
|
184
161
|
|
|
185
162
|
it('parallel tools: re-arm persists while ANY tool is in flight', () => {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
rearmCount: 0,
|
|
195
|
-
maxRearms: ORPHANED_REPLY_MAX_REARMS,
|
|
196
|
-
})).toBe(true)
|
|
197
|
-
|
|
198
|
-
// Two complete
|
|
199
|
-
tracker.onEvent({ kind: 'tool_result', toolUseId: 'read_1' })
|
|
200
|
-
tracker.onEvent({ kind: 'tool_result', toolUseId: 'read_2' })
|
|
201
|
-
|
|
202
|
-
// Still re-arming: edit_1 open
|
|
203
|
-
expect(shouldRearmInsteadOfFire({
|
|
204
|
-
midToolCall: tracker.isMidToolCall(),
|
|
205
|
-
rearmCount: 1,
|
|
206
|
-
maxRearms: ORPHANED_REPLY_MAX_REARMS,
|
|
207
|
-
})).toBe(true)
|
|
163
|
+
const flight = new ToolFlightTracker()
|
|
164
|
+
flight.onEvent({ kind: 'tool_use', toolUseId: 'read_1' })
|
|
165
|
+
flight.onEvent({ kind: 'tool_use', toolUseId: 'read_2' })
|
|
166
|
+
flight.onEvent({ kind: 'tool_use', toolUseId: 'edit_1' })
|
|
167
|
+
const t = staleTracker()
|
|
168
|
+
expect(
|
|
169
|
+
t.decideOnExpiry({ working: flight.isMidToolCall(), humanWaiting: false, now: STALE_NOW, windowMs: W, maxRearms: MAX }).rearm,
|
|
170
|
+
).toBe(true)
|
|
208
171
|
|
|
209
|
-
|
|
210
|
-
|
|
172
|
+
flight.onEvent({ kind: 'tool_result', toolUseId: 'read_1' })
|
|
173
|
+
flight.onEvent({ kind: 'tool_result', toolUseId: 'read_2' })
|
|
174
|
+
expect(
|
|
175
|
+
t.decideOnExpiry({ working: flight.isMidToolCall(), humanWaiting: false, now: STALE_NOW, windowMs: W, maxRearms: MAX }).rearm,
|
|
176
|
+
).toBe(true) // edit_1 still open
|
|
211
177
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
maxRearms:
|
|
216
|
-
|
|
178
|
+
flight.onEvent({ kind: 'tool_result', toolUseId: 'edit_1' })
|
|
179
|
+
const t2 = staleTracker() // fresh so the earlier rearms don't confound the cap
|
|
180
|
+
expect(
|
|
181
|
+
t2.decideOnExpiry({ working: flight.isMidToolCall(), humanWaiting: false, now: STALE_NOW, windowMs: W, maxRearms: MAX }).rearm,
|
|
182
|
+
).toBe(false) // all complete, not recently streaming → fire
|
|
217
183
|
})
|
|
218
184
|
|
|
219
185
|
it('cap fires even mid-tool after 20 re-arms (wedged tool surfaces)', () => {
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
rearmCount: i,
|
|
228
|
-
maxRearms: ORPHANED_REPLY_MAX_REARMS,
|
|
229
|
-
})).toBe(true)
|
|
186
|
+
const flight = new ToolFlightTracker()
|
|
187
|
+
flight.onEvent({ kind: 'tool_use', toolUseId: 'hung_bash' })
|
|
188
|
+
const t = staleTracker()
|
|
189
|
+
for (let i = 0; i < MAX; i++) {
|
|
190
|
+
expect(
|
|
191
|
+
t.decideOnExpiry({ working: flight.isMidToolCall(), humanWaiting: false, now: STALE_NOW, windowMs: W, maxRearms: MAX }).rearm,
|
|
192
|
+
).toBe(true)
|
|
230
193
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
rearmCount: ORPHANED_REPLY_MAX_REARMS,
|
|
236
|
-
maxRearms: ORPHANED_REPLY_MAX_REARMS,
|
|
237
|
-
})).toBe(false)
|
|
194
|
+
// Cap exceeded — fire despite in-flight.
|
|
195
|
+
expect(
|
|
196
|
+
t.decideOnExpiry({ working: flight.isMidToolCall(), humanWaiting: false, now: STALE_NOW, windowMs: W, maxRearms: MAX }).rearm,
|
|
197
|
+
).toBe(false)
|
|
238
198
|
})
|
|
239
199
|
})
|
|
240
200
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the bridge-side outstanding permission-request ledger
|
|
3
|
+
* (#2861 / #2859).
|
|
4
|
+
*
|
|
5
|
+
* The ledger is the primitive that closes R2 (bridge dropped permission
|
|
6
|
+
* requests while the gateway IPC was down). bridge.ts wires it as:
|
|
7
|
+
* - permission_request notification → ledger.add(params) (BEFORE the
|
|
8
|
+
* connect check), then send if connected else buffer.
|
|
9
|
+
* - onPermission (verdict delivered) → ledger.delete(requestId).
|
|
10
|
+
* - onConnect (IPC (re)connect) → re-send ledger.all().
|
|
11
|
+
*
|
|
12
|
+
* bridge.ts has top-level side effects and isn't unit-importable, so here we
|
|
13
|
+
* (a) unit-test the ledger primitive directly and (b) drive a faithful
|
|
14
|
+
* re-implementation of the three wiring points against a fake IPC to prove the
|
|
15
|
+
* buffer / re-send / delete behaviours. The source wiring itself is pinned in
|
|
16
|
+
* permission-rearm-wiring.test.ts.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect } from 'vitest'
|
|
20
|
+
import {
|
|
21
|
+
createOutstandingPermissionLedger,
|
|
22
|
+
type OutstandingPermissionParams,
|
|
23
|
+
} from '../bridge/permission-ledger.js'
|
|
24
|
+
|
|
25
|
+
function params(requestId: string, tool = 'mcp__brevo__post'): OutstandingPermissionParams {
|
|
26
|
+
return {
|
|
27
|
+
request_id: requestId,
|
|
28
|
+
tool_name: tool,
|
|
29
|
+
description: `${tool} description`,
|
|
30
|
+
input_preview: '{"foo":"bar"}',
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('createOutstandingPermissionLedger', () => {
|
|
35
|
+
it('starts empty', () => {
|
|
36
|
+
const led = createOutstandingPermissionLedger()
|
|
37
|
+
expect(led.size).toBe(0)
|
|
38
|
+
expect(led.all()).toEqual([])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('add + all + has', () => {
|
|
42
|
+
const led = createOutstandingPermissionLedger()
|
|
43
|
+
led.add(params('aaaaa'))
|
|
44
|
+
led.add(params('bbbbb'))
|
|
45
|
+
expect(led.size).toBe(2)
|
|
46
|
+
expect(led.has('aaaaa')).toBe(true)
|
|
47
|
+
expect(led.has('zzzzz')).toBe(false)
|
|
48
|
+
expect(led.all().map(p => p.request_id)).toEqual(['aaaaa', 'bbbbb'])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('add is idempotent on request_id (latest params win)', () => {
|
|
52
|
+
const led = createOutstandingPermissionLedger()
|
|
53
|
+
led.add(params('aaaaa', 'mcp__brevo__post'))
|
|
54
|
+
led.add(params('aaaaa', 'mcp__brevo__patch'))
|
|
55
|
+
expect(led.size).toBe(1)
|
|
56
|
+
expect(led.all()[0].tool_name).toBe('mcp__brevo__patch')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('delete removes the entry (verdict delivered)', () => {
|
|
60
|
+
const led = createOutstandingPermissionLedger()
|
|
61
|
+
led.add(params('aaaaa'))
|
|
62
|
+
led.delete('aaaaa')
|
|
63
|
+
expect(led.size).toBe(0)
|
|
64
|
+
expect(led.has('aaaaa')).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('delete of an unknown id is a no-op', () => {
|
|
68
|
+
const led = createOutstandingPermissionLedger()
|
|
69
|
+
led.add(params('aaaaa'))
|
|
70
|
+
led.delete('nope')
|
|
71
|
+
expect(led.size).toBe(1)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Faithful re-implementation of the three bridge wiring points. Mirrors
|
|
77
|
+
// bridge.ts exactly so the buffer / re-send / delete contract is exercised
|
|
78
|
+
// without importing the side-effecting module.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
interface FakeIpc {
|
|
82
|
+
connected: boolean
|
|
83
|
+
sent: Array<{ requestId: string }>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function makeHarness(rearmEnabled = true) {
|
|
87
|
+
const ledger = createOutstandingPermissionLedger()
|
|
88
|
+
const ipc: FakeIpc = { connected: false, sent: [] }
|
|
89
|
+
|
|
90
|
+
// Mirrors the permission_request notification handler.
|
|
91
|
+
function onPermissionRequest(p: OutstandingPermissionParams): void {
|
|
92
|
+
if (rearmEnabled) ledger.add(p)
|
|
93
|
+
if (!ipc.connected) {
|
|
94
|
+
// BUFFER (not drop) when re-arm enabled.
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
ipc.sent.push({ requestId: p.request_id })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Mirrors onPermission (verdict delivered).
|
|
101
|
+
function onVerdict(requestId: string): void {
|
|
102
|
+
ledger.delete(requestId)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Mirrors flushOutstandingPermissionRequests on onConnect.
|
|
106
|
+
function onConnect(): void {
|
|
107
|
+
ipc.connected = true
|
|
108
|
+
if (!rearmEnabled) return
|
|
109
|
+
for (const p of ledger.all()) ipc.sent.push({ requestId: p.request_id })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function disconnect(): void {
|
|
113
|
+
ipc.connected = false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { ledger, ipc, onPermissionRequest, onVerdict, onConnect, disconnect }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
describe('bridge permission-request wiring (buffer / re-send / delete)', () => {
|
|
120
|
+
it('buffers when disconnected — no drop, request retained', () => {
|
|
121
|
+
const h = makeHarness()
|
|
122
|
+
h.onPermissionRequest(params('aaaaa'))
|
|
123
|
+
// Nothing sent (gateway down) but the ledger retained it.
|
|
124
|
+
expect(h.ipc.sent).toEqual([])
|
|
125
|
+
expect(h.ledger.has('aaaaa')).toBe(true)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('re-sends every outstanding request on (re)connect', () => {
|
|
129
|
+
const h = makeHarness()
|
|
130
|
+
h.onPermissionRequest(params('aaaaa'))
|
|
131
|
+
h.onPermissionRequest(params('bbbbb'))
|
|
132
|
+
expect(h.ipc.sent).toEqual([]) // still down
|
|
133
|
+
|
|
134
|
+
h.onConnect() // gateway comes up
|
|
135
|
+
expect(h.ipc.sent.map(s => s.requestId).sort()).toEqual(['aaaaa', 'bbbbb'])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('re-sends again after a disconnect/reconnect cycle (gateway restart)', () => {
|
|
139
|
+
const h = makeHarness()
|
|
140
|
+
h.onConnect() // connected
|
|
141
|
+
h.onPermissionRequest(params('aaaaa')) // sent live
|
|
142
|
+
expect(h.ipc.sent.map(s => s.requestId)).toEqual(['aaaaa'])
|
|
143
|
+
|
|
144
|
+
h.disconnect() // gateway restarts
|
|
145
|
+
h.onConnect() // reconnect → re-send outstanding
|
|
146
|
+
expect(h.ipc.sent.map(s => s.requestId)).toEqual(['aaaaa', 'aaaaa'])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('a delivered verdict removes the entry — no re-send after resolution', () => {
|
|
150
|
+
const h = makeHarness()
|
|
151
|
+
h.onPermissionRequest(params('aaaaa'))
|
|
152
|
+
h.onVerdict('aaaaa') // operator tapped, verdict delivered
|
|
153
|
+
expect(h.ledger.has('aaaaa')).toBe(false)
|
|
154
|
+
|
|
155
|
+
h.onConnect() // reconnect
|
|
156
|
+
expect(h.ipc.sent).toEqual([]) // nothing re-sent
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('kill switch: disabled → drop-when-disconnected, no ledger, no re-send', () => {
|
|
160
|
+
const h = makeHarness(false)
|
|
161
|
+
h.onPermissionRequest(params('aaaaa')) // disconnected → dropped, not recorded
|
|
162
|
+
expect(h.ledger.size).toBe(0)
|
|
163
|
+
h.onConnect()
|
|
164
|
+
expect(h.ipc.sent).toEqual([]) // nothing to re-send
|
|
165
|
+
})
|
|
166
|
+
})
|
|
@@ -58,7 +58,7 @@ describe('no-repeat-on-timeout wiring', () => {
|
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
it('onPermissionRequest short-circuits a recent-timeout duplicate before posting a card', () => {
|
|
61
|
-
const fn = slice(GATEWAY, 'onPermissionRequest(',
|
|
61
|
+
const fn = slice(GATEWAY, 'onPermissionRequest(', 6000)
|
|
62
62
|
const dupIdx = fn.indexOf('isRecentTimeoutDuplicate(')
|
|
63
63
|
const cardIdx = fn.indexOf('pendingPermissions.set(requestId')
|
|
64
64
|
expect(dupIdx).toBeGreaterThan(-1)
|