Files
windmill/cli/test/tar_creation_unit.test.ts
centdix 5fd2c1a129 chore(cli): separate unit tests from integration tests and fix test cleanup (#8562)
* fix(cli): separate unit tests from integration tests and fix test cleanup

- Rename 14 non-backend test files to *_unit.test.ts convention
- Add UNIT_ONLY env var guard in setup.ts to skip cargo build/backend startup
- Add test:unit and test:integration scripts to package.json
- Use setsid on Linux for process group management so stop() kills both
  cargo and the windmill child process
- Fix exit handler to kill process group instead of just the direct child
- Add cleanupStaleTestResources() to drop orphaned windmill_test_* databases
  and kill orphaned backend processes on startup
- Rewrite TESTING.md with current bun-based instructions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): fix process group approach - kill by db name instead of setsid

The setsid approach didn't work because setsid forks, making the PID
we get from Bun.spawn ephemeral. Instead, kill orphaned windmill child
processes by matching our unique database name in /proc/pid/environ.

Also add afterAll hook in setup.ts so full async cleanup (process kill
+ database drop) runs when all tests complete normally, not just on
SIGINT/SIGTERM.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): address PR review feedback

- Remove duplicate cleanupStaleTestResources() call in getTestBackend()
  (already called in setup.ts)
- Add regex guard on database names before SQL interpolation
- Extract shared killWindmillProcessesByEnvMatch() helper to deduplicate
  process-killing logic
- Remove redundant test:integration script (test already runs everything)
- Flip setup.ts to if/else pattern for readability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:13:33 +00:00

141 lines
4.3 KiB
TypeScript

/**
* Unit tests for the tar creation utility.
* These tests require no backend — they test standalone tar logic.
*/
import { expect, test, describe } from "bun:test";
import { createTarBlob, type TarEntry } from "../src/utils/tar.ts";
import { extract, type Headers } from "tar-stream";
import { Readable } from "node:stream";
/** Extract all entries from a tarball Blob into a map of name -> content string */
async function extractTar(
blob: Blob
): Promise<Map<string, { content: string; header: Headers }>> {
const result = new Map<string, { content: string; header: Headers }>();
const ex = extract();
const buffer = Buffer.from(await blob.arrayBuffer());
return new Promise((resolve, reject) => {
ex.on("entry", (header, stream, next) => {
const chunks: Buffer[] = [];
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
stream.on("end", () => {
result.set(header.name, {
content: Buffer.concat(chunks).toString("utf-8"),
header,
});
next();
});
stream.on("error", reject);
stream.resume();
});
ex.on("finish", () => resolve(result));
ex.on("error", reject);
Readable.from(buffer).pipe(ex);
});
}
describe("createTarBlob", () => {
test("single file tarball", async () => {
const entries: TarEntry[] = [
{ name: "main.js", content: 'console.log("hello");' },
];
const blob = await createTarBlob(entries);
const extracted = await extractTar(blob);
expect(extracted.size).toBe(1);
expect(extracted.has("main.js")).toBe(true);
expect(extracted.get("main.js")!.content).toBe('console.log("hello");');
});
test("multiple output files", async () => {
const entries: TarEntry[] = [
{ name: "main.js", content: 'import "./chunk-abc.js";' },
{ name: "chunk-abc.js", content: "export const x = 42;" },
{ name: "chunk-def.js", content: "export const y = 99;" },
];
const blob = await createTarBlob(entries);
const extracted = await extractTar(blob);
expect(extracted.size).toBe(3);
expect(extracted.get("main.js")!.content).toBe(
'import "./chunk-abc.js";'
);
expect(extracted.get("chunk-abc.js")!.content).toBe(
"export const x = 42;"
);
expect(extracted.get("chunk-def.js")!.content).toBe(
"export const y = 99;"
);
});
test("single file with assets", async () => {
const entries: TarEntry[] = [
{ name: "main.js", content: "const data = require('./data.json');" },
{ name: "data.json", content: '{"key":"value"}' },
];
const blob = await createTarBlob(entries);
const extracted = await extractTar(blob);
expect(extracted.size).toBe(2);
expect(extracted.has("main.js")).toBe(true);
expect(extracted.has("data.json")).toBe(true);
expect(extracted.get("data.json")!.content).toBe('{"key":"value"}');
});
test("produces a valid Blob", async () => {
const entries: TarEntry[] = [
{ name: "main.js", content: "module.exports = {};" },
];
const blob = await createTarBlob(entries);
expect(blob).toBeInstanceOf(Blob);
expect(blob.size).toBeGreaterThan(0);
// Tar blocks are 512-byte aligned
expect(blob.size % 512).toBe(0);
});
test("file naming — entries have exact names given", async () => {
const entries: TarEntry[] = [
{ name: "main.js", content: "entry point" },
{ name: "lib/utils.js", content: "utils" },
];
const blob = await createTarBlob(entries);
const extracted = await extractTar(blob);
// Names should be exactly as provided (no leading slash)
expect(extracted.has("main.js")).toBe(true);
expect(extracted.has("lib/utils.js")).toBe(true);
});
test("handles Buffer content", async () => {
const entries: TarEntry[] = [
{ name: "main.js", content: Buffer.from("buffer content") },
];
const blob = await createTarBlob(entries);
const extracted = await extractTar(blob);
expect(extracted.get("main.js")!.content).toBe("buffer content");
});
test("handles Uint8Array content", async () => {
const content = new TextEncoder().encode("uint8 content");
const entries: TarEntry[] = [
{ name: "main.js", content },
];
const blob = await createTarBlob(entries);
const extracted = await extractTar(blob);
expect(extracted.get("main.js")!.content).toBe("uint8 content");
});
});