@intentius/chant 0.44.14 → 0.46.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 +14 -0
- 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/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/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 +13 -0
- 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/cli/registry.d.ts +7 -0
- package/dist/cli/registry.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/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.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 +21 -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 +85 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/env.d.ts +12 -1
- package/dist/env.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/lexicon.d.ts +182 -2
- package/dist/lexicon.d.ts.map +1 -1
- 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/index.d.ts +1 -0
- package/dist/lifecycle/index.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/teardown.d.ts +130 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/types.d.ts +7 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +2 -1
- package/dist/lint/rules/index.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/builders.d.ts +36 -7
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/yaml.d.ts +0 -8
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +6 -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 +275 -5
- package/src/cli/commands/build.ts +86 -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 +46 -2
- package/src/cli/commands/check-lexicon.ts +64 -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 +10 -1
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +19 -6
- 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 +24 -4
- package/src/cli/handlers/lifecycle.test.ts +348 -1
- package/src/cli/handlers/lifecycle.ts +235 -3
- 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 +60 -8
- package/src/cli/main.ts +17 -4
- package/src/cli/registry.ts +7 -0
- 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 +22 -2
- 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 +49 -3
- package/src/codegen/validate.ts +36 -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 +40 -0
- package/src/config.ts +159 -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/env.test.ts +35 -1
- package/src/env.ts +17 -3
- 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/lexicon.ts +182 -2
- 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/index.ts +1 -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/teardown.test.ts +537 -0
- package/src/lifecycle/teardown.ts +357 -0
- package/src/lifecycle/types.ts +7 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/policy.ts +14 -3
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- package/src/lint/rules/index.ts +4 -1
- 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.ts +40 -7
- package/src/op/index.ts +1 -1
- package/src/ownership.test.ts +63 -1
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +15 -0
- package/src/yaml.ts +19 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
|
-
import { auditCommand, tokenForHost, coverageNotes } from "./audit";
|
|
3
|
+
import { auditCommand, tokenForHost, coverageNotes, installLine, NO_LEXICONS_EXIT_CODE } from "./audit";
|
|
4
4
|
import { discoverByDetection, loadAuditPlugins } from "../../audit/discover";
|
|
5
5
|
import { MissingLexiconError, type AuditInput, type AuditLexicon } from "../../audit/core";
|
|
6
6
|
|
|
@@ -190,6 +190,88 @@ describe("auditCommand", () => {
|
|
|
190
190
|
expect(result.error).toMatch(/npm i @intentius\/chant-lexicon-github/);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
describe("lexicon coverage (#1623)", () => {
|
|
194
|
+
const MIXED = fileURLToPath(new URL("./__fixtures__/audit-coverage", import.meta.url));
|
|
195
|
+
|
|
196
|
+
test("zero lexicons: says it had nothing to look with, names each file's lexicon, prints the npx line, exits 2", async () => {
|
|
197
|
+
const result = await auditCommand({ path: MIXED, plugins: [] });
|
|
198
|
+
expect(result.success).toBe(true);
|
|
199
|
+
expect(result.status).toBe("no-lexicons");
|
|
200
|
+
expect(result.exitCode).toBe(NO_LEXICONS_EXIT_CODE);
|
|
201
|
+
expect(result.stream).toBe("stderr");
|
|
202
|
+
expect(result.findings).toEqual([]);
|
|
203
|
+
expect(result.output).toContain("nothing to look with");
|
|
204
|
+
expect(result.output).not.toMatch(/Audited \d+ files/);
|
|
205
|
+
expect(result.output).toContain(".github/workflows/ci.yml -> github");
|
|
206
|
+
expect(result.output).toContain("k8s/deploy.yaml -> k8s");
|
|
207
|
+
expect(result.output).toContain("infra/stack.json -> aws");
|
|
208
|
+
expect(result.output).toContain("Dockerfile -> docker");
|
|
209
|
+
expect(result.output).toContain("infra/main.tf -> terraform (not audited; see chant carve)");
|
|
210
|
+
// The install line names only the lexicons the files wanted, never terraform.
|
|
211
|
+
expect(result.output).toContain(
|
|
212
|
+
`npx -p @intentius/chant -p @intentius/chant-lexicon-github -p @intentius/chant-lexicon-docker -p @intentius/chant-lexicon-aws -p @intentius/chant-lexicon-k8s chant audit ${MIXED}`,
|
|
213
|
+
);
|
|
214
|
+
expect(result.output).not.toContain("chant-lexicon-terraform");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("zero lexicons with --json: status no-lexicons plus the unclaimed list and install line", async () => {
|
|
218
|
+
const result = await auditCommand({ path: MIXED, plugins: [], format: "json" });
|
|
219
|
+
expect(result.exitCode).toBe(NO_LEXICONS_EXIT_CODE);
|
|
220
|
+
expect(result.stream).toBe("stdout");
|
|
221
|
+
const json = JSON.parse(result.output);
|
|
222
|
+
expect(json.status).toBe("no-lexicons");
|
|
223
|
+
expect(json.findings).toEqual([]);
|
|
224
|
+
expect(json.missingLexicons).toEqual(["github", "docker", "aws", "k8s"]);
|
|
225
|
+
expect(json.unclaimed).toContainEqual({ path: "k8s/deploy.yaml", lexicon: "k8s" });
|
|
226
|
+
expect(json.unclaimed).toContainEqual({ path: "infra/main.tf", lexicon: "terraform" });
|
|
227
|
+
expect(json.install).toMatch(/^npx -p @intentius\/chant /);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("zero lexicons on an empty dir still refuses to report clean", async () => {
|
|
231
|
+
const tmp = join(tmpdir(), `chant-audit-nolex-${process.pid}`);
|
|
232
|
+
const { mkdirSync } = await import("fs");
|
|
233
|
+
mkdirSync(tmp, { recursive: true });
|
|
234
|
+
const result = await auditCommand({ path: tmp, plugins: [] });
|
|
235
|
+
expect(result.status).toBe("no-lexicons");
|
|
236
|
+
expect(result.exitCode).toBe(NO_LEXICONS_EXIT_CODE);
|
|
237
|
+
expect(result.output).toContain("nothing to look with");
|
|
238
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("partial: github loaded, k8s/aws/docker absent -> audit runs and a one-line hint names the gap", async () => {
|
|
242
|
+
const all = await loadAuditPlugins();
|
|
243
|
+
const github = all.filter((p) => p.name === "github");
|
|
244
|
+
expect(github).toHaveLength(1);
|
|
245
|
+
const result = await auditCommand({ path: MIXED, plugins: github });
|
|
246
|
+
expect(result.status).toBe("ok");
|
|
247
|
+
expect(result.exitCode).toBe(0);
|
|
248
|
+
expect(result.scanned).toEqual([".github/workflows/ci.yml"]);
|
|
249
|
+
expect(result.output).toMatch(/^Note: 3 files look like docker\/aws\/k8s but those lexicons are not installed, so they were skipped \(npm i @intentius\/chant-lexicon-docker @intentius\/chant-lexicon-aws @intentius\/chant-lexicon-k8s\)\./);
|
|
250
|
+
expect(result.output).toContain("1 Terraform file skipped; the audit does not read HCL (see chant carve).");
|
|
251
|
+
expect(result.unclaimed).toContainEqual({ path: "k8s/deploy.yaml", lexicon: "k8s" });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("partial with --json: status ok and the unclaimed files ride along", async () => {
|
|
255
|
+
const all = await loadAuditPlugins();
|
|
256
|
+
const result = await auditCommand({ path: MIXED, plugins: all.filter((p) => p.name === "github"), format: "json" });
|
|
257
|
+
const json = JSON.parse(result.output);
|
|
258
|
+
expect(json.status).toBe("ok");
|
|
259
|
+
expect(json.unclaimed.map((u: { lexicon: string }) => u.lexicon).sort()).toEqual(["aws", "docker", "k8s", "terraform"]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("all lexicons loaded: no coverage note, no unclaimed (terraform aside)", async () => {
|
|
263
|
+
const result = await auditCommand({ path: MIXED, plugins: await loadAuditPlugins() });
|
|
264
|
+
expect(result.status).toBe("ok");
|
|
265
|
+
expect(result.unclaimed).toEqual([{ path: "infra/main.tf", lexicon: "terraform" }]);
|
|
266
|
+
expect(result.output).not.toContain("not installed");
|
|
267
|
+
expect(result.scanned.sort()).toEqual([".github/workflows/ci.yml", "Dockerfile", "infra/stack.json", "k8s/deploy.yaml"]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("installLine puts every lexicon on the same npx -p path", () => {
|
|
271
|
+
expect(installLine(["github", "gitlab"], ".")).toBe("npx -p @intentius/chant -p @intentius/chant-lexicon-github -p @intentius/chant-lexicon-gitlab chant audit .");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
193
275
|
test("a path with no CI files succeeds with a clear message", async () => {
|
|
194
276
|
const tmp = join(tmpdir(), `chant-audit-empty-${process.pid}`);
|
|
195
277
|
const { mkdirSync } = await import("fs");
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, statSync, writeFileSync } from "fs";
|
|
9
9
|
import { auditFiles, type AuditInput, type AuditFinding, type ChecksProvider } from "../../audit/core";
|
|
10
|
-
import {
|
|
10
|
+
import { AUDIT_LEXICONS, classifyFiles, collectCandidates, loadAuditPlugins, unclaimedFiles, type DetectPlugin, type RepoFile, type UnclaimedFile } from "../../audit/discover";
|
|
11
11
|
import { RULE_CATALOG, resolveAuditCatalog, type RuleMeta } from "../../audit/catalog";
|
|
12
12
|
import { renderMarkdown } from "../../audit/report";
|
|
13
13
|
import { renderHtml, type ReportTheme } from "../../audit/report-html";
|
|
14
|
-
import { buildReportJson, type AuditSnapshot } from "../../audit/report-model";
|
|
14
|
+
import { buildReportJson, REPORT_SCHEMA_VERSION, type AuditSnapshot } from "../../audit/report-model";
|
|
15
15
|
import { fetchRepoFiles, resolveActionSha, resolveImageDigest, resolveRepoCommit, parseRepoUrl, FetchError } from "../../audit/fetch";
|
|
16
16
|
import { extractUnpinnedActions, extractUnpinnedImages } from "../../audit/proof";
|
|
17
17
|
import type { ProveOptions } from "../../audit/proof";
|
|
@@ -45,6 +45,8 @@ export interface AuditCommandOptions {
|
|
|
45
45
|
now?: string;
|
|
46
46
|
/** Tool version recorded in the HTML snapshot. */
|
|
47
47
|
toolVersion?: string;
|
|
48
|
+
/** Injectable detection plugins (testing); defaults to every installed audit lexicon. */
|
|
49
|
+
plugins?: DetectPlugin[];
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface AuditCommandResult {
|
|
@@ -58,6 +60,94 @@ export interface AuditCommandResult {
|
|
|
58
60
|
error?: string;
|
|
59
61
|
/** Set when the report was written to a file (via `output`). */
|
|
60
62
|
wroteTo?: string;
|
|
63
|
+
/**
|
|
64
|
+
* `"no-lexicons"` when not a single audit lexicon resolved, so nothing was
|
|
65
|
+
* inspected (#1623). `output` then carries the diagnostic, not a report.
|
|
66
|
+
*/
|
|
67
|
+
status?: "ok" | "no-lexicons";
|
|
68
|
+
/** Candidate files that looked like they wanted a lexicon that is not installed. */
|
|
69
|
+
unclaimed?: UnclaimedFile[];
|
|
70
|
+
/** Where `output` belongs; diagnostics go to stderr, reports to stdout (default). */
|
|
71
|
+
stream?: "stdout" | "stderr";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Exit code when the audit had no lexicons to look with. Distinct from 1 (findings / failure). */
|
|
75
|
+
export const NO_LEXICONS_EXIT_CODE = 2;
|
|
76
|
+
|
|
77
|
+
/** The npm package that provides an audit lexicon's detection and checks. */
|
|
78
|
+
function lexiconPackage(name: string): string {
|
|
79
|
+
return `@intentius/chant-lexicon-${name}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Missing audit lexicons the unclaimed files pointed at, in first-seen order. `terraform` is not installable. */
|
|
83
|
+
function wantedLexicons(unclaimed: UnclaimedFile[]): string[] {
|
|
84
|
+
return [...new Set(unclaimed.map((u) => u.lexicon))].filter((l) => l !== "terraform");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The exact one-liner that gives a bare `npx @intentius/chant audit` the
|
|
89
|
+
* lexicons it needs. Every wanted lexicon is a `-p` package so npx puts all of
|
|
90
|
+
* them on the same resolution path.
|
|
91
|
+
*/
|
|
92
|
+
export function installLine(lexicons: string[], target: string): string {
|
|
93
|
+
const pkgs = ["@intentius/chant", ...lexicons.map(lexiconPackage)];
|
|
94
|
+
return `npx ${pkgs.map((p) => `-p ${p}`).join(" ")} chant audit ${target}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One-line coverage hint for the partial case: some lexicons loaded, others wanted by files on disk. */
|
|
98
|
+
function missingLexiconHint(unclaimed: UnclaimedFile[]): string | undefined {
|
|
99
|
+
const wanted = wantedLexicons(unclaimed);
|
|
100
|
+
const tf = unclaimed.filter((u) => u.lexicon === "terraform").length;
|
|
101
|
+
const parts: string[] = [];
|
|
102
|
+
if (wanted.length > 0) {
|
|
103
|
+
const n = unclaimed.length - tf;
|
|
104
|
+
parts.push(
|
|
105
|
+
`${n} file${n === 1 ? " looks" : "s look"} like ${wanted.join("/")} but ${wanted.length === 1 ? "that lexicon is" : "those lexicons are"} not installed, so ${n === 1 ? "it was" : "they were"} skipped` +
|
|
106
|
+
` (npm i ${wanted.map(lexiconPackage).join(" ")}).`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (tf > 0) parts.push(`${tf} Terraform file${tf === 1 ? "" : "s"} skipped; the audit does not read HCL (see chant carve).`);
|
|
110
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Human-readable diagnostic for the zero-lexicon case. */
|
|
114
|
+
function renderNoLexicons(target: string, unclaimed: UnclaimedFile[]): string {
|
|
115
|
+
const lines: string[] = [];
|
|
116
|
+
lines.push(`chant audit had nothing to look with: no audit lexicon is installed, so nothing under ${target} was inspected.`);
|
|
117
|
+
lines.push("This is not a clean result. Detection and checks live in the lexicon packages.");
|
|
118
|
+
const wanted = wantedLexicons(unclaimed);
|
|
119
|
+
if (unclaimed.length > 0) {
|
|
120
|
+
lines.push("", "Files that wanted a lexicon:");
|
|
121
|
+
for (const u of unclaimed) {
|
|
122
|
+
const note = u.lexicon === "terraform" ? "terraform (not audited; see chant carve)" : u.lexicon;
|
|
123
|
+
lines.push(` ${u.path} -> ${note}`);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
lines.push("", "No file under the target looked like CI, Kubernetes, Helm, Docker, CloudFormation, ARM, or Config Connector either.");
|
|
127
|
+
}
|
|
128
|
+
lines.push("", "Run it with the lexicons those files need:");
|
|
129
|
+
lines.push(` ${installLine(wanted.length > 0 ? wanted : [...AUDIT_LEXICONS], target)}`);
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Machine-readable form of the zero-lexicon diagnostic (`status: "no-lexicons"`). */
|
|
134
|
+
function renderNoLexiconsJson(target: string, unclaimed: UnclaimedFile[]): string {
|
|
135
|
+
const wanted = wantedLexicons(unclaimed);
|
|
136
|
+
return JSON.stringify(
|
|
137
|
+
{
|
|
138
|
+
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
139
|
+
tool: { name: "chant-audit" },
|
|
140
|
+
status: "no-lexicons",
|
|
141
|
+
target,
|
|
142
|
+
summary: { total: 0 },
|
|
143
|
+
findings: [],
|
|
144
|
+
unclaimed,
|
|
145
|
+
missingLexicons: wanted,
|
|
146
|
+
install: installLine(wanted.length > 0 ? wanted : [...AUDIT_LEXICONS], target),
|
|
147
|
+
},
|
|
148
|
+
null,
|
|
149
|
+
2,
|
|
150
|
+
);
|
|
61
151
|
}
|
|
62
152
|
|
|
63
153
|
/**
|
|
@@ -205,20 +295,21 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
205
295
|
|
|
206
296
|
const isUrl = /^https?:\/\//.test(options.path);
|
|
207
297
|
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
298
|
+
// Detection lives in the lexicon plugins (each one's `detectTemplate`), so a
|
|
299
|
+
// lexicon that isn't installed can't claim its files. Every branch below
|
|
300
|
+
// therefore also computes which candidate files looked like they wanted an
|
|
301
|
+
// absent lexicon, so "nothing found" and "had nothing to look with" never
|
|
302
|
+
// render the same way (#1623).
|
|
303
|
+
const plugins = options.plugins ?? (await loadAuditPlugins());
|
|
304
|
+
let candidates: RepoFile[];
|
|
213
305
|
if (isUrl) {
|
|
214
306
|
try {
|
|
215
307
|
// Fetch the whole repo's candidate files (all lexicons, not just CI) and
|
|
216
308
|
// run them through the same classifier the local path uses (#420).
|
|
217
|
-
|
|
309
|
+
candidates = await fetchRepoFiles(options.path, {
|
|
218
310
|
token: options.token ?? tokenForHost(options.path),
|
|
219
311
|
fetchImpl: options.fetchImpl,
|
|
220
312
|
});
|
|
221
|
-
inputs = classifyFiles(files, await loadAuditPlugins());
|
|
222
313
|
} catch (err) {
|
|
223
314
|
const msg = err instanceof FetchError ? err.message : err instanceof Error ? err.message : String(err);
|
|
224
315
|
return { success: false, output: "", findings: [], scanned: [], exitCode: 1, error: msg };
|
|
@@ -227,25 +318,25 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
227
318
|
if (!existsSync(options.path)) {
|
|
228
319
|
return { success: false, output: "", findings: [], scanned: [], exitCode: 1, error: `Path not found: ${options.path}` };
|
|
229
320
|
}
|
|
230
|
-
// One walk, plugin-delegated detection.
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
const plugins = await loadAuditPlugins();
|
|
235
|
-
const loaded = new Set(plugins.map((p) => p.name));
|
|
236
|
-
const missing = AUDIT_LEXICONS.filter((n) => !loaded.has(n));
|
|
237
|
-
if (missing.length > 0) {
|
|
238
|
-
missingHint =
|
|
239
|
-
` Some lexicon packages are not installed, so files of those types were not detected: ${missing.join(", ")}.` +
|
|
240
|
-
` Install what you need, e.g. npm i ${missing.map((n) => `@intentius/chant-lexicon-${n}`).join(" ")}`;
|
|
241
|
-
}
|
|
242
|
-
inputs = discoverByDetection(options.path, plugins);
|
|
321
|
+
// One walk, plugin-delegated detection. CI (path), Dockerfiles (name), and
|
|
322
|
+
// Helm charts (bundle) are special-cased by the classifier since content
|
|
323
|
+
// shape alone can't disambiguate them.
|
|
324
|
+
candidates = collectCandidates(options.path);
|
|
243
325
|
}
|
|
244
|
-
|
|
326
|
+
const inputs = classifyFiles(candidates, plugins);
|
|
327
|
+
const unclaimed = unclaimedFiles(candidates, inputs, plugins);
|
|
245
328
|
const scanned = inputs.map((i) => i.path);
|
|
246
329
|
|
|
330
|
+
if (plugins.length === 0) {
|
|
331
|
+
const output = format === "json" ? renderNoLexiconsJson(options.path, unclaimed) : renderNoLexicons(options.path, unclaimed);
|
|
332
|
+
return { success: true, status: "no-lexicons", output, findings: [], scanned: [], unclaimed, exitCode: NO_LEXICONS_EXIT_CODE, stream: format === "json" ? "stdout" : "stderr" };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const missingLexiconNote = missingLexiconHint(unclaimed);
|
|
336
|
+
|
|
247
337
|
if (inputs.length === 0) {
|
|
248
|
-
|
|
338
|
+
const output = `No auditable files found under ${options.path}.${missingLexiconNote ? ` ${missingLexiconNote}` : ""}`;
|
|
339
|
+
return { success: true, status: "ok", output, findings: [], scanned: [], unclaimed, exitCode: 0 };
|
|
249
340
|
}
|
|
250
341
|
|
|
251
342
|
let findings: AuditFinding[];
|
|
@@ -262,6 +353,7 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
262
353
|
|
|
263
354
|
if (tier === "merge-worthy") findings = findings.filter((f) => isMergeWorthy(f, catalog));
|
|
264
355
|
const notes = coverageNotes(inputs);
|
|
356
|
+
if (missingLexiconNote) notes.push(missingLexiconNote);
|
|
265
357
|
|
|
266
358
|
// Diff-bearing renderers (markdown, html) need action SHAs / image digests
|
|
267
359
|
// resolved up front (sync maps so rendering stays synchronous).
|
|
@@ -304,7 +396,7 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
304
396
|
switch (format) {
|
|
305
397
|
case "json": {
|
|
306
398
|
const snapshot = await buildSnapshot(options, scanned, isUrl);
|
|
307
|
-
output = JSON.stringify(buildReportJson(findings, { snapshot, toolVersion: options.toolVersion, catalog }), null, 2);
|
|
399
|
+
output = JSON.stringify(buildReportJson(findings, { snapshot, toolVersion: options.toolVersion, catalog, unclaimed }), null, 2);
|
|
308
400
|
break;
|
|
309
401
|
}
|
|
310
402
|
case "sarif":
|
|
@@ -329,10 +421,10 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
329
421
|
} catch (err) {
|
|
330
422
|
return { success: false, output, findings, scanned, exitCode: 1, error: `Failed to write ${options.output}: ${err instanceof Error ? err.message : String(err)}` };
|
|
331
423
|
}
|
|
332
|
-
return { success: true, output, findings, scanned, exitCode, wroteTo: options.output };
|
|
424
|
+
return { success: true, status: "ok", output, findings, scanned, unclaimed, exitCode, wroteTo: options.output };
|
|
333
425
|
}
|
|
334
426
|
|
|
335
|
-
return { success: true, output, findings, scanned, exitCode };
|
|
427
|
+
return { success: true, status: "ok", output, findings, scanned, unclaimed, exitCode };
|
|
336
428
|
}
|
|
337
429
|
|
|
338
430
|
/** Print an audit result to stdout. */
|
|
@@ -345,5 +437,6 @@ export function printAuditResult(result: AuditCommandResult): void {
|
|
|
345
437
|
console.error(`Wrote report to ${result.wroteTo}`);
|
|
346
438
|
return;
|
|
347
439
|
}
|
|
348
|
-
console.
|
|
440
|
+
if (result.stream === "stderr") console.error(result.output);
|
|
441
|
+
else console.log(result.output);
|
|
349
442
|
}
|
|
@@ -169,6 +169,7 @@ export const testEntity = {
|
|
|
169
169
|
format: "json",
|
|
170
170
|
serializers: [awsSerializer],
|
|
171
171
|
fold: true,
|
|
172
|
+
verbose: true,
|
|
172
173
|
});
|
|
173
174
|
|
|
174
175
|
expect(result.success).toBe(true);
|
|
@@ -183,6 +184,262 @@ export const testEntity = {
|
|
|
183
184
|
}
|
|
184
185
|
});
|
|
185
186
|
|
|
187
|
+
describe("#1396 — ownership.env resolves from the env build parameter", () => {
|
|
188
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
189
|
+
const runtimePath = resolvePath(thisDir, "../../runtime");
|
|
190
|
+
const paramsPath = resolvePath(thisDir, "../../params");
|
|
191
|
+
|
|
192
|
+
// A label-stamping serializer in miniature: the resource's own `env` prop
|
|
193
|
+
// (from params.env) next to the ownership marker's env (from context).
|
|
194
|
+
const labelSerializer: Serializer = {
|
|
195
|
+
name: "aws",
|
|
196
|
+
rulePrefix: "TEST",
|
|
197
|
+
serialize: (entities, _outputs, context) =>
|
|
198
|
+
JSON.stringify(
|
|
199
|
+
[...entities.values()].map((e) => ({
|
|
200
|
+
label: (e as unknown as { props: { env: string } }).props.env,
|
|
201
|
+
marker: context?.ownership,
|
|
202
|
+
})),
|
|
203
|
+
),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
async function writeProject(ownershipEnv: string) {
|
|
207
|
+
await writeFile(
|
|
208
|
+
join(testDir, "chant.config.ts"),
|
|
209
|
+
`
|
|
210
|
+
export default {
|
|
211
|
+
ownership: { stack: "fountain", env: ${ownershipEnv} },
|
|
212
|
+
buildParams: {
|
|
213
|
+
env: { type: "string", default: "dev", env: "CHANT_TEST_1396_ENV" },
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
`,
|
|
217
|
+
);
|
|
218
|
+
await writeFile(
|
|
219
|
+
join(testDir, "resources.ts"),
|
|
220
|
+
`
|
|
221
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
222
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
223
|
+
`,
|
|
224
|
+
);
|
|
225
|
+
await writeFile(
|
|
226
|
+
join(testDir, "main.ts"),
|
|
227
|
+
`
|
|
228
|
+
import { Bucket } from "./resources";
|
|
229
|
+
import { params } from ${JSON.stringify(paramsPath)};
|
|
230
|
+
export const bucket = new Bucket({ env: params.env });
|
|
231
|
+
`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function built(): Array<{ label: string; marker?: { stack: string; env?: string } }> {
|
|
236
|
+
return JSON.parse(readFileSync(outputFile, "utf-8"));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
test("--param env=prod drives both the label and the ownership marker", async () => {
|
|
240
|
+
await writeProject(`{ param: "env" }`);
|
|
241
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
242
|
+
try {
|
|
243
|
+
const result = await buildCommand({
|
|
244
|
+
path: testDir,
|
|
245
|
+
output: outputFile,
|
|
246
|
+
format: "json",
|
|
247
|
+
serializers: [labelSerializer],
|
|
248
|
+
params: { env: "prod" },
|
|
249
|
+
});
|
|
250
|
+
expect(result.errors).toEqual([]);
|
|
251
|
+
expect(result.success).toBe(true);
|
|
252
|
+
expect(built()).toEqual([{ label: "prod", marker: { stack: "fountain", env: "prod" } }]);
|
|
253
|
+
} finally {
|
|
254
|
+
errorSpy.mockRestore();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("with no parameter supplied, both follow the declared default", async () => {
|
|
259
|
+
await writeProject(`{ param: "env" }`);
|
|
260
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
261
|
+
try {
|
|
262
|
+
const result = await buildCommand({
|
|
263
|
+
path: testDir,
|
|
264
|
+
output: outputFile,
|
|
265
|
+
format: "json",
|
|
266
|
+
serializers: [labelSerializer],
|
|
267
|
+
});
|
|
268
|
+
expect(result.success).toBe(true);
|
|
269
|
+
expect(built()).toEqual([{ label: "dev", marker: { stack: "fountain", env: "dev" } }]);
|
|
270
|
+
} finally {
|
|
271
|
+
errorSpy.mockRestore();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("a literal ownership.env that disagrees with --param env is warned about", async () => {
|
|
276
|
+
await writeProject(`"dev"`);
|
|
277
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
278
|
+
try {
|
|
279
|
+
const result = await buildCommand({
|
|
280
|
+
path: testDir,
|
|
281
|
+
output: outputFile,
|
|
282
|
+
format: "json",
|
|
283
|
+
serializers: [labelSerializer],
|
|
284
|
+
params: { env: "prod" },
|
|
285
|
+
});
|
|
286
|
+
expect(result.success).toBe(true);
|
|
287
|
+
expect(built()).toEqual([{ label: "prod", marker: { stack: "fountain", env: "dev" } }]);
|
|
288
|
+
expect(result.warnings.some((w) => w.includes('ownership.env is "dev"') && w.includes('"prod"'))).toBe(true);
|
|
289
|
+
} finally {
|
|
290
|
+
errorSpy.mockRestore();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("a reference to an undeclared parameter fails the build", async () => {
|
|
295
|
+
await writeProject(`{ param: "environment" }`);
|
|
296
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
297
|
+
try {
|
|
298
|
+
const result = await buildCommand({
|
|
299
|
+
path: testDir,
|
|
300
|
+
output: outputFile,
|
|
301
|
+
format: "json",
|
|
302
|
+
serializers: [labelSerializer],
|
|
303
|
+
});
|
|
304
|
+
expect(result.success).toBe(false);
|
|
305
|
+
expect(result.errors.some((e) => e.includes('build parameter "environment"'))).toBe(true);
|
|
306
|
+
} finally {
|
|
307
|
+
errorSpy.mockRestore();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe("#1221 — dynamic-env legality against declared environments", () => {
|
|
313
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
314
|
+
const runtimePath = resolvePath(thisDir, "../../runtime");
|
|
315
|
+
const paramsPath = resolvePath(thisDir, "../../params");
|
|
316
|
+
|
|
317
|
+
// Serializer exposing the physical name (from params.env interpolation)
|
|
318
|
+
// next to the ownership marker, so one test can assert both are disjoint
|
|
319
|
+
// across two builds.
|
|
320
|
+
const namingSerializer: Serializer = {
|
|
321
|
+
name: "aws",
|
|
322
|
+
rulePrefix: "TEST",
|
|
323
|
+
serialize: (entities, _outputs, context) =>
|
|
324
|
+
JSON.stringify(
|
|
325
|
+
[...entities.values()].map((e) => ({
|
|
326
|
+
name: (e as unknown as { props: { name: string } }).props.name,
|
|
327
|
+
marker: context?.ownership,
|
|
328
|
+
})),
|
|
329
|
+
),
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
async function writeProject(environments: string) {
|
|
333
|
+
await writeFile(
|
|
334
|
+
join(testDir, "chant.config.ts"),
|
|
335
|
+
`
|
|
336
|
+
export default {
|
|
337
|
+
environments: ${environments},
|
|
338
|
+
ownership: { stack: "billing", env: { param: "env" } },
|
|
339
|
+
buildParams: {
|
|
340
|
+
env: { type: "string", default: "dev" },
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
`,
|
|
344
|
+
);
|
|
345
|
+
await writeFile(
|
|
346
|
+
join(testDir, "resources.ts"),
|
|
347
|
+
`
|
|
348
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
349
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
350
|
+
`,
|
|
351
|
+
);
|
|
352
|
+
await writeFile(
|
|
353
|
+
join(testDir, "main.ts"),
|
|
354
|
+
`
|
|
355
|
+
import { Bucket } from "./resources";
|
|
356
|
+
import { params } from ${JSON.stringify(paramsPath)};
|
|
357
|
+
export const uploads = new Bucket({ name: \`billing-\${params.env}-uploads\` });
|
|
358
|
+
`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function built(): Array<{ name: string; marker?: { stack: string; env?: string } }> {
|
|
363
|
+
return JSON.parse(readFileSync(outputFile, "utf-8"));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function buildWithEnv(env: string) {
|
|
367
|
+
return buildCommand({
|
|
368
|
+
path: testDir,
|
|
369
|
+
output: outputFile,
|
|
370
|
+
format: "json",
|
|
371
|
+
serializers: [namingSerializer],
|
|
372
|
+
params: { env },
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
test("--param env=pr-42 is legal when a pr-* pattern entry is declared", async () => {
|
|
377
|
+
await writeProject(`["dev", "prod", "pr-*"]`);
|
|
378
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
379
|
+
try {
|
|
380
|
+
const result = await buildWithEnv("pr-42");
|
|
381
|
+
expect(result.errors).toEqual([]);
|
|
382
|
+
expect(result.success).toBe(true);
|
|
383
|
+
expect(built()).toEqual([{ name: "billing-pr-42-uploads", marker: { stack: "billing", env: "pr-42" } }]);
|
|
384
|
+
} finally {
|
|
385
|
+
errorSpy.mockRestore();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("--param env outside the declared entries (no pattern covers it) fails the build", async () => {
|
|
390
|
+
await writeProject(`["dev", "prod"]`);
|
|
391
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
392
|
+
try {
|
|
393
|
+
const result = await buildWithEnv("pr-42");
|
|
394
|
+
expect(result.success).toBe(false);
|
|
395
|
+
expect(result.errors.some((e) => e.includes('Unknown environment "pr-42"'))).toBe(true);
|
|
396
|
+
} finally {
|
|
397
|
+
errorSpy.mockRestore();
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("a literal entry still matches by equality", async () => {
|
|
402
|
+
await writeProject(`["dev", "prod", "pr-*"]`);
|
|
403
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
404
|
+
try {
|
|
405
|
+
const result = await buildWithEnv("prod");
|
|
406
|
+
expect(result.errors).toEqual([]);
|
|
407
|
+
expect(built()).toEqual([{ name: "billing-prod-uploads", marker: { stack: "billing", env: "prod" } }]);
|
|
408
|
+
} finally {
|
|
409
|
+
errorSpy.mockRestore();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("two builds with different --param env yield disjoint names and disjoint markers", async () => {
|
|
414
|
+
await writeProject(`["dev", "prod", "pr-*"]`);
|
|
415
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
416
|
+
try {
|
|
417
|
+
expect((await buildWithEnv("pr-1")).success).toBe(true);
|
|
418
|
+
const first = built();
|
|
419
|
+
expect((await buildWithEnv("pr-2")).success).toBe(true);
|
|
420
|
+
const second = built();
|
|
421
|
+
expect(first).toEqual([{ name: "billing-pr-1-uploads", marker: { stack: "billing", env: "pr-1" } }]);
|
|
422
|
+
expect(second).toEqual([{ name: "billing-pr-2-uploads", marker: { stack: "billing", env: "pr-2" } }]);
|
|
423
|
+
expect(first[0].name).not.toBe(second[0].name);
|
|
424
|
+
expect(first[0].marker?.env).not.toBe(second[0].marker?.env);
|
|
425
|
+
} finally {
|
|
426
|
+
errorSpy.mockRestore();
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("a project with no declared environments accepts any dynamic value, as before", async () => {
|
|
431
|
+
await writeProject(`undefined`);
|
|
432
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
433
|
+
try {
|
|
434
|
+
const result = await buildWithEnv("anything-goes");
|
|
435
|
+
expect(result.errors).toEqual([]);
|
|
436
|
+
expect(built()).toEqual([{ name: "billing-anything-goes-uploads", marker: { stack: "billing", env: "anything-goes" } }]);
|
|
437
|
+
} finally {
|
|
438
|
+
errorSpy.mockRestore();
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
186
443
|
test("#1064 — a declared build-time parameter binds to params.<name> and folds to a literal", async () => {
|
|
187
444
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
188
445
|
const runtimePath = resolvePath(thisDir, "../../runtime");
|
|
@@ -235,10 +492,10 @@ export const testEntity = {
|
|
|
235
492
|
expect(result.resourceCount).toBe(1);
|
|
236
493
|
expect(result.buildParams).toEqual([{ name: "tier", value: "production", source: "cli" }]);
|
|
237
494
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
expect(
|
|
495
|
+
// #1424 — without --verbose the echo is a one-line count by source.
|
|
496
|
+
const logged = errorSpy.mock.calls.map((call) => String(call[0]));
|
|
497
|
+
expect(logged.some((line) => line.includes("1 build parameter resolved (1 from cli)"))).toBe(true);
|
|
498
|
+
expect(logged.some((line) => line.includes("[param] tier"))).toBe(false);
|
|
242
499
|
} finally {
|
|
243
500
|
errorSpy.mockRestore();
|
|
244
501
|
}
|
|
@@ -346,6 +603,19 @@ export const testEntity = {
|
|
|
346
603
|
|
|
347
604
|
expect(result.success).toBe(true);
|
|
348
605
|
expect(result.resourceCount).toBe(1);
|
|
606
|
+
// #1424 — the default report is one summary line, not one line per file.
|
|
607
|
+
const logged = errorSpy.mock.calls.map((call) => String(call[0]));
|
|
608
|
+
expect(logged.some((line) => /^.*fold: \d+ files? folded, \d+ ran/.test(line))).toBe(true);
|
|
609
|
+
expect(logged.some((line) => line.includes("[fold:"))).toBe(false);
|
|
610
|
+
|
|
611
|
+
errorSpy.mockClear();
|
|
612
|
+
const verboseResult = await buildCommand({
|
|
613
|
+
path: testDir,
|
|
614
|
+
format: "json",
|
|
615
|
+
serializers: [mockSerializer],
|
|
616
|
+
verbose: true,
|
|
617
|
+
});
|
|
618
|
+
expect(verboseResult.success).toBe(true);
|
|
349
619
|
const anyFoldLine = errorSpy.mock.calls.map((call) => String(call[0])).some((line) => line.includes("[fold:"));
|
|
350
620
|
expect(anyFoldLine).toBe(true);
|
|
351
621
|
|
|
@@ -360,7 +630,7 @@ export const testEntity = {
|
|
|
360
630
|
expect(runResult.resourceCount).toBe(1);
|
|
361
631
|
const anyFoldLineOff = errorSpy.mock.calls
|
|
362
632
|
.map((call) => String(call[0]))
|
|
363
|
-
.some((line) => line.includes("[fold:"));
|
|
633
|
+
.some((line) => line.includes("[fold:") || line.includes("fold: "));
|
|
364
634
|
expect(anyFoldLineOff).toBe(false);
|
|
365
635
|
} finally {
|
|
366
636
|
errorSpy.mockRestore();
|