Files
cc-switch/tests/utils/textNormalization.test.ts
Jason 1f1becfb42 fix(toml): only normalize smart quotes used as string delimiters
normalizeTomlText replaced every curly/fullwidth quote in the text before
TOML parsing, including quotes inside already-valid strings. MCP import and
validation (tomlUtils, mcpDraft) and the Codex config editor therefore
silently rewrote values such as args = ["don’t"] to "don't", and rejected
valid configs like args = ["say“hello”"] or args = ['don’t'] because the
inner quotes became delimiters.

Scan the text instead: smart quotes outside strings are delimiters and
become ASCII; strings opened by an ASCII quote keep smart quotes as
content; strings opened by a smart quote may close with either form;
multi-line strings and comments are handled. To keep the Chinese IME case
working (the editor normalizes each keystroke, so the left quote is already
ASCII when the right smart quote is typed), an ASCII-opened string left
unterminated at end of line is re-scanned with smart closers, and if the
strict result still fails to parse, a lenient scan is used only when it
parses. Escaped smart double quotes in basic strings become \". Text
without smart quotes is returned unchanged.

Refs #8559
2026-10-20 21:50:16 +08:00

97 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { parse as parseToml } from "smol-toml";
import { describe, expect, it } from "vitest";
import { normalizeTomlText } from "@/utils/textNormalization";
import { tomlToMcpServer, validateToml } from "@/utils/tomlUtils";
describe("normalizeTomlText", () => {
it("normalizes smart quotes used as string delimiters", () => {
expect(normalizeTomlText("command = “npx”\nargs = [‘-y’]")).toBe(
"command = \"npx\"\nargs = ['-y']",
);
expect(normalizeTomlText("[mcp_servers."demo"]")).toBe(
'[mcp_servers."demo"]',
);
});
it("keeps smart quotes inside strings opened by ASCII quotes", () => {
const input = [
'a = "don’t"',
'b = "say“hello”"',
"c = 'don’t'",
'd = """multi “line” ’text’"""',
"e = '''it’s'''",
'f = "escaped \\" then “x”"',
].join("\n");
expect(normalizeTomlText(input)).toBe(input);
});
it("keeps quotes of the other family inside smart-quoted strings", () => {
expect(normalizeTomlText("a = “don’t”")).toBe('a = "don’t"');
expect(parseToml(normalizeTomlText("a = “don’t”")).a).toBe("don’t");
});
it("closes an ASCII-opened string with a smart quote when nothing else closes it", () => {
// 编辑器逐字归一化后,输入法打出的左引号已是 ASCII,右引号仍是弯引号
expect(normalizeTomlText('model = "gpt”\nx = 1')).toBe(
'model = "gpt"\nx = 1',
);
expect(normalizeTomlText("name = 'abc’")).toBe("name = 'abc'");
});
it("repairs smart closers that are followed by more ASCII quotes on the line", () => {
// 合法文档不会走到这里:只有严格结果解析失败时才把字符串里的弯引号当闭合符
expect(normalizeTomlText('args = ["npx”, "-y"]')).toBe(
'args = ["npx", "-y"]',
);
expect(normalizeTomlText('model = "gpt” # "example"')).toBe(
'model = "gpt" # "example"',
);
});
it("turns escaped smart double quotes into ASCII escapes", () => {
const normalized = normalizeTomlText("args = [“say \\“hello\\””]");
expect(normalized).toBe('args = ["say \\"hello\\""]');
expect(parseToml(normalized).args).toEqual(['say "hello"']);
});
it("leaves comments untouched", () => {
const input = "# “comment” it’s\nkey = “v” # don’t";
expect(normalizeTomlText(input)).toBe(
'# “comment” it’s\nkey = "v" # don’t',
);
});
it("does not touch unterminated strings beyond their opening quote", () => {
expect(normalizeTomlText("x = “abc")).toBe('x = "abc');
});
});
describe("tomlUtils with smart quotes in valid strings (#8559)", () => {
it("preserves a right single quotation mark in args", () => {
const input = 'command = "echo"\nargs = ["don’t"]';
expect(validateToml(input)).toBe("");
expect(tomlToMcpServer(input).args).toEqual(["don’t"]);
});
it("accepts curly double quotes inside a basic string", () => {
const input = 'command = "echo"\nargs = ["say“hello”"]';
expect(validateToml(input)).toBe("");
expect(tomlToMcpServer(input).args).toEqual(["say“hello”"]);
});
it("accepts curly single quotes inside a literal string", () => {
const input = "command = 'echo'\nargs = ['don’t']";
expect(validateToml(input)).toBe("");
expect(tomlToMcpServer(input).args).toEqual(["don’t"]);
});
it("still accepts configs written entirely with smart quotes", () => {
const input = "command = “npx”\nargs = [“-y”, “pkg”]";
expect(validateToml(input)).toBe("");
expect(tomlToMcpServer(input)).toMatchObject({
command: "npx",
args: ["-y", "pkg"],
});
});
});