@intentius/chant 0.49.0 → 0.50.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/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +9 -0
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +6 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +11 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/verbs/index.d.ts +6 -1
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +21 -14
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +93 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +4 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +29 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +42 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +14 -0
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +18 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/serializer.d.ts +18 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.ts +9 -0
- package/src/audit/discover.ts +29 -2
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +2 -0
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.ts +8 -3
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +215 -1
- package/src/cli/commands/audit.ts +86 -17
- package/src/cli/commands/build.test.ts +167 -2
- package/src/cli/commands/build.ts +114 -23
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +10 -1
- package/src/cli/handlers/lifecycle.ts +12 -4
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/server.test.ts +265 -2
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/registry.ts +14 -2
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/verbs/index.ts +6 -1
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/composite.ts +31 -2
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +82 -19
- package/src/fold/subset.ts +79 -41
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +103 -2
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +4 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +45 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/evl001-non-literal-expression.test.ts +8 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +6 -6
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +59 -5
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +30 -0
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +45 -9
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +18 -1
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/serializer.ts +18 -0
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step-output reference tests (chant #1290).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors activity-contract.test.ts's shape: a small lifecycleDiff-shaped
|
|
5
|
+
* contract so the test doesn't need a real lexicon.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { activity, phase, effect } from "./builders";
|
|
11
|
+
import type { OpConfig } from "./types";
|
|
12
|
+
import { activityContract, type ActivityContract } from "./activity-contract";
|
|
13
|
+
import { stepOutput, isStepOutputRef, collectStepOutputRefs, validateStepOutputRefs } from "./step-output-ref";
|
|
14
|
+
import { EffectReceipt } from "../effect-receipt";
|
|
15
|
+
|
|
16
|
+
const lifecycleDiffContract = activityContract(
|
|
17
|
+
"lifecycleDiff",
|
|
18
|
+
z.strictObject({ env: z.string(), live: z.boolean().optional() }),
|
|
19
|
+
z.object({ output: z.string(), exitCode: z.number(), drifted: z.boolean(), driftedStacks: z.array(z.string()) }),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const applyStacksContract = activityContract("applyStacks", z.strictObject({ stacks: z.array(z.string()) }));
|
|
23
|
+
|
|
24
|
+
// A producer with no `returns` schema — every arg is fine, nothing to
|
|
25
|
+
// validate a reference into its output against.
|
|
26
|
+
const noReturnContract = activityContract("shellCmd", z.strictObject({ cmd: z.string() }));
|
|
27
|
+
|
|
28
|
+
function opWith(config: Partial<OpConfig> & Pick<OpConfig, "phases">): Pick<OpConfig, "name" | "phases" | "onFailure"> {
|
|
29
|
+
return { name: "test-op", ...config };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── stepOutput() / isStepOutputRef() ────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
describe("stepOutput()", () => {
|
|
35
|
+
it("brands its result so isStepOutputRef recognizes it", () => {
|
|
36
|
+
const ref = stepOutput("diff", "driftedStacks");
|
|
37
|
+
expect(isStepOutputRef(ref)).toBe(true);
|
|
38
|
+
expect(ref.step).toBe("diff");
|
|
39
|
+
expect(ref.path).toBe("driftedStacks");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("omitting path references the whole return value", () => {
|
|
43
|
+
const ref = stepOutput("diff");
|
|
44
|
+
expect(ref.path).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("accepts a step object with an id, not just a bare string", () => {
|
|
48
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
49
|
+
expect(stepOutput(diff, "driftedStacks")).toEqual(stepOutput("diff", "driftedStacks"));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("throws when the step has no id", () => {
|
|
53
|
+
expect(() => stepOutput({}, "x")).toThrow(/no `id`/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("isStepOutputRef rejects a plain object with the same shape", () => {
|
|
57
|
+
expect(isStepOutputRef({ step: "diff", path: "x" })).toBe(false);
|
|
58
|
+
expect(isStepOutputRef(null)).toBe(false);
|
|
59
|
+
expect(isStepOutputRef(undefined)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("throws when coerced to a primitive — references only, no expressions", () => {
|
|
63
|
+
const ref = stepOutput("diff", "count");
|
|
64
|
+
expect(() => `${ref}`).toThrow(/coerced to a primitive/);
|
|
65
|
+
expect(() => Number(ref)).toThrow(/coerced to a primitive/);
|
|
66
|
+
expect(() => String(ref)).toThrow(/coerced to a primitive/);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ── activity()'s `.out` proxy ───────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
describe("activity().out", () => {
|
|
73
|
+
it("diff.out.field builds a reference to this step's declared output", () => {
|
|
74
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
75
|
+
const ref = diff.out.driftedStacks;
|
|
76
|
+
expect(isStepOutputRef(ref)).toBe(true);
|
|
77
|
+
expect(ref.step).toBe("diff");
|
|
78
|
+
expect(ref.path).toBe("driftedStacks");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("diff.out itself (unaccessed) references the whole return value", () => {
|
|
82
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
83
|
+
expect(isStepOutputRef(diff.out)).toBe(true);
|
|
84
|
+
expect(diff.out.path).toBeUndefined();
|
|
85
|
+
expect(diff.out.step).toBe("diff");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("throws when the step has no id", () => {
|
|
89
|
+
const diff = activity("lifecycleDiff", { env: "prod" });
|
|
90
|
+
expect(() => diff.out).toThrow(/has no id/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("out is non-enumerable — doesn't leak into the step's own JSON shape", () => {
|
|
94
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
95
|
+
expect(Object.keys(diff)).not.toContain("out");
|
|
96
|
+
expect(JSON.stringify(diff)).not.toContain("out");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("string third arg is still a profile, unchanged (backward compatible)", () => {
|
|
100
|
+
const step = activity("kubectlApply", { manifest: "k8s.yaml" }, "longInfra");
|
|
101
|
+
expect(step.profile).toBe("longInfra");
|
|
102
|
+
expect(step.id).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── collectStepOutputRefs() ─────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
describe("collectStepOutputRefs()", () => {
|
|
109
|
+
it("finds a ref nested inside an object and an array", () => {
|
|
110
|
+
const ref1 = stepOutput("diff", "a");
|
|
111
|
+
const ref2 = stepOutput("diff", "b");
|
|
112
|
+
const found = collectStepOutputRefs({ config: { stacks: [ref1, "literal", ref2] } });
|
|
113
|
+
expect(found).toEqual([ref1, ref2]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("returns nothing for plain args", () => {
|
|
117
|
+
expect(collectStepOutputRefs({ env: "prod", n: 1, list: [1, 2] })).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── validateStepOutputRefs() ─────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe("validateStepOutputRefs() — passing Ops", () => {
|
|
124
|
+
it("a same-phase-later-step reference against a valid path passes", () => {
|
|
125
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
126
|
+
const apply = activity("applyStacks", { stacks: diff.out.driftedStacks });
|
|
127
|
+
const config = opWith({ phases: [phase("Reconcile", [diff, apply])] });
|
|
128
|
+
const contracts = new Map<string, ActivityContract>([
|
|
129
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
130
|
+
["applyStacks", applyStacksContract],
|
|
131
|
+
]);
|
|
132
|
+
expect(validateStepOutputRefs(config, contracts)).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("a reference from a later phase passes", () => {
|
|
136
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
137
|
+
const apply = activity("applyStacks", { stacks: diff.out.driftedStacks });
|
|
138
|
+
const config = opWith({ phases: [phase("Diff", [diff]), phase("Apply", [apply])] });
|
|
139
|
+
const contracts = new Map<string, ActivityContract>([
|
|
140
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
141
|
+
["applyStacks", applyStacksContract],
|
|
142
|
+
]);
|
|
143
|
+
expect(validateStepOutputRefs(config, contracts)).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("a whole-value reference (no path) passes even without checking the producer's return shape", () => {
|
|
147
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
148
|
+
const apply = activity("applyStacks", { stacks: stepOutput("diff") });
|
|
149
|
+
const config = opWith({ phases: [phase("P", [diff, apply])] });
|
|
150
|
+
const contracts = new Map<string, ActivityContract>([
|
|
151
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
152
|
+
["applyStacks", applyStacksContract],
|
|
153
|
+
]);
|
|
154
|
+
expect(validateStepOutputRefs(config, contracts)).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("no refs anywhere — no issues", () => {
|
|
158
|
+
const config = opWith({ phases: [phase("P", [activity("lifecycleDiff", { env: "prod" })])] });
|
|
159
|
+
expect(validateStepOutputRefs(config, new Map())).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("validateStepOutputRefs() — the four failure classes from #1290", () => {
|
|
164
|
+
it("flags a reference to an unknown producer step id", () => {
|
|
165
|
+
const apply = activity("applyStacks", { stacks: stepOutput("nope", "driftedStacks") });
|
|
166
|
+
const config = opWith({ phases: [phase("P", [apply])] });
|
|
167
|
+
const issues = validateStepOutputRefs(config, new Map([["applyStacks", applyStacksContract]]));
|
|
168
|
+
expect(issues.some((i) => i.message.includes('unknown step id "nope"'))).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("flags a path that doesn't exist on the producer's declared return schema", () => {
|
|
172
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
173
|
+
const apply = activity("applyStacks", { stacks: stepOutput("diff", "notAField") });
|
|
174
|
+
const config = opWith({ phases: [phase("P", [diff, apply])] });
|
|
175
|
+
const contracts = new Map<string, ActivityContract>([
|
|
176
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
177
|
+
["applyStacks", applyStacksContract],
|
|
178
|
+
]);
|
|
179
|
+
const issues = validateStepOutputRefs(config, contracts);
|
|
180
|
+
expect(issues.some((i) => i.message.includes('path "notAField"') && i.message.includes("does not exist"))).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("flags a reference to a later step", () => {
|
|
184
|
+
const apply = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
|
|
185
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
186
|
+
const config = opWith({ phases: [phase("Apply", [apply]), phase("Diff", [diff])] });
|
|
187
|
+
const contracts = new Map<string, ActivityContract>([
|
|
188
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
189
|
+
["applyStacks", applyStacksContract],
|
|
190
|
+
]);
|
|
191
|
+
const issues = validateStepOutputRefs(config, contracts);
|
|
192
|
+
expect(issues.some((i) => i.message.includes("later phase"))).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("flags a reference into a contract-less producer", () => {
|
|
196
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
197
|
+
const apply = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
|
|
198
|
+
const config = opWith({ phases: [phase("P", [diff, apply])] });
|
|
199
|
+
// No contract registered for lifecycleDiff at all.
|
|
200
|
+
const issues = validateStepOutputRefs(config, new Map([["applyStacks", applyStacksContract]]));
|
|
201
|
+
expect(issues.some((i) => i.message.includes("no registered activity contract"))).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("validateStepOutputRefs() — structural coverage", () => {
|
|
206
|
+
it("flags a self-reference", () => {
|
|
207
|
+
const step = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
208
|
+
step.args = { env: "prod", x: stepOutput("diff", "drifted") };
|
|
209
|
+
const config = opWith({ phases: [phase("P", [step])] });
|
|
210
|
+
const issues = validateStepOutputRefs(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
211
|
+
expect(issues.some((i) => i.message.includes("does not run before this step"))).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("flags a reference to a step in the same parallel phase", () => {
|
|
215
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
216
|
+
const apply = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
|
|
217
|
+
const config = opWith({ phases: [phase("P", [diff, apply], { parallel: true })] });
|
|
218
|
+
const contracts = new Map<string, ActivityContract>([
|
|
219
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
220
|
+
["applyStacks", applyStacksContract],
|
|
221
|
+
]);
|
|
222
|
+
const issues = validateStepOutputRefs(config, contracts);
|
|
223
|
+
expect(issues.some((i) => i.message.includes("same parallel phase"))).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("flags a duplicate step id", () => {
|
|
227
|
+
const diff1 = activity("lifecycleDiff", { env: "a" }, { id: "diff" });
|
|
228
|
+
const diff2 = activity("lifecycleDiff", { env: "b" }, { id: "diff" });
|
|
229
|
+
const apply = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
|
|
230
|
+
const config = opWith({ phases: [phase("P", [diff1, diff2, apply])] });
|
|
231
|
+
const contracts = new Map<string, ActivityContract>([
|
|
232
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
233
|
+
["applyStacks", applyStacksContract],
|
|
234
|
+
]);
|
|
235
|
+
const issues = validateStepOutputRefs(config, contracts);
|
|
236
|
+
expect(issues.some((i) => i.message.includes("used by more than one step"))).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("flags a producer contract with no returns schema", () => {
|
|
240
|
+
const cmd = activity("shellCmd", { cmd: "true" }, { id: "cmd" });
|
|
241
|
+
const apply = activity("applyStacks", { stacks: stepOutput("cmd", "x") });
|
|
242
|
+
const config = opWith({ phases: [phase("P", [cmd, apply])] });
|
|
243
|
+
const contracts = new Map<string, ActivityContract>([
|
|
244
|
+
["shellCmd", noReturnContract],
|
|
245
|
+
["applyStacks", applyStacksContract],
|
|
246
|
+
]);
|
|
247
|
+
const issues = validateStepOutputRefs(config, contracts);
|
|
248
|
+
expect(issues.some((i) => i.message.includes("declares no return schema"))).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("flags a reference authored inside onFailure", () => {
|
|
252
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
253
|
+
const rollback = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
|
|
254
|
+
const config = opWith({ phases: [phase("P", [diff])], onFailure: [phase("Rollback", [rollback])] });
|
|
255
|
+
const contracts = new Map<string, ActivityContract>([
|
|
256
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
257
|
+
["applyStacks", applyStacksContract],
|
|
258
|
+
]);
|
|
259
|
+
const issues = validateStepOutputRefs(config, contracts);
|
|
260
|
+
expect(issues.some((i) => i.message.includes("onFailure compensation"))).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("flags a reference authored inside an effect step's nested steps", () => {
|
|
264
|
+
const receipt = EffectReceipt("seeded", { effect: "db-seed", flavor: "hash", inputs: { file: "seed.sql", version: 3 } });
|
|
265
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
266
|
+
const nested = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
|
|
267
|
+
const effectStep = effect(receipt, [nested]);
|
|
268
|
+
const config = opWith({ phases: [phase("P", [diff, effectStep])] });
|
|
269
|
+
const contracts = new Map<string, ActivityContract>([
|
|
270
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
271
|
+
["applyStacks", applyStacksContract],
|
|
272
|
+
]);
|
|
273
|
+
const issues = validateStepOutputRefs(config, contracts);
|
|
274
|
+
expect(issues.some((i) => i.message.includes("nested inside an effect step"))).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("reports every issue across multiple refs, not just the first", () => {
|
|
278
|
+
const apply1 = activity("applyStacks", { stacks: stepOutput("nope1", "x") });
|
|
279
|
+
const apply2 = activity("applyStacks", { stacks: stepOutput("nope2", "x") });
|
|
280
|
+
const config = opWith({ phases: [phase("P", [apply1, apply2])] });
|
|
281
|
+
const issues = validateStepOutputRefs(config, new Map([["applyStacks", applyStacksContract]]));
|
|
282
|
+
expect(issues.some((i) => i.message.includes("nope1"))).toBe(true);
|
|
283
|
+
expect(issues.some((i) => i.message.includes("nope2"))).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ── validateStepOutputRefs() — cross-contract primitive-type check (#1950-3) ──
|
|
288
|
+
|
|
289
|
+
describe("validateStepOutputRefs() — cross-contract type compatibility", () => {
|
|
290
|
+
const takeCountContract = activityContract("takeCount", z.strictObject({ count: z.number() }));
|
|
291
|
+
|
|
292
|
+
it("flags a string-returning path fed into a number-typed arg", () => {
|
|
293
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
294
|
+
const consume = activity("takeCount", { count: stepOutput("diff", "output") });
|
|
295
|
+
const config = opWith({ phases: [phase("P", [diff, consume])] });
|
|
296
|
+
const contracts = new Map<string, ActivityContract>([
|
|
297
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
298
|
+
["takeCount", takeCountContract],
|
|
299
|
+
]);
|
|
300
|
+
const issues = validateStepOutputRefs(config, contracts);
|
|
301
|
+
expect(issues.some((i) => i.message.includes("type mismatch") && i.message.includes("string") && i.message.includes("number"))).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("a matching type (array of strings into array of strings) passes", () => {
|
|
305
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
306
|
+
const apply = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
|
|
307
|
+
const config = opWith({ phases: [phase("P", [diff, apply])] });
|
|
308
|
+
const contracts = new Map<string, ActivityContract>([
|
|
309
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
310
|
+
["applyStacks", applyStacksContract],
|
|
311
|
+
]);
|
|
312
|
+
expect(validateStepOutputRefs(config, contracts)).toEqual([]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("bails silently when the consumer has no registered contract", () => {
|
|
316
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
317
|
+
const consume = activity("takeCount", { count: stepOutput("diff", "output") });
|
|
318
|
+
const config = opWith({ phases: [phase("P", [diff, consume])] });
|
|
319
|
+
// No contract registered for "takeCount" — nothing to compare the consumer's arg type against.
|
|
320
|
+
const issues = validateStepOutputRefs(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
321
|
+
expect(issues.some((i) => i.message.includes("type mismatch"))).toBe(false);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("bails silently on a whole-value reference (no path)", () => {
|
|
325
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
326
|
+
const consume = activity("takeCount", { count: stepOutput("diff") });
|
|
327
|
+
const config = opWith({ phases: [phase("P", [diff, consume])] });
|
|
328
|
+
const contracts = new Map<string, ActivityContract>([
|
|
329
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
330
|
+
["takeCount", takeCountContract],
|
|
331
|
+
]);
|
|
332
|
+
expect(validateStepOutputRefs(config, contracts).some((i) => i.message.includes("type mismatch"))).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
});
|