refactor: cut cyclomatic complexity of five hot spots and pin their behavior with tests
Risk labels, menu callback parsing, settings callbacks, table cell width and markdown link targets now use tables and small helpers (CC <= 10).
This commit is contained in:
1 parent
bf4a2021e8
commit
6a6e75fe32
8 files changed
+648
-294
No files matched your search
@@ -350,3 +350,47 @@ test("Command templates report missing required placeholders", () => {
|
||||
/Missing command template value: missing/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Command template risk labels cover each detection branch and its inverse", () => {
|
||||
const cases: Array<[string, string[]]> = [
|
||||
["echo hello", []],
|
||||
["bash script.sh", ["risk.shell"]],
|
||||
["sh -c {script}", ["risk.shell", "risk.eval"]],
|
||||
["node app.js", []],
|
||||
["node -e {code}", ["risk.eval"]],
|
||||
["bun --eval {code}", ["risk.eval"]],
|
||||
["python3 script.py", []],
|
||||
["python3 -c {code}", ["risk.eval"]],
|
||||
["ruby -e {code}", ["risk.eval"]],
|
||||
["rm file.txt", []],
|
||||
["rm -rf build", ["risk.destructive_fs"]],
|
||||
["rm -f build", ["risk.destructive_fs"]],
|
||||
["rm /etc/hosts", ["risk.destructive_fs"]],
|
||||
["mv a.txt b.txt", []],
|
||||
["cp a.txt ~/b.txt", ["risk.broad_fs_write"]],
|
||||
["rsync a b", ["risk.network"]],
|
||||
["rsync a /dst", ["risk.broad_fs_write", "risk.network"]],
|
||||
["curl https://example.com", ["risk.network"]],
|
||||
["git status", []],
|
||||
["git clone {url}", ["risk.network"]],
|
||||
["git push origin main", ["risk.external_side_effect", "risk.network"]],
|
||||
["npm install", ["risk.network"]],
|
||||
["npm publish", ["risk.external_side_effect", "risk.network"]],
|
||||
["kubectl get pods", ["risk.external_side_effect"]],
|
||||
["sleep 5", ["risk.long_running"]],
|
||||
["tail file.log", []],
|
||||
["tail -f file.log", ["risk.long_running"]],
|
||||
["tool --serve", ["risk.long_running"]],
|
||||
["systemctl restart x", ["risk.platform_specific"]],
|
||||
["gpg --decrypt {file}", ["risk.secret_touching"]],
|
||||
["tool --api-key {key}", ["risk.secret_touching"]],
|
||||
["tool --name value", []],
|
||||
];
|
||||
for (const [template, expected] of cases) {
|
||||
assert.deepEqual(
|
||||
getCommandTemplateRiskLabels(template),
|
||||
expected,
|
||||
template,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -261,3 +261,109 @@ test("Settings runtime opens menus and applies stale-message fallback toggles",
|
||||
"answer:Time injection: hidden",
|
||||
]);
|
||||
});
|
||||
|
||||
function createSettingsCallbackHarness() {
|
||||
const calls: string[] = [];
|
||||
const deps = {
|
||||
isProactivePushEnabled: () => false,
|
||||
getVoiceReplyMode: () => "manual" as const,
|
||||
isVoiceReplyModeConfigured: () => true,
|
||||
getTimeInjectionMode: () => "hidden" as const,
|
||||
areDraftPreviewsEnabled: () => false,
|
||||
getAssistantRenderingMode: () => "rich" as const,
|
||||
setProactivePushEnabled: async (enabled: boolean) => {
|
||||
calls.push(`proactive:${enabled}`);
|
||||
},
|
||||
setDraftPreviewsEnabled: async (enabled: boolean) => {
|
||||
calls.push(`draft-previews:${enabled}`);
|
||||
},
|
||||
setAssistantRenderingMode: async (mode: "rich" | "html") => {
|
||||
calls.push(`rendering:${mode}`);
|
||||
},
|
||||
setVoiceReplyMode: async (
|
||||
mode: "manual" | "mirror" | "always" | undefined,
|
||||
) => {
|
||||
calls.push(`voice:${mode ?? "hidden"}`);
|
||||
},
|
||||
setTimeInjectionMode: async (mode: "hidden" | "always" | "interval") => {
|
||||
calls.push(`time:${mode}`);
|
||||
},
|
||||
updateSettingsMessage: async (text: string) => {
|
||||
calls.push(`update:${text.split("\n")[0]}`);
|
||||
},
|
||||
answerCallbackQuery: async (_id: string, text?: string) => {
|
||||
calls.push(`answer:${text ?? ""}`);
|
||||
},
|
||||
};
|
||||
return { calls, deps };
|
||||
}
|
||||
|
||||
test("Settings callback action opens each page, including legacy aliases", async () => {
|
||||
const pages: Array<[string, string]> = [
|
||||
["settings:list", "update:<b>⚙️ Settings:</b>"],
|
||||
["settings:open:proactive", "update:<b>📌 Proactive push:</b> <code>off</code>"],
|
||||
["settings:open:draft-previews", "update:<b>📝 Draft previews:</b> <code>off</code>"],
|
||||
["settings:open:rich-drafts", "update:<b>📝 Draft previews:</b> <code>off</code>"],
|
||||
["settings:open:assistant-rendering", "update:<b>🧾 Assistant rendering:</b> <code>rich</code>"],
|
||||
["settings:open:voice-reply", "update:<b>👄 Voice reply mode:</b> <code>manual</code>"],
|
||||
["settings:open:time-injection", "update:<b>🕒 Time injection mode:</b> <code>hidden</code>"],
|
||||
["settings:open:time", "update:<b>🕒 Time injection mode:</b> <code>hidden</code>"],
|
||||
];
|
||||
for (const [data, update] of pages) {
|
||||
const { calls, deps } = createSettingsCallbackHarness();
|
||||
assert.equal(
|
||||
await handleTelegramSettingsMenuCallbackAction("q", data, deps),
|
||||
true,
|
||||
data,
|
||||
);
|
||||
assert.deepEqual(calls, [update, "answer:"], data);
|
||||
}
|
||||
});
|
||||
|
||||
test("Settings callback action applies each mode and the inverse branch of each toggle", async () => {
|
||||
const cases: Array<[string, string[]]> = [
|
||||
["settings:set:voice-reply:mirror", ["voice:mirror", "update:<b>👄 Voice reply mode:</b> <code>manual</code>", "answer:Voice reply mode: mirror"]],
|
||||
["settings:set:voice-reply:always", ["voice:always", "update:<b>👄 Voice reply mode:</b> <code>manual</code>", "answer:Voice reply mode: always"]],
|
||||
["settings:set:time-injection:interval", ["time:interval", "update:<b>🕒 Time injection mode:</b> <code>hidden</code>", "answer:Time injection: interval"]],
|
||||
["settings:set:time-injection:off", ["time:hidden", "update:<b>🕒 Time injection mode:</b> <code>hidden</code>", "answer:Time injection: hidden"]],
|
||||
["settings:set:time:always", ["time:always", "update:<b>🕒 Time injection mode:</b> <code>hidden</code>", "answer:Time injection: always"]],
|
||||
["settings:set:draft-previews:off", ["draft-previews:false", "update:<b>📝 Draft previews:</b> <code>off</code>", "answer:Draft previews disabled"]],
|
||||
["settings:set:rich-drafts:on", ["draft-previews:true", "update:<b>📝 Draft previews:</b> <code>off</code>", "answer:Draft previews enabled"]],
|
||||
["settings:set:assistant-rendering:rich", ["rendering:rich", "update:<b>🧾 Assistant rendering:</b> <code>rich</code>", "answer:Rendering: rich"]],
|
||||
["settings:set:proactive:off", ["proactive:false", "update:<b>📌 Proactive push:</b> <code>off</code>", "answer:Proactive push disabled"]],
|
||||
];
|
||||
for (const [data, expected] of cases) {
|
||||
const { calls, deps } = createSettingsCallbackHarness();
|
||||
assert.equal(
|
||||
await handleTelegramSettingsMenuCallbackAction("q", data, deps),
|
||||
true,
|
||||
data,
|
||||
);
|
||||
assert.deepEqual(calls, expected, data);
|
||||
}
|
||||
});
|
||||
|
||||
test("Settings callback action only acknowledges invalid or unknown settings values", async () => {
|
||||
for (const data of [
|
||||
"settings:set:voice-reply:bogus",
|
||||
"settings:set:time:bogus",
|
||||
"settings:set:assistant-rendering:bogus",
|
||||
"settings:set:draft-previews:maybe",
|
||||
"settings:set:proactive:maybe",
|
||||
"settings:unknown",
|
||||
]) {
|
||||
const { calls, deps } = createSettingsCallbackHarness();
|
||||
assert.equal(
|
||||
await handleTelegramSettingsMenuCallbackAction("q", data, deps),
|
||||
true,
|
||||
data,
|
||||
);
|
||||
assert.deepEqual(calls, ["answer:"], data);
|
||||
}
|
||||
const { calls, deps } = createSettingsCallbackHarness();
|
||||
assert.equal(
|
||||
await handleTelegramSettingsMenuCallbackAction("q", undefined, deps),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
@@ -2394,3 +2394,45 @@ test("Settings menu marks one-line on/off checkbox controls symmetrically", () =
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("Menu callback parser maps every status alias and model action, and ignores the rest", () => {
|
||||
for (const action of ["model", "thinking", "queue", "settings"] as const) {
|
||||
for (const prefix of ["menu", "status"]) {
|
||||
assert.deepEqual(parseTelegramMenuCallbackAction(`${prefix}:${action}`), {
|
||||
kind: "status",
|
||||
action,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const action of [
|
||||
"noop",
|
||||
"scope",
|
||||
"page",
|
||||
"pages",
|
||||
"open",
|
||||
"pick",
|
||||
"pick-selected",
|
||||
"scope-enable",
|
||||
"scope-disable",
|
||||
"scope-toggle",
|
||||
]) {
|
||||
assert.deepEqual(parseTelegramMenuCallbackAction(`model:${action}:7`), {
|
||||
kind: "model",
|
||||
action,
|
||||
value: "7",
|
||||
});
|
||||
}
|
||||
assert.deepEqual(parseTelegramMenuCallbackAction("thinking:set:"), {
|
||||
kind: "thinking:set",
|
||||
level: "",
|
||||
});
|
||||
assert.deepEqual(parseTelegramMenuCallbackAction("model:unknown:1"), {
|
||||
kind: "ignore",
|
||||
});
|
||||
assert.deepEqual(parseTelegramMenuCallbackAction("menu:other"), {
|
||||
kind: "ignore",
|
||||
});
|
||||
assert.deepEqual(parseTelegramMenuCallbackAction(undefined), {
|
||||
kind: "ignore",
|
||||
});
|
||||
});
|
||||
@@ -294,6 +294,82 @@ test("Markdown table padding handles CJK and combining marks", () => {
|
||||
assert.match(chunks[0]?.text ?? "", /東京 \| city /u);
|
||||
});
|
||||
|
||||
test("Markdown table padding measures wide, zero-width, and control code points", () => {
|
||||
// [cell, expected display width]
|
||||
const cells: Array<[string, number]> = [
|
||||
["a", 1],
|
||||
["\u{d55c}", 2], // Hangul syllable
|
||||
["\u{ff21}", 2], // fullwidth Latin
|
||||
["\u{2329}", 2], // angle bracket
|
||||
["\u{f900}", 2], // CJK compatibility ideograph
|
||||
["\u{fe10}", 2], // vertical form
|
||||
["\u{fe30}", 2], // CJK compatibility form
|
||||
["\u{ffe0}", 2], // fullwidth sign
|
||||
["\u{1100}", 2], // Hangul Jamo
|
||||
["é", 1], // combining mark
|
||||
["ab", 2], // zero-width joiner
|
||||
["a️", 1], // variation selector
|
||||
["a\u0001b", 2], // control character
|
||||
["ab", 2],
|
||||
];
|
||||
const markdown = [
|
||||
"| W | N |",
|
||||
"| --- | --- |",
|
||||
...cells.map(([cell], index) => `| ${cell} | c${index} |`),
|
||||
].join("\n");
|
||||
const chunks = renderTelegramMessage(markdown, { mode: "markdown" });
|
||||
assert.equal(chunks.length, 1);
|
||||
const expectedRows = [
|
||||
"W | N ",
|
||||
"--- | ---",
|
||||
...cells.map(
|
||||
([cell, width], index) =>
|
||||
`${cell}${" ".repeat(3 - width)} | ${`c${index}`.padEnd(3)}`,
|
||||
),
|
||||
];
|
||||
assert.equal(
|
||||
chunks[0]?.text,
|
||||
`<pre><code class="language-markdown">${expectedRows.join("\n")}</code></pre>`,
|
||||
);
|
||||
});
|
||||
|
||||
test("Markdown link targets accept valid destinations and titles and reject malformed ones", () => {
|
||||
const link = (href: string) => `<a href="${href}">a</a>`;
|
||||
const cases: Array<[string, string]> = [
|
||||
// plain destinations
|
||||
["[a](https://x.com)", link("https://x.com")],
|
||||
["[a]( https://x.com )", link("https://x.com")],
|
||||
["[a](https://x.com/a_(b))", link("https://x.com/a_(b)")],
|
||||
[String.raw`[a](https://x.com/a\)b)`, link(String.raw`https://x.com/a\)b`)],
|
||||
[String.raw`[a](https://x.com/a\ b)`, link(String.raw`https://x.com/a\ b`)],
|
||||
["[a](https://x.com/a b)", "[a](https://x.com/a b)"],
|
||||
["[a](https://x.com/a(b)", "[a](https://x.com/a(b)"],
|
||||
// angle-bracket destinations
|
||||
["[a](<https://x.com/a b>)", link("https://x.com/a b")],
|
||||
[String.raw`[a](<https://x.com/a\>b>)`, link(String.raw`https://x.com/a\>b`)],
|
||||
["[a](<https://x.com/a", "[a](<https://x.com/a"],
|
||||
["[a](<>)", "[a](<>)"],
|
||||
// missing destination or end of input
|
||||
["[a]()", "[a]()"],
|
||||
["[a](", "[a]("],
|
||||
["[a]( ", "[a]( "],
|
||||
// titles
|
||||
['[a](https://x.com "T")', link("https://x.com")],
|
||||
["[a](https://x.com 'T')", link("https://x.com")],
|
||||
["[a](https://x.com (T))", link("https://x.com")],
|
||||
['[a](<https://x.com> "T")', link("https://x.com")],
|
||||
["[a](https://x.com )", link("https://x.com")],
|
||||
[String.raw`[a](https://x.com "T\"q")`, link("https://x.com")],
|
||||
['[a](https://x.com "T)', '[a](https://x.com "T)'],
|
||||
[String.raw`[a](https://x.com "T\")`, String.raw`[a](https://x.com "T\")`],
|
||||
["[a](https://x.com T)", "[a](https://x.com T)"],
|
||||
];
|
||||
for (const [input, expected] of cases) {
|
||||
const chunks = renderTelegramMessage(input, { mode: "markdown" });
|
||||
assert.equal(chunks[0]?.text, expected, input);
|
||||
}
|
||||
});
|
||||
|
||||
test("Links, code spans, and underscore-heavy text coexist safely", () => {
|
||||
const chunks = renderTelegramMessage(
|
||||
"See [docs](https://example.com), run `foo_bar()` and keep foo_bar.txt literal",
|
||||
|
||||
Reference in new issue
Block a user