Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

173 lines
6.2 KiB
TypeScript

/**
* End-to-end coverage for `scaffold()`: the filesystem half of init.
*
* Each test runs against a fresh tempdir so writes don't collide. The
* suite's job is to verify the file tree, the overwrite policy, and
* the "the scaffold round-trips through the loader" invariant — the
* scaffolder shouldn't produce a manifest that its own validator
* rejects (when the user has supplied all required fields).
*/
import { mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { InitError, scaffold } from "../src/init/scaffold.js";
import type { ScaffoldInputs } from "../src/init/templates.js";
import { loadManifest } from "../src/manifest/load.js";
const FULL_INPUTS: ScaffoldInputs = {
slug: "gallery",
publisher: "did:plc:abc123def456",
publisherHandle: "example.com",
license: "MIT",
author: { name: "Jane Doe" },
security: { email: "security@example.com" },
description: undefined,
repo: undefined,
};
const MINIMAL_INPUTS: ScaffoldInputs = {
slug: "gallery",
publisher: undefined,
publisherHandle: undefined,
license: undefined,
author: undefined,
security: undefined,
description: undefined,
repo: undefined,
};
describe("scaffold", () => {
let targetDir: string;
beforeEach(async () => {
// Each test gets its own tempdir, then immediately removes the
// dir so the scaffolder is creating from scratch (matches what
// a real `init` invocation does into a brand-new directory).
const root = await mkdtemp(join(tmpdir(), "emdash-init-test-"));
targetDir = join(root, "plugin");
});
afterEach(async () => {
// rm the parent root, not just targetDir, so we don't leak
// the mkdtemp parent behind.
const root = targetDir.replace(/\/plugin$/, "");
await rm(root, { recursive: true, force: true });
});
it("writes the expected file tree", async () => {
const result = await scaffold({ targetDir, inputs: FULL_INPUTS, force: false });
expect(result.written).toHaveLength(7);
// Spot-check the structure rather than pinning the array order.
const fileSet = new Set(result.written.map((p) => p.replace(`${targetDir}/`, "")));
expect(fileSet.has("emdash-plugin.jsonc")).toBe(true);
expect(fileSet.has("package.json")).toBe(true);
expect(fileSet.has("tsconfig.json")).toBe(true);
expect(fileSet.has(".gitignore")).toBe(true);
expect(fileSet.has("README.md")).toBe(true);
expect(fileSet.has("src/plugin.ts")).toBe(true);
expect(fileSet.has("tests/plugin.test.ts")).toBe(true);
});
it("produces a manifest that the loader accepts", async () => {
await scaffold({ targetDir, inputs: FULL_INPUTS, force: false });
const { manifest } = await loadManifest(targetDir);
expect(manifest.slug).toBe("gallery");
// `version` is intentionally omitted from the scaffold manifest;
// the build reads it from package.json instead.
expect(manifest.version).toBeUndefined();
expect(manifest.publisher).toBe("did:plc:abc123def456");
expect(manifest.license).toBe("MIT");
});
it("produces a minimal manifest that round-trips through the loader (with empty publisher)", async () => {
// Minimal scaffold writes TODO placeholders. The loader's JSONC
// parse succeeds; the schema rejects on `publisher` being empty.
// We catch that explicitly so the user knows what to fix.
await scaffold({ targetDir, inputs: MINIMAL_INPUTS, force: false });
await expect(loadManifest(targetDir)).rejects.toMatchObject({
name: "ManifestError",
code: "MANIFEST_VALIDATION_ERROR",
});
});
it("refuses to overwrite an existing file without --force", async () => {
// Pre-create one of the target files with different content.
const { mkdir } = await import("node:fs/promises");
await mkdir(targetDir, { recursive: true });
await writeFile(join(targetDir, "package.json"), "{}", "utf8");
await expect(scaffold({ targetDir, inputs: FULL_INPUTS, force: false })).rejects.toMatchObject({
name: "InitError",
code: "TARGET_FILE_EXISTS",
conflicts: ["package.json"],
});
// The original file must be untouched.
const contents = await readFile(join(targetDir, "package.json"), "utf8");
expect(contents).toBe("{}");
});
it("does not partially write when a conflict is detected", async () => {
// Same setup as above. The conflict check runs BEFORE any
// write, so nothing else should appear in the target dir.
const { mkdir } = await import("node:fs/promises");
await mkdir(targetDir, { recursive: true });
await writeFile(join(targetDir, "package.json"), "{}", "utf8");
await expect(scaffold({ targetDir, inputs: FULL_INPUTS, force: false })).rejects.toThrow(
InitError,
);
const entries = await readdir(targetDir);
// Only the pre-existing file should be there.
expect(entries).toEqual(["package.json"]);
});
it("overwrites existing files when --force is set", async () => {
const { mkdir } = await import("node:fs/promises");
await mkdir(targetDir, { recursive: true });
await writeFile(join(targetDir, "package.json"), "{}", "utf8");
await scaffold({ targetDir, inputs: FULL_INPUTS, force: true });
const contents = await readFile(join(targetDir, "package.json"), "utf8");
// The scaffold wrote the real package.json over the stub.
const parsed = JSON.parse(contents) as { name: string };
expect(parsed.name).toBe("gallery");
});
it("creates intermediate directories (src/, tests/)", async () => {
await scaffold({ targetDir, inputs: FULL_INPUTS, force: false });
const srcStat = await stat(join(targetDir, "src"));
const testsStat = await stat(join(targetDir, "tests"));
expect(srcStat.isDirectory()).toBe(true);
expect(testsStat.isDirectory()).toBe(true);
});
it("invokes onFileWritten once per file in scaffold order", async () => {
const calls: string[] = [];
await scaffold({
targetDir,
inputs: FULL_INPUTS,
force: false,
onFileWritten: (rel) => calls.push(rel),
});
// The seven files, in some deterministic order (see FILES in
// scaffold.ts). The exact order is part of the API surface
// for CLI progress output.
expect(calls).toEqual([
"emdash-plugin.jsonc",
"package.json",
"tsconfig.json",
".gitignore",
"README.md",
"src/plugin.ts",
"tests/plugin.test.ts",
]);
});
});