import { describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { fromFileUrl, join } from "@std/path"; import type { PatternIndexPublishRequest } from "@commonfabric/pattern-index/client"; import { Identity } from "@commonfabric/identity"; import { experimentalOptionsFromEnv, Runtime, runtimePresets, } from "@commonfabric/runner"; import { StorageManager } from "../../runner/src/storage/cache.deno.ts"; import { runDenoCommandWithTemporaryLock } from "@commonfabric/test-support/isolated-deno"; import { compileAtom, defaultSeedIo, denoFmtCheck, durableEntryIdentity, fabricSeedDeps, GENERATIONS_FILE, main, parseArguments, parseGenerations, publishRequestFor, requiredSetting, resolveSettings, runSeed, SEED_DIRECTORY, type SeedDeps, seedDepsFrom, type SeedGenerations, type SeedIo, seedMetadataFromSource, seedSourcePaths, SeedUsageError, serializeGenerations, USAGE, withGeneration, } from "../scripts/seed-pattern-index.ts"; const REPO_ROOT = fromFileUrl(new URL("../../..", import.meta.url)); const withDoc = (doc: string) => `${doc}\nexport const X = 1;\n`; /** A record holding no atom, for a case that is not about supersession. */ const EMPTY_GENERATIONS: SeedGenerations = { note: "a note", atoms: {} }; describe("seed-pattern-index", () => { describe("seedMetadataFromSource()", () => { it("takes the description from the first paragraph only, so the claim stays narrow", () => { const metadata = seedMetadataFromSource( "atom", withDoc(`/** * Counts a number up and down by a configurable step. * * Embed it as \`\` and it mutates the host's cell. * * @hashtags counter, number * @keywords count, increment */`), ); expect(metadata.description).toBe( "Counts a number up and down by a configurable step.", ); }); it("folds a description wrapped across lines back onto one line", () => { const metadata = seedMetadataFromSource( "atom", withDoc(`/** * Keeps a list of labeled amounts and shows * the formatted running total. * * @hashtags money * @keywords total */`), ); expect(metadata.description).toBe( "Keeps a list of labeled amounts and shows the formatted running total.", ); }); it("reads hashtags and keywords, including a tag list wrapped across lines", () => { const metadata = seedMetadataFromSource( "atom", withDoc(`/** * Rolls a die. * * @hashtags dice, random, roll * @keywords roll dice, random number, * pick a number */`), ); expect(metadata.hashtags).toEqual(["dice", "random", "roll"]); expect(metadata.keywords).toEqual([ "roll dice", "random number", "pick a number", ]); }); // // Refusals // // An atom the index cannot be searched for is worth less than no atom, and // one described by a guess is worse than either, so each of these stops the // run before anything is published. // it("refuses an atom with no doc comment", () => { expect(() => seedMetadataFromSource("atom", "export const X = 1;\n")) .toThrow(/no leading doc comment/); }); it("refuses an atom whose doc comment opens straight into a tag", () => { expect(() => seedMetadataFromSource( "atom", withDoc("/**\n * @hashtags a\n * @keywords b\n */"), ) ).toThrow(/opens with no description/); }); it("refuses an atom that declares no hashtags", () => { expect(() => seedMetadataFromSource( "atom", withDoc("/**\n * Does a thing.\n *\n * @keywords b\n */"), ) ).toThrow(/no @hashtags/); }); it("refuses an atom that declares no keywords", () => { expect(() => seedMetadataFromSource( "atom", withDoc("/**\n * Does a thing.\n *\n * @hashtags a\n */"), ) ).toThrow(/no @keywords/); }); }); describe("seedSourcePaths()", () => { it("answers the atoms in a stable order and descends into no subdirectory", async () => { const paths = await seedSourcePaths(join(REPO_ROOT, SEED_DIRECTORY)); const names = paths.map((path) => path.split("/").pop()); expect(names).toEqual([...names].sort()); // `demo/` holds the adopter that embeds the atoms; seeding it would // publish the demonstration as though it were a part. expect(names.some((name) => name === undefined)).toBe(false); expect(paths.every((path) => path.endsWith(".tsx"))).toBe(true); expect(paths.some((path) => path.includes("/demo/"))).toBe(false); }); it("skips a directory entry that is not an atom", async () => { const directory = await Deno.makeTempDir(); try { await Deno.writeTextFile(join(directory, "atom.tsx"), "export {};\n"); await Deno.writeTextFile( join(directory, "notes.md"), "# not an atom\n", ); await Deno.writeTextFile(join(directory, "helper.ts"), "export {};\n"); await Deno.writeTextFile( join(directory, "atom.test.tsx"), "export {};\n", ); await Deno.mkdir(join(directory, "demo")); const paths = await seedSourcePaths(directory); expect(paths.map((path) => path.split("/").pop())).toEqual([ "atom.tsx", ]); } finally { await Deno.remove(directory, { recursive: true }); } }); it("finds every atom this seed publishes", async () => { const paths = await seedSourcePaths(join(REPO_ROOT, SEED_DIRECTORY)); expect(paths.map((path) => path.split("/").pop())).toEqual([ "amount-ledger.tsx", "bills-this-month.tsx", "budget-vs-actual.tsx", "check-list.tsx", "counter.tsx", "dice-roller.tsx", "ledger-month-transactions.tsx", "mailbox-month-headers.tsx", "option-picker.tsx", "sortable-table.tsx", "source-row-count.tsx", "spend-by-category.tsx", ]); }); }); describe("every seeded atom", () => { it("carries a description, hashtags and keywords the index can rank on", async () => { const paths = await seedSourcePaths(join(REPO_ROOT, SEED_DIRECTORY)); for (const path of paths) { const name = path.split("/").pop() ?? path; const metadata = seedMetadataFromSource( name, await Deno.readTextFile(path), ); expect(metadata.description.length).toBeGreaterThan(40); expect(metadata.hashtags.length).toBeGreaterThan(2); expect(metadata.keywords.length).toBeGreaterThan(4); } }); }); describe("parseArguments()", () => { it("reads the dry-run flag, repeated --only names, and named settings", () => { const parsed = parseArguments([ "--dry-run", "--only", "counter", "--only", "dice-roller", "--api-url", "http://localhost:8040", ]); expect(parsed.dryRun).toBe(true); expect(parsed.only).toEqual(["counter", "dice-roller"]); expect(parsed.named.get("api-url")).toBe("http://localhost:8040"); expect(parsed.help).toBe(false); }); it("defaults to publishing every atom", () => { const parsed = parseArguments([]); expect(parsed.dryRun).toBe(false); expect(parsed.only).toEqual([]); }); it("reads both spellings of help", () => { expect(parseArguments(["--help"]).help).toBe(true); expect(parseArguments(["-h"]).help).toBe(true); }); it("refuses an argument it cannot place", () => { // A dropped flag would seed more than the caller asked for, against a // shared corpus, so an argument the parser cannot place stops the run. expect(() => parseArguments(["counter"])).toThrow(SeedUsageError); }); it("refuses a flag whose value is missing", () => { expect(() => parseArguments(["--only"])).toThrow(/needs the name/); expect(() => parseArguments(["--api-url"])).toThrow(/needs a value/); }); }); describe("requiredSetting()", () => { const parsed = parseArguments(["--space", "from-flag"]); it("prefers the flag over the environment", () => { expect(requiredSetting(parsed, "space", "CF_SPACE", () => "from-env")) .toBe("from-flag"); }); it("falls back to the environment", () => { expect( requiredSetting( parseArguments([]), "space", "CF_SPACE", () => "from-env", ), ).toBe("from-env"); }); it("names the flag and the variable when neither supplies it", () => { expect(() => requiredSetting( parseArguments([]), "space", "CF_SPACE", () => undefined, ) ).toThrow(/--space is required \(or set CF_SPACE\)/); }); it("treats an empty environment value as absent", () => { expect(() => requiredSetting(parseArguments([]), "space", "CF_SPACE", () => "") ).toThrow(SeedUsageError); }); }); describe("USAGE", () => { it("names the directory the atoms are read from", () => { expect(USAGE).toContain(SEED_DIRECTORY); }); }); describe("parseGenerations()", () => { it("answers the record a well-formed file holds", () => { const record = parseGenerations( "record.json", '{"note":"a note","atoms":{"counter":["one","two"]}}', ); expect(record.note).toBe("a note"); expect(record.atoms.counter).toEqual(["one", "two"]); }); // // Refusals // // A chain the run cannot read is a chain a new generation would be // published without, so each of these stops the run instead. // it("names the file that is not JSON", () => { expect(() => parseGenerations("record.json", "{")) .toThrow(/record\.json is not JSON/); }); it("refuses a document that is not a {note, atoms} record", () => { expect(() => parseGenerations("record.json", '{"atoms":{}}')) .toThrow(/holds no \{note, atoms\} record/); }); it("refuses a record whose `atoms` is an array", () => { // An array is an object with numeric keys, so one admitted here would be // read as a record keying every chain by its position. expect(() => parseGenerations("record.json", '{"note":"n","atoms":[]}')) .toThrow(/holds no \{note, atoms\} record/); }); it("refuses an atom recorded as something other than identities", () => { expect(() => parseGenerations( "record.json", '{"note":"n","atoms":{"counter":"one"}}', ) ).toThrow(/records counter as something other than/); }); it("refuses an atom recorded under an empty chain", () => { expect(() => parseGenerations("record.json", '{"note":"n","atoms":{"counter":[]}}') ).toThrow(/records counter as something other than/); }); }); describe("withGeneration()", () => { const record: SeedGenerations = { note: "a note", atoms: { counter: ["one"] }, }; it("appends an identity the chain does not end with", () => { expect(withGeneration(record, "counter", "two").atoms.counter) .toEqual(["one", "two"]); }); it("starts a chain for an atom the record does not hold", () => { expect(withGeneration(record, "dice-roller", "two").atoms["dice-roller"]) .toEqual(["two"]); }); it("answers the record itself when the chain already ends there", () => { expect(withGeneration(record, "counter", "one")).toBe(record); }); }); describe("serializeGenerations()", () => { it("writes the atoms in name order, and ends with a newline", () => { const text = serializeGenerations({ note: "a note", atoms: { counter: ["one"], "amount-ledger": ["two"] }, }); expect(text.endsWith("\n")).toBe(true); expect(text.indexOf("amount-ledger")).toBeLessThan( text.indexOf("counter"), ); }); it("round-trips a record through the parser", () => { const record: SeedGenerations = { note: "a note", atoms: { counter: ["one", "two"] }, }; expect(parseGenerations("record.json", serializeGenerations(record))) .toEqual(record); }); }); describe("the recorded generations", () => { it("names only atoms the seed publishes", async () => { // A chain under a name no atom carries is one no seeding will ever // extend, and the identity it names is what a reader would take for the // current generation of something. An atom with no chain is the ordinary // case of one the index does not hold yet. const record = parseGenerations( GENERATIONS_FILE, await Deno.readTextFile(join(REPO_ROOT, GENERATIONS_FILE)), ); const paths = await seedSourcePaths(join(REPO_ROOT, SEED_DIRECTORY)); const names = paths.map((path) => (path.split("/").pop() ?? "").replace(/\.tsx$/, "") ); expect(Object.keys(record.atoms).filter((name) => !names.includes(name))) .toEqual([]); }); it("is written as the seeder writes it", async () => { const text = await Deno.readTextFile(join(REPO_ROOT, GENERATIONS_FILE)); expect(serializeGenerations(parseGenerations(GENERATIONS_FILE, text))) .toBe(text); }); }); describe("runSeed()", () => { const directory = join(REPO_ROOT, SEED_DIRECTORY); interface Recorder { deps: SeedDeps; published: string[]; recorded: string[]; compiled: string[]; lines: string[]; errors: string[]; /** Every record `writeGenerations` was handed, in the order it was. */ written: SeedGenerations[]; } /** A record naming one prior generation of `counter` and nothing else. */ const priorCounter: SeedGenerations = { note: "a note", atoms: { counter: ["counter-first"] }, }; const recorder = ( overrides: Partial = {}, ): Recorder => { const published: string[] = []; const recorded: string[] = []; const compiled: string[] = []; const lines: string[] = []; const errors: string[] = []; const written: SeedGenerations[] = []; const deps: SeedDeps = { compile: (path) => { compiled.push(path); return Promise.resolve({ patternId: `id-for-${path.split("/").pop()}`, program: { main: "/main.tsx", files: [{ name: "/main.tsx", contents: "export {};" }], }, argumentSchema: { type: "object" }, resultSchema: { type: "object" }, }); }, publish: (request) => { published.push(request.patternId); return Promise.resolve({ patternId: request.patternId, created: true, }); }, recordCreated: (patternId) => { recorded.push(patternId); return Promise.resolve(); }, checkFormatting: () => Promise.resolve([]), readGenerations: () => Promise.resolve({ note: "a note", atoms: {} } as SeedGenerations), writeGenerations: (generations) => { written.push(generations); return Promise.resolve(); }, log: (line) => lines.push(line), logError: (line) => errors.push(line), ...overrides, }; return { deps, published, recorded, compiled, lines, errors, written }; }; it("publishes every atom and records a created event for each", async () => { const r = recorder(); const code = await runSeed( { dryRun: false, only: [], directory }, r.deps, ); expect(code).toBe(0); expect(r.published.length).toBe(12); expect(r.recorded).toEqual(r.published); expect(r.lines.at(-1)).toContain("12 published, 0 already held"); }); it("publishes nothing in a dry run, having compiled everything", async () => { const r = recorder(); const code = await runSeed({ dryRun: true, only: [], directory }, r.deps); expect(code).toBe(0); expect(r.compiled.length).toBe(12); expect(r.published).toEqual([]); expect(r.recorded).toEqual([]); expect(r.lines.at(-1)).toContain("nothing published"); }); it("prints the import specifier a composing pattern would write", async () => { const r = recorder(); await runSeed({ dryRun: true, only: ["counter"], directory }, r.deps); expect( r.lines.some((line) => line.includes("cf:pattern:id-for-counter.tsx")), ) .toBe(true); }); it("seeds only the named atom", async () => { const r = recorder(); await runSeed({ dryRun: false, only: ["counter"], directory }, r.deps); expect(r.published).toEqual(["id-for-counter.tsx"]); }); it("records no event for an atom the index already holds", async () => { // Re-running must not create duplicates: the index answers // `created:false` for an identity it already holds, and that records no // further event. const r = recorder({ publish: (request) => Promise.resolve({ patternId: request.patternId, created: false }), }); const code = await runSeed( { dryRun: false, only: [], directory }, r.deps, ); expect(code).toBe(0); expect(r.recorded).toEqual([]); expect(r.lines.at(-1)).toContain("0 published, 12 already held"); }); it("publishes nothing when an atom compiles to no durable identity", async () => { // An entry under a keyless identity could never be loaded by anything // else, so the run stops rather than seeding one. const r = recorder({ compile: () => Promise.resolve({ program: { main: "/main.tsx", files: [{ name: "/main.tsx", contents: "export {};" }], }, }), }); const code = await runSeed( { dryRun: false, only: [], directory }, r.deps, ); expect(code).toBe(1); expect(r.published).toEqual([]); expect(r.errors.join("\n")).toContain("no durable content-addressed"); }); /** A recorder whose publish also keeps the requests it was handed. */ const requestRecorder = ( generations: SeedGenerations, created = true, ): { recorder: Recorder; requests: PatternIndexPublishRequest[] } => { const requests: PatternIndexPublishRequest[] = []; return { recorder: recorder({ readGenerations: () => Promise.resolve(generations), publish: (request) => { requests.push(request); return Promise.resolve({ patternId: request.patternId, created }); }, }), requests, }; }; it("names the recorded generation as what a new identity supersedes", async () => { const { recorder: r, requests } = requestRecorder(priorCounter); await runSeed({ dryRun: false, only: ["counter"], directory }, r.deps); expect(requests[0].priorPatternId).toBe("counter-first"); }); it("names no prior generation for an atom the record does not hold", async () => { const { recorder: r, requests } = requestRecorder(priorCounter); await runSeed( { dryRun: false, only: ["dice-roller"], directory }, r.deps, ); expect(requests[0].priorPatternId).toBeUndefined(); }); it("names no prior generation for the identity already recorded", async () => { // The same source compiles to the same identity, so a re-run of an atom // nothing changed would otherwise publish it as superseding itself. const { recorder: r, requests } = requestRecorder({ note: "a note", atoms: { counter: ["id-for-counter.tsx"] }, }, false); await runSeed({ dryRun: false, only: ["counter"], directory }, r.deps); expect(requests[0].priorPatternId).toBeUndefined(); expect(r.written).toEqual([]); }); it("appends what it published to the record", async () => { const r = recorder({ readGenerations: () => Promise.resolve(priorCounter), }); await runSeed({ dryRun: false, only: ["counter"], directory }, r.deps); expect(r.written.length).toBe(1); expect(r.written[0].atoms.counter).toEqual([ "counter-first", "id-for-counter.tsx", ]); expect(r.lines.join("\n")).toContain(GENERATIONS_FILE); }); it("records what it published before a later atom fails", async () => { // The index holds an entry from the moment it accepts it, so a run that // dies afterwards must leave that identity recorded: a record missing it // makes the next run's `priorPatternId` name a generation the index has // already displaced. const r = recorder({ publish: (request) => request.patternId === "id-for-counter.tsx" ? Promise.resolve({ patternId: request.patternId, created: true }) : Promise.reject(new Error("the index refused")), }); await expect( runSeed( { dryRun: false, only: ["counter", "dice-roller"], directory }, r.deps, ), ).rejects.toThrow("the index refused"); expect(r.written.length).toBe(1); expect(r.written[0].atoms["counter"]).toEqual(["id-for-counter.tsx"]); }); it("leaves the record alone in a dry run", async () => { const r = recorder({ readGenerations: () => Promise.resolve(priorCounter), }); await runSeed({ dryRun: true, only: ["counter"], directory }, r.deps); expect(r.written).toEqual([]); }); it("stops on a record it cannot read, having compiled nothing", async () => { const r = recorder({ readGenerations: () => Promise.reject(new Error("record is broken")), }); await expect( runSeed({ dryRun: true, only: [], directory }, r.deps), ).rejects.toThrow("record is broken"); expect(r.compiled).toEqual([]); }); it("refuses a selection that matches no atom", async () => { const r = recorder(); await expect( runSeed({ dryRun: true, only: ["nope"], directory }, r.deps), ).rejects.toThrow(SeedUsageError); expect(r.compiled).toEqual([]); }); }); describe("durableEntryIdentity()", () => { it("keeps a content-addressed identity", () => { expect(durableEntryIdentity("docnE_YXUkal8R92pI4Lz7zAg0w")).toBe( "docnE_YXUkal8R92pI4Lz7zAg0w", ); }); it("answers undefined for an absent identity", () => { expect(durableEntryIdentity(undefined)).toBeUndefined(); }); it("refuses a keyless identity", () => { // An entry published under a keyless identity could never be loaded by // any other runtime, so it must not reach the index at all. expect(durableEntryIdentity("keyless:abc123")).toBeUndefined(); }); }); describe("resolveSettings()", () => { it("resolves every setting from the environment", () => { const env = (key: string) => ({ CF_HARNESS_FABRIC_API_URL: "http://localhost:8040", CF_HARNESS_FABRIC_IDENTITY: "/keys/agent.pkcs8", CF_HARNESS_FABRIC_SPACE: "seeds", CF_HARNESS_PATTERN_INDEX_URL: "https://index.example", })[key]; expect(resolveSettings(parseArguments([]), env)).toEqual({ apiUrl: "http://localhost:8040", identityKeyPath: "/keys/agent.pkcs8", space: "seeds", indexBaseUrl: "https://index.example", }); }); it("names the first setting nothing supplies", () => { expect(() => resolveSettings(parseArguments([]), () => undefined)) .toThrow(/--api-url is required/); }); }); describe("compileAtom()", () => { it("answers a durable identity and the real argument schema for an atom", async () => { const signer = await Identity.fromPassphrase("seed-pattern-index test"); const runtime = new Runtime(runtimePresets.patternTest({ apiUrl: new URL(import.meta.url), storageManager: StorageManager.emulate({ as: signer }), experimental: experimentalOptionsFromEnv(Deno.env.get), })); try { const root = join(REPO_ROOT, "packages/patterns"); const compiled = await compileAtom( runtime, signer.did(), join(root, "primitives/counter.tsx"), root, ); // The identity is a content hash, so it is durable under any runtime. expect(compiled.patternId).toBeDefined(); expect(durableEntryIdentity(compiled.patternId)).toBe( compiled.patternId, ); expect(compiled.program.files.length).toBeGreaterThan(0); // The whole point of the seed: an atom that takes real inputs. const schema = compiled.argumentSchema as { properties?: Record; }; expect(Object.keys(schema.properties ?? {})).toEqual([ "value", "step", "label", "min", "max", ]); } finally { await runtime.dispose?.(); } }); }); describe("main()", () => { const io = ( overrides: Partial = {}, ): { io: SeedIo; lines: string[]; errors: string[]; published: string[]; } => { const lines: string[] = []; const errors: string[] = []; const published: string[] = []; return { lines, errors, published, io: { env: (key) => ({ CF_HARNESS_FABRIC_API_URL: "http://localhost:8040", CF_HARNESS_FABRIC_IDENTITY: "/keys/agent.pkcs8", CF_HARNESS_FABRIC_SPACE: "seeds", CF_HARNESS_PATTERN_INDEX_URL: "https://index.example", })[key], log: (line) => lines.push(line), logError: (line) => errors.push(line), repoRoot: REPO_ROOT, createDeps: (_settings, _root, log, logError) => Promise.resolve({ compile: (path) => Promise.resolve({ patternId: `id-${path.split("/").pop()}`, program: { main: "/main.tsx", files: [{ name: "/main.tsx", contents: "export {};" }], }, }), publish: (request) => { published.push(request.patternId); return Promise.resolve({ patternId: request.patternId, created: true, }); }, recordCreated: () => Promise.resolve(), readGenerations: () => Promise.resolve(EMPTY_GENERATIONS), writeGenerations: () => Promise.resolve(), checkFormatting: () => Promise.resolve([]), log, logError, }), ...overrides, }, }; }; it("seeds every atom and answers success", async () => { const h = io(); expect(await main([], h.io)).toBe(0); expect(h.published.length).toBe(12); }); it("publishes nothing on a dry run", async () => { const h = io(); expect(await main(["--dry-run"], h.io)).toBe(0); expect(h.published).toEqual([]); }); it("prints usage for --help without connecting to anything", async () => { const h = io({ createDeps: () => Promise.reject(new Error("must not connect")), }); expect(await main(["--help"], h.io)).toBe(0); expect(h.lines.join("\n")).toContain( "Usage: deno task seed-pattern-index", ); }); it("answers 2 and prints usage for an argument it cannot place", async () => { const h = io(); expect(await main(["counter"], h.io)).toBe(2); expect(h.errors.join("\n")).toContain("unexpected argument"); expect(h.published).toEqual([]); }); it("answers 2 when a required setting is missing", async () => { const h = io({ env: () => undefined }); expect(await main([], h.io)).toBe(2); expect(h.errors.join("\n")).toContain("--api-url is required"); }); it("answers 2 for a selection that matches no atom", async () => { const h = io(); expect(await main(["--only", "nope"], h.io)).toBe(2); expect(h.published).toEqual([]); }); it("answers 1 when the run fails for a reason that is not usage", async () => { const h = io({ createDeps: () => Promise.reject(new Error("fabric unreachable")), }); expect(await main([], h.io)).toBe(1); expect(h.errors.join("\n")).toContain("fabric unreachable"); }); it("answers 1 when an atom compiles to no durable identity", async () => { const h = io({ createDeps: (_s, _r, log, logError) => Promise.resolve({ compile: () => Promise.resolve({ program: { main: "/main.tsx", files: [{ name: "/main.tsx", contents: "export {};" }], }, }), publish: () => Promise.reject(new Error("must not publish")), recordCreated: () => Promise.reject(new Error("must not record")), checkFormatting: () => Promise.resolve([]), readGenerations: () => Promise.resolve(EMPTY_GENERATIONS), writeGenerations: () => Promise.reject(new Error("must not record generations")), log, logError, }), }); expect(await main([], h.io)).toBe(1); }); }); describe("defaultSeedIo()", () => { it("reads the environment and roots itself at the repository", () => { const io = defaultSeedIo(); expect(io.repoRoot).toBe(REPO_ROOT); expect(io.createDeps).toBe(fabricSeedDeps); // Reads the real environment, and writes where a script writes. Deno.env.set("CF_SEED_IO_PROBE", "present"); try { expect(io.env("CF_SEED_IO_PROBE")).toBe("present"); } finally { Deno.env.delete("CF_SEED_IO_PROBE"); } expect(io.env("CF_SEED_IO_ABSENT")).toBeUndefined(); io.log(""); io.logError(""); }); }); describe("the formatting guard", () => { const directory = join(REPO_ROOT, SEED_DIRECTORY); it("publishes nothing when an atom is not formatted, and names it", async () => { // An entry identity is a hash of the source bytes, so publishing // unformatted source mints an id the repository cannot reproduce: the // next `deno fmt` changes the bytes and the same atom seeds again under a // second id. One atom, two entries, competing in search. const published: string[] = []; const errors: string[] = []; const compiled: string[] = []; const code = await runSeed({ dryRun: false, only: [], directory }, { compile: (path) => { compiled.push(path); return Promise.resolve({ patternId: "id", program: { main: "/m.tsx", files: [] }, }); }, publish: (request) => { published.push(request.patternId); return Promise.resolve({ patternId: request.patternId, created: true, }); }, recordCreated: () => Promise.resolve(), readGenerations: () => Promise.resolve(EMPTY_GENERATIONS), writeGenerations: () => Promise.resolve(), checkFormatting: (paths) => Promise.resolve([paths[0]]), log: () => {}, logError: (line) => errors.push(line), }); expect(code).toBe(1); expect(published).toEqual([]); // Refused before anything compiles, so no id is minted at all. expect(compiled).toEqual([]); expect(errors.join("\n")).toContain( "changes the next time `deno fmt` runs", ); expect(errors.join("\n")).toContain("amount-ledger.tsx"); }); it("proceeds when every atom is formatted", async () => { const published: string[] = []; const code = await runSeed({ dryRun: false, only: ["counter"], directory, }, { compile: () => Promise.resolve({ patternId: "id", program: { main: "/m.tsx", files: [] }, }), publish: (request) => { published.push(request.patternId); return Promise.resolve({ patternId: request.patternId, created: true, }); }, recordCreated: () => Promise.resolve(), readGenerations: () => Promise.resolve(EMPTY_GENERATIONS), writeGenerations: () => Promise.resolve(), checkFormatting: () => Promise.resolve([]), log: () => {}, logError: () => {}, }); expect(code).toBe(0); expect(published).toEqual(["id"]); }); it("finds the committed atoms already formatted", async () => { // The guard is only as good as the check behind it, so the real one is // exercised against the atoms as committed. const paths = await seedSourcePaths(directory); expect(await denoFmtCheck(paths)).toEqual([]); }); it("names only the unformatted file among several", async () => { // Deliberately mixes a formatted file in with the unformatted one, and // asserts BOTH that the offender is named and that the innocent file is // not. Given a single path those two claims coincide with "blame // everything I was given", which is what `denoFmtCheck` falls back to // when it cannot read deno's answer — so a one-file version of this test // passes against a parse that never worked, and passed against a fixture // that did not exist at all. // Written outside the repository so the repo's own formatting gates // cannot rewrite the thing under test. const directory = await Deno.makeTempDir(); try { const unformatted = join(directory, "unformatted.ts"); const formatted = join(directory, "formatted.ts"); await Deno.writeTextFile( unformatted, "export const x = {a:1, b:2}\n", ); await Deno.writeTextFile(formatted, "export const y = 1;\n"); const named = await denoFmtCheck([formatted, unformatted]); expect(named.length).toBe(1); // deno reports the path it resolved, so compare by what it names. expect(named[0].endsWith("unformatted.ts")).toBe(true); } finally { await Deno.remove(directory, { recursive: true }); } }); it("refuses everything it was given when deno names no file", async () => { // The fallback itself: a non-zero exit deno named no file for is still a // refusal to publish, because seeding on an unreadable answer is the // silence this guard exists to prevent. const directory = await Deno.makeTempDir(); await Deno.remove(directory, { recursive: true }); const absent = join(directory, "gone.ts"); expect(await denoFmtCheck([absent])).toEqual([absent]); }); }); describe("seedDepsFrom()", () => { const client = () => { const published: string[] = []; const events: string[] = []; return { published, events, get: () => Promise.resolve({ publishPattern: (request: { patternId: string }) => { published.push(request.patternId); return Promise.resolve({ patternId: request.patternId, created: true, }); }, recordEvent: ( request: { patternId: string; eventType: string }, ) => { events.push(`${request.patternId}:${request.eventType}`); return Promise.resolve({ ok: true }); }, }), }; }; const depsOver = (c: ReturnType) => { const lines: string[] = []; return { lines, deps: seedDepsFrom({ // Never called in these cases; `compileAtom` is covered on its own // against a real runtime. runtime: {} as never, space: "did:key:zTest" as never, getClient: c.get, repoRoot: "/repo", log: (line: string) => lines.push(line), logError: () => {}, }), }; }; it("publishes through the client it was given", async () => { const c = client(); const { deps } = depsOver(c); const response = await deps.publish( { patternId: "abc", program: { main: "/m", files: [] } } as never, ); expect(response).toEqual({ patternId: "abc", created: true }); expect(c.published).toEqual(["abc"]); }); it("records a created event through the same client", async () => { const c = client(); const { deps } = depsOver(c); await deps.recordCreated("abc"); expect(c.events).toEqual(["abc:created"]); }); it("logs through the callbacks it was given", () => { const c = client(); const { deps, lines } = depsOver(c); deps.log("hello"); expect(lines).toEqual(["hello"]); }); it("checks formatting with the repository's own formatter", () => { const c = client(); const { deps } = depsOver(c); expect(deps.checkFormatting).toBe(denoFmtCheck); }); }); describe("fabricSeedDeps()", () => { const settings = { apiUrl: "http://localhost:8060", identityKeyPath: "/keys/agent.pkcs8", space: "seeds", indexBaseUrl: "https://index.example", }; it("wires the fabric settings to the session and the index URL to the client", async () => { // Which setting reaches the fabric and which reaches the index are both // strings, so wiring one to the other compiles and fails only in the // deployment. This is the assertion that catches it. let sessionArgs: unknown; let clientArgs: unknown[] = []; const deps = await fabricSeedDeps( settings, "/repo", () => {}, () => {}, ((config: unknown) => { sessionArgs = config; return () => Promise.resolve({ pieces: { runtime: {} as never, getSpace: () => "did:key:zSpace", }, }); }) as never, ((...args: unknown[]) => { clientArgs = args; return () => Promise.resolve({} as never); }) as never, ); expect(sessionArgs).toEqual({ apiUrl: "http://localhost:8060", identityKeyPath: "/keys/agent.pkcs8", space: "seeds", }); expect(clientArgs[0]).toEqual({ baseUrl: "https://index.example" }); expect(clientArgs[1]).toBe("/keys/agent.pkcs8"); expect(deps.checkFormatting).toBe(denoFmtCheck); }); it("fails when the identity keyfile is absent", async () => { // Exercises the constructions it performs by default rather than the // stand-ins, and fails on the keyfile before reaching any network. await expect( fabricSeedDeps( { ...settings, identityKeyPath: "/keys/definitely-absent.pkcs8" }, "/repo", () => {}, () => {}, ), ).rejects.toThrow(); }); }); describe("as a program", () => { it("prints usage and exits zero", async () => { // The one path that runs the file as a script rather than importing it. // `--help` neither connects to a fabric nor writes anything, so this is a // smoke test of the entry point itself: it parses, prints, and exits 0. // // Runs through the isolated-lock helper, which points the child at a copy // of `deno.lock` so resolving dependencies cannot rewrite the real one, // and which spawns `Deno.execPath()` rather than whichever `deno` is on // PATH — a different Deno reads transpiled sources from its own part of // the cache, and reports coverage with every file missing. const output = await runDenoCommandWithTemporaryLock({ root: REPO_ROOT, args: (lock) => [ "run", "--allow-read", "--allow-env", `--lock=${lock}`, join(REPO_ROOT, "packages/cf-harness/scripts/seed-pattern-index.ts"), "--help", ], }); expect(output.code).toBe(0); expect(new TextDecoder().decode(output.stdout)).toContain( "Usage: deno task seed-pattern-index", ); }); }); describe("publishRequestFor()", () => { const entry = { name: "counter", path: "/repo/packages/patterns/primitives/counter.tsx", metadata: { description: "Counts a number up and down.", hashtags: ["counter"], keywords: ["count"], }, patternId: "abc123", program: { main: "/primitives/counter.tsx", files: [{ name: "/primitives/counter.tsx", contents: "export {};" }], }, argumentSchema: { type: "object" }, resultSchema: { type: "object" }, }; it("publishes under the compiled entry identity and carries the derived metadata", () => { const request = publishRequestFor(entry); expect(request.patternId).toBe("abc123"); expect(request.description).toBe("Counts a number up and down."); expect(request.hashtags).toEqual(["counter"]); expect(request.keywords).toEqual(["count"]); expect(request.program.main).toBe("/primitives/counter.tsx"); }); it("publishes the curated atom discoverably", () => { expect(publishRequestFor(entry).discoverable).toBe(true); }); it("answers no dependencies for an atom that imports no published pattern", () => { expect(publishRequestFor(entry).dependencies).toEqual([]); }); it("reports the published patterns an atom composes", () => { const request = publishRequestFor({ ...entry, program: { main: "/main.tsx", files: [{ name: "/main.tsx", contents: 'import X from "cf:pattern:dep-1";', }], }, }); expect(request.dependencies).toEqual(["dep-1"]); }); }); });