@intentius/chant 0.44.13 → 0.45.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/discover.d.ts +26 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +10 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +16 -7
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +20 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +9 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +28 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +27 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/codegen/docs-pages.d.ts +39 -0
- package/dist/codegen/docs-pages.d.ts.map +1 -0
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-sidebar.d.ts +13 -2
- package/dist/codegen/docs-sidebar.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +30 -10
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +14 -2
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/surface-snapshot.d.ts +27 -3
- package/dist/codegen/surface-snapshot.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +11 -3
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +13 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/config.d.ts +57 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +100 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +12 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ops.d.ts +17 -0
- package/dist/graph-ops.d.ts.map +1 -0
- package/dist/lifecycle/change-set.d.ts +23 -2
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +8 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +7 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/managed-fields.d.ts +33 -21
- package/dist/managed-fields.d.ts.map +1 -1
- package/dist/observation.d.ts +23 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/yaml.d.ts +0 -8
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +20 -0
- package/src/audit/discover.test.ts +55 -1
- package/src/audit/discover.ts +70 -2
- package/src/audit/edge-init-safety.test.ts +149 -18
- package/src/audit/report-model.ts +12 -1
- package/src/audit/rules-doc.ts +1 -0
- package/src/build-params.test.ts +52 -0
- package/src/cli/build-params-cli.test.ts +25 -8
- package/src/cli/build-params-cli.ts +37 -23
- package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
- package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
- package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
- package/src/cli/commands/audit.test.ts +83 -1
- package/src/cli/commands/audit.ts +121 -28
- package/src/cli/commands/build.test.ts +144 -5
- package/src/cli/commands/build.ts +66 -14
- package/src/cli/commands/check-lexicon-docs.ts +30 -0
- package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
- package/src/cli/commands/check-lexicon-examples.ts +66 -17
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +19 -4
- package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
- package/src/cli/commands/init-lexicon.test.ts +42 -1
- package/src/cli/commands/init-lexicon.ts +3 -1
- package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.ts +1 -1
- package/src/cli/commands/onboard.test.ts +319 -225
- package/src/cli/commands/onboard.ts +144 -58
- package/src/cli/handlers/build.test.ts +3 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/graph.test.ts +44 -0
- package/src/cli/handlers/graph.ts +22 -2
- package/src/cli/handlers/lifecycle.test.ts +117 -0
- package/src/cli/handlers/lifecycle.ts +16 -0
- package/src/cli/handlers/run.test.ts +5 -5
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/handlers/search-live.test.ts +217 -0
- package/src/cli/handlers/search.test.ts +41 -1
- package/src/cli/handlers/search.ts +55 -6
- package/src/cli/main.ts +5 -3
- package/src/cli/security-boundary.test.ts +2 -1
- package/src/codegen/docs-pages.test.ts +194 -0
- package/src/codegen/docs-pages.ts +138 -0
- package/src/codegen/docs-sections.ts +0 -8
- package/src/codegen/docs-sidebar.ts +50 -37
- package/src/codegen/docs-types.ts +32 -4
- package/src/codegen/docs.ts +78 -40
- package/src/codegen/lexicon-regen.ts +3 -1
- package/src/codegen/rolling-upgrade.test.ts +1 -1
- package/src/codegen/rolling-upgrade.ts +1 -1
- package/src/codegen/surface-snapshot.test.ts +139 -0
- package/src/codegen/surface-snapshot.ts +297 -122
- package/src/codegen/validate.test.ts +16 -3
- package/src/codegen/validate.ts +17 -7
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability-plugin.ts +23 -2
- package/src/components/driver.test.ts +45 -0
- package/src/components/driver.ts +64 -27
- package/src/components/index.ts +2 -0
- package/src/components/starter-plugin.ts +5 -2
- package/src/config.test.ts +56 -1
- package/src/config.ts +152 -8
- package/src/discovery/fold-import.test.ts +427 -2
- package/src/discovery/fold-import.ts +163 -7
- package/src/discovery/sandbox/driver.test.ts +11 -0
- package/src/fold/fold.test.ts +41 -0
- package/src/fold/fold.ts +372 -2
- package/src/graph-ir-live.test.ts +42 -0
- package/src/graph-ir.ts +42 -2
- package/src/graph-ops.test.ts +69 -0
- package/src/graph-ops.ts +42 -0
- package/src/lifecycle/change-set.test.ts +41 -0
- package/src/lifecycle/change-set.ts +30 -2
- package/src/lifecycle/deep-observe.ts +4 -0
- package/src/lifecycle/observe.ts +29 -2
- package/src/lifecycle/replay.ts +3 -0
- package/src/lifecycle/snapshot.ts +3 -0
- package/src/lifecycle/types.ts +7 -0
- package/src/lint/policy.ts +14 -3
- package/src/managed-fields.test.ts +15 -42
- package/src/managed-fields.ts +43 -26
- package/src/observation.test.ts +24 -8
- package/src/observation.ts +48 -4
- package/src/op/builders-exports.test.ts +60 -0
- package/src/op/index.ts +1 -1
- package/src/ownership.test.ts +63 -1
- package/src/yaml.test.ts +15 -0
- package/src/yaml.ts +19 -2
|
@@ -416,6 +416,7 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
416
416
|
buildResult,
|
|
417
417
|
json,
|
|
418
418
|
stack: target.stack,
|
|
419
|
+
region: target.region,
|
|
419
420
|
componentStacks,
|
|
420
421
|
baseline,
|
|
421
422
|
updateBaseline: args.updateBaseline,
|
|
@@ -650,6 +651,11 @@ interface LiveDiffArgs {
|
|
|
650
651
|
/** Deployed stack name for a multi-stack project (#932); scopes the live
|
|
651
652
|
* observation (which CloudFormation stack to query) and the snapshot read. */
|
|
652
653
|
stack?: string;
|
|
654
|
+
/** Region that stack is deployed in, from `stacks[].region` (#1264). Same
|
|
655
|
+
* contract as snapshot (#1261): without it every stack is read against the
|
|
656
|
+
* ambient region, so a multi-region estate reports its out-of-region stacks
|
|
657
|
+
* as absent rather than unobserved. */
|
|
658
|
+
region?: string;
|
|
653
659
|
/** Component projects deploy one CFN stack per component; observe them all and
|
|
654
660
|
* union (the same fix graph/plan use), else every deployed resource reads as
|
|
655
661
|
* "missing". Empty → the single-stack observe path. */
|
|
@@ -710,6 +716,9 @@ async function observeLexicon(
|
|
|
710
716
|
declared: Set<string>;
|
|
711
717
|
entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
|
|
712
718
|
stack?: string;
|
|
719
|
+
/** Region the stack is deployed in (#1264); the read targets it instead of
|
|
720
|
+
* the ambient region. */
|
|
721
|
+
region?: string;
|
|
713
722
|
componentStacks?: string[];
|
|
714
723
|
owned?: boolean;
|
|
715
724
|
/** `--namespace <ns>` (#1629): where to read an entity that declares no
|
|
@@ -723,6 +732,7 @@ async function observeLexicon(
|
|
|
723
732
|
buildOutput: opts.buildOutput,
|
|
724
733
|
entityNames,
|
|
725
734
|
entities: opts.entities,
|
|
735
|
+
...(opts.region ? { region: opts.region } : {}),
|
|
726
736
|
...(opts.owned !== undefined ? { owned: opts.owned } : {}),
|
|
727
737
|
...(opts.namespace ? { namespace: opts.namespace } : {}),
|
|
728
738
|
};
|
|
@@ -746,6 +756,7 @@ async function observeLexicon(
|
|
|
746
756
|
resources: {},
|
|
747
757
|
unobserved: unobservedAll(entityNames, "read-failed", message, opts.entities),
|
|
748
758
|
queried: {},
|
|
759
|
+
notes: [],
|
|
749
760
|
};
|
|
750
761
|
}
|
|
751
762
|
}
|
|
@@ -811,6 +822,7 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<LiveDiffOutcome
|
|
|
811
822
|
declared,
|
|
812
823
|
entities,
|
|
813
824
|
stack: args.stack,
|
|
825
|
+
region: args.region,
|
|
814
826
|
componentStacks: args.componentStacks,
|
|
815
827
|
...(args.namespace ? { namespace: args.namespace } : {}),
|
|
816
828
|
});
|
|
@@ -839,6 +851,7 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<LiveDiffOutcome
|
|
|
839
851
|
buildOutput,
|
|
840
852
|
entities,
|
|
841
853
|
stack: args.stack,
|
|
854
|
+
region: args.region,
|
|
842
855
|
componentStacks: args.componentStacks,
|
|
843
856
|
baseline: baselineForLexicon(args.baseline, lexiconName),
|
|
844
857
|
});
|
|
@@ -1181,6 +1194,9 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1181
1194
|
// The resolved read address rides every entry, not just unobserved
|
|
1182
1195
|
// ones — the diff path already passes it (#1620); plan lost it.
|
|
1183
1196
|
queried: observed.queried,
|
|
1197
|
+
}, {
|
|
1198
|
+
// Attribution survives the flat merge below (#1674).
|
|
1199
|
+
lexicon: lexiconName,
|
|
1184
1200
|
});
|
|
1185
1201
|
merged.entries.push(...cs.entries);
|
|
1186
1202
|
checked++;
|
|
@@ -101,7 +101,7 @@ function makeOp(name: string, depends: string[] = []): [string, { config: { name
|
|
|
101
101
|
function setupTemporalClient(mock: ReturnType<typeof createMockTemporalClient>) {
|
|
102
102
|
loadTemporalClientMock.mockResolvedValue({
|
|
103
103
|
Connection: { connect: vi.fn(async () => ({})) },
|
|
104
|
-
Client: vi.fn(()
|
|
104
|
+
Client: vi.fn(function () { return mock.client; }) as unknown as new () => unknown,
|
|
105
105
|
});
|
|
106
106
|
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
107
107
|
resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
|
|
@@ -951,9 +951,9 @@ describe("runOpComponents", () => {
|
|
|
951
951
|
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", expect.objectContaining({
|
|
952
952
|
buildParams: [{ name: "tier", value: "light", source: "default" }],
|
|
953
953
|
}));
|
|
954
|
-
//
|
|
955
|
-
//
|
|
956
|
-
expect(stderr.join("\n")).toContain("
|
|
954
|
+
// The echo is a one-line count (#1424); the provenance forwarded above
|
|
955
|
+
// is where the value is asserted.
|
|
956
|
+
expect(stderr.join("\n")).toContain("1 build parameter resolved (1 default)");
|
|
957
957
|
});
|
|
958
958
|
|
|
959
959
|
test("--param overrides a declared default and is threaded through to runComponents", async () => {
|
|
@@ -978,7 +978,7 @@ describe("runOpComponents", () => {
|
|
|
978
978
|
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", expect.objectContaining({
|
|
979
979
|
buildParams: [{ name: "tier", value: "production", source: "cli" }],
|
|
980
980
|
}));
|
|
981
|
-
expect(stderr.join("\n")).toContain("
|
|
981
|
+
expect(stderr.join("\n")).toContain("1 build parameter resolved (1 from cli)");
|
|
982
982
|
vi.restoreAllMocks();
|
|
983
983
|
});
|
|
984
984
|
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -714,6 +714,7 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
714
714
|
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
715
715
|
cli: parseParamFlags(ctx.args.param),
|
|
716
716
|
paramsFile: ctx.args.paramsFile,
|
|
717
|
+
verbose: ctx.args.verbose,
|
|
717
718
|
});
|
|
718
719
|
if (!paramsResolution.success) {
|
|
719
720
|
for (const message of paramsResolution.errors) console.error(message);
|
|
@@ -0,0 +1,217 @@
|
|
|
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
|
+
// #1263 — `chant search --live` against an unreachable endpoint returned the
|
|
7
|
+
// declared rows, exit 0, and a footer naming derived facts it never computed.
|
|
8
|
+
// The observe layer already records a thrown read as NOT-OBSERVED
|
|
9
|
+
// (`read-failed`, #1089); these tests pin that the handler carries it through
|
|
10
|
+
// to the rows, the footer, stderr, and the exit code. `observeResources` is
|
|
11
|
+
// real here so the plumbing from a throwing `describeResources` is exercised.
|
|
12
|
+
|
|
13
|
+
const discoverMock = vi.fn();
|
|
14
|
+
vi.mock("../../discovery/index", () => ({
|
|
15
|
+
discover: (...a: unknown[]) => discoverMock(...a),
|
|
16
|
+
}));
|
|
17
|
+
const loadPluginsMock = vi.fn();
|
|
18
|
+
const resolveLexMock = vi.fn();
|
|
19
|
+
vi.mock("../plugins", async () => {
|
|
20
|
+
const actual = await vi.importActual<typeof import("../plugins")>("../plugins");
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
loadPlugins: (...a: unknown[]) => loadPluginsMock(...a),
|
|
24
|
+
resolveProjectLexicons: (...a: unknown[]) => resolveLexMock(...a),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
const hasSnapshotMock = vi.fn((..._a: unknown[]) => Promise.resolve(false));
|
|
28
|
+
vi.mock("../../lifecycle/replay", () => ({
|
|
29
|
+
replaySnapshots: vi.fn(),
|
|
30
|
+
hasSnapshot: (...a: unknown[]) => hasSnapshotMock(...a),
|
|
31
|
+
}));
|
|
32
|
+
const loadChantConfigMock = vi.fn();
|
|
33
|
+
vi.mock("../../config", async () => {
|
|
34
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
35
|
+
return { ...actual, loadChantConfig: (...a: unknown[]) => loadChantConfigMock(...a) };
|
|
36
|
+
});
|
|
37
|
+
const buildResultMock = vi.fn();
|
|
38
|
+
vi.mock("../../build", async () => {
|
|
39
|
+
const actual = await vi.importActual<typeof import("../../build")>("../../build");
|
|
40
|
+
return {
|
|
41
|
+
...actual,
|
|
42
|
+
build: (...a: unknown[]) => Promise.resolve(buildResultMock(...a)),
|
|
43
|
+
buildProject: (...a: unknown[]) => Promise.resolve(buildResultMock(...a)),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const { runSearch } = await import("./search");
|
|
48
|
+
|
|
49
|
+
function decl<T extends object>(base: T): Declarable & T {
|
|
50
|
+
return { [DECLARABLE_MARKER]: true, ...base } as Declarable & T;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
|
|
54
|
+
return {
|
|
55
|
+
command: "search", path: "kind:Instance",
|
|
56
|
+
format: "", fix: false, watch: false, verbose: false, help: false, live: true, env: "dev",
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("search --live when the live read fails (#1263)", () => {
|
|
62
|
+
let out: string[];
|
|
63
|
+
let err: string[];
|
|
64
|
+
const entities = new Map<string, Declarable>([
|
|
65
|
+
["webServer", decl({ lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} })],
|
|
66
|
+
["privateServer", decl({ lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} })],
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
out = [];
|
|
71
|
+
err = [];
|
|
72
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { out.push(s); });
|
|
73
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { err.push(s); });
|
|
74
|
+
discoverMock.mockReset();
|
|
75
|
+
discoverMock.mockResolvedValue({ entities, errors: [], sourceFiles: [] });
|
|
76
|
+
buildResultMock.mockReset();
|
|
77
|
+
buildResultMock.mockReturnValue({ errors: [], entities, outputs: new Map([["aws", ""]]), warnings: [] });
|
|
78
|
+
loadChantConfigMock.mockReset();
|
|
79
|
+
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
80
|
+
resolveLexMock.mockReset();
|
|
81
|
+
resolveLexMock.mockResolvedValue(["aws"]);
|
|
82
|
+
hasSnapshotMock.mockReset();
|
|
83
|
+
hasSnapshotMock.mockResolvedValue(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const deadEndpoint = createMockPlugin({
|
|
87
|
+
name: "aws",
|
|
88
|
+
describeResources: async () => {
|
|
89
|
+
throw new Error("connect ECONNREFUSED 127.0.0.1:9999");
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("exits non-zero, names the lexicon and cause, and marks every row unobserved", async () => {
|
|
94
|
+
loadPluginsMock.mockResolvedValue([deadEndpoint]);
|
|
95
|
+
const exit = await runSearch({ args: makeArgs(), plugins: [], serializers: [] });
|
|
96
|
+
expect(exit).toBe(1);
|
|
97
|
+
const stderr = err.join("\n");
|
|
98
|
+
expect(stderr).toContain("aws");
|
|
99
|
+
expect(stderr).toContain("ECONNREFUSED 127.0.0.1:9999");
|
|
100
|
+
expect(stderr).toContain("declared-only");
|
|
101
|
+
const stdout = out.join("\n");
|
|
102
|
+
expect(stdout).toContain("webServer AWS::EC2::Instance (unobserved: read-failed)");
|
|
103
|
+
expect(stdout).toContain("privateServer AWS::EC2::Instance (unobserved: read-failed)");
|
|
104
|
+
expect(stdout).toContain("live read failed (aws) · 2/2 rows unobserved");
|
|
105
|
+
// Nothing was read, so no fold over live topology could have run.
|
|
106
|
+
expect(stdout).not.toContain("also derived");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("points at a recorded snapshot when one exists", async () => {
|
|
110
|
+
loadPluginsMock.mockResolvedValue([deadEndpoint]);
|
|
111
|
+
hasSnapshotMock.mockResolvedValue(true);
|
|
112
|
+
const exit = await runSearch({ args: makeArgs(), plugins: [], serializers: [] });
|
|
113
|
+
expect(exit).toBe(1);
|
|
114
|
+
expect(out.join("\n")).toContain("--at latest");
|
|
115
|
+
expect(err.join("\n")).toContain("--at latest");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a working live read still exits 0 with bound rows", async () => {
|
|
119
|
+
loadPluginsMock.mockResolvedValue([
|
|
120
|
+
createMockPlugin({
|
|
121
|
+
name: "aws",
|
|
122
|
+
describeResources: async () => ({
|
|
123
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK", owned: true },
|
|
124
|
+
privateServer: { type: "AWS::EC2::Instance", physicalId: "i-2", status: "OK", owned: true },
|
|
125
|
+
}),
|
|
126
|
+
}),
|
|
127
|
+
]);
|
|
128
|
+
const exit = await runSearch({ args: makeArgs(), plugins: [], serializers: [] });
|
|
129
|
+
expect(exit).toBe(0);
|
|
130
|
+
const stdout = out.join("\n");
|
|
131
|
+
expect(stdout).toContain("webServer AWS::EC2::Instance i-1");
|
|
132
|
+
expect(stdout).toContain("observed live · bound 2/2");
|
|
133
|
+
expect(err.join("\n")).not.toContain("live read failed");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// #1265 — a lexicon's run-level note ("ownership filter unavailable on this
|
|
138
|
+
// path") printed once per stack, ahead of the rows. It is one fact about the
|
|
139
|
+
// read, so it is said once, and it qualifies the answer, so it follows it.
|
|
140
|
+
describe("search --live run-level notes (#1265)", () => {
|
|
141
|
+
const NOTE =
|
|
142
|
+
"ownership filter unavailable on describeResources (no tags from describe-stack-resources) — returning all, each with an explicit `unknown` verdict; use `chant import --from <env> --owned` for ownership-filtered export";
|
|
143
|
+
let out: string[];
|
|
144
|
+
let err: string[];
|
|
145
|
+
// One log of everything, in the order it was written, so the test can see
|
|
146
|
+
// whether the note came before or after the rows.
|
|
147
|
+
let all: string[];
|
|
148
|
+
const entities = new Map<string, Declarable>([
|
|
149
|
+
["webServer", decl({ lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} })],
|
|
150
|
+
["privateServer", decl({ lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} })],
|
|
151
|
+
]);
|
|
152
|
+
const calls: string[] = [];
|
|
153
|
+
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
out = [];
|
|
156
|
+
err = [];
|
|
157
|
+
all = [];
|
|
158
|
+
calls.length = 0;
|
|
159
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { out.push(s); all.push(s); });
|
|
160
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { err.push(s); all.push(s); });
|
|
161
|
+
vi.spyOn(console, "warn").mockImplementation((s: string) => { all.push(s); });
|
|
162
|
+
discoverMock.mockReset();
|
|
163
|
+
discoverMock.mockResolvedValue({ entities, errors: [], sourceFiles: [] });
|
|
164
|
+
buildResultMock.mockReset();
|
|
165
|
+
buildResultMock.mockReturnValue({ errors: [], entities, outputs: new Map([["aws", ""]]), warnings: [] });
|
|
166
|
+
loadChantConfigMock.mockReset();
|
|
167
|
+
loadChantConfigMock.mockResolvedValue({
|
|
168
|
+
config: { stacks: [{ name: "net" }, { name: "data" }, { name: "app" }, { name: "edge" }] },
|
|
169
|
+
});
|
|
170
|
+
resolveLexMock.mockReset();
|
|
171
|
+
resolveLexMock.mockResolvedValue(["aws"]);
|
|
172
|
+
hasSnapshotMock.mockReset();
|
|
173
|
+
hasSnapshotMock.mockResolvedValue(false);
|
|
174
|
+
loadPluginsMock.mockResolvedValue([
|
|
175
|
+
createMockPlugin({
|
|
176
|
+
name: "aws",
|
|
177
|
+
describeResources: async (opts: { stack?: string }) => {
|
|
178
|
+
calls.push(opts.stack ?? "");
|
|
179
|
+
return {
|
|
180
|
+
observation: "v1" as const,
|
|
181
|
+
resources: {
|
|
182
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK", ownership: "unknown" as const },
|
|
183
|
+
privateServer: { type: "AWS::EC2::Instance", physicalId: "i-2", status: "OK", ownership: "unknown" as const },
|
|
184
|
+
},
|
|
185
|
+
notes: [NOTE],
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("four stacks, one note, after the rows and the provenance line", async () => {
|
|
193
|
+
const exit = await runSearch({ args: makeArgs(), plugins: [], serializers: [] });
|
|
194
|
+
expect(exit).toBe(0);
|
|
195
|
+
expect(calls).toEqual(["net", "data", "app", "edge"]);
|
|
196
|
+
const notes = all.filter((line) => line.includes("ownership filter unavailable"));
|
|
197
|
+
expect(notes).toHaveLength(1);
|
|
198
|
+
expect(notes[0]).toContain(`[aws] ${NOTE}`);
|
|
199
|
+
const noteAt = all.findIndex((line) => line.includes("ownership filter unavailable"));
|
|
200
|
+
const lastRow = all.findIndex((line) => line.startsWith("privateServer AWS::EC2::Instance i-2"));
|
|
201
|
+
const footer = all.findIndex((line) => line.includes("observed live · bound 2/2"));
|
|
202
|
+
expect(lastRow).toBeGreaterThanOrEqual(0);
|
|
203
|
+
expect(footer).toBeGreaterThan(lastRow);
|
|
204
|
+
expect(noteAt).toBeGreaterThan(footer);
|
|
205
|
+
// stderr, like every other warning; the rows stay clean on stdout.
|
|
206
|
+
expect(out.join("\n")).not.toContain("ownership filter unavailable");
|
|
207
|
+
expect(err.join("\n")).toContain("ownership filter unavailable");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("a miss still says the note, after the miss", async () => {
|
|
211
|
+
const exit = await runSearch({ args: makeArgs({ path: "kind:Nope" }), plugins: [], serializers: [] });
|
|
212
|
+
expect(exit).toBe(0);
|
|
213
|
+
const notes = all.filter((line) => line.includes("ownership filter unavailable"));
|
|
214
|
+
expect(notes).toHaveLength(1);
|
|
215
|
+
expect(all.indexOf("(no matches)")).toBeLessThan(all.findIndex((l) => l.includes("ownership filter unavailable")));
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect, vi } from "vitest";
|
|
2
2
|
import { __searchInternals } from "./search";
|
|
3
3
|
|
|
4
|
-
const { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss } = __searchInternals;
|
|
4
|
+
const { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss, provenance } = __searchInternals;
|
|
5
5
|
|
|
6
6
|
function node(id: string, kind: string, attrs: Record<string, unknown> = {}) {
|
|
7
7
|
return { id, kind, lexicon: "aws", attrs } as never;
|
|
@@ -53,6 +53,13 @@ describe("search formatting", () => {
|
|
|
53
53
|
test("compact row with live physical id, skipping object placeholders", () => {
|
|
54
54
|
const live = node("webServer", "AWS::EC2::Instance", { physicalId: "i-abc" });
|
|
55
55
|
expect(formatRow(live, [])).toBe("webServer AWS::EC2::Instance i-abc");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// #1263 — a row nobody could read printed blank where the physical id goes,
|
|
59
|
+
// which reads as "declared, not provisioned".
|
|
60
|
+
test("a row the live read could not observe says so in place of the physical id", () => {
|
|
61
|
+
const n = node("webServer", "AWS::EC2::Instance", { _status: "neutral", _unobserved: "read-failed" });
|
|
62
|
+
expect(formatRow(n, [])).toBe("webServer AWS::EC2::Instance (unobserved: read-failed)");
|
|
56
63
|
const src = node("webServer", "AWS::EC2::Instance", { InstanceId: { $ref: "webServer.InstanceId" } });
|
|
57
64
|
expect(formatRow(src, [])).toBe("webServer AWS::EC2::Instance");
|
|
58
65
|
});
|
|
@@ -353,3 +360,36 @@ describe("--show name matching", () => {
|
|
|
353
360
|
expect(capture(() => showMiss([n] as never, ["Region"]))).toBe("");
|
|
354
361
|
});
|
|
355
362
|
});
|
|
363
|
+
|
|
364
|
+
// #1263 — a `--live` read that failed entirely returned the declared rows with
|
|
365
|
+
// the same footer as a working answer and exit 0.
|
|
366
|
+
describe("provenance of a failed live read", () => {
|
|
367
|
+
const capture = (fn: () => void): string => {
|
|
368
|
+
const lines: string[] = [];
|
|
369
|
+
const spy = vi.spyOn(console, "log").mockImplementation((s: string) => { lines.push(s); });
|
|
370
|
+
fn();
|
|
371
|
+
spy.mockRestore();
|
|
372
|
+
return lines.join("\n");
|
|
373
|
+
};
|
|
374
|
+
const rows = [
|
|
375
|
+
node("a", "AWS::EC2::Instance", { _unobserved: "read-failed" }),
|
|
376
|
+
node("b", "AWS::EC2::Instance", { _unobserved: "read-failed" }),
|
|
377
|
+
] as never;
|
|
378
|
+
|
|
379
|
+
test("names the lexicon and counts the unobserved rows", () => {
|
|
380
|
+
const out = capture(() => provenance(rows, { kind: "live" }, undefined, ["aws: connect ECONNREFUSED 127.0.0.1:9999"]));
|
|
381
|
+
expect(out).toContain("live read failed (aws)");
|
|
382
|
+
expect(out).toContain("2/2 rows unobserved");
|
|
383
|
+
expect(out).toContain("physical ids unavailable");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("points at a recorded snapshot when one exists", () => {
|
|
387
|
+
const out = capture(() => provenance(rows, { kind: "live" }, "yes", ["aws: connect ECONNREFUSED"]));
|
|
388
|
+
expect(out).toContain("--at latest");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("a working live read is unchanged", () => {
|
|
392
|
+
const bound = [{ id: "a", kind: "AWS::EC2::Instance", lexicon: "aws", attrs: {}, physicalId: "i-1" }] as never;
|
|
393
|
+
expect(capture(() => provenance(bound, { kind: "live" }))).toBe("— observed live · bound 1/1");
|
|
394
|
+
});
|
|
395
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { build } from "../../build";
|
|
3
|
-
import { buildGraphIr, buildLiveGraphIr, sourceOverlayGraphs, type GraphIR, type IRNode, type IREdge } from "../../graph-ir";
|
|
3
|
+
import { buildGraphIr, buildLiveGraphIr, collectUnobserved, sourceOverlayGraphs, type GraphIR, type IRNode, type IREdge } from "../../graph-ir";
|
|
4
4
|
import { buildDeclaredPerStack } from "../../graph-declared";
|
|
5
5
|
import { enrichEffectiveTopology } from "../../graph-effective";
|
|
6
6
|
import { reconstructEdges, mergeCatalogs, type ReferenceCatalog } from "../../graph-refs";
|
|
@@ -61,6 +61,13 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
61
61
|
|
|
62
62
|
let ir: GraphIR;
|
|
63
63
|
let source: AnswerSource = { kind: "declared" };
|
|
64
|
+
// Lexicons whose live read threw (#1263). "Nothing observed" and "could not
|
|
65
|
+
// observe" are different claims; this is what carries the second one.
|
|
66
|
+
const liveFailures: string[] = [];
|
|
67
|
+
// Run-level notes from the read (#1265) — "ownership could not be filtered
|
|
68
|
+
// on this path" — printed with the provenance footer, after the rows, so the
|
|
69
|
+
// answer is not preceded by what qualifies it.
|
|
70
|
+
let liveNotes: string[] = [];
|
|
64
71
|
// Kinds that can exist in the account without being declared (#1278). Known
|
|
65
72
|
// without a scan, so it costs nothing to mention.
|
|
66
73
|
let ambientKinds: string[] = [];
|
|
@@ -122,8 +129,15 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
122
129
|
stacks,
|
|
123
130
|
ambient: args.ambient === true,
|
|
124
131
|
});
|
|
125
|
-
|
|
132
|
+
// A thrown read is not a warning. Printed as one, it sat between the
|
|
133
|
+
// ownership-filter notices that also print on a working run and carried
|
|
134
|
+
// no signal (#1263). Name the lexicon and the cause, as an error.
|
|
135
|
+
for (const e of observed.errors) {
|
|
136
|
+
liveFailures.push(e);
|
|
137
|
+
console.error(formatError({ message: `live read failed — ${e}` }));
|
|
138
|
+
}
|
|
126
139
|
observations = observed.observations;
|
|
140
|
+
liveNotes = observed.notes ?? [];
|
|
127
141
|
source = { kind: "live" };
|
|
128
142
|
}
|
|
129
143
|
let live = buildLiveGraphIr(observations);
|
|
@@ -189,7 +203,10 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
189
203
|
stacks.length > 0
|
|
190
204
|
? await buildDeclaredPerStack(stacks, projectPath)
|
|
191
205
|
: buildGraphIr((await discover(resolve(args.src ?? config.sourceDir ?? "."))).entities, projectPath);
|
|
192
|
-
|
|
206
|
+
// Carry the NOT-OBSERVED half of the tri-state (#1089) onto the rows, so a
|
|
207
|
+
// declared entity nobody could read is painted `_unobserved` and a row can
|
|
208
|
+
// say so instead of printing blank where a physical id would go (#1263).
|
|
209
|
+
ir = sourceOverlayGraphs(declared, live, { unobserved: collectUnobserved(observations) });
|
|
193
210
|
// Containment goes on AFTER the overlay, not through it.
|
|
194
211
|
//
|
|
195
212
|
// `sourceOverlayGraphs` admits a live edge only when one end is foreign,
|
|
@@ -242,6 +259,7 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
242
259
|
console.log("(no matches)");
|
|
243
260
|
availableAttrs(terms, ir);
|
|
244
261
|
if (args.explain) explain(terms, matches, ir, nodeById, query);
|
|
262
|
+
for (const n of liveNotes) console.error(formatWarning({ message: n }));
|
|
245
263
|
return 0;
|
|
246
264
|
}
|
|
247
265
|
for (const n of matches) {
|
|
@@ -254,12 +272,28 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
254
272
|
source.kind === "live" && !matches.some((n) => n.physicalId) && args.env
|
|
255
273
|
? (await hasSnapshot(String(args.env))) ? "yes" : undefined
|
|
256
274
|
: undefined;
|
|
257
|
-
provenance(matches, source, recorded);
|
|
275
|
+
provenance(matches, source, recorded, liveFailures);
|
|
276
|
+
// Qualifies the provenance line, so it sits with it: one line per distinct
|
|
277
|
+
// note for the whole run, not one per stack, and after the rows (#1265).
|
|
278
|
+
for (const n of liveNotes) console.error(formatWarning({ message: n }));
|
|
258
279
|
ambientHint(matches, ambientKinds, args.ambient === true, replayAmbient);
|
|
259
280
|
showMiss(matches, show);
|
|
260
281
|
regionSpread(terms, matches, show);
|
|
261
282
|
derivedSurface(terms, matches, ir, backed);
|
|
262
283
|
if (args.explain) explain(terms, matches, ir, nodeById, query);
|
|
284
|
+
// The caller asked for a live answer and at least one lexicon could not be
|
|
285
|
+
// read. The rows above are still printed — they are what the source declares,
|
|
286
|
+
// labelled as such — but the command did not do what it was asked, and a
|
|
287
|
+
// script or agent needs to see that without parsing the footer (#1263).
|
|
288
|
+
if (liveFailures.length > 0) {
|
|
289
|
+
console.error(formatError({
|
|
290
|
+
message: `live read failed for ${liveFailures.length} lexicon${liveFailures.length === 1 ? "" : "s"} — answer is declared-only`,
|
|
291
|
+
hint: recorded
|
|
292
|
+
? "answer from the recorded snapshot with --at latest, or drop --live for a declared-only query"
|
|
293
|
+
: "drop --live for a declared-only query, or record a snapshot with chant lifecycle snapshot",
|
|
294
|
+
}));
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
263
297
|
return 0;
|
|
264
298
|
}
|
|
265
299
|
|
|
@@ -379,13 +413,24 @@ function ambientHint(
|
|
|
379
413
|
* already done. That is a fact about the query, printed for every query, and it
|
|
380
414
|
* encodes no expected answer.
|
|
381
415
|
*/
|
|
382
|
-
function provenance(matches: IRNode[], source: AnswerSource, recorded?: string): void {
|
|
416
|
+
function provenance(matches: IRNode[], source: AnswerSource, recorded?: string, liveFailures: string[] = []): void {
|
|
383
417
|
if (source.kind === "declared") {
|
|
384
418
|
console.log("— declared only · no observation · physical ids unavailable");
|
|
385
419
|
return;
|
|
386
420
|
}
|
|
387
421
|
const bound = matches.filter((n) => n.physicalId).length;
|
|
388
422
|
const what = source.kind === "live" ? "live read" : "snapshot";
|
|
423
|
+
if (liveFailures.length > 0) {
|
|
424
|
+
// Could not observe, as distinct from observed nothing. The rows carry the
|
|
425
|
+
// per-entity verdict; this names the lexicons that failed in one line.
|
|
426
|
+
const lexicons = liveFailures.map((e) => e.split(":")[0]).join(", ");
|
|
427
|
+
const unobserved = matches.filter((n) => (n.attrs as Record<string, unknown> | undefined)?._unobserved).length;
|
|
428
|
+
const rest = recorded
|
|
429
|
+
? "a snapshot of this environment is recorded — answer from it with --at latest"
|
|
430
|
+
: "answered from the declared graph · physical ids unavailable";
|
|
431
|
+
console.log(`— live read failed (${lexicons}) · ${unobserved}/${matches.length} rows unobserved · ${rest}`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
389
434
|
if (bound === 0) {
|
|
390
435
|
// The estate was asked for and nothing came back bound. Naming it is the
|
|
391
436
|
// difference between "these do not exist" and "nobody could see them".
|
|
@@ -742,6 +787,10 @@ function formatRow(n: IRNode, show: string[]): string {
|
|
|
742
787
|
// skip source-mode AttrRef placeholders (objects).
|
|
743
788
|
const physical = (n as { physicalId?: unknown }).physicalId ?? attrs["physicalId"] ?? attrs["InstanceId"] ?? attrs["Id"];
|
|
744
789
|
if (physical != null && typeof physical !== "object") parts.push(String(physical));
|
|
790
|
+
// A row nobody could read says so where its physical id would go (#1263). A
|
|
791
|
+
// blank there reads as "declared, not provisioned", which the read never
|
|
792
|
+
// established; the reason is the tri-state's (#1089).
|
|
793
|
+
else if (typeof attrs["_unobserved"] === "string") parts.push(`(unobserved: ${attrs["_unobserved"]})`);
|
|
745
794
|
for (const key of show) {
|
|
746
795
|
// Match the name case-insensitively, and report what was actually found.
|
|
747
796
|
// AWS attribute names are PascalCase and chant's derived ones are not, so a
|
|
@@ -763,4 +812,4 @@ function formatRow(n: IRNode, show: string[]): string {
|
|
|
763
812
|
}
|
|
764
813
|
|
|
765
814
|
/** Internals exposed for unit tests. */
|
|
766
|
-
export const __searchInternals = { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss };
|
|
815
|
+
export const __searchInternals = { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss, provenance };
|
package/src/cli/main.ts
CHANGED
|
@@ -542,7 +542,9 @@ Options:
|
|
|
542
542
|
--fix Auto-fix fixable issues (lint command)
|
|
543
543
|
--force Force overwrite existing files (import command)
|
|
544
544
|
-w, --watch Watch for changes and rebuild/re-lint (build, lint)
|
|
545
|
-
-v, --verbose Show stack traces on errors
|
|
545
|
+
-v, --verbose Show stack traces on errors; (build) list every
|
|
546
|
+
resolved build parameter and per-file fold decision
|
|
547
|
+
instead of the one-line summaries
|
|
546
548
|
-h, --help Show this help message
|
|
547
549
|
-p, --profile <name> Temporal worker profile to use (run command)
|
|
548
550
|
--local Run an Op with the local in-process executor (default)
|
|
@@ -572,8 +574,8 @@ Options:
|
|
|
572
574
|
composite factory calls (#1022/#1023), falling back
|
|
573
575
|
to run per-file for anything else outside the fold
|
|
574
576
|
subset (a cross-file-only reference, a re-export,
|
|
575
|
-
\`export default\`, ...). Logs
|
|
576
|
-
|
|
577
|
+
\`export default\`, ...). Logs a fold/run count
|
|
578
|
+
(per-file lines under --verbose). DEFAULT since #1134 — this flag forces it on
|
|
577
579
|
over a chant.config.ts \`build.fold: false\`.
|
|
578
580
|
--no-fold (build) Opt out of folding for this invocation: every
|
|
579
581
|
source module is imported and run, the pre-#1134
|
|
@@ -99,7 +99,8 @@ describe("CLI end-to-end — resolveProjectLexicons must not execute project sou
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
test("`chant build --fold` on a project with no `lexicons` config does not execute the hostile file", () => {
|
|
102
|
-
|
|
102
|
+
// --verbose: the per-file fold decision lines are behind it (#1424).
|
|
103
|
+
const { status, stdout, stderr } = runCli(["build", testDir, "--fold", "--verbose"]);
|
|
103
104
|
|
|
104
105
|
expect(stderr).not.toMatch(/EXFIL/);
|
|
105
106
|
expect(stdout).not.toMatch(/EXFIL/);
|