// Patch-Operation tests: verify add/replace/remove on a SourceFile-in- // memory. Strategy: build a minimal feature-file as input, apply the // change, parse the output, assert structural equivalence to the // expected pattern set. The test trusts parse + render (both already // pinned by parse.test.ts + render-roundtrip.test.ts) so a failure // here narrows the cause to patch.ts itself. import { describe, expect, test } from "bun:test"; import { resolve } from "node:path"; import { Project, type SourceFile } from "ts-morph"; import { parseSourceFile } from "../parse"; import { addPattern, applyChanges, type PatternId, removePattern, replacePattern } from "../patch"; import { createFeaturePatcher } from "../patcher"; import type { FeaturePattern } from "../patterns"; const STARTER = ` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; defineFeature("inventory", (r) => { r.entity({ name: "item", fields: { name: { type: "text", required: true } } }); r.metric({ name: "created", type: "counter" }); }); `; function makeSourceFile(content: string): SourceFile { const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true, useInMemoryFileSystem: true, }); return project.createSourceFile(`f-${Math.random()}.ts`, content); } const SAMPLE_LOC = { file: "x", start: { line: 1, column: 1 }, end: { line: 1, column: 1 }, raw: "", }; const newSecretPattern: FeaturePattern = { kind: "secret", source: SAMPLE_LOC, shortName: "stripeKey", options: { description: "Stripe API key" } as never, }; const replacementMetric: FeaturePattern = { kind: "metric", source: SAMPLE_LOC, shortName: "created", options: { type: "histogram" } as never, }; describe("addPattern — appends in canonical Object-Form", () => { test("appends a new r.* call at the end of the setup callback", () => { const sf = makeSourceFile(STARTER); addPattern(sf, newSecretPattern); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); expect(reparsed.patterns.length).toBe(3); const lastPattern = reparsed.patterns[2]; expect(lastPattern?.kind).toBe("secret"); if (lastPattern?.kind === "secret") { expect(lastPattern.shortName).toBe("stripeKey"); } }); test("appended call is in canonical Object-Form (single object arg)", () => { const sf = makeSourceFile(STARTER); addPattern(sf, newSecretPattern); const text = sf.getFullText(); expect(text).toMatch(/r\.secret\(\{\s+name:\s*"stripeKey"/); }); test("first add into an empty setup callback", () => { const empty = ` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; defineFeature("blank", (r) => { }); `; const sf = makeSourceFile(empty); addPattern(sf, newSecretPattern); const reparsed = parseSourceFile(sf); expect(reparsed.patterns.length).toBe(1); expect(reparsed.patterns[0]?.kind).toBe("secret"); }); }); describe("replacePattern — substitutes the matching call", () => { test("replaces an entity by name with a new entity definition", () => { const sf = makeSourceFile(STARTER); const id: PatternId = { kind: "entity", entityName: "item" }; const replacement: FeaturePattern = { kind: "entity", source: SAMPLE_LOC, entityName: "item", definition: { fields: { name: { type: "text", required: true }, sku: { type: "text" }, }, } as never, }; replacePattern(sf, id, replacement); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); const entity = reparsed.patterns.find((p) => p.kind === "entity"); expect(entity).toMatchObject({ kind: "entity", entityName: "item", }); if (entity?.kind === "entity") { const fields = (entity.definition as { fields: Record }).fields; expect(Object.keys(fields)).toEqual(["name", "sku"]); } }); test("replaces a metric by short name", () => { const sf = makeSourceFile(STARTER); const id: PatternId = { kind: "metric", shortName: "created" }; replacePattern(sf, id, replacementMetric); const reparsed = parseSourceFile(sf); const metric = reparsed.patterns.find((p) => p.kind === "metric"); expect(metric).toMatchObject({ kind: "metric", shortName: "created" }); if (metric?.kind === "metric") { expect(metric.options).toMatchObject({ type: "histogram" }); } }); test("throws when no call matches the id", () => { const sf = makeSourceFile(STARTER); const id: PatternId = { kind: "entity", entityName: "doesNotExist" }; expect(() => replacePattern(sf, id, { ...replacementMetric, kind: "entity", entityName: "doesNotExist", definition: { fields: {} } as never, } as FeaturePattern), ).toThrow(/no call found/); }); }); describe("removePattern — deletes the matching call cleanly", () => { test("removes the metric call entirely", () => { const sf = makeSourceFile(STARTER); const id: PatternId = { kind: "metric", shortName: "created" }; removePattern(sf, id); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); expect(reparsed.patterns.find((p) => p.kind === "metric")).toBeUndefined(); expect(reparsed.patterns.length).toBe(1); }); test("add → remove leaves the file with the same patterns as before", () => { const sf = makeSourceFile(STARTER); const before = parseSourceFile(sf).patterns.map((p) => p.kind); addPattern(sf, newSecretPattern); removePattern(sf, { kind: "secret", shortName: "stripeKey" }); const after = parseSourceFile(sf).patterns.map((p) => p.kind); expect(after).toEqual(before); }); test("throws when no call matches the id", () => { const sf = makeSourceFile(STARTER); expect(() => removePattern(sf, { kind: "entity", entityName: "ghost" })).toThrow( /no call found/, ); }); }); describe("applyChanges — bulk operations in order", () => { test("processes add + replace + remove in order", () => { const sf = makeSourceFile(STARTER); applyChanges(sf, [ { op: "add", pattern: newSecretPattern }, { op: "replace", id: { kind: "metric", shortName: "created" }, pattern: replacementMetric, }, { op: "remove", id: { kind: "entity", entityName: "item" } }, ]); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); const kinds = reparsed.patterns.map((p) => p.kind).sort(); expect(kinds).toEqual(["metric", "secret"]); const metric = reparsed.patterns.find((p) => p.kind === "metric"); if (metric?.kind === "metric") { expect(metric.options).toMatchObject({ type: "histogram" }); } }); }); describe("custom-code survival — patches don't disturb non-r.* code", () => { test("helpers, comments, imports between calls remain unchanged", () => { const featureWithCustomCode = ` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; import { z } from "zod"; const SHARED_HELPER = "computed-at-module-init"; defineFeature("inventory", (r) => { // Top-of-feature comment — must survive patches. const localConst = 42; r.entity({ name: "item", fields: { name: { type: "text", required: true } } }); // Comment between entity and metric — must survive add operations. r.metric({ name: "created", type: "counter" }); }); function unrelatedHelper() { return SHARED_HELPER; } `; const sf = makeSourceFile(featureWithCustomCode); addPattern(sf, newSecretPattern); const text = sf.getFullText(); expect(text).toContain("// Top-of-feature comment"); expect(text).toContain("const localConst = 42;"); expect(text).toContain("// Comment between entity and metric"); expect(text).toContain("function unrelatedHelper()"); expect(text).toContain('SHARED_HELPER = "computed-at-module-init"'); }); }); describe("patch coverage for the remaining pattern-kinds", () => { test("add + remove via PatternId for httpRoute (method+path key)", () => { const sf = makeSourceFile(STARTER); const patcher = createFeaturePatcher(sf); patcher.addHttpRoute({ method: "GET", path: "/health", handlerSource: "async (c) => c.json({ ok: true })", }); expect(parseSourceFile(sf).patterns).toHaveLength(3); removePattern(sf, { kind: "httpRoute", method: "GET", path: "/health" }); const after = parseSourceFile(sf); expect(after.patterns.find((p) => p.kind === "httpRoute")).toBeUndefined(); }); test("add + remove via PatternId for projection (name key)", () => { const sf = makeSourceFile(STARTER); createFeaturePatcher(sf).addProjection({ name: "summary", sourceEntity: "item", applySources: { "todo:event:created": "async (event, ctx) => {}" }, }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "projection")).toBeDefined(); removePattern(sf, { kind: "projection", name: "summary" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "projection")).toBeUndefined(); }); test("add + remove via PatternId for multiStreamProjection", () => { const sf = makeSourceFile(STARTER); createFeaturePatcher(sf).addMultiStreamProjection({ name: "tenantSummary", applySources: { "todo:event:created": "async (event, ctx) => {}" }, }); removePattern(sf, { kind: "multiStreamProjection", name: "tenantSummary" }); expect( parseSourceFile(sf).patterns.find((p) => p.kind === "multiStreamProjection"), ).toBeUndefined(); }); test("add + remove via PatternId for useExtension (name+entity key)", () => { const sf = makeSourceFile(STARTER); createFeaturePatcher(sf).addUseExtension({ extension: "auditLog", entity: "item" }); removePattern(sf, { kind: "useExtension", extensionName: "auditLog", entityName: "item", }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "useExtension")).toBeUndefined(); }); test("add + remove via PatternId for notification (name key)", () => { const sf = makeSourceFile(STARTER); createFeaturePatcher(sf).addNotification({ name: "itemCreated", trigger: { on: "item" }, recipientSource: "async (event, ctx) => []", dataSource: "async (event, ctx) => ({})", }); removePattern(sf, { kind: "notification", notificationName: "itemCreated" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "notification")).toBeUndefined(); }); test("add + remove via PatternId for authClaims (singleton key)", () => { const sf = makeSourceFile(STARTER); createFeaturePatcher(sf).addAuthClaims({ handlerSource: 'async (user, ctx) => ({ teamId: "t1" })', }); removePattern(sf, { kind: "authClaims" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "authClaims")).toBeUndefined(); }); test("add + remove via PatternId for defineEvent with migrations", () => { const sf = makeSourceFile(STARTER); createFeaturePatcher(sf).addDefineEvent({ name: "itemCreated", schemaSource: "z.object({ id: z.string() })", version: 2, migrations: { "1": "(old) => old" }, }); removePattern(sf, { kind: "defineEvent", eventName: "itemCreated" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "defineEvent")).toBeUndefined(); }); test("add + remove via PatternId for nav (id key)", () => { const sf = makeSourceFile(STARTER); createFeaturePatcher(sf).addNav({ definition: { id: "items", label: "Items", screen: "inventory:screen:list" }, }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "nav")).toBeDefined(); removePattern(sf, { kind: "nav", id: "items" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "nav")).toBeUndefined(); }); test("add + remove via PatternId for workspace (id key)", () => { const sf = makeSourceFile(STARTER); createFeaturePatcher(sf).addWorkspace({ definition: { id: "admin", label: "Admin" } }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "workspace")).toBeDefined(); removePattern(sf, { kind: "workspace", id: "admin" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "workspace")).toBeUndefined(); }); test("add + remove via PatternId for extendsRegistrar (extensionName key)", () => { const sf = makeSourceFile(STARTER); addPattern(sf, { kind: "extendsRegistrar", source: SAMPLE_LOC, extensionName: "auditLog", defBody: { ...SAMPLE_LOC, raw: "{ hooks: {} }" }, }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "extendsRegistrar")).toBeDefined(); removePattern(sf, { kind: "extendsRegistrar", extensionName: "auditLog" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "extendsRegistrar")).toBeUndefined(); }); }); describe("singleton-pattern guards", () => { test("findCallForId throws when a singleton-kind appears twice", () => { const file = ` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; defineFeature("dup", (r) => { r.systemScope(); r.systemScope(); }); `; const sf = makeSourceFile(file); expect(() => removePattern(sf, { kind: "systemScope" })).toThrow(/singleton/); }); }); describe("legacy positional → canonical Object-Form on replace", () => { test("a positional-form r.entity call is replaced with object-form", () => { const legacyFile = ` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; defineFeature("legacy", (r) => { r.entity("item", { fields: { name: { type: "text", required: true } } }); }); `; const sf = makeSourceFile(legacyFile); const id: PatternId = { kind: "entity", entityName: "item" }; const replacement: FeaturePattern = { kind: "entity", source: SAMPLE_LOC, entityName: "item", definition: { fields: { name: { type: "text", required: true }, code: { type: "text" } }, } as never, }; replacePattern(sf, id, replacement); const text = sf.getFullText(); expect(text).toMatch(/r\.entity\(\{\s+name: "item",/); expect(text).not.toContain('r.entity("item"'); }); }); describe("addPattern — error paths", () => { test("throws when no defineFeature call is present", () => { const sf = makeSourceFile("export const x = 1;"); expect(() => addPattern(sf, newSecretPattern)).toThrow(/no defineFeature/); }); }); describe("callMatchesId — positional and object-form lookups", () => { const LEGACY = ` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; defineFeature("legacy", (r) => { r.relation("task", "owner", { kind: "belongsTo", to: "user" }); r.hook("postSave", "task", () => {}); r.hook("preDelete", { allOf: "task" }, () => {}); r.job("nightly", { trigger: { cron: "0 0 * * *" } }, async () => {}); r.writeHandler("task:create", z.object({}), async () => ({})); r.queryHandler("task:list", z.object({}), async () => []); r.notification("taskCreated", { trigger: { on: "task" }, recipient: () => ({}), data: () => ({}), }); }); `; test("removePattern finds a positional relation by entity+name", () => { const sf = makeSourceFile(LEGACY); removePattern(sf, { kind: "relation", entityName: "task", relationName: "owner" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "relation")).toBeUndefined(); }); test("removePattern finds a positional hook by type+string target", () => { const sf = makeSourceFile(LEGACY); removePattern(sf, { kind: "hook", hookType: "postSave", target: "task" }); expect(parseSourceFile(sf).patterns.filter((p) => p.kind === "hook")).toHaveLength(1); }); test("removePattern finds a positional hook with an allOf target", () => { const sf = makeSourceFile(LEGACY); removePattern(sf, { kind: "hook", hookType: "preDelete", target: { allOf: "task" } }); expect(parseSourceFile(sf).patterns.filter((p) => p.kind === "hook")).toHaveLength(1); }); test("removePattern finds a positional job by name", () => { const sf = makeSourceFile(LEGACY); removePattern(sf, { kind: "job", jobName: "nightly" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "job")).toBeUndefined(); }); test("removePattern finds a writeHandler by positional name", () => { const sf = makeSourceFile(LEGACY); removePattern(sf, { kind: "writeHandler", handlerName: "task:create" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "writeHandler")).toBeUndefined(); }); test("removePattern finds a queryHandler by positional name", () => { const sf = makeSourceFile(LEGACY); removePattern(sf, { kind: "queryHandler", handlerName: "task:list" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "queryHandler")).toBeUndefined(); }); test("removePattern finds a positional notification by name", () => { const sf = makeSourceFile(LEGACY); removePattern(sf, { kind: "notification", notificationName: "taskCreated" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "notification")).toBeUndefined(); }); }); describe("removePattern — blank-line collapse", () => { test("removing an appended pattern collapses the preceding blank line", () => { const sf = makeSourceFile(STARTER); addPattern(sf, newSecretPattern); const linesBefore = sf.getFullText().split("\n").length; removePattern(sf, { kind: "secret", shortName: "stripeKey" }); const linesAfter = sf.getFullText().split("\n").length; expect(linesAfter).toBeLessThan(linesBefore); expect(parseSourceFile(sf).patterns.map((p) => p.kind)).toEqual(["entity", "metric"]); }); }); describe("replacePattern — object-form hook with allOf target", () => { test("replaces an object-form hook registered with an allOf target", () => { const file = ` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; defineFeature("hooks", (r) => { r.hook({ type: "postSave", target: { allOf: "task" }, handler: () => {} }); }); `; const sf = makeSourceFile(file); replacePattern( sf, { kind: "hook", hookType: "postSave", target: { allOf: "task" } }, { kind: "hook", source: SAMPLE_LOC, hookType: "postSave", target: { allOf: "task" }, fnBody: { ...SAMPLE_LOC, raw: "() => { /* updated */ }" }, }, ); expect(sf.getFullText()).toContain("/* updated */"); }); }); describe("callMatchesId — object-form lookups", () => { const OBJECT_FORM = ` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; defineFeature("object", (r) => { r.relation({ entity: "task", name: "owner", kind: "belongsTo", to: "user" }); r.job({ name: "nightly", trigger: { cron: "0 0 * * *" }, handler: async () => {} }); r.useExtension({ name: "audit", entity: "task" }); r.writeHandler({ name: "task:create", schema: z.object({}), handler: async () => ({}), }); r.notification({ name: "taskCreated", trigger: { on: "task" }, recipient: () => ({}), data: () => ({}), }); }); `; test("removePattern finds an object-form relation", () => { const sf = makeSourceFile(OBJECT_FORM); removePattern(sf, { kind: "relation", entityName: "task", relationName: "owner" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "relation")).toBeUndefined(); }); test("removePattern finds an object-form job", () => { const sf = makeSourceFile(OBJECT_FORM); removePattern(sf, { kind: "job", jobName: "nightly" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "job")).toBeUndefined(); }); test("removePattern finds an object-form useExtension", () => { const sf = makeSourceFile(OBJECT_FORM); removePattern(sf, { kind: "useExtension", extensionName: "audit", entityName: "task" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "useExtension")).toBeUndefined(); }); test("removePattern finds an object-form writeHandler", () => { const sf = makeSourceFile(OBJECT_FORM); removePattern(sf, { kind: "writeHandler", handlerName: "task:create" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "writeHandler")).toBeUndefined(); }); test("removePattern finds an object-form notification", () => { const sf = makeSourceFile(OBJECT_FORM); removePattern(sf, { kind: "notification", notificationName: "taskCreated" }); expect(parseSourceFile(sf).patterns.find((p) => p.kind === "notification")).toBeUndefined(); }); }); // #2121 — findCallForId only accepted a StringLiteral first argument, so // it couldn't locate calls authored with an imported/local constant name // (the dominant style in the framework's own bundled-features, per #1746 // on the parser side). Covers both the single-arg case (matchFirstArgString) // and the two-arg case (relation/hook/useExtension's second positional arg), // same-file and genuinely cross-file via a real filesystem Project. describe("findCallForId resolves identifier-authored names (#2121)", () => { function loadFixture(relPath: string): SourceFile { const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true, }); return project.addSourceFileAtPath(resolve(__dirname, relPath)); } const SAME_FILE_CONST = ` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; const ENTITY = "task"; defineFeature("inventory", (r) => { r.entity(ENTITY, { fields: { name: { type: "text", required: true } } }); }); `; test("removePattern finds a call whose name arg is a same-file const identifier", () => { const sf = makeSourceFile(SAME_FILE_CONST); removePattern(sf, { kind: "entity", entityName: "task" }); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); expect(reparsed.patterns).toEqual([]); }); test("replacePattern finds a call whose name arg is a same-file const identifier", () => { const sf = makeSourceFile(SAME_FILE_CONST); const replacement: FeaturePattern = { kind: "entity", source: SAMPLE_LOC, entityName: "task", definition: { fields: { name: { type: "text", required: true }, sku: { type: "text" } }, } as never, }; replacePattern(sf, { kind: "entity", entityName: "task" }, replacement); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); const entity = reparsed.patterns.find((p) => p.kind === "entity"); if (entity?.kind === "entity") { const fields = (entity.definition as { fields: Record }).fields; expect(Object.keys(fields)).toEqual(["name", "sku"]); } else { throw new Error("expected an entity pattern"); } }); test("does not match when the identifier resolves to a different string", () => { const sf = makeSourceFile(SAME_FILE_CONST); expect(() => removePattern(sf, { kind: "entity", entityName: "other" })).toThrow( /no call found/, ); }); test("removePattern finds an extendsRegistrar call authored with a cross-file imported constant (#1746 fixture)", () => { const sf = loadFixture("fixtures/cross-file-name-const/feature.ts"); removePattern(sf, { kind: "extendsRegistrar", extensionName: "audit" }); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); expect(reparsed.patterns).toEqual([]); }); test("removePattern resolves both positional args when authored as cross-file imported constants", () => { const sf = loadFixture("fixtures/cross-file-patch-const/feature.ts"); removePattern(sf, { kind: "useExtension", extensionName: "tenant-data", entityName: "item", }); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); expect(reparsed.patterns).toEqual([]); }); test("removePattern finds an object-form useExtension whose entity is an inline { name } ref", () => { const sf = makeSourceFile(` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; defineFeature("inventory", (r) => { r.useExtension({ name: "audit", entity: { name: "item" } }); }); `); removePattern(sf, { kind: "useExtension", extensionName: "audit", entityName: "item" }); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); expect(reparsed.patterns).toEqual([]); }); }); // #2133 — matchArgString (shared by relation's second positional arg, // hook's target, and useExtension's entity) only accepted a string literal // or a name-resolving identifier. The parser itself is more permissive at // two of those positions: useExtension's entity (round3.ts:445) and hook's // target (hooks.ts:75) both additionally accept an inline `{ name: "..." }` // object ref, same as the object-form fix in #2121 — so findCallForId // couldn't locate a call authored that way. relation's second positional // arg stays narrow on purpose: round2.ts:170 parses it via readNameLiteral, // not readNameOrRef, so widening it would exceed what the parser accepts. describe("callMatchesId — positional inline-ref args (#2133)", () => { test("removePattern finds a positional useExtension whose entity is an inline { name } ref", () => { const sf = makeSourceFile(` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; defineFeature("inventory", (r) => { r.useExtension("audit", { name: "item" }); }); `); removePattern(sf, { kind: "useExtension", extensionName: "audit", entityName: "item" }); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); expect(reparsed.patterns).toEqual([]); }); test("removePattern finds a positional hook whose target is an inline { name } ref", () => { const sf = makeSourceFile(` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; defineFeature("hooks", (r) => { r.hook("postSave", { name: "task" }, () => {}); }); `); removePattern(sf, { kind: "hook", hookType: "postSave", target: "task" }); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); expect(reparsed.patterns).toEqual([]); }); // Boundary, not a gap left open by this fix: readDataLiteralNode (used by // readNameOrRef's object-literal branch) keeps a nested Identifier as a // RawRefSentinel instead of resolving it — same as the parser side, which // is why this call fails to extract too (not just to patch-match). test("does not resolve an identifier nested inside an inline-ref positional arg", () => { const sf = makeSourceFile(` import { defineFeature } from "@cosmicdrift/kumiko-framework/engine"; const ENTITY_NAME = "item"; defineFeature("inventory", (r) => { r.useExtension("audit", { name: ENTITY_NAME }); }); `); expect(() => removePattern(sf, { kind: "useExtension", extensionName: "audit", entityName: "item" }), ).toThrow(/no call found/); }); }); describe("ai step patterns — shorthand stepKey (#1491)", () => { const AI_STEP_FIXTURE = ` import { defineFeature, defineWorkflow, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine"; import { z } from "zod"; const paramsSchema = z.object({}); const stepKey = "generate"; defineFeature("ai-demo", (r) => { defineWorkflow({ name: "demo", steps: stepsPipeline(() => [ aiGenerateStep({ stepKey, promptKey: "demo:generate", promptFallback: "Write.", paramsSchema, defaults: { enabled: true, params: {} }, input: (ctx) => ctx.event.label, }), ]), }); }); `; test("removePattern finds ai.generate with shorthand stepKey", () => { const sf = makeSourceFile(AI_STEP_FIXTURE); removePattern(sf, { kind: "ai.generate", stepKey: "generate" }); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); expect(reparsed.patterns.filter((p) => p.kind.startsWith("ai."))).toEqual([]); }); test("replacePattern finds ai.generate with shorthand stepKey", () => { const sf = makeSourceFile(AI_STEP_FIXTURE); const replacement: FeaturePattern = { kind: "ai.generate", source: SAMPLE_LOC, stepKey: "generate", promptKey: "demo:generate", promptFallback: "Updated.", defaults: { enabled: true, params: {} }, paramsSchemaSource: { ...SAMPLE_LOC, raw: "paramsSchema" }, inputBody: { ...SAMPLE_LOC, raw: "(ctx) => ctx.event.label" }, }; replacePattern(sf, { kind: "ai.generate", stepKey: "generate" }, replacement); const reparsed = parseSourceFile(sf); expect(reparsed.errors).toEqual([]); expect(reparsed.patterns).toContainEqual( expect.objectContaining({ kind: "ai.generate", stepKey: "generate", promptFallback: "Updated.", }), ); expect(sf.getFullText()).toContain("defineWorkflow"); }); });