From 632c34d120d61aaef7946bbfb247487a68eba820 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Tue, 19 May 2026 14:27:38 -0400 Subject: [PATCH] chore: strengthen package test task --- package.json | 15 ++++--- scripts/build.mjs | 29 ++++++++++++++ scripts/package-smoke.mjs | 65 +++++++++++++++++++++++++++++++ scripts/test-all.mjs | 27 +++++++++++++ test/esm-ambiguous-module.test.ts | 4 +- test/package.test.ts | 29 ++++++++++++++ 6 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 scripts/build.mjs create mode 100644 scripts/package-smoke.mjs create mode 100644 scripts/test-all.mjs diff --git a/package.json b/package.json index ea7db2d..b92826f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..76f9d75 --- /dev/null +++ b/scripts/build.mjs @@ -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); diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs new file mode 100644 index 0000000..f9622e7 --- /dev/null +++ b/scripts/package-smoke.mjs @@ -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 }); +} diff --git a/scripts/test-all.mjs b/scripts/test-all.mjs new file mode 100644 index 0000000..5dafc4d --- /dev/null +++ b/scripts/test-all.mjs @@ -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"]); diff --git a/test/esm-ambiguous-module.test.ts b/test/esm-ambiguous-module.test.ts index d4602af..3cfc5e5 100644 --- a/test/esm-ambiguous-module.test.ts +++ b/test/esm-ambiguous-module.test.ts @@ -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", diff --git a/test/package.test.ts b/test/package.test.ts index 030d1aa..623fea4 100644 --- a/test/package.test.ts +++ b/test/package.test.ts @@ -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");