@wenrwa/marketplace-cli 0.4.5 → 0.4.6
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/dist/__tests__/bridge-activity-parser.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-parser.test.js +0 -0
- package/dist/__tests__/bridge-activity-tap.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-tap.test.js +178 -0
- package/dist/__tests__/bridge-api-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-api-client.test.js +69 -0
- package/dist/__tests__/bridge-browser-driver.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-driver.test.js +379 -0
- package/dist/__tests__/bridge-browser-frames.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-frames.test.js +166 -0
- package/dist/__tests__/bridge-browser-idle.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-idle.test.js +259 -0
- package/dist/__tests__/bridge-browser-relay.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-relay.test.js +670 -0
- package/dist/__tests__/bridge-bundle-installer.test.d.ts +1 -0
- package/dist/__tests__/bridge-bundle-installer.test.js +126 -0
- package/dist/__tests__/bridge-cdp-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-cdp-client.test.js +133 -0
- package/dist/__tests__/bridge-chromium-launcher.test.d.ts +1 -0
- package/dist/__tests__/bridge-chromium-launcher.test.js +202 -0
- package/dist/__tests__/bridge-command-browser-options.test.d.ts +1 -0
- package/dist/__tests__/bridge-command-browser-options.test.js +37 -0
- package/dist/__tests__/bridge-config-mcp.test.js +4 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.js +125 -0
- package/dist/__tests__/bridge-context-writer.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-writer.test.js +161 -0
- package/dist/__tests__/bridge-device-registration.test.d.ts +1 -0
- package/dist/__tests__/bridge-device-registration.test.js +33 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.js +37 -0
- package/dist/__tests__/bridge-folder-picker.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker.test.js +66 -0
- package/dist/__tests__/bridge-folder-registry.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-registry.test.js +107 -0
- package/dist/__tests__/bridge-frame-pacer.test.d.ts +1 -0
- package/dist/__tests__/bridge-frame-pacer.test.js +102 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.js +107 -0
- package/dist/__tests__/bridge-local-add-core.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-add-core.test.js +48 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.js +88 -0
- package/dist/__tests__/bridge-local-path-validation.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation.test.js +45 -0
- package/dist/__tests__/bridge-login-status.test.d.ts +1 -0
- package/dist/__tests__/bridge-login-status.test.js +60 -0
- package/dist/__tests__/bridge-relay-pucc14.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-pucc14.test.js +230 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.js +197 -0
- package/dist/__tests__/bridge-server.test.js +22 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.d.ts +1 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.js +317 -0
- package/dist/__tests__/devices.test.d.ts +1 -0
- package/dist/__tests__/devices.test.js +66 -0
- package/dist/__tests__/local.test.d.ts +1 -0
- package/dist/__tests__/local.test.js +147 -0
- package/dist/bridge/activityParser.d.ts +53 -0
- package/dist/bridge/activityParser.js +202 -0
- package/dist/bridge/apiClient.d.ts +56 -0
- package/dist/bridge/apiClient.js +50 -0
- package/dist/bridge/browserDriver.d.ts +98 -0
- package/dist/bridge/browserDriver.js +265 -0
- package/dist/bridge/browserFrames.d.ts +182 -0
- package/dist/bridge/browserFrames.js +179 -0
- package/dist/bridge/browserIdle.d.ts +75 -0
- package/dist/bridge/browserIdle.js +120 -0
- package/dist/bridge/bundleInstaller.d.ts +31 -0
- package/dist/bridge/bundleInstaller.js +127 -0
- package/dist/bridge/cdpClient.d.ts +47 -0
- package/dist/bridge/cdpClient.js +102 -0
- package/dist/bridge/chromiumLauncher.d.ts +45 -0
- package/dist/bridge/chromiumLauncher.js +113 -0
- package/dist/bridge/config.d.ts +3 -0
- package/dist/bridge/contextWriter.d.ts +31 -0
- package/dist/bridge/contextWriter.js +139 -0
- package/dist/bridge/deviceRegistration.d.ts +28 -0
- package/dist/bridge/deviceRegistration.js +19 -0
- package/dist/bridge/folderPicker.d.ts +34 -0
- package/dist/bridge/folderPicker.js +36 -0
- package/dist/bridge/folderPickerRunner.d.ts +3 -0
- package/dist/bridge/folderPickerRunner.js +25 -0
- package/dist/bridge/folderRegistry.d.ts +19 -0
- package/dist/bridge/folderRegistry.js +100 -0
- package/dist/bridge/framePacer.d.ts +61 -0
- package/dist/bridge/framePacer.js +79 -0
- package/dist/bridge/localAdd.d.ts +28 -0
- package/dist/bridge/localAdd.js +67 -0
- package/dist/bridge/localPathValidation.d.ts +20 -0
- package/dist/bridge/localPathValidation.js +58 -0
- package/dist/bridge/localPathValidationFs.d.ts +2 -0
- package/dist/bridge/localPathValidationFs.js +98 -0
- package/dist/bridge/loginStatus.d.ts +38 -0
- package/dist/bridge/loginStatus.js +56 -0
- package/dist/bridge/realPty.js +6 -2
- package/dist/bridge/relayClient.d.ts +124 -0
- package/dist/bridge/relayClient.js +368 -5
- package/dist/bridge/repoCheckout.d.ts +31 -0
- package/dist/bridge/repoCheckout.js +18 -0
- package/dist/bridge/server.d.ts +33 -0
- package/dist/bridge/server.js +98 -10
- package/dist/bridge/sessionManager.d.ts +18 -0
- package/dist/bridge/sessionManager.js +15 -1
- package/dist/commands/bridge.d.ts +6 -0
- package/dist/commands/bridge.js +155 -1
- package/dist/commands/devices.d.ts +9 -0
- package/dist/commands/devices.js +53 -0
- package/dist/commands/local.d.ts +9 -0
- package/dist/commands/local.js +141 -0
- package/dist/index.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* cbs-01 — v1 cloud browser session frame protocol (types + guards only).
|
|
5
|
+
*
|
|
6
|
+
* These are the pane<->container frame shapes for streaming a headless
|
|
7
|
+
* Chromium session (docs/prd/cloud-browser-session.md). This slice defines
|
|
8
|
+
* the shapes and runtime type guards ONLY — no relay/backend wiring, no CDP
|
|
9
|
+
* driver. See cli/src/bridge/browserFrames.ts for the contract doc block.
|
|
10
|
+
*/
|
|
11
|
+
const browserFrames_1 = require("../bridge/browserFrames");
|
|
12
|
+
describe('browser frame protocol v1 — type guards (cbs-01)', () => {
|
|
13
|
+
it('pins the protocol version', () => {
|
|
14
|
+
expect(browserFrames_1.BROWSER_FRAME_PROTOCOL_VERSION).toBe(1);
|
|
15
|
+
});
|
|
16
|
+
describe('browser-open (pane -> container)', () => {
|
|
17
|
+
it('accepts a valid frame', () => {
|
|
18
|
+
const frame = { t: 'browser-open', sessionId: 'browser:c-1', url: 'https://example.com' };
|
|
19
|
+
expect((0, browserFrames_1.isBrowserOpenFrame)(frame)).toBe(true);
|
|
20
|
+
expect((0, browserFrames_1.isBrowserFrame)(frame)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it('rejects wrong t, missing sessionId, missing/non-string url', () => {
|
|
23
|
+
expect((0, browserFrames_1.isBrowserOpenFrame)({ t: 'browser-input', sessionId: 'x', url: 'https://x' })).toBe(false);
|
|
24
|
+
expect((0, browserFrames_1.isBrowserOpenFrame)({ t: 'browser-open', url: 'https://x' })).toBe(false);
|
|
25
|
+
expect((0, browserFrames_1.isBrowserOpenFrame)({ t: 'browser-open', sessionId: 'x' })).toBe(false);
|
|
26
|
+
expect((0, browserFrames_1.isBrowserOpenFrame)({ t: 'browser-open', sessionId: 'x', url: 42 })).toBe(false);
|
|
27
|
+
expect((0, browserFrames_1.isBrowserOpenFrame)(null)).toBe(false);
|
|
28
|
+
expect((0, browserFrames_1.isBrowserOpenFrame)('nope')).toBe(false);
|
|
29
|
+
expect((0, browserFrames_1.isBrowserOpenFrame)(undefined)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('browser-input (pane -> container) — event discriminated union', () => {
|
|
33
|
+
it('accepts a mouse event', () => {
|
|
34
|
+
const frame = {
|
|
35
|
+
t: 'browser-input',
|
|
36
|
+
sessionId: 'browser:c-1',
|
|
37
|
+
event: { kind: 'mouse', type: 'down', x: 10, y: 20, button: 'left', modifiers: { shift: true } },
|
|
38
|
+
};
|
|
39
|
+
expect((0, browserFrames_1.isBrowserInputFrame)(frame)).toBe(true);
|
|
40
|
+
expect((0, browserFrames_1.isBrowserInputEvent)(frame.event)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it('accepts a mouse move without button/modifiers (both optional)', () => {
|
|
43
|
+
const event = { kind: 'mouse', type: 'move', x: 1, y: 2 };
|
|
44
|
+
expect((0, browserFrames_1.isBrowserInputEvent)(event)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('rejects a mouse event with a bad button or non-numeric coords', () => {
|
|
47
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'mouse', type: 'down', x: '1', y: 2 })).toBe(false);
|
|
48
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'mouse', type: 'down', x: 1, y: 2, button: 'nope' })).toBe(false);
|
|
49
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'mouse', type: 'sideways', x: 1, y: 2 })).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
it('accepts a key event', () => {
|
|
52
|
+
const event = { kind: 'key', type: 'down', key: 'a', code: 'KeyA', modifiers: { ctrl: true } };
|
|
53
|
+
expect((0, browserFrames_1.isBrowserInputEvent)(event)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it('rejects a key event missing key/code or bad type', () => {
|
|
56
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'key', type: 'down', code: 'KeyA' })).toBe(false);
|
|
57
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'key', type: 'down', key: 'a' })).toBe(false);
|
|
58
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'key', type: 'held', key: 'a', code: 'KeyA' })).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
it('accepts an insertText event', () => {
|
|
61
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'insertText', text: 'hunter2' })).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
it('rejects an insertText event with a non-string text', () => {
|
|
64
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'insertText', text: 5 })).toBe(false);
|
|
65
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'insertText' })).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
it('accepts a wheel event', () => {
|
|
68
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'wheel', x: 1, y: 2, deltaX: 0, deltaY: 12.5 })).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
it('rejects a wheel event with a missing delta', () => {
|
|
71
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'wheel', x: 1, y: 2, deltaX: 0 })).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it('rejects an unknown event kind', () => {
|
|
74
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({ kind: 'gesture', x: 1, y: 2 })).toBe(false);
|
|
75
|
+
expect((0, browserFrames_1.isBrowserInputEvent)(null)).toBe(false);
|
|
76
|
+
expect((0, browserFrames_1.isBrowserInputEvent)({})).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it('rejects a browser-input frame with an invalid event or missing sessionId', () => {
|
|
79
|
+
expect((0, browserFrames_1.isBrowserInputFrame)({ t: 'browser-input', sessionId: 'x', event: { kind: 'bogus' } })).toBe(false);
|
|
80
|
+
expect((0, browserFrames_1.isBrowserInputFrame)({ t: 'browser-input', event: { kind: 'insertText', text: 'a' } })).toBe(false);
|
|
81
|
+
expect((0, browserFrames_1.isBrowserInputFrame)({ t: 'browser-open', sessionId: 'x', event: { kind: 'insertText', text: 'a' } })).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('browser-frame (container -> pane) — JPEG screencast frame', () => {
|
|
85
|
+
it('accepts a valid frame', () => {
|
|
86
|
+
const frame = { t: 'browser-frame', sessionId: 'browser:c-1', seq: 42, data: 'YmFzZTY0', w: 1280, h: 720 };
|
|
87
|
+
expect((0, browserFrames_1.isBrowserScreenFrame)(frame)).toBe(true);
|
|
88
|
+
expect((0, browserFrames_1.isBrowserFrame)(frame)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
it('rejects a non-numeric seq/w/h or non-string data', () => {
|
|
91
|
+
expect((0, browserFrames_1.isBrowserScreenFrame)({ t: 'browser-frame', sessionId: 'x', seq: '1', data: 'a', w: 1, h: 1 })).toBe(false);
|
|
92
|
+
expect((0, browserFrames_1.isBrowserScreenFrame)({ t: 'browser-frame', sessionId: 'x', seq: 1, data: 1, w: 1, h: 1 })).toBe(false);
|
|
93
|
+
expect((0, browserFrames_1.isBrowserScreenFrame)({ t: 'browser-frame', sessionId: 'x', seq: 1, data: 'a', w: -1, h: 1 })).toBe(false);
|
|
94
|
+
expect((0, browserFrames_1.isBrowserScreenFrame)({ t: 'browser-frame', sessionId: 'x', seq: -1, data: 'a', w: 1, h: 1 })).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('browser-state (container -> pane)', () => {
|
|
98
|
+
it('accepts every valid state value', () => {
|
|
99
|
+
for (const state of ['opening', 'open', 'asleep', 'closed']) {
|
|
100
|
+
expect((0, browserFrames_1.isBrowserStateFrame)({ t: 'browser-state', sessionId: 'browser:c-1', state })).toBe(true);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
it('accepts optional url/title/reason', () => {
|
|
104
|
+
const frame = {
|
|
105
|
+
t: 'browser-state',
|
|
106
|
+
sessionId: 'browser:c-1',
|
|
107
|
+
state: 'open',
|
|
108
|
+
url: 'https://example.com',
|
|
109
|
+
title: 'Example',
|
|
110
|
+
reason: undefined,
|
|
111
|
+
};
|
|
112
|
+
expect((0, browserFrames_1.isBrowserStateFrame)(frame)).toBe(true);
|
|
113
|
+
expect((0, browserFrames_1.isBrowserStateFrame)({ t: 'browser-state', sessionId: 'x', state: 'closed', reason: 'idle-timeout' })).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
it('rejects an invalid state value', () => {
|
|
116
|
+
expect((0, browserFrames_1.isBrowserStateFrame)({ t: 'browser-state', sessionId: 'x', state: 'sleeping' })).toBe(false);
|
|
117
|
+
expect((0, browserFrames_1.isBrowserStateFrame)({ t: 'browser-state', sessionId: 'x' })).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
// cbs-06 — CLI mirror of arena's cbs-05 addition (lib/browserFrames.ts on
|
|
121
|
+
// arena master). The pane's keep-newest ack contract needs a frame shape
|
|
122
|
+
// cbs-01 didn't define; this pins the CLI side to the EXACT same shape so
|
|
123
|
+
// the two repos' hand-maintained mirrors don't drift.
|
|
124
|
+
describe('browser-frame-ack (pane -> container) — cbs-06 mirror of arena cbs-05', () => {
|
|
125
|
+
it('accepts a valid frame', () => {
|
|
126
|
+
const frame = { t: 'browser-frame-ack', sessionId: 'browser:c-1', seq: 3 };
|
|
127
|
+
expect((0, browserFrames_1.isBrowserFrameAckFrame)(frame)).toBe(true);
|
|
128
|
+
expect((0, browserFrames_1.isBrowserFrame)(frame)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
it('rejects wrong t, missing sessionId, missing/non-number seq', () => {
|
|
131
|
+
expect((0, browserFrames_1.isBrowserFrameAckFrame)({ t: 'browser-frame', sessionId: 'x', seq: 1 })).toBe(false);
|
|
132
|
+
expect((0, browserFrames_1.isBrowserFrameAckFrame)({ t: 'browser-frame-ack', seq: 1 })).toBe(false);
|
|
133
|
+
expect((0, browserFrames_1.isBrowserFrameAckFrame)({ t: 'browser-frame-ack', sessionId: 'x' })).toBe(false);
|
|
134
|
+
expect((0, browserFrames_1.isBrowserFrameAckFrame)({ t: 'browser-frame-ack', sessionId: 'x', seq: '1' })).toBe(false);
|
|
135
|
+
expect((0, browserFrames_1.isBrowserFrameAckFrame)(null)).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
it('accepts seq: 0 (a valid monotonic starting point)', () => {
|
|
138
|
+
expect((0, browserFrames_1.isBrowserFrameAckFrame)({ t: 'browser-frame-ack', sessionId: 'x', seq: 0 })).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
it('rejects a negative seq', () => {
|
|
141
|
+
expect((0, browserFrames_1.isBrowserFrameAckFrame)({ t: 'browser-frame-ack', sessionId: 'x', seq: -1 })).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
// cbs-06 — CLI mirror of arena's cbs-05 addition. The pane's explicit 💤
|
|
145
|
+
// sleep control needs a minimal control frame cbs-01 didn't define.
|
|
146
|
+
describe('browser-sleep (pane -> container) — cbs-06 mirror of arena cbs-05', () => {
|
|
147
|
+
it('accepts a valid frame (no payload beyond identity)', () => {
|
|
148
|
+
const frame = { t: 'browser-sleep', sessionId: 'browser:c-1' };
|
|
149
|
+
expect((0, browserFrames_1.isBrowserSleepFrame)(frame)).toBe(true);
|
|
150
|
+
expect((0, browserFrames_1.isBrowserFrame)(frame)).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
it('rejects wrong t and missing sessionId', () => {
|
|
153
|
+
expect((0, browserFrames_1.isBrowserSleepFrame)({ t: 'browser-open', sessionId: 'x' })).toBe(false);
|
|
154
|
+
expect((0, browserFrames_1.isBrowserSleepFrame)({ t: 'browser-sleep' })).toBe(false);
|
|
155
|
+
expect((0, browserFrames_1.isBrowserSleepFrame)(null)).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('isBrowserFrame — top-level discriminator', () => {
|
|
159
|
+
it('rejects frames with an unrelated or missing t', () => {
|
|
160
|
+
expect((0, browserFrames_1.isBrowserFrame)({ t: 'open', sessionId: 'x' })).toBe(false);
|
|
161
|
+
expect((0, browserFrames_1.isBrowserFrame)({ sessionId: 'x' })).toBe(false);
|
|
162
|
+
expect((0, browserFrames_1.isBrowserFrame)(null)).toBe(false);
|
|
163
|
+
expect((0, browserFrames_1.isBrowserFrame)(42)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* cbs-06 — browser idle timer (pure-ish, injectable clocks + interval).
|
|
5
|
+
*
|
|
6
|
+
* Tracks two liveness signals for a cloud browser session — last INPUT
|
|
7
|
+
* (browser-input frames) and last ACK (browser-frame-ack frames, a proxy
|
|
8
|
+
* for "someone is actually looking at the stream") — and fires onIdle()
|
|
9
|
+
* once BOTH have been stale past the threshold (PRD: 15 minutes), mirroring
|
|
10
|
+
* the container-level idle convention (container-lifecycle.service.ts:
|
|
11
|
+
* "Idle is (a) AND (b) both true; either signal alone is enough to stay
|
|
12
|
+
* awake").
|
|
13
|
+
*
|
|
14
|
+
* Suspend-safety (docs/prd/cloud-browser-session.md, issues/cbs-06): the
|
|
15
|
+
* container RAM-suspends when idle. A resume after hours must not insta-fire
|
|
16
|
+
* from a stale wall-clock delta. Every periodic check compares the WALL
|
|
17
|
+
* delta since the last check against the MONOTONIC delta — a suspend
|
|
18
|
+
* freezes CLOCK_MONOTONIC but not CLOCK_REALTIME, so a wall delta far
|
|
19
|
+
* exceeding the monotonic delta is the suspend's signature; the tracker
|
|
20
|
+
* resets its idle baseline instead of evaluating/firing that tick.
|
|
21
|
+
*/
|
|
22
|
+
const browserIdle_1 = require("../bridge/browserIdle");
|
|
23
|
+
function fakeClocks(startMs = 1_000_000) {
|
|
24
|
+
let wall = startMs;
|
|
25
|
+
let mono = 0;
|
|
26
|
+
return {
|
|
27
|
+
nowFn: () => wall,
|
|
28
|
+
monotonicNowFn: () => mono,
|
|
29
|
+
advanceBoth(ms) {
|
|
30
|
+
wall += ms;
|
|
31
|
+
mono += ms;
|
|
32
|
+
},
|
|
33
|
+
/** Simulates a suspend: wall jumps by `ms`, monotonic barely moves. */
|
|
34
|
+
advanceWallOnly(ms, monoTickMs = 0) {
|
|
35
|
+
wall += ms;
|
|
36
|
+
mono += monoTickMs;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function fakeInterval() {
|
|
41
|
+
let cb = null;
|
|
42
|
+
let ms = null;
|
|
43
|
+
return {
|
|
44
|
+
setIntervalFn: (fn, intervalMs) => {
|
|
45
|
+
cb = fn;
|
|
46
|
+
ms = intervalMs;
|
|
47
|
+
return 'handle';
|
|
48
|
+
},
|
|
49
|
+
clearIntervalFn: () => {
|
|
50
|
+
cb = null;
|
|
51
|
+
},
|
|
52
|
+
tick: () => cb?.(),
|
|
53
|
+
intervalMs: () => ms,
|
|
54
|
+
isCancelled: () => cb === null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
describe('createBrowserIdleTracker (cbs-06)', () => {
|
|
58
|
+
it('pins the default threshold to 15 minutes', () => {
|
|
59
|
+
expect(browserIdle_1.DEFAULT_BROWSER_IDLE_THRESHOLD_MS).toBe(15 * 60_000);
|
|
60
|
+
});
|
|
61
|
+
it('does not fire before the threshold elapses on either signal', () => {
|
|
62
|
+
const clocks = fakeClocks();
|
|
63
|
+
const interval = fakeInterval();
|
|
64
|
+
const onIdle = jest.fn();
|
|
65
|
+
const tracker = (0, browserIdle_1.createBrowserIdleTracker)({
|
|
66
|
+
onIdle,
|
|
67
|
+
thresholdMs: 15 * 60_000,
|
|
68
|
+
checkIntervalMs: 30_000,
|
|
69
|
+
nowFn: clocks.nowFn,
|
|
70
|
+
monotonicNowFn: clocks.monotonicNowFn,
|
|
71
|
+
setIntervalFn: interval.setIntervalFn,
|
|
72
|
+
clearIntervalFn: interval.clearIntervalFn,
|
|
73
|
+
});
|
|
74
|
+
tracker.start();
|
|
75
|
+
clocks.advanceBoth(14 * 60_000);
|
|
76
|
+
interval.tick();
|
|
77
|
+
expect(onIdle).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
it('fires onIdle once BOTH lastInputAt and lastAckAt are stale past the threshold', () => {
|
|
80
|
+
const clocks = fakeClocks();
|
|
81
|
+
const interval = fakeInterval();
|
|
82
|
+
const onIdle = jest.fn();
|
|
83
|
+
const tracker = (0, browserIdle_1.createBrowserIdleTracker)({
|
|
84
|
+
onIdle,
|
|
85
|
+
thresholdMs: 15 * 60_000,
|
|
86
|
+
checkIntervalMs: 30_000,
|
|
87
|
+
nowFn: clocks.nowFn,
|
|
88
|
+
monotonicNowFn: clocks.monotonicNowFn,
|
|
89
|
+
setIntervalFn: interval.setIntervalFn,
|
|
90
|
+
clearIntervalFn: interval.clearIntervalFn,
|
|
91
|
+
});
|
|
92
|
+
tracker.start();
|
|
93
|
+
clocks.advanceBoth(15 * 60_000 + 1);
|
|
94
|
+
interval.tick();
|
|
95
|
+
expect(onIdle).toHaveBeenCalledTimes(1);
|
|
96
|
+
});
|
|
97
|
+
it('either signal alone (recent input OR recent ack) keeps it awake', () => {
|
|
98
|
+
const clocks = fakeClocks();
|
|
99
|
+
const interval = fakeInterval();
|
|
100
|
+
const onIdle = jest.fn();
|
|
101
|
+
const tracker = (0, browserIdle_1.createBrowserIdleTracker)({
|
|
102
|
+
onIdle,
|
|
103
|
+
thresholdMs: 15 * 60_000,
|
|
104
|
+
checkIntervalMs: 30_000,
|
|
105
|
+
nowFn: clocks.nowFn,
|
|
106
|
+
monotonicNowFn: clocks.monotonicNowFn,
|
|
107
|
+
setIntervalFn: interval.setIntervalFn,
|
|
108
|
+
clearIntervalFn: interval.clearIntervalFn,
|
|
109
|
+
});
|
|
110
|
+
tracker.start();
|
|
111
|
+
// Input goes stale, but ack keeps arriving — someone is watching.
|
|
112
|
+
clocks.advanceBoth(10 * 60_000);
|
|
113
|
+
tracker.recordAck();
|
|
114
|
+
clocks.advanceBoth(10 * 60_000);
|
|
115
|
+
tracker.recordAck();
|
|
116
|
+
clocks.advanceBoth(10 * 60_000);
|
|
117
|
+
interval.tick();
|
|
118
|
+
expect(onIdle).not.toHaveBeenCalled(); // ack signal alone: 10min stale, not 15
|
|
119
|
+
// Now let ack go stale too, past the threshold from its last bump.
|
|
120
|
+
clocks.advanceBoth(15 * 60_000 + 1);
|
|
121
|
+
interval.tick();
|
|
122
|
+
expect(onIdle).toHaveBeenCalledTimes(1);
|
|
123
|
+
});
|
|
124
|
+
it('recordInput() resets the input baseline independent of the ack baseline', () => {
|
|
125
|
+
const clocks = fakeClocks();
|
|
126
|
+
const interval = fakeInterval();
|
|
127
|
+
const onIdle = jest.fn();
|
|
128
|
+
const tracker = (0, browserIdle_1.createBrowserIdleTracker)({
|
|
129
|
+
onIdle,
|
|
130
|
+
thresholdMs: 15 * 60_000,
|
|
131
|
+
checkIntervalMs: 30_000,
|
|
132
|
+
nowFn: clocks.nowFn,
|
|
133
|
+
monotonicNowFn: clocks.monotonicNowFn,
|
|
134
|
+
setIntervalFn: interval.setIntervalFn,
|
|
135
|
+
clearIntervalFn: interval.clearIntervalFn,
|
|
136
|
+
});
|
|
137
|
+
tracker.start();
|
|
138
|
+
clocks.advanceBoth(10 * 60_000);
|
|
139
|
+
tracker.recordInput();
|
|
140
|
+
clocks.advanceBoth(10 * 60_000);
|
|
141
|
+
interval.tick();
|
|
142
|
+
expect(onIdle).not.toHaveBeenCalled(); // input reset 10min ago, ack 20min ago -> input signal keeps it awake
|
|
143
|
+
clocks.advanceBoth(5 * 60_000 + 1);
|
|
144
|
+
interval.tick();
|
|
145
|
+
expect(onIdle).toHaveBeenCalledTimes(1);
|
|
146
|
+
});
|
|
147
|
+
it('a suspend-sized wall/monotonic gap resets the idle baseline instead of firing', () => {
|
|
148
|
+
const clocks = fakeClocks();
|
|
149
|
+
const interval = fakeInterval();
|
|
150
|
+
const onIdle = jest.fn();
|
|
151
|
+
const tracker = (0, browserIdle_1.createBrowserIdleTracker)({
|
|
152
|
+
onIdle,
|
|
153
|
+
thresholdMs: 15 * 60_000,
|
|
154
|
+
checkIntervalMs: 30_000,
|
|
155
|
+
suspendGapToleranceMs: 5_000,
|
|
156
|
+
nowFn: clocks.nowFn,
|
|
157
|
+
monotonicNowFn: clocks.monotonicNowFn,
|
|
158
|
+
setIntervalFn: interval.setIntervalFn,
|
|
159
|
+
clearIntervalFn: interval.clearIntervalFn,
|
|
160
|
+
});
|
|
161
|
+
tracker.start();
|
|
162
|
+
// Some real idle time accrues first (10 min, still under threshold).
|
|
163
|
+
clocks.advanceBoth(10 * 60_000);
|
|
164
|
+
interval.tick();
|
|
165
|
+
expect(onIdle).not.toHaveBeenCalled();
|
|
166
|
+
// Container suspends for 5 hours: wall jumps 5h, monotonic barely moves
|
|
167
|
+
// (a single ~30s tick's worth, simulating the resume tick).
|
|
168
|
+
clocks.advanceWallOnly(5 * 60 * 60_000, 30_000);
|
|
169
|
+
interval.tick();
|
|
170
|
+
// Must NOT fire — this is a suspend gap, not real idle time.
|
|
171
|
+
expect(onIdle).not.toHaveBeenCalled();
|
|
172
|
+
// Baseline was reset on resume: only a further NORMAL 30s has passed
|
|
173
|
+
// since the reset, nowhere near the 15-minute threshold.
|
|
174
|
+
clocks.advanceBoth(30_000);
|
|
175
|
+
interval.tick();
|
|
176
|
+
expect(onIdle).not.toHaveBeenCalled();
|
|
177
|
+
// But real elapsed time from the reset point DOES eventually fire.
|
|
178
|
+
clocks.advanceBoth(15 * 60_000);
|
|
179
|
+
interval.tick();
|
|
180
|
+
expect(onIdle).toHaveBeenCalledTimes(1);
|
|
181
|
+
});
|
|
182
|
+
it('normal small scheduling jitter (well within tolerance) is not mistaken for a suspend gap', () => {
|
|
183
|
+
const clocks = fakeClocks();
|
|
184
|
+
const interval = fakeInterval();
|
|
185
|
+
const onIdle = jest.fn();
|
|
186
|
+
const tracker = (0, browserIdle_1.createBrowserIdleTracker)({
|
|
187
|
+
onIdle,
|
|
188
|
+
thresholdMs: 15 * 60_000,
|
|
189
|
+
checkIntervalMs: 30_000,
|
|
190
|
+
suspendGapToleranceMs: 5_000,
|
|
191
|
+
nowFn: clocks.nowFn,
|
|
192
|
+
monotonicNowFn: clocks.monotonicNowFn,
|
|
193
|
+
setIntervalFn: interval.setIntervalFn,
|
|
194
|
+
clearIntervalFn: interval.clearIntervalFn,
|
|
195
|
+
});
|
|
196
|
+
tracker.start();
|
|
197
|
+
// 16 minutes of idle time accrued across ticks that each run a couple
|
|
198
|
+
// hundred ms "late" (wall slightly ahead of monotonic each tick, but
|
|
199
|
+
// always under the 5s tolerance) — must still fire normally.
|
|
200
|
+
for (let i = 0; i < 32; i++) {
|
|
201
|
+
clocks.advanceWallOnly(30_000 + 200, 30_000);
|
|
202
|
+
interval.tick();
|
|
203
|
+
}
|
|
204
|
+
expect(onIdle).toHaveBeenCalledTimes(1);
|
|
205
|
+
});
|
|
206
|
+
it('onIdle fires at most once (latched) even if further ticks arrive', () => {
|
|
207
|
+
const clocks = fakeClocks();
|
|
208
|
+
const interval = fakeInterval();
|
|
209
|
+
const onIdle = jest.fn();
|
|
210
|
+
const tracker = (0, browserIdle_1.createBrowserIdleTracker)({
|
|
211
|
+
onIdle,
|
|
212
|
+
thresholdMs: 15 * 60_000,
|
|
213
|
+
checkIntervalMs: 30_000,
|
|
214
|
+
nowFn: clocks.nowFn,
|
|
215
|
+
monotonicNowFn: clocks.monotonicNowFn,
|
|
216
|
+
setIntervalFn: interval.setIntervalFn,
|
|
217
|
+
clearIntervalFn: interval.clearIntervalFn,
|
|
218
|
+
});
|
|
219
|
+
tracker.start();
|
|
220
|
+
clocks.advanceBoth(16 * 60_000);
|
|
221
|
+
interval.tick();
|
|
222
|
+
interval.tick();
|
|
223
|
+
clocks.advanceBoth(60_000);
|
|
224
|
+
interval.tick();
|
|
225
|
+
expect(onIdle).toHaveBeenCalledTimes(1);
|
|
226
|
+
});
|
|
227
|
+
it('stop() cancels the interval', () => {
|
|
228
|
+
const clocks = fakeClocks();
|
|
229
|
+
const interval = fakeInterval();
|
|
230
|
+
const tracker = (0, browserIdle_1.createBrowserIdleTracker)({
|
|
231
|
+
onIdle: jest.fn(),
|
|
232
|
+
nowFn: clocks.nowFn,
|
|
233
|
+
monotonicNowFn: clocks.monotonicNowFn,
|
|
234
|
+
setIntervalFn: interval.setIntervalFn,
|
|
235
|
+
clearIntervalFn: interval.clearIntervalFn,
|
|
236
|
+
});
|
|
237
|
+
tracker.start();
|
|
238
|
+
expect(interval.isCancelled()).toBe(false);
|
|
239
|
+
tracker.stop();
|
|
240
|
+
expect(interval.isCancelled()).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
it('start() is idempotent (calling twice does not double-register the interval)', () => {
|
|
243
|
+
const clocks = fakeClocks();
|
|
244
|
+
let registrations = 0;
|
|
245
|
+
const tracker = (0, browserIdle_1.createBrowserIdleTracker)({
|
|
246
|
+
onIdle: jest.fn(),
|
|
247
|
+
nowFn: clocks.nowFn,
|
|
248
|
+
monotonicNowFn: clocks.monotonicNowFn,
|
|
249
|
+
setIntervalFn: (fn) => {
|
|
250
|
+
registrations += 1;
|
|
251
|
+
return fn;
|
|
252
|
+
},
|
|
253
|
+
clearIntervalFn: () => { },
|
|
254
|
+
});
|
|
255
|
+
tracker.start();
|
|
256
|
+
tracker.start();
|
|
257
|
+
expect(registrations).toBe(1);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|