/*
RFC-0488: PBT test for the material credits provenance registry migrator —
verifies idempotency (f(f(x)) == f(x)) over credit sidecar transformations.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-0488: initial PBT test for material credits migrator.
*/
import { test, expect } from "vitest";
import fc from "fast-check";
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { rfc0488Migrator } from "./rfc-0488.ts";
import type { SternsystemData, MigrationContext } from "./types.ts";
const ctx: MigrationContext = {
systemId: "test",
missionId: "test-mission",
logger: { info: () => {} },
};
const SOURCE_TYPES = [
"human-made",
"ai-assisted",
"ai-generated",
"composite",
"third-party",
"commissioned",
"licensed-third-party",
"customer-supplied",
"public-domain",
"screenshot",
] as const;
const PARTY_KINDS = ["Person", "Organization", "AIAgent", "AIModel", "AIPlatform"] as const;
const PARTY_ROLES = [
"creator",
"coCreator",
"commissionedBy",
"producer",
"promptAuthor",
"reviewer",
"approver",
"rightsHolder",
"editor",
"contributor",
"photographer",
"illustrator",
] as const;
const sourceTypeArb = fc.constantFrom(...SOURCE_TYPES);
const partyArb = fc.record({
role: fc.constantFrom(...PARTY_ROLES),
name: fc.string({ minLength: 1, maxLength: 20 }),
kind: fc.constantFrom(...PARTY_KINDS),
});
const creditRecordArb = fc.record({
id: fc.string({ minLength: 1, maxLength: 20 }),
sourceType: sourceTypeArb,
parties: fc.array(partyArb, { minLength: 1, maxLength: 4 }),
license: fc.record({
label: fc.string({ minLength: 1, maxLength: 20 }),
copyrightNotice: fc.option(fc.constant("Copyright © Warpgogol. All rights reserved.")),
}),
});
async function withTempDir(fn: (dir: string) => Promise): Promise {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc-0488-test-"));
try {
await fn(dir);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
}
async function writeCreditsFile(dir: string, record: unknown): Promise {
const assetsDir = path.join(dir, "src", "content", "pages", "de", "assets");
await fs.mkdir(assetsDir, { recursive: true });
const filePath = path.join(assetsDir, "test-image.credits.yaml");
const { stringify } = await import("yaml");
await fs.writeFile(filePath, stringify(record), "utf8");
return filePath;
}
async function readCreditsFile(filePath: string): Promise> {
const raw = await fs.readFile(filePath, "utf8");
const { parse } = await import("yaml");
return parse(raw) as Record;
}
test("rfc-0488 migrator is idempotent: f(f(x)) == f(x) for random credit records", async () => {
await fc.asyncProperty(creditRecordArb, async (record) => {
await withTempDir(async (dir) => {
const filePath = await writeCreditsFile(dir, record);
const data: SternsystemData = { rootPath: dir, dataPaths: [] };
await rfc0488Migrator.transform(data, ctx);
const content1 = await readCreditsFile(filePath);
await rfc0488Migrator.transform(data, ctx);
const content2 = await readCreditsFile(filePath);
expect(content2).toEqual(content1);
});
});
});
test("rfc-0488 migrator adds status: active to records without status", async () => {
await withTempDir(async (dir) => {
const filePath = await writeCreditsFile(dir, {
id: "test",
sourceType: "human-made",
parties: [{ role: "creator", name: "Test", kind: "Person" }],
license: { label: "MIT" },
});
const data: SternsystemData = { rootPath: dir, dataPaths: [] };
await rfc0488Migrator.transform(data, ctx);
const result = await readCreditsFile(filePath);
expect(result.status).toBe("active");
});
});
test("rfc-0488 migrator adds aiUsage to ai-generated records with default copyright", async () => {
await withTempDir(async (dir) => {
const filePath = await writeCreditsFile(dir, {
id: "test-ai",
sourceType: "ai-generated",
parties: [{ role: "aiPlatform", name: "Midjourney", kind: "AIPlatform" }],
license: {
label: "Alle Rechte vorbehalten",
copyrightNotice: "Copyright © Warpgogol. All rights reserved.",
},
});
const data: SternsystemData = { rootPath: dir, dataPaths: [] };
await rfc0488Migrator.transform(data, ctx);
const result = await readCreditsFile(filePath);
expect(result.aiUsage).toBeDefined();
expect((result.aiUsage as Record).kind).toBe("ai-generated");
expect((result.aiUsage as Record).copyrightClaimed).toBe(false);
});
});
test("rfc-0488 migrator sets copyrightClaimed: true when Person creator exists", async () => {
await withTempDir(async (dir) => {
const filePath = await writeCreditsFile(dir, {
id: "test-ai-person",
sourceType: "ai-generated",
parties: [
{ role: "creator", name: "John Doe", kind: "Person" },
{ role: "aiPlatform", name: "Midjourney", kind: "AIPlatform" },
],
license: {
label: "Alle Rechte vorbehalten",
copyrightNotice: "Copyright © Warpgogol. All rights reserved.",
},
});
const data: SternsystemData = { rootPath: dir, dataPaths: [] };
await rfc0488Migrator.transform(data, ctx);
const result = await readCreditsFile(filePath);
expect(result.aiUsage).toBeDefined();
expect((result.aiUsage as Record).copyrightClaimed).toBe(true);
});
});
test("rfc-0488 migrator adds unverified usageBasis to third-party records", async () => {
await withTempDir(async (dir) => {
const filePath = await writeCreditsFile(dir, {
id: "test-third-party",
sourceType: "third-party",
parties: [{ role: "creator", name: "Someone", kind: "Person" }],
license: { label: "Fair Use" },
});
const data: SternsystemData = { rootPath: dir, dataPaths: [] };
await rfc0488Migrator.transform(data, ctx);
const result = await readCreditsFile(filePath);
expect(result.usageBasis).toBeDefined();
expect((result.usageBasis as Record).type).toBe("unverified");
});
});
test("rfc-0488 migrator renames Organization creator to commissionedBy for human-made records", async () => {
await withTempDir(async (dir) => {
const filePath = await writeCreditsFile(dir, {
id: "test-org-creator",
sourceType: "human-made",
parties: [{ role: "creator", name: "Warpgogol Inc", kind: "Organization" }],
license: { label: "All rights reserved" },
});
const data: SternsystemData = { rootPath: dir, dataPaths: [] };
await rfc0488Migrator.transform(data, ctx);
const result = await readCreditsFile(filePath);
const parties = result.parties as Array<{ role: string; kind: string }>;
expect(parties[0].role).toBe("commissionedBy");
expect(result.status).toBe("needs-review");
});
});