| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
- import { tmpdir } from "node:os";
- import { dirname, join } from "node:path";
- import { describe, expect, it } from "vitest";
- import { deleteAsset, importAssets, listAssets, readAssetEntries, refreshEditEngineAssets } from "../src/assets.js";
- import { createEditEngine } from "../src/edit-engine.js";
- const PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
- const OGG_BASE64 = "T2dnUwAAAAAAAAAAAAAAAAAAAAAAAE9nZ1M=";
- const FIXTURE_FILES: Record<string, string> = {
- "content/time.yaml": [
- "id: default",
- "dayLengthTicks: 240",
- "seasons:",
- " - { name: spring, dayCount: 14 }",
- "start: { day: 1, season: spring }",
- "",
- ].join("\n"),
- "content/maps/farm.yaml": [
- "id: farm",
- "width: 2",
- "height: 2",
- "tilesetRef: farm-tiles",
- "spawn: { x: 0, y: 0 }",
- "layers:",
- " - name: ground",
- " data:",
- " - [grass, grass]",
- " - [grass, grass]",
- "",
- ].join("\n"),
- };
- async function tmpDir(): Promise<string> {
- return mkdtemp(join(tmpdir(), "editor-assets-"));
- }
- async function fixtureProject(): Promise<string> {
- const dir = await tmpDir();
- for (const [rel, content] of Object.entries(FIXTURE_FILES)) {
- const target = join(dir, rel);
- await mkdir(dirname(target), { recursive: true });
- await writeFile(target, content, "utf8");
- }
- await mkdir(join(dir, "content", "assets"), { recursive: true });
- await mkdir(join(dir, "sources"), { recursive: true });
- return dir;
- }
- async function writeSource(dir: string, name: string, base64: string): Promise<string> {
- const full = join(dir, "sources", name);
- await mkdir(dirname(full), { recursive: true });
- await writeFile(full, Buffer.from(base64, "base64"));
- return full;
- }
- describe("listAssets (F2)", () => {
- it("scans png + ogg with dataUrl on images only, sorted by id", async () => {
- const dir = await tmpDir();
- try {
- await mkdir(join(dir, "content", "assets"), { recursive: true });
- await writeFile(join(dir, "content", "assets", "b-1.ogg"), Buffer.from(OGG_BASE64, "base64"));
- await writeFile(join(dir, "content", "assets", "a-1.png"), Buffer.from(PNG_BASE64, "base64"));
- const assets = await listAssets(dir);
- expect(assets.map((a) => a.id)).toEqual(["a-1", "b-1"]);
- const image = assets[0];
- expect(image?.kind).toBe("image");
- expect(image?.mime).toBe("image/png");
- expect(image?.dataUrl).toBe(`data:image/png;base64,${PNG_BASE64}`);
- expect(image?.size).toBe(Buffer.from(PNG_BASE64, "base64").length);
- const audio = assets[1];
- expect(audio?.kind).toBe("audio");
- expect(audio?.mime).toBe("audio/ogg");
- expect(audio?.dataUrl).toBeUndefined();
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- it("ignores non-asset files and a missing assets dir", async () => {
- const dir = await tmpDir();
- try {
- await mkdir(join(dir, "content", "assets"), { recursive: true });
- await writeFile(join(dir, "content", "assets", "notes.txt"), "not an asset");
- expect(await listAssets(dir)).toEqual([]);
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- });
- describe("importAssets (F2)", () => {
- it("copies source files into content/assets with a sanitized id", async () => {
- const dir = await tmpDir();
- try {
- const src = await writeSource(dir, "My Icon (1).png", PNG_BASE64);
- const src2 = await writeSource(dir, "click-1.ogg", OGG_BASE64);
- const result = await importAssets(dir, [src, src2]);
- expect(result.errors).toBeUndefined();
- expect(result.imported.map((a) => a.id)).toEqual(["my-icon-1", "click-1"]);
- expect(await readdir(join(dir, "content", "assets"))).toEqual(["click-1.ogg", "my-icon-1.png"]);
- const original = await readFile(src);
- expect(await readFile(join(dir, "content", "assets", "my-icon-1.png"))).toEqual(original);
- expect(result.imported.find((a) => a.id === "my-icon-1")?.dataUrl).toBe(`data:image/png;base64,${PNG_BASE64}`);
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- it("dedupes a colliding basename with a -2 suffix and never overwrites", async () => {
- const dir = await tmpDir();
- try {
- await mkdir(join(dir, "content", "assets"), { recursive: true });
- await writeFile(join(dir, "content", "assets", "tile-1.png"), Buffer.from(OGG_BASE64, "base64"));
- const src = await writeSource(dir, "tile-1.png", PNG_BASE64);
- const srcAgain = await writeSource(dir, "tile-1.png", PNG_BASE64);
- const first = await importAssets(dir, [src]);
- expect(first.imported.map((a) => a.id)).toEqual(["tile-1-2"]);
- expect((await stat(join(dir, "content", "assets", "tile-1.png"))).size).toBe(
- Buffer.from(OGG_BASE64, "base64").length,
- );
- const second = await importAssets(dir, [srcAgain]);
- expect(second.imported.map((a) => a.id)).toEqual(["tile-1-3"]);
- expect(await readdir(join(dir, "content", "assets"))).toEqual(["tile-1-2.png", "tile-1-3.png", "tile-1.png"]);
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- it("rejects unsupported extensions per-file while still importing the valid ones", async () => {
- const dir = await tmpDir();
- try {
- const ok = await writeSource(dir, "grass-1.png", PNG_BASE64);
- const bad = await writeSource(dir, "readme.txt", "hello");
- const result = await importAssets(dir, [bad, ok]);
- expect(result.imported.map((a) => a.id)).toEqual(["grass-1"]);
- expect(result.errors).toHaveLength(1);
- expect(result.errors?.[0]?.path).toBe(bad);
- expect(result.errors?.[0]?.message).toMatch(/unsupported asset type/);
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- });
- describe("deleteAsset (F2)", () => {
- it("removes the file for a known id and errors for an unknown one", async () => {
- const dir = await tmpDir();
- try {
- await mkdir(join(dir, "content", "assets"), { recursive: true });
- await writeFile(join(dir, "content", "assets", "a-1.png"), Buffer.from(PNG_BASE64, "base64"));
- await writeFile(join(dir, "content", "assets", "b-1.ogg"), Buffer.from(OGG_BASE64, "base64"));
- expect(await deleteAsset(dir, "a-1")).toEqual({ ok: true });
- expect(await readdir(join(dir, "content", "assets"))).toEqual(["b-1.ogg"]);
- const missing = await deleteAsset(dir, "a-1");
- expect(missing.ok).toBe(false);
- expect(missing.error).toBe("asset not found: a-1");
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- });
- describe("readAssetEntries + refreshEditEngineAssets (F2)", () => {
- it("reads the same table shape the ContentLoader produces (D31 parity)", async () => {
- const dir = await tmpDir();
- try {
- await mkdir(join(dir, "content", "assets"), { recursive: true });
- await writeFile(join(dir, "content", "assets", "grass-1.png"), Buffer.from(PNG_BASE64, "base64"));
- await writeFile(join(dir, "content", "assets", "click-1.ogg"), Buffer.from(OGG_BASE64, "base64"));
- const entries = await readAssetEntries(dir);
- expect(entries.get("grass-1")).toEqual({ kind: "image", mime: "image/png", data: PNG_BASE64 });
- expect(entries.get("click-1")).toEqual({ kind: "audio", mime: "audio/ogg", data: OGG_BASE64 });
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- it("rebuilds the edit engine's asset table after an import + delete", async () => {
- const dir = await fixtureProject();
- try {
- const engine = createEditEngine(dir);
- await engine.start();
- try {
- expect(engine.assets.size).toBe(0);
- const src = await writeSource(dir, "grass-1.png", PNG_BASE64);
- const imported = await importAssets(dir, [src]);
- expect(imported.imported.map((a) => a.id)).toEqual(["grass-1"]);
- await refreshEditEngineAssets(engine, dir);
- expect(engine.assets.get("grass-1")?.kind).toBe("image");
- await expect(deleteAsset(dir, "grass-1")).resolves.toEqual({ ok: true });
- await refreshEditEngineAssets(engine, dir);
- expect(engine.assets.size).toBe(0);
- } finally {
- await engine.stop();
- }
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- });
|