chore: strengthen package test task

This commit is contained in:
Tobi Lutke 2026-05-19 14:27:38 -04:00
parent d9348f43a0
commit 632c34d120
No known key found for this signature in database
6 changed files with 162 additions and 7 deletions

View File

@ -18,17 +18,22 @@
"bin/",
"dist/",
"skills/",
"scripts/build.mjs",
"scripts/check-package-grammars.mjs",
"scripts/package-smoke.mjs",
"scripts/test-all.mjs",
"LICENSE",
"CHANGELOG.md"
],
"scripts": {
"prepare": "[ -d .git ] && ./scripts/install-hooks.sh || true",
"build": "tsc -p tsconfig.build.json && printf '#!/usr/bin/env node\n' | cat - dist/cli/qmd.js > dist/cli/qmd.tmp && mv dist/cli/qmd.tmp dist/cli/qmd.js && chmod +x dist/cli/qmd.js",
"test": "bun run test:unit",
"test:node": "node ./node_modules/vitest/vitest.mjs run --reporter=verbose",
"test:bun": "bun test --preload ./src/test-preload.ts",
"test:unit": "bun run test:node -- test/ && bun run test:bun -- test/",
"build": "node scripts/build.mjs",
"test": "node scripts/test-all.mjs",
"test:types": "node ./node_modules/typescript/bin/tsc -p tsconfig.build.json --noEmit",
"test:node": "node ./node_modules/vitest/vitest.mjs run --reporter=verbose --testTimeout 60000",
"test:bun": "bun test --timeout 60000 --preload ./src/test-preload.ts",
"test:unit": "CI=true node ./node_modules/vitest/vitest.mjs run --reporter=verbose --testTimeout 60000 test/ && CI=true bun test --timeout 60000 --preload ./src/test-preload.ts test/",
"test:package": "node scripts/package-smoke.mjs",
"qmd": "tsx src/cli/qmd.ts",
"index": "tsx src/cli/qmd.ts index",
"vector": "tsx src/cli/qmd.ts vector",

29
scripts/build.mjs Normal file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { chmodSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(fileURLToPath(new URL("..", import.meta.url)));
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: root,
stdio: "inherit",
shell: process.platform === "win32",
...options,
});
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
run(process.execPath, [join(root, "node_modules", "typescript", "bin", "tsc"), "-p", "tsconfig.build.json"]);
const cliPath = join(root, "dist", "cli", "qmd.js");
const tmpPath = `${cliPath}.tmp`;
const built = readFileSync(cliPath, "utf8");
const withoutExistingShebang = built.startsWith("#!") ? built.slice(built.indexOf("\n") + 1) : built;
writeFileSync(tmpPath, `#!/usr/bin/env node\n${withoutExistingShebang}`);
renameSync(tmpPath, cliPath);
chmodSync(cliPath, 0o755);

65
scripts/package-smoke.mjs Normal file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("..", import.meta.url));
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
function run(label, command, args, options = {}) {
console.log(`==> ${label}`);
const { quiet, ...spawnOptions } = options;
const result = spawnSync(command, args, {
cwd: root,
stdio: quiet ? "pipe" : "inherit",
shell: process.platform === "win32",
...spawnOptions,
});
if (result.status !== 0) {
console.error(`Package smoke failed: ${label}`);
if (quiet) {
if (result.stdout) process.stderr.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
}
process.exit(result.status ?? 1);
}
}
function assertPath(path, label = path) {
const full = join(root, path);
if (!existsSync(full)) {
console.error(`Package smoke failed: missing ${label} (${path})`);
process.exit(1);
}
return full;
}
run("build compiled package", process.execPath, ["scripts/build.mjs"]);
run("AST grammar runtime packages", process.execPath, ["scripts/check-package-grammars.mjs"]);
for (const entry of pkg.files ?? []) {
assertPath(entry.replace(/\/$/, ""), `package.json files[] entry ${entry}`);
}
for (const [name, binPath] of Object.entries(pkg.bin ?? {})) {
const full = assertPath(binPath, `bin ${name}`);
const mode = statSync(full).mode;
if ((mode & 0o111) === 0) {
console.error(`Package smoke failed: bin ${name} is not executable (${binPath})`);
process.exit(1);
}
}
assertPath("dist/index.js", "compiled main export");
assertPath("dist/index.d.ts", "compiled type export");
assertPath("dist/cli/qmd.js", "compiled CLI");
run("compiled CLI under Node", process.execPath, ["dist/cli/qmd.js", "--help"], { quiet: true });
run("package wrapper", "sh", ["bin/qmd", "--help"], { quiet: true });
if (process.env.QMD_SKIP_BUN_SMOKE === "1") {
console.log("==> compiled CLI under Bun (skipped by QMD_SKIP_BUN_SMOKE=1)");
} else {
run("compiled CLI under Bun", "bun", ["dist/cli/qmd.js", "--help"], { quiet: true });
}

27
scripts/test-all.mjs Normal file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("..", import.meta.url));
function run(label, command, args, options = {}) {
console.log(`==> ${label}`);
const { env: extraEnv, ...spawnOptions } = options;
const result = spawnSync(command, args, {
cwd: root,
stdio: "inherit",
shell: process.platform === "win32",
env: { ...process.env, ...(extraEnv ?? {}) },
...spawnOptions,
});
if (result.status !== 0) {
console.error(`Test task failed: ${label}`);
process.exit(result.status ?? 1);
}
}
run("TypeScript build typecheck", process.execPath, [join(root, "node_modules", "typescript", "bin", "tsc"), "-p", "tsconfig.build.json", "--noEmit"]);
run("Vitest suite under Node", process.execPath, [join(root, "node_modules", "vitest", "vitest.mjs"), "run", "--reporter=verbose", "--testTimeout", "60000", "test/"], { env: { CI: "true" } });
run("Bun test suite", "bun", ["test", "--timeout", "60000", "--preload", "./src/test-preload.ts", "test/"], { env: { CI: "true" } });
run("Package smoke", process.execPath, ["scripts/package-smoke.mjs"]);

View File

@ -9,14 +9,14 @@ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
describe("Node ESM entrypoints", () => {
test("CLI --index path normalizes via setIndexName/setConfigIndexName under Node 22+", () => {
execFileSync("npm", ["run", "build"], {
execFileSync(process.execPath, ["scripts/build.mjs"], {
cwd: repoRoot,
encoding: "utf-8",
stdio: "pipe",
});
const indexPath = join(mkdtempSync(join(tmpdir(), "qmd-index-")), "nested", "idx");
const output = execFileSync("node", ["dist/cli/qmd.js", "--index", indexPath, "--version"], {
const output = execFileSync(process.execPath, ["dist/cli/qmd.js", "--index", indexPath, "--version"], {
cwd: repoRoot,
encoding: "utf-8",
stdio: "pipe",

View File

@ -5,6 +5,32 @@ import { join } from "node:path";
const root = new URL("..", import.meta.url);
const pkg = JSON.parse(readFileSync(new URL("package.json", root), "utf8"));
describe("package test task", () => {
test("runs typecheck, unit tests, and package smoke checks", () => {
expect(pkg.scripts.test).toContain("scripts/test-all.mjs");
expect(pkg.scripts["test:types"]).toContain("tsconfig.build.json --noEmit");
expect(pkg.scripts["test:unit"]).toContain("vitest.mjs");
expect(pkg.scripts["test:unit"]).toContain("bun test");
expect(pkg.scripts["test:unit"]).toContain("CI=true");
expect(pkg.scripts["test:package"]).toContain("scripts/package-smoke.mjs");
const testAllScript = readFileSync(new URL("scripts/test-all.mjs", root), "utf8");
expect(testAllScript).toContain("TypeScript build typecheck");
expect(testAllScript).toContain("Vitest suite under Node");
expect(testAllScript).toContain("Bun test suite");
expect(testAllScript).toContain("Package smoke");
const packageSmokeScript = readFileSync(new URL("scripts/package-smoke.mjs", root), "utf8");
expect(packageSmokeScript).toContain("scripts/build.mjs");
expect(packageSmokeScript).toContain("scripts/check-package-grammars.mjs");
expect(packageSmokeScript).toContain("compiled CLI under Node");
expect(packageSmokeScript).toContain("compiled CLI under Bun");
expect(packageSmokeScript).toContain("package wrapper");
});
});
describe("package grammar distribution", () => {
test("installs AST grammar wasm packages as required runtime dependencies", () => {
for (const dep of ["tree-sitter-typescript", "tree-sitter-python", "tree-sitter-go", "tree-sitter-rust"]) {
@ -17,7 +43,10 @@ describe("package grammar distribution", () => {
expect(pkg.scripts, "package.json scripts").toHaveProperty("smoke:package-grammars");
expect(String(pkg.scripts["smoke:package-grammars"])).toContain("check-package-grammars");
expect(pkg.files, "published package files").toContain("scripts/build.mjs");
expect(pkg.files, "published package files").toContain("scripts/check-package-grammars.mjs");
expect(pkg.files, "published package files").toContain("scripts/package-smoke.mjs");
expect(pkg.files, "published package files").toContain("scripts/test-all.mjs");
expect(pkg.files, "published package files").toContain("skills/");
const qmdSkill = readFileSync(new URL("skills/qmd/SKILL.md", root), "utf8");
expect(qmdSkill).toContain("# QMD - Query Markdown Documents");