@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
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { commandBuildParams } from "../build-params-cli";
|
|
4
|
+
import { build, type BuildResult } from "../../build";
|
|
5
|
+
import { readEnvironmentSnapshots, fetchLifecycle } from "../../lifecycle/git";
|
|
6
|
+
import { buildChangeSet, type ChangeSet, type ChangeSetEntry } from "../../lifecycle/change-set";
|
|
7
|
+
import {
|
|
8
|
+
planReceipts,
|
|
9
|
+
mergeReceiptEntries,
|
|
10
|
+
observedValueResolver,
|
|
11
|
+
readReceiptValue,
|
|
12
|
+
type ReceiptReading,
|
|
13
|
+
} from "../../lifecycle/receipt-plan";
|
|
14
|
+
import { collectEffectReceipts, isEffectReceipt, type EffectReceiptDeclaration } from "../../effect-receipt";
|
|
15
|
+
import { evaluateScenario, type ScenarioVerdict } from "../../lifecycle/scenario-eval";
|
|
16
|
+
import { collectScenarios, type ScenarioDeclaration, type ScenarioGiven } from "../../lifecycle/scenario";
|
|
17
|
+
import { isResourceDeclarable } from "../../declarable";
|
|
18
|
+
import { loadChantConfig } from "../../config";
|
|
19
|
+
import { unknownEnvError } from "../../env";
|
|
20
|
+
import { collectBuildRootContributors } from "../plugins";
|
|
21
|
+
import { resolveBuildRoot } from "./lifecycle";
|
|
22
|
+
import { formatError, formatSuccess, formatBold } from "../format";
|
|
23
|
+
import type { CommandContext } from "../registry";
|
|
24
|
+
import type { LifecycleSnapshot } from "../../lifecycle/types";
|
|
25
|
+
import type { UnobservedEntity } from "../../observation";
|
|
26
|
+
import type { ResourceMetadata } from "../../lexicon";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `chant scenario check` (#1292) — evaluate every declared `Scenario`
|
|
30
|
+
* offline, against a fixture snapshot standing in for live observation.
|
|
31
|
+
*
|
|
32
|
+
* Mirrors `runLifecyclePlan` (./lifecycle.ts): build the project, walk every
|
|
33
|
+
* lexicon's declared entities, produce a `ChangeSet`. The one substitution is
|
|
34
|
+
* the whole point — a scenario's `given` fixture stands in for
|
|
35
|
+
* `observeLexicon`'s live read, so no plugin ever makes a network call and no
|
|
36
|
+
* credentials are ever asked for. `evaluateScenario` (../../lifecycle/scenario-eval.ts)
|
|
37
|
+
* checks the resulting change set against the scenario's `expect`.
|
|
38
|
+
*
|
|
39
|
+
* A scenario has exactly one fixture, not a before-and-after pair, so the
|
|
40
|
+
* change set it produces never has an `observedThen` to diff against — the
|
|
41
|
+
* `update` count is therefore always 0. That is a real, not a placeholder,
|
|
42
|
+
* limitation of what a single-snapshot scenario can assert: presence
|
|
43
|
+
* (`create`/`delete`/`noop`) and ownership are fully expressive here; drift
|
|
44
|
+
* since a prior read is not, because there is no prior read in this model,
|
|
45
|
+
* only the one fixture.
|
|
46
|
+
*
|
|
47
|
+
* Effect receipts (#1832) get the same replacement `runLifecyclePlan` gives
|
|
48
|
+
* them: `collectEffectReceipts` pulls every declared receipt out of the
|
|
49
|
+
* generic axis, `evaluateOneScenario` builds a {@link ReceiptReading} per
|
|
50
|
+
* receipt from the SAME fixture data that stands in for `observed.resources`/
|
|
51
|
+
* `observed.unobserved` elsewhere in this function, and `planReceipts` +
|
|
52
|
+
* `mergeReceiptEntries` (../../lifecycle/receipt-plan.ts) replace whatever the
|
|
53
|
+
* generic classification proposed for a receipt with its real `effect`
|
|
54
|
+
* classification — never a bare create/noop. A receipt whose reading can't be
|
|
55
|
+
* derived from the fixture (its lexicon has no fixture data, or a reference
|
|
56
|
+
* input the receipt depends on isn't among the fixture's recorded attributes)
|
|
57
|
+
* is classified exactly the way `runLifecyclePlan` classifies it: an
|
|
58
|
+
* unreadable receipt lands `unobserved`, loudly; a receipt whose reference
|
|
59
|
+
* input can't resolve still proposes the fire, with an "unresolved input"
|
|
60
|
+
* note, never a guessed digest.
|
|
61
|
+
*/
|
|
62
|
+
export async function runScenarioCheck(ctx: CommandContext): Promise<number> {
|
|
63
|
+
const { args, plugins, serializers } = ctx;
|
|
64
|
+
|
|
65
|
+
const { config } = await loadChantConfig(resolve("."));
|
|
66
|
+
const declaredParams = await commandBuildParams(config.buildParams, args);
|
|
67
|
+
if (!declaredParams) return 1;
|
|
68
|
+
|
|
69
|
+
const buildResult = await build(resolveBuildRoot(args, config), serializers, undefined, {
|
|
70
|
+
buildParams: declaredParams,
|
|
71
|
+
buildRoots: collectBuildRootContributors(plugins, config as unknown as Record<string, unknown>, resolve(".")),
|
|
72
|
+
});
|
|
73
|
+
if (buildResult.errors.length > 0) {
|
|
74
|
+
console.error(formatError({ message: "Build failed — fix errors before checking scenarios" }));
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const scenarios = collectScenarios(buildResult.entities);
|
|
79
|
+
if (scenarios.size === 0) {
|
|
80
|
+
console.log("No scenarios declared — nothing to check.");
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Same read surface `runLifecyclePlan` uses (lifecycle.ts) — computed once,
|
|
85
|
+
// ahead of the per-scenario loop, since it's a property of the build, not
|
|
86
|
+
// of any one scenario's fixture.
|
|
87
|
+
const receipts = collectEffectReceipts(buildResult.entities);
|
|
88
|
+
|
|
89
|
+
// `snapshot(env)` reads the chant/lifecycle orphan branch — fetch once,
|
|
90
|
+
// up front, the same pre-read `runLifecyclePlan` does, rather than once per
|
|
91
|
+
// scenario that needs it.
|
|
92
|
+
if ([...scenarios.values()].some((s) => s.given.kind === "env")) {
|
|
93
|
+
await fetchLifecycle();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const results: Array<{ entityName: string; scenario: ScenarioDeclaration; verdict: ScenarioVerdict; env: string }> = [];
|
|
97
|
+
for (const [entityName, scenario] of scenarios) {
|
|
98
|
+
if (scenario.given.kind === "env") {
|
|
99
|
+
const envErr = unknownEnvError(scenario.given.env, config.environments);
|
|
100
|
+
if (envErr) {
|
|
101
|
+
results.push({
|
|
102
|
+
entityName,
|
|
103
|
+
scenario,
|
|
104
|
+
env: scenario.given.env,
|
|
105
|
+
verdict: { pass: false, checks: [{ clause: "given", pass: false, detail: envErr }] },
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const { verdict, env } = await evaluateOneScenario(scenario, buildResult, receipts);
|
|
111
|
+
results.push({ entityName, scenario, verdict, env });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (args.json) {
|
|
115
|
+
console.log(
|
|
116
|
+
JSON.stringify(
|
|
117
|
+
results.map((r) => ({ name: r.scenario.name, entity: r.entityName, env: r.env, ...r.verdict })),
|
|
118
|
+
null,
|
|
119
|
+
2,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
for (const r of results) printResult(r.entityName, r.scenario, r.env, r.verdict);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const failed = results.filter((r) => !r.verdict.pass).length;
|
|
127
|
+
const total = results.length;
|
|
128
|
+
if (failed > 0) {
|
|
129
|
+
console.error(formatError({ message: `${failed}/${total} scenario(s) failed` }));
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
console.log(formatSuccess(`${total}/${total} scenario(s) passed`));
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function printResult(entityName: string, scenario: ScenarioDeclaration, env: string, verdict: ScenarioVerdict): void {
|
|
137
|
+
const status = verdict.pass ? formatSuccess("PASS") : "FAIL";
|
|
138
|
+
console.log(`\n${formatBold(scenario.name)} [${entityName}] — given ${describeGiven(scenario.given)}${env ? `, env ${env}` : ""}: ${status}`);
|
|
139
|
+
for (const check of verdict.checks) {
|
|
140
|
+
if (check.pass) continue;
|
|
141
|
+
console.log(` ${check.clause}: ${check.detail ?? "failed"}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Human-readable rendering of a scenario's `given`, for output and error messages. */
|
|
146
|
+
function describeGiven(given: ScenarioGiven): string {
|
|
147
|
+
return given.kind === "file" ? given.path : `env "${given.env}"`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface GivenResolution {
|
|
151
|
+
env: string;
|
|
152
|
+
/** Fixture data per lexicon it covers. Empty when nothing could be read. */
|
|
153
|
+
perLexicon: Map<string, LifecycleSnapshot>;
|
|
154
|
+
/** Set when the fixture itself could not be resolved — the scenario fails on this alone. */
|
|
155
|
+
error?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Read `given`'s fixture data. Offline: a file read for `snapshot(path)`, a
|
|
159
|
+
* git-plumbing read of the already-fetched orphan branch for `snapshot(env)` —
|
|
160
|
+
* never a live provider call. */
|
|
161
|
+
async function resolveGiven(given: ScenarioGiven): Promise<GivenResolution> {
|
|
162
|
+
if (given.kind === "file") {
|
|
163
|
+
const abs = resolve(given.path);
|
|
164
|
+
let raw: string;
|
|
165
|
+
try {
|
|
166
|
+
raw = await readFile(abs, "utf8");
|
|
167
|
+
} catch {
|
|
168
|
+
return { env: "", perLexicon: new Map(), error: `fixture not found: ${given.path}` };
|
|
169
|
+
}
|
|
170
|
+
let snap: LifecycleSnapshot;
|
|
171
|
+
try {
|
|
172
|
+
snap = JSON.parse(raw) as LifecycleSnapshot;
|
|
173
|
+
} catch {
|
|
174
|
+
return { env: "", perLexicon: new Map(), error: `fixture is not valid JSON: ${given.path}` };
|
|
175
|
+
}
|
|
176
|
+
if (typeof snap.lexicon !== "string" || typeof snap.environment !== "string" || typeof snap.resources !== "object") {
|
|
177
|
+
return {
|
|
178
|
+
env: typeof snap.environment === "string" ? snap.environment : "",
|
|
179
|
+
perLexicon: new Map(),
|
|
180
|
+
error: `${given.path} is not a LifecycleSnapshot — missing lexicon/environment/resources`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return { env: snap.environment, perLexicon: new Map([[snap.lexicon, snap]]) };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const stored = await readEnvironmentSnapshots(given.env);
|
|
187
|
+
if (stored.size === 0) {
|
|
188
|
+
return {
|
|
189
|
+
env: given.env,
|
|
190
|
+
perLexicon: new Map(),
|
|
191
|
+
error: `no recorded snapshot for environment "${given.env}" on chant/lifecycle — record one with \`chant lifecycle snapshot ${given.env}\``,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const perLexicon = new Map<string, LifecycleSnapshot>();
|
|
195
|
+
for (const [key, content] of stored) {
|
|
196
|
+
const snap = JSON.parse(content) as LifecycleSnapshot;
|
|
197
|
+
perLexicon.set(snap.lexicon ?? key, snap);
|
|
198
|
+
}
|
|
199
|
+
return { env: given.env, perLexicon };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build the merged change set for one scenario and evaluate it. A lexicon the
|
|
204
|
+
* fixture has no data for is never silently read as "nothing declared, all
|
|
205
|
+
* absent" — every entity it declares is marked `unobserved` (#1089's own
|
|
206
|
+
* discipline: a hole the fixture cannot fill is a hole, not a guess).
|
|
207
|
+
*
|
|
208
|
+
* Effect receipts (#1832) get the same treatment `runLifecyclePlan` gives
|
|
209
|
+
* them (lifecycle.ts:1145,1184,1213-1242,1278-1280): a receipt joins the
|
|
210
|
+
* declared axis alongside ordinary resources, a {@link ReceiptReading} is
|
|
211
|
+
* built per receipt from the SAME fixture data (`observedNow`/`unobserved`)
|
|
212
|
+
* every other entity in this loop reads, and `planReceipts` +
|
|
213
|
+
* `mergeReceiptEntries` replace whatever the generic classification proposed
|
|
214
|
+
* for the receipt with the real `effect` classification before the change set
|
|
215
|
+
* ever reaches `evaluateScenario` — an unfired receipt never reads as a bare
|
|
216
|
+
* `create`, and a stale one never reads as a clean `noop`.
|
|
217
|
+
*/
|
|
218
|
+
async function evaluateOneScenario(
|
|
219
|
+
scenario: ScenarioDeclaration,
|
|
220
|
+
buildResult: BuildResult,
|
|
221
|
+
receipts: ReadonlyMap<string, EffectReceiptDeclaration>,
|
|
222
|
+
): Promise<{ verdict: ScenarioVerdict; env: string }> {
|
|
223
|
+
const resolved = await resolveGiven(scenario.given);
|
|
224
|
+
if (resolved.error) {
|
|
225
|
+
return {
|
|
226
|
+
env: resolved.env,
|
|
227
|
+
verdict: { pass: false, checks: [{ clause: "given", pass: false, detail: resolved.error }] },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const declaredByLexicon = new Map<string, Set<string>>();
|
|
232
|
+
for (const [name, entity] of buildResult.entities) {
|
|
233
|
+
// A receipt has no `props` payload of its own but is declared, diffed,
|
|
234
|
+
// and observed like any resource (#1832) — it joins the declared axis so
|
|
235
|
+
// its lexicon's fixture data can confirm presence, absence, or a hole,
|
|
236
|
+
// the same as `runLifecyclePlan` (lifecycle.ts:1184).
|
|
237
|
+
if (!isResourceDeclarable(entity) && !isEffectReceipt(entity)) continue;
|
|
238
|
+
if (!declaredByLexicon.has(entity.lexicon)) declaredByLexicon.set(entity.lexicon, new Set());
|
|
239
|
+
declaredByLexicon.get(entity.lexicon)!.add(name);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Every fixture lexicon's `resources` merged, for resolving a receipt's
|
|
243
|
+
// reference inputs against the fixture the same way `runLifecyclePlan`
|
|
244
|
+
// resolves them against its merged live observation (lifecycle.ts:1149,1213).
|
|
245
|
+
const allObservedResources: Record<string, ResourceMetadata> = {};
|
|
246
|
+
const receiptReadings = new Map<string, ReceiptReading>();
|
|
247
|
+
|
|
248
|
+
const lexicons = new Set<string>([...declaredByLexicon.keys(), ...resolved.perLexicon.keys()]);
|
|
249
|
+
const entries: ChangeSetEntry[] = [];
|
|
250
|
+
for (const lexiconName of lexicons) {
|
|
251
|
+
const declared = declaredByLexicon.get(lexiconName) ?? new Set<string>();
|
|
252
|
+
const fixture = resolved.perLexicon.get(lexiconName);
|
|
253
|
+
const observedNow = fixture?.resources ?? {};
|
|
254
|
+
const unobserved: Record<string, UnobservedEntity> = { ...(fixture?.unobserved ?? {}) };
|
|
255
|
+
if (!fixture) {
|
|
256
|
+
for (const name of declared) {
|
|
257
|
+
if (!(name in unobserved)) {
|
|
258
|
+
unobserved[name] = {
|
|
259
|
+
reason: "filtered",
|
|
260
|
+
detail: `given ${describeGiven(scenario.given)} has no recorded data for lexicon "${lexiconName}"`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
Object.assign(allObservedResources, observedNow);
|
|
267
|
+
for (const [receiptName, receipt] of receipts) {
|
|
268
|
+
if (receipt.lexicon !== lexiconName) continue;
|
|
269
|
+
const live = observedNow[receiptName];
|
|
270
|
+
const hole = unobserved[receiptName];
|
|
271
|
+
if (live) {
|
|
272
|
+
receiptReadings.set(receiptName, {
|
|
273
|
+
observed: true,
|
|
274
|
+
present: true,
|
|
275
|
+
value: readReceiptValue(live.attributes),
|
|
276
|
+
type: live.type,
|
|
277
|
+
...(live.physicalId ? { physicalId: live.physicalId } : {}),
|
|
278
|
+
lexicon: lexiconName,
|
|
279
|
+
});
|
|
280
|
+
} else if (hole) {
|
|
281
|
+
receiptReadings.set(receiptName, {
|
|
282
|
+
observed: false,
|
|
283
|
+
present: false,
|
|
284
|
+
lexicon: lexiconName,
|
|
285
|
+
...(hole.type ? { type: hole.type } : {}),
|
|
286
|
+
unobservedReason: hole.reason,
|
|
287
|
+
...(hole.detail ? { unobservedDetail: hole.detail } : {}),
|
|
288
|
+
});
|
|
289
|
+
} else {
|
|
290
|
+
// Neither in the fixture's resources nor named unobserved: the
|
|
291
|
+
// fixture stands in for a live read that confirmed the receipt
|
|
292
|
+
// absent — the same claim `buildChangeSet` reads off a bare
|
|
293
|
+
// observation (#1089).
|
|
294
|
+
receiptReadings.set(receiptName, { observed: true, present: false, lexicon: lexiconName });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// No `observedThen`: a scenario has one fixture, standing in for live
|
|
299
|
+
// observation only — never a second, prior read to diff against. See this
|
|
300
|
+
// module's top comment on what that means for `update`.
|
|
301
|
+
const cs = buildChangeSet(
|
|
302
|
+
resolved.env,
|
|
303
|
+
{ declared, observedNow, observedThen: undefined, unobserved },
|
|
304
|
+
{ lexicon: lexiconName },
|
|
305
|
+
);
|
|
306
|
+
entries.push(...cs.entries);
|
|
307
|
+
}
|
|
308
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
309
|
+
|
|
310
|
+
const merged: ChangeSet = { env: resolved.env, entries };
|
|
311
|
+
// Receipt classification (#1832): replace whatever the generic pass above
|
|
312
|
+
// proposed for a receipt — a `create` for one confirmed absent, a `noop`
|
|
313
|
+
// for one present but stale — with the real `effect` classification, the
|
|
314
|
+
// same replacement `runLifecyclePlan` performs (lifecycle.ts:1278-1280).
|
|
315
|
+
if (receipts.size > 0) {
|
|
316
|
+
const receiptEntries = planReceipts(receipts, receiptReadings, observedValueResolver(allObservedResources));
|
|
317
|
+
mergeReceiptEntries(merged, receipts, receiptEntries);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { env: resolved.env, verdict: evaluateScenario(merged, scenario.expect) };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Fallback for `chant scenario <unknown subcommand>` — mirrors `runLifecycleUnknown`. */
|
|
324
|
+
export async function runScenarioUnknown(ctx: CommandContext): Promise<number> {
|
|
325
|
+
console.error(formatError({
|
|
326
|
+
message: `Unknown scenario subcommand: ${ctx.args.extraPositional ?? ctx.args.path}`,
|
|
327
|
+
hint: "Available: chant scenario check",
|
|
328
|
+
}));
|
|
329
|
+
return 1;
|
|
330
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type { ParsedArgs } from "../registry";
|
|
3
|
+
import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
|
|
4
|
+
import { createMockPlugin } from "../../../../test-utils/src/mock-plugin";
|
|
5
|
+
|
|
6
|
+
// #1268 — query-scoped drift: `chant search --at latest --check-live` (and its
|
|
7
|
+
// reverse, `--live --check-snapshot`) compares the matched rows against the
|
|
8
|
+
// observation the primary answer did NOT use, reusing `diffLive` — the same
|
|
9
|
+
// engine `lifecycle diff --live` uses — scoped to just those rows.
|
|
10
|
+
|
|
11
|
+
const discoverMock = vi.fn();
|
|
12
|
+
vi.mock("../../discovery/index", () => ({
|
|
13
|
+
discover: (...a: unknown[]) => discoverMock(...a),
|
|
14
|
+
}));
|
|
15
|
+
const loadPluginsMock = vi.fn();
|
|
16
|
+
const resolveLexMock = vi.fn();
|
|
17
|
+
vi.mock("../plugins", async () => {
|
|
18
|
+
const actual = await vi.importActual<typeof import("../plugins")>("../plugins");
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
loadPlugins: (...a: unknown[]) => loadPluginsMock(...a),
|
|
22
|
+
resolveProjectLexicons: (...a: unknown[]) => resolveLexMock(...a),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
const replaySnapshotsMock = vi.fn();
|
|
26
|
+
const hasSnapshotMock = vi.fn((..._a: unknown[]) => Promise.resolve(false));
|
|
27
|
+
vi.mock("../../lifecycle/replay", () => ({
|
|
28
|
+
replaySnapshots: (...a: unknown[]) => replaySnapshotsMock(...a),
|
|
29
|
+
hasSnapshot: (...a: unknown[]) => hasSnapshotMock(...a),
|
|
30
|
+
}));
|
|
31
|
+
const loadChantConfigMock = vi.fn();
|
|
32
|
+
vi.mock("../../config", async () => {
|
|
33
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
34
|
+
return { ...actual, loadChantConfig: (...a: unknown[]) => loadChantConfigMock(...a) };
|
|
35
|
+
});
|
|
36
|
+
const buildResultMock = vi.fn();
|
|
37
|
+
vi.mock("../../build", async () => {
|
|
38
|
+
const actual = await vi.importActual<typeof import("../../build")>("../../build");
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
build: (...a: unknown[]) => Promise.resolve(buildResultMock(...a)),
|
|
42
|
+
buildProject: (...a: unknown[]) => Promise.resolve(buildResultMock(...a)),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const { runSearch } = await import("./search");
|
|
47
|
+
|
|
48
|
+
function decl<T extends object>(base: T): Declarable & T {
|
|
49
|
+
return { [DECLARABLE_MARKER]: true, ...base } as Declarable & T;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entities = new Map<string, Declarable>([
|
|
53
|
+
["webServer", decl({ lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} })],
|
|
54
|
+
["launchTemplateServer", decl({ lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} })],
|
|
55
|
+
["dbServer", decl({ lexicon: "aws", entityType: "AWS::RDS::Instance", props: {} })],
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
|
|
59
|
+
return {
|
|
60
|
+
command: "search", path: "kind:Instance",
|
|
61
|
+
format: "", fix: false, watch: false, verbose: false, help: false, live: false, env: "dev",
|
|
62
|
+
...overrides,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("search query-scoped drift (#1268)", () => {
|
|
67
|
+
let out: string[];
|
|
68
|
+
let err: string[];
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
out = [];
|
|
72
|
+
err = [];
|
|
73
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { out.push(s); });
|
|
74
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { err.push(s); });
|
|
75
|
+
discoverMock.mockReset();
|
|
76
|
+
discoverMock.mockResolvedValue({ entities, errors: [], sourceFiles: [] });
|
|
77
|
+
buildResultMock.mockReset();
|
|
78
|
+
buildResultMock.mockReturnValue({ errors: [], entities, outputs: new Map([["aws", ""]]), warnings: [] });
|
|
79
|
+
loadChantConfigMock.mockReset();
|
|
80
|
+
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
81
|
+
resolveLexMock.mockReset();
|
|
82
|
+
resolveLexMock.mockResolvedValue(["aws"]);
|
|
83
|
+
hasSnapshotMock.mockReset();
|
|
84
|
+
hasSnapshotMock.mockResolvedValue(false);
|
|
85
|
+
replaySnapshotsMock.mockReset();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("--check-live without --at is refused", async () => {
|
|
89
|
+
const exit = await runSearch({ args: makeArgs({ checkLive: true }), plugins: [], serializers: [] });
|
|
90
|
+
expect(exit).toBe(1);
|
|
91
|
+
expect(err.join("\n")).toContain("--check-live needs --at");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("--check-snapshot without --live is refused", async () => {
|
|
95
|
+
const exit = await runSearch({ args: makeArgs({ checkSnapshot: true }), plugins: [], serializers: [] });
|
|
96
|
+
expect(exit).toBe(1);
|
|
97
|
+
expect(err.join("\n")).toContain("--check-snapshot needs --live");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("--fail-on-drift without a check flag is refused", async () => {
|
|
101
|
+
const exit = await runSearch({ args: makeArgs({ failOnDrift: true }), plugins: [], serializers: [] });
|
|
102
|
+
expect(exit).toBe(1);
|
|
103
|
+
expect(err.join("\n")).toContain("--fail-on-drift needs --check-live or --check-snapshot");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("--at --check-live: drift only for matched rows, using diffLive's categories", async () => {
|
|
107
|
+
replaySnapshotsMock.mockResolvedValue({
|
|
108
|
+
observations: [{
|
|
109
|
+
lexicon: "aws",
|
|
110
|
+
resources: {
|
|
111
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK", attributes: { SubnetId: "subnet-old" } },
|
|
112
|
+
launchTemplateServer: { type: "AWS::EC2::Instance", physicalId: "i-2", status: "OK", attributes: {} },
|
|
113
|
+
},
|
|
114
|
+
}],
|
|
115
|
+
commit: "a1b2c3d4e5f6", timestamp: "2026-08-01T03:15:00.000Z", depth: "identity",
|
|
116
|
+
});
|
|
117
|
+
loadPluginsMock.mockResolvedValue([
|
|
118
|
+
createMockPlugin({
|
|
119
|
+
name: "aws",
|
|
120
|
+
describeResources: async () => ({
|
|
121
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK", attributes: { SubnetId: "subnet-new" } },
|
|
122
|
+
launchTemplateServer: { type: "AWS::EC2::Instance", physicalId: "i-2", status: "OK", attributes: {} },
|
|
123
|
+
dbServer: { type: "AWS::RDS::Instance", physicalId: "db-1", status: "OK" },
|
|
124
|
+
}),
|
|
125
|
+
}),
|
|
126
|
+
]);
|
|
127
|
+
const exit = await runSearch({
|
|
128
|
+
args: makeArgs({ path: "kind:EC2::Instance", at: "latest", checkLive: true }),
|
|
129
|
+
plugins: [], serializers: [],
|
|
130
|
+
});
|
|
131
|
+
expect(exit).toBe(0);
|
|
132
|
+
const stdout = out.join("\n");
|
|
133
|
+
expect(stdout).toContain("webServer attributes.SubnetId: subnet-old → subnet-new — drifted");
|
|
134
|
+
expect(stdout).toContain("checked against a live read · 1 of 2 matched drifted");
|
|
135
|
+
// dbServer isn't matched by kind:EC2::Instance and must never enter the diff.
|
|
136
|
+
expect(stdout).not.toContain("dbServer");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("--fail-on-drift exits non-zero when the scoped check finds drift", async () => {
|
|
140
|
+
replaySnapshotsMock.mockResolvedValue({
|
|
141
|
+
observations: [{ lexicon: "aws", resources: { webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK" } } }],
|
|
142
|
+
commit: "a1b2c3d", timestamp: "t", depth: "identity",
|
|
143
|
+
});
|
|
144
|
+
loadPluginsMock.mockResolvedValue([
|
|
145
|
+
createMockPlugin({
|
|
146
|
+
name: "aws",
|
|
147
|
+
describeResources: async () => ({
|
|
148
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "UPDATING" },
|
|
149
|
+
}),
|
|
150
|
+
}),
|
|
151
|
+
]);
|
|
152
|
+
const exit = await runSearch({
|
|
153
|
+
args: makeArgs({ path: "kind:EC2::Instance", at: "latest", checkLive: true, failOnDrift: true }),
|
|
154
|
+
plugins: [], serializers: [],
|
|
155
|
+
});
|
|
156
|
+
expect(exit).toBe(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("no drift exits 0 even with --fail-on-drift", async () => {
|
|
160
|
+
replaySnapshotsMock.mockResolvedValue({
|
|
161
|
+
observations: [{ lexicon: "aws", resources: { webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK" } } }],
|
|
162
|
+
commit: "a1b2c3d", timestamp: "t", depth: "identity",
|
|
163
|
+
});
|
|
164
|
+
loadPluginsMock.mockResolvedValue([
|
|
165
|
+
createMockPlugin({
|
|
166
|
+
name: "aws",
|
|
167
|
+
describeResources: async () => ({
|
|
168
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK" },
|
|
169
|
+
}),
|
|
170
|
+
}),
|
|
171
|
+
]);
|
|
172
|
+
const exit = await runSearch({
|
|
173
|
+
args: makeArgs({ path: "webServer", at: "latest", checkLive: true, failOnDrift: true }),
|
|
174
|
+
plugins: [], serializers: [],
|
|
175
|
+
});
|
|
176
|
+
expect(exit).toBe(0);
|
|
177
|
+
expect(out.join("\n")).toContain("no drift across 1 matched");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("--live --check-snapshot: a resource newly observed since the snapshot", async () => {
|
|
181
|
+
loadPluginsMock.mockResolvedValue([
|
|
182
|
+
createMockPlugin({
|
|
183
|
+
name: "aws",
|
|
184
|
+
describeResources: async () => ({
|
|
185
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK" },
|
|
186
|
+
}),
|
|
187
|
+
}),
|
|
188
|
+
]);
|
|
189
|
+
replaySnapshotsMock.mockResolvedValue({
|
|
190
|
+
observations: [{ lexicon: "aws", resources: {} }],
|
|
191
|
+
commit: "a1b2c3d", timestamp: "t", depth: "identity",
|
|
192
|
+
});
|
|
193
|
+
const exit = await runSearch({
|
|
194
|
+
args: makeArgs({ path: "webServer", live: true, checkSnapshot: true }),
|
|
195
|
+
plugins: [], serializers: [],
|
|
196
|
+
});
|
|
197
|
+
expect(exit).toBe(0);
|
|
198
|
+
expect(out.join("\n")).toContain("webServer — newly observed since the recorded snapshot");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("--check-snapshot with nothing recorded is a note, not a failure", async () => {
|
|
202
|
+
loadPluginsMock.mockResolvedValue([
|
|
203
|
+
createMockPlugin({
|
|
204
|
+
name: "aws",
|
|
205
|
+
describeResources: async () => ({
|
|
206
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK" },
|
|
207
|
+
}),
|
|
208
|
+
}),
|
|
209
|
+
]);
|
|
210
|
+
replaySnapshotsMock.mockResolvedValue({ error: `No snapshots found for environment "dev"` });
|
|
211
|
+
const exit = await runSearch({
|
|
212
|
+
args: makeArgs({ path: "kind:EC2::Instance", live: true, checkSnapshot: true }),
|
|
213
|
+
plugins: [], serializers: [],
|
|
214
|
+
});
|
|
215
|
+
expect(exit).toBe(0);
|
|
216
|
+
expect(err.join("\n")).toContain("--check-snapshot: No snapshots found");
|
|
217
|
+
expect(out.join("\n")).not.toContain("checked against");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("a live read that fails during --check-live reports unobserved, never missing (#1089)", async () => {
|
|
221
|
+
replaySnapshotsMock.mockResolvedValue({
|
|
222
|
+
observations: [{ lexicon: "aws", resources: { webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK" } } }],
|
|
223
|
+
commit: "a1b2c3d", timestamp: "t", depth: "identity",
|
|
224
|
+
});
|
|
225
|
+
loadPluginsMock.mockResolvedValue([
|
|
226
|
+
createMockPlugin({
|
|
227
|
+
name: "aws",
|
|
228
|
+
describeResources: async () => { throw new Error("ECONNREFUSED"); },
|
|
229
|
+
}),
|
|
230
|
+
]);
|
|
231
|
+
const exit = await runSearch({
|
|
232
|
+
args: makeArgs({ path: "kind:EC2::Instance", at: "latest", checkLive: true }),
|
|
233
|
+
plugins: [], serializers: [],
|
|
234
|
+
});
|
|
235
|
+
// The check-live read itself failed, which is the existing --live failure
|
|
236
|
+
// path (#1263) — a real problem distinct from any drift verdict.
|
|
237
|
+
expect(exit).toBe(1);
|
|
238
|
+
const stdout = out.join("\n");
|
|
239
|
+
expect(stdout).toContain("? webServer");
|
|
240
|
+
expect(stdout).not.toContain("missing");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("depth note: a deep-recorded snapshot compared here at identity says so", async () => {
|
|
244
|
+
replaySnapshotsMock.mockResolvedValue({
|
|
245
|
+
observations: [{ lexicon: "aws", resources: { webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK" } } }],
|
|
246
|
+
commit: "a1b2c3d", timestamp: "t", depth: "deep",
|
|
247
|
+
});
|
|
248
|
+
loadPluginsMock.mockResolvedValue([
|
|
249
|
+
createMockPlugin({
|
|
250
|
+
name: "aws",
|
|
251
|
+
describeResources: async () => ({
|
|
252
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK" },
|
|
253
|
+
}),
|
|
254
|
+
}),
|
|
255
|
+
]);
|
|
256
|
+
const exit = await runSearch({
|
|
257
|
+
args: makeArgs({ path: "webServer", at: "latest", checkLive: true }),
|
|
258
|
+
plugins: [], serializers: [],
|
|
259
|
+
});
|
|
260
|
+
expect(exit).toBe(0);
|
|
261
|
+
expect(out.join("\n")).toContain("snapshot recorded at deep depth, compared here at identity");
|
|
262
|
+
});
|
|
263
|
+
});
|