import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { build, partitionByLexicon, detectCrossLexiconRefs, collectLexiconOutputs, computeStackGraph } from "./build"; import { output } from "./lexicon-output"; import { AttrRef } from "./attrref"; import { INTRINSIC_MARKER } from "./intrinsic"; import type { Serializer } from "./serializer"; import type { Declarable } from "./declarable"; import { DECLARABLE_MARKER } from "./declarable"; import { mkdir, writeFile, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; describe("build", () => { let testDir: string; beforeEach(async () => { testDir = join(tmpdir(), `chant-build-test-${Date.now()}-${Math.random()}`); await mkdir(testDir, { recursive: true }); }); afterEach(async () => { await rm(testDir, { recursive: true, force: true }); }); test("builds empty directory successfully", async () => { const mockSerializer: Serializer = { name: "test", rulePrefix: "TEST", serialize: (_entities) => "serialized output", }; const result = await build(testDir, [mockSerializer]); expect(result.outputs.size).toBe(0); expect(result.entities.size).toBe(0); expect(result.warnings).toEqual([]); expect(result.errors).toEqual([]); }); test("discovers and builds entities with single lexicon", async () => { // Create a test infrastructure file with a simple entity const infraFile = join(testDir, "test.infra.ts"); await writeFile( infraFile, ` export const testEntity = { lexicon: "test", entityType: "TestEntity", [Symbol.for("chant.declarable")]: true, }; ` ); const mockSerializer: Serializer = { name: "test", rulePrefix: "TEST", serialize: (_entities) => "serialized output", }; const result = await build(testDir, [mockSerializer]); expect(result.outputs.size).toBe(1); expect(result.outputs.get("test")).toBe("serialized output"); expect(result.entities.size).toBe(1); expect(result.entities.has("testEntity")).toBe(true); expect(result.errors.length).toBe(0); expect(result.foldDecisions).toEqual([]); }); test("#1022 — --fold folds a leaf-only module and surfaces foldDecisions", async () => { const { dirname, resolve: resolvePath } = await import("node:path"); const { fileURLToPath } = await import("node:url"); const thisDir = dirname(fileURLToPath(import.meta.url)); const runtimePath = resolvePath(thisDir, "runtime"); await writeFile( join(testDir, "resources.ts"), ` import { createResource } from ${JSON.stringify(runtimePath)}; export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" }); ` ); await writeFile( join(testDir, "main.ts"), ` import { Bucket } from "./resources"; throw new Error("must never execute — sentinel for #1022 fold verification"); export const bucket = new Bucket({ name: "my-bucket" }); ` ); const mockSerializer: Serializer = { name: "aws", rulePrefix: "TEST", serialize: (entities) => JSON.stringify([...entities.keys()]), }; const result = await build(testDir, [mockSerializer], undefined, { fold: true }); expect(result.errors).toEqual([]); expect(result.entities.size).toBe(1); expect(result.entities.has("bucket")).toBe(true); const mainDecision = result.foldDecisions.find((d) => d.file.endsWith("main.ts")); expect(mainDecision?.mode).toBe("fold"); }); test("handles discovery errors", async () => { // Create a test file with syntax error const infraFile = join(testDir, "broken.infra.ts"); await writeFile(infraFile, "this is not valid typescript {{{"); const mockSerializer: Serializer = { name: "test", rulePrefix: "TEST", serialize: (_entities) => "serialized output", }; const result = await build(testDir, [mockSerializer]); expect(result.entities.size).toBe(0); expect(result.errors.length).toBeGreaterThan(0); }); test("handles circular dependencies", async () => { // Create test file with circular entity dependencies (not import cycles) await writeFile( join(testDir, "entities.ts"), ` export const entity1 = { lexicon: "test", entityType: "TestEntity", [Symbol.for("chant.declarable")]: true, }; export const entity2 = { lexicon: "test", entityType: "TestEntity", [Symbol.for("chant.declarable")]: true, ref1: entity1, }; // Create circular reference after declaration entity1.ref2 = entity2; ` ); const mockSerializer: Serializer = { name: "test", rulePrefix: "TEST", serialize: (_entities) => "serialized output", }; const result = await build(testDir, [mockSerializer]); expect(result.outputs.get("test")).toBe("serialized output"); // Should have a BuildError for circular dependency detected by topological sort const hasCircularError = result.errors.some( (err) => err.name === "BuildError" && err.message.includes("Circular dependency") ); expect(hasCircularError).toBe(true); }); test("calls serializer.serialize()", async () => { let serializeCalled = false; const infraFile = join(testDir, "test.infra.ts"); await writeFile( infraFile, ` export const testEntity = { lexicon: "test", entityType: "TestEntity", [Symbol.for("chant.declarable")]: true, }; ` ); const mockSerializer: Serializer = { name: "test", rulePrefix: "TEST", serialize: (_entities) => { serializeCalled = true; return "custom serialization"; }, }; const result = await build(testDir, [mockSerializer]); expect(serializeCalled).toBe(true); expect(result.outputs.get("test")).toBe("custom serialization"); }); test("two-lexicon project produces two outputs", async () => { const infraFile = join(testDir, "multi.infra.ts"); await writeFile( infraFile, ` export const alphaEntity = { lexicon: "alpha", entityType: "Bucket", [Symbol.for("chant.declarable")]: true, }; export const betaEntity = { lexicon: "beta", entityType: "Storage", [Symbol.for("chant.declarable")]: true, }; ` ); const alphaSerializer: Serializer = { name: "alpha", rulePrefix: "ALPHA", serialize: (entities) => JSON.stringify({ alpha: Array.from(entities.keys()) }), }; const betaSerializer: Serializer = { name: "beta", rulePrefix: "BETA", serialize: (entities) => JSON.stringify({ beta: Array.from(entities.keys()) }), }; const result = await build(testDir, [alphaSerializer, betaSerializer]); expect(result.outputs.size).toBe(2); expect(result.outputs.has("alpha")).toBe(true); expect(result.outputs.has("beta")).toBe(true); const alphaRaw = result.outputs.get("alpha")!; const alphaOutput = JSON.parse(typeof alphaRaw === "string" ? alphaRaw : alphaRaw.primary); expect(alphaOutput.alpha).toContain("alphaEntity"); const betaRaw = result.outputs.get("beta")!; const betaOutput = JSON.parse(typeof betaRaw === "string" ? betaRaw : betaRaw.primary); expect(betaOutput.beta).toContain("betaEntity"); }); test("warns when no serializer found for a lexicon", async () => { const infraFile = join(testDir, "test.infra.ts"); await writeFile( infraFile, ` export const testEntity = { lexicon: "unknown", entityType: "TestEntity", [Symbol.for("chant.declarable")]: true, }; ` ); const result = await build(testDir, []); expect(result.warnings.length).toBeGreaterThan(0); expect(result.warnings[0]).toContain('No serializer found for lexicon "unknown"'); }); }); describe("build-root contributors (#1548 piece 3)", () => { let testDir: string; beforeEach(async () => { testDir = join(tmpdir(), `chant-buildroots-test-${Date.now()}-${Math.random()}`); await mkdir(testDir, { recursive: true }); }); afterEach(async () => { await rm(testDir, { recursive: true, force: true }); }); const entity = (entityType: string): Declarable => ({ lexicon: "test", entityType, kind: "resource", props: {}, [DECLARABLE_MARKER]: true, }) as unknown as Declarable; const recordingSerializer = (seen: string[][]): Serializer => ({ name: "test", rulePrefix: "TEST", serialize: (entities) => { seen.push([...entities.keys()]); return "out"; }, }); test("contributed entities join the entity set and reach the serializer", async () => { const seen: string[][] = []; const result = await build(testDir, [recordingSerializer(seen)], undefined, { buildRoots: [ async () => ({ entities: new Map([["overlays/prod/deploymentApp", entity("TestEntity")]]), warnings: ["rendered a stray doc"], }), ], }); expect(result.errors).toEqual([]); expect(result.entities.has("overlays/prod/deploymentApp")).toBe(true); expect(seen).toEqual([["overlays/prod/deploymentApp"]]); expect(result.warnings).toContain("rendered a stray doc"); }); test("a contributor that throws becomes a build error carrying its message, not a thrown stack", async () => { const result = await build(testDir, [], undefined, { buildRoots: [ async () => { throw new Error("neither the `kustomize` binary nor `kubectl` was found on PATH"); }, ], }); expect(result.errors).toHaveLength(1); expect(result.errors[0].message).toContain("neither the `kustomize` binary nor `kubectl`"); }); test("a contributed name colliding with a discovered entity is a build error, never an overwrite", async () => { await writeFile( join(testDir, "app.infra.ts"), ` export const discovered = { lexicon: "test", entityType: "Discovered", props: {}, [Symbol.for("chant.declarable")]: true, }; `, ); const result = await build(testDir, [recordingSerializer([])], undefined, { buildRoots: [async () => ({ entities: new Map([["discovered", entity("Contributed")]]) })], }); expect(result.errors.some((e) => e.message.includes('"discovered" collides'))).toBe(true); expect(result.entities.get("discovered")?.entityType).toBe("Discovered"); }); test("contributors run exactly once per build", async () => { let calls = 0; await build(testDir, [], undefined, { buildRoots: [ async () => { calls++; return { entities: new Map() }; }, ], }); expect(calls).toBe(1); }); }); describe("partitionByLexicon", () => { test("partitions entities by lexicon", () => { const entities = new Map([ [ "bucket", { lexicon: "alpha", entityType: "Bucket", [DECLARABLE_MARKER]: true } as Declarable, ], [ "storage", { lexicon: "beta", entityType: "Storage", [DECLARABLE_MARKER]: true } as Declarable, ], [ "handler", { lexicon: "alpha", entityType: "Function", [DECLARABLE_MARKER]: true } as Declarable, ], ]); const partitions = partitionByLexicon(entities); expect(partitions.size).toBe(2); expect(partitions.get("alpha")!.size).toBe(2); expect(partitions.get("beta")!.size).toBe(1); expect(partitions.get("alpha")!.has("bucket")).toBe(true); expect(partitions.get("alpha")!.has("handler")).toBe(true); expect(partitions.get("beta")!.has("storage")).toBe(true); }); test("single lexicon produces one partition", () => { const entities = new Map([ [ "bucket", { lexicon: "alpha", entityType: "Bucket", [DECLARABLE_MARKER]: true } as Declarable, ], [ "handler", { lexicon: "alpha", entityType: "Function", [DECLARABLE_MARKER]: true } as Declarable, ], ]); const partitions = partitionByLexicon(entities); expect(partitions.size).toBe(1); expect(partitions.get("alpha")!.size).toBe(2); }); test("empty entities produces empty partitions", () => { const entities = new Map(); const partitions = partitionByLexicon(entities); expect(partitions.size).toBe(0); }); test("property-kind entities partition by their own lexicon", () => { const entities = new Map([ [ "bucket", { lexicon: "alpha", entityType: "Bucket", [DECLARABLE_MARKER]: true } as Declarable, ], [ "bucketPolicy", { lexicon: "alpha", entityType: "BucketPolicy", kind: "property", [DECLARABLE_MARKER]: true, } as Declarable, ], ]); const partitions = partitionByLexicon(entities); expect(partitions.size).toBe(1); expect(partitions.get("alpha")!.size).toBe(2); expect(partitions.get("alpha")!.has("bucketPolicy")).toBe(true); }); }); describe("detectCrossLexiconRefs", () => { test("cross-lexicon AttrRef is auto-detected without explicit output()", () => { const alphaBucket = { lexicon: "alpha", entityType: "Alpha::Storage::Bucket", [DECLARABLE_MARKER]: true, } as Declarable; const bucketEndpoint = new AttrRef(alphaBucket, "Endpoint"); const ghAction = { lexicon: "github", entityType: "Action", [DECLARABLE_MARKER]: true, props: { url: bucketEndpoint }, } as unknown as Declarable; const entities = new Map([ ["dataBucket", alphaBucket], ["deployAction", ghAction], ]); const detected = detectCrossLexiconRefs(entities); expect(detected).toHaveLength(1); expect(detected[0].sourceLexicon).toBe("alpha"); expect(detected[0].sourceEntity).toBe("dataBucket"); expect(detected[0].sourceAttribute).toBe("Endpoint"); expect(detected[0].outputName).toBe("dataBucketEndpoint"); expect(detected[0].outputName).toMatch(/^[A-Za-z0-9]+$/); }); test("explicit output() overrides auto-detected name", () => { const alphaBucket = { lexicon: "alpha", entityType: "Alpha::Storage::Bucket", [DECLARABLE_MARKER]: true, } as Declarable; const bucketArn = new AttrRef(alphaBucket, "Arn"); const explicitOutput = output(bucketArn, "MyCustomArnName"); const ghAction = { lexicon: "github", entityType: "Action", [DECLARABLE_MARKER]: true, props: { arn: bucketArn, out: explicitOutput }, } as unknown as Declarable; const entities = new Map([ ["dataBucket", alphaBucket], ["deployAction", ghAction], ]); // Auto-detect finds the cross-lexicon ref const autoDetected = detectCrossLexiconRefs(entities); expect(autoDetected).toHaveLength(1); expect(autoDetected[0].outputName).toBe("dataBucketArn"); // But when collecting explicit outputs, the explicit one is found const explicitOutputs = collectLexiconOutputs(entities); expect(explicitOutputs).toHaveLength(1); expect(explicitOutputs[0].outputName).toBe("MyCustomArnName"); // Merge logic: explicit wins (same parent object + attribute) const explicitRefs = explicitOutputs.map((o: { _sourceParent: WeakRef | null; sourceAttribute: string | null }) => ({ parent: o._sourceParent?.deref(), attribute: o.sourceAttribute, })); const merged = [ ...explicitOutputs, ...autoDetected.filter((auto) => { const autoParent = auto._sourceParent?.deref(); return !explicitRefs.some( (e: { parent: object | undefined; attribute: string | null }) => e.parent === autoParent && e.attribute === auto.sourceAttribute ); }), ]; expect(merged).toHaveLength(1); expect(merged[0].outputName).toBe("MyCustomArnName"); }); test("same-lexicon AttrRef is NOT auto-detected", () => { const alphaBucket = { lexicon: "alpha", entityType: "Alpha::Storage::Bucket", [DECLARABLE_MARKER]: true, } as Declarable; const bucketArn = new AttrRef(alphaBucket, "Arn"); const alphaFunction = { lexicon: "alpha", entityType: "Alpha::Compute::Function", [DECLARABLE_MARKER]: true, props: { bucketArn }, } as unknown as Declarable; const entities = new Map([ ["dataBucket", alphaBucket], ["handler", alphaFunction], ]); const detected = detectCrossLexiconRefs(entities); expect(detected).toHaveLength(0); }); test("intrinsic-based output() is passed to every lexicon's serializer", () => { const alphaEntity = { lexicon: "alpha", entityType: "Alpha::Resource", [DECLARABLE_MARKER]: true, } as Declarable; const mockIntrinsic = { [INTRINSIC_MARKER]: true as const, toJSON: () => ({ "Fn::Sub": "http://example.com/path" }), }; const intrinsicOutput = output(mockIntrinsic, "MyUrl"); const entities = new Map([ ["alphaEntity", alphaEntity], ["myUrl", intrinsicOutput as unknown as Declarable], ]); const collected = collectLexiconOutputs(entities); expect(collected).toHaveLength(1); expect(collected[0].outputName).toBe("MyUrl"); expect(collected[0].sourceLexicon).toBe(""); expect(collected[0].getOutputValue()).toEqual({ "Fn::Sub": "http://example.com/path" }); }); // chant #1121 — `output(, name)`, exactly the // shape of a top-level `export const oParamName = output(data.Name, // "oParamName")`, must reach the serializer as a plain `Value`, never a // `Fn::GetAtt` pointing at the output's own logical id. test("literal-valued output() emits its value verbatim, not a self-referencing Fn::GetAtt", () => { const literalOutput = output("fold-output-repro", "oParamName"); const entities = new Map([ ["oParamName", literalOutput as unknown as Declarable], ]); const collected = collectLexiconOutputs(entities); expect(collected).toHaveLength(1); expect(collected[0].outputName).toBe("oParamName"); expect(collected[0].sourceEntity).toBe(""); expect(collected[0].sourceAttribute).toBeNull(); expect(collected[0].getOutputValue()).toBe("fold-output-repro"); }); test("deduplicates when same cross-lexicon ref appears in multiple entities", () => { const alphaBucket = { lexicon: "alpha", entityType: "Alpha::Storage::Bucket", [DECLARABLE_MARKER]: true, } as Declarable; const bucketEndpoint = new AttrRef(alphaBucket, "Endpoint"); const ghAction1 = { lexicon: "github", entityType: "Action", [DECLARABLE_MARKER]: true, props: { url: bucketEndpoint }, } as unknown as Declarable; const ghAction2 = { lexicon: "github", entityType: "Action", [DECLARABLE_MARKER]: true, props: { endpoint: bucketEndpoint }, } as unknown as Declarable; const entities = new Map([ ["dataBucket", alphaBucket], ["action1", ghAction1], ["action2", ghAction2], ]); const detected = detectCrossLexiconRefs(entities); expect(detected).toHaveLength(1); expect(detected[0].outputName).toBe("dataBucketEndpoint"); }); // chant#930 — a nested Fn::GetAtt attribute path (dots) on a cross-lexicon // ref must not leak into the auto-generated Output logical id: CFN logical // ids (including Outputs keys) are alphanumeric-only (^[A-Za-z0-9]+$), and // cfn-lint (E6001) rejects anything else. test("auto-generated Output logical id is valid CFN form for a nested attribute path", () => { const alphaBucket = { lexicon: "alpha", entityType: "Alpha::Storage::Bucket", [DECLARABLE_MARKER]: true, } as Declarable; const nestedRef = new AttrRef( alphaBucket, "MetadataConfiguration.AnnotationTableConfiguration.TableArn" ); const ghAction = { lexicon: "github", entityType: "Action", [DECLARABLE_MARKER]: true, props: { arn: nestedRef }, } as unknown as Declarable; const entities = new Map([ ["foundationArtifactBucket", alphaBucket], ["deployAction", ghAction], ]); const detected = detectCrossLexiconRefs(entities); expect(detected).toHaveLength(1); expect(detected[0].outputName).toMatch(/^[A-Za-z0-9]+$/); expect(detected[0].outputName).toBe( "foundationArtifactBucketMetadataConfigurationAnnotationTableConfigurationTableArn" ); }); // chant#959 — referencing a WHOLE resource object (e.g. `Ref(bucket)`, which // embeds the resource) must not harvest the resource's latent per-attribute // AttrRefs as cross-lexicon outputs. Every resource instance materializes an // AttrRef for every attribute in its spec (runtime.ts); config-gated ones // (S3 `WebsiteURL`, `MetadataConfiguration.*`) don't exist at deploy time // unless the matching config block is set, so emitting `Fn::GetAtt` outputs // for them makes real CloudFormation reject the template. Only an // explicitly-accessed attribute (a standalone AttrRef value) should output. test("does not harvest a whole resource's latent attribute refs (Ref of a resource)", () => { // A resource that carries materialized AttrRef accessors for every attribute, // exactly as runtime.ts builds them — including config-gated ones. const bucket = { lexicon: "aws", entityType: "AWS::S3::Bucket", [DECLARABLE_MARKER]: true, } as unknown as Record; for (const attr of ["Arn", "DomainName", "WebsiteURL", "MetadataConfiguration.InventoryTableConfiguration.TableName"]) { bucket[attr.replace(/\W/g, "")] = new AttrRef(bucket as unknown as Declarable, attr); } // An output entity (no lexicon) that references the whole bucket object, // the way `output(Ref(bucket), "oArtifactBucket")` does — a Ref intrinsic // whose value is the resource object itself. const refIntrinsic = { [INTRINSIC_MARKER]: true as const, value: bucket, toJSON: () => ({ Ref: "foundationArtifactBucket" }), }; const bucketOutput = { entityType: "Output", [DECLARABLE_MARKER]: true, props: { ref: refIntrinsic }, } as unknown as Declarable; const entities = new Map([ ["foundationArtifactBucket", bucket as unknown as Declarable], ["oArtifactBucket", bucketOutput], ]); const detected = detectCrossLexiconRefs(entities); expect(detected).toHaveLength(0); }); // chant #1137 — `detectCrossLexiconRefs`'s walk used to check // `value instanceof AttrRef`, which returns false for an AttrRef built by // a SEPARATELY-LOADED copy of `./attrref` (the same dual-npm-copy hazard // #1122 fixed for `LexiconOutput`: a lexicon pinned to a chant range that // doesn't overlap the project's own gets its own nested // `node_modules/@intentius/chant`). `vi.resetModules()` + a fresh dynamic // import reproduces that split module graph exactly. Before the fix, a // foreign AttrRef here falls through to the generic object walk instead // of being recognized, and the auto-detected `Outputs` entry vanishes // silently — no error, just a missing cross-lexicon output. test("detects a cross-lexicon ref built by a second, separately-loaded copy of AttrRef", async () => { const alphaBucket = { lexicon: "alpha", entityType: "Alpha::Storage::Bucket", [DECLARABLE_MARKER]: true, } as Declarable; vi.resetModules(); const secondCopy = await import("./attrref"); // Sanity check that this really is a distinct module instance — the // premise the rest of the test depends on. expect(secondCopy.AttrRef).not.toBe(AttrRef); const foreignRef = new secondCopy.AttrRef(alphaBucket, "Endpoint"); // The historic bug: instanceof fails across separately-loaded copies of // chant-core, even though the two classes are structurally identical. expect(foreignRef instanceof AttrRef).toBe(false); const ghAction = { lexicon: "github", entityType: "Action", [DECLARABLE_MARKER]: true, props: { url: foreignRef }, } as unknown as Declarable; const entities = new Map([ ["dataBucket", alphaBucket], ["deployAction", ghAction], ]); // The fix: `isAttrRefLike` duck-types on shape, so a foreign-copy // AttrRef is still recognized and auto-detected as a cross-lexicon output. const detected = detectCrossLexiconRefs(entities); expect(detected).toHaveLength(1); expect(detected[0].sourceLexicon).toBe("alpha"); expect(detected[0].sourceEntity).toBe("dataBucket"); expect(detected[0].sourceAttribute).toBe("Endpoint"); vi.resetModules(); }); }); describe("computeStackGraph (#200 — cross-stack apply ordering)", () => { const ent = (lexicon: string, props: Record = {}): Declarable => ({ lexicon, entityType: `${lexicon}::X`, [DECLARABLE_MARKER]: true, props }) as unknown as Declarable; test("infers a consumer→producer edge from a cross-lexicon AttrRef", () => { const vpc = ent("aws"); const svc = ent("k8s", { vpcId: new AttrRef(vpc, "id") }); const g = computeStackGraph(new Map([["vpc", vpc], ["svc", svc]]), ["aws", "k8s"]); expect(g.edges).toEqual([{ from: "k8s", to: "aws" }]); expect(g.order).toEqual(["aws", "k8s"]); // producer before consumer expect(g.waves).toEqual([["aws"], ["k8s"]]); // separate waves — ordered expect(g.cycles).toEqual([]); }); test("independent stacks share a wave (parallel-safe)", () => { const g = computeStackGraph(new Map([["a", ent("aws")], ["b", ent("gcp")]]), ["aws", "gcp"]); expect(g.edges).toEqual([]); expect(g.waves).toEqual([["aws", "gcp"]]); // one wave — no inter-dependency }); test("reports a dependency cycle", () => { const a = ent("x"); const b = ent("y", { ref: new AttrRef(a, "out") }); // close the loop: a references b (a as unknown as { props: Record }).props = { ref: new AttrRef(b, "out") }; const g = computeStackGraph(new Map([["a", a], ["b", b]]), ["x", "y"]); expect(g.edges).toEqual(expect.arrayContaining([{ from: "x", to: "y" }, { from: "y", to: "x" }])); expect(g.cycles).toEqual([["x", "y"]]); expect(g.order).toEqual([]); // nothing is orderable inside a cycle }); test("a diamond resolves into three waves", () => { // base ← (left, right) ← top const base = ent("base"); const left = ent("left", { b: new AttrRef(base, "id") }); const right = ent("right", { b: new AttrRef(base, "id") }); const top = ent("top", { l: new AttrRef(left, "id"), r: new AttrRef(right, "id") }); const g = computeStackGraph( new Map([["base", base], ["left", left], ["right", right], ["top", top]]), ["base", "left", "right", "top"], ); expect(g.waves).toEqual([["base"], ["left", "right"], ["top"]]); }); // chant #1137 — same dual-npm-copy hazard as detectCrossLexiconRefs above, // this time for the cross-stack apply-ordering graph: a foreign-copy // AttrRef that fails `instanceof` here used to fall through to the // generic object walk instead of producing an edge, silently dropping a // real cross-stack dependency (which can misorder — or fail to detect a // cycle in — the apply order this graph exists to compute). test("infers a consumer→producer edge from an AttrRef built by a second, separately-loaded copy", async () => { const vpc = ent("aws"); vi.resetModules(); const secondCopy = await import("./attrref"); expect(secondCopy.AttrRef).not.toBe(AttrRef); const foreignRef = new secondCopy.AttrRef(vpc, "id"); expect(foreignRef instanceof AttrRef).toBe(false); // the historic bug const svc = ent("k8s", { vpcId: foreignRef }); const g = computeStackGraph(new Map([["vpc", vpc], ["svc", svc]]), ["aws", "k8s"]); expect(g.edges).toEqual([{ from: "k8s", to: "aws" }]); expect(g.order).toEqual(["aws", "k8s"]); vi.resetModules(); }); });