@intentius/chant 0.45.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/core.d.ts +21 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +3 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +14 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +10 -0
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +8 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/config.d.ts +54 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +15 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +223 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +132 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +12 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +4 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/builders.d.ts +134 -8
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/types.d.ts +31 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +11 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +11 -2
- package/src/audit/rules-doc.ts +11 -1
- package/src/build.test.ts +41 -0
- package/src/build.ts +34 -6
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/audit.test.ts +53 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.test.ts +211 -0
- package/src/cli/commands/build.ts +126 -8
- package/src/cli/commands/check-lexicon.test.ts +45 -1
- package/src/cli/commands/check-lexicon.ts +45 -0
- package/src/cli/commands/lexicon-surface-diff.ts +9 -0
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +32 -7
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.ts +4 -4
- package/src/cli/handlers/lifecycle.test.ts +345 -1
- package/src/cli/handlers/lifecycle.ts +300 -11
- package/src/cli/handlers/search.ts +5 -2
- package/src/cli/main.ts +12 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +58 -1
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +7 -0
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/lexicon-regen.ts +19 -1
- package/src/codegen/validate.test.ts +33 -0
- package/src/codegen/validate.ts +21 -2
- package/src/components/auto-release.ts +6 -0
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +8 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/config.test.ts +55 -0
- package/src/config.ts +88 -1
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/fold/subset.test.ts +26 -0
- package/src/fold/subset.ts +45 -19
- package/src/index.ts +4 -0
- package/src/lexicon.ts +227 -2
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/index.ts +3 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/teardown.test.ts +568 -0
- package/src/lifecycle/teardown.ts +359 -0
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.ts +15 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +27 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +8 -1
- package/src/lint/rules/index.ts +10 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/builders.ts +179 -8
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/index.ts +10 -3
- package/src/op/local-executor.ts +172 -25
- package/src/op/op.test.ts +25 -2
- package/src/op/receipt-store.ts +211 -0
- package/src/op/types.ts +33 -1
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +12 -0
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1832 — the generic apply path provably never writes an effect receipt.
|
|
3
|
+
*
|
|
4
|
+
* Appliers consume the serialized build outputs, so the write-exclusion lives
|
|
5
|
+
* at serializer-input assembly (build step 7): entities carrying
|
|
6
|
+
* EFFECT_RECEIPT_MARKER are withheld from the apply-bound entity map and ride
|
|
7
|
+
* `SerializeContext.receipts` instead. These tests drive a real build over a
|
|
8
|
+
* fixture containing a receipt, spy on the serializer (the applier-input
|
|
9
|
+
* boundary), and drive the serialized output through a mock applier to show
|
|
10
|
+
* the receipt reaches neither its desired set nor its prune candidates.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
13
|
+
import { build } from "./build";
|
|
14
|
+
import { splitReceiptEntities, EffectReceipt } from "./effect-receipt";
|
|
15
|
+
import type { Serializer, SerializeContext } from "./serializer";
|
|
16
|
+
import type { Declarable } from "./declarable";
|
|
17
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
|
|
21
|
+
const RECEIPT_FIXTURE = `
|
|
22
|
+
export const bucket = {
|
|
23
|
+
lexicon: "test",
|
|
24
|
+
entityType: "Test::Bucket",
|
|
25
|
+
props: { name: "assets" },
|
|
26
|
+
[Symbol.for("chant.declarable")]: true,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// The shape a lexicon-materialized receipt row has (#1835): its own lexicon
|
|
30
|
+
// and entityType, carrying the effect-receipt marker.
|
|
31
|
+
export const dbMigrated = {
|
|
32
|
+
lexicon: "test",
|
|
33
|
+
entityType: "Test::Receipt",
|
|
34
|
+
name: "dbMigrated",
|
|
35
|
+
effect: "db-migrate",
|
|
36
|
+
flavor: "existence",
|
|
37
|
+
inputs: {},
|
|
38
|
+
[Symbol.for("chant.declarable")]: true,
|
|
39
|
+
[Symbol.for("chant.effect-receipt")]: true,
|
|
40
|
+
};
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
describe("effect-receipt apply write-exclusion (#1832)", () => {
|
|
44
|
+
let testDir: string;
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
testDir = join(tmpdir(), `chant-receipt-excl-${Date.now()}-${Math.random()}`);
|
|
48
|
+
await mkdir(testDir, { recursive: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await rm(testDir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("the serializer's apply-bound entity map never contains the receipt; it rides ctx.receipts", async () => {
|
|
56
|
+
await writeFile(join(testDir, "infra.infra.ts"), RECEIPT_FIXTURE);
|
|
57
|
+
|
|
58
|
+
const seen: Array<{ entities: string[]; receipts: string[] }> = [];
|
|
59
|
+
const serializer: Serializer = {
|
|
60
|
+
name: "test",
|
|
61
|
+
rulePrefix: "TEST",
|
|
62
|
+
serialize: (entities, _outputs, context?: SerializeContext) => {
|
|
63
|
+
seen.push({
|
|
64
|
+
entities: [...entities.keys()],
|
|
65
|
+
receipts: [...(context?.receipts?.keys() ?? [])],
|
|
66
|
+
});
|
|
67
|
+
return JSON.stringify({ resources: [...entities.keys()] });
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = await build(testDir, [serializer]);
|
|
72
|
+
|
|
73
|
+
expect(result.errors).toEqual([]);
|
|
74
|
+
expect(result.entities.has("dbMigrated")).toBe(true); // declared, diffed, observed like any resource
|
|
75
|
+
expect(seen).toHaveLength(1);
|
|
76
|
+
expect(seen[0].entities).toEqual(["bucket"]); // withheld from the apply-bound set
|
|
77
|
+
expect(seen[0].receipts).toEqual(["dbMigrated"]); // still visible to the serializer (#1835)
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a mock-plugin apply driven from the build output never writes the receipt and never prunes it", async () => {
|
|
81
|
+
await writeFile(join(testDir, "infra.infra.ts"), RECEIPT_FIXTURE);
|
|
82
|
+
|
|
83
|
+
// The mock plugin's serializer: apply-bound resources into the document
|
|
84
|
+
// appliers consume; the receipts core withheld into a visibility section
|
|
85
|
+
// OUTSIDE it, the way #1835's materialization renders them.
|
|
86
|
+
const serializer: Serializer = {
|
|
87
|
+
name: "test",
|
|
88
|
+
rulePrefix: "TEST",
|
|
89
|
+
serialize: (entities, _outputs, context?: SerializeContext) =>
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
resources: [...entities.keys()],
|
|
92
|
+
receiptVisibility: [...(context?.receipts?.keys() ?? [])],
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
95
|
+
const result = await build(testDir, [serializer]);
|
|
96
|
+
expect(result.errors).toEqual([]);
|
|
97
|
+
|
|
98
|
+
// The mock plugin's applier — the generic shape every lexicon applier
|
|
99
|
+
// has: desired set from the document's apply-bound section, writes for
|
|
100
|
+
// desired, prune candidates from owned live resources absent from
|
|
101
|
+
// desired minus the receipts the seam declared. The live scope holds the
|
|
102
|
+
// declared bucket, a genuine owned orphan, and the receipt the effect
|
|
103
|
+
// step wrote.
|
|
104
|
+
const writes: string[] = [];
|
|
105
|
+
const pruneCandidates: string[] = [];
|
|
106
|
+
const mockApply = (manifest: string, live: Array<{ name: string; owned: boolean }>): void => {
|
|
107
|
+
const doc = JSON.parse(manifest) as { resources: string[]; receiptVisibility: string[] };
|
|
108
|
+
const desired = new Set<string>(doc.resources);
|
|
109
|
+
const receipts = new Set<string>(doc.receiptVisibility);
|
|
110
|
+
for (const name of desired) writes.push(name);
|
|
111
|
+
for (const r of live) {
|
|
112
|
+
if (r.owned && !desired.has(r.name) && !receipts.has(r.name)) pruneCandidates.push(r.name);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
mockApply(result.outputs.get("test") as string, [
|
|
117
|
+
{ name: "bucket", owned: true },
|
|
118
|
+
{ name: "orphan", owned: true },
|
|
119
|
+
{ name: "dbMigrated", owned: true },
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// The applier was never handed the receipt: it cannot write it.
|
|
123
|
+
expect(writes).toEqual(["bucket"]);
|
|
124
|
+
expect(writes).not.toContain("dbMigrated");
|
|
125
|
+
// The genuine orphan is a prune candidate; the receipt never is.
|
|
126
|
+
expect(pruneCandidates).toEqual(["orphan"]);
|
|
127
|
+
// And the core seam is what made both true: the apply-bound split
|
|
128
|
+
// recognized the receipt by its marker alone.
|
|
129
|
+
const { applyBound } = splitReceiptEntities(result.entities as Map<string, Declarable>);
|
|
130
|
+
expect(applyBound.has("dbMigrated")).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a partition holding only core-factory receipts serializes nothing apply-bound and warns about nothing", async () => {
|
|
134
|
+
// The core factory's receipts live in the "chant" pseudo-lexicon, which
|
|
135
|
+
// has no serializer until a lexicon materializes them (#1835). That is
|
|
136
|
+
// the designed shape, not a missing-serializer warning.
|
|
137
|
+
await writeFile(
|
|
138
|
+
join(testDir, "infra.infra.ts"),
|
|
139
|
+
`
|
|
140
|
+
export const bucket = {
|
|
141
|
+
lexicon: "test",
|
|
142
|
+
entityType: "Test::Bucket",
|
|
143
|
+
props: {},
|
|
144
|
+
[Symbol.for("chant.declarable")]: true,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const migrated = {
|
|
148
|
+
lexicon: "chant",
|
|
149
|
+
entityType: "Chant::EffectReceipt",
|
|
150
|
+
name: "migrated",
|
|
151
|
+
effect: "migrate",
|
|
152
|
+
flavor: "existence",
|
|
153
|
+
inputs: {},
|
|
154
|
+
[Symbol.for("chant.declarable")]: true,
|
|
155
|
+
[Symbol.for("chant.effect-receipt")]: true,
|
|
156
|
+
};
|
|
157
|
+
`,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const serializer: Serializer = {
|
|
161
|
+
name: "test",
|
|
162
|
+
rulePrefix: "TEST",
|
|
163
|
+
serialize: (entities) => JSON.stringify({ resources: [...entities.keys()] }),
|
|
164
|
+
};
|
|
165
|
+
const result = await build(testDir, [serializer]);
|
|
166
|
+
|
|
167
|
+
expect(result.errors).toEqual([]);
|
|
168
|
+
expect(result.warnings.filter((w) => w.includes("chant"))).toEqual([]);
|
|
169
|
+
expect(result.outputs.has("chant")).toBe(false);
|
|
170
|
+
expect((result.outputs.get("test") as string).includes("migrated")).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("splitReceiptEntities partitions a mixed map by the marker alone", () => {
|
|
174
|
+
const receipt = EffectReceipt("r", { effect: "e", flavor: "existence" });
|
|
175
|
+
const plain = {
|
|
176
|
+
lexicon: "test",
|
|
177
|
+
entityType: "Test::Bucket",
|
|
178
|
+
props: {},
|
|
179
|
+
[Symbol.for("chant.declarable")]: true,
|
|
180
|
+
} as unknown as Declarable;
|
|
181
|
+
const { applyBound, receipts } = splitReceiptEntities(
|
|
182
|
+
new Map<string, Declarable>([
|
|
183
|
+
["r", receipt],
|
|
184
|
+
["b", plain],
|
|
185
|
+
]),
|
|
186
|
+
);
|
|
187
|
+
expect([...applyBound.keys()]).toEqual(["b"]);
|
|
188
|
+
expect([...receipts.keys()]).toEqual(["r"]);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
3
|
+
import { join, dirname, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import * as ts from "typescript";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import {
|
|
9
|
+
EffectReceipt,
|
|
10
|
+
isEffectReceipt,
|
|
11
|
+
collectEffectReceipts,
|
|
12
|
+
canonicalJson,
|
|
13
|
+
receiptExpectation,
|
|
14
|
+
resolveReceiptExpectation,
|
|
15
|
+
referenceInputPaths,
|
|
16
|
+
EFFECT_RECEIPT_MARKER,
|
|
17
|
+
EFFECT_RECEIPT_ENTITY_TYPE,
|
|
18
|
+
EFFECT_RECEIPT_FLAVORS,
|
|
19
|
+
EXISTENCE_EXPECTATION,
|
|
20
|
+
type EffectReceiptDeclaration,
|
|
21
|
+
} from "./effect-receipt";
|
|
22
|
+
import { isDeclarable, DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
23
|
+
import { INTRINSIC_MARKER, type Intrinsic } from "./intrinsic";
|
|
24
|
+
import { AttrRef } from "./attrref";
|
|
25
|
+
import { build, partitionByLexicon } from "./build";
|
|
26
|
+
import type { Serializer } from "./serializer";
|
|
27
|
+
import { encodeEntitySet, decodeEntitySet, type EntitySetWire } from "./discovery/entity-wire";
|
|
28
|
+
import { resolveAttrRefs } from "./discovery/resolve";
|
|
29
|
+
import type { LintContext } from "./lint/rule";
|
|
30
|
+
import {
|
|
31
|
+
evl001NonLiteralExpressionRule,
|
|
32
|
+
evl002ControlFlowResourceRule,
|
|
33
|
+
evl003DynamicPropertyAccessRule,
|
|
34
|
+
evl004SpreadNonConstRule,
|
|
35
|
+
evl005ResourceBlockBodyRule,
|
|
36
|
+
evl007InvalidSiblingsRule,
|
|
37
|
+
evl009CompositeNoConstantRule,
|
|
38
|
+
evl010CompositeNoTransformRule,
|
|
39
|
+
} from "./lint/rules";
|
|
40
|
+
|
|
41
|
+
function sha256(canonical: string): string {
|
|
42
|
+
return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fakeIntrinsic(json: unknown): Intrinsic {
|
|
46
|
+
return {
|
|
47
|
+
[INTRINSIC_MARKER]: true,
|
|
48
|
+
toJSON: () => json,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("EffectReceipt — factory", () => {
|
|
53
|
+
test("existence flavor carries name, effect, flavor, inputs", () => {
|
|
54
|
+
const receipt = EffectReceipt("migrated", { effect: "db-migrate", flavor: "existence" });
|
|
55
|
+
expect(receipt.name).toBe("migrated");
|
|
56
|
+
expect(receipt.effect).toBe("db-migrate");
|
|
57
|
+
expect(receipt.flavor).toBe("existence");
|
|
58
|
+
expect(receipt.inputs).toEqual({});
|
|
59
|
+
expect(receipt.lexicon).toBe("chant");
|
|
60
|
+
expect(receipt.entityType).toBe(EFFECT_RECEIPT_ENTITY_TYPE);
|
|
61
|
+
expect(EFFECT_RECEIPT_FLAVORS).toEqual(["existence", "hash"]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("hash flavor records static inputs verbatim, frozen", () => {
|
|
65
|
+
const receipt = EffectReceipt("seeded", {
|
|
66
|
+
effect: "db-seed",
|
|
67
|
+
flavor: "hash",
|
|
68
|
+
inputs: { schema: "v42", tables: ["users", "orders"] },
|
|
69
|
+
});
|
|
70
|
+
expect(receipt.flavor).toBe("hash");
|
|
71
|
+
expect(receipt.inputs).toEqual({ schema: "v42", tables: ["users", "orders"] });
|
|
72
|
+
expect(Object.isFrozen(receipt.inputs)).toBe(true);
|
|
73
|
+
expect(Object.isFrozen((receipt.inputs as { tables: string[] }).tables)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("declared fields are immutable; the object stays extensible for discovery metadata", () => {
|
|
77
|
+
const receipt = EffectReceipt("r", { effect: "e", flavor: "existence" });
|
|
78
|
+
expect(() => {
|
|
79
|
+
(receipt as { effect: string }).effect = "mutated";
|
|
80
|
+
}).toThrow();
|
|
81
|
+
// Discovery stamps symbol-keyed metadata onto entities — that must work.
|
|
82
|
+
const stamp = Symbol.for("chant.test.stamp");
|
|
83
|
+
(receipt as unknown as Record<symbol, unknown>)[stamp] = "ok";
|
|
84
|
+
expect((receipt as unknown as Record<symbol, unknown>)[stamp]).toBe("ok");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("intrinsic inputs are left live (not frozen) so discovery can assign logical names", () => {
|
|
88
|
+
const parent = { name: "bucket" };
|
|
89
|
+
const ref = new AttrRef(parent, "arn");
|
|
90
|
+
EffectReceipt("r", { effect: "e", flavor: "hash", inputs: { target: ref } });
|
|
91
|
+
expect(() => ref._setLogicalName("Bucket")).not.toThrow();
|
|
92
|
+
expect(ref.getLogicalName()).toBe("Bucket");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("rejects an empty name, an empty effect, an unknown flavor, and non-object inputs", () => {
|
|
96
|
+
expect(() => EffectReceipt("", { effect: "e", flavor: "existence" })).toThrow(/name/);
|
|
97
|
+
expect(() => EffectReceipt("r", { effect: "", flavor: "existence" })).toThrow(/effect/);
|
|
98
|
+
expect(() =>
|
|
99
|
+
// @ts-expect-error — the flavor union is closed
|
|
100
|
+
EffectReceipt("r", { effect: "e", flavor: "digest" }),
|
|
101
|
+
).toThrow(/unknown flavor/);
|
|
102
|
+
expect(() =>
|
|
103
|
+
// @ts-expect-error — inputs must be a plain object
|
|
104
|
+
EffectReceipt("r", { effect: "e", flavor: "hash", inputs: ["a"] }),
|
|
105
|
+
).toThrow(/inputs/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a receipt is a Declarable and an EffectReceipt; the guard rejects unmarked shapes", () => {
|
|
109
|
+
const receipt = EffectReceipt("r", { effect: "e", flavor: "existence" });
|
|
110
|
+
expect(isDeclarable(receipt)).toBe(true);
|
|
111
|
+
expect(isEffectReceipt(receipt)).toBe(true);
|
|
112
|
+
expect((receipt as unknown as Record<symbol, unknown>)[EFFECT_RECEIPT_MARKER]).toBe(true);
|
|
113
|
+
expect(isEffectReceipt({ name: "r", effect: "e", flavor: "existence" })).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("the marker recognizes a lexicon-materialized resource core knows nothing about", () => {
|
|
117
|
+
// A lexicon row (#1835) stamps the SAME Symbol.for marker on its own
|
|
118
|
+
// resource — core's guards must recognize it without aws knowledge.
|
|
119
|
+
const materialized = {
|
|
120
|
+
[DECLARABLE_MARKER]: true,
|
|
121
|
+
[EFFECT_RECEIPT_MARKER]: true,
|
|
122
|
+
lexicon: "aws",
|
|
123
|
+
entityType: "AWS::SSM::Parameter",
|
|
124
|
+
kind: "resource",
|
|
125
|
+
props: { Name: "/chant-receipts/app/prod/db-migrate", Type: "String" },
|
|
126
|
+
} as unknown as Declarable;
|
|
127
|
+
expect(isEffectReceipt(materialized)).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("canonicalJson — JCS-style", () => {
|
|
132
|
+
test("sorts object keys and strips whitespace, at every depth", () => {
|
|
133
|
+
expect(canonicalJson({ b: 1, a: { d: 2, c: 3 } })).toBe('{"a":{"c":3,"d":2},"b":1}');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("number forms canonicalize to one encoding", () => {
|
|
137
|
+
expect(canonicalJson({ n: 1.0 })).toBe('{"n":1}');
|
|
138
|
+
expect(canonicalJson({ n: 1e2 })).toBe('{"n":100}');
|
|
139
|
+
expect(canonicalJson({ n: -0 })).toBe('{"n":0}');
|
|
140
|
+
expect(canonicalJson({ n: 1e21 })).toBe('{"n":1e+21}');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("honors toJSON, which is what keeps references in placeholder form", () => {
|
|
144
|
+
const placeholder = fakeIntrinsic({ __attrRef: { entity: "Bucket", attribute: "arn" } });
|
|
145
|
+
expect(canonicalJson({ target: placeholder })).toBe(
|
|
146
|
+
'{"target":{"__attrRef":{"attribute":"arn","entity":"Bucket"}}}',
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("drops undefined object properties; arrays keep order and encode holes as null", () => {
|
|
151
|
+
expect(canonicalJson({ a: 1, b: undefined })).toBe('{"a":1}');
|
|
152
|
+
expect(canonicalJson([1, undefined, 2])).toBe("[1,null,2]");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("throws on non-representable values instead of silently coercing", () => {
|
|
156
|
+
expect(() => canonicalJson({ n: Number.NaN })).toThrow(/non-finite/);
|
|
157
|
+
expect(() => canonicalJson({ n: Number.POSITIVE_INFINITY })).toThrow(/non-finite/);
|
|
158
|
+
expect(() => canonicalJson({ f: () => 1 })).toThrow(/function/);
|
|
159
|
+
expect(() => canonicalJson({ b: 10n })).toThrow(/bigint/);
|
|
160
|
+
const cyclic: Record<string, unknown> = {};
|
|
161
|
+
cyclic.self = cyclic;
|
|
162
|
+
expect(() => canonicalJson(cyclic)).toThrow(/circular/);
|
|
163
|
+
expect(() => canonicalJson(undefined)).toThrow(/not representable/);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("receiptExpectation — static inputs hash at synthesis", () => {
|
|
168
|
+
test("existence flavor is the marker constant, inputs or not", () => {
|
|
169
|
+
expect(receiptExpectation(EffectReceipt("a", { effect: "e", flavor: "existence" }))).toBe(
|
|
170
|
+
EXISTENCE_EXPECTATION,
|
|
171
|
+
);
|
|
172
|
+
expect(
|
|
173
|
+
receiptExpectation(EffectReceipt("b", { effect: "e", flavor: "existence", inputs: { v: 1 } })),
|
|
174
|
+
).toBe(EXISTENCE_EXPECTATION);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("hash flavor digests canonical {effect, inputs} — deterministic across key order and number forms", () => {
|
|
178
|
+
const one = EffectReceipt("r1", {
|
|
179
|
+
effect: "db-migrate",
|
|
180
|
+
flavor: "hash",
|
|
181
|
+
inputs: { schema: "v42", batch: 100 },
|
|
182
|
+
});
|
|
183
|
+
const two = EffectReceipt("r2", {
|
|
184
|
+
effect: "db-migrate",
|
|
185
|
+
flavor: "hash",
|
|
186
|
+
inputs: { batch: 1e2, schema: "v42" },
|
|
187
|
+
});
|
|
188
|
+
expect(receiptExpectation(one)).toBe(receiptExpectation(two));
|
|
189
|
+
expect(receiptExpectation(one)).toBe(
|
|
190
|
+
sha256(canonicalJson({ effect: "db-migrate", inputs: { batch: 100, schema: "v42" } })),
|
|
191
|
+
);
|
|
192
|
+
expect(receiptExpectation(one)).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("the effect name is bound into the digest — same inputs, different effect, different expectation", () => {
|
|
196
|
+
const a = EffectReceipt("a", { effect: "db-migrate", flavor: "hash", inputs: { v: 1 } });
|
|
197
|
+
const b = EffectReceipt("b", { effect: "db-seed", flavor: "hash", inputs: { v: 1 } });
|
|
198
|
+
expect(receiptExpectation(a)).not.toBe(receiptExpectation(b));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("a hash receipt with reference inputs refuses at synthesis — references never resolve here", () => {
|
|
202
|
+
const receipt = EffectReceipt("r", {
|
|
203
|
+
effect: "e",
|
|
204
|
+
flavor: "hash",
|
|
205
|
+
inputs: { static: "s", nested: { target: fakeIntrinsic("live") } },
|
|
206
|
+
});
|
|
207
|
+
expect(referenceInputPaths(receipt)).toEqual(["inputs.nested.target"]);
|
|
208
|
+
expect(() => receiptExpectation(receipt)).toThrow(/inputs\.nested\.target/);
|
|
209
|
+
expect(() => receiptExpectation(receipt)).toThrow(/never at synthesis/);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("resolveReceiptExpectation — the plan/effect-step side of the split", () => {
|
|
214
|
+
test("placeholder and resolved forms differ, and resolution is deterministic", () => {
|
|
215
|
+
const placeholder = fakeIntrinsic({ __attrRef: { entity: "Db", attribute: "endpoint" } });
|
|
216
|
+
const receipt = EffectReceipt("r", {
|
|
217
|
+
effect: "db-migrate",
|
|
218
|
+
flavor: "hash",
|
|
219
|
+
inputs: { endpoint: placeholder, schema: "v42" },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const resolver = (ref: Intrinsic): unknown => {
|
|
223
|
+
expect(ref).toBe(placeholder);
|
|
224
|
+
return "db.prod.internal:5432";
|
|
225
|
+
};
|
|
226
|
+
const resolvedOnce = resolveReceiptExpectation(receipt, resolver);
|
|
227
|
+
const resolvedTwice = resolveReceiptExpectation(receipt, resolver);
|
|
228
|
+
expect(resolvedOnce).toBe(resolvedTwice);
|
|
229
|
+
|
|
230
|
+
// The digest is over RESOLVED inputs — not over the placeholder envelope.
|
|
231
|
+
const placeholderDigest = sha256(
|
|
232
|
+
canonicalJson({ effect: "db-migrate", inputs: receipt.inputs }),
|
|
233
|
+
);
|
|
234
|
+
const expectedDigest = sha256(
|
|
235
|
+
canonicalJson({
|
|
236
|
+
effect: "db-migrate",
|
|
237
|
+
inputs: { endpoint: "db.prod.internal:5432", schema: "v42" },
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
expect(resolvedOnce).toBe(expectedDigest);
|
|
241
|
+
expect(resolvedOnce).not.toBe(placeholderDigest);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("a fully static hash receipt resolves to exactly its synthesis-time expectation", () => {
|
|
245
|
+
const receipt = EffectReceipt("r", { effect: "e", flavor: "hash", inputs: { v: 1 } });
|
|
246
|
+
const neverCalled = (): unknown => {
|
|
247
|
+
throw new Error("resolver must not be called for static inputs");
|
|
248
|
+
};
|
|
249
|
+
expect(resolveReceiptExpectation(receipt, neverCalled)).toBe(receiptExpectation(receipt));
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("existence flavor resolves to the marker constant without consulting the resolver", () => {
|
|
253
|
+
const receipt = EffectReceipt("r", {
|
|
254
|
+
effect: "e",
|
|
255
|
+
flavor: "existence",
|
|
256
|
+
inputs: { ref: fakeIntrinsic("x") },
|
|
257
|
+
});
|
|
258
|
+
const neverCalled = (): unknown => {
|
|
259
|
+
throw new Error("resolver must not be called for an existence receipt");
|
|
260
|
+
};
|
|
261
|
+
expect(resolveReceiptExpectation(receipt, neverCalled)).toBe(EXISTENCE_EXPECTATION);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("resolver answers are validated: undefined and reference-for-reference throw with the path", () => {
|
|
265
|
+
const receipt = EffectReceipt("r", {
|
|
266
|
+
effect: "e",
|
|
267
|
+
flavor: "hash",
|
|
268
|
+
inputs: { a: fakeIntrinsic("x") },
|
|
269
|
+
});
|
|
270
|
+
expect(() => resolveReceiptExpectation(receipt, () => undefined)).toThrow(/inputs\.a/);
|
|
271
|
+
expect(() => resolveReceiptExpectation(receipt, () => fakeIntrinsic("y"))).toThrow(
|
|
272
|
+
/another reference/,
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("references resolve at any depth, including inside arrays", () => {
|
|
277
|
+
const receipt = EffectReceipt("r", {
|
|
278
|
+
effect: "e",
|
|
279
|
+
flavor: "hash",
|
|
280
|
+
inputs: { list: [1, fakeIntrinsic("x"), { deep: fakeIntrinsic("y") }] },
|
|
281
|
+
});
|
|
282
|
+
expect(referenceInputPaths(receipt)).toEqual(["inputs.list[1]", "inputs.list[2].deep"]);
|
|
283
|
+
const byPath: Record<string, unknown> = {
|
|
284
|
+
"inputs.list[1]": "one",
|
|
285
|
+
"inputs.list[2].deep": "two",
|
|
286
|
+
};
|
|
287
|
+
const digest = resolveReceiptExpectation(receipt, (_ref, path) => byPath[path]);
|
|
288
|
+
expect(digest).toBe(
|
|
289
|
+
sha256(canonicalJson({ effect: "e", inputs: { list: [1, "one", { deep: "two" }] } })),
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("marker survives the entity-wire codec (#1828's sandbox-brand mold)", () => {
|
|
295
|
+
test("a receipt round-trips its marker and declared fields through pure JSON", () => {
|
|
296
|
+
const receipt = EffectReceipt("migrated", {
|
|
297
|
+
effect: "db-migrate",
|
|
298
|
+
flavor: "hash",
|
|
299
|
+
inputs: { schema: "v42" },
|
|
300
|
+
});
|
|
301
|
+
const entities = new Map<string, Declarable>([["migrated", receipt]]);
|
|
302
|
+
resolveAttrRefs(entities);
|
|
303
|
+
|
|
304
|
+
const wire = encodeEntitySet(entities);
|
|
305
|
+
expect(() => JSON.parse(JSON.stringify(wire))).not.toThrow();
|
|
306
|
+
|
|
307
|
+
const decoded = decodeEntitySet(JSON.parse(JSON.stringify(wire)) as EntitySetWire);
|
|
308
|
+
const decodedReceipt = decoded.get("migrated");
|
|
309
|
+
expect(decodedReceipt).toBeDefined();
|
|
310
|
+
expect(isEffectReceipt(decodedReceipt)).toBe(true);
|
|
311
|
+
const r = decodedReceipt as unknown as EffectReceiptShape;
|
|
312
|
+
expect(r.name).toBe("migrated");
|
|
313
|
+
expect(r.effect).toBe("db-migrate");
|
|
314
|
+
expect(r.flavor).toBe("hash");
|
|
315
|
+
expect(r.inputs).toEqual({ schema: "v42" });
|
|
316
|
+
// The decoded receipt still hashes to the same expectation.
|
|
317
|
+
expect(receiptExpectation(decodedReceipt as EffectReceiptDeclaration)).toBe(receiptExpectation(receipt));
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
interface EffectReceiptShape {
|
|
321
|
+
name: string;
|
|
322
|
+
effect: string;
|
|
323
|
+
flavor: string;
|
|
324
|
+
inputs: unknown;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe("EVL-clean — a project declaring receipts passes lint", () => {
|
|
329
|
+
const evlRules = [
|
|
330
|
+
evl001NonLiteralExpressionRule,
|
|
331
|
+
evl002ControlFlowResourceRule,
|
|
332
|
+
evl003DynamicPropertyAccessRule,
|
|
333
|
+
evl004SpreadNonConstRule,
|
|
334
|
+
evl005ResourceBlockBodyRule,
|
|
335
|
+
evl007InvalidSiblingsRule,
|
|
336
|
+
evl009CompositeNoConstantRule,
|
|
337
|
+
evl010CompositeNoTransformRule,
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
test("both flavors, static and referenced inputs, produce zero EVL diagnostics", () => {
|
|
341
|
+
const source = `
|
|
342
|
+
import { EffectReceipt } from "@intentius/chant";
|
|
343
|
+
import { dbCluster } from "./db.infra";
|
|
344
|
+
|
|
345
|
+
export const migrated = EffectReceipt("migrated", {
|
|
346
|
+
effect: "db-migrate",
|
|
347
|
+
flavor: "hash",
|
|
348
|
+
inputs: { schema: "v42", endpoint: dbCluster.endpoint },
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
export const seeded = EffectReceipt("seeded", {
|
|
352
|
+
effect: "db-seed",
|
|
353
|
+
flavor: "existence",
|
|
354
|
+
});
|
|
355
|
+
`;
|
|
356
|
+
const sourceFile = ts.createSourceFile("receipts.infra.ts", source, ts.ScriptTarget.Latest, true);
|
|
357
|
+
const context: LintContext = {
|
|
358
|
+
sourceFile,
|
|
359
|
+
entities: [],
|
|
360
|
+
filePath: "receipts.infra.ts",
|
|
361
|
+
lexicon: undefined,
|
|
362
|
+
};
|
|
363
|
+
for (const rule of evlRules) {
|
|
364
|
+
expect(rule.check(context)).toEqual([]);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe("receipts and discovery (the marker identifies, it does not exclude)", () => {
|
|
370
|
+
test("collectEffectReceipts finds receipts in a built project; partitions keep them", async () => {
|
|
371
|
+
const testDir = join(tmpdir(), `chant-effect-receipt-${Date.now()}-${Math.random()}`);
|
|
372
|
+
await mkdir(testDir, { recursive: true });
|
|
373
|
+
try {
|
|
374
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
375
|
+
const modulePath = resolvePath(thisDir, "effect-receipt");
|
|
376
|
+
await writeFile(
|
|
377
|
+
join(testDir, "receipts.infra.ts"),
|
|
378
|
+
`
|
|
379
|
+
import { EffectReceipt } from ${JSON.stringify(modulePath)};
|
|
380
|
+
export const migrated = EffectReceipt("migrated", {
|
|
381
|
+
effect: "db-migrate",
|
|
382
|
+
flavor: "hash",
|
|
383
|
+
inputs: { schema: "v42" },
|
|
384
|
+
});
|
|
385
|
+
export const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
|
|
386
|
+
export const realResource = {
|
|
387
|
+
lexicon: "test",
|
|
388
|
+
entityType: "Test::Thing",
|
|
389
|
+
[Symbol.for("chant.declarable")]: true,
|
|
390
|
+
};
|
|
391
|
+
`,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const testSerializer: Serializer = {
|
|
395
|
+
name: "test",
|
|
396
|
+
rulePrefix: "TEST",
|
|
397
|
+
serialize: (entities) => JSON.stringify([...entities.keys()]),
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const result = await build(testDir, [testSerializer]);
|
|
401
|
+
expect(result.errors).toEqual([]);
|
|
402
|
+
expect(result.entities.size).toBe(3);
|
|
403
|
+
|
|
404
|
+
const receipts = collectEffectReceipts(result.entities);
|
|
405
|
+
expect([...receipts.keys()].sort()).toEqual(["migrated", "seeded"]);
|
|
406
|
+
expect(receipts.get("migrated")!.effect).toBe("db-migrate");
|
|
407
|
+
expect(receiptExpectation(receipts.get("migrated")!)).toMatch(/^sha256:/);
|
|
408
|
+
|
|
409
|
+
// Unlike secret declarations (#1828), receipts are NOT excluded from
|
|
410
|
+
// partitioning — they become real resources via a lexicon row (#1835),
|
|
411
|
+
// so the partition for their lexicon keeps them.
|
|
412
|
+
const partitions = partitionByLexicon(result.entities);
|
|
413
|
+
expect(partitions.get("chant")).toBeDefined();
|
|
414
|
+
expect([...partitions.get("chant")!.keys()].sort()).toEqual(["migrated", "seeded"]);
|
|
415
|
+
} finally {
|
|
416
|
+
await rm(testDir, { recursive: true, force: true });
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
});
|