switchroom 0.13.63 → 0.13.65
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/cli/switchroom.js +18 -13
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +13 -6
- package/telegram-plugin/gateway/gateway.ts +41 -8
- package/telegram-plugin/uat/scenarios/greeting-reply-dm.test.ts +48 -0
- package/telegram-plugin/uat/scenarios/jtbd-webkite-read-dm.test.ts +115 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -49083,8 +49083,8 @@ var {
|
|
|
49083
49083
|
} = import__.default;
|
|
49084
49084
|
|
|
49085
49085
|
// src/build-info.ts
|
|
49086
|
-
var VERSION = "0.13.
|
|
49087
|
-
var COMMIT_SHA = "
|
|
49086
|
+
var VERSION = "0.13.65";
|
|
49087
|
+
var COMMIT_SHA = "51894764";
|
|
49088
49088
|
|
|
49089
49089
|
// src/cli/agent.ts
|
|
49090
49090
|
init_source();
|
|
@@ -49940,6 +49940,10 @@ var AGENT_CONFIG_MCP_TOOLS = [
|
|
|
49940
49940
|
var HOSTD_MCP_TOOLS = [
|
|
49941
49941
|
"mcp__hostd__update_check"
|
|
49942
49942
|
];
|
|
49943
|
+
var WEBKITE_MCP_TOOLS = [
|
|
49944
|
+
"mcp__webkite",
|
|
49945
|
+
"mcp__webkite__*"
|
|
49946
|
+
];
|
|
49943
49947
|
var LEGACY_SWITCHROOM_MCP_TOKENS = ["mcp__switchroom", "mcp__switchroom__*"];
|
|
49944
49948
|
var LEGACY_HOSTD_BLANKET_TOKENS = ["mcp__hostd", "mcp__hostd__*"];
|
|
49945
49949
|
var DEFAULT_READ_ONLY_PREAPPROVED_TOOLS = [
|
|
@@ -50755,7 +50759,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
50755
50759
|
...usesSwitchroomTelegramPlugin(agentConfig) ? SWITCHROOM_TELEGRAM_MCP_TOOLS : [],
|
|
50756
50760
|
...hindsightEnabled ? HINDSIGHT_MCP_TOOLS : [],
|
|
50757
50761
|
...AGENT_CONFIG_MCP_TOOLS,
|
|
50758
|
-
...HOSTD_MCP_TOOLS
|
|
50762
|
+
...HOSTD_MCP_TOOLS,
|
|
50763
|
+
...agentConfig.mcp_servers?.["webkite"] === false ? [] : WEBKITE_MCP_TOOLS
|
|
50759
50764
|
]);
|
|
50760
50765
|
const hindsightAutoRecallEnabled = hindsightEnabled && agentConfig.memory?.auto_recall !== false;
|
|
50761
50766
|
const hindsightBankId = agentConfig.memory?.collection ?? name;
|
|
@@ -50816,7 +50821,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
50816
50821
|
}
|
|
50817
50822
|
settings.permissions = settings.permissions ?? {};
|
|
50818
50823
|
const allow = (Array.isArray(settings.permissions.allow) ? settings.permissions.allow : []).filter((p) => !LEGACY_HOSTD_BLANKET_TOKENS.includes(p));
|
|
50819
|
-
|
|
50824
|
+
const webkiteAllowTools = agentConfig.mcp_servers?.["webkite"] === false ? [] : WEBKITE_MCP_TOOLS;
|
|
50825
|
+
for (const t of [...AGENT_CONFIG_MCP_TOOLS, ...HOSTD_MCP_TOOLS, ...webkiteAllowTools]) {
|
|
50820
50826
|
if (!allow.includes(t))
|
|
50821
50827
|
allow.push(t);
|
|
50822
50828
|
}
|
|
@@ -51373,16 +51379,14 @@ function buildSettingsHooksBlock(p) {
|
|
|
51373
51379
|
const useHotReloadStable = agentConfig.channels?.telegram?.hotReloadStable === true;
|
|
51374
51380
|
const turnPacingDirective = "<turn-pacing>You are messaging a human via Telegram. The framework " + "automatically shows the user a live preview in their compose area as " + 'you work \u2014 they see "Read a file", "Ran 2 commands", etc. as your ' + `tool_use events stream. You do NOT need to ack manually.
|
|
51375
51381
|
|
|
51376
|
-
` +
|
|
51377
|
-
|
|
51378
|
-
` + `Call reply only when you have something substantive to deliver:
|
|
51379
|
-
` + ` - The actual answer (any length \u2014 short or long)
|
|
51380
|
-
` + ` - A genuine question back to the user
|
|
51381
|
-
` + " - A real mid-work milestone or pivot that changes what the user " + 'should expect (e.g. "halfway through \u2014 found an unexpected issue, ' + `want me to continue?"). Not "still working".
|
|
51382
|
+
` + "ALWAYS reply to a message the user sends you. A direct message " + 'expects a response: a greeting ("hi", "hey", "you there?") gets a ' + "greeting back; a thanks gets a brief acknowledgement; a question " + "gets an answer. NEVER end a turn with NO_REPLY when the user has " + "just sent you something \u2014 NO_REPLY is only for genuine non-prompts " + `(a system-synthesized event you have already fully handled).
|
|
51382
51383
|
|
|
51383
|
-
` + "
|
|
51384
|
+
` + "What you should NOT do is send a placeholder ack BEFORE doing the " + 'work \u2014 no "on it", "good question \u2014 one sec", "let me dig in", ' + '"checking now". Those add chat clutter on top of the activity ' + "preview the user already sees, and the preview clears the moment " + `your real reply lands. Do not ack-then-answer; just answer.
|
|
51384
51385
|
|
|
51385
|
-
` +
|
|
51386
|
+
` + `So:
|
|
51387
|
+
` + " - Trivial / social message \u2192 reply once, briefly, in your voice. " + `The reply IS the response.
|
|
51388
|
+
` + ` - Question with a short answer \u2192 just reply with the answer.
|
|
51389
|
+
` + " - Complex tool-driven work \u2192 go straight to the tools (the " + "compose-area preview is the ambient liveness signal), then reply " + 'once with the answer or a genuine mid-work pivot ("halfway ' + 'through \u2014 found an unexpected issue, want me to continue?"). Not ' + '"still working".</turn-pacing>';
|
|
51386
51390
|
const switchroomUserPromptSubmit = [
|
|
51387
51391
|
...useHotReloadStable ? [
|
|
51388
51392
|
{
|
|
@@ -51541,7 +51545,8 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
51541
51545
|
...usesSwitchroomTelegramPlugin(agentConfig) ? SWITCHROOM_TELEGRAM_MCP_TOOLS : [],
|
|
51542
51546
|
...hindsightEnabled ? HINDSIGHT_MCP_TOOLS : [],
|
|
51543
51547
|
...AGENT_CONFIG_MCP_TOOLS,
|
|
51544
|
-
...HOSTD_MCP_TOOLS
|
|
51548
|
+
...HOSTD_MCP_TOOLS,
|
|
51549
|
+
...agentConfig.mcp_servers?.["webkite"] === false ? [] : WEBKITE_MCP_TOOLS
|
|
51545
51550
|
]);
|
|
51546
51551
|
const desiredDeny = dedupe2([
|
|
51547
51552
|
...tools.deny ?? [],
|
package/package.json
CHANGED
|
@@ -49716,10 +49716,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
49716
49716
|
}
|
|
49717
49717
|
|
|
49718
49718
|
// ../src/build-info.ts
|
|
49719
|
-
var VERSION = "0.13.
|
|
49720
|
-
var COMMIT_SHA = "
|
|
49721
|
-
var COMMIT_DATE = "2026-05-
|
|
49722
|
-
var LATEST_PR =
|
|
49719
|
+
var VERSION = "0.13.65";
|
|
49720
|
+
var COMMIT_SHA = "51894764";
|
|
49721
|
+
var COMMIT_DATE = "2026-05-28T05:00:08Z";
|
|
49722
|
+
var LATEST_PR = 1950;
|
|
49723
49723
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
49724
49724
|
|
|
49725
49725
|
// gateway/boot-version.ts
|
|
@@ -51489,6 +51489,13 @@ var ANSWER_STREAM_VISIBLE_ENABLED = (() => {
|
|
|
51489
51489
|
return false;
|
|
51490
51490
|
return true;
|
|
51491
51491
|
})();
|
|
51492
|
+
var DRAFT_MIRROR_ENABLED = (() => {
|
|
51493
|
+
const raw = process.env.SWITCHROOM_DRAFT_MIRROR;
|
|
51494
|
+
if (raw == null)
|
|
51495
|
+
return false;
|
|
51496
|
+
const v = raw.trim().toLowerCase();
|
|
51497
|
+
return !(v === "0" || v === "false" || v === "off" || v === "no");
|
|
51498
|
+
})();
|
|
51492
51499
|
var progressDriver = null;
|
|
51493
51500
|
var unpinProgressCardForChat = null;
|
|
51494
51501
|
var getPinnedProgressCardMessageId = null;
|
|
@@ -53856,7 +53863,7 @@ function handleSessionEvent(ev) {
|
|
|
53856
53863
|
clearActivitySummary(turn);
|
|
53857
53864
|
}
|
|
53858
53865
|
}
|
|
53859
|
-
if (!turn.replyCalled && !isTelegramSurfaceTool(name)) {
|
|
53866
|
+
if (!DRAFT_MIRROR_ENABLED && !turn.replyCalled && !isTelegramSurfaceTool(name)) {
|
|
53860
53867
|
const rendered = registerAndRender(turn.toolActivity, name);
|
|
53861
53868
|
if (rendered != null) {
|
|
53862
53869
|
turn.activityPendingRender = rendered;
|
|
@@ -53884,7 +53891,7 @@ function handleSessionEvent(ev) {
|
|
|
53884
53891
|
chatId: turn.sessionChatId,
|
|
53885
53892
|
isPrivateChat: turn.isDm,
|
|
53886
53893
|
threadId: turn.sessionThreadId,
|
|
53887
|
-
...ANSWER_STREAM_VISIBLE_ENABLED ? { minInitialChars: 1 } : { sendMessageDraft: sendMessageDraftFn },
|
|
53894
|
+
...DRAFT_MIRROR_ENABLED ? { sendMessageDraft: sendMessageDraftFn } : ANSWER_STREAM_VISIBLE_ENABLED ? { minInitialChars: 1 } : { sendMessageDraft: sendMessageDraftFn },
|
|
53888
53895
|
sendMessage: async (chatId, text, params) => {
|
|
53889
53896
|
const tid = params?.message_thread_id;
|
|
53890
53897
|
const silent = params?.purpose !== "materialize";
|
|
@@ -3192,6 +3192,26 @@ const ANSWER_STREAM_VISIBLE_ENABLED = (() => {
|
|
|
3192
3192
|
if (v === '0' || v === 'false' || v === 'off' || v === 'no') return false
|
|
3193
3193
|
return true
|
|
3194
3194
|
})()
|
|
3195
|
+
|
|
3196
|
+
// Draft-mirror preview (RFC docs/rfcs/draft-mirror-preview.md), Phase 1.
|
|
3197
|
+
// When enabled, the model's prose narration streams into the ephemeral
|
|
3198
|
+
// compose-area draft (sendMessageDraft) instead of a visible real
|
|
3199
|
+
// message — a live "what's it doing" preview that clears when the
|
|
3200
|
+
// reply lands. Default OFF (canary flag). When on it (a) forces the
|
|
3201
|
+
// answer-stream onto draft transport regardless of
|
|
3202
|
+
// ANSWER_STREAM_VISIBLE_ENABLED, and (b) suppresses the activity-summary
|
|
3203
|
+
// tool-count draft so the two don't collide on the single per-chat
|
|
3204
|
+
// draft slot. Delivery on a no-reply turn is owned by turn-flush
|
|
3205
|
+
// (decideTurnFlush → capturedText fresh send), NOT answer-stream
|
|
3206
|
+
// materialize() — which is dead on the draft-only path (streamMsgId
|
|
3207
|
+
// stays null, so its turn-end gate is false). Kill switch:
|
|
3208
|
+
// SWITCHROOM_DRAFT_MIRROR unset/0/false/off/no.
|
|
3209
|
+
const DRAFT_MIRROR_ENABLED = (() => {
|
|
3210
|
+
const raw = process.env.SWITCHROOM_DRAFT_MIRROR
|
|
3211
|
+
if (raw == null) return false
|
|
3212
|
+
const v = raw.trim().toLowerCase()
|
|
3213
|
+
return !(v === '0' || v === 'false' || v === 'off' || v === 'no')
|
|
3214
|
+
})()
|
|
3195
3215
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3196
3216
|
const progressDriver: any = null
|
|
3197
3217
|
const unpinProgressCardForChat: ((chatId: string, threadId: number | undefined) => void) | null = null
|
|
@@ -7110,7 +7130,13 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
7110
7130
|
// exactly once at a time and re-running until pending matches
|
|
7111
7131
|
// the last-sent. Captures `turn` so a late drain after turn-swap
|
|
7112
7132
|
// can't corrupt the next turn's atom.
|
|
7113
|
-
|
|
7133
|
+
// DRAFT_MIRROR (RFC draft-mirror-preview, Phase 1): the model's
|
|
7134
|
+
// prose narration owns the single per-chat draft slot. Suppress
|
|
7135
|
+
// the activity-summary tool-count draft so the two don't collide
|
|
7136
|
+
// (Telegram shows one draft per chat — the later write clobbers
|
|
7137
|
+
// the earlier). The activity-summary code stays intact for the
|
|
7138
|
+
// kill-switch path; it's retired for good only in Phase 4.
|
|
7139
|
+
if (!DRAFT_MIRROR_ENABLED && !turn.replyCalled && !isTelegramSurfaceTool(name)) {
|
|
7114
7140
|
const rendered = registerAndRender(turn.toolActivity, name)
|
|
7115
7141
|
if (rendered != null) {
|
|
7116
7142
|
turn.activityPendingRender = rendered
|
|
@@ -7158,13 +7184,20 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
7158
7184
|
chatId: turn.sessionChatId,
|
|
7159
7185
|
isPrivateChat: turn.isDm,
|
|
7160
7186
|
threadId: turn.sessionThreadId,
|
|
7161
|
-
//
|
|
7162
|
-
//
|
|
7163
|
-
//
|
|
7164
|
-
//
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7187
|
+
// Transport selection:
|
|
7188
|
+
// - DRAFT_MIRROR (RFC draft-mirror-preview, Phase 1): force
|
|
7189
|
+
// the ephemeral compose-area draft so narration is a
|
|
7190
|
+
// clears-on-reply preview. Wins over visible-answer-stream.
|
|
7191
|
+
// No-reply delivery is owned by turn-flush, not materialize.
|
|
7192
|
+
// - else #869-Phase1 visible-answer-stream: omit the draft
|
|
7193
|
+
// API so the lane edits a user-visible chat-timeline
|
|
7194
|
+
// message (minInitialChars:1 opens it on the first chunk).
|
|
7195
|
+
// - else legacy: draft transport.
|
|
7196
|
+
...(DRAFT_MIRROR_ENABLED
|
|
7197
|
+
? { sendMessageDraft: sendMessageDraftFn }
|
|
7198
|
+
: ANSWER_STREAM_VISIBLE_ENABLED
|
|
7199
|
+
? { minInitialChars: 1 }
|
|
7200
|
+
: { sendMessageDraft: sendMessageDraftFn }),
|
|
7168
7201
|
// #1075: route through robustApiCall so flood-wait,
|
|
7169
7202
|
// benign-400, and THREAD_NOT_FOUND are handled uniformly
|
|
7170
7203
|
// instead of crashing the answer-stream loop on a deleted
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Greeting reply scenario — driver DMs the test bot a bare "hi", bot
|
|
3
|
+
* MUST reply (not NO_REPLY).
|
|
4
|
+
*
|
|
5
|
+
* Regression gate for the v0.13.61 turn-pacing v3 over-correction: the
|
|
6
|
+
* "don't ack" directive made the model classify a bare greeting as
|
|
7
|
+
* "not substantive" and end the turn with NO_REPLY, leaving the user
|
|
8
|
+
* staring at silence. v4 (this fix) re-asserts "always reply to a
|
|
9
|
+
* direct message; a greeting gets a greeting."
|
|
10
|
+
*
|
|
11
|
+
* Runs against real Telegram. Same env requirements as
|
|
12
|
+
* smoke-dm-reply.test.ts. Invoke via `bun run test:uat greeting`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect } from "vitest";
|
|
16
|
+
import { spinUp } from "../harness.js";
|
|
17
|
+
|
|
18
|
+
describe("uat: greeting gets a reply (v4 turn-pacing regression gate)", () => {
|
|
19
|
+
it(
|
|
20
|
+
"driver DMs a bare 'hi' and the bot replies within 60s (not NO_REPLY)",
|
|
21
|
+
async () => {
|
|
22
|
+
const sc = await spinUp({ agent: "test-harness" });
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const started = Date.now();
|
|
26
|
+
await sc.sendDM("hi");
|
|
27
|
+
|
|
28
|
+
// A greeting should come back fast — well under the silence-poke
|
|
29
|
+
// soft window. 60s budget tolerates a cold turn but a NO_REPLY
|
|
30
|
+
// (the bug) would blow past it via the 300s framework fallback.
|
|
31
|
+
const reply = await sc.expectMessage(/.+/, {
|
|
32
|
+
from: "bot",
|
|
33
|
+
timeout: 60_000,
|
|
34
|
+
});
|
|
35
|
+
const elapsed = Date.now() - started;
|
|
36
|
+
|
|
37
|
+
expect(reply.text.length).toBeGreaterThan(0);
|
|
38
|
+
expect(reply.senderUserId).toBe(sc.botUserId);
|
|
39
|
+
// The bug path replies only after the 300s framework fallback.
|
|
40
|
+
// A real greeting reply lands fast; assert it beat the fallback.
|
|
41
|
+
expect(elapsed).toBeLessThan(60_000);
|
|
42
|
+
} finally {
|
|
43
|
+
await sc.tearDown();
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
90_000,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JTBD scenario — the agent fetches the web via webkite, transparently.
|
|
3
|
+
*
|
|
4
|
+
* Validates the v0.13.62/63 webkite rollout end-to-end through real
|
|
5
|
+
* Telegram: the user sends a URL and asks about its content WITHOUT
|
|
6
|
+
* ever naming "webkite". The agent must:
|
|
7
|
+
*
|
|
8
|
+
* 1. Reach for webkite on its own (the native WebFetch/WebSearch
|
|
9
|
+
* tools are denied fleet-wide — see scaffold.ts
|
|
10
|
+
* WEBKITE_FLEET_DENY_TOOLS — so the ONLY way the agent can answer
|
|
11
|
+
* a "read this URL" prompt is via the webkite_* MCP tools). If the
|
|
12
|
+
* agent returns the page's content, webkite did the work by
|
|
13
|
+
* construction — there is no other web-fetch tool available.
|
|
14
|
+
*
|
|
15
|
+
* 2. Render JavaScript. The target is `quotes.toscrape.com/js/`, a
|
|
16
|
+
* purpose-built scraping-practice SPA whose quotes are injected by
|
|
17
|
+
* JS at runtime. A raw HTTP fetch (what the old WebFetch did) sees
|
|
18
|
+
* an empty page — `curl` returns zero `class="quote"` nodes. Only
|
|
19
|
+
* a JS-executing renderer (webkite → cloakbrowser headless
|
|
20
|
+
* Chromium) produces the visible quote text. So a correct quote in
|
|
21
|
+
* the reply is positive proof that JS rendering happened.
|
|
22
|
+
*
|
|
23
|
+
* The first quote on that page is Einstein's "The world as we have
|
|
24
|
+
* created it is a process of our thinking…". We assert the reply names
|
|
25
|
+
* Einstein AND carries a recognizable fragment of that quote.
|
|
26
|
+
*
|
|
27
|
+
* ## What this catches that other UATs don't
|
|
28
|
+
*
|
|
29
|
+
* - `jtbd-fast-trivial-dm` proves the agent replies fast, but never
|
|
30
|
+
* touches a tool. This is the first UAT that forces a real web fetch.
|
|
31
|
+
* - The in-container `webkite read` smoke proves the binary works, but
|
|
32
|
+
* not that the *model* chooses webkite unprompted over a denied
|
|
33
|
+
* WebFetch, nor that the full inbound→claude→MCP→outbound path works.
|
|
34
|
+
*
|
|
35
|
+
* ## Failure modes this guards against
|
|
36
|
+
*
|
|
37
|
+
* - A regression that re-enables WebFetch (the model might fetch raw
|
|
38
|
+
* HTML and miss the JS-rendered quotes → wrong/empty answer).
|
|
39
|
+
* - webkite MCP not wired / not trusted (agent says it can't browse).
|
|
40
|
+
* - cloakbrowser broken (agent returns the empty static page → no
|
|
41
|
+
* quote, or a "page had no content" apology).
|
|
42
|
+
* - The glibc regression that the v0.13.62 canary caught (webkite
|
|
43
|
+
* dead-on-arrival → agent can't browse at all).
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { describe, it, expect } from "vitest";
|
|
47
|
+
import { spinUp } from "../harness.js";
|
|
48
|
+
|
|
49
|
+
const AGENT = "test-harness";
|
|
50
|
+
|
|
51
|
+
// JS-rendered scraping-practice page. Quotes exist ONLY after JS runs;
|
|
52
|
+
// a raw fetch sees none. Stable, purpose-built, no auth.
|
|
53
|
+
const JS_URL = "https://quotes.toscrape.com/js/";
|
|
54
|
+
|
|
55
|
+
// Deliberately does NOT mention webkite, fetch, browser, or any tool —
|
|
56
|
+
// a natural "read this for me" ask. The agent must pick the tool.
|
|
57
|
+
const PROMPT =
|
|
58
|
+
`Open ${JS_URL} and tell me the exact text of the very first quote ` +
|
|
59
|
+
`on the page and who said it. Just the quote and the author.`;
|
|
60
|
+
|
|
61
|
+
// The first quote's author + a distinctive fragment of its text.
|
|
62
|
+
const EXPECTED_AUTHOR = /einstein/i;
|
|
63
|
+
const EXPECTED_FRAGMENT =
|
|
64
|
+
/world as we have created it|process of our thinking|changing our thinking/i;
|
|
65
|
+
|
|
66
|
+
// Phrases that would indicate the agent FAILED to browse (fell back to
|
|
67
|
+
// "I can't access the web" or got the empty static page).
|
|
68
|
+
const CANT_BROWSE = [
|
|
69
|
+
/can.?t (access|browse|open|reach|fetch)/i,
|
|
70
|
+
/unable to (access|browse|open|reach|fetch)/i,
|
|
71
|
+
/no content|empty page|couldn.?t (find|load)/i,
|
|
72
|
+
/don.?t have (web|internet|browsing)/i,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
describe("uat: agent fetches the web via webkite (JS page, unprompted)", () => {
|
|
76
|
+
it(
|
|
77
|
+
"URL prompt → agent returns JS-rendered content (proves webkite + cloakbrowser)",
|
|
78
|
+
async () => {
|
|
79
|
+
const sc = await spinUp({ agent: AGENT });
|
|
80
|
+
try {
|
|
81
|
+
await sc.sendDM(PROMPT);
|
|
82
|
+
|
|
83
|
+
// Generous budget: a real cloakbrowser render of an SPA is
|
|
84
|
+
// slower than a trivial reply (Chromium spawn + JS execution).
|
|
85
|
+
const reply = await sc.expectMessage(EXPECTED_FRAGMENT, {
|
|
86
|
+
from: "bot",
|
|
87
|
+
timeout: 90_000,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Positive proof: the JS-gated quote text came back.
|
|
91
|
+
expect(reply.text).toMatch(EXPECTED_FRAGMENT);
|
|
92
|
+
// And the author — confirms it parsed the actual quote, not noise.
|
|
93
|
+
expect(reply.text).toMatch(EXPECTED_AUTHOR);
|
|
94
|
+
|
|
95
|
+
// Negative proof: no "I can't browse" fallback. (WebFetch is
|
|
96
|
+
// denied, so a failure to use webkite surfaces as an apology,
|
|
97
|
+
// not a wrong fetch.)
|
|
98
|
+
const failedToBrowse = CANT_BROWSE.some((re) => re.test(reply.text));
|
|
99
|
+
expect(
|
|
100
|
+
failedToBrowse,
|
|
101
|
+
`agent reply looks like a can't-browse fallback: ${JSON.stringify(reply.text.slice(0, 300))}`,
|
|
102
|
+
).toBe(false);
|
|
103
|
+
|
|
104
|
+
console.log(
|
|
105
|
+
`[webkite-read] agent returned JS-rendered quote via webkite — ` +
|
|
106
|
+
`WebFetch denied, cloakbrowser rendered the SPA. ` +
|
|
107
|
+
`reply: ${JSON.stringify(reply.text.slice(0, 200))}`,
|
|
108
|
+
);
|
|
109
|
+
} finally {
|
|
110
|
+
await sc.tearDown();
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
120_000,
|
|
114
|
+
);
|
|
115
|
+
});
|