switchroom 0.18.24 → 0.18.26
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 +59 -11
- package/dist/host-control/main.js +1 -1
- package/package.json +2 -2
- package/telegram-plugin/dist/bridge/bridge.js +26 -0
- package/telegram-plugin/dist/gateway/gateway.js +1827 -831
- package/telegram-plugin/dist/server.js +26 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +7 -0
- package/telegram-plugin/gateway/gateway.ts +314 -3
- package/telegram-plugin/gateway/model-command.ts +188 -56
- package/telegram-plugin/gateway/redelivery-decision.ts +139 -0
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +42 -1
- package/telegram-plugin/history.ts +118 -0
- package/telegram-plugin/registry/turns-schema.ts +89 -1
- package/telegram-plugin/render/code-segments.ts +210 -0
- package/telegram-plugin/render/dollar-math-guard.ts +126 -0
- package/telegram-plugin/render/emphasis-guard.ts +158 -0
- package/telegram-plugin/render/inline-pairs-guard.ts +171 -0
- package/telegram-plugin/render/line-start-guard.ts +167 -0
- package/telegram-plugin/render/rich-render.ts +7 -0
- package/telegram-plugin/rich-send.ts +48 -2
- package/telegram-plugin/session-tail.ts +185 -0
- package/telegram-plugin/subagent-watcher.ts +45 -0
- package/telegram-plugin/tests/crash-redelivery-resume-exclusion.test.ts +133 -0
- package/telegram-plugin/tests/crash-redelivery-wiring.test.ts +72 -0
- package/telegram-plugin/tests/history.test.ts +91 -0
- package/telegram-plugin/tests/model-command.test.ts +189 -12
- package/telegram-plugin/tests/redelivery-decision.test.ts +84 -0
- package/telegram-plugin/tests/registry-turns.test.ts +51 -0
- package/telegram-plugin/tests/render/dollar-math-guard.test.ts +162 -0
- package/telegram-plugin/tests/render/emphasis-guard.test.ts +205 -0
- package/telegram-plugin/tests/render/guard-composition.test.ts +138 -0
- package/telegram-plugin/tests/render/inline-pairs-guard.test.ts +171 -0
- package/telegram-plugin/tests/render/line-start-guard.test.ts +164 -0
- package/telegram-plugin/tests/session-model-source.test.ts +11 -0
- package/telegram-plugin/tests/session-tail.test.ts +145 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +50 -0
- package/telegram-plugin/tests/tool-activity-summary.test.ts +109 -0
- package/telegram-plugin/tests/trailing-answer-projector.test.ts +124 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +125 -0
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +306 -0
- package/telegram-plugin/tool-activity-summary.ts +54 -3
- package/telegram-plugin/worker-activity-feed.ts +104 -0
- package/vendor/hindsight-memory/scripts/backfill_transcripts.py +762 -0
- package/vendor/hindsight-memory/scripts/drain_pending.py +13 -1
- package/vendor/hindsight-memory/scripts/lib/client.py +14 -4
- package/vendor/hindsight-memory/scripts/lib/config.py +8 -0
- package/vendor/hindsight-memory/scripts/lib/pacing.py +102 -0
- package/vendor/hindsight-memory/scripts/lib/watermark.py +213 -0
- package/vendor/hindsight-memory/scripts/reconcile_tail.py +344 -0
- package/vendor/hindsight-memory/scripts/retain.py +299 -143
- package/vendor/hindsight-memory/scripts/session_start.py +14 -0
- package/vendor/hindsight-memory/scripts/tests/test_backfill.py +362 -0
- package/vendor/hindsight-memory/scripts/tests/test_reconcile_durability.py +350 -0
- package/vendor/hindsight-memory/tests/test_hooks.py +8 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { guardAccidentalEmphasis } from "../../render/emphasis-guard.js";
|
|
3
|
+
|
|
4
|
+
// Any U+1D400–U+1D7FF codepoint would be a mathematical-alphanumeric glyph; the
|
|
5
|
+
// source never emits them — kept as a canary that the guard adds none.
|
|
6
|
+
const MATH_GLYPH = /[\u{1D400}-\u{1D7FF}]/u;
|
|
7
|
+
|
|
8
|
+
/** Strip defusing backslashes so we can assert the reader-visible/copy text is
|
|
9
|
+
* byte-identical to the original prose. */
|
|
10
|
+
function copyText(s: string): string {
|
|
11
|
+
return s.replace(/\\([_*])/g, "$1");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("guardAccidentalEmphasis (#3252) — accidental emphasis IS neutralised", () => {
|
|
15
|
+
it("escapes intra-word underscores in a snake_case identifier", () => {
|
|
16
|
+
const out = guardAccidentalEmphasis("the var is file_name_here in scope");
|
|
17
|
+
// No unescaped intra-word `_` remains to pair into an italic span.
|
|
18
|
+
expect(out).toContain("file\\_name\\_here");
|
|
19
|
+
expect(copyText(out)).toBe("the var is file_name_here in scope");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("escapes intra-word asterisks in inline multiplication `a*b*c`", () => {
|
|
23
|
+
const out = guardAccidentalEmphasis("compute a*b*c for the product");
|
|
24
|
+
expect(out).toContain("a\\*b\\*c");
|
|
25
|
+
expect(copyText(out)).toBe("compute a*b*c for the product");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("escapes digit-flanked `2*3*4` multiplication (2+ asterisks can pair)", () => {
|
|
29
|
+
const out = guardAccidentalEmphasis("the area is 2*3*4 units");
|
|
30
|
+
expect(out).toContain("2\\*3\\*4");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("escapes a mixed digit/letter intra-word underscore run `v2_final_rc`", () => {
|
|
34
|
+
const out = guardAccidentalEmphasis("ship v2_final_rc today");
|
|
35
|
+
expect(out).toContain("v2\\_final\\_rc");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("adds no math-italic glyphs", () => {
|
|
39
|
+
expect(guardAccidentalEmphasis("file_name_here and a*b")).not.toMatch(MATH_GLYPH);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("guardAccidentalEmphasis (#3252) — intended emphasis is LEFT UNTOUCHED (the critical no-false-positive cases)", () => {
|
|
44
|
+
it("leaves `**bold**` verbatim", () => {
|
|
45
|
+
const s = "this is **bold** text";
|
|
46
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("leaves `*italic*` verbatim", () => {
|
|
50
|
+
const s = "this is *italic* text";
|
|
51
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("leaves `_italic_` verbatim", () => {
|
|
55
|
+
const s = "this is _italic_ text";
|
|
56
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("leaves a multi-word `*italic phrase*` verbatim (boundary-flanked delimiters)", () => {
|
|
60
|
+
const s = "please read *the whole thing* carefully";
|
|
61
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("leaves `**bold**` and `_italic_` mixed in one line verbatim", () => {
|
|
65
|
+
const s = "**Header** then _emphasis_ and more **bold**";
|
|
66
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("leaves an intended italic that WRAPS a glob (`*.ts is a glob*`) verbatim", () => {
|
|
70
|
+
// The opening `*` is boundary-flanked (space before, `.` after) → not
|
|
71
|
+
// intra-word → deliberately left alone so the intended italic survives.
|
|
72
|
+
const s = "note: *.ts is a glob* pattern";
|
|
73
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("leaves a leading-underscore identifier `_private` verbatim (indistinguishable from italic)", () => {
|
|
77
|
+
const s = "the _private field and _internal state";
|
|
78
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("leaves space-flanked operators (`3 * 4`) verbatim — GFM cannot emphasise them", () => {
|
|
82
|
+
const s = "the product 3 * 4 equals 12";
|
|
83
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("leaves a bare trailing glob (`rm *`) verbatim", () => {
|
|
87
|
+
const s = "run rm * to clear the dir";
|
|
88
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("leaves a lone glob (`*.ts`) verbatim", () => {
|
|
92
|
+
const s = "match *.ts files only";
|
|
93
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("guardAccidentalEmphasis (#3252) — pair threshold (finding 4)", () => {
|
|
98
|
+
// A single delimiter in the whole message can NEVER form an emphasis pair, so
|
|
99
|
+
// a lone intra-word `_`/`*` is provably inert and is left byte-identical.
|
|
100
|
+
it("leaves a LONE intra-word underscore `file_name` verbatim (can't pair)", () => {
|
|
101
|
+
const s = "the var is file_name in scope";
|
|
102
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("leaves a LONE intra-word asterisk `2*3` verbatim (can't pair)", () => {
|
|
106
|
+
const s = "the area is 2*3 units";
|
|
107
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("STILL escapes a lone intra-word `_` when a later `_italic_` gives it a pair", () => {
|
|
111
|
+
// Three underscores total (one intra-word + an intended `_italic_`); Telegram
|
|
112
|
+
// could mis-pair them, so the intra-word `_` MUST be escaped even though it is
|
|
113
|
+
// the only intra-word one. Threshold 2 (total count) catches this correctly.
|
|
114
|
+
const out = guardAccidentalEmphasis("the file_name and _italic_ here");
|
|
115
|
+
expect(out).toContain("file\\_name");
|
|
116
|
+
// The intended italic delimiters (boundary-flanked) are left untouched.
|
|
117
|
+
expect(out).toContain("_italic_");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("guardAccidentalEmphasis (#3252) — link / autolink awareness (finding 1)", () => {
|
|
122
|
+
it("does NOT escape intra-word `_` inside a markdown link destination", () => {
|
|
123
|
+
const s = "see [docs](https://x.io/a_b_c_d) for more";
|
|
124
|
+
// URL underscores are structural → passed through UNMODIFIED.
|
|
125
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("does NOT escape `_` inside a bare autolinked URL", () => {
|
|
129
|
+
const s = "read https://x.io/foo_bar_baz now";
|
|
130
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("STILL guards the link LABEL prose while protecting the URL", () => {
|
|
134
|
+
const out = guardAccidentalEmphasis("[file_name_here](https://x.io/a_b_c)");
|
|
135
|
+
// Label (rendered prose) is escaped; the `(url)` destination is verbatim.
|
|
136
|
+
expect(out).toContain("[file\\_name\\_here]");
|
|
137
|
+
expect(out).toContain("(https://x.io/a_b_c)");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("protects a link destination WITH a title", () => {
|
|
141
|
+
const s = 'see [docs](https://x.io/a_b_c "the_title") now';
|
|
142
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("guardAccidentalEmphasis (#3252) — trailing-alnum emphasis closer (finding 5)", () => {
|
|
147
|
+
// `*cat*s` / `_cat_s`: the closing delimiter is intra-word (t*s / t_s). Under
|
|
148
|
+
// strict CommonMark it cannot close emphasis, but Telegram may be more liberal
|
|
149
|
+
// (UAT-gated). Documented current behaviour: the intra-word closer is escaped
|
|
150
|
+
// (2 asterisks/underscores total → armed). Copy-round-trips to the original.
|
|
151
|
+
it("escapes the intra-word closer of `*cat*s`", () => {
|
|
152
|
+
const out = guardAccidentalEmphasis("a *cat*s tail");
|
|
153
|
+
expect(out).toContain("*cat\\*s");
|
|
154
|
+
expect(copyText(out)).toBe("a *cat*s tail");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("escapes the intra-word closer of `_cat_s`", () => {
|
|
158
|
+
const out = guardAccidentalEmphasis("a _cat_s tail");
|
|
159
|
+
expect(out).toContain("_cat\\_s");
|
|
160
|
+
expect(copyText(out)).toBe("a _cat_s tail");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("guardAccidentalEmphasis (#3252) — code-span awareness", () => {
|
|
165
|
+
it("never touches intra-word `_`/`*` inside a code span", () => {
|
|
166
|
+
const s = "the identifier `file_name_here` and math `a*b*c` are code";
|
|
167
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("escapes prose but leaves an adjacent code span verbatim", () => {
|
|
171
|
+
// Two intra-word underscores in prose (`file_name_here`) → armed; the code
|
|
172
|
+
// span's `file_name` is verbatim and does NOT count toward the threshold.
|
|
173
|
+
const out = guardAccidentalEmphasis("prose file_name_here but code `file_name`");
|
|
174
|
+
expect(out).toContain("file\\_name\\_here but code `file_name`");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("never touches intra-word delimiters inside a fenced code block", () => {
|
|
178
|
+
const s = "```\nfoo_bar = a*b*c\n```\ntail";
|
|
179
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("guardAccidentalEmphasis (#3252) — no-op & idempotency", () => {
|
|
184
|
+
it("is a strict no-op when there is no `_` or `*` at all", () => {
|
|
185
|
+
const s = "a plain sentence with no delimiters at all";
|
|
186
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("is a strict no-op when the only `_`/`*` are intended emphasis", () => {
|
|
190
|
+
const s = "**bold** and _italic_ only";
|
|
191
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("is idempotent: guarding already-guarded text is a strict no-op", () => {
|
|
195
|
+
const once = guardAccidentalEmphasis("file_name_here and a*b");
|
|
196
|
+
expect(guardAccidentalEmphasis(once)).toBe(once);
|
|
197
|
+
expect(once).not.toContain("\\\\_"); // never a doubled backslash
|
|
198
|
+
expect(once).not.toContain("\\\\*");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("does not double-escape a pre-escaped `\\_` intra-word delimiter", () => {
|
|
202
|
+
const s = "file\\_name here";
|
|
203
|
+
expect(guardAccidentalEmphasis(s)).toBe(s);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { guardAccidentalFormatting, richMessage } from "../../rich-send.js";
|
|
3
|
+
|
|
4
|
+
// Any U+1D400–U+1D7FF codepoint = a mathematical-alphanumeric glyph (what a
|
|
5
|
+
// `$…$` math span typesets to on the reader's screen).
|
|
6
|
+
const MATH_GLYPH = /[\u{1D400}-\u{1D7FF}]/u;
|
|
7
|
+
|
|
8
|
+
/** Strip the defusing backslashes so we can assert the reader-visible / copied
|
|
9
|
+
* text is byte-identical to the original ASCII. */
|
|
10
|
+
function copyText(s: string): string {
|
|
11
|
+
return s.replace(/\\([$_*>.~=|])/g, "$1");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The composed seam guard (#3252): guardAccidentalEmphasis →
|
|
15
|
+
// guardAccidentalBlockConstructs → guardAccidentalInlinePairs → guardDollarMath.
|
|
16
|
+
// These tests prove the guards COMPOSE without one guard's inserted backslash
|
|
17
|
+
// creating or destroying a signal for another — the interference question.
|
|
18
|
+
describe("guardAccidentalFormatting composition (#3252)", () => {
|
|
19
|
+
it("neutralises a message that trips emphasis + dollar + tilde at once", () => {
|
|
20
|
+
// `file_name_here` (2 intra-word `_` → can pair), `~$5M … ~$10M`
|
|
21
|
+
// (digit-adjacent tildes AND a two-dollar currency span) — three guards fire.
|
|
22
|
+
const src = "the file_name_here budget trims ~$5M, down from ~$10M last year";
|
|
23
|
+
const out = guardAccidentalFormatting(src);
|
|
24
|
+
// Emphasis: the intra-word underscores are escaped (no `_…_` pair).
|
|
25
|
+
expect(out).toContain("file\\_name\\_here");
|
|
26
|
+
// Dollar: no unescaped `$` remains to open a math span.
|
|
27
|
+
expect(out).not.toMatch(/(?<!\\)\$/);
|
|
28
|
+
// Tilde: BOTH approximation tildes escaped so no `~…~` strikethrough forms.
|
|
29
|
+
// This is the interference case — dollar runs last so its `\$` never hides
|
|
30
|
+
// the digit-adjacent tildes from the inline-pairs guard.
|
|
31
|
+
expect(out).not.toMatch(/(?<!\\)~/);
|
|
32
|
+
expect(out).toContain("\\~\\$5M");
|
|
33
|
+
expect(out).toContain("\\~\\$10M");
|
|
34
|
+
// No math glyphs, and stripping the defusers yields the original prose.
|
|
35
|
+
expect(out).not.toMatch(MATH_GLYPH);
|
|
36
|
+
expect(copyText(out)).toBe(src);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("handles a line-start blockquote + operator-mark + dollar body", () => {
|
|
40
|
+
const src = ">2x growth means x==y and a==b while spend hit $40 and $80";
|
|
41
|
+
const out = guardAccidentalFormatting(src);
|
|
42
|
+
// Line-start `>2x` blockquote-promotion escaped.
|
|
43
|
+
expect(out).toContain("\\>2x");
|
|
44
|
+
// Both `==` operators neutralised (word-flanked → not intended ==mark==).
|
|
45
|
+
expect(out).toContain("x\\=\\=y");
|
|
46
|
+
expect(out).toContain("a\\=\\=b");
|
|
47
|
+
// Dollars escaped.
|
|
48
|
+
expect(out).not.toMatch(/(?<!\\)\$/);
|
|
49
|
+
expect(copyText(out)).toBe(src);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("leaves a 4-digit-year accidental ordered-list alone except the dot", () => {
|
|
53
|
+
const src = "2026. was the year the file_name_here convention changed";
|
|
54
|
+
const out = guardAccidentalFormatting(src);
|
|
55
|
+
expect(out).toContain("2026\\. was");
|
|
56
|
+
expect(out).toContain("file\\_name\\_here");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("never touches DELIBERATE formatting: bold, italic, strike, code, blockquote", () => {
|
|
60
|
+
const src =
|
|
61
|
+
"**bold** and *italic* and _under_ and ~~strike~~ and `a_b*c` and\n> real quote";
|
|
62
|
+
// No accidental signal anywhere → strict no-op.
|
|
63
|
+
expect(guardAccidentalFormatting(src)).toBe(src);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("never touches content inside code spans / fenced blocks", () => {
|
|
67
|
+
const src =
|
|
68
|
+
"prose $10 and $20 but the snippet `x==y && file_name ~5` stays verbatim";
|
|
69
|
+
const out = guardAccidentalFormatting(src);
|
|
70
|
+
expect(out).toContain("`x==y && file_name ~5`");
|
|
71
|
+
// Prose dollars still escaped outside the code span.
|
|
72
|
+
expect(out).not.toMatch(/(?<!\\)\$(?=\d)/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("is idempotent: composing already-composed text is a strict no-op", () => {
|
|
76
|
+
const src = "file_name_here ~$5M and ~$10M with x==y and a==b, >2x, 2026. done";
|
|
77
|
+
const once = guardAccidentalFormatting(src);
|
|
78
|
+
expect(guardAccidentalFormatting(once)).toBe(once);
|
|
79
|
+
// No doubled backslashes anywhere.
|
|
80
|
+
expect(once).not.toMatch(/\\\\/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("passes a markdown link with a `_`/`~`/`==`-laden URL through UNMODIFIED (finding 1)", () => {
|
|
84
|
+
const src = "see [docs](https://x.io/a_b_c?p==1&q~2) and [more](https://y.io/foo_bar)";
|
|
85
|
+
// Every trigger char in the URL is structural → the whole body is verbatim.
|
|
86
|
+
expect(guardAccidentalFormatting(src)).toBe(src);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("passes a bare autolinked URL through UNMODIFIED (finding 1)", () => {
|
|
90
|
+
const src = "read https://en.wikipedia.org/wiki/Foo_Bar_Baz today";
|
|
91
|
+
expect(guardAccidentalFormatting(src)).toBe(src);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("guards the link LABEL but protects the destination (finding 1)", () => {
|
|
95
|
+
const out = guardAccidentalFormatting("[a_b_c](https://x.io/p_q_r) spent $5 and $9");
|
|
96
|
+
// Label prose escaped; URL verbatim; prose dollars still escaped.
|
|
97
|
+
expect(out).toContain("[a\\_b\\_c]");
|
|
98
|
+
expect(out).toContain("(https://x.io/p_q_r)");
|
|
99
|
+
expect(out).not.toMatch(/(?<!\\)\$/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("passes a GFM table with empty cells through UNMODIFIED (finding 3)", () => {
|
|
103
|
+
// A real table (has a delimiter row) — its structural pipes/empty cells and
|
|
104
|
+
// any `_`/`$` inside cells must survive. `|a||b|` = a real empty cell.
|
|
105
|
+
const src = ["| A | B | C |", "| --- | --- | --- |", "|a||b||c|", "| x_y | $5 | ~3 |"].join("\n");
|
|
106
|
+
expect(guardAccidentalFormatting(src)).toBe(src);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("STILL guards a genuine accidental `a||b` in prose (not a table)", () => {
|
|
110
|
+
// Two word-flanked `||` in PROSE (no delimiter row) → armed spoiler guard.
|
|
111
|
+
const out = guardAccidentalFormatting("logic is a||b and c||d here");
|
|
112
|
+
expect(out).toContain("a\\|\\|b");
|
|
113
|
+
expect(out).toContain("c\\|\\|d");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// FINDING 2 (documented non-issue): switchroom's IR renderer emits code
|
|
117
|
+
// blocks ONLY as backtick FENCES (render.ts renderCodeBlock), never as
|
|
118
|
+
// 4-space-indented CommonMark code. So renderer output reaching richMessage()
|
|
119
|
+
// never contains an indented code block. The emphasis/inline-pairs/dollar
|
|
120
|
+
// guards therefore do NOT special-case a 4-space indent (only line-start does,
|
|
121
|
+
// for its own blockquote/list heuristics). This test PINS that current
|
|
122
|
+
// behaviour: a 4-space-indented prose line is treated as ordinary prose and
|
|
123
|
+
// its currency `$` is guarded like any other. (If a future change ever routes
|
|
124
|
+
// raw indented code blocks to the wire, revisit — see guards-integration-note.)
|
|
125
|
+
it("treats a 4-space-indented line as prose (finding 2 — renderer emits fences, not indented code)", () => {
|
|
126
|
+
const out = guardAccidentalFormatting(" spend was $5 and $10 total");
|
|
127
|
+
expect(out).toContain("\\$5");
|
|
128
|
+
expect(out).toContain("\\$10");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("richMessage() applies the composed guard on the wire body exactly once", () => {
|
|
132
|
+
const src = "file_name spend ~$5M vs ~$10M, x==y";
|
|
133
|
+
const wire = richMessage(src);
|
|
134
|
+
expect(wire.markdown).toBe(guardAccidentalFormatting(src));
|
|
135
|
+
// And re-wrapping (streaming re-render) is byte-stable.
|
|
136
|
+
expect(richMessage(wire.markdown).markdown).toBe(wire.markdown);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { guardAccidentalInlinePairs } from "../../render/inline-pairs-guard.js";
|
|
3
|
+
|
|
4
|
+
// Strip defusing backslashes so we can assert the reader-visible text is
|
|
5
|
+
// byte-identical to the original prose (the backslash is consumed by Telegram's
|
|
6
|
+
// parser; copy-paste yields the clean glyph).
|
|
7
|
+
function copyText(s: string): string {
|
|
8
|
+
return s.replace(/\\([~=|])/g, "$1");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("guardAccidentalInlinePairs — NO false positives on INTENDED formatting", () => {
|
|
12
|
+
// These are the hard constraint: the agent uses these DELIBERATELY. They must
|
|
13
|
+
// pass through byte-for-byte untouched.
|
|
14
|
+
it("leaves intended `~~strike~~` (word content) untouched", () => {
|
|
15
|
+
const s = "the ~~deprecated~~ flag is gone";
|
|
16
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("leaves intended single-tilde `~struck~` (word content) untouched", () => {
|
|
20
|
+
const s = "mark it ~struck~ for now";
|
|
21
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("leaves intended `==mark==` highlight untouched", () => {
|
|
25
|
+
const s = "please ==highlight this== part";
|
|
26
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("leaves two intended `==mark==` highlights untouched", () => {
|
|
30
|
+
const s = "==first== and also ==second== matter";
|
|
31
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("leaves intended `||spoiler||` untouched", () => {
|
|
35
|
+
const s = "the twist is ||he was dead|| all along";
|
|
36
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("leaves two intended `||spoiler||` spans untouched", () => {
|
|
40
|
+
const s = "reveal ||one|| then ||two|| slowly";
|
|
41
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("leaves intended `code` spans untouched (even with operators inside)", () => {
|
|
45
|
+
const s = "run `x==y` and `a||b` and `~10` in the shell";
|
|
46
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("leaves a single intended strike of a number `~~2024~~` untouched (below threshold)", () => {
|
|
50
|
+
const s = "the ~~2024~~ figure was revised";
|
|
51
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("leaves a lone comparison `x==y` untouched (cannot form a pair)", () => {
|
|
55
|
+
const s = "the check is x==y in the loop";
|
|
56
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("leaves a lone approximation `~10` untouched (cannot form a pair)", () => {
|
|
60
|
+
const s = "roughly ~10 items remain";
|
|
61
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("leaves space-flanked `a == b == c` untouched (cannot open a span)", () => {
|
|
65
|
+
const s = "assert a == b == c holds";
|
|
66
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("is a strict no-op for text with none of the trigger chars", () => {
|
|
70
|
+
const s = "a perfectly ordinary sentence with no specials";
|
|
71
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("guardAccidentalInlinePairs — neutralises ACCIDENTAL pairs", () => {
|
|
76
|
+
it("breaks an accidental strikethrough from two approximation tildes", () => {
|
|
77
|
+
const s = "trims to ~10 units, down from ~20 last week";
|
|
78
|
+
const out = guardAccidentalInlinePairs(s);
|
|
79
|
+
// No unescaped digit-adjacent tilde survives to open a `~…~` span.
|
|
80
|
+
expect(out).not.toMatch(/(?<!\\)~(?=\$?\.?\d)/);
|
|
81
|
+
expect(out).toContain("\\~10");
|
|
82
|
+
expect(out).toContain("\\~20");
|
|
83
|
+
// Reader-visible prose unchanged once the defuser is stripped.
|
|
84
|
+
expect(copyText(out)).toBe(s);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("breaks an accidental strike from currency-approx tildes (`~$5M` / `~$9M`)", () => {
|
|
88
|
+
const s = "budget is ~$5M now, was ~$9M before";
|
|
89
|
+
const out = guardAccidentalInlinePairs(s);
|
|
90
|
+
expect(out).not.toMatch(/(?<!\\)~(?=\$?\.?\d)/);
|
|
91
|
+
expect(copyText(out)).toBe(s);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("breaks the inner tilde of a `~~10 ... ~~20` double-open accidental pair", () => {
|
|
95
|
+
const s = "about ~~10 to ~~20 percent";
|
|
96
|
+
const out = guardAccidentalInlinePairs(s);
|
|
97
|
+
expect(out).not.toMatch(/(?<!\\)~(?=\$?\.?\d)/);
|
|
98
|
+
expect(copyText(out)).toBe(s);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("breaks an accidental highlight from two `==` comparison operators", () => {
|
|
102
|
+
const s = "check x==y and then p==q in the guard";
|
|
103
|
+
const out = guardAccidentalInlinePairs(s);
|
|
104
|
+
expect(out).not.toMatch(/(?<=\w)==(?=\w)/);
|
|
105
|
+
expect(out).toContain("x\\=\\=y");
|
|
106
|
+
expect(out).toContain("p\\=\\=q");
|
|
107
|
+
expect(copyText(out)).toBe(s);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("breaks an accidental spoiler from two `||` logical-or operators", () => {
|
|
111
|
+
const s = "when a||b and c||d both hold";
|
|
112
|
+
const out = guardAccidentalInlinePairs(s);
|
|
113
|
+
expect(out).not.toMatch(/(?<=\w)\|\|(?=\w)/);
|
|
114
|
+
expect(out).toContain("a\\|\\|b");
|
|
115
|
+
expect(out).toContain("c\\|\\|d");
|
|
116
|
+
expect(copyText(out)).toBe(s);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("never escapes operators INSIDE a code span while fixing prose ones", () => {
|
|
120
|
+
const s = "prose x==y and p==q but code `m==n` stays";
|
|
121
|
+
const out = guardAccidentalInlinePairs(s);
|
|
122
|
+
// Prose operators neutralised…
|
|
123
|
+
expect(out).toContain("x\\=\\=y");
|
|
124
|
+
expect(out).toContain("p\\=\\=q");
|
|
125
|
+
// …code span verbatim.
|
|
126
|
+
expect(out).toContain("`m==n`");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("guardAccidentalInlinePairs — link / autolink awareness (finding 1)", () => {
|
|
131
|
+
it("leaves `==` inside a markdown link destination UNMODIFIED", () => {
|
|
132
|
+
const s = "see [a](https://x.io?p==1) and [b](https://y.io?q==2)";
|
|
133
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("leaves `~` (digit-adjacent) inside link destinations UNMODIFIED", () => {
|
|
137
|
+
const s = "see [a](https://x.io/~5x) and [b](https://y.io/~10y)";
|
|
138
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("leaves `==` inside a bare autolinked URL UNMODIFIED", () => {
|
|
142
|
+
const s = "open https://x.io/path?a==1&b==2 in a browser";
|
|
143
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("guardAccidentalInlinePairs — table awareness (finding 3)", () => {
|
|
148
|
+
it("leaves a real table's empty cells (`|a||b|`) UNMODIFIED", () => {
|
|
149
|
+
const s = ["| A | B | C |", "| --- | --- | --- |", "|a||b||c|"].join("\n");
|
|
150
|
+
expect(guardAccidentalInlinePairs(s)).toBe(s);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("STILL guards a genuine accidental `a||b` in prose (no delimiter row)", () => {
|
|
154
|
+
const out = guardAccidentalInlinePairs("eval a||b and c||d in code");
|
|
155
|
+
expect(out).toContain("a\\|\\|b");
|
|
156
|
+
expect(out).toContain("c\\|\\|d");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("guardAccidentalInlinePairs — idempotency", () => {
|
|
161
|
+
it("guarding already-guarded tilde text is a strict no-op", () => {
|
|
162
|
+
const once = guardAccidentalInlinePairs("trims to ~10 units, down from ~20 last week");
|
|
163
|
+
expect(guardAccidentalInlinePairs(once)).toBe(once);
|
|
164
|
+
expect(once).not.toContain("\\\\~"); // never a doubled backslash
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("guarding already-guarded `==`/`||` text is a strict no-op", () => {
|
|
168
|
+
const once = guardAccidentalInlinePairs("check x==y and p==q; also a||b and c||d");
|
|
169
|
+
expect(guardAccidentalInlinePairs(once)).toBe(once);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { guardAccidentalBlockConstructs } from "../../render/line-start-guard.js";
|
|
3
|
+
|
|
4
|
+
/** Strip the defusing backslashes so we can assert the reader-visible text is
|
|
5
|
+
* byte-identical to the original prose (Telegram consumes the `\`). */
|
|
6
|
+
function copyText(s: string): string {
|
|
7
|
+
return s.replace(/\\([>.)])/g, "$1");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("guardAccidentalBlockConstructs (#3252) — INTENDED formatting is never touched", () => {
|
|
11
|
+
// ── Headings: deferred entirely, must pass through untouched ──
|
|
12
|
+
it("leaves an intended `# ` heading untouched (deferred family)", () => {
|
|
13
|
+
const s = "# Project status\nAll green.";
|
|
14
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("leaves `## `/`### ` sub-headings untouched", () => {
|
|
18
|
+
const s = "## Section\n### Subsection\nbody";
|
|
19
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("leaves `#1` (no space, not a GFM heading) untouched — Telegram renders it literally", () => {
|
|
23
|
+
const s = "#1 priority is latency";
|
|
24
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("leaves `# of items` untouched — deferred (indistinguishable from a heading)", () => {
|
|
28
|
+
const s = "# of items in the cart";
|
|
29
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ── Intended blockquotes: `> ` with a space, never touched ──
|
|
33
|
+
it("leaves an intended `> quoted` blockquote untouched", () => {
|
|
34
|
+
const s = "> This is a real quote\n> spanning two lines";
|
|
35
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("leaves `> 50% of users` untouched — spaced form is a valid/intended blockquote", () => {
|
|
39
|
+
const s = "> 50% of users prefer dark mode";
|
|
40
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── Bullet lists: deferred entirely, never touched ──
|
|
44
|
+
it("leaves `- ` bullets untouched (deferred family)", () => {
|
|
45
|
+
const s = "- first item\n- second item\n- 5 apples";
|
|
46
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("leaves `* ` and `+ ` bullets untouched", () => {
|
|
50
|
+
const s = "* star bullet\n+ plus bullet";
|
|
51
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("leaves `- 5 degrees` untouched — ambiguous with a bullet, deferred", () => {
|
|
55
|
+
const s = "- 5 degrees below freezing";
|
|
56
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── Ordered lists: small numbers are real lists, never touched ──
|
|
60
|
+
it("leaves a real `1.`/`2.`/`3.` numbered list untouched", () => {
|
|
61
|
+
const s = "1. First we plan\n2. Then we build\n3. Finally we ship";
|
|
62
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("leaves a 2–3 digit list marker (`42.`, `100.`) untouched — deferred", () => {
|
|
66
|
+
const s = "42. answer\n100. century";
|
|
67
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("leaves a decimal `3.14` untouched (no space after dot => not a list)", () => {
|
|
71
|
+
const s = "3.14 is pi and 2.71 is e";
|
|
72
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── Non-triggering prose ──
|
|
76
|
+
it("is a strict no-op for ordinary prose with no line-start triggers", () => {
|
|
77
|
+
const s = "just a normal sentence about the weather today";
|
|
78
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("guardAccidentalBlockConstructs (#3252) — accidental constructs ARE escaped", () => {
|
|
83
|
+
it("escapes a line-start `>2x` comparison (accidental blockquote)", () => {
|
|
84
|
+
const out = guardAccidentalBlockConstructs(">2x faster than before");
|
|
85
|
+
expect(out).toBe("\\>2x faster than before");
|
|
86
|
+
expect(copyText(out)).toBe(">2x faster than before");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("escapes a line-start `>50%` comparison", () => {
|
|
90
|
+
const out = guardAccidentalBlockConstructs(">50% of the budget");
|
|
91
|
+
expect(out).toBe("\\>50% of the budget");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("escapes a line-start `>=3` comparison", () => {
|
|
95
|
+
const out = guardAccidentalBlockConstructs(">=3 retries required");
|
|
96
|
+
expect(out).toBe("\\>=3 retries required");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("escapes an accidental blockquote only on the offending line, mid-paragraph", () => {
|
|
100
|
+
const s = "Latency improved.\n>2x faster now\nShipping today.";
|
|
101
|
+
const out = guardAccidentalBlockConstructs(s);
|
|
102
|
+
expect(out).toBe("Latency improved.\n\\>2x faster now\nShipping today.");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("escapes a 4+ digit year promoted to an ordered-list item (`2026. was`)", () => {
|
|
106
|
+
const out = guardAccidentalBlockConstructs("2026. was a great year for the team");
|
|
107
|
+
expect(out).toBe("2026\\. was a great year for the team");
|
|
108
|
+
expect(copyText(out)).toBe("2026. was a great year for the team");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("escapes a `1999) ` year with the paren delimiter", () => {
|
|
112
|
+
const out = guardAccidentalBlockConstructs("1999) marked the dot-com peak");
|
|
113
|
+
expect(out).toBe("1999\\) marked the dot-com peak");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("escapes the accidental ordered-list line only, inside a paragraph", () => {
|
|
117
|
+
const s = "Consider the timeline.\n2026. was pivotal\nThat is all.";
|
|
118
|
+
const out = guardAccidentalBlockConstructs(s);
|
|
119
|
+
expect(out).toBe("Consider the timeline.\n2026\\. was pivotal\nThat is all.");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("guardAccidentalBlockConstructs (#3252) — code / indent safety", () => {
|
|
124
|
+
it("never touches a `>` at the start of a FENCED code block line", () => {
|
|
125
|
+
const s = "```\n>2x in shell\n2026. code year\n```";
|
|
126
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("never touches a `>` inside an inline code span", () => {
|
|
130
|
+
const s = "use `>2x` in the expression";
|
|
131
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("does not treat a prose `>` that begins mid-line (after a code span) as a line start", () => {
|
|
135
|
+
// The `>5` is NOT at a real line start (an inline code span precedes it on
|
|
136
|
+
// the same line), so it must be left alone.
|
|
137
|
+
const s = "the value `x` >5 always";
|
|
138
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("leaves a 4-space indented (code) line untouched", () => {
|
|
142
|
+
const s = " >2x indented code\n 2026. also code";
|
|
143
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("still escapes a construct indented 1–3 spaces (valid block context)", () => {
|
|
147
|
+
const out = guardAccidentalBlockConstructs(" >2x slightly indented");
|
|
148
|
+
expect(out).toBe(" \\>2x slightly indented");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("guardAccidentalBlockConstructs (#3252) — idempotency & no-op signal", () => {
|
|
153
|
+
it("is idempotent: guarding already-guarded text is a strict no-op", () => {
|
|
154
|
+
const once = guardAccidentalBlockConstructs(">2x and\n2026. year");
|
|
155
|
+
expect(guardAccidentalBlockConstructs(once)).toBe(once);
|
|
156
|
+
expect(once).not.toContain("\\\\"); // never a doubled backslash
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("short-circuits to a strict no-op when there is no `>` and no 4+ digit marker", () => {
|
|
160
|
+
const s = "1. a\n2. b\n- c\n# d\n> e";
|
|
161
|
+
// All intended constructs; contains `>` so it runs, but produces no change.
|
|
162
|
+
expect(guardAccidentalBlockConstructs(s)).toBe(s);
|
|
163
|
+
});
|
|
164
|
+
});
|