switchroom 0.18.25 → 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.
@@ -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
+ });