test: migrate tests from centralized mjs to colocated TypeScript

Move test files from tests/ directory to colocate with source code,
convert from .mjs to .ts using tsx runner, add workspaces and npm
cache to CI workflow.
This commit is contained in:
Jim Liu 宝玉 2026-03-13 16:36:06 -05:00
parent 484b00109f
commit 774ad784d8
15 changed files with 2600 additions and 42 deletions

View File

@ -16,6 +16,10 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Run tests - name: Run tests
run: npm test run: npm test

1958
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,14 @@
"name": "baoyu-skills", "name": "baoyu-skills",
"private": true, "private": true,
"type": "module", "type": "module",
"workspaces": [
"packages/*"
],
"scripts": { "scripts": {
"test": "node --test", "test": "node --import tsx --test",
"test:coverage": "node --experimental-test-coverage --test" "test:coverage": "node --import tsx --experimental-test-coverage --test"
},
"devDependencies": {
"tsx": "^4.20.5"
} }
} }

View 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`);
});

View File

@ -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...",
);
});

View File

@ -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);
});

View 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>`);
});

View File

@ -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"),
},
]);
});

View File

@ -7,18 +7,18 @@ import test from "node:test";
import { import {
listReleaseFiles, listReleaseFiles,
validateSelfContainedRelease, validateSelfContainedRelease,
} from "../scripts/lib/release-files.mjs"; } from "./release-files.mjs";
async function makeTempDir(prefix) { async function makeTempDir(prefix: string): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), prefix)); return fs.mkdtemp(path.join(os.tmpdir(), prefix));
} }
async function writeFile(filePath, contents = "") { async function writeFile(filePath: string, contents = ""): Promise<void> {
await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, contents); await fs.writeFile(filePath, contents);
} }
async function writeJson(filePath, value) { async function writeJson(filePath: string, value: unknown): Promise<void> {
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`); await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
} }

View File

@ -4,18 +4,18 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import test from "node:test"; import test from "node:test";
import { syncSharedSkillPackages } from "../scripts/lib/shared-skill-packages.mjs"; import { syncSharedSkillPackages } from "./shared-skill-packages.mjs";
async function makeTempDir(prefix) { async function makeTempDir(prefix: string): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), prefix)); return fs.mkdtemp(path.join(os.tmpdir(), prefix));
} }
async function writeFile(filePath, contents = "") { async function writeFile(filePath: string, contents = ""): Promise<void> {
await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, contents); await fs.writeFile(filePath, contents);
} }
async function writeJson(filePath, value) { async function writeJson(filePath: string, value: unknown): Promise<void> {
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`); await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
} }
@ -53,13 +53,13 @@ test("syncSharedSkillPackages vendors workspace packages into skill scripts", as
const updatedPackageJson = JSON.parse( const updatedPackageJson = JSON.parse(
await fs.readFile(path.join(consumerDir, "package.json"), "utf8"), await fs.readFile(path.join(consumerDir, "package.json"), "utf8"),
); ) as { dependencies: Record<string, string> };
assert.equal(updatedPackageJson.dependencies["baoyu-md"], "file:./vendor/baoyu-md"); assert.equal(updatedPackageJson.dependencies["baoyu-md"], "file:./vendor/baoyu-md");
assert.equal(updatedPackageJson.dependencies.kleur, "^4.1.5"); assert.equal(updatedPackageJson.dependencies.kleur, "^4.1.5");
const vendoredPackageJson = JSON.parse( const vendoredPackageJson = JSON.parse(
await fs.readFile(path.join(consumerDir, "vendor", "baoyu-md", "package.json"), "utf8"), await fs.readFile(path.join(consumerDir, "vendor", "baoyu-md", "package.json"), "utf8"),
); ) as { name: string };
assert.equal(vendoredPackageJson.name, "baoyu-md"); assert.equal(vendoredPackageJson.name, "baoyu-md");
const vendoredFile = await fs.readFile( const vendoredFile = await fs.readFile(

View File

@ -2,8 +2,9 @@ import assert from "node:assert/strict";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import test from "node:test"; import test, { type TestContext } from "node:test";
import type { CliArgs, ExtendConfig } from "./types.ts";
import { import {
createTaskArgs, createTaskArgs,
detectProvider, detectProvider,
@ -16,9 +17,9 @@ import {
normalizeOutputImagePath, normalizeOutputImagePath,
parseArgs, parseArgs,
parseSimpleYaml, parseSimpleYaml,
} from "../../../skills/baoyu-image-gen/scripts/main.ts"; } from "./main.ts";
function makeArgs(overrides = {}) { function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
return { return {
prompt: null, prompt: null,
promptFiles: [], promptFiles: [],
@ -39,8 +40,11 @@ function makeArgs(overrides = {}) {
}; };
} }
function useEnv(t, values) { function useEnv(
const previous = new Map(); t: TestContext,
values: Record<string, string | null>,
): void {
const previous = new Map<string, string | undefined>();
for (const [key, value] of Object.entries(values)) { for (const [key, value] of Object.entries(values)) {
previous.set(key, process.env[key]); previous.set(key, process.env[key]);
if (value == null) { if (value == null) {
@ -61,7 +65,7 @@ function useEnv(t, values) {
}); });
} }
async function makeTempDir(prefix) { async function makeTempDir(prefix: string): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), prefix)); return fs.mkdtemp(path.join(os.tmpdir(), prefix));
} }
@ -161,7 +165,7 @@ test("mergeConfig only fills values missing from CLI args", () => {
default_quality: "2k", default_quality: "2k",
default_aspect_ratio: "3:2", default_aspect_ratio: "3:2",
default_image_size: "2K", default_image_size: "2K",
}, } satisfies Partial<ExtendConfig>,
); );
assert.equal(merged.provider, "openai"); assert.equal(merged.provider, "openai");
@ -219,7 +223,7 @@ test("batch worker and provider-rate-limit configuration prefer env over EXTEND
BAOYU_IMAGE_GEN_GOOGLE_START_INTERVAL_MS: "450", BAOYU_IMAGE_GEN_GOOGLE_START_INTERVAL_MS: "450",
}); });
const extendConfig = { const extendConfig: Partial<ExtendConfig> = {
batch: { batch: {
max_workers: 7, max_workers: 7,
provider_limits: { provider_limits: {
@ -263,7 +267,7 @@ test("loadBatchTasks and createTaskArgs resolve batch-relative paths", async (t)
const loaded = await loadBatchTasks(batchFile); const loaded = await loadBatchTasks(batchFile);
assert.equal(loaded.jobs, 2); assert.equal(loaded.jobs, 2);
assert.equal(loaded.batchDir, path.dirname(batchFile)); assert.equal(loaded.batchDir, path.dirname(batchFile));
assert.equal(loaded.tasks[0].id, "hero"); assert.equal(loaded.tasks[0]?.id, "hero");
const taskArgs = createTaskArgs( const taskArgs = createTaskArgs(
makeArgs({ makeArgs({
@ -271,7 +275,7 @@ test("loadBatchTasks and createTaskArgs resolve batch-relative paths", async (t)
quality: "2k", quality: "2k",
json: true, json: true,
}), }),
loaded.tasks[0], loaded.tasks[0]!,
loaded.batchDir, loaded.batchDir,
); );

View File

@ -5,7 +5,7 @@ import {
getSizeFromAspectRatio, getSizeFromAspectRatio,
normalizeSize, normalizeSize,
parseAspectRatio, parseAspectRatio,
} from "../../../skills/baoyu-image-gen/scripts/providers/dashscope.ts"; } from "./dashscope.ts";
test("DashScope aspect-ratio parsing accepts numeric ratios only", () => { test("DashScope aspect-ratio parsing accepts numeric ratios only", () => {
assert.deepEqual(parseAspectRatio("3:2"), { width: 3, height: 2 }); assert.deepEqual(parseAspectRatio("3:2"), { width: 3, height: 2 });

View File

@ -1,6 +1,7 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import test from "node:test"; import test, { type TestContext } from "node:test";
import type { CliArgs } from "../types.ts";
import { import {
addAspectRatioToPrompt, addAspectRatioToPrompt,
buildGoogleUrl, buildGoogleUrl,
@ -11,10 +12,13 @@ import {
isGoogleImagen, isGoogleImagen,
isGoogleMultimodal, isGoogleMultimodal,
normalizeGoogleModelId, normalizeGoogleModelId,
} from "../../../skills/baoyu-image-gen/scripts/providers/google.ts"; } from "./google.ts";
function useEnv(t, values) { function useEnv(
const previous = new Map(); t: TestContext,
values: Record<string, string | null>,
): void {
const previous = new Map<string, string | undefined>();
for (const [key, value] of Object.entries(values)) { for (const [key, value] of Object.entries(values)) {
previous.set(key, process.env[key]); previous.set(key, process.env[key]);
if (value == null) { if (value == null) {
@ -35,6 +39,27 @@ function useEnv(t, values) {
}); });
} }
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
return {
prompt: null,
promptFiles: [],
imagePath: null,
provider: null,
model: null,
aspectRatio: null,
size: null,
quality: null,
imageSize: null,
referenceImages: [],
n: 1,
batchFile: null,
jobs: null,
json: false,
help: false,
...overrides,
};
}
test("Google provider helpers normalize model IDs and select image size defaults", () => { test("Google provider helpers normalize model IDs and select image size defaults", () => {
assert.equal( assert.equal(
normalizeGoogleModelId("models/gemini-3.1-flash-image-preview"), normalizeGoogleModelId("models/gemini-3.1-flash-image-preview"),
@ -42,14 +67,8 @@ test("Google provider helpers normalize model IDs and select image size defaults
); );
assert.equal(isGoogleMultimodal("models/gemini-3-pro-image-preview"), true); assert.equal(isGoogleMultimodal("models/gemini-3-pro-image-preview"), true);
assert.equal(isGoogleImagen("imagen-3.0-generate-002"), true); assert.equal(isGoogleImagen("imagen-3.0-generate-002"), true);
assert.equal( assert.equal(getGoogleImageSize(makeArgs({ imageSize: null, quality: "2k" })), "2K");
getGoogleImageSize({ imageSize: null, quality: "2k" }), assert.equal(getGoogleImageSize(makeArgs({ imageSize: "4K", quality: "normal" })), "4K");
"2K",
);
assert.equal(
getGoogleImageSize({ imageSize: "4K", quality: "normal" }),
"4K",
);
}); });
test("Google URL builder appends v1beta when the base URL does not already include it", (t) => { test("Google URL builder appends v1beta when the base URL does not already include it", (t) => {

View File

@ -6,7 +6,7 @@ import {
getMimeType, getMimeType,
getOpenAISize, getOpenAISize,
parseAspectRatio, parseAspectRatio,
} from "../../../skills/baoyu-image-gen/scripts/providers/openai.ts"; } from "./openai.ts";
test("OpenAI aspect-ratio parsing and size selection match model families", () => { test("OpenAI aspect-ratio parsing and size selection match model families", () => {
assert.deepEqual(parseAspectRatio("16:9"), { width: 16, height: 9 }); assert.deepEqual(parseAspectRatio("16:9"), { width: 16, height: 9 });

View File

@ -1,17 +1,30 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import test from "node:test"; import test from "node:test";
import type { CliArgs } from "../types.ts";
import { import {
buildInput, buildInput,
extractOutputUrl, extractOutputUrl,
parseModelId, parseModelId,
} from "../../../skills/baoyu-image-gen/scripts/providers/replicate.ts"; } from "./replicate.ts";
function makeArgs(overrides = {}) { function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
return { return {
prompt: null,
promptFiles: [],
imagePath: null,
provider: null,
model: null,
aspectRatio: null, aspectRatio: null,
size: null,
quality: null, quality: null,
imageSize: null,
referenceImages: [],
n: 1, n: 1,
batchFile: null,
jobs: null,
json: false,
help: false,
...overrides, ...overrides,
}; };
} }
@ -69,20 +82,20 @@ test("Replicate input builder maps aspect ratio, image count, quality, and refs"
test("Replicate output extraction supports string, array, and object URLs", () => { test("Replicate output extraction supports string, array, and object URLs", () => {
assert.equal( assert.equal(
extractOutputUrl({ output: "https://example.com/a.png" }), extractOutputUrl({ output: "https://example.com/a.png" } as never),
"https://example.com/a.png", "https://example.com/a.png",
); );
assert.equal( assert.equal(
extractOutputUrl({ output: ["https://example.com/b.png"] }), extractOutputUrl({ output: ["https://example.com/b.png"] } as never),
"https://example.com/b.png", "https://example.com/b.png",
); );
assert.equal( assert.equal(
extractOutputUrl({ output: { url: "https://example.com/c.png" } }), extractOutputUrl({ output: { url: "https://example.com/c.png" } } as never),
"https://example.com/c.png", "https://example.com/c.png",
); );
assert.throws( assert.throws(
() => extractOutputUrl({ output: { invalid: true } }), () => extractOutputUrl({ output: { invalid: true } } as never),
/Unexpected Replicate output format/, /Unexpected Replicate output format/,
); );
}); });