105 lines
5.5 KiB
TypeScript
105 lines
5.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { advancePastAtomicRanges, scanMarkdownImageStreaming, stableStreamingMarkdown } from "../../lib/canvas/markdown-image-streaming";
|
|
import { splitMarkdownImages } from "../../lib/canvas/markdown-image-segments";
|
|
|
|
const url = "https://scqny.jiansun.vip/ps-skill-api/results/job_test/01-%E6%A0%B7%E4%BE%8B.jpg";
|
|
const wrapped = `[](${url})`;
|
|
const other = `[](https://example.com/original.jpg)`;
|
|
|
|
test("every live network prefix withholds the entire unfinished linked image", () => {
|
|
const before = "## 生成图\n";
|
|
for (let n = 1; n < wrapped.length; n += 1) {
|
|
assert.equal(stableStreamingMarkdown(before + wrapped.slice(0, n), true), before, `prefix ${n}`);
|
|
}
|
|
assert.equal(stableStreamingMarkdown(before + wrapped, true), before + wrapped);
|
|
// An interrupted / completed response keeps malformed original text visible.
|
|
for (const suffix of ["[", "[!", "[]("]) {
|
|
assert.equal(stableStreamingMarkdown(before + suffix, false), before + suffix);
|
|
}
|
|
});
|
|
|
|
test("every synthetic typewriter cursor exposes complete image markup or none", () => {
|
|
const content = `生成图\n${wrapped}\n${other}\n可编辑文件`;
|
|
const ranges = scanMarkdownImageStreaming(content).ranges;
|
|
assert.equal(ranges.length, 2);
|
|
let cursor = 0;
|
|
while (cursor < content.length) {
|
|
cursor = advancePastAtomicRanges(content.length, cursor + 1, ranges);
|
|
const visible = content.slice(0, cursor);
|
|
for (const range of ranges) {
|
|
assert.ok(cursor <= range.start || cursor >= range.end, `split at ${cursor}`);
|
|
}
|
|
if (cursor >= ranges[0].end) {
|
|
assert.ok(splitMarkdownImages(visible).some((item) => item.type === "image"));
|
|
}
|
|
}
|
|
assert.equal(cursor, content.length);
|
|
assert.deepEqual(splitMarkdownImages(other), [{ type: "text", value: other }]);
|
|
});
|
|
|
|
test("handles ordinary images, URL brackets, escaped text and code safely", () => {
|
|
const simple = ".jpg)";
|
|
for (let n = 1; n < simple.length; n += 1) {
|
|
assert.equal(stableStreamingMarkdown("前文 " + simple.slice(0, n), true), "前文 ", `prefix ${n}`);
|
|
}
|
|
const literal = "`";
|
|
assert.equal(stableStreamingMarkdown(literal, true), literal);
|
|
const fence = "~~~md\n[, fence);
|
|
assert.deepEqual(scanMarkdownImageStreaming(fence + simple).ranges, [{ start: fence.length, end: fence.length + simple.length }]);
|
|
const invalid = " and later text";
|
|
assert.equal(stableStreamingMarkdown(invalid, true), invalid);
|
|
});
|
|
|
|
test("does not change normal links or security-sensitive complete markup", () => {
|
|
const values = ["[普通链接](https://example.com/a)", "[](javascript:alert(1))", other];
|
|
for (const value of values) assert.equal(stableStreamingMarkdown(value, true), value);
|
|
});
|
|
|
|
test("coalesces overlapping typewriter table and image ranges without looping", () => {
|
|
const ranges = [{ start: 0, end: 20 }, { start: 10, end: 35 }, { start: 35, end: 40 }];
|
|
assert.equal(advancePastAtomicRanges(50, 1, ranges), 35);
|
|
assert.equal(advancePastAtomicRanges(50, 36, ranges), 40);
|
|
assert.equal(advancePastAtomicRanges(50, 50, ranges), 50);
|
|
});
|
|
|
|
test("SKU result links are atomic across every typewriter cursor and live fragment", () => {
|
|
const links = [
|
|
"[beike-sku-template2-batch.json](https://scqny.jiansun.vip/ps-skill-api/results/job_test/795c72cb-batch.json)",
|
|
"[beike-sku-template2-report.json](https://scqny.jiansun.vip/ps-skill-api/results/job_test/af588253-report.json)",
|
|
];
|
|
const text = `生成图\n${wrapped}\n可编辑文件\n- ${links[0]}\n- ${links[1]}\n完成`;
|
|
const ranges = scanMarkdownImageStreaming(text).ranges;
|
|
assert.equal(ranges.length, 3);
|
|
for (const link of links) {
|
|
const start = text.indexOf(link);
|
|
for (let n = 1; n < link.length; n += 1) {
|
|
const prefix = text.slice(0, start + n);
|
|
assert.equal(stableStreamingMarkdown(prefix, true), text.slice(0, start), `link prefix ${n}`);
|
|
}
|
|
assert.equal(stableStreamingMarkdown(text.slice(0, start + link.length), true), text.slice(0, start + link.length));
|
|
}
|
|
let cursor = 0;
|
|
while (cursor < text.length) {
|
|
cursor = advancePastAtomicRanges(text.length, cursor + 1, ranges);
|
|
assert.ok(ranges.every((range) => cursor <= range.start || cursor >= range.end), `split at ${cursor}`);
|
|
}
|
|
assert.equal(cursor, text.length);
|
|
});
|
|
|
|
test("ordinary brackets, code, escaped markers and unfinished final links remain available", () => {
|
|
for (const value of ["普通文字", "[状态] 已完成", "[引用][reference]", "\\[转义](https://example.com)", "`[code](https://example.com`", "~~~md\n[example](https://example.com\n~~~\n"]) {
|
|
assert.equal(stableStreamingMarkdown(value, true), value);
|
|
assert.equal(stableStreamingMarkdown(value, false), value);
|
|
}
|
|
for (const partial of ["[状态", "[文件](https://example.com/", "[文件]"]) {
|
|
assert.equal(stableStreamingMarkdown(partial, false), partial);
|
|
assert.equal(stableStreamingMarkdown(partial + "\n普通文字", true), partial + "\n普通文字");
|
|
}
|
|
const titleLink = '[文档](https://example.com/a "标题")';
|
|
assert.deepEqual(scanMarkdownImageStreaming(titleLink).ranges, [{ start: 0, end: titleLink.length }]);
|
|
const unsafeLink = "[危险链接](javascript:alert(1))";
|
|
assert.equal(stableStreamingMarkdown(unsafeLink, false), unsafeLink);
|
|
});
|