switchroom 0.20.7 → 0.20.9
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/agent-scheduler/index.js +111 -14
- package/dist/auth-broker/index.js +113 -30
- package/dist/cli/autoaccept-poll.js +5 -3
- package/dist/cli/drive-write-pretool.mjs +5 -3
- package/dist/cli/ms-365-write-pretool.mjs +5 -3
- package/dist/cli/notion-write-pretool.mjs +67 -6
- package/dist/cli/switchroom.js +389 -31
- package/dist/host-control/main.js +69 -8
- package/dist/vault/approvals/kernel-server.js +68 -7
- package/dist/vault/broker/server.js +68 -7
- package/package.json +1 -1
- package/profiles/default/CLAUDE.md.hbs +12 -13
- package/telegram-plugin/ask-user.ts +6 -7
- package/telegram-plugin/bridge/ipc-client.ts +17 -1
- package/telegram-plugin/dist/bridge/bridge.js +5 -2
- package/telegram-plugin/dist/gateway/gateway.js +410 -178
- package/telegram-plugin/dist/server.js +5 -2
- package/telegram-plugin/gateway/auth-broker-client.ts +1 -1
- package/telegram-plugin/gateway/auth-command.ts +4 -2
- package/telegram-plugin/gateway/boot-reason.ts +61 -0
- package/telegram-plugin/gateway/checklist-fallback.ts +8 -1
- package/telegram-plugin/gateway/cron-session.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +36 -34
- package/telegram-plugin/gateway/narrative-lane.ts +21 -1
- package/telegram-plugin/gateway/outbound-send-path.ts +9 -1
- package/telegram-plugin/gateway/represent-delivery-guard.ts +33 -2
- package/telegram-plugin/gateway/stream-render.ts +11 -2
- package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +21 -1
- package/telegram-plugin/gateway/subagent-handback-marker.ts +94 -0
- package/telegram-plugin/gateway/throttle-tier-wiring.ts +93 -18
- package/telegram-plugin/render/emphasis-guard.ts +92 -12
- package/telegram-plugin/render/line-start-guard.ts +27 -2
- package/telegram-plugin/sticker-aliases.ts +12 -14
- package/telegram-plugin/tests/ask-user.test.ts +15 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +88 -0
- package/telegram-plugin/tests/checklist-fallback.test.ts +21 -0
- package/telegram-plugin/tests/cron-bridge-drain-spool-ack.test.ts +150 -0
- package/telegram-plugin/tests/handback-tasknotif-dedup.test.ts +248 -0
- package/telegram-plugin/tests/ipc-client-reconnect-rejection.test.ts +70 -0
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +86 -0
- package/telegram-plugin/tests/queued-card-surface.test.ts +66 -0
- package/telegram-plugin/tests/render/emphasis-guard.test.ts +105 -6
- package/telegram-plugin/tests/render/heading-guard-blockquote-glued-hash.test.ts +123 -36
- package/telegram-plugin/tests/reply-quote-wire.test.ts +47 -0
- package/telegram-plugin/tests/represent-guard.test.ts +45 -0
- package/telegram-plugin/tests/sticker-aliases.test.ts +43 -0
- package/telegram-plugin/tests/throttle-tier-probe-only.test.ts +216 -0
- package/telegram-plugin/tests/throttle-tier-route-429-wiring.test.ts +92 -0
- package/telegram-plugin/tests/throttle-tier-route-429.test.ts +71 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +83 -0
- package/telegram-plugin/throttle-tier.ts +59 -0
- package/telegram-plugin/turn-flush-safety.ts +79 -0
|
@@ -78,18 +78,117 @@ describe("guardAccidentalEmphasis (#3252) — intended emphasis is LEFT UNTOUCHE
|
|
|
78
78
|
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
it("leaves
|
|
82
|
-
const s = "
|
|
81
|
+
it("leaves a lone glob (`*.ts`) verbatim", () => {
|
|
82
|
+
const s = "match *.ts files only";
|
|
83
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("guardAccidentalEmphasis (#3464) — boundary-flanked `*` (whitespace/boundary on BOTH sides) IS escaped", () => {
|
|
88
|
+
// A `*` with whitespace or a string boundary on both immediate sides is
|
|
89
|
+
// neither left- nor right-flanking under GFM — it can never open or close
|
|
90
|
+
// emphasis, so escaping it is always safe and never touches an intended span.
|
|
91
|
+
it("escapes a space-flanked operator `3 * 4` (flipped from the old leave-alone pin)", () => {
|
|
92
|
+
const out = guardAccidentalEmphasis("the product 3 * 4 equals 12");
|
|
93
|
+
expect(out).toBe("the product 3 \\* 4 equals 12");
|
|
94
|
+
expect(copyText(out)).toBe("the product 3 * 4 equals 12");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("escapes a bare trailing glob `rm *` (space before, EOL after — boundary both sides)", () => {
|
|
98
|
+
// `run rm * to clear` — the `*` is whitespace-flanked on both sides.
|
|
99
|
+
const out = guardAccidentalEmphasis("run rm * to clear the dir");
|
|
100
|
+
expect(out).toBe("run rm \\* to clear the dir");
|
|
101
|
+
expect(copyText(out)).toBe("run rm * to clear the dir");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("escapes a single space-flanked `*` in `a * b`", () => {
|
|
105
|
+
const out = guardAccidentalEmphasis("compute a * b now");
|
|
106
|
+
expect(out).toBe("compute a \\* b now");
|
|
107
|
+
expect(copyText(out)).toBe("compute a * b now");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("escapes BOTH space-flanked `*` in `a * b * c`", () => {
|
|
111
|
+
const out = guardAccidentalEmphasis("compute a * b * c now");
|
|
112
|
+
expect(out).toBe("compute a \\* b \\* c now");
|
|
113
|
+
expect(copyText(out)).toBe("compute a * b * c now");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("escapes a `*` at the string end (` *`) — boundary on the trailing side", () => {
|
|
117
|
+
const out = guardAccidentalEmphasis("clear with rm *");
|
|
118
|
+
expect(out).toBe("clear with rm \\*");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("PRESERVES a line-leading `* ` — it is a list BULLET, not an operator (#3464 blocker)", () => {
|
|
122
|
+
// A line-leading `* ` cannot be disambiguated from a `*`-bullet, and Telegram
|
|
123
|
+
// renders it as a bullet regardless — matching origin/main. Escaping it would
|
|
124
|
+
// break `*`-bullets on every reply AND (since this arm runs before the
|
|
125
|
+
// heading guard) disarm the glued-`#` fix for `* #4382`. So it stays verbatim.
|
|
126
|
+
const s = "* is the multiply op";
|
|
83
127
|
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
84
128
|
});
|
|
85
129
|
|
|
86
|
-
it("
|
|
87
|
-
|
|
130
|
+
it("escapes a bare lone `*` (boundary on both sides, no trailing space → not a bullet)", () => {
|
|
131
|
+
expect(guardAccidentalEmphasis("*")).toBe("\\*");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("leaves `*italic*` untouched (delimiters word-adjacent on the inner side)", () => {
|
|
135
|
+
const s = "this is *italic* text";
|
|
88
136
|
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
89
137
|
});
|
|
90
138
|
|
|
91
|
-
it("leaves
|
|
92
|
-
const s = "
|
|
139
|
+
it("leaves `**bold**` untouched", () => {
|
|
140
|
+
const s = "this is **bold** text";
|
|
141
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("leaves intra-word `x*y` behaviour to the intra-word arm (single `*`, not boundary-flanked)", () => {
|
|
145
|
+
// A lone intra-word `*` (one asterisk total) stays byte-identical per the
|
|
146
|
+
// pair threshold; the boundary arm does not match it (alnum on both sides).
|
|
147
|
+
const s = "the value x*y here";
|
|
148
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("is idempotent on a boundary-flanked escape (double-apply is byte-identical)", () => {
|
|
152
|
+
const once = guardAccidentalEmphasis("a * b and c * d");
|
|
153
|
+
expect(guardAccidentalEmphasis(once)).toBe(once);
|
|
154
|
+
expect(once).not.toContain("\\\\*");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("escapes a `*` alone on its own line (`\\n` counts as whitespace, no bullet)", () => {
|
|
158
|
+
const out = guardAccidentalEmphasis("line one\n*\nline two");
|
|
159
|
+
expect(out).toBe("line one\n\\*\nline two");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("guardAccidentalEmphasis (#3464) — line-leading `*` BULLETS are preserved (blocker 2)", () => {
|
|
164
|
+
it("leaves a multi-line `*`-bullet list unchanged", () => {
|
|
165
|
+
const s = "* bullet a\n* bullet b";
|
|
166
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("leaves a mixed `*`/`-` bullet list unchanged", () => {
|
|
170
|
+
const s = "* one\n- two\n* three";
|
|
171
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("leaves `+ item` and `- item` bullets unchanged (they carry no `*`)", () => {
|
|
175
|
+
expect(guardAccidentalEmphasis("+ item")).toBe("+ item");
|
|
176
|
+
expect(guardAccidentalEmphasis("- item")).toBe("- item");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("leaves an indented (≤3 space) `*` bullet unchanged", () => {
|
|
180
|
+
const s = " * indented bullet";
|
|
181
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("STILL escapes a non-bullet `*` on a line that ALSO has a `*` bullet", () => {
|
|
185
|
+
// Line 1 is a bullet (preserved); line 2 has a space-flanked operator (escaped).
|
|
186
|
+
const out = guardAccidentalEmphasis("* bullet\ncompute a * b");
|
|
187
|
+
expect(out).toBe("* bullet\ncompute a \\* b");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("is a strict no-op for a pure `*`-bullet list (arm stays disarmed)", () => {
|
|
191
|
+
const s = "* a\n* b\n* c";
|
|
93
192
|
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
94
193
|
});
|
|
95
194
|
});
|
|
@@ -14,49 +14,43 @@ import { guardAccidentalFormatting } from "../../rich-send.js";
|
|
|
14
14
|
// On the renderer-BYPASS seam (cards / banners / status / approval sends), no
|
|
15
15
|
// such belt runs, so the glued `#` reaches Telegram unescaped.
|
|
16
16
|
//
|
|
17
|
-
// ──
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
17
|
+
// ── Live UAT confirmed the promotion → these pins are now FLIPPED (#3464) ─────
|
|
18
|
+
// The question was a fact we could NOT determine from the byte stream: does
|
|
19
|
+
// Telegram's non-spec Bot API rich parser actually promote a space-less `#` to a
|
|
20
|
+
// heading when it sits AFTER a `>`/list marker, the way it demonstrably does at
|
|
21
|
+
// a bare line start (`#3460` → giant heading, #3306/#3463)?
|
|
22
22
|
// - CommonMark treats `> #3460` as a blockquote whose content is the paragraph
|
|
23
23
|
// `#3460` (no ATX heading — no space after `#`); `> # Heading` (WITH space)
|
|
24
24
|
// is a real nested heading. Telegram's promotion of the SPACE-LESS form is
|
|
25
|
-
// the documented non-spec deviation
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// wrong "fix" adding stray backslashes would itself corrupt legitimate
|
|
36
|
-
// formatting — the exact thing to avoid. So this test DOCUMENTS the current,
|
|
37
|
-
// deliberately-conservative behavior; if live UAT later confirms Telegram DOES
|
|
38
|
-
// promote here, extend the guard and flip these expectations in the same PR.
|
|
25
|
+
// the documented non-spec deviation.
|
|
26
|
+
// Issue #3464 said: "Verify against Telegram live-UAT whether the non-spec
|
|
27
|
+
// heading promotion actually fires inside blockquotes/lists before adding
|
|
28
|
+
// escaping (avoid stray backslashes if it does not)." That live UAT has now run
|
|
29
|
+
// and CONFIRMED the promotion fires in the nested position — a `#` glued after a
|
|
30
|
+
// `>`/list marker is promoted to a heading exactly as at a bare line start. So
|
|
31
|
+
// `guardAccidentalHeading` now escapes it (via `ACCIDENTAL_HEADING_AFTER_MARKER`
|
|
32
|
+
// in line-start-guard.ts), and the previously-pinned "left untouched"
|
|
33
|
+
// expectations are flipped to assert the escaped outcome. A real nested heading
|
|
34
|
+
// (`> # Heading`, space AFTER the `#`) and a bare `#` remain untouched (below).
|
|
39
35
|
|
|
40
|
-
describe("guardAccidentalHeading — glued `#` after a blockquote/list marker
|
|
41
|
-
it("
|
|
42
|
-
expect(guardAccidentalHeading("> #3460 done")).toBe(">
|
|
36
|
+
describe("guardAccidentalHeading — glued `#` after a blockquote/list marker IS escaped (#3464, live-UAT confirmed)", () => {
|
|
37
|
+
it("escapes `> #3460` (glued hash after a blockquote marker)", () => {
|
|
38
|
+
expect(guardAccidentalHeading("> #3460 done")).toBe("> \\#3460 done");
|
|
43
39
|
});
|
|
44
40
|
|
|
45
|
-
it("
|
|
46
|
-
expect(guardAccidentalHeading("- #3460 done")).toBe("-
|
|
41
|
+
it("escapes `- #3460` (glued hash after an unordered-list marker)", () => {
|
|
42
|
+
expect(guardAccidentalHeading("- #3460 done")).toBe("- \\#3460 done");
|
|
47
43
|
});
|
|
48
44
|
|
|
49
|
-
it("
|
|
50
|
-
expect(guardAccidentalHeading("* #3460 x")).toBe("*
|
|
45
|
+
it("escapes `* #3460` (glued hash after a `*` bullet)", () => {
|
|
46
|
+
expect(guardAccidentalHeading("* #3460 x")).toBe("* \\#3460 x");
|
|
51
47
|
});
|
|
52
48
|
|
|
53
|
-
it("
|
|
54
|
-
expect(guardAccidentalHeading("1. #3460 x")).toBe("1.
|
|
49
|
+
it("escapes `1. #3460` (glued hash after an ordered-list marker)", () => {
|
|
50
|
+
expect(guardAccidentalHeading("1. #3460 x")).toBe("1. \\#3460 x");
|
|
55
51
|
});
|
|
56
52
|
|
|
57
|
-
it("still escapes the SAME `#3460` at a bare line start (the confirmed case)", () => {
|
|
58
|
-
// Proves the untouched results above are the `^`-anchor scope, not the guard
|
|
59
|
-
// being disabled: at a real line start the accidental heading IS escaped.
|
|
53
|
+
it("still escapes the SAME `#3460` at a bare line start (the original confirmed case)", () => {
|
|
60
54
|
expect(guardAccidentalHeading("#3460 done")).toBe("\\#3460 done");
|
|
61
55
|
});
|
|
62
56
|
});
|
|
@@ -71,16 +65,109 @@ describe("guardAccidentalHeading — a real nested heading (space form) must sta
|
|
|
71
65
|
});
|
|
72
66
|
});
|
|
73
67
|
|
|
74
|
-
describe("guardAccidentalFormatting (universal seam) —
|
|
75
|
-
it("
|
|
76
|
-
expect(guardAccidentalFormatting("> #3460 done")).toBe(">
|
|
68
|
+
describe("guardAccidentalFormatting (universal seam) — glued `#` after a marker IS escaped end-to-end (#3464)", () => {
|
|
69
|
+
it("escapes `> #3460` through the full composition", () => {
|
|
70
|
+
expect(guardAccidentalFormatting("> #3460 done")).toBe("> \\#3460 done");
|
|
77
71
|
});
|
|
78
72
|
|
|
79
|
-
it("
|
|
80
|
-
expect(guardAccidentalFormatting("- #3460 done")).toBe("-
|
|
73
|
+
it("escapes `- #3460` through the full composition", () => {
|
|
74
|
+
expect(guardAccidentalFormatting("- #3460 done")).toBe("- \\#3460 done");
|
|
81
75
|
});
|
|
82
76
|
|
|
83
77
|
it("still escapes a bare line-leading `#3460` at the seam (control)", () => {
|
|
84
78
|
expect(guardAccidentalFormatting("#3460 done")).toBe("\\#3460 done");
|
|
85
79
|
});
|
|
86
80
|
});
|
|
81
|
+
|
|
82
|
+
describe("guardAccidentalHeading (#3464) — every list/blockquote marker shape + nesting is escaped", () => {
|
|
83
|
+
it("escapes `- #4382's x` (apostrophe-suffixed hash after a `-` bullet)", () => {
|
|
84
|
+
expect(guardAccidentalHeading("- #4382's x")).toBe("- \\#4382's x");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("escapes `* #x` (after a `*` bullet)", () => {
|
|
88
|
+
expect(guardAccidentalHeading("* #x")).toBe("* \\#x");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("escapes `+ #x` (after a `+` bullet)", () => {
|
|
92
|
+
expect(guardAccidentalHeading("+ #x")).toBe("+ \\#x");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("escapes `1. #x` (after a `.`-delimited ordered marker)", () => {
|
|
96
|
+
expect(guardAccidentalHeading("1. #x")).toBe("1. \\#x");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("escapes `1) #x` (after a `)`-delimited ordered marker)", () => {
|
|
100
|
+
expect(guardAccidentalHeading("1) #x")).toBe("1) \\#x");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("escapes `> #x` (after a blockquote marker)", () => {
|
|
104
|
+
expect(guardAccidentalHeading("> #x")).toBe("> \\#x");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("escapes nested `- > #x` (a bullet then a blockquote marker)", () => {
|
|
108
|
+
expect(guardAccidentalHeading("- > #x")).toBe("- > \\#x");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("guardAccidentalHeading (#3464) — invariants that must stay untouched", () => {
|
|
113
|
+
it("leaves `- # Heading` (real nested heading, space after `#`)", () => {
|
|
114
|
+
expect(guardAccidentalHeading("- # Heading")).toBe("- # Heading");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("leaves `# Heading` (real bare heading, space after `#`)", () => {
|
|
118
|
+
expect(guardAccidentalHeading("# Heading")).toBe("# Heading");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("still escapes bare `#4382 done` (unchanged from before this PR)", () => {
|
|
122
|
+
expect(guardAccidentalHeading("#4382 done")).toBe("\\#4382 done");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("guardAccidentalFormatting (#3464) — `*`-bullet marker survives emphasis AND the glued `#` is escaped (blocker 1, end-to-end)", () => {
|
|
127
|
+
// The composed pipeline (rich-send.ts) runs guardAccidentalEmphasis BEFORE
|
|
128
|
+
// guardAccidentalHeading. If the emphasis arm escaped the leading `* ` bullet,
|
|
129
|
+
// the LITERAL `*` marker the heading guard needs would be gone and the glued
|
|
130
|
+
// `#` would NOT be escaped. These tests exercise the REAL ordering — the unit
|
|
131
|
+
// tests above call the heading guard in isolation and cannot catch that.
|
|
132
|
+
it("escapes `* #3460 x` end-to-end (marker preserved, `#` escaped)", () => {
|
|
133
|
+
expect(guardAccidentalFormatting("* #3460 x")).toBe("* \\#3460 x");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("escapes `* #x` end-to-end", () => {
|
|
137
|
+
expect(guardAccidentalFormatting("* #x")).toBe("* \\#x");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("escapes `- #3460 x` end-to-end (regression guard — `-` marker never touched)", () => {
|
|
141
|
+
expect(guardAccidentalFormatting("- #3460 x")).toBe("- \\#3460 x");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("escapes `> #x` end-to-end", () => {
|
|
145
|
+
expect(guardAccidentalFormatting("> #x")).toBe("> \\#x");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("leaves a plain `*`-bullet list unchanged end-to-end (no glued `#`)", () => {
|
|
149
|
+
const s = "* bullet a\n* bullet b";
|
|
150
|
+
expect(guardAccidentalFormatting(s)).toBe(s);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("leaves a mixed `*`/`-` bullet list unchanged end-to-end", () => {
|
|
154
|
+
const s = "* one\n- two\n* three";
|
|
155
|
+
expect(guardAccidentalFormatting(s)).toBe(s);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("guardAccidentalHeading (#3464) — idempotence (double-apply is byte-identical)", () => {
|
|
160
|
+
for (const input of [
|
|
161
|
+
"- #4382 done",
|
|
162
|
+
"> #x",
|
|
163
|
+
"- > #x",
|
|
164
|
+
"1. #x",
|
|
165
|
+
"#4382 done",
|
|
166
|
+
]) {
|
|
167
|
+
it(`is idempotent on ${JSON.stringify(input)}`, () => {
|
|
168
|
+
const once = guardAccidentalHeading(input);
|
|
169
|
+
expect(guardAccidentalHeading(once)).toBe(once);
|
|
170
|
+
expect(once).not.toContain("\\\\#");
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
@@ -433,3 +433,50 @@ describe('quote_text lands on the wire as ReplyParameters.quote (a String)', ()
|
|
|
433
433
|
expect(long.startsWith(quote)).toBe(true)
|
|
434
434
|
})
|
|
435
435
|
})
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* #4368 — a fabricated reply anchor on the wire.
|
|
439
|
+
*
|
|
440
|
+
* The model's `reply` tool can quote a SYNTHETIC inbound: a boot-resume /
|
|
441
|
+
* subagent-handback / cron turn fabricates a `message_id` from `Date.now()`
|
|
442
|
+
* (~1.78e13). Telegram's `reply_parameters.message_id` HARD-rejects anything
|
|
443
|
+
* out of the signed-int32 range (400 `field "message_id" must be a valid
|
|
444
|
+
* Number`), and `allow_sending_without_reply` does NOT bypass that — so quoting
|
|
445
|
+
* a synthetic id used to 400 EVERY chunk of the reply, losing the answer.
|
|
446
|
+
*
|
|
447
|
+
* `executeReply` (whose body is `sendReply`) must route `args.reply_to` through
|
|
448
|
+
* `parseSourceMessageId` so an out-of-range anchor is DROPPED and the reply
|
|
449
|
+
* lands UNANCHORED. This reads what actually goes over the wire — a builder-
|
|
450
|
+
* level assertion would not catch a future opts transform re-introducing it.
|
|
451
|
+
*/
|
|
452
|
+
describe('synthetic reply anchor is dropped on the wire (#4368)', () => {
|
|
453
|
+
it('quoting an out-of-int32 reply_to sends UNANCHORED and still delivers', async () => {
|
|
454
|
+
const h = makeHarness()
|
|
455
|
+
const res = await sendReply(h.deps, {
|
|
456
|
+
args: {
|
|
457
|
+
chat_id: CHAT,
|
|
458
|
+
text: 'The answer must land even when the quoted inbound was synthetic.',
|
|
459
|
+
reply_to: 1_785_000_000_000,
|
|
460
|
+
},
|
|
461
|
+
turn: null,
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const sends = h.sends()
|
|
465
|
+
expect(sends.length).toBeGreaterThan(0)
|
|
466
|
+
// The fabricated id never reaches the wire: no reply_parameters at all …
|
|
467
|
+
expect(h.replyParams(sends[0])).toBeUndefined()
|
|
468
|
+
// … and specifically no out-of-range message_id anywhere in the payload.
|
|
469
|
+
expect(JSON.stringify(sends[0].payload)).not.toContain('1785000000000')
|
|
470
|
+
// … and the answer actually delivered (not a failure notice).
|
|
471
|
+
expect(res.content[0]?.text ?? '').toMatch(/sent/i)
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('a real (in-range) reply_to is still honored as a quote anchor', async () => {
|
|
475
|
+
const h = makeHarness()
|
|
476
|
+
await sendReply(h.deps, {
|
|
477
|
+
args: { chat_id: CHAT, text: 'ok', reply_to: 4242 },
|
|
478
|
+
turn: null,
|
|
479
|
+
})
|
|
480
|
+
expect(h.replyParams(h.sends()[0])!).toEqual({ message_id: 4242 })
|
|
481
|
+
})
|
|
482
|
+
})
|
|
@@ -336,6 +336,51 @@ describe("makeSessionBusyDrainDeferral — F2 bounded busy-defer for the idle dr
|
|
|
336
336
|
expect(off(true, 0)).toBe(false);
|
|
337
337
|
expect(off(true, 10_000)).toBe(false);
|
|
338
338
|
});
|
|
339
|
+
|
|
340
|
+
it("starts a FRESH bound for a new deferral episode after a call gap — a buffer emptied by bridge re-register (no busy=false call) must not pin a stale t0 (#4341 follow-up)", () => {
|
|
341
|
+
const BOUND = 20_000; // small bound so a realistic 5s poll cadence spans it
|
|
342
|
+
const defer = makeSessionBusyDrainDeferral(BOUND); // default staleGap = 15s
|
|
343
|
+
|
|
344
|
+
// Episode A: a represent is buffered while the session is busy. The idle
|
|
345
|
+
// drain gate polls it every ~5s (< staleGap) and defers each time.
|
|
346
|
+
expect(defer(true, 0)).toBe(true); // t0 = 0
|
|
347
|
+
expect(defer(true, 5_000)).toBe(true);
|
|
348
|
+
expect(defer(true, 10_000)).toBe(true);
|
|
349
|
+
|
|
350
|
+
// The buffer is now emptied by a bridge re-register (onClientRegistered)
|
|
351
|
+
// while the session is STILL busy. That drain path does NOT consult this
|
|
352
|
+
// predicate, so there is no busy=false call — deferringSince stays at 0 on
|
|
353
|
+
// the buggy code. Time then advances well past the bound with the buffer
|
|
354
|
+
// empty (predicate not called).
|
|
355
|
+
|
|
356
|
+
// Episode B: a brand-new represent is buffered while the session is busy,
|
|
357
|
+
// long after t0. This is a NEW mid-answer session — the represent MUST stay
|
|
358
|
+
// deferred (its own bound has not elapsed), NOT be drained immediately.
|
|
359
|
+
// Buggy code: now - stalePinnedT0 (2_000_000 - 0) >= BOUND → false → the
|
|
360
|
+
// represent is drained into the mid-answer session, reopening the duplicate
|
|
361
|
+
// window. Fixed code: the >15s call gap starts a fresh episode clock at
|
|
362
|
+
// t=2_000_000, so it defers.
|
|
363
|
+
const B0 = 2_000_000;
|
|
364
|
+
expect(defer(true, B0)).toBe(true);
|
|
365
|
+
// ...and the fresh episode is still bounded from ITS OWN start, polled at the
|
|
366
|
+
// real ~5s cadence (each gap < staleGap, so no further reset).
|
|
367
|
+
expect(defer(true, B0 + 5_000)).toBe(true);
|
|
368
|
+
expect(defer(true, B0 + 10_000)).toBe(true);
|
|
369
|
+
expect(defer(true, B0 + 15_000)).toBe(true);
|
|
370
|
+
expect(defer(true, B0 + BOUND)).toBe(false); // fresh bound elapsed → drains
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("does NOT reset the clock on the normal poll cadence — a genuinely wedged busy session still drains at the bound (wedge budget preserved)", () => {
|
|
374
|
+
const BOUND = 20_000;
|
|
375
|
+
const defer = makeSessionBusyDrainDeferral(BOUND); // default staleGap = 15s
|
|
376
|
+
// Polls arrive every 5s (< staleGap) throughout one continuous episode, so
|
|
377
|
+
// the clock is never reset and the bound elapses on schedule.
|
|
378
|
+
expect(defer(true, 0)).toBe(true);
|
|
379
|
+
expect(defer(true, 5_000)).toBe(true);
|
|
380
|
+
expect(defer(true, 10_000)).toBe(true);
|
|
381
|
+
expect(defer(true, 15_000)).toBe(true);
|
|
382
|
+
expect(defer(true, 20_000)).toBe(false); // bound reached — drains, not silenced forever
|
|
383
|
+
});
|
|
339
384
|
});
|
|
340
385
|
|
|
341
386
|
describe("obligationSweep — F2 decision half: a poke-cleared-but-busy session defers, then re-asks (bounded)", () => {
|
|
@@ -230,3 +230,46 @@ describe('resolveGifSendArgs', () => {
|
|
|
230
230
|
expect(r.replyTo).toBe(88)
|
|
231
231
|
})
|
|
232
232
|
})
|
|
233
|
+
|
|
234
|
+
// #4368 — a fabricated reply anchor (a synthetic boot-resume/handback/cron
|
|
235
|
+
// inbound id at `Date.now()` scale, ~1.78e13) is out of the signed-int32 range
|
|
236
|
+
// Telegram accepts for `reply_parameters.message_id`. It must be DROPPED at the
|
|
237
|
+
// resolver boundary so the sticker/GIF still sends (unanchored) rather than the
|
|
238
|
+
// agent's echoed synthetic id 400ing the whole send. Before the fix these
|
|
239
|
+
// resolvers ran `Number(raw.reply_to)`, which is finite and > 0 for a 13-digit
|
|
240
|
+
// timestamp, so the fabricated id passed straight through onto the wire.
|
|
241
|
+
const SYNTHETIC_MESSAGE_ID = String(1_785_000_000_000)
|
|
242
|
+
|
|
243
|
+
describe('resolveStickerSendArgs — synthetic reply anchor (#4368)', () => {
|
|
244
|
+
it('drops an out-of-int32 reply_to so the sticker sends unanchored', () => {
|
|
245
|
+
const r = resolveStickerSendArgs(
|
|
246
|
+
{ chat_id: '1', sticker: SAMPLE_FILE_ID, reply_to: SYNTHETIC_MESSAGE_ID },
|
|
247
|
+
{},
|
|
248
|
+
)
|
|
249
|
+
expect(r.replyTo).toBeUndefined()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('still preserves a real (in-range) reply_to', () => {
|
|
253
|
+
const r = resolveStickerSendArgs(
|
|
254
|
+
{ chat_id: '1', sticker: SAMPLE_FILE_ID, reply_to: '4242' },
|
|
255
|
+
{},
|
|
256
|
+
)
|
|
257
|
+
expect(r.replyTo).toBe(4242)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('resolveGifSendArgs — synthetic reply anchor (#4368)', () => {
|
|
262
|
+
it('drops an out-of-int32 reply_to so the GIF sends unanchored', () => {
|
|
263
|
+
const r = resolveGifSendArgs({
|
|
264
|
+
chat_id: '1',
|
|
265
|
+
gif: SAMPLE_FILE_ID,
|
|
266
|
+
reply_to: SYNTHETIC_MESSAGE_ID,
|
|
267
|
+
})
|
|
268
|
+
expect(r.replyTo).toBeUndefined()
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('still preserves a real (in-range) reply_to', () => {
|
|
272
|
+
const r = resolveGifSendArgs({ chat_id: '1', gif: SAMPLE_FILE_ID, reply_to: '4242' })
|
|
273
|
+
expect(r.replyTo).toBe(4242)
|
|
274
|
+
})
|
|
275
|
+
})
|