@intentius/chant 0.49.0 → 0.51.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/op-progress.d.ts +57 -0
- package/dist/cli/handlers/op-progress.d.ts.map +1 -0
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/run-client.d.ts +21 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run-report.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/handlers/search.d.ts +22 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.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 +47 -3
- 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/run-progress.d.ts +7 -5
- package/dist/components/run-progress.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 +134 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/assert-live.d.ts +77 -0
- package/dist/lifecycle/assert-live.d.ts.map +1 -0
- package/dist/lifecycle/change-set.d.ts +17 -0
- package/dist/lifecycle/change-set.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/disruption.d.ts +96 -0
- package/dist/lifecycle/disruption.d.ts.map +1 -0
- 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 +6 -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/replay.d.ts +2 -0
- package/dist/lifecycle/replay.d.ts.map +1 -1
- 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 +7 -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/testing.d.ts +23 -2
- package/dist/testing.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.test.ts +90 -0
- package/src/cli/handlers/lifecycle.ts +30 -5
- package/src/cli/handlers/op-progress.test.ts +202 -0
- package/src/cli/handlers/op-progress.ts +192 -0
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/run-client.test.ts +82 -0
- package/src/cli/handlers/run-client.ts +85 -2
- package/src/cli/handlers/run-report.test.ts +62 -0
- package/src/cli/handlers/run-report.ts +20 -58
- package/src/cli/handlers/run.test.ts +144 -0
- package/src/cli/handlers/run.ts +40 -18
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/handlers/search-drift.test.ts +263 -0
- package/src/cli/handlers/search.ts +150 -1
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +81 -1
- package/src/cli/mcp/op-tools.ts +17 -6
- package/src/cli/mcp/resource-handlers.ts +13 -5
- 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 +47 -3
- 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/run-progress.ts +9 -5
- 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 +154 -2
- package/src/lifecycle/assert-live.test.ts +125 -0
- package/src/lifecycle/assert-live.ts +154 -0
- package/src/lifecycle/change-set.ts +35 -3
- 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/disruption.test.ts +186 -0
- package/src/lifecycle/disruption.ts +224 -0
- 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 +6 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/replay.test.ts +25 -0
- package/src/lifecycle/replay.ts +11 -3
- 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 +52 -10
- package/src/op/local-output.ts +1 -1
- 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/testing.test.ts +89 -2
- package/src/testing.ts +63 -3
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
package/src/cli/handlers/run.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { spawn as spawnChild, type ChildProcess } from "node:child_process";
|
|
|
5
5
|
import { loadChantConfig, resolveAutoReleaseDisabled, type ChantConfig } from "../../config";
|
|
6
6
|
import { discoverOps } from "../../op/discover";
|
|
7
7
|
import { loadActivities, loadProfiles } from "../../op/activity-registry";
|
|
8
|
-
import { runOpLocally, findGate, LocalGateUnsupportedError, OpRunFailure } from "../../op/local-executor";
|
|
8
|
+
import { runOpLocally, findGate, LocalGateUnsupportedError, OpRunFailure, type StepRecord } from "../../op/local-executor";
|
|
9
9
|
import { renderHuman, renderJson } from "../../op/local-output";
|
|
10
10
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
11
11
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
@@ -15,11 +15,13 @@ import {
|
|
|
15
15
|
connectionOptions,
|
|
16
16
|
resolveWorkflowId,
|
|
17
17
|
resolveProfile,
|
|
18
|
+
fetchNormalizedHistory,
|
|
18
19
|
type WorkflowHandleRaw,
|
|
19
20
|
type WorkflowExecutionDescription,
|
|
20
21
|
type WorkflowHistoryRaw,
|
|
21
22
|
} from "./run-client";
|
|
22
23
|
import { generateReport, writeReport } from "./run-report";
|
|
24
|
+
import { extractStepRecords, countActivities, queryGateState } from "./op-progress";
|
|
23
25
|
import { runComponents, resolveComponentTargets, findComponentGate, listComponents } from "../../components/cli-support";
|
|
24
26
|
import { renderDriverHuman, renderDriverJson } from "../../components/driver-output";
|
|
25
27
|
import { ndjsonProgressSink } from "../../components/run-progress";
|
|
@@ -250,12 +252,12 @@ export async function runOpStatus(ctx: CommandContext): Promise<number> {
|
|
|
250
252
|
if (ctx.args.components) return runComponentStatus(ctx, name);
|
|
251
253
|
|
|
252
254
|
const projectPath = resolve(".");
|
|
253
|
-
let client, desc: WorkflowExecutionDescription, history: WorkflowHistoryRaw;
|
|
255
|
+
let client, desc: WorkflowExecutionDescription, history: WorkflowHistoryRaw, handle: WorkflowHandleRaw;
|
|
254
256
|
try {
|
|
255
257
|
({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
|
|
256
|
-
|
|
258
|
+
handle = client.workflow.getHandle(resolveWorkflowId(name));
|
|
257
259
|
desc = await handle.describe();
|
|
258
|
-
history = await handle
|
|
260
|
+
history = await fetchNormalizedHistory(handle);
|
|
259
261
|
} catch (err) {
|
|
260
262
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
261
263
|
return 1;
|
|
@@ -269,13 +271,19 @@ export async function runOpStatus(ctx: CommandContext): Promise<number> {
|
|
|
269
271
|
console.log(` Started : ${desc.startTime.toISOString()}`);
|
|
270
272
|
if (desc.closeTime) console.log(` Closed : ${desc.closeTime.toISOString()}`);
|
|
271
273
|
|
|
272
|
-
const
|
|
273
|
-
const completed = events.filter((e) => e.eventType === "ActivityTaskCompleted").length;
|
|
274
|
-
const scheduled = events.filter((e) => e.eventType === "ActivityTaskScheduled").length;
|
|
274
|
+
const { completed, scheduled } = countActivities(history);
|
|
275
275
|
if (scheduled > 0) {
|
|
276
276
|
console.log(` Activities : ${completed}/${scheduled} completed`);
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
// Queryable gate state (#1676) — undefined means the query itself failed
|
|
280
|
+
// (an ungated Op never registers a "gateState" handler), so print nothing;
|
|
281
|
+
// null means the query answered "no gate pending" — also nothing to print.
|
|
282
|
+
const gate = await queryGateState(handle);
|
|
283
|
+
if (gate) {
|
|
284
|
+
console.log(` Gate : ${gate.signalName}${gate.description ? ` — ${gate.description}` : ""} (waiting since ${gate.since})`);
|
|
285
|
+
}
|
|
286
|
+
|
|
279
287
|
return 0;
|
|
280
288
|
}
|
|
281
289
|
|
|
@@ -293,7 +301,7 @@ async function runComponentStatus(ctx: CommandContext, name: string): Promise<nu
|
|
|
293
301
|
({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
|
|
294
302
|
const handle = client.workflow.getHandle(componentWorkflowId(name));
|
|
295
303
|
desc = await handle.describe();
|
|
296
|
-
history = await handle
|
|
304
|
+
history = await fetchNormalizedHistory(handle);
|
|
297
305
|
} catch (err) {
|
|
298
306
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
299
307
|
return 1;
|
|
@@ -307,9 +315,7 @@ async function runComponentStatus(ctx: CommandContext, name: string): Promise<nu
|
|
|
307
315
|
console.log(` Started : ${desc.startTime.toISOString()}`);
|
|
308
316
|
if (desc.closeTime) console.log(` Closed : ${desc.closeTime.toISOString()}`);
|
|
309
317
|
|
|
310
|
-
const
|
|
311
|
-
const completed = events.filter((e) => e.eventType === "ActivityTaskCompleted").length;
|
|
312
|
-
const scheduled = events.filter((e) => e.eventType === "ActivityTaskScheduled").length;
|
|
318
|
+
const { completed, scheduled } = countActivities(history);
|
|
313
319
|
if (scheduled > 0) {
|
|
314
320
|
console.log(` Activities : ${completed}/${scheduled} completed`);
|
|
315
321
|
}
|
|
@@ -528,9 +534,7 @@ async function waitForTemporalServer(address: string, maxWaitMs = 30_000): Promi
|
|
|
528
534
|
}
|
|
529
535
|
|
|
530
536
|
function renderProgress(opName: string, history: WorkflowHistoryRaw): void {
|
|
531
|
-
const
|
|
532
|
-
const completed = events.filter((e) => e.eventType === "ActivityTaskCompleted").length;
|
|
533
|
-
const scheduled = events.filter((e) => e.eventType === "ActivityTaskScheduled").length;
|
|
537
|
+
const { completed, scheduled } = countActivities(history);
|
|
534
538
|
process.stderr.write(
|
|
535
539
|
`\r${formatInfo(`[${opName}]`)} ${completed}/${scheduled} activities completed`,
|
|
536
540
|
);
|
|
@@ -954,7 +958,7 @@ async function runComponentTemporal(
|
|
|
954
958
|
while (true) {
|
|
955
959
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
956
960
|
const desc = await handle.describe();
|
|
957
|
-
const history = await handle
|
|
961
|
+
const history = await fetchNormalizedHistory(handle);
|
|
958
962
|
renderProgress(component.name, history);
|
|
959
963
|
if (TERMINAL_STATUSES.has(desc.status.name)) {
|
|
960
964
|
process.stderr.write("\n");
|
|
@@ -1160,7 +1164,7 @@ async function runOpTemporal(ctx: CommandContext): Promise<number> {
|
|
|
1160
1164
|
({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
|
|
1161
1165
|
const handle = client.workflow.getHandle(resolveWorkflowId(opName));
|
|
1162
1166
|
desc = await handle.describe();
|
|
1163
|
-
history = await handle
|
|
1167
|
+
history = await fetchNormalizedHistory(handle);
|
|
1164
1168
|
} catch (err) {
|
|
1165
1169
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
1166
1170
|
return 1;
|
|
@@ -1248,7 +1252,23 @@ async function runOpTemporal(ctx: CommandContext): Promise<number> {
|
|
|
1248
1252
|
return 1;
|
|
1249
1253
|
}
|
|
1250
1254
|
|
|
1251
|
-
// Poll for progress until terminal state
|
|
1255
|
+
// Poll for progress until terminal state.
|
|
1256
|
+
//
|
|
1257
|
+
// `--progress-json` (chant #1676): stream one NDJSON StepRecord per line to
|
|
1258
|
+
// stdout as each declared step settles — the durable-path counterpart to
|
|
1259
|
+
// `chant run <name> --json`'s local-executor record shape (op-progress.ts).
|
|
1260
|
+
// Purely additive: when the flag is absent `emitProgress` stays undefined
|
|
1261
|
+
// and every `emitProgress?.(...)` call is a no-op, so poll-loop behavior
|
|
1262
|
+
// is unchanged. `extractStepRecords` is stable and append-only across
|
|
1263
|
+
// polls (same declared order, a growing history), so a plain emitted-count
|
|
1264
|
+
// watermark is enough to emit only what's new each round.
|
|
1265
|
+
const emitProgress = ctx.args.progressJson ? ndjsonProgressSink<StepRecord>() : undefined;
|
|
1266
|
+
let emittedCount = 0;
|
|
1267
|
+
const emitNewRecords = (records: StepRecord[]) => {
|
|
1268
|
+
for (const record of records.slice(emittedCount)) emitProgress?.(record);
|
|
1269
|
+
emittedCount = records.length;
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1252
1272
|
let finalDesc: WorkflowExecutionDescription | undefined;
|
|
1253
1273
|
let finalHistory: WorkflowHistoryRaw | undefined;
|
|
1254
1274
|
|
|
@@ -1256,12 +1276,14 @@ async function runOpTemporal(ctx: CommandContext): Promise<number> {
|
|
|
1256
1276
|
while (true) {
|
|
1257
1277
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1258
1278
|
const desc = await handle.describe();
|
|
1259
|
-
const history = await handle
|
|
1279
|
+
const history = await fetchNormalizedHistory(handle);
|
|
1260
1280
|
|
|
1261
1281
|
renderProgress(opName, history);
|
|
1282
|
+
if (emitProgress) emitNewRecords(extractStepRecords(config, history));
|
|
1262
1283
|
|
|
1263
1284
|
if (TERMINAL_STATUSES.has(desc.status.name)) {
|
|
1264
1285
|
process.stderr.write("\n");
|
|
1286
|
+
if (emitProgress) emitNewRecords(extractStepRecords(config, history, { final: true }));
|
|
1265
1287
|
finalDesc = desc;
|
|
1266
1288
|
finalHistory = history;
|
|
1267
1289
|
break;
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import type { BuildResult } from "../../build";
|
|
6
|
+
import type { ParsedArgs } from "../registry";
|
|
7
|
+
import { Scenario, snapshot } from "../../lifecycle/scenario";
|
|
8
|
+
import { EffectReceipt, receiptExpectation, type EffectReceiptDeclaration } from "../../effect-receipt";
|
|
9
|
+
import type { ResourceMetadata } from "../../lexicon";
|
|
10
|
+
import type { LifecycleSnapshot } from "../../lifecycle/types";
|
|
11
|
+
|
|
12
|
+
const buildMock = vi.fn();
|
|
13
|
+
const fetchLifecycleMock = vi.fn();
|
|
14
|
+
const readEnvironmentSnapshotsMock = vi.fn();
|
|
15
|
+
const loadChantConfigMock = vi.fn();
|
|
16
|
+
|
|
17
|
+
vi.mock("../../build", async () => {
|
|
18
|
+
const actual = await vi.importActual<typeof import("../../build")>("../../build");
|
|
19
|
+
return { ...actual, build: (...args: unknown[]) => buildMock(...args) };
|
|
20
|
+
});
|
|
21
|
+
vi.mock("../../lifecycle/git", () => ({
|
|
22
|
+
fetchLifecycle: () => fetchLifecycleMock(),
|
|
23
|
+
readEnvironmentSnapshots: (...args: unknown[]) => readEnvironmentSnapshotsMock(...args),
|
|
24
|
+
}));
|
|
25
|
+
vi.mock("../../config", async () => {
|
|
26
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
27
|
+
return { ...actual, loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args) };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const { runScenarioCheck, runScenarioUnknown } = await import("./scenario");
|
|
31
|
+
|
|
32
|
+
function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
|
|
33
|
+
return {
|
|
34
|
+
command: "scenario",
|
|
35
|
+
path: "check",
|
|
36
|
+
format: "",
|
|
37
|
+
fix: false,
|
|
38
|
+
watch: false,
|
|
39
|
+
verbose: false,
|
|
40
|
+
help: false,
|
|
41
|
+
live: false,
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A synthetic BuildResult — same shape ./lifecycle.test.ts's makeBuildResult
|
|
47
|
+
* uses: plain `{lexicon, entityType, props}` objects, not real Declarables,
|
|
48
|
+
* since `isResourceDeclarable` only checks for a `props` key. Scenario
|
|
49
|
+
* entities must be real `Scenario(...)` objects — collection keys on the
|
|
50
|
+
* marker symbol. */
|
|
51
|
+
function makeBuildResult(
|
|
52
|
+
resourcesByLexicon: Record<string, string[]>,
|
|
53
|
+
scenarios: Record<string, ReturnType<typeof Scenario>>,
|
|
54
|
+
receipts: Record<string, EffectReceiptDeclaration> = {},
|
|
55
|
+
): BuildResult {
|
|
56
|
+
const entities = new Map<string, unknown>();
|
|
57
|
+
for (const [lexicon, names] of Object.entries(resourcesByLexicon)) {
|
|
58
|
+
for (const name of names) entities.set(name, { lexicon, entityType: `${lexicon}::Mock`, props: {} });
|
|
59
|
+
}
|
|
60
|
+
for (const [name, decl] of Object.entries(scenarios)) entities.set(name, decl);
|
|
61
|
+
for (const [name, decl] of Object.entries(receipts)) entities.set(name, decl);
|
|
62
|
+
return {
|
|
63
|
+
outputs: new Map(Object.keys(resourcesByLexicon).map((l) => [l, "{}"])),
|
|
64
|
+
entities,
|
|
65
|
+
dependencies: new Map(),
|
|
66
|
+
errors: [],
|
|
67
|
+
warnings: [],
|
|
68
|
+
manifest: { lexicons: Object.keys(resourcesByLexicon), outputs: {}, deployOrder: Object.keys(resourcesByLexicon) },
|
|
69
|
+
sourceFileCount: 1,
|
|
70
|
+
} as unknown as BuildResult;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* An aws-materialized effect receipt shaped exactly like
|
|
75
|
+
* lexicons/aws/src/effect-receipt-row.ts's real factory (an `AWS::SSM::Parameter`
|
|
76
|
+
* row with `props: { Type: "String" }`) — built inline off the core
|
|
77
|
+
* `EffectReceipt` factory rather than importing the lexicon package, the same
|
|
78
|
+
* "minimal test EffectReceipt" pattern receipt-plan.test.ts uses. Carrying
|
|
79
|
+
* `props` is the load-bearing detail: it is what made the reviewer's bug
|
|
80
|
+
* reproducible — `isResourceDeclarable` (packages/core/src/declarable.ts) key
|
|
81
|
+
* on `"props" in value`, so an aws-materialized receipt WAS swept into
|
|
82
|
+
* `evaluateOneScenario`'s old isResourceDeclarable-only declared axis and
|
|
83
|
+
* generically misclassified, exactly as a bare `AWS::S3::Bucket` would be.
|
|
84
|
+
*/
|
|
85
|
+
function makeAwsReceipt(
|
|
86
|
+
name: string,
|
|
87
|
+
effect: string,
|
|
88
|
+
flavor: "existence" | "hash",
|
|
89
|
+
inputs?: Record<string, unknown>,
|
|
90
|
+
): EffectReceiptDeclaration {
|
|
91
|
+
const core = EffectReceipt(name, { effect, flavor, inputs });
|
|
92
|
+
return {
|
|
93
|
+
...core,
|
|
94
|
+
lexicon: "aws",
|
|
95
|
+
entityType: "AWS::SSM::Parameter",
|
|
96
|
+
props: { Type: "String" },
|
|
97
|
+
} as unknown as EffectReceiptDeclaration;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const meta = (overrides: Partial<ResourceMetadata> = {}): ResourceMetadata => ({
|
|
101
|
+
type: "AWS::S3::Bucket",
|
|
102
|
+
status: "CREATE_COMPLETE",
|
|
103
|
+
ownership: "owned",
|
|
104
|
+
...overrides,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const snap = (overrides: Partial<LifecycleSnapshot> = {}): LifecycleSnapshot => ({
|
|
108
|
+
lexicon: "aws",
|
|
109
|
+
environment: "prod",
|
|
110
|
+
commit: "abc123",
|
|
111
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
112
|
+
resources: {},
|
|
113
|
+
...overrides,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("runScenarioCheck", () => {
|
|
117
|
+
let stdoutBuf: string[];
|
|
118
|
+
let stderrBuf: string[];
|
|
119
|
+
let tmpFiles: string[];
|
|
120
|
+
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
stdoutBuf = [];
|
|
123
|
+
stderrBuf = [];
|
|
124
|
+
tmpFiles = [];
|
|
125
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { stdoutBuf.push(s); });
|
|
126
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { stderrBuf.push(s); });
|
|
127
|
+
buildMock.mockReset();
|
|
128
|
+
fetchLifecycleMock.mockReset();
|
|
129
|
+
fetchLifecycleMock.mockResolvedValue(undefined);
|
|
130
|
+
readEnvironmentSnapshotsMock.mockReset();
|
|
131
|
+
readEnvironmentSnapshotsMock.mockResolvedValue(new Map());
|
|
132
|
+
loadChantConfigMock.mockReset();
|
|
133
|
+
loadChantConfigMock.mockResolvedValue({ config: { environments: [{ name: "prod" }, { name: "staging" }] } });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
afterEach(async () => {
|
|
137
|
+
for (const d of tmpFiles) await rm(d, { recursive: true, force: true });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
function combined(): string {
|
|
141
|
+
return [...stdoutBuf, ...stderrBuf].join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function writeFixture(content: LifecycleSnapshot): Promise<string> {
|
|
145
|
+
const dir = await mkdtemp(join(tmpdir(), "chant-scenario-"));
|
|
146
|
+
const path = join(dir, "baseline.json");
|
|
147
|
+
await writeFile(path, JSON.stringify(content));
|
|
148
|
+
tmpFiles.push(dir);
|
|
149
|
+
return path;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
test("no scenarios declared: exits 0 and says so", async () => {
|
|
153
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }, {}));
|
|
154
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
155
|
+
expect(exit).toBe(0);
|
|
156
|
+
expect(stdoutBuf.join("\n")).toContain("No scenarios declared");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("build failure exits 1 before any scenario runs", async () => {
|
|
160
|
+
buildMock.mockResolvedValue({ ...makeBuildResult({}, {}), errors: [{}] });
|
|
161
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
162
|
+
expect(exit).toBe(1);
|
|
163
|
+
expect(stderrBuf.join("\n")).toContain("Build failed");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("a noop scenario passes against a fixture matching the declared entity", async () => {
|
|
167
|
+
const fixturePath = await writeFixture(snap({ resources: { bucket: meta() } }));
|
|
168
|
+
const scenario = Scenario("plan-neutral", { given: snapshot(fixturePath), expect: { noop: true } });
|
|
169
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }, { plan: scenario }));
|
|
170
|
+
|
|
171
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
172
|
+
expect(exit).toBe(0);
|
|
173
|
+
expect(stdoutBuf.join("\n")).toContain("plan-neutral");
|
|
174
|
+
expect(stdoutBuf.join("\n")).toContain("PASS");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("a noop scenario fails with a legible detail when the fixture lacks a declared resource (create proposed)", async () => {
|
|
178
|
+
const fixturePath = await writeFixture(snap({ resources: {} }));
|
|
179
|
+
const scenario = Scenario("should-be-neutral", { given: snapshot(fixturePath), expect: { noop: true } });
|
|
180
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }, { plan: scenario }));
|
|
181
|
+
|
|
182
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
183
|
+
expect(exit).toBe(1);
|
|
184
|
+
const out = stdoutBuf.join("\n");
|
|
185
|
+
expect(out).toContain("FAIL");
|
|
186
|
+
expect(out).toContain("noop:");
|
|
187
|
+
expect(out).toContain("1 create");
|
|
188
|
+
expect(out).toContain("bucket");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a deletes scenario passes when the fixture's extra owned resource is proposed for delete", async () => {
|
|
192
|
+
const fixturePath = await writeFixture(snap({ resources: { legacy: meta({ ownership: "owned" }) } }));
|
|
193
|
+
const scenario = Scenario("drop legacy", {
|
|
194
|
+
given: snapshot(fixturePath),
|
|
195
|
+
expect: { deletes: [{ name: "legacy", ownership: "owned" }], create: 0, update: 0 },
|
|
196
|
+
});
|
|
197
|
+
buildMock.mockResolvedValue(makeBuildResult({}, { plan: scenario }));
|
|
198
|
+
|
|
199
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
200
|
+
expect(exit).toBe(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// buildChangeSet never classifies a non-owned resource as `delete` (only
|
|
204
|
+
// `adopt`) — the ownership safety property is structural, upstream of
|
|
205
|
+
// evaluateScenario. A `deletes` expectation against a foreign resource
|
|
206
|
+
// therefore fails as "not proposed", never as an ownership mismatch; that
|
|
207
|
+
// IS the assertion worth making — the scenario correctly refuses to treat
|
|
208
|
+
// an adopt candidate as the delete it expected.
|
|
209
|
+
test("a deletes scenario fails and names the resource when the fixture marks it foreign (never auto-deleted)", async () => {
|
|
210
|
+
const fixturePath = await writeFixture(snap({ resources: { legacy: meta({ ownership: "foreign" }) } }));
|
|
211
|
+
const scenario = Scenario("drop legacy", {
|
|
212
|
+
given: snapshot(fixturePath),
|
|
213
|
+
expect: { deletes: [{ name: "legacy", ownership: "owned" }] },
|
|
214
|
+
});
|
|
215
|
+
buildMock.mockResolvedValue(makeBuildResult({}, { plan: scenario }));
|
|
216
|
+
|
|
217
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
218
|
+
expect(exit).toBe(1);
|
|
219
|
+
const out = stdoutBuf.join("\n");
|
|
220
|
+
expect(out).toContain("deletes:");
|
|
221
|
+
expect(out).toContain("legacy");
|
|
222
|
+
expect(out).toContain("not proposed for delete");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("given: snapshot(path) — a missing fixture file fails the scenario, not the whole run", async () => {
|
|
226
|
+
const scenario = Scenario("s", { given: snapshot("fixtures/does-not-exist.json"), expect: { noop: true } });
|
|
227
|
+
buildMock.mockResolvedValue(makeBuildResult({}, { plan: scenario }));
|
|
228
|
+
|
|
229
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
230
|
+
expect(exit).toBe(1);
|
|
231
|
+
expect(stdoutBuf.join("\n")).toContain("fixture not found");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("given: snapshot(env) — an unknown environment fails with the declared-environments hint", async () => {
|
|
235
|
+
const scenario = Scenario("s", { given: snapshot("nowhere"), expect: { noop: true } });
|
|
236
|
+
buildMock.mockResolvedValue(makeBuildResult({}, { plan: scenario }));
|
|
237
|
+
|
|
238
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
239
|
+
expect(exit).toBe(1);
|
|
240
|
+
expect(stdoutBuf.join("\n")).toMatch(/Unknown environment/);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("given: snapshot(env) — no recorded snapshot fails with a recording hint", async () => {
|
|
244
|
+
readEnvironmentSnapshotsMock.mockResolvedValue(new Map());
|
|
245
|
+
const scenario = Scenario("s", { given: snapshot("prod"), expect: { noop: true } });
|
|
246
|
+
buildMock.mockResolvedValue(makeBuildResult({}, { plan: scenario }));
|
|
247
|
+
|
|
248
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
249
|
+
expect(exit).toBe(1);
|
|
250
|
+
expect(stdoutBuf.join("\n")).toContain("chant lifecycle snapshot prod");
|
|
251
|
+
expect(fetchLifecycleMock).toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("given: snapshot(env) — replays every recorded lexicon's snapshot", async () => {
|
|
255
|
+
readEnvironmentSnapshotsMock.mockResolvedValue(
|
|
256
|
+
new Map([["aws", JSON.stringify(snap({ resources: { bucket: meta() } }))]]),
|
|
257
|
+
);
|
|
258
|
+
const scenario = Scenario("s", { given: snapshot("prod"), expect: { noop: true } });
|
|
259
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }, { plan: scenario }));
|
|
260
|
+
|
|
261
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
262
|
+
expect(exit).toBe(0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("a lexicon the fixture doesn't cover is marked unobserved, not silently create", async () => {
|
|
266
|
+
const fixturePath = await writeFixture(snap({ lexicon: "aws", resources: { bucket: meta() } }));
|
|
267
|
+
const scenario = Scenario("multi-lexicon", {
|
|
268
|
+
given: snapshot(fixturePath),
|
|
269
|
+
expect: { unobserved: "refuse" },
|
|
270
|
+
});
|
|
271
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"], k8s: ["web"] }, { plan: scenario }));
|
|
272
|
+
|
|
273
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
274
|
+
expect(exit).toBe(1);
|
|
275
|
+
const out = stdoutBuf.join("\n");
|
|
276
|
+
expect(out).toContain("unobserved:");
|
|
277
|
+
expect(out).toContain("web");
|
|
278
|
+
expect(out).not.toContain("1 create"); // bucket must not have become a spurious pass/fail signal here
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("multiple scenarios: overall exit reflects any failure, and every scenario is reported", async () => {
|
|
282
|
+
const fixturePath = await writeFixture(snap({ resources: { bucket: meta() } }));
|
|
283
|
+
const passing = Scenario("passing", { given: snapshot(fixturePath), expect: { noop: true } });
|
|
284
|
+
const failing = Scenario("failing", { given: snapshot(fixturePath), expect: { create: 5 } });
|
|
285
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }, { passing, failing }));
|
|
286
|
+
|
|
287
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
288
|
+
expect(exit).toBe(1);
|
|
289
|
+
const out = combined();
|
|
290
|
+
expect(out).toContain("passing");
|
|
291
|
+
expect(out).toContain("failing");
|
|
292
|
+
expect(out).toContain("1/2 scenario(s) failed");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("--json emits every scenario's verdict as structured data", async () => {
|
|
296
|
+
const fixturePath = await writeFixture(snap({ resources: { bucket: meta() } }));
|
|
297
|
+
const scenario = Scenario("plan-neutral", { given: snapshot(fixturePath), expect: { noop: true } });
|
|
298
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }, { plan: scenario }));
|
|
299
|
+
|
|
300
|
+
const exit = await runScenarioCheck({ args: makeArgs({ json: true }), plugins: [], serializers: [] });
|
|
301
|
+
expect(exit).toBe(0);
|
|
302
|
+
const parsed = JSON.parse(stdoutBuf[0]);
|
|
303
|
+
expect(parsed).toEqual([{ name: "plan-neutral", entity: "plan", env: "prod", pass: true, checks: [{ clause: "noop", pass: true }] }]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
307
|
+
// Effect receipts (#1832 parity): the reviewer's two reproductions against
|
|
308
|
+
// `evaluateOneScenario`, now that it routes receipts through the same
|
|
309
|
+
// collectEffectReceipts → planReceipts → mergeReceiptEntries pipeline
|
|
310
|
+
// `runLifecyclePlan` uses (lifecycle.ts), instead of classifying them
|
|
311
|
+
// through isResourceDeclarable + buildChangeSet alone.
|
|
312
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
test("a declared-but-unfired receipt classifies as effect, never as a generic create", async () => {
|
|
315
|
+
// Confirmed absent: the fixture's aws lexicon has no "dbMigrated" entry
|
|
316
|
+
// in `resources` and names nothing `unobserved` for it — the same
|
|
317
|
+
// "looked and it's not there" claim a live read makes.
|
|
318
|
+
const receipt = makeAwsReceipt("dbMigrated", "db-migrate", "existence");
|
|
319
|
+
const fixturePath = await writeFixture(snap({ lexicon: "aws", resources: {} }));
|
|
320
|
+
const scenario = Scenario("nothing pending", { given: snapshot(fixturePath), expect: { noop: true } });
|
|
321
|
+
buildMock.mockResolvedValue(makeBuildResult({}, { check: scenario }, { dbMigrated: receipt }));
|
|
322
|
+
|
|
323
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
324
|
+
|
|
325
|
+
// Before the fix, isResourceDeclarable(receipt) is true (the aws row
|
|
326
|
+
// carries `props`), so the generic classification proposed a `create` —
|
|
327
|
+
// exactly the reviewer's "false noop FAILURE" reproduction. Fixed, the
|
|
328
|
+
// receipt pipeline reclassifies it as `effect` (the fire is pending),
|
|
329
|
+
// which never reads as a `create`.
|
|
330
|
+
expect(exit).toBe(1);
|
|
331
|
+
const out = combined();
|
|
332
|
+
expect(out).toContain("FAIL");
|
|
333
|
+
expect(out).not.toContain("1 create");
|
|
334
|
+
expect(out).toContain("0 create");
|
|
335
|
+
expect(out).toContain("1 effect");
|
|
336
|
+
expect(out).toContain("dbMigrated");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("a stale receipt in the fixture never reads as a clean noop pass", async () => {
|
|
340
|
+
// hash-flavor, static inputs: `receiptExpectation` is computable offline,
|
|
341
|
+
// no reference resolver needed. The fixture's live value is a DIFFERENT
|
|
342
|
+
// digest — a stale receipt, present but wrong, exactly the reviewer's
|
|
343
|
+
// "false noop PASS" reproduction (observedThen is always undefined for a
|
|
344
|
+
// scenario, so the generic pass sees no drift and calls it `noop`).
|
|
345
|
+
const receipt = makeAwsReceipt("seeded", "seed", "hash", { rows: 10 });
|
|
346
|
+
const staleValue = receiptExpectation(makeAwsReceipt("seeded", "seed", "hash", { rows: 9 }));
|
|
347
|
+
const fixturePath = await writeFixture(
|
|
348
|
+
snap({ lexicon: "aws", resources: { seeded: meta({ type: "AWS::SSM::Parameter", attributes: { value: staleValue } }) } }),
|
|
349
|
+
);
|
|
350
|
+
const scenario = Scenario("seed already ran", { given: snapshot(fixturePath), expect: { noop: true } });
|
|
351
|
+
buildMock.mockResolvedValue(makeBuildResult({}, { check: scenario }, { seeded: receipt }));
|
|
352
|
+
|
|
353
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
354
|
+
|
|
355
|
+
expect(exit).toBe(1);
|
|
356
|
+
const out = combined();
|
|
357
|
+
expect(out).toContain("FAIL");
|
|
358
|
+
expect(out).toContain("1 effect");
|
|
359
|
+
expect(out).toContain("seeded");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("a receipt whose live value matches its expectation is a genuine noop pass", async () => {
|
|
363
|
+
// The positive control for the two reproductions above: a receipt that
|
|
364
|
+
// HAS fired, with the right value, is still a clean noop — the pipeline
|
|
365
|
+
// doesn't just fail every scenario touching a receipt.
|
|
366
|
+
const receipt = makeAwsReceipt("bootstrapped", "bootstrap", "existence");
|
|
367
|
+
const fixturePath = await writeFixture(
|
|
368
|
+
snap({
|
|
369
|
+
lexicon: "aws",
|
|
370
|
+
resources: { bootstrapped: meta({ type: "AWS::SSM::Parameter", attributes: { value: receiptExpectation(receipt) } }) },
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
const scenario = Scenario("already bootstrapped", { given: snapshot(fixturePath), expect: { noop: true } });
|
|
374
|
+
buildMock.mockResolvedValue(makeBuildResult({}, { check: scenario }, { bootstrapped: receipt }));
|
|
375
|
+
|
|
376
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
377
|
+
|
|
378
|
+
expect(exit).toBe(0);
|
|
379
|
+
expect(combined()).toContain("PASS");
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("runScenarioUnknown", () => {
|
|
384
|
+
test("reports the unknown subcommand and exits 1", async () => {
|
|
385
|
+
const stderrBuf: string[] = [];
|
|
386
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { stderrBuf.push(s); });
|
|
387
|
+
const exit = await runScenarioUnknown({ args: makeArgs({ path: "bogus" }), plugins: [], serializers: [] });
|
|
388
|
+
expect(exit).toBe(1);
|
|
389
|
+
expect(stderrBuf.join("\n")).toContain("bogus");
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
394
|
+
// Fixture-driven integration test (#1292): a tiny declared project + a
|
|
395
|
+
// committed fixture snapshot. Exercises the real path end to end — factory,
|
|
396
|
+
// evaluator, and the CLI handler's fixture replay — with no mocked fixture
|
|
397
|
+
// I/O: the JSON file is written to disk and read back for real.
|
|
398
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
399
|
+
describe("fixture-driven integration", () => {
|
|
400
|
+
let stdoutBuf: string[];
|
|
401
|
+
let dir: string;
|
|
402
|
+
|
|
403
|
+
beforeEach(async () => {
|
|
404
|
+
stdoutBuf = [];
|
|
405
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { stdoutBuf.push(s); });
|
|
406
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
407
|
+
buildMock.mockReset();
|
|
408
|
+
loadChantConfigMock.mockReset();
|
|
409
|
+
loadChantConfigMock.mockResolvedValue({ config: { environments: [{ name: "prod" }] } });
|
|
410
|
+
dir = await mkdtemp(join(tmpdir(), "chant-scenario-integration-"));
|
|
411
|
+
await writeFile(
|
|
412
|
+
join(dir, "prod-baseline.json"),
|
|
413
|
+
JSON.stringify(
|
|
414
|
+
snap({
|
|
415
|
+
resources: {
|
|
416
|
+
webBucket: meta({ type: "AWS::S3::Bucket", physicalId: "arn:aws:s3:::web" }),
|
|
417
|
+
},
|
|
418
|
+
}),
|
|
419
|
+
),
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
afterEach(async () => {
|
|
424
|
+
await rm(dir, { recursive: true, force: true });
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("the committed fixture: the declared project as-is is plan-neutral", async () => {
|
|
428
|
+
const fixturePath = join(dir, "prod-baseline.json");
|
|
429
|
+
const scenario = Scenario("web stack is plan-neutral", { given: snapshot(fixturePath), expect: { noop: true } });
|
|
430
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["webBucket"] }, { neutralityCheck: scenario }));
|
|
431
|
+
|
|
432
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
433
|
+
|
|
434
|
+
expect(exit).toBe(0);
|
|
435
|
+
expect(stdoutBuf.join("\n")).toContain("PASS");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("dropping the declaration from source, against the same fixture, fails the scenario with a legible diff naming the resource", async () => {
|
|
439
|
+
const fixturePath = join(dir, "prod-baseline.json");
|
|
440
|
+
const scenario = Scenario("web stack is plan-neutral", { given: snapshot(fixturePath), expect: { noop: true } });
|
|
441
|
+
// The mutation: webBucket no longer declared (dropped from source), same
|
|
442
|
+
// committed fixture. The fixture still remembers it as owned+live, so the
|
|
443
|
+
// plan now proposes deleting it — exactly the silent-delete the issue
|
|
444
|
+
// describes chant having no way to catch today.
|
|
445
|
+
buildMock.mockResolvedValue(makeBuildResult({}, { neutralityCheck: scenario }));
|
|
446
|
+
|
|
447
|
+
const exit = await runScenarioCheck({ args: makeArgs(), plugins: [], serializers: [] });
|
|
448
|
+
|
|
449
|
+
expect(exit).toBe(1);
|
|
450
|
+
const out = stdoutBuf.join("\n");
|
|
451
|
+
expect(out).toContain("FAIL");
|
|
452
|
+
expect(out).toContain("noop:");
|
|
453
|
+
expect(out).toContain("1 delete");
|
|
454
|
+
expect(out).toContain("webBucket");
|
|
455
|
+
});
|
|
456
|
+
});
|