| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
- import { tmpdir } from "node:os";
- import { join } from "node:path";
- import { describe, expect, it } from "vitest";
- import { inspectProject, listContent, openProject, readYaml, resolveWithin, stats, writeYaml } from "../src/project.js";
- import { newProject } from "../src/template.js";
- async function tmpDir(): Promise<string> {
- return mkdtemp(join(tmpdir(), "editor-project-"));
- }
- describe("openProject", () => {
- it("accepts a folder with content/ and returns project info", async () => {
- const dir = await tmpDir();
- try {
- await mkdir(join(dir, "content"));
- const project = await openProject(dir);
- expect(project.root).toBe(dir);
- expect(project.name).toBe(dir.split("/").pop());
- expect(project.hasContent).toBe(true);
- expect(project.hasMods).toBe(false);
- expect(project.projectYaml).toBeNull();
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- it("rejects a folder without content/", async () => {
- const dir = await tmpDir();
- try {
- await expect(openProject(dir)).rejects.toThrow(/missing content/);
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- it("uses project.yaml name when present", async () => {
- const dir = await tmpDir();
- try {
- await mkdir(join(dir, "content"));
- await writeFile(join(dir, "project.yaml"), "name: My Game\n", "utf8");
- const project = await openProject(dir);
- expect(project.name).toBe("My Game");
- expect(project.projectYaml?.name).toBe("My Game");
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- it("tolerates a malformed project.yaml", async () => {
- const dir = await tmpDir();
- try {
- await mkdir(join(dir, "content"));
- await writeFile(join(dir, "project.yaml"), "name: [broken\n", "utf8");
- const project = await openProject(dir);
- expect(project.name).toBe(dir.split("/").pop());
- expect(project.projectYaml).toBeNull();
- expect(project.projectYamlError).toBeDefined();
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- });
- describe("listContent and stats", () => {
- it("lists kinds with counts and relative file paths on a generated project", async () => {
- const dir = await tmpDir();
- try {
- const project = await newProject("Test Farm", dir);
- const listing = await listContent(project.root);
- const byKind = new Map(listing.kinds.map((k) => [k.kind, k]));
- expect(byKind.get("map")?.count).toBe(1);
- expect(byKind.get("item")?.count).toBe(3);
- expect(byKind.get("crop")?.count).toBe(1);
- expect(byKind.get("time")?.count).toBe(1);
- expect(byKind.get("weather")?.count).toBe(1);
- expect(byKind.get("interact")?.count).toBe(1);
- expect(byKind.get("scene")?.count).toBe(1);
- const hoe = byKind.get("item")?.files.find((f) => f.name === "hoe.yaml");
- expect(hoe?.path).toBe("content/items/hoe.yaml");
- const time = byKind.get("time")?.files[0];
- expect(time?.path).toBe("content/time.yaml");
- expect(listing.mods).toEqual([]);
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- it("returns stats with per-kind counts", async () => {
- const dir = await tmpDir();
- try {
- const project = await newProject("Stats Farm", dir);
- const result = await stats(project.root);
- expect(result.kinds.map).toBe(1);
- expect(result.kinds.item).toBe(3);
- expect(result.total).toBe(9);
- expect(result.mods).toBe(0);
- expect(result.assets).toBe(0);
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- it("counts mods when a mods/ dir has entries", async () => {
- const dir = await tmpDir();
- try {
- await mkdir(join(dir, "content"), { recursive: true });
- await mkdir(join(dir, "mods", "my-mod"), { recursive: true });
- await writeFile(join(dir, "mods", "my-mod", "index.mjs"), "export const hooks = {}\n", "utf8");
- const listing = await listContent(dir);
- expect(listing.mods).toEqual(["my-mod"]);
- expect((await stats(dir)).mods).toBe(1);
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- });
- describe("resolveWithin", () => {
- it("accepts paths inside the root", () => {
- expect(resolveWithin("/a/b", "content/maps/farm.yaml")).toBe("/a/b/content/maps/farm.yaml");
- expect(resolveWithin("/a/b", ".")).toBe("/a/b");
- });
- it("rejects parent traversal and absolute escapes", () => {
- expect(() => resolveWithin("/a/b", "../secret.yaml")).toThrow(/escapes project root/);
- expect(() => resolveWithin("/a/b", "content/../../x.yaml")).toThrow(/escapes project root/);
- expect(() => resolveWithin("/a/b", "/etc/passwd")).toThrow(/escapes project root/);
- });
- });
- describe("readYaml and writeYaml", () => {
- it("roundtrips yaml content and creates nested directories", async () => {
- const dir = await tmpDir();
- try {
- await mkdir(join(dir, "content"), { recursive: true });
- await writeYaml(dir, "content/items/hoe.yaml", "id: hoe\n");
- expect(await readYaml(dir, "content/items/hoe.yaml")).toBe("id: hoe\n");
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- it("rejects non-yaml paths", async () => {
- const dir = await tmpDir();
- try {
- await expect(writeYaml(dir, "content/notes.txt", "x")).rejects.toThrow(/not a yaml path/);
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- it("rejects traversal in read and write", async () => {
- const dir = await tmpDir();
- try {
- await expect(writeYaml(dir, "../evil.yaml", "x")).rejects.toThrow(/escapes project root/);
- await expect(readYaml(dir, "content/../../evil.yaml")).rejects.toThrow(/escapes project root/);
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- });
- describe("inspectProject", () => {
- it("inspects a folder without requiring content/", async () => {
- const dir = await tmpDir();
- try {
- const project = await inspectProject(dir);
- expect(project.hasContent).toBe(false);
- expect(project.hasMods).toBe(false);
- } finally {
- await rm(dir, { recursive: true, force: true });
- }
- });
- });
|