diff --git a/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts b/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts new file mode 100644 index 0000000..a3e93b4 --- /dev/null +++ b/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts @@ -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, +): void { + const previous = new Map(); + 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 { + return fs.mkdtemp(path.join(os.tmpdir(), prefix)); +} + +async function startDebugServer(port: number): Promise { + 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((resolve, reject) => { + server.once("error", reject); + server.listen(port, "127.0.0.1", () => resolve()); + }); + + return server; +} + +async function closeServer(server: http.Server): Promise { + await new Promise((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`); +}); diff --git a/skills/baoyu-danger-x-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts b/skills/baoyu-danger-x-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts new file mode 100644 index 0000000..a3e93b4 --- /dev/null +++ b/skills/baoyu-danger-x-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts @@ -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, +): void { + const previous = new Map(); + 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 { + return fs.mkdtemp(path.join(os.tmpdir(), prefix)); +} + +async function startDebugServer(port: number): Promise { + 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((resolve, reject) => { + server.once("error", reject); + server.listen(port, "127.0.0.1", () => resolve()); + }); + + return server; +} + +async function closeServer(server: http.Server): Promise { + await new Promise((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`); +}); diff --git a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/content.test.ts b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/content.test.ts new file mode 100644 index 0000000..91dbd72 --- /dev/null +++ b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/content.test.ts @@ -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 = ` +![cover](cover.png) +## “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...", + ); +}); diff --git a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/document.test.ts b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/document.test.ts new file mode 100644 index 0000000..c188acc --- /dev/null +++ b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/document.test.ts @@ -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 { + 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); +}); diff --git a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.test.ts b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.test.ts new file mode 100644 index 0000000..2cab343 --- /dev/null +++ b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.test.ts @@ -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; }", + "
Hello
", + ".hljs { color: blue; }", + ); + + assert.match(html, /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>`); +}); diff --git a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/images.test.ts b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/images.test.ts new file mode 100644 index 0000000..cc187c2 --- /dev/null +++ b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/images.test.ts @@ -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( + `![cover](images/cover.png)\n\nText\n\n![diagram](images/diagram.webp)`, + "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"), + }, + ]); +}); diff --git a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts new file mode 100644 index 0000000..a3e93b4 --- /dev/null +++ b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts @@ -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`); +}); diff --git a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/content.test.ts b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/content.test.ts new file mode 100644 index 0000000..91dbd72 --- /dev/null +++ b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/content.test.ts @@ -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 = ` +![cover](cover.png) +## “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...", + ); +}); diff --git a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/document.test.ts b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/document.test.ts new file mode 100644 index 0000000..c188acc --- /dev/null +++ b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/document.test.ts @@ -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); +}); diff --git a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.test.ts b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.test.ts new file mode 100644 index 0000000..2cab343 --- /dev/null +++ b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.test.ts @@ -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>`); +}); diff --git a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/images.test.ts b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/images.test.ts new file mode 100644 index 0000000..cc187c2 --- /dev/null +++ b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/images.test.ts @@ -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( + `![cover](images/cover.png)\n\nText\n\n![diagram](images/diagram.webp)`, + "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"), + }, + ]); +}); diff --git a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts new file mode 100644 index 0000000..a3e93b4 --- /dev/null +++ b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts @@ -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`); +}); diff --git a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/content.test.ts b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/content.test.ts new file mode 100644 index 0000000..91dbd72 --- /dev/null +++ b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/content.test.ts @@ -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 = ` +![cover](cover.png) +## “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...", + ); +}); diff --git a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/document.test.ts b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/document.test.ts new file mode 100644 index 0000000..c188acc --- /dev/null +++ b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/document.test.ts @@ -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); +}); diff --git a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/html-builder.test.ts b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/html-builder.test.ts new file mode 100644 index 0000000..2cab343 --- /dev/null +++ b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/html-builder.test.ts @@ -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>`); +}); diff --git a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/images.test.ts b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/images.test.ts new file mode 100644 index 0000000..cc187c2 --- /dev/null +++ b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/images.test.ts @@ -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( + `![cover](images/cover.png)\n\nText\n\n![diagram](images/diagram.webp)`, + "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"), + }, + ]); +}); diff --git a/skills/baoyu-post-to-x/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts b/skills/baoyu-post-to-x/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts new file mode 100644 index 0000000..a3e93b4 --- /dev/null +++ b/skills/baoyu-post-to-x/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts @@ -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`); +}); diff --git a/skills/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts b/skills/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts new file mode 100644 index 0000000..a3e93b4 --- /dev/null +++ b/skills/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts @@ -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`); +});