Merge branch 'worktree-test+suite-overhaul-and-real-fixtures' into chore/consolidate-v2.0.0

# Conflicts:
#	tests/integration/generated/settings-matrix.test.ts
#	tests/integration/platform/api.test.ts
#	tests/integration/platform/concurrent.test.ts
#	tests/integration/platform/factory-multi-input.test.ts
#	tests/integration/security/adversarial-comprehensive.test.ts
#	tests/integration/security/adversarial-coverage-gaps.test.ts
#	tests/integration/security/adversarial-extended.test.ts
#	tests/integration/security/adversarial-final-gaps.test.ts
#	tests/integration/security/adversarial-matrix.test.ts
#	tests/integration/security/adversarial-security.test.ts
#	tests/integration/security/adversarial.test.ts
#	tests/integration/tools/image/color-adjustments.test.ts
This commit is contained in:
SnapOtter
2026-06-21 02:18:53 +08:00
547 changed files with 37191 additions and 4059 deletions
+129
View File
@@ -0,0 +1,129 @@
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import {
generateBackground,
getDominantBackground,
} from "../../../apps/api/src/lib/beautify/backgrounds.js";
describe("generateBackground", () => {
it("generates a solid color background", async () => {
const buf = await generateBackground({
type: "solid",
color: "#ff0000",
width: 200,
height: 100,
});
const meta = await sharp(buf).metadata();
expect(meta.width).toBe(200);
expect(meta.height).toBe(100);
expect(meta.channels).toBe(4);
});
it("generates a linear gradient background", async () => {
const buf = await generateBackground({
type: "linear-gradient",
stops: [
{ color: "#667eea", position: 0 },
{ color: "#764ba2", position: 100 },
],
angle: 135,
width: 400,
height: 300,
});
const meta = await sharp(buf).metadata();
expect(meta.width).toBe(400);
expect(meta.height).toBe(300);
});
it("generates a radial gradient background", async () => {
const buf = await generateBackground({
type: "radial-gradient",
stops: [
{ color: "#667eea", position: 0 },
{ color: "#764ba2", position: 100 },
],
width: 400,
height: 300,
});
const meta = await sharp(buf).metadata();
expect(meta.width).toBe(400);
expect(meta.height).toBe(300);
});
it("generates a transparent background", async () => {
const buf = await generateBackground({
type: "transparent",
width: 200,
height: 100,
});
const meta = await sharp(buf).metadata();
expect(meta.width).toBe(200);
expect(meta.height).toBe(100);
expect(meta.hasAlpha).toBe(true);
});
it("generates an image background", async () => {
const red = await sharp({
create: {
width: 800,
height: 600,
channels: 4,
background: { r: 255, g: 0, b: 0, alpha: 1 },
},
})
.png()
.toBuffer();
const buf = await generateBackground({
type: "image",
imageBuffer: red,
width: 400,
height: 300,
});
const meta = await sharp(buf).metadata();
expect(meta.width).toBe(400);
expect(meta.height).toBe(300);
});
it("generates multi-stop linear gradient", async () => {
const buf = await generateBackground({
type: "linear-gradient",
stops: [
{ color: "#0f0c29", position: 0 },
{ color: "#302b63", position: 50 },
{ color: "#24243e", position: 100 },
],
angle: 135,
width: 400,
height: 300,
});
const meta = await sharp(buf).metadata();
expect(meta.width).toBe(400);
expect(meta.height).toBe(300);
});
});
describe("getDominantBackground", () => {
it("returns solid color for solid type", () => {
const result = getDominantBackground({ type: "solid", color: "#ff0000" });
expect(result).toEqual({ r: 255, g: 0, b: 0, alpha: 1 });
});
it("returns last gradient stop for gradient type", () => {
const result = getDominantBackground({
type: "linear-gradient",
stops: [
{ color: "#667eea", position: 0 },
{ color: "#764ba2", position: 100 },
],
});
expect(result.r).toBe(118);
expect(result.g).toBe(75);
expect(result.b).toBe(162);
});
it("returns transparent for transparent type", () => {
const result = getDominantBackground({ type: "transparent" });
expect(result.alpha).toBe(0);
});
});
+118
View File
@@ -0,0 +1,118 @@
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { renderFrame } from "../../../apps/api/src/lib/beautify/frames.js";
describe("renderFrame", () => {
const makeImage = (w: number, h: number) =>
sharp({
create: {
width: w,
height: h,
channels: 4,
background: { r: 200, g: 200, b: 200, alpha: 1 },
},
})
.png()
.toBuffer();
it("returns original image for frame 'none'", async () => {
const img = await makeImage(400, 300);
const result = await renderFrame(img, "none");
const meta = await sharp(result).metadata();
expect(meta.width).toBe(400);
expect(meta.height).toBe(300);
});
it("renders macOS light frame with title bar above image", async () => {
const img = await makeImage(400, 300);
const result = await renderFrame(img, "macos-light", "My App");
const meta = await sharp(result).metadata();
expect(meta.width).toBe(400);
expect(meta.height!).toBeGreaterThan(300);
});
it("renders macOS dark frame", async () => {
const img = await makeImage(400, 300);
const result = await renderFrame(img, "macos-dark");
const meta = await sharp(result).metadata();
expect(meta.width).toBe(400);
expect(meta.height!).toBeGreaterThan(300);
});
it("renders Windows light frame", async () => {
const img = await makeImage(400, 300);
const result = await renderFrame(img, "windows-light");
const meta = await sharp(result).metadata();
expect(meta.width).toBe(400);
expect(meta.height!).toBeGreaterThan(300);
});
it("renders Windows dark frame", async () => {
const img = await makeImage(400, 300);
const result = await renderFrame(img, "windows-dark");
const meta = await sharp(result).metadata();
expect(meta.width).toBe(400);
expect(meta.height!).toBeGreaterThan(300);
});
it("renders browser light frame with URL bar", async () => {
const img = await makeImage(400, 300);
const result = await renderFrame(img, "browser-light", "example.com");
const meta = await sharp(result).metadata();
expect(meta.width).toBe(400);
expect(meta.height!).toBeGreaterThan(300);
});
it("renders browser dark frame", async () => {
const img = await makeImage(400, 300);
const result = await renderFrame(img, "browser-dark", "app.test.com");
const meta = await sharp(result).metadata();
expect(meta.width).toBe(400);
expect(meta.height!).toBeGreaterThan(300);
});
it("renders iPhone frame with image composited into screen area", async () => {
const img = await makeImage(400, 800);
const result = await renderFrame(img, "iphone");
const meta = await sharp(result).metadata();
expect(meta.width!).toBeGreaterThan(400);
expect(meta.height!).toBeGreaterThan(800);
});
it("renders MacBook frame", async () => {
const img = await makeImage(800, 500);
const result = await renderFrame(img, "macbook");
const meta = await sharp(result).metadata();
expect(meta.width!).toBeGreaterThan(800);
});
it("renders iPad frame", async () => {
const img = await makeImage(400, 600);
const result = await renderFrame(img, "ipad");
const meta = await sharp(result).metadata();
expect(meta.width!).toBeGreaterThan(400);
});
it("renders iPhone dark frame", async () => {
const img = await makeImage(400, 800);
const result = await renderFrame(img, "iphone-dark");
const meta = await sharp(result).metadata();
expect(meta.width!).toBeGreaterThan(400);
});
it("handles wide images", async () => {
const img = await makeImage(1920, 1080);
const result = await renderFrame(img, "macos-light", "Wide App");
const meta = await sharp(result).metadata();
expect(meta.width).toBe(1920);
expect(meta.height!).toBeGreaterThan(1080);
});
it("handles narrow images", async () => {
const img = await makeImage(100, 200);
const result = await renderFrame(img, "browser-light", "narrow.app");
const meta = await sharp(result).metadata();
expect(meta.width).toBe(100);
expect(meta.height!).toBeGreaterThan(200);
});
});
+87
View File
@@ -0,0 +1,87 @@
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { applyShadow } from "../../../apps/api/src/lib/beautify/shadow.js";
describe("applyShadow", () => {
const makeImage = (w: number, h: number) =>
sharp({
create: {
width: w,
height: h,
channels: 4,
background: { r: 100, g: 100, b: 255, alpha: 1 },
},
})
.png()
.toBuffer();
it("returns larger buffer with shadow applied", async () => {
const img = await makeImage(200, 150);
const result = await applyShadow(img, {
blur: 20,
offsetX: 0,
offsetY: 10,
color: "#000000",
opacity: 30,
});
const meta = await sharp(result.buffer).metadata();
expect(meta.width).toBeGreaterThan(200);
expect(meta.height).toBeGreaterThan(150);
expect(result.padLeft).toBeGreaterThanOrEqual(0);
expect(result.padTop).toBeGreaterThanOrEqual(0);
});
it("returns image position within shadow canvas", async () => {
const img = await makeImage(200, 150);
const result = await applyShadow(img, {
blur: 20,
offsetX: 5,
offsetY: 10,
color: "#000000",
opacity: 50,
});
expect(result.imgX).toBeDefined();
expect(result.imgY).toBeDefined();
});
it("handles zero-blur shadow gracefully", async () => {
const img = await makeImage(100, 100);
const result = await applyShadow(img, {
blur: 0,
offsetX: 0,
offsetY: 0,
color: "#000000",
opacity: 0,
});
const meta = await sharp(result.buffer).metadata();
expect(meta.width).toBeGreaterThanOrEqual(100);
});
it("handles negative offsets", async () => {
const img = await makeImage(200, 150);
const result = await applyShadow(img, {
blur: 15,
offsetX: -10,
offsetY: -5,
color: "#ff0000",
opacity: 40,
});
const meta = await sharp(result.buffer).metadata();
expect(meta.width).toBeGreaterThan(200);
expect(meta.height).toBeGreaterThan(150);
});
it("produces RGBA output with alpha channel", async () => {
const img = await makeImage(100, 100);
const result = await applyShadow(img, {
blur: 10,
offsetX: 0,
offsetY: 5,
color: "#000000",
opacity: 30,
});
const meta = await sharp(result.buffer).metadata();
expect(meta.hasAlpha).toBe(true);
expect(meta.channels).toBe(4);
});
});
-3
View File
@@ -1,4 +1,3 @@
import { join } from "node:path";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import {
@@ -10,8 +9,6 @@ import {
createGradientBackground,
} from "../../../apps/api/src/lib/bg-effects.js";
const _FIXTURES = join(__dirname, "../../fixtures");
async function createTestImage(
width: number,
height: number,
+65
View File
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { friendlyError } from "../../../apps/api/src/lib/errors.js";
const GENERIC = "Processing failed. The file may be in an unsupported or corrupted format.";
describe("friendlyError", () => {
it("collapses raw ffmpeg stderr dumps to a safe sentence", () => {
const dump =
"ffmpeg exited 234: Input #0, gif ... Pixel format 'gbrap' is not widely supported. Conversion failed!";
expect(friendlyError(dump)).toBe(GENERIC);
});
it("collapses raw ffprobe stderr dumps", () => {
expect(friendlyError("ffprobe exited 1: moov atom not found")).toBe(GENERIC);
});
it("collapses python tracebacks", () => {
expect(friendlyError("Traceback (most recent call last):\n File x\nValueError: boom")).toBe(
GENERIC,
);
});
it("collapses very long (>280 char) messages", () => {
expect(friendlyError("x".repeat(400))).toBe(GENERIC);
});
it("collapses multi-line dumps (>3 lines)", () => {
expect(friendlyError("l1\nl2\nl3\nl4\nl5")).toBe(GENERIC);
});
it("preserves intentional, user-facing validation messages", () => {
for (const msg of [
"This video has no audio track to normalize",
"Reverse is limited to clips up to 5 minutes",
"Crop rectangle 9999x9999+0+0 exceeds video size 640x360",
"No subtitle track found in this video",
]) {
expect(friendlyError(msg)).toBe(msg);
}
});
it("does NOT collapse clean messages that merely contain tool-ish words (false-positive guard)", () => {
// The old regex matched "conversion failed" / "pixel format" and would have
// wrongly collapsed these legitimate messages.
expect(friendlyError("SVG conversion failed")).toBe("SVG conversion failed");
expect(friendlyError("PDF conversion failed")).toBe("PDF conversion failed");
expect(friendlyError("Unsupported pixel format in source")).toBe(
"Unsupported pixel format in source",
);
});
it("scrubs internal filesystem paths", () => {
expect(friendlyError("decode failed at /data/ai/models/whisper")).toBe(
"decode failed at [internal]",
);
expect(friendlyError("wrote /tmp/workspace/out.mp4")).toBe("wrote [internal]");
});
it("is idempotent (safe to apply at every error surface)", () => {
const dump = "ffmpeg exited 1: boom";
expect(friendlyError(friendlyError(dump))).toBe(friendlyError(dump));
const ok = "Region exceeds image bounds";
expect(friendlyError(friendlyError(ok))).toBe(ok);
});
});
+10 -13
View File
@@ -1,5 +1,3 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import type { EditMetadataSettings } from "../../../apps/api/src/lib/exiftool.js";
@@ -8,8 +6,7 @@ import {
inspectMetadata,
writeMetadata,
} from "../../../apps/api/src/lib/exiftool.js";
const FIXTURES = join(import.meta.dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
describe("buildTagArgs", () => {
it("returns empty array for empty settings", () => {
@@ -289,33 +286,33 @@ describe("buildTagArgs", () => {
describe("inspectMetadata", () => {
it("returns correct filename and fileSize for JPEG with EXIF", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const result = await inspectMetadata(buf, "test-with-exif.jpg");
expect(result.filename).toBe("test-with-exif.jpg");
expect(result.fileSize).toBe(buf.length);
});
it("returns non-null exif object for JPEG with EXIF data", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const result = await inspectMetadata(buf, "test-with-exif.jpg");
expect(result.exif).not.toBeNull();
expect(typeof result.exif).toBe("object");
});
it("returns keywords array (may be empty)", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const result = await inspectMetadata(buf, "test-with-exif.jpg");
expect(Array.isArray(result.keywords)).toBe(true);
});
it("returns null for GPS when no GPS data present", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const result = await inspectMetadata(buf, "test-with-exif.jpg");
expect(result.gps).toBeNull();
});
it("works with PNG which has no EXIF -- returns null for exif, iptc, xmp, gps", async () => {
const buf = readFileSync(join(FIXTURES, "test-1x1.png"));
const buf = readFixture(fixtures.image.edge.px1);
const result = await inspectMetadata(buf, "test-1x1.png");
expect(result.filename).toBe("test-1x1.png");
expect(result.fileSize).toBe(buf.length);
@@ -328,13 +325,13 @@ describe("inspectMetadata", () => {
describe("writeMetadata", () => {
it("empty tags array returns buffer unchanged", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const result = await writeMetadata(buf, "test-with-exif.jpg", []);
expect(Buffer.compare(result, buf)).toBe(0);
});
it("writing -Artist=TestArtist then inspecting shows the artist", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const written = await writeMetadata(buf, "test-with-exif.jpg", ["-Artist=TestArtist"]);
const result = await inspectMetadata(written, "test-with-exif.jpg");
expect(result.exif).not.toBeNull();
@@ -342,7 +339,7 @@ describe("writeMetadata", () => {
});
it("writing multiple tags works", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const written = await writeMetadata(buf, "test-with-exif.jpg", [
"-Artist=MultiTest",
"-Copyright=2024 Test Corp",
@@ -353,7 +350,7 @@ describe("writeMetadata", () => {
});
it("returns a valid image buffer that Sharp can read", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const written = await writeMetadata(buf, "test-with-exif.jpg", ["-Software=SnapOtter"]);
const meta = await sharp(written).metadata();
expect(meta.format).toBe("jpeg");
+21 -24
View File
@@ -1,11 +1,8 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { decodeToSharpCompat, needsCliDecode } from "../../../apps/api/src/lib/format-decoders.js";
import { encodeQoi } from "../../../apps/api/src/lib/format-encoders.js";
const FIXTURES = join(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
function isImageMagickError(err: unknown): boolean {
if (!(err instanceof Error)) return false;
@@ -176,7 +173,7 @@ describe("decodeToSharpCompat", () => {
it("decodes BMP to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.bmp"));
const input = readFixture(fixtures.image.formats("bmp"));
const result = await decodeToSharpCompat(input, "bmp");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -188,7 +185,7 @@ describe("decodeToSharpCompat", () => {
it("decodes ICO to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.ico"));
const input = readFixture(fixtures.image.formats("ico"));
const result = await decodeToSharpCompat(input, "ico");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -200,7 +197,7 @@ describe("decodeToSharpCompat", () => {
it("decodes TGA to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.tga"));
const input = readFixture(fixtures.image.formats("tga"));
const result = await decodeToSharpCompat(input, "tga");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -212,7 +209,7 @@ describe("decodeToSharpCompat", () => {
it("decodes PSD to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.psd"));
const input = readFixture(fixtures.image.formats("psd"));
const result = await decodeToSharpCompat(input, "psd");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -224,7 +221,7 @@ describe("decodeToSharpCompat", () => {
it("decodes EXR to valid PNG (requires EXR delegate)", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.exr"));
const input = readFixture(fixtures.image.formats("exr"));
const result = await decodeToSharpCompat(input, "exr");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -238,7 +235,7 @@ describe("decodeToSharpCompat", () => {
it("decodes HDR to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.hdr"));
const input = readFixture(fixtures.image.formats("hdr"));
const result = await decodeToSharpCompat(input, "hdr");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -250,7 +247,7 @@ describe("decodeToSharpCompat", () => {
it("decodes JXL to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.jxl"));
const input = readFixture(fixtures.image.formats("jxl"));
const result = await decodeToSharpCompat(input, "jxl");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -262,7 +259,7 @@ describe("decodeToSharpCompat", () => {
it("decodes JP2 to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.jp2"));
const input = readFixture(fixtures.image.formats("jp2"));
const result = await decodeToSharpCompat(input, "jp2");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -274,7 +271,7 @@ describe("decodeToSharpCompat", () => {
it("decodes DDS to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.dds"));
const input = readFixture(fixtures.image.formats("dds"));
const result = await decodeToSharpCompat(input, "dds");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -286,7 +283,7 @@ describe("decodeToSharpCompat", () => {
it("decodes CUR using ICO decoder to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.cur"));
const input = readFixture(fixtures.image.formats("cur"));
const result = await decodeToSharpCompat(input, "cur");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -298,7 +295,7 @@ describe("decodeToSharpCompat", () => {
it("decodes DPX to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.dpx"));
const input = readFixture(fixtures.image.formats("dpx"));
const result = await decodeToSharpCompat(input, "dpx");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -310,7 +307,7 @@ describe("decodeToSharpCompat", () => {
it("decodes FITS to valid PNG (requires FITS delegate)", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.fits"));
const input = readFixture(fixtures.image.formats("fits"));
const result = await decodeToSharpCompat(input, "fits");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -324,7 +321,7 @@ describe("decodeToSharpCompat", () => {
it("decodes PPM to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.ppm"));
const input = readFixture(fixtures.image.formats("ppm"));
const result = await decodeToSharpCompat(input, "ppm");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -336,7 +333,7 @@ describe("decodeToSharpCompat", () => {
it("decodes PGM to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.pgm"));
const input = readFixture(fixtures.image.formats("pgm"));
const result = await decodeToSharpCompat(input, "pgm");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -348,7 +345,7 @@ describe("decodeToSharpCompat", () => {
it("decodes PBM to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.pbm"));
const input = readFixture(fixtures.image.formats("pbm"));
const result = await decodeToSharpCompat(input, "pbm");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -365,7 +362,7 @@ describe("decodeToSharpCompat - individual decoder verification", () => {
for (const fmt of formats) {
it(`${fmt}: output has non-zero dimensions`, async () => {
try {
const input = await readFile(join(FIXTURES, `formats/sample.${fmt}`));
const input = readFixture(fixtures.image.formats(fmt));
const result = await decodeToSharpCompat(input, fmt);
const { width, height } = await assertValidImage(result);
expect(width).toBeGreaterThan(0);
@@ -382,7 +379,7 @@ describe("decodeToSharpCompat - individual decoder verification", () => {
for (const fmt of delegateFormats) {
it(`${fmt}: output has non-zero dimensions (requires delegate)`, async () => {
try {
const input = await readFile(join(FIXTURES, `formats/sample.${fmt}`));
const input = readFixture(fixtures.image.formats(fmt));
const result = await decodeToSharpCompat(input, fmt);
const { width, height } = await assertValidImage(result);
expect(width).toBeGreaterThan(0);
@@ -399,14 +396,14 @@ describe("decodeToSharpCompat - individual decoder verification", () => {
describe("decodeToSharpCompat - QOI decoder", () => {
it("decodes QOI fixture to valid PNG", async () => {
const input = await readFile(join(FIXTURES, "formats/sample.qoi"));
const input = readFixture(fixtures.image.formats("qoi"));
const result = await decodeToSharpCompat(input, "qoi");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
});
it("round-trips PNG -> QOI -> PNG", async () => {
const png = await readFile(join(FIXTURES, "formats/sample.png"));
const png = readFixture(fixtures.image.formats("png"));
const qoi = await encodeQoi(png);
const decoded = await decodeToSharpCompat(Buffer.from(qoi), "qoi");
expect(isPng(decoded)).toBe(true);
@@ -417,7 +414,7 @@ describe("decodeToSharpCompat - QOI decoder", () => {
});
it("decoded QOI produces a buffer sharp can process further", async () => {
const input = await readFile(join(FIXTURES, "formats/sample.qoi"));
const input = readFixture(fixtures.image.formats("qoi"));
const decoded = await decodeToSharpCompat(input, "qoi");
const resized = await sharp(decoded).resize(10, 10).png().toBuffer();
expect(resized.length).toBeGreaterThan(0);
+3 -5
View File
@@ -1,5 +1,4 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import {
@@ -8,8 +7,7 @@ import {
encodeJp2,
encodeQoi,
} from "../../../apps/api/src/lib/format-encoders.js";
const FIXTURES = join(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
async function createTestPng(width = 50, height = 50): Promise<Buffer> {
return sharp({
@@ -52,7 +50,7 @@ describe("encodeQoi", () => {
});
it("encodes fixture image to QOI", async () => {
const input = await readFile(join(FIXTURES, "test-200x150.png"));
const input = readFixture(fixtures.image.base.png200);
const result = await encodeQoi(input);
expect(result.length).toBeGreaterThan(100);
});
@@ -170,7 +168,7 @@ describe("encodeJp2", () => {
});
it("produces valid output at different quality levels", async () => {
const input = await readFile(join(FIXTURES, "test-200x150.png"));
const input = readFixture(fixtures.image.base.png200);
try {
const low = await encodeJp2(input, 10);
const high = await encodeJp2(input, 90);
+9 -11
View File
@@ -1,10 +1,8 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { decodeHeic, ensureSharpCompat } from "../../../apps/api/src/lib/heic-converter.js";
const FIXTURES = join(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
function isPng(buf: Buffer): boolean {
return buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47;
@@ -20,7 +18,7 @@ function makeHeicHeader(brand: string): Buffer {
describe("decodeHeic", () => {
it("decodes sample.heic to a valid PNG buffer", async () => {
const heicBuf = await readFile(join(FIXTURES, "formats/sample.heic"));
const heicBuf = readFixture(fixtures.image.formats("heic"));
const result = await decodeHeic(heicBuf);
expect(isPng(result)).toBe(true);
const meta = await sharp(result).metadata();
@@ -29,7 +27,7 @@ describe("decodeHeic", () => {
});
it("decodes sample.heif to a valid PNG buffer", { timeout: 60_000 }, async () => {
const heifBuf = await readFile(join(FIXTURES, "formats/sample.heif"));
const heifBuf = readFixture(fixtures.image.formats("heif"));
const result = await decodeHeic(heifBuf);
expect(isPng(result)).toBe(true);
const meta = await sharp(result).metadata();
@@ -38,7 +36,7 @@ describe("decodeHeic", () => {
});
it("preserves image dimensions after decoding", async () => {
const heicBuf = await readFile(join(FIXTURES, "formats/sample.heic"));
const heicBuf = readFixture(fixtures.image.formats("heic"));
const result = await decodeHeic(heicBuf);
const meta = await sharp(result).metadata();
expect(meta.width).toBeGreaterThan(0);
@@ -56,7 +54,7 @@ describe("decodeHeic", () => {
});
it("cleans up temp files after success", { timeout: 60_000 }, async () => {
const heicBuf = await readFile(join(FIXTURES, "formats/sample.heic"));
const heicBuf = readFixture(fixtures.image.formats("heic"));
const { tmpdir } = await import("node:os");
const { readdirSync } = await import("node:fs");
const beforeSet = new Set(
@@ -75,25 +73,25 @@ describe("decodeHeic", () => {
describe("ensureSharpCompat", () => {
it("decodes a HEIC buffer to PNG", async () => {
const heicBuf = await readFile(join(FIXTURES, "formats/sample.heic"));
const heicBuf = readFixture(fixtures.image.formats("heic"));
const result = await ensureSharpCompat(heicBuf);
expect(isPng(result)).toBe(true);
});
it("passes through a PNG buffer unchanged", async () => {
const pngBuf = await readFile(join(FIXTURES, "formats/sample.png"));
const pngBuf = readFixture(fixtures.image.formats("png"));
const result = await ensureSharpCompat(pngBuf);
expect(result).toBe(pngBuf);
});
it("passes through a JPEG buffer unchanged", async () => {
const jpgBuf = await readFile(join(FIXTURES, "formats/sample.jpg"));
const jpgBuf = readFixture(fixtures.image.formats("jpg"));
const result = await ensureSharpCompat(jpgBuf);
expect(result).toBe(jpgBuf);
});
it("passes through a WebP buffer unchanged", async () => {
const webpBuf = await readFile(join(FIXTURES, "formats/sample.webp"));
const webpBuf = readFixture(fixtures.image.formats("webp"));
const result = await ensureSharpCompat(webpBuf);
expect(result).toBe(webpBuf);
});
+188
View File
@@ -0,0 +1,188 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const TEMPLATES_DIR = join(__dirname, "../../../apps/api/static/meme-templates");
const MANIFEST_PATH = join(TEMPLATES_DIR, "meme-templates.json");
const VALID_CATEGORIES = ["reaction", "comparison", "opinion", "animals", "classic"];
interface TextBox {
id: string;
x: number;
y: number;
width: number;
height: number;
defaultText: string;
}
interface Template {
id: string;
name: string;
aliases: string[];
tags: string[];
category: string;
filename: string;
width: number;
height: number;
popularity: number;
textBoxes: TextBox[];
}
interface Manifest {
version: number;
categories: string[];
templates: Template[];
}
function loadManifest(): Manifest {
const raw = readFileSync(MANIFEST_PATH, "utf-8");
return JSON.parse(raw);
}
describe("meme template manifest validation", () => {
it("manifest file exists and is valid JSON with version 1 and non-empty templates array", () => {
expect(existsSync(MANIFEST_PATH)).toBe(true);
const raw = readFileSync(MANIFEST_PATH, "utf-8");
let manifest: Manifest;
expect(() => {
manifest = JSON.parse(raw);
}).not.toThrow();
manifest = JSON.parse(raw);
expect(manifest.version).toBe(1);
expect(Array.isArray(manifest.templates)).toBe(true);
expect(manifest.templates.length).toBeGreaterThan(0);
});
it("has no duplicate template IDs", () => {
const manifest = loadManifest();
const ids = manifest.templates.map((t) => t.id);
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(ids.length);
// Identify duplicates for a useful error message
const seen = new Set<string>();
const duplicates: string[] = [];
for (const id of ids) {
if (seen.has(id)) {
duplicates.push(id);
}
seen.add(id);
}
expect(duplicates, `Duplicate template IDs: ${duplicates.join(", ")}`).toHaveLength(0);
});
it("every template has required fields with correct types", () => {
const manifest = loadManifest();
for (const template of manifest.templates) {
const label = `template "${template.id || template.name || "unknown"}"`;
// Required string fields
expect(typeof template.id, `${label}: id must be a string`).toBe("string");
expect(template.id.length, `${label}: id must not be empty`).toBeGreaterThan(0);
expect(typeof template.name, `${label}: name must be a string`).toBe("string");
expect(template.name.length, `${label}: name must not be empty`).toBeGreaterThan(0);
// aliases must be an array
expect(Array.isArray(template.aliases), `${label}: aliases must be an array`).toBe(true);
// tags must be an array
expect(Array.isArray(template.tags), `${label}: tags must be an array`).toBe(true);
// category must be valid
expect(
VALID_CATEGORIES.includes(template.category),
`${label}: category "${template.category}" is not one of ${VALID_CATEGORIES.join(", ")}`,
).toBe(true);
// filename
expect(typeof template.filename, `${label}: filename must be a string`).toBe("string");
expect(template.filename.length, `${label}: filename must not be empty`).toBeGreaterThan(0);
// width and height must be positive numbers
expect(typeof template.width, `${label}: width must be a number`).toBe("number");
expect(template.width, `${label}: width must be positive`).toBeGreaterThan(0);
expect(typeof template.height, `${label}: height must be a number`).toBe("number");
expect(template.height, `${label}: height must be positive`).toBeGreaterThan(0);
// popularity must be non-negative
expect(typeof template.popularity, `${label}: popularity must be a number`).toBe("number");
expect(
template.popularity,
`${label}: popularity must be non-negative`,
).toBeGreaterThanOrEqual(0);
// textBoxes must be a non-empty array
expect(Array.isArray(template.textBoxes), `${label}: textBoxes must be an array`).toBe(true);
expect(
template.textBoxes.length,
`${label}: must have at least one textBox`,
).toBeGreaterThanOrEqual(1);
}
});
it("text box coordinates are in valid percentage range (0-100)", () => {
const manifest = loadManifest();
for (const template of manifest.templates) {
for (const box of template.textBoxes) {
const label = `template "${template.id}" textBox "${box.id}"`;
expect(box.x, `${label}: x must be >= 0`).toBeGreaterThanOrEqual(0);
expect(box.x, `${label}: x must be <= 100`).toBeLessThanOrEqual(100);
expect(box.y, `${label}: y must be >= 0`).toBeGreaterThanOrEqual(0);
expect(box.y, `${label}: y must be <= 100`).toBeLessThanOrEqual(100);
expect(box.width, `${label}: width must be >= 0`).toBeGreaterThanOrEqual(0);
expect(box.width, `${label}: width must be <= 100`).toBeLessThanOrEqual(100);
expect(box.height, `${label}: height must be >= 0`).toBeGreaterThanOrEqual(0);
expect(box.height, `${label}: height must be <= 100`).toBeLessThanOrEqual(100);
}
}
});
it("no duplicate text box IDs within a template", () => {
const manifest = loadManifest();
for (const template of manifest.templates) {
const boxIds = template.textBoxes.map((b) => b.id);
const uniqueBoxIds = new Set(boxIds);
expect(uniqueBoxIds.size, `template "${template.id}" has duplicate textBox IDs`).toBe(
boxIds.length,
);
}
});
it("every template has a corresponding full-size image in full/ directory", () => {
const manifest = loadManifest();
const fullDir = join(TEMPLATES_DIR, "full");
for (const template of manifest.templates) {
const imagePath = join(fullDir, template.filename);
expect(
existsSync(imagePath),
`template "${template.id}": missing full image at full/${template.filename}`,
).toBe(true);
}
});
it("every template has a corresponding thumbnail in thumbs/ directory", () => {
const manifest = loadManifest();
const thumbsDir = join(TEMPLATES_DIR, "thumbs");
for (const template of manifest.templates) {
// Thumbnail uses the same base name but with .webp extension
const baseName = template.filename.replace(/\.[^.]+$/, "");
const thumbFilename = `${baseName}.webp`;
const thumbPath = join(thumbsDir, thumbFilename);
expect(
existsSync(thumbPath),
`template "${template.id}": missing thumbnail at thumbs/${thumbFilename}`,
).toBe(true);
}
});
});
+4 -6
View File
@@ -1,12 +1,10 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { resolveOutputFormat } from "../../../apps/api/src/lib/output-format.js";
import { fixtures, readFixture } from "../../fixtures/index.js";
const FIXTURES = join(__dirname, "..", "..", "fixtures");
const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
const PNG = readFileSync(join(FIXTURES, "test-200x150.png"));
const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp"));
const JPG = readFixture(fixtures.image.base.jpg100);
const PNG = readFixture(fixtures.image.base.png200);
const WEBP = readFixture(fixtures.image.base.webp50);
describe("resolveOutputFormat", () => {
it("detects JPEG input and returns jpeg config", async () => {
+64
View File
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { parsePageRange } from "../../../apps/api/src/routes/tools/pdf-to-image.js";
describe("parsePageRange", () => {
it("returns all pages for 'all'", () => {
expect(parsePageRange("all", 5)).toEqual([1, 2, 3, 4, 5]);
});
it("returns all pages for empty string", () => {
expect(parsePageRange("", 3)).toEqual([1, 2, 3]);
});
it("parses a single page", () => {
expect(parsePageRange("2", 5)).toEqual([2]);
});
it("parses a range", () => {
expect(parsePageRange("1-3", 5)).toEqual([1, 2, 3]);
});
it("parses mixed ranges and singles", () => {
expect(parsePageRange("1-3, 5", 5)).toEqual([1, 2, 3, 5]);
});
it("deduplicates overlapping ranges", () => {
expect(parsePageRange("1-3, 2-4", 5)).toEqual([1, 2, 3, 4]);
});
it("sorts output", () => {
expect(parsePageRange("5, 1, 3", 5)).toEqual([1, 3, 5]);
});
it("handles whitespace", () => {
expect(parsePageRange(" 1 - 3 , 5 ", 5)).toEqual([1, 2, 3, 5]);
});
it("throws on page 0", () => {
expect(() => parsePageRange("0", 5)).toThrow();
});
it("throws on negative page", () => {
expect(() => parsePageRange("-1", 5)).toThrow();
});
it("throws on page exceeding total", () => {
expect(() => parsePageRange("6", 5)).toThrow(/out of range/);
});
it("throws on range exceeding total", () => {
expect(() => parsePageRange("3-7", 5)).toThrow(/out of range/);
});
it("throws on reversed range", () => {
expect(() => parsePageRange("5-2", 5)).toThrow();
});
it("throws on non-numeric input", () => {
expect(() => parsePageRange("abc", 5)).toThrow();
});
it("throws on empty range segment", () => {
expect(() => parsePageRange("1,,3", 5)).toThrow();
});
});
+20 -22
View File
@@ -4,8 +4,7 @@ import { mkdir, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const FIXTURES = join(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
// ---------------------------------------------------------------------------
// 1. File validation
@@ -32,7 +31,7 @@ describe("validateImageBuffer", () => {
// -- Valid formats --------------------------------------------------------
it("accepts a valid PNG file", async () => {
const buf = await readFile(join(FIXTURES, "test-200x150.png"));
const buf = readFixture(fixtures.image.base.png200);
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
@@ -43,7 +42,7 @@ describe("validateImageBuffer", () => {
});
it("accepts a valid JPEG file", async () => {
const buf = await readFile(join(FIXTURES, "test-100x100.jpg"));
const buf = readFixture(fixtures.image.base.jpg100);
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
@@ -54,7 +53,7 @@ describe("validateImageBuffer", () => {
});
it("accepts a valid WebP file", async () => {
const buf = await readFile(join(FIXTURES, "test-50x50.webp"));
const buf = readFixture(fixtures.image.base.webp50);
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
@@ -65,7 +64,7 @@ describe("validateImageBuffer", () => {
});
it("accepts a tiny 1x1 PNG file", async () => {
const buf = await readFile(join(FIXTURES, "test-1x1.png"));
const buf = readFixture(fixtures.image.edge.px1);
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
@@ -130,7 +129,7 @@ describe("validateImageBuffer", () => {
});
it("accepts a HEIC file with correct magic bytes", async () => {
const heicBuf = await readFile(join(FIXTURES, "test-200x150.heic"));
const heicBuf = readFixture(fixtures.image.base.heic200);
const result = await validateImageBuffer(heicBuf);
expect(result.valid).toBe(true);
if (result.valid) {
@@ -323,7 +322,7 @@ describe("validateImageBuffer", () => {
origMod.env.MAX_MEGAPIXELS = 0.0001; // 100 pixels -- 200x150 = 30000px >> 100
try {
const buf = await readFile(join(FIXTURES, "test-200x150.png"));
const buf = readFixture(fixtures.image.base.png200);
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(false);
if (!result.valid) {
@@ -572,73 +571,72 @@ describe("validateImageBuffer", () => {
describe("validates exotic formats", () => {
const { readFileSync } = require("node:fs");
const FORMATS_DIR = join(FIXTURES, "formats");
it("accepts PBM file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.pbm"));
const buf = readFixture(fixtures.image.formats("pbm"));
const result = await validateImageBuffer(buf, "sample.pbm");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("pbm");
});
it("accepts PGM file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.pgm"));
const buf = readFixture(fixtures.image.formats("pgm"));
const result = await validateImageBuffer(buf, "sample.pgm");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("pgm");
});
it("accepts PPM file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.ppm"));
const buf = readFixture(fixtures.image.formats("ppm"));
const result = await validateImageBuffer(buf, "sample.ppm");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("ppm");
});
it("accepts DDS file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.dds"));
const buf = readFixture(fixtures.image.formats("dds"));
const result = await validateImageBuffer(buf, "sample.dds");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("dds");
});
it("accepts DPX file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.dpx"));
const buf = readFixture(fixtures.image.formats("dpx"));
const result = await validateImageBuffer(buf, "sample.dpx");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("dpx");
});
it("accepts FITS file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.fits"));
const buf = readFixture(fixtures.image.formats("fits"));
const result = await validateImageBuffer(buf, "sample.fits");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("fits");
});
it("accepts JP2 file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.jp2"));
const buf = readFixture(fixtures.image.formats("jp2"));
const result = await validateImageBuffer(buf, "sample.jp2");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("jp2");
});
it("accepts QOI file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.qoi"));
const buf = readFixture(fixtures.image.formats("qoi"));
const result = await validateImageBuffer(buf, "sample.qoi");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("qoi");
});
it("accepts SVGZ file (detected as svg)", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.svgz"));
const buf = readFixture(fixtures.image.formats("svgz"));
const result = await validateImageBuffer(buf, "sample.svgz");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("svg");
});
it("accepts EPS file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.eps"));
const buf = readFixture(fixtures.image.formats("eps"));
const result = await validateImageBuffer(buf, "sample.eps");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("eps");
@@ -659,21 +657,21 @@ describe("validateImageBuffer", () => {
});
it("accepts HEIF file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.heif"));
const buf = readFixture(fixtures.image.formats("heif"));
const result = await validateImageBuffer(buf, "sample.heif");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("heif");
});
it("accepts APNG file (detected as png)", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.apng"));
const buf = readFixture(fixtures.image.formats("apng"));
const result = await validateImageBuffer(buf, "sample.apng");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("png");
});
it("accepts DNG file (detected as raw)", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.dng"));
const buf = readFixture(fixtures.image.formats("dng"));
const result = await validateImageBuffer(buf, "sample.dng");
expect(result.valid).toBe(true);
if (result.valid) {
+210
View File
@@ -0,0 +1,210 @@
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
/**
* Unit tests for the venv upgrade-stamp mechanism in docker/entrypoint.sh.
*
* The entrypoint compares /opt/venv/.venv-version (baked into the image at
* build time) against /data/ai/venv/.venv-version (persisted on the volume).
* When missing or mismatched, the venv is nuked and re-copied so upgraded
* base packages take effect immediately.
*
* These tests exercise the bootstrap logic in isolation using temp dirs.
*/
let root: string;
let optVenv: string;
let dataAi: string;
let aiVenv: string;
let aiVenvTmp: string;
let installedJson: string;
// Self-contained shell script mirroring the entrypoint bootstrap block.
// Uses env vars for paths so we can point at temp directories.
const BOOTSTRAP_SCRIPT = `
#!/bin/sh
set -e
AI_VENV="$TEST_AI_VENV"
AI_VENV_TMP="$TEST_AI_VENV_TMP"
OPT_VENV="$TEST_OPT_VENV"
if [ -d "$AI_VENV_TMP" ]; then
echo "CLEANUP_INTERRUPTED"
rm -rf "$AI_VENV_TMP"
fi
if [ -d "$OPT_VENV" ]; then
NEED_BOOTSTRAP=false
if [ ! -d "$AI_VENV" ]; then
NEED_BOOTSTRAP=true
echo "FIRST_RUN"
elif [ -f "$OPT_VENV/.venv-version" ]; then
IMAGE_STAMP=$(cat "$OPT_VENV/.venv-version")
CURRENT_STAMP=""
if [ -f "$AI_VENV/.venv-version" ]; then
CURRENT_STAMP=$(cat "$AI_VENV/.venv-version")
fi
if [ "$CURRENT_STAMP" != "$IMAGE_STAMP" ]; then
NEED_BOOTSTRAP=true
echo "STAMP_MISMATCH"
fi
fi
if [ "$NEED_BOOTSTRAP" = true ]; then
rm -rf "$AI_VENV"
cp -r "$OPT_VENV" "$AI_VENV_TMP"
mv "$AI_VENV_TMP" "$AI_VENV"
if [ -f "$TEST_INSTALLED_JSON" ]; then
echo '{"bundles":{}}' > "$TEST_INSTALLED_JSON"
echo "BUNDLES_RESET"
fi
echo "VENV_READY"
else
echo "SKIP"
fi
else
echo "NO_OPT_VENV"
fi
`;
function runBootstrap(): string {
return execFileSync("/bin/sh", ["-c", BOOTSTRAP_SCRIPT], {
env: {
TEST_AI_VENV: aiVenv,
TEST_AI_VENV_TMP: aiVenvTmp,
TEST_OPT_VENV: optVenv,
TEST_INSTALLED_JSON: installedJson,
PATH: process.env.PATH,
},
encoding: "utf-8",
}).trim();
}
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "venv-stamp-"));
optVenv = join(root, "opt-venv");
dataAi = join(root, "data-ai");
aiVenv = join(dataAi, "venv");
aiVenvTmp = join(dataAi, "venv.bootstrapping");
installedJson = join(dataAi, "installed.json");
// Simulate /opt/venv with a stamp and a marker file
mkdirSync(optVenv, { recursive: true });
writeFileSync(join(optVenv, ".venv-version"), "abc123\n");
writeFileSync(join(optVenv, "marker.txt"), "base-package-content");
mkdirSync(dataAi, { recursive: true });
});
afterEach(() => {
rmSync(root, { recursive: true, force: true });
});
describe("venv upgrade stamp", () => {
it("bootstraps on first run when no venv exists", () => {
const output = runBootstrap();
expect(output).toContain("FIRST_RUN");
expect(output).toContain("VENV_READY");
expect(existsSync(join(aiVenv, ".venv-version"))).toBe(true);
expect(readFileSync(join(aiVenv, "marker.txt"), "utf-8")).toBe("base-package-content");
});
it("skips when stamps match", () => {
// Simulate an existing venv with matching stamp
mkdirSync(aiVenv, { recursive: true });
writeFileSync(join(aiVenv, ".venv-version"), "abc123\n");
const output = runBootstrap();
expect(output).toBe("SKIP");
});
it("refreshes when stamps differ (upgrade)", () => {
// Simulate stale venv with old stamp
mkdirSync(aiVenv, { recursive: true });
writeFileSync(join(aiVenv, ".venv-version"), "old-hash\n");
writeFileSync(join(aiVenv, "stale-file.txt"), "should-be-removed");
const output = runBootstrap();
expect(output).toContain("STAMP_MISMATCH");
expect(output).toContain("VENV_READY");
// Old content replaced with fresh copy
expect(existsSync(join(aiVenv, "stale-file.txt"))).toBe(false);
expect(readFileSync(join(aiVenv, "marker.txt"), "utf-8")).toBe("base-package-content");
expect(readFileSync(join(aiVenv, ".venv-version"), "utf-8")).toBe("abc123\n");
});
it("refreshes when venv has no stamp (pre-stamp image upgrade)", () => {
// Simulate old venv without any stamp file
mkdirSync(aiVenv, { recursive: true });
writeFileSync(join(aiVenv, "old-pkg.txt"), "legacy");
const output = runBootstrap();
expect(output).toContain("STAMP_MISMATCH");
expect(output).toContain("VENV_READY");
expect(existsSync(join(aiVenv, "old-pkg.txt"))).toBe(false);
});
it("resets installed.json when refreshing", () => {
// Simulate existing venv + installed bundles
mkdirSync(aiVenv, { recursive: true });
writeFileSync(join(aiVenv, ".venv-version"), "old-hash\n");
writeFileSync(
installedJson,
JSON.stringify({
bundles: {
"background-removal": {
version: "1.0.0",
installedAt: "2025-01-01T00:00:00Z",
},
},
}),
);
const output = runBootstrap();
expect(output).toContain("BUNDLES_RESET");
const data = JSON.parse(readFileSync(installedJson, "utf-8"));
expect(data).toEqual({ bundles: {} });
});
it("does not reset installed.json when no bundles were installed", () => {
// No installed.json exists
mkdirSync(aiVenv, { recursive: true });
writeFileSync(join(aiVenv, ".venv-version"), "old-hash\n");
const output = runBootstrap();
expect(output).toContain("VENV_READY");
expect(output).not.toContain("BUNDLES_RESET");
});
it("does nothing when /opt/venv has no stamp (old image)", () => {
// Remove the stamp from the image venv
rmSync(join(optVenv, ".venv-version"));
mkdirSync(aiVenv, { recursive: true });
writeFileSync(join(aiVenv, "existing.txt"), "keep");
const output = runBootstrap();
expect(output).toBe("SKIP");
// Existing venv untouched
expect(existsSync(join(aiVenv, "existing.txt"))).toBe(true);
});
it("cleans up interrupted bootstrap from previous start", () => {
mkdirSync(aiVenvTmp, { recursive: true });
writeFileSync(join(aiVenvTmp, "partial.txt"), "incomplete");
const output = runBootstrap();
expect(output).toContain("CLEANUP_INTERRUPTED");
expect(existsSync(aiVenvTmp)).toBe(false);
});
it("does nothing when /opt/venv does not exist", () => {
rmSync(optVenv, { recursive: true });
const output = runBootstrap();
expect(output).toBe("NO_OPT_VENV");
});
});