chore: sync shared skill package vendor test files
This commit is contained in:
parent
0c02b81885
commit
de7dc85361
171
skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts
vendored
Normal file
171
skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import http from "node:http";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findChromeExecutable,
|
||||||
|
findExistingChromeDebugPort,
|
||||||
|
getFreePort,
|
||||||
|
resolveSharedChromeProfileDir,
|
||||||
|
waitForChromeDebugPort,
|
||||||
|
} from "./index.ts";
|
||||||
|
|
||||||
|
function useEnv(
|
||||||
|
t: TestContext,
|
||||||
|
values: Record<string, string | null>,
|
||||||
|
): void {
|
||||||
|
const previous = new Map<string, string | undefined>();
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
previous.set(key, process.env[key]);
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.after(() => {
|
||||||
|
for (const [key, value] of previous.entries()) {
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDebugServer(port: number): Promise<http.Server> {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.url === "/json/version") {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(port, "127.0.0.1", () => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeServer(server: http.Server): Promise<void> {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
|
|
||||||
|
const dynamicPort = await getFreePort();
|
||||||
|
assert.ok(Number.isInteger(dynamicPort));
|
||||||
|
assert.ok(dynamicPort > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findChromeExecutable prefers env overrides and falls back to candidate paths", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-chrome-bin-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const envChrome = path.join(root, "env-chrome");
|
||||||
|
const fallbackChrome = path.join(root, "fallback-chrome");
|
||||||
|
await fs.writeFile(envChrome, "");
|
||||||
|
await fs.writeFile(fallbackChrome, "");
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: envChrome });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
envChrome,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: null });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
fallbackChrome,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveSharedChromeProfileDir supports env overrides, WSL paths, and default suffixes", (t) => {
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: "/tmp/custom-profile" });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
envNames: ["BAOYU_SHARED_PROFILE"],
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.resolve("/tmp/custom-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: null });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
wslWindowsHome: "/mnt/c/Users/demo",
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.join("/mnt/c/Users/demo", ".local", "share", "demo-app", "demo-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fallback = resolveSharedChromeProfileDir({
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
});
|
||||||
|
assert.match(fallback, /demo-app[\\/]demo-profile$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findExistingChromeDebugPort reads DevToolsActivePort and validates it against a live endpoint", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-profile-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await findExistingChromeDebugPort({ profileDir: root, timeoutMs: 1000 });
|
||||||
|
assert.equal(found, port);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
const serverPromise = (async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
})();
|
||||||
|
|
||||||
|
const websocketUrl = await waitForChromeDebugPort(port, 4000, {
|
||||||
|
includeLastError: true,
|
||||||
|
});
|
||||||
|
await serverPromise;
|
||||||
|
|
||||||
|
assert.equal(websocketUrl, `ws://127.0.0.1:${port}/devtools/browser/demo`);
|
||||||
|
});
|
||||||
171
skills/baoyu-danger-x-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts
vendored
Normal file
171
skills/baoyu-danger-x-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import http from "node:http";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findChromeExecutable,
|
||||||
|
findExistingChromeDebugPort,
|
||||||
|
getFreePort,
|
||||||
|
resolveSharedChromeProfileDir,
|
||||||
|
waitForChromeDebugPort,
|
||||||
|
} from "./index.ts";
|
||||||
|
|
||||||
|
function useEnv(
|
||||||
|
t: TestContext,
|
||||||
|
values: Record<string, string | null>,
|
||||||
|
): void {
|
||||||
|
const previous = new Map<string, string | undefined>();
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
previous.set(key, process.env[key]);
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.after(() => {
|
||||||
|
for (const [key, value] of previous.entries()) {
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDebugServer(port: number): Promise<http.Server> {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.url === "/json/version") {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(port, "127.0.0.1", () => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeServer(server: http.Server): Promise<void> {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
|
|
||||||
|
const dynamicPort = await getFreePort();
|
||||||
|
assert.ok(Number.isInteger(dynamicPort));
|
||||||
|
assert.ok(dynamicPort > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findChromeExecutable prefers env overrides and falls back to candidate paths", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-chrome-bin-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const envChrome = path.join(root, "env-chrome");
|
||||||
|
const fallbackChrome = path.join(root, "fallback-chrome");
|
||||||
|
await fs.writeFile(envChrome, "");
|
||||||
|
await fs.writeFile(fallbackChrome, "");
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: envChrome });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
envChrome,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: null });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
fallbackChrome,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveSharedChromeProfileDir supports env overrides, WSL paths, and default suffixes", (t) => {
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: "/tmp/custom-profile" });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
envNames: ["BAOYU_SHARED_PROFILE"],
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.resolve("/tmp/custom-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: null });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
wslWindowsHome: "/mnt/c/Users/demo",
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.join("/mnt/c/Users/demo", ".local", "share", "demo-app", "demo-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fallback = resolveSharedChromeProfileDir({
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
});
|
||||||
|
assert.match(fallback, /demo-app[\\/]demo-profile$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findExistingChromeDebugPort reads DevToolsActivePort and validates it against a live endpoint", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-profile-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await findExistingChromeDebugPort({ profileDir: root, timeoutMs: 1000 });
|
||||||
|
assert.equal(found, port);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
const serverPromise = (async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
})();
|
||||||
|
|
||||||
|
const websocketUrl = await waitForChromeDebugPort(port, 4000, {
|
||||||
|
includeLastError: true,
|
||||||
|
});
|
||||||
|
await serverPromise;
|
||||||
|
|
||||||
|
assert.equal(websocketUrl, `ws://127.0.0.1:${port}/devtools/browser/demo`);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
extractSummaryFromBody,
|
||||||
|
extractTitleFromMarkdown,
|
||||||
|
parseFrontmatter,
|
||||||
|
pickFirstString,
|
||||||
|
serializeFrontmatter,
|
||||||
|
stripWrappingQuotes,
|
||||||
|
toFrontmatterString,
|
||||||
|
} from "./content.ts";
|
||||||
|
|
||||||
|
test("parseFrontmatter extracts YAML fields and strips wrapping quotes", () => {
|
||||||
|
const input = `---
|
||||||
|
title: "Hello World"
|
||||||
|
author: ‘Baoyu’
|
||||||
|
summary: plain text
|
||||||
|
---
|
||||||
|
# Heading
|
||||||
|
|
||||||
|
Body`;
|
||||||
|
|
||||||
|
const result = parseFrontmatter(input);
|
||||||
|
|
||||||
|
assert.deepEqual(result.frontmatter, {
|
||||||
|
title: "Hello World",
|
||||||
|
author: "Baoyu",
|
||||||
|
summary: "plain text",
|
||||||
|
});
|
||||||
|
assert.match(result.body, /^# Heading/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseFrontmatter returns original content when no frontmatter exists", () => {
|
||||||
|
const input = "# No frontmatter";
|
||||||
|
assert.deepEqual(parseFrontmatter(input), {
|
||||||
|
frontmatter: {},
|
||||||
|
body: input,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("serializeFrontmatter renders YAML only when fields exist", () => {
|
||||||
|
assert.equal(serializeFrontmatter({}), "");
|
||||||
|
assert.equal(
|
||||||
|
serializeFrontmatter({ title: "Hello", author: "Baoyu" }),
|
||||||
|
"---\ntitle: Hello\nauthor: Baoyu\n---\n",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("quote and frontmatter string helpers normalize mixed scalar values", () => {
|
||||||
|
assert.equal(stripWrappingQuotes(`" quoted "`), "quoted");
|
||||||
|
assert.equal(stripWrappingQuotes("“ 中文标题 ”"), "中文标题");
|
||||||
|
assert.equal(stripWrappingQuotes("plain"), "plain");
|
||||||
|
|
||||||
|
assert.equal(toFrontmatterString("'hello'"), "hello");
|
||||||
|
assert.equal(toFrontmatterString(42), "42");
|
||||||
|
assert.equal(toFrontmatterString(false), "false");
|
||||||
|
assert.equal(toFrontmatterString({}), undefined);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
pickFirstString({ summary: 123, title: "" }, ["title", "summary"]),
|
||||||
|
"123",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("markdown title and summary extraction skip non-body content and clean formatting", () => {
|
||||||
|
const markdown = `
|
||||||
|

|
||||||
|
## “My Title”
|
||||||
|
|
||||||
|
Body paragraph
|
||||||
|
`;
|
||||||
|
assert.equal(extractTitleFromMarkdown(markdown), "My Title");
|
||||||
|
|
||||||
|
const summary = extractSummaryFromBody(
|
||||||
|
`
|
||||||
|
# Heading
|
||||||
|
> quote
|
||||||
|
- list
|
||||||
|
1. ordered
|
||||||
|
\`\`\`
|
||||||
|
code
|
||||||
|
\`\`\`
|
||||||
|
This is **the first paragraph** with [a link](https://example.com) and \`inline code\` that should be summarized cleanly.
|
||||||
|
`,
|
||||||
|
70,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
summary,
|
||||||
|
"This is the first paragraph with a link and inline code that should...",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
|
import { COLOR_PRESETS, FONT_FAMILY_MAP } from "./constants.ts";
|
||||||
|
import {
|
||||||
|
buildMarkdownDocumentMeta,
|
||||||
|
formatTimestamp,
|
||||||
|
resolveColorToken,
|
||||||
|
resolveFontFamilyToken,
|
||||||
|
resolveMarkdownStyle,
|
||||||
|
resolveRenderOptions,
|
||||||
|
} from "./document.ts";
|
||||||
|
|
||||||
|
function useCwd(t: TestContext, cwd: string): void {
|
||||||
|
const previous = process.cwd();
|
||||||
|
process.chdir(cwd);
|
||||||
|
t.after(() => {
|
||||||
|
process.chdir(previous);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
test("document token resolvers map known presets and allow passthrough values", () => {
|
||||||
|
assert.equal(resolveColorToken("green"), COLOR_PRESETS.green);
|
||||||
|
assert.equal(resolveColorToken("#123456"), "#123456");
|
||||||
|
assert.equal(resolveColorToken(), undefined);
|
||||||
|
|
||||||
|
assert.equal(resolveFontFamilyToken("mono"), FONT_FAMILY_MAP.mono);
|
||||||
|
assert.equal(resolveFontFamilyToken("Custom Font"), "Custom Font");
|
||||||
|
assert.equal(resolveFontFamilyToken(), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatTimestamp uses compact sortable datetime output", () => {
|
||||||
|
const date = new Date("2026-03-13T21:04:05.000Z");
|
||||||
|
const pad = (value: number) => String(value).padStart(2, "0");
|
||||||
|
const expected = `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
|
||||||
|
date.getDate(),
|
||||||
|
)}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
||||||
|
|
||||||
|
assert.equal(formatTimestamp(date), expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildMarkdownDocumentMeta prefers frontmatter and falls back to markdown title and summary", () => {
|
||||||
|
const metaFromYaml = buildMarkdownDocumentMeta(
|
||||||
|
"# Markdown Title\n\nBody summary paragraph that should be ignored.",
|
||||||
|
{
|
||||||
|
title: `" YAML Title "`,
|
||||||
|
author: "'Baoyu'",
|
||||||
|
summary: `" YAML Summary "`,
|
||||||
|
},
|
||||||
|
"fallback",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(metaFromYaml, {
|
||||||
|
title: "YAML Title",
|
||||||
|
author: "Baoyu",
|
||||||
|
description: "YAML Summary",
|
||||||
|
});
|
||||||
|
|
||||||
|
const metaFromMarkdown = buildMarkdownDocumentMeta(
|
||||||
|
`## “Markdown Title”\n\nThis is the first body paragraph that should become the summary because it is long enough.`,
|
||||||
|
{},
|
||||||
|
"fallback",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(metaFromMarkdown.title, "Markdown Title");
|
||||||
|
assert.match(metaFromMarkdown.description ?? "", /^This is the first body paragraph/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveMarkdownStyle merges theme defaults with explicit overrides", () => {
|
||||||
|
const style = resolveMarkdownStyle({
|
||||||
|
theme: "modern",
|
||||||
|
primaryColor: "#112233",
|
||||||
|
fontFamily: "Custom Sans",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(style.primaryColor, "#112233");
|
||||||
|
assert.equal(style.fontFamily, "Custom Sans");
|
||||||
|
assert.equal(style.fontSize, "15px");
|
||||||
|
assert.equal(style.containerBg, "rgba(250, 249, 245, 1)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveRenderOptions loads workspace EXTEND settings and lets explicit options win", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-md-render-options-");
|
||||||
|
useCwd(t, root);
|
||||||
|
|
||||||
|
const extendPath = path.join(
|
||||||
|
root,
|
||||||
|
".baoyu-skills",
|
||||||
|
"baoyu-markdown-to-html",
|
||||||
|
"EXTEND.md",
|
||||||
|
);
|
||||||
|
await fs.mkdir(path.dirname(extendPath), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
extendPath,
|
||||||
|
`---
|
||||||
|
default_theme: modern
|
||||||
|
default_color: green
|
||||||
|
default_font_family: mono
|
||||||
|
default_font_size: 17
|
||||||
|
default_code_theme: nord
|
||||||
|
mac_code_block: false
|
||||||
|
show_line_number: true
|
||||||
|
cite: true
|
||||||
|
count: true
|
||||||
|
legend: title-alt
|
||||||
|
keep_title: true
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fromExtend = resolveRenderOptions();
|
||||||
|
assert.equal(fromExtend.theme, "modern");
|
||||||
|
assert.equal(fromExtend.primaryColor, COLOR_PRESETS.green);
|
||||||
|
assert.equal(fromExtend.fontFamily, FONT_FAMILY_MAP.mono);
|
||||||
|
assert.equal(fromExtend.fontSize, "17px");
|
||||||
|
assert.equal(fromExtend.codeTheme, "nord");
|
||||||
|
assert.equal(fromExtend.isMacCodeBlock, false);
|
||||||
|
assert.equal(fromExtend.isShowLineNumber, true);
|
||||||
|
assert.equal(fromExtend.citeStatus, true);
|
||||||
|
assert.equal(fromExtend.countStatus, true);
|
||||||
|
assert.equal(fromExtend.legend, "title-alt");
|
||||||
|
assert.equal(fromExtend.keepTitle, true);
|
||||||
|
|
||||||
|
const explicit = resolveRenderOptions({
|
||||||
|
theme: "simple",
|
||||||
|
fontSize: "18px",
|
||||||
|
keepTitle: false,
|
||||||
|
});
|
||||||
|
assert.equal(explicit.theme, "simple");
|
||||||
|
assert.equal(explicit.fontSize, "18px");
|
||||||
|
assert.equal(explicit.keepTitle, false);
|
||||||
|
});
|
||||||
71
skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.test.ts
vendored
Normal file
71
skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.test.ts
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { DEFAULT_STYLE } from "./constants.ts";
|
||||||
|
import {
|
||||||
|
buildCss,
|
||||||
|
buildHtmlDocument,
|
||||||
|
modifyHtmlStructure,
|
||||||
|
normalizeCssText,
|
||||||
|
normalizeInlineCss,
|
||||||
|
removeFirstHeading,
|
||||||
|
} from "./html-builder.ts";
|
||||||
|
|
||||||
|
test("buildCss injects style variables and concatenates base and theme CSS", () => {
|
||||||
|
const css = buildCss("body { color: red; }", ".theme { color: blue; }");
|
||||||
|
|
||||||
|
assert.match(css, /--md-primary-color: #0F4C81;/);
|
||||||
|
assert.match(css, /body \{ color: red; \}/);
|
||||||
|
assert.match(css, /\.theme \{ color: blue; \}/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildHtmlDocument includes optional meta tags and code theme CSS", () => {
|
||||||
|
const html = buildHtmlDocument(
|
||||||
|
{
|
||||||
|
title: "Doc",
|
||||||
|
author: "Baoyu",
|
||||||
|
description: "Summary",
|
||||||
|
},
|
||||||
|
"body { color: red; }",
|
||||||
|
"<article>Hello</article>",
|
||||||
|
".hljs { color: blue; }",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(html, /<title>Doc<\/title>/);
|
||||||
|
assert.match(html, /meta name="author" content="Baoyu"/);
|
||||||
|
assert.match(html, /meta name="description" content="Summary"/);
|
||||||
|
assert.match(html, /<style>body \{ color: red; \}<\/style>/);
|
||||||
|
assert.match(html, /<style>\.hljs \{ color: blue; \}<\/style>/);
|
||||||
|
assert.match(html, /<article>Hello<\/article>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeCssText and normalizeInlineCss replace variables and strip declarations", () => {
|
||||||
|
const rawCss = `
|
||||||
|
:root { --md-primary-color: #000; --md-font-size: 12px; --foreground: 0 0% 5%; }
|
||||||
|
.box { color: var(--md-primary-color); font-size: var(--md-font-size); background: hsl(var(--foreground)); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const normalizedCss = normalizeCssText(rawCss, DEFAULT_STYLE);
|
||||||
|
assert.match(normalizedCss, /color: #0F4C81/);
|
||||||
|
assert.match(normalizedCss, /font-size: 16px/);
|
||||||
|
assert.match(normalizedCss, /background: #3f3f3f/);
|
||||||
|
assert.doesNotMatch(normalizedCss, /--md-primary-color/);
|
||||||
|
|
||||||
|
const normalizedHtml = normalizeInlineCss(
|
||||||
|
`<style>${rawCss}</style><div style="color: var(--md-primary-color)"></div>`,
|
||||||
|
DEFAULT_STYLE,
|
||||||
|
);
|
||||||
|
assert.match(normalizedHtml, /color: #0F4C81/);
|
||||||
|
assert.doesNotMatch(normalizedHtml, /var\(--md-primary-color\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("HTML structure helpers hoist nested lists and remove the first heading", () => {
|
||||||
|
const nestedList = `<ul><li>Parent<ul><li>Child</li></ul></li></ul>`;
|
||||||
|
assert.equal(
|
||||||
|
modifyHtmlStructure(nestedList),
|
||||||
|
`<ul><li>Parent</li><ul><li>Child</li></ul></ul>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const html = `<h1>Title</h1><p>Intro</p><h2>Sub</h2>`;
|
||||||
|
assert.equal(removeFirstHeading(html), `<p>Intro</p><h2>Sub</h2>`);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getImageExtension,
|
||||||
|
replaceMarkdownImagesWithPlaceholders,
|
||||||
|
resolveContentImages,
|
||||||
|
resolveImagePath,
|
||||||
|
} from "./images.ts";
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
test("replaceMarkdownImagesWithPlaceholders rewrites markdown and tracks image metadata", () => {
|
||||||
|
const result = replaceMarkdownImagesWithPlaceholders(
|
||||||
|
`\n\nText\n\n`,
|
||||||
|
"IMG_",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.markdown, `IMG_1\n\nText\n\nIMG_2`);
|
||||||
|
assert.deepEqual(result.images, [
|
||||||
|
{ alt: "cover", originalPath: "images/cover.png", placeholder: "IMG_1" },
|
||||||
|
{ alt: "diagram", originalPath: "images/diagram.webp", placeholder: "IMG_2" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("image extension and local fallback resolution handle common path variants", async (t) => {
|
||||||
|
assert.equal(getImageExtension("https://example.com/a.jpeg?x=1"), "jpeg");
|
||||||
|
assert.equal(getImageExtension("/tmp/figure"), "png");
|
||||||
|
|
||||||
|
const root = await makeTempDir("baoyu-md-images-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const baseDir = path.join(root, "article");
|
||||||
|
const tempDir = path.join(root, "tmp");
|
||||||
|
await fs.mkdir(baseDir, { recursive: true });
|
||||||
|
await fs.mkdir(tempDir, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(baseDir, "figure.webp"), "webp");
|
||||||
|
|
||||||
|
const resolved = await resolveImagePath("figure.png", baseDir, tempDir, "test");
|
||||||
|
assert.equal(resolved, path.join(baseDir, "figure.webp"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveContentImages resolves image placeholders against the content directory", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-md-content-images-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const baseDir = path.join(root, "article");
|
||||||
|
const tempDir = path.join(root, "tmp");
|
||||||
|
await fs.mkdir(baseDir, { recursive: true });
|
||||||
|
await fs.mkdir(tempDir, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(baseDir, "cover.png"), "png");
|
||||||
|
|
||||||
|
const resolved = await resolveContentImages(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
alt: "cover",
|
||||||
|
originalPath: "cover.png",
|
||||||
|
placeholder: "IMG_1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
baseDir,
|
||||||
|
tempDir,
|
||||||
|
"test",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(resolved, [
|
||||||
|
{
|
||||||
|
alt: "cover",
|
||||||
|
originalPath: "cover.png",
|
||||||
|
placeholder: "IMG_1",
|
||||||
|
localPath: path.join(baseDir, "cover.png"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
171
skills/baoyu-post-to-wechat/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts
vendored
Normal file
171
skills/baoyu-post-to-wechat/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import http from "node:http";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findChromeExecutable,
|
||||||
|
findExistingChromeDebugPort,
|
||||||
|
getFreePort,
|
||||||
|
resolveSharedChromeProfileDir,
|
||||||
|
waitForChromeDebugPort,
|
||||||
|
} from "./index.ts";
|
||||||
|
|
||||||
|
function useEnv(
|
||||||
|
t: TestContext,
|
||||||
|
values: Record<string, string | null>,
|
||||||
|
): void {
|
||||||
|
const previous = new Map<string, string | undefined>();
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
previous.set(key, process.env[key]);
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.after(() => {
|
||||||
|
for (const [key, value] of previous.entries()) {
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDebugServer(port: number): Promise<http.Server> {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.url === "/json/version") {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(port, "127.0.0.1", () => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeServer(server: http.Server): Promise<void> {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
|
|
||||||
|
const dynamicPort = await getFreePort();
|
||||||
|
assert.ok(Number.isInteger(dynamicPort));
|
||||||
|
assert.ok(dynamicPort > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findChromeExecutable prefers env overrides and falls back to candidate paths", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-chrome-bin-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const envChrome = path.join(root, "env-chrome");
|
||||||
|
const fallbackChrome = path.join(root, "fallback-chrome");
|
||||||
|
await fs.writeFile(envChrome, "");
|
||||||
|
await fs.writeFile(fallbackChrome, "");
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: envChrome });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
envChrome,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: null });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
fallbackChrome,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveSharedChromeProfileDir supports env overrides, WSL paths, and default suffixes", (t) => {
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: "/tmp/custom-profile" });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
envNames: ["BAOYU_SHARED_PROFILE"],
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.resolve("/tmp/custom-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: null });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
wslWindowsHome: "/mnt/c/Users/demo",
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.join("/mnt/c/Users/demo", ".local", "share", "demo-app", "demo-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fallback = resolveSharedChromeProfileDir({
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
});
|
||||||
|
assert.match(fallback, /demo-app[\\/]demo-profile$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findExistingChromeDebugPort reads DevToolsActivePort and validates it against a live endpoint", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-profile-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await findExistingChromeDebugPort({ profileDir: root, timeoutMs: 1000 });
|
||||||
|
assert.equal(found, port);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
const serverPromise = (async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
})();
|
||||||
|
|
||||||
|
const websocketUrl = await waitForChromeDebugPort(port, 4000, {
|
||||||
|
includeLastError: true,
|
||||||
|
});
|
||||||
|
await serverPromise;
|
||||||
|
|
||||||
|
assert.equal(websocketUrl, `ws://127.0.0.1:${port}/devtools/browser/demo`);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
extractSummaryFromBody,
|
||||||
|
extractTitleFromMarkdown,
|
||||||
|
parseFrontmatter,
|
||||||
|
pickFirstString,
|
||||||
|
serializeFrontmatter,
|
||||||
|
stripWrappingQuotes,
|
||||||
|
toFrontmatterString,
|
||||||
|
} from "./content.ts";
|
||||||
|
|
||||||
|
test("parseFrontmatter extracts YAML fields and strips wrapping quotes", () => {
|
||||||
|
const input = `---
|
||||||
|
title: "Hello World"
|
||||||
|
author: ‘Baoyu’
|
||||||
|
summary: plain text
|
||||||
|
---
|
||||||
|
# Heading
|
||||||
|
|
||||||
|
Body`;
|
||||||
|
|
||||||
|
const result = parseFrontmatter(input);
|
||||||
|
|
||||||
|
assert.deepEqual(result.frontmatter, {
|
||||||
|
title: "Hello World",
|
||||||
|
author: "Baoyu",
|
||||||
|
summary: "plain text",
|
||||||
|
});
|
||||||
|
assert.match(result.body, /^# Heading/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseFrontmatter returns original content when no frontmatter exists", () => {
|
||||||
|
const input = "# No frontmatter";
|
||||||
|
assert.deepEqual(parseFrontmatter(input), {
|
||||||
|
frontmatter: {},
|
||||||
|
body: input,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("serializeFrontmatter renders YAML only when fields exist", () => {
|
||||||
|
assert.equal(serializeFrontmatter({}), "");
|
||||||
|
assert.equal(
|
||||||
|
serializeFrontmatter({ title: "Hello", author: "Baoyu" }),
|
||||||
|
"---\ntitle: Hello\nauthor: Baoyu\n---\n",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("quote and frontmatter string helpers normalize mixed scalar values", () => {
|
||||||
|
assert.equal(stripWrappingQuotes(`" quoted "`), "quoted");
|
||||||
|
assert.equal(stripWrappingQuotes("“ 中文标题 ”"), "中文标题");
|
||||||
|
assert.equal(stripWrappingQuotes("plain"), "plain");
|
||||||
|
|
||||||
|
assert.equal(toFrontmatterString("'hello'"), "hello");
|
||||||
|
assert.equal(toFrontmatterString(42), "42");
|
||||||
|
assert.equal(toFrontmatterString(false), "false");
|
||||||
|
assert.equal(toFrontmatterString({}), undefined);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
pickFirstString({ summary: 123, title: "" }, ["title", "summary"]),
|
||||||
|
"123",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("markdown title and summary extraction skip non-body content and clean formatting", () => {
|
||||||
|
const markdown = `
|
||||||
|

|
||||||
|
## “My Title”
|
||||||
|
|
||||||
|
Body paragraph
|
||||||
|
`;
|
||||||
|
assert.equal(extractTitleFromMarkdown(markdown), "My Title");
|
||||||
|
|
||||||
|
const summary = extractSummaryFromBody(
|
||||||
|
`
|
||||||
|
# Heading
|
||||||
|
> quote
|
||||||
|
- list
|
||||||
|
1. ordered
|
||||||
|
\`\`\`
|
||||||
|
code
|
||||||
|
\`\`\`
|
||||||
|
This is **the first paragraph** with [a link](https://example.com) and \`inline code\` that should be summarized cleanly.
|
||||||
|
`,
|
||||||
|
70,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
summary,
|
||||||
|
"This is the first paragraph with a link and inline code that should...",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
|
import { COLOR_PRESETS, FONT_FAMILY_MAP } from "./constants.ts";
|
||||||
|
import {
|
||||||
|
buildMarkdownDocumentMeta,
|
||||||
|
formatTimestamp,
|
||||||
|
resolveColorToken,
|
||||||
|
resolveFontFamilyToken,
|
||||||
|
resolveMarkdownStyle,
|
||||||
|
resolveRenderOptions,
|
||||||
|
} from "./document.ts";
|
||||||
|
|
||||||
|
function useCwd(t: TestContext, cwd: string): void {
|
||||||
|
const previous = process.cwd();
|
||||||
|
process.chdir(cwd);
|
||||||
|
t.after(() => {
|
||||||
|
process.chdir(previous);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
test("document token resolvers map known presets and allow passthrough values", () => {
|
||||||
|
assert.equal(resolveColorToken("green"), COLOR_PRESETS.green);
|
||||||
|
assert.equal(resolveColorToken("#123456"), "#123456");
|
||||||
|
assert.equal(resolveColorToken(), undefined);
|
||||||
|
|
||||||
|
assert.equal(resolveFontFamilyToken("mono"), FONT_FAMILY_MAP.mono);
|
||||||
|
assert.equal(resolveFontFamilyToken("Custom Font"), "Custom Font");
|
||||||
|
assert.equal(resolveFontFamilyToken(), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatTimestamp uses compact sortable datetime output", () => {
|
||||||
|
const date = new Date("2026-03-13T21:04:05.000Z");
|
||||||
|
const pad = (value: number) => String(value).padStart(2, "0");
|
||||||
|
const expected = `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
|
||||||
|
date.getDate(),
|
||||||
|
)}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
||||||
|
|
||||||
|
assert.equal(formatTimestamp(date), expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildMarkdownDocumentMeta prefers frontmatter and falls back to markdown title and summary", () => {
|
||||||
|
const metaFromYaml = buildMarkdownDocumentMeta(
|
||||||
|
"# Markdown Title\n\nBody summary paragraph that should be ignored.",
|
||||||
|
{
|
||||||
|
title: `" YAML Title "`,
|
||||||
|
author: "'Baoyu'",
|
||||||
|
summary: `" YAML Summary "`,
|
||||||
|
},
|
||||||
|
"fallback",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(metaFromYaml, {
|
||||||
|
title: "YAML Title",
|
||||||
|
author: "Baoyu",
|
||||||
|
description: "YAML Summary",
|
||||||
|
});
|
||||||
|
|
||||||
|
const metaFromMarkdown = buildMarkdownDocumentMeta(
|
||||||
|
`## “Markdown Title”\n\nThis is the first body paragraph that should become the summary because it is long enough.`,
|
||||||
|
{},
|
||||||
|
"fallback",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(metaFromMarkdown.title, "Markdown Title");
|
||||||
|
assert.match(metaFromMarkdown.description ?? "", /^This is the first body paragraph/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveMarkdownStyle merges theme defaults with explicit overrides", () => {
|
||||||
|
const style = resolveMarkdownStyle({
|
||||||
|
theme: "modern",
|
||||||
|
primaryColor: "#112233",
|
||||||
|
fontFamily: "Custom Sans",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(style.primaryColor, "#112233");
|
||||||
|
assert.equal(style.fontFamily, "Custom Sans");
|
||||||
|
assert.equal(style.fontSize, "15px");
|
||||||
|
assert.equal(style.containerBg, "rgba(250, 249, 245, 1)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveRenderOptions loads workspace EXTEND settings and lets explicit options win", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-md-render-options-");
|
||||||
|
useCwd(t, root);
|
||||||
|
|
||||||
|
const extendPath = path.join(
|
||||||
|
root,
|
||||||
|
".baoyu-skills",
|
||||||
|
"baoyu-markdown-to-html",
|
||||||
|
"EXTEND.md",
|
||||||
|
);
|
||||||
|
await fs.mkdir(path.dirname(extendPath), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
extendPath,
|
||||||
|
`---
|
||||||
|
default_theme: modern
|
||||||
|
default_color: green
|
||||||
|
default_font_family: mono
|
||||||
|
default_font_size: 17
|
||||||
|
default_code_theme: nord
|
||||||
|
mac_code_block: false
|
||||||
|
show_line_number: true
|
||||||
|
cite: true
|
||||||
|
count: true
|
||||||
|
legend: title-alt
|
||||||
|
keep_title: true
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fromExtend = resolveRenderOptions();
|
||||||
|
assert.equal(fromExtend.theme, "modern");
|
||||||
|
assert.equal(fromExtend.primaryColor, COLOR_PRESETS.green);
|
||||||
|
assert.equal(fromExtend.fontFamily, FONT_FAMILY_MAP.mono);
|
||||||
|
assert.equal(fromExtend.fontSize, "17px");
|
||||||
|
assert.equal(fromExtend.codeTheme, "nord");
|
||||||
|
assert.equal(fromExtend.isMacCodeBlock, false);
|
||||||
|
assert.equal(fromExtend.isShowLineNumber, true);
|
||||||
|
assert.equal(fromExtend.citeStatus, true);
|
||||||
|
assert.equal(fromExtend.countStatus, true);
|
||||||
|
assert.equal(fromExtend.legend, "title-alt");
|
||||||
|
assert.equal(fromExtend.keepTitle, true);
|
||||||
|
|
||||||
|
const explicit = resolveRenderOptions({
|
||||||
|
theme: "simple",
|
||||||
|
fontSize: "18px",
|
||||||
|
keepTitle: false,
|
||||||
|
});
|
||||||
|
assert.equal(explicit.theme, "simple");
|
||||||
|
assert.equal(explicit.fontSize, "18px");
|
||||||
|
assert.equal(explicit.keepTitle, false);
|
||||||
|
});
|
||||||
71
skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.test.ts
vendored
Normal file
71
skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.test.ts
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { DEFAULT_STYLE } from "./constants.ts";
|
||||||
|
import {
|
||||||
|
buildCss,
|
||||||
|
buildHtmlDocument,
|
||||||
|
modifyHtmlStructure,
|
||||||
|
normalizeCssText,
|
||||||
|
normalizeInlineCss,
|
||||||
|
removeFirstHeading,
|
||||||
|
} from "./html-builder.ts";
|
||||||
|
|
||||||
|
test("buildCss injects style variables and concatenates base and theme CSS", () => {
|
||||||
|
const css = buildCss("body { color: red; }", ".theme { color: blue; }");
|
||||||
|
|
||||||
|
assert.match(css, /--md-primary-color: #0F4C81;/);
|
||||||
|
assert.match(css, /body \{ color: red; \}/);
|
||||||
|
assert.match(css, /\.theme \{ color: blue; \}/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildHtmlDocument includes optional meta tags and code theme CSS", () => {
|
||||||
|
const html = buildHtmlDocument(
|
||||||
|
{
|
||||||
|
title: "Doc",
|
||||||
|
author: "Baoyu",
|
||||||
|
description: "Summary",
|
||||||
|
},
|
||||||
|
"body { color: red; }",
|
||||||
|
"<article>Hello</article>",
|
||||||
|
".hljs { color: blue; }",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(html, /<title>Doc<\/title>/);
|
||||||
|
assert.match(html, /meta name="author" content="Baoyu"/);
|
||||||
|
assert.match(html, /meta name="description" content="Summary"/);
|
||||||
|
assert.match(html, /<style>body \{ color: red; \}<\/style>/);
|
||||||
|
assert.match(html, /<style>\.hljs \{ color: blue; \}<\/style>/);
|
||||||
|
assert.match(html, /<article>Hello<\/article>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeCssText and normalizeInlineCss replace variables and strip declarations", () => {
|
||||||
|
const rawCss = `
|
||||||
|
:root { --md-primary-color: #000; --md-font-size: 12px; --foreground: 0 0% 5%; }
|
||||||
|
.box { color: var(--md-primary-color); font-size: var(--md-font-size); background: hsl(var(--foreground)); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const normalizedCss = normalizeCssText(rawCss, DEFAULT_STYLE);
|
||||||
|
assert.match(normalizedCss, /color: #0F4C81/);
|
||||||
|
assert.match(normalizedCss, /font-size: 16px/);
|
||||||
|
assert.match(normalizedCss, /background: #3f3f3f/);
|
||||||
|
assert.doesNotMatch(normalizedCss, /--md-primary-color/);
|
||||||
|
|
||||||
|
const normalizedHtml = normalizeInlineCss(
|
||||||
|
`<style>${rawCss}</style><div style="color: var(--md-primary-color)"></div>`,
|
||||||
|
DEFAULT_STYLE,
|
||||||
|
);
|
||||||
|
assert.match(normalizedHtml, /color: #0F4C81/);
|
||||||
|
assert.doesNotMatch(normalizedHtml, /var\(--md-primary-color\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("HTML structure helpers hoist nested lists and remove the first heading", () => {
|
||||||
|
const nestedList = `<ul><li>Parent<ul><li>Child</li></ul></li></ul>`;
|
||||||
|
assert.equal(
|
||||||
|
modifyHtmlStructure(nestedList),
|
||||||
|
`<ul><li>Parent</li><ul><li>Child</li></ul></ul>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const html = `<h1>Title</h1><p>Intro</p><h2>Sub</h2>`;
|
||||||
|
assert.equal(removeFirstHeading(html), `<p>Intro</p><h2>Sub</h2>`);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getImageExtension,
|
||||||
|
replaceMarkdownImagesWithPlaceholders,
|
||||||
|
resolveContentImages,
|
||||||
|
resolveImagePath,
|
||||||
|
} from "./images.ts";
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
test("replaceMarkdownImagesWithPlaceholders rewrites markdown and tracks image metadata", () => {
|
||||||
|
const result = replaceMarkdownImagesWithPlaceholders(
|
||||||
|
`\n\nText\n\n`,
|
||||||
|
"IMG_",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.markdown, `IMG_1\n\nText\n\nIMG_2`);
|
||||||
|
assert.deepEqual(result.images, [
|
||||||
|
{ alt: "cover", originalPath: "images/cover.png", placeholder: "IMG_1" },
|
||||||
|
{ alt: "diagram", originalPath: "images/diagram.webp", placeholder: "IMG_2" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("image extension and local fallback resolution handle common path variants", async (t) => {
|
||||||
|
assert.equal(getImageExtension("https://example.com/a.jpeg?x=1"), "jpeg");
|
||||||
|
assert.equal(getImageExtension("/tmp/figure"), "png");
|
||||||
|
|
||||||
|
const root = await makeTempDir("baoyu-md-images-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const baseDir = path.join(root, "article");
|
||||||
|
const tempDir = path.join(root, "tmp");
|
||||||
|
await fs.mkdir(baseDir, { recursive: true });
|
||||||
|
await fs.mkdir(tempDir, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(baseDir, "figure.webp"), "webp");
|
||||||
|
|
||||||
|
const resolved = await resolveImagePath("figure.png", baseDir, tempDir, "test");
|
||||||
|
assert.equal(resolved, path.join(baseDir, "figure.webp"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveContentImages resolves image placeholders against the content directory", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-md-content-images-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const baseDir = path.join(root, "article");
|
||||||
|
const tempDir = path.join(root, "tmp");
|
||||||
|
await fs.mkdir(baseDir, { recursive: true });
|
||||||
|
await fs.mkdir(tempDir, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(baseDir, "cover.png"), "png");
|
||||||
|
|
||||||
|
const resolved = await resolveContentImages(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
alt: "cover",
|
||||||
|
originalPath: "cover.png",
|
||||||
|
placeholder: "IMG_1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
baseDir,
|
||||||
|
tempDir,
|
||||||
|
"test",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(resolved, [
|
||||||
|
{
|
||||||
|
alt: "cover",
|
||||||
|
originalPath: "cover.png",
|
||||||
|
placeholder: "IMG_1",
|
||||||
|
localPath: path.join(baseDir, "cover.png"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
171
skills/baoyu-post-to-weibo/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts
vendored
Normal file
171
skills/baoyu-post-to-weibo/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import http from "node:http";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findChromeExecutable,
|
||||||
|
findExistingChromeDebugPort,
|
||||||
|
getFreePort,
|
||||||
|
resolveSharedChromeProfileDir,
|
||||||
|
waitForChromeDebugPort,
|
||||||
|
} from "./index.ts";
|
||||||
|
|
||||||
|
function useEnv(
|
||||||
|
t: TestContext,
|
||||||
|
values: Record<string, string | null>,
|
||||||
|
): void {
|
||||||
|
const previous = new Map<string, string | undefined>();
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
previous.set(key, process.env[key]);
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.after(() => {
|
||||||
|
for (const [key, value] of previous.entries()) {
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDebugServer(port: number): Promise<http.Server> {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.url === "/json/version") {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(port, "127.0.0.1", () => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeServer(server: http.Server): Promise<void> {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
|
|
||||||
|
const dynamicPort = await getFreePort();
|
||||||
|
assert.ok(Number.isInteger(dynamicPort));
|
||||||
|
assert.ok(dynamicPort > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findChromeExecutable prefers env overrides and falls back to candidate paths", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-chrome-bin-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const envChrome = path.join(root, "env-chrome");
|
||||||
|
const fallbackChrome = path.join(root, "fallback-chrome");
|
||||||
|
await fs.writeFile(envChrome, "");
|
||||||
|
await fs.writeFile(fallbackChrome, "");
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: envChrome });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
envChrome,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: null });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
fallbackChrome,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveSharedChromeProfileDir supports env overrides, WSL paths, and default suffixes", (t) => {
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: "/tmp/custom-profile" });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
envNames: ["BAOYU_SHARED_PROFILE"],
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.resolve("/tmp/custom-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: null });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
wslWindowsHome: "/mnt/c/Users/demo",
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.join("/mnt/c/Users/demo", ".local", "share", "demo-app", "demo-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fallback = resolveSharedChromeProfileDir({
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
});
|
||||||
|
assert.match(fallback, /demo-app[\\/]demo-profile$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findExistingChromeDebugPort reads DevToolsActivePort and validates it against a live endpoint", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-profile-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await findExistingChromeDebugPort({ profileDir: root, timeoutMs: 1000 });
|
||||||
|
assert.equal(found, port);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
const serverPromise = (async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
})();
|
||||||
|
|
||||||
|
const websocketUrl = await waitForChromeDebugPort(port, 4000, {
|
||||||
|
includeLastError: true,
|
||||||
|
});
|
||||||
|
await serverPromise;
|
||||||
|
|
||||||
|
assert.equal(websocketUrl, `ws://127.0.0.1:${port}/devtools/browser/demo`);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
extractSummaryFromBody,
|
||||||
|
extractTitleFromMarkdown,
|
||||||
|
parseFrontmatter,
|
||||||
|
pickFirstString,
|
||||||
|
serializeFrontmatter,
|
||||||
|
stripWrappingQuotes,
|
||||||
|
toFrontmatterString,
|
||||||
|
} from "./content.ts";
|
||||||
|
|
||||||
|
test("parseFrontmatter extracts YAML fields and strips wrapping quotes", () => {
|
||||||
|
const input = `---
|
||||||
|
title: "Hello World"
|
||||||
|
author: ‘Baoyu’
|
||||||
|
summary: plain text
|
||||||
|
---
|
||||||
|
# Heading
|
||||||
|
|
||||||
|
Body`;
|
||||||
|
|
||||||
|
const result = parseFrontmatter(input);
|
||||||
|
|
||||||
|
assert.deepEqual(result.frontmatter, {
|
||||||
|
title: "Hello World",
|
||||||
|
author: "Baoyu",
|
||||||
|
summary: "plain text",
|
||||||
|
});
|
||||||
|
assert.match(result.body, /^# Heading/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseFrontmatter returns original content when no frontmatter exists", () => {
|
||||||
|
const input = "# No frontmatter";
|
||||||
|
assert.deepEqual(parseFrontmatter(input), {
|
||||||
|
frontmatter: {},
|
||||||
|
body: input,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("serializeFrontmatter renders YAML only when fields exist", () => {
|
||||||
|
assert.equal(serializeFrontmatter({}), "");
|
||||||
|
assert.equal(
|
||||||
|
serializeFrontmatter({ title: "Hello", author: "Baoyu" }),
|
||||||
|
"---\ntitle: Hello\nauthor: Baoyu\n---\n",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("quote and frontmatter string helpers normalize mixed scalar values", () => {
|
||||||
|
assert.equal(stripWrappingQuotes(`" quoted "`), "quoted");
|
||||||
|
assert.equal(stripWrappingQuotes("“ 中文标题 ”"), "中文标题");
|
||||||
|
assert.equal(stripWrappingQuotes("plain"), "plain");
|
||||||
|
|
||||||
|
assert.equal(toFrontmatterString("'hello'"), "hello");
|
||||||
|
assert.equal(toFrontmatterString(42), "42");
|
||||||
|
assert.equal(toFrontmatterString(false), "false");
|
||||||
|
assert.equal(toFrontmatterString({}), undefined);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
pickFirstString({ summary: 123, title: "" }, ["title", "summary"]),
|
||||||
|
"123",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("markdown title and summary extraction skip non-body content and clean formatting", () => {
|
||||||
|
const markdown = `
|
||||||
|

|
||||||
|
## “My Title”
|
||||||
|
|
||||||
|
Body paragraph
|
||||||
|
`;
|
||||||
|
assert.equal(extractTitleFromMarkdown(markdown), "My Title");
|
||||||
|
|
||||||
|
const summary = extractSummaryFromBody(
|
||||||
|
`
|
||||||
|
# Heading
|
||||||
|
> quote
|
||||||
|
- list
|
||||||
|
1. ordered
|
||||||
|
\`\`\`
|
||||||
|
code
|
||||||
|
\`\`\`
|
||||||
|
This is **the first paragraph** with [a link](https://example.com) and \`inline code\` that should be summarized cleanly.
|
||||||
|
`,
|
||||||
|
70,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
summary,
|
||||||
|
"This is the first paragraph with a link and inline code that should...",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
|
import { COLOR_PRESETS, FONT_FAMILY_MAP } from "./constants.ts";
|
||||||
|
import {
|
||||||
|
buildMarkdownDocumentMeta,
|
||||||
|
formatTimestamp,
|
||||||
|
resolveColorToken,
|
||||||
|
resolveFontFamilyToken,
|
||||||
|
resolveMarkdownStyle,
|
||||||
|
resolveRenderOptions,
|
||||||
|
} from "./document.ts";
|
||||||
|
|
||||||
|
function useCwd(t: TestContext, cwd: string): void {
|
||||||
|
const previous = process.cwd();
|
||||||
|
process.chdir(cwd);
|
||||||
|
t.after(() => {
|
||||||
|
process.chdir(previous);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
test("document token resolvers map known presets and allow passthrough values", () => {
|
||||||
|
assert.equal(resolveColorToken("green"), COLOR_PRESETS.green);
|
||||||
|
assert.equal(resolveColorToken("#123456"), "#123456");
|
||||||
|
assert.equal(resolveColorToken(), undefined);
|
||||||
|
|
||||||
|
assert.equal(resolveFontFamilyToken("mono"), FONT_FAMILY_MAP.mono);
|
||||||
|
assert.equal(resolveFontFamilyToken("Custom Font"), "Custom Font");
|
||||||
|
assert.equal(resolveFontFamilyToken(), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatTimestamp uses compact sortable datetime output", () => {
|
||||||
|
const date = new Date("2026-03-13T21:04:05.000Z");
|
||||||
|
const pad = (value: number) => String(value).padStart(2, "0");
|
||||||
|
const expected = `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
|
||||||
|
date.getDate(),
|
||||||
|
)}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
||||||
|
|
||||||
|
assert.equal(formatTimestamp(date), expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildMarkdownDocumentMeta prefers frontmatter and falls back to markdown title and summary", () => {
|
||||||
|
const metaFromYaml = buildMarkdownDocumentMeta(
|
||||||
|
"# Markdown Title\n\nBody summary paragraph that should be ignored.",
|
||||||
|
{
|
||||||
|
title: `" YAML Title "`,
|
||||||
|
author: "'Baoyu'",
|
||||||
|
summary: `" YAML Summary "`,
|
||||||
|
},
|
||||||
|
"fallback",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(metaFromYaml, {
|
||||||
|
title: "YAML Title",
|
||||||
|
author: "Baoyu",
|
||||||
|
description: "YAML Summary",
|
||||||
|
});
|
||||||
|
|
||||||
|
const metaFromMarkdown = buildMarkdownDocumentMeta(
|
||||||
|
`## “Markdown Title”\n\nThis is the first body paragraph that should become the summary because it is long enough.`,
|
||||||
|
{},
|
||||||
|
"fallback",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(metaFromMarkdown.title, "Markdown Title");
|
||||||
|
assert.match(metaFromMarkdown.description ?? "", /^This is the first body paragraph/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveMarkdownStyle merges theme defaults with explicit overrides", () => {
|
||||||
|
const style = resolveMarkdownStyle({
|
||||||
|
theme: "modern",
|
||||||
|
primaryColor: "#112233",
|
||||||
|
fontFamily: "Custom Sans",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(style.primaryColor, "#112233");
|
||||||
|
assert.equal(style.fontFamily, "Custom Sans");
|
||||||
|
assert.equal(style.fontSize, "15px");
|
||||||
|
assert.equal(style.containerBg, "rgba(250, 249, 245, 1)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveRenderOptions loads workspace EXTEND settings and lets explicit options win", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-md-render-options-");
|
||||||
|
useCwd(t, root);
|
||||||
|
|
||||||
|
const extendPath = path.join(
|
||||||
|
root,
|
||||||
|
".baoyu-skills",
|
||||||
|
"baoyu-markdown-to-html",
|
||||||
|
"EXTEND.md",
|
||||||
|
);
|
||||||
|
await fs.mkdir(path.dirname(extendPath), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
extendPath,
|
||||||
|
`---
|
||||||
|
default_theme: modern
|
||||||
|
default_color: green
|
||||||
|
default_font_family: mono
|
||||||
|
default_font_size: 17
|
||||||
|
default_code_theme: nord
|
||||||
|
mac_code_block: false
|
||||||
|
show_line_number: true
|
||||||
|
cite: true
|
||||||
|
count: true
|
||||||
|
legend: title-alt
|
||||||
|
keep_title: true
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fromExtend = resolveRenderOptions();
|
||||||
|
assert.equal(fromExtend.theme, "modern");
|
||||||
|
assert.equal(fromExtend.primaryColor, COLOR_PRESETS.green);
|
||||||
|
assert.equal(fromExtend.fontFamily, FONT_FAMILY_MAP.mono);
|
||||||
|
assert.equal(fromExtend.fontSize, "17px");
|
||||||
|
assert.equal(fromExtend.codeTheme, "nord");
|
||||||
|
assert.equal(fromExtend.isMacCodeBlock, false);
|
||||||
|
assert.equal(fromExtend.isShowLineNumber, true);
|
||||||
|
assert.equal(fromExtend.citeStatus, true);
|
||||||
|
assert.equal(fromExtend.countStatus, true);
|
||||||
|
assert.equal(fromExtend.legend, "title-alt");
|
||||||
|
assert.equal(fromExtend.keepTitle, true);
|
||||||
|
|
||||||
|
const explicit = resolveRenderOptions({
|
||||||
|
theme: "simple",
|
||||||
|
fontSize: "18px",
|
||||||
|
keepTitle: false,
|
||||||
|
});
|
||||||
|
assert.equal(explicit.theme, "simple");
|
||||||
|
assert.equal(explicit.fontSize, "18px");
|
||||||
|
assert.equal(explicit.keepTitle, false);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { DEFAULT_STYLE } from "./constants.ts";
|
||||||
|
import {
|
||||||
|
buildCss,
|
||||||
|
buildHtmlDocument,
|
||||||
|
modifyHtmlStructure,
|
||||||
|
normalizeCssText,
|
||||||
|
normalizeInlineCss,
|
||||||
|
removeFirstHeading,
|
||||||
|
} from "./html-builder.ts";
|
||||||
|
|
||||||
|
test("buildCss injects style variables and concatenates base and theme CSS", () => {
|
||||||
|
const css = buildCss("body { color: red; }", ".theme { color: blue; }");
|
||||||
|
|
||||||
|
assert.match(css, /--md-primary-color: #0F4C81;/);
|
||||||
|
assert.match(css, /body \{ color: red; \}/);
|
||||||
|
assert.match(css, /\.theme \{ color: blue; \}/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildHtmlDocument includes optional meta tags and code theme CSS", () => {
|
||||||
|
const html = buildHtmlDocument(
|
||||||
|
{
|
||||||
|
title: "Doc",
|
||||||
|
author: "Baoyu",
|
||||||
|
description: "Summary",
|
||||||
|
},
|
||||||
|
"body { color: red; }",
|
||||||
|
"<article>Hello</article>",
|
||||||
|
".hljs { color: blue; }",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(html, /<title>Doc<\/title>/);
|
||||||
|
assert.match(html, /meta name="author" content="Baoyu"/);
|
||||||
|
assert.match(html, /meta name="description" content="Summary"/);
|
||||||
|
assert.match(html, /<style>body \{ color: red; \}<\/style>/);
|
||||||
|
assert.match(html, /<style>\.hljs \{ color: blue; \}<\/style>/);
|
||||||
|
assert.match(html, /<article>Hello<\/article>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeCssText and normalizeInlineCss replace variables and strip declarations", () => {
|
||||||
|
const rawCss = `
|
||||||
|
:root { --md-primary-color: #000; --md-font-size: 12px; --foreground: 0 0% 5%; }
|
||||||
|
.box { color: var(--md-primary-color); font-size: var(--md-font-size); background: hsl(var(--foreground)); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const normalizedCss = normalizeCssText(rawCss, DEFAULT_STYLE);
|
||||||
|
assert.match(normalizedCss, /color: #0F4C81/);
|
||||||
|
assert.match(normalizedCss, /font-size: 16px/);
|
||||||
|
assert.match(normalizedCss, /background: #3f3f3f/);
|
||||||
|
assert.doesNotMatch(normalizedCss, /--md-primary-color/);
|
||||||
|
|
||||||
|
const normalizedHtml = normalizeInlineCss(
|
||||||
|
`<style>${rawCss}</style><div style="color: var(--md-primary-color)"></div>`,
|
||||||
|
DEFAULT_STYLE,
|
||||||
|
);
|
||||||
|
assert.match(normalizedHtml, /color: #0F4C81/);
|
||||||
|
assert.doesNotMatch(normalizedHtml, /var\(--md-primary-color\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("HTML structure helpers hoist nested lists and remove the first heading", () => {
|
||||||
|
const nestedList = `<ul><li>Parent<ul><li>Child</li></ul></li></ul>`;
|
||||||
|
assert.equal(
|
||||||
|
modifyHtmlStructure(nestedList),
|
||||||
|
`<ul><li>Parent</li><ul><li>Child</li></ul></ul>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const html = `<h1>Title</h1><p>Intro</p><h2>Sub</h2>`;
|
||||||
|
assert.equal(removeFirstHeading(html), `<p>Intro</p><h2>Sub</h2>`);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getImageExtension,
|
||||||
|
replaceMarkdownImagesWithPlaceholders,
|
||||||
|
resolveContentImages,
|
||||||
|
resolveImagePath,
|
||||||
|
} from "./images.ts";
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
test("replaceMarkdownImagesWithPlaceholders rewrites markdown and tracks image metadata", () => {
|
||||||
|
const result = replaceMarkdownImagesWithPlaceholders(
|
||||||
|
`\n\nText\n\n`,
|
||||||
|
"IMG_",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.markdown, `IMG_1\n\nText\n\nIMG_2`);
|
||||||
|
assert.deepEqual(result.images, [
|
||||||
|
{ alt: "cover", originalPath: "images/cover.png", placeholder: "IMG_1" },
|
||||||
|
{ alt: "diagram", originalPath: "images/diagram.webp", placeholder: "IMG_2" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("image extension and local fallback resolution handle common path variants", async (t) => {
|
||||||
|
assert.equal(getImageExtension("https://example.com/a.jpeg?x=1"), "jpeg");
|
||||||
|
assert.equal(getImageExtension("/tmp/figure"), "png");
|
||||||
|
|
||||||
|
const root = await makeTempDir("baoyu-md-images-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const baseDir = path.join(root, "article");
|
||||||
|
const tempDir = path.join(root, "tmp");
|
||||||
|
await fs.mkdir(baseDir, { recursive: true });
|
||||||
|
await fs.mkdir(tempDir, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(baseDir, "figure.webp"), "webp");
|
||||||
|
|
||||||
|
const resolved = await resolveImagePath("figure.png", baseDir, tempDir, "test");
|
||||||
|
assert.equal(resolved, path.join(baseDir, "figure.webp"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveContentImages resolves image placeholders against the content directory", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-md-content-images-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const baseDir = path.join(root, "article");
|
||||||
|
const tempDir = path.join(root, "tmp");
|
||||||
|
await fs.mkdir(baseDir, { recursive: true });
|
||||||
|
await fs.mkdir(tempDir, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(baseDir, "cover.png"), "png");
|
||||||
|
|
||||||
|
const resolved = await resolveContentImages(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
alt: "cover",
|
||||||
|
originalPath: "cover.png",
|
||||||
|
placeholder: "IMG_1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
baseDir,
|
||||||
|
tempDir,
|
||||||
|
"test",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(resolved, [
|
||||||
|
{
|
||||||
|
alt: "cover",
|
||||||
|
originalPath: "cover.png",
|
||||||
|
placeholder: "IMG_1",
|
||||||
|
localPath: path.join(baseDir, "cover.png"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import http from "node:http";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findChromeExecutable,
|
||||||
|
findExistingChromeDebugPort,
|
||||||
|
getFreePort,
|
||||||
|
resolveSharedChromeProfileDir,
|
||||||
|
waitForChromeDebugPort,
|
||||||
|
} from "./index.ts";
|
||||||
|
|
||||||
|
function useEnv(
|
||||||
|
t: TestContext,
|
||||||
|
values: Record<string, string | null>,
|
||||||
|
): void {
|
||||||
|
const previous = new Map<string, string | undefined>();
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
previous.set(key, process.env[key]);
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.after(() => {
|
||||||
|
for (const [key, value] of previous.entries()) {
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDebugServer(port: number): Promise<http.Server> {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.url === "/json/version") {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(port, "127.0.0.1", () => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeServer(server: http.Server): Promise<void> {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
|
|
||||||
|
const dynamicPort = await getFreePort();
|
||||||
|
assert.ok(Number.isInteger(dynamicPort));
|
||||||
|
assert.ok(dynamicPort > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findChromeExecutable prefers env overrides and falls back to candidate paths", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-chrome-bin-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const envChrome = path.join(root, "env-chrome");
|
||||||
|
const fallbackChrome = path.join(root, "fallback-chrome");
|
||||||
|
await fs.writeFile(envChrome, "");
|
||||||
|
await fs.writeFile(fallbackChrome, "");
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: envChrome });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
envChrome,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: null });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
fallbackChrome,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveSharedChromeProfileDir supports env overrides, WSL paths, and default suffixes", (t) => {
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: "/tmp/custom-profile" });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
envNames: ["BAOYU_SHARED_PROFILE"],
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.resolve("/tmp/custom-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: null });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
wslWindowsHome: "/mnt/c/Users/demo",
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.join("/mnt/c/Users/demo", ".local", "share", "demo-app", "demo-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fallback = resolveSharedChromeProfileDir({
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
});
|
||||||
|
assert.match(fallback, /demo-app[\\/]demo-profile$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findExistingChromeDebugPort reads DevToolsActivePort and validates it against a live endpoint", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-profile-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await findExistingChromeDebugPort({ profileDir: root, timeoutMs: 1000 });
|
||||||
|
assert.equal(found, port);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
const serverPromise = (async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
})();
|
||||||
|
|
||||||
|
const websocketUrl = await waitForChromeDebugPort(port, 4000, {
|
||||||
|
includeLastError: true,
|
||||||
|
});
|
||||||
|
await serverPromise;
|
||||||
|
|
||||||
|
assert.equal(websocketUrl, `ws://127.0.0.1:${port}/devtools/browser/demo`);
|
||||||
|
});
|
||||||
171
skills/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts
vendored
Normal file
171
skills/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import http from "node:http";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findChromeExecutable,
|
||||||
|
findExistingChromeDebugPort,
|
||||||
|
getFreePort,
|
||||||
|
resolveSharedChromeProfileDir,
|
||||||
|
waitForChromeDebugPort,
|
||||||
|
} from "./index.ts";
|
||||||
|
|
||||||
|
function useEnv(
|
||||||
|
t: TestContext,
|
||||||
|
values: Record<string, string | null>,
|
||||||
|
): void {
|
||||||
|
const previous = new Map<string, string | undefined>();
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
previous.set(key, process.env[key]);
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.after(() => {
|
||||||
|
for (const [key, value] of previous.entries()) {
|
||||||
|
if (value == null) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string): Promise<string> {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDebugServer(port: number): Promise<http.Server> {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.url === "/json/version") {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(port, "127.0.0.1", () => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeServer(server: http.Server): Promise<void> {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
|
|
||||||
|
const dynamicPort = await getFreePort();
|
||||||
|
assert.ok(Number.isInteger(dynamicPort));
|
||||||
|
assert.ok(dynamicPort > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findChromeExecutable prefers env overrides and falls back to candidate paths", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-chrome-bin-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const envChrome = path.join(root, "env-chrome");
|
||||||
|
const fallbackChrome = path.join(root, "fallback-chrome");
|
||||||
|
await fs.writeFile(envChrome, "");
|
||||||
|
await fs.writeFile(fallbackChrome, "");
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: envChrome });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
envChrome,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_CHROME_PATH: null });
|
||||||
|
assert.equal(
|
||||||
|
findChromeExecutable({
|
||||||
|
envNames: ["BAOYU_CHROME_PATH"],
|
||||||
|
candidates: { default: [fallbackChrome] },
|
||||||
|
}),
|
||||||
|
fallbackChrome,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveSharedChromeProfileDir supports env overrides, WSL paths, and default suffixes", (t) => {
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: "/tmp/custom-profile" });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
envNames: ["BAOYU_SHARED_PROFILE"],
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.resolve("/tmp/custom-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEnv(t, { BAOYU_SHARED_PROFILE: null });
|
||||||
|
assert.equal(
|
||||||
|
resolveSharedChromeProfileDir({
|
||||||
|
wslWindowsHome: "/mnt/c/Users/demo",
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
}),
|
||||||
|
path.join("/mnt/c/Users/demo", ".local", "share", "demo-app", "demo-profile"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fallback = resolveSharedChromeProfileDir({
|
||||||
|
appDataDirName: "demo-app",
|
||||||
|
profileDirName: "demo-profile",
|
||||||
|
});
|
||||||
|
assert.match(fallback, /demo-app[\\/]demo-profile$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findExistingChromeDebugPort reads DevToolsActivePort and validates it against a live endpoint", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-profile-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await findExistingChromeDebugPort({ profileDir: root, timeoutMs: 1000 });
|
||||||
|
assert.equal(found, port);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
const serverPromise = (async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
})();
|
||||||
|
|
||||||
|
const websocketUrl = await waitForChromeDebugPort(port, 4000, {
|
||||||
|
includeLastError: true,
|
||||||
|
});
|
||||||
|
await serverPromise;
|
||||||
|
|
||||||
|
assert.equal(websocketUrl, `ws://127.0.0.1:${port}/devtools/browser/demo`);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue