@intentius/chant 0.21.0 → 0.23.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/build.d.ts +7 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +55 -0
- package/dist/cli/build-params-cli.d.ts.map +1 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +14 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/lsp/server.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +33 -2
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +28 -0
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +71 -9
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts +27 -4
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +39 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +11 -1
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +14 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/serializer-walker.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build.ts +9 -0
- package/src/cli/build-params-cli.test.ts +139 -0
- package/src/cli/build-params-cli.ts +107 -0
- package/src/cli/commands/build.ts +25 -36
- package/src/cli/commands/check-lexicon-examples.ts +16 -1
- package/src/cli/commands/lint.test.ts +74 -0
- package/src/cli/commands/lint.ts +33 -9
- package/src/cli/handlers/build.test.ts +147 -0
- package/src/cli/handlers/build.ts +23 -8
- package/src/cli/handlers/run.test.ts +160 -5
- package/src/cli/handlers/run.ts +46 -8
- package/src/cli/lsp/server.ts +7 -2
- package/src/components/cli-support.test.ts +221 -3
- package/src/components/cli-support.ts +37 -6
- package/src/components/discover.test.ts +63 -1
- package/src/components/discover.ts +42 -0
- package/src/discovery/fold-import.test.ts +328 -1
- package/src/discovery/fold-import.ts +414 -31
- package/src/discovery/index.test.ts +131 -0
- package/src/discovery/index.ts +53 -8
- package/src/discovery/sandbox/fold-boundary.test.ts +254 -0
- package/src/fold/fold.ts +6 -2
- package/src/fold/subset.test.ts +95 -15
- package/src/fold/subset.ts +46 -5
- package/src/lint/engine.ts +12 -0
- package/src/lint/rule.ts +14 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +39 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +1 -1
- package/src/serializer-walker.ts +14 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `chant build --components --generate <lexicon>` (generate mode,
|
|
3
|
+
* #563) — specifically its chant #1108 build-time-parameter resolution,
|
|
4
|
+
* which was entirely missing before this fix: `generateComponentsPipeline`'s
|
|
5
|
+
* `discoverComponents` call never resolved `chant.config.ts`'s declared
|
|
6
|
+
* `buildParams`, so a `*.component.ts` file reading `params.<name>` always
|
|
7
|
+
* saw `{}` under `chant build --components --generate`, exactly like `chant
|
|
8
|
+
* run --components` (see ../handlers/run.test.ts's "build-time parameters"
|
|
9
|
+
* describe block for the equivalent local/`--temporal` coverage).
|
|
10
|
+
*
|
|
11
|
+
* Mocks `generateComponentsPipeline` and `loadChantConfig` and exercises the
|
|
12
|
+
* public `runBuild` dispatcher (`runGenerateComponents` itself isn't
|
|
13
|
+
* exported), mirroring `run.test.ts`'s style of driving the handler through
|
|
14
|
+
* its `CommandContext` entrypoint rather than reaching into private helpers.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
17
|
+
import type { ParsedArgs } from "../registry";
|
|
18
|
+
|
|
19
|
+
const generateComponentsPipelineMock = vi.fn();
|
|
20
|
+
const loadChantConfigMock = vi.fn();
|
|
21
|
+
|
|
22
|
+
vi.mock("../../components/cli-support", () => ({
|
|
23
|
+
generateComponentsPipeline: (...args: unknown[]) => generateComponentsPipelineMock(...args),
|
|
24
|
+
}));
|
|
25
|
+
vi.mock("../../config", async () => {
|
|
26
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
27
|
+
return { ...actual, loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args) };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const { runBuild } = await import("./build");
|
|
31
|
+
|
|
32
|
+
function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
|
|
33
|
+
return {
|
|
34
|
+
command: "build",
|
|
35
|
+
path: ".",
|
|
36
|
+
format: "",
|
|
37
|
+
fix: false,
|
|
38
|
+
watch: false,
|
|
39
|
+
verbose: false,
|
|
40
|
+
help: false,
|
|
41
|
+
live: false,
|
|
42
|
+
components: true,
|
|
43
|
+
generate: "gitlab",
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeStderrSpy() {
|
|
49
|
+
const buf: string[] = [];
|
|
50
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { buf.push(s); });
|
|
51
|
+
return buf;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("runBuild --components --generate (chant #1108 build-time parameters)", () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
generateComponentsPipelineMock.mockReset();
|
|
57
|
+
loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("no declared buildParams → generateComponentsPipeline is called with an empty provenance array", async () => {
|
|
61
|
+
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
|
|
62
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
63
|
+
|
|
64
|
+
const exit = await runBuild({ args: makeArgs(), plugins: [], serializers: [] });
|
|
65
|
+
|
|
66
|
+
expect(exit).toBe(0);
|
|
67
|
+
expect(generateComponentsPipelineMock).toHaveBeenCalledWith(".", "gitlab", { env: undefined }, undefined, []);
|
|
68
|
+
vi.restoreAllMocks();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("chant.config.ts's declared buildParams resolve, log, and are forwarded to generateComponentsPipeline", async () => {
|
|
72
|
+
loadChantConfigMock.mockResolvedValue({
|
|
73
|
+
config: { buildParams: { tier: { type: "string", default: "light" } } },
|
|
74
|
+
});
|
|
75
|
+
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
|
|
76
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
77
|
+
const stderr = makeStderrSpy();
|
|
78
|
+
|
|
79
|
+
const exit = await runBuild({ args: makeArgs(), plugins: [], serializers: [] });
|
|
80
|
+
|
|
81
|
+
expect(exit).toBe(0);
|
|
82
|
+
expect(generateComponentsPipelineMock).toHaveBeenCalledWith(
|
|
83
|
+
".",
|
|
84
|
+
"gitlab",
|
|
85
|
+
{ env: undefined },
|
|
86
|
+
undefined,
|
|
87
|
+
[{ name: "tier", value: "light", source: "default" }],
|
|
88
|
+
);
|
|
89
|
+
expect(stderr.join("\n")).toContain("[param] tier");
|
|
90
|
+
vi.restoreAllMocks();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("--param overrides a declared default", async () => {
|
|
94
|
+
loadChantConfigMock.mockResolvedValue({
|
|
95
|
+
config: { buildParams: { tier: { type: "string", default: "light" } } },
|
|
96
|
+
});
|
|
97
|
+
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
|
|
98
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
99
|
+
|
|
100
|
+
const exit = await runBuild({
|
|
101
|
+
args: makeArgs({ param: ["tier=production"] }),
|
|
102
|
+
plugins: [],
|
|
103
|
+
serializers: [],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(exit).toBe(0);
|
|
107
|
+
expect(generateComponentsPipelineMock).toHaveBeenCalledWith(
|
|
108
|
+
".",
|
|
109
|
+
"gitlab",
|
|
110
|
+
{ env: undefined },
|
|
111
|
+
undefined,
|
|
112
|
+
[{ name: "tier", value: "production", source: "cli" }],
|
|
113
|
+
);
|
|
114
|
+
vi.restoreAllMocks();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("an unresolved required build-time parameter → exit 1, never reaches generateComponentsPipeline", async () => {
|
|
118
|
+
loadChantConfigMock.mockResolvedValue({
|
|
119
|
+
config: { buildParams: { tier: { type: "string" } } },
|
|
120
|
+
});
|
|
121
|
+
const stderr = makeStderrSpy();
|
|
122
|
+
|
|
123
|
+
const exit = await runBuild({ args: makeArgs(), plugins: [], serializers: [] });
|
|
124
|
+
|
|
125
|
+
expect(exit).toBe(1);
|
|
126
|
+
expect(stderr.join("\n")).toMatch(/"tier"/);
|
|
127
|
+
expect(generateComponentsPipelineMock).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("an enum violation on --param → exit 1, never reaches generateComponentsPipeline", async () => {
|
|
131
|
+
loadChantConfigMock.mockResolvedValue({
|
|
132
|
+
config: { buildParams: { tier: { type: "string", enum: ["light", "production"] } } },
|
|
133
|
+
});
|
|
134
|
+
const stderr = makeStderrSpy();
|
|
135
|
+
|
|
136
|
+
const exit = await runBuild({
|
|
137
|
+
args: makeArgs({ param: ["tier=bogus"] }),
|
|
138
|
+
plugins: [],
|
|
139
|
+
serializers: [],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(exit).toBe(1);
|
|
143
|
+
expect(stderr.join("\n")).toMatch(/"tier"/);
|
|
144
|
+
expect(stderr.join("\n")).toMatch(/bogus/);
|
|
145
|
+
expect(generateComponentsPipelineMock).not.toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -4,6 +4,8 @@ import { buildCommand, buildCommandWatch, printErrors, printWarnings, resolveBui
|
|
|
4
4
|
import { formatError, formatInfo, formatSuccess, formatBold } from "../format";
|
|
5
5
|
import type { CommandContext } from "../registry";
|
|
6
6
|
import { generateComponentsPipeline } from "../../components/cli-support";
|
|
7
|
+
import { loadChantConfig, type ChantConfig } from "../../config";
|
|
8
|
+
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* `chant build --components --generate <lexicon>` — generate mode (#563,
|
|
@@ -12,11 +14,30 @@ import { generateComponentsPipeline } from "../../components/cli-support";
|
|
|
12
14
|
* normal lexicon build: no entity discovery, no serializers, no post-synth
|
|
13
15
|
* checks — those apply to lexicon resources, which is a different input to
|
|
14
16
|
* a different command path (`chant build` without `--components`).
|
|
17
|
+
*
|
|
18
|
+
* chant #1108 — resolves this invocation's declared build-time parameters
|
|
19
|
+
* (`chant.config.ts`'s `buildParams`, against `--param`/`--params-file`/a
|
|
20
|
+
* declared `env` mapping) the exact same way `chant build` does
|
|
21
|
+
* (`resolveCliBuildParams`, shared with `buildCommand`), BEFORE
|
|
22
|
+
* `generateComponentsPipeline` discovers/imports any `*.component.ts` file.
|
|
23
|
+
* Before this, `params.*` (`@intentius/chant/params`) was always `{}` under
|
|
24
|
+
* this command too — generate mode shares `discoverComponents` with `chant
|
|
25
|
+
* run --components`, so it had the identical gap.
|
|
15
26
|
*/
|
|
16
27
|
async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
17
28
|
const { args } = ctx;
|
|
18
29
|
const lexicon = args.generate as string;
|
|
19
30
|
|
|
31
|
+
const { config } = await loadChantConfig(resolve(args.path)).catch(() => ({ config: {} as ChantConfig }));
|
|
32
|
+
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
33
|
+
cli: parseParamFlags(args.param),
|
|
34
|
+
paramsFile: args.paramsFile,
|
|
35
|
+
});
|
|
36
|
+
if (!paramsResolution.success) {
|
|
37
|
+
for (const message of paramsResolution.errors) console.error(message);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
// Which lexicons support generate mode is a property of the loaded lexicon
|
|
21
42
|
// plugins (those implementing `generateComponentPipeline`, #688), not a
|
|
22
43
|
// hard-coded core list — `generateComponentsPipeline` returns a descriptive
|
|
@@ -26,6 +47,7 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
26
47
|
lexicon,
|
|
27
48
|
{ env: args.env },
|
|
28
49
|
args.sandbox,
|
|
50
|
+
paramsResolution.provenance,
|
|
29
51
|
);
|
|
30
52
|
|
|
31
53
|
if (!result.success) {
|
|
@@ -87,14 +109,7 @@ export async function runBuild(ctx: CommandContext): Promise<number> {
|
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
// #1064 — `--param name=value`, repeated, into a flat { name: value } record.
|
|
90
|
-
const params = args.param
|
|
91
|
-
? Object.fromEntries(
|
|
92
|
-
args.param.map((entry) => {
|
|
93
|
-
const eq = entry.indexOf("=");
|
|
94
|
-
return eq === -1 ? [entry, ""] : [entry.slice(0, eq), entry.slice(eq + 1)];
|
|
95
|
-
}),
|
|
96
|
-
)
|
|
97
|
-
: undefined;
|
|
112
|
+
const params = parseParamFlags(args.param);
|
|
98
113
|
|
|
99
114
|
if (args.watch) {
|
|
100
115
|
const cleanup = buildCommandWatch({
|
|
@@ -878,7 +878,7 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
|
|
|
878
878
|
const exit = await runOp({ args: makeArgs({ path: "svc", components: true, temporal: false }), plugins: [], serializers: [] });
|
|
879
879
|
|
|
880
880
|
expect(exit).toBe(0);
|
|
881
|
-
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: undefined, componentOutputs: {} });
|
|
881
|
+
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: undefined, componentOutputs: {}, buildParams: [] });
|
|
882
882
|
expect(discoverOpsMock).not.toHaveBeenCalled();
|
|
883
883
|
vi.restoreAllMocks();
|
|
884
884
|
});
|
|
@@ -900,6 +900,87 @@ describe("runOpComponents", () => {
|
|
|
900
900
|
expect(runComponentsMock).not.toHaveBeenCalled();
|
|
901
901
|
});
|
|
902
902
|
|
|
903
|
+
// ── build-time parameters (chant #1108) — resolved BEFORE dispatch ────────
|
|
904
|
+
|
|
905
|
+
describe("build-time parameters", () => {
|
|
906
|
+
test("chant.config.ts's declared buildParams resolve and log before dispatching to runComponents", async () => {
|
|
907
|
+
loadChantConfigMock.mockResolvedValue({
|
|
908
|
+
config: { buildParams: { tier: { type: "string", default: "light" } } },
|
|
909
|
+
});
|
|
910
|
+
runComponentsMock.mockResolvedValue({
|
|
911
|
+
success: true,
|
|
912
|
+
selected: ["svc"],
|
|
913
|
+
run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
|
|
914
|
+
});
|
|
915
|
+
const stderr = makeStderrSpy();
|
|
916
|
+
|
|
917
|
+
const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
|
|
918
|
+
|
|
919
|
+
expect(exit).toBe(0);
|
|
920
|
+
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", expect.objectContaining({
|
|
921
|
+
buildParams: [{ name: "tier", value: "light", source: "default" }],
|
|
922
|
+
}));
|
|
923
|
+
expect(stderr.join("\n")).toContain("[param] tier");
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
test("--param overrides a declared default and is threaded through to runComponents", async () => {
|
|
927
|
+
loadChantConfigMock.mockResolvedValue({
|
|
928
|
+
config: { buildParams: { tier: { type: "string", default: "light" } } },
|
|
929
|
+
});
|
|
930
|
+
runComponentsMock.mockResolvedValue({
|
|
931
|
+
success: true,
|
|
932
|
+
selected: ["svc"],
|
|
933
|
+
run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
|
|
934
|
+
});
|
|
935
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
936
|
+
const stderr = makeStderrSpy();
|
|
937
|
+
|
|
938
|
+
const exit = await runOpComponents({
|
|
939
|
+
args: makeArgs({ path: "svc", temporal: false, param: ["tier=production"] }),
|
|
940
|
+
plugins: [],
|
|
941
|
+
serializers: [],
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
expect(exit).toBe(0);
|
|
945
|
+
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", expect.objectContaining({
|
|
946
|
+
buildParams: [{ name: "tier", value: "production", source: "cli" }],
|
|
947
|
+
}));
|
|
948
|
+
expect(stderr.join("\n")).toContain("[param] tier");
|
|
949
|
+
vi.restoreAllMocks();
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
test("an unresolved required build-time parameter → exit 1 with a formatted error naming it, never reaches runComponents (the previously-{} probe, now a hard stop instead)", async () => {
|
|
953
|
+
loadChantConfigMock.mockResolvedValue({
|
|
954
|
+
config: { buildParams: { tier: { type: "string" } } },
|
|
955
|
+
});
|
|
956
|
+
const stderr = makeStderrSpy();
|
|
957
|
+
|
|
958
|
+
const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
|
|
959
|
+
|
|
960
|
+
expect(exit).toBe(1);
|
|
961
|
+
expect(stderr.join("\n")).toMatch(/"tier"/);
|
|
962
|
+
expect(runComponentsMock).not.toHaveBeenCalled();
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
test("an enum violation on --param → exit 1 with a formatted error, never reaches runComponents", async () => {
|
|
966
|
+
loadChantConfigMock.mockResolvedValue({
|
|
967
|
+
config: { buildParams: { tier: { type: "string", enum: ["light", "production"] } } },
|
|
968
|
+
});
|
|
969
|
+
const stderr = makeStderrSpy();
|
|
970
|
+
|
|
971
|
+
const exit = await runOpComponents({
|
|
972
|
+
args: makeArgs({ path: "svc", temporal: false, param: ["tier=bogus"] }),
|
|
973
|
+
plugins: [],
|
|
974
|
+
serializers: [],
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
expect(exit).toBe(1);
|
|
978
|
+
expect(stderr.join("\n")).toMatch(/"tier"/);
|
|
979
|
+
expect(stderr.join("\n")).toMatch(/bogus/);
|
|
980
|
+
expect(runComponentsMock).not.toHaveBeenCalled();
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
|
|
903
984
|
test("happy path: single component, human output, exit 0", async () => {
|
|
904
985
|
runComponentsMock.mockResolvedValue({
|
|
905
986
|
success: true,
|
|
@@ -916,7 +997,7 @@ describe("runOpComponents", () => {
|
|
|
916
997
|
const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
|
|
917
998
|
|
|
918
999
|
expect(exit).toBe(0);
|
|
919
|
-
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: undefined, componentOutputs: {} });
|
|
1000
|
+
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: undefined, componentOutputs: {}, buildParams: [] });
|
|
920
1001
|
const printed = stderrWrite.mock.calls.map((c) => String(c[0])).join("");
|
|
921
1002
|
expect(printed).toContain("interpret run completed");
|
|
922
1003
|
vi.restoreAllMocks();
|
|
@@ -928,7 +1009,7 @@ describe("runOpComponents", () => {
|
|
|
928
1009
|
|
|
929
1010
|
await runOpComponents({ args: makeArgs({ path: "svc", env: "staging", temporal: false }), plugins: [], serializers: [] });
|
|
930
1011
|
|
|
931
|
-
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: "staging", componentOutputs: {} });
|
|
1012
|
+
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: "staging", componentOutputs: {}, buildParams: [] });
|
|
932
1013
|
vi.restoreAllMocks();
|
|
933
1014
|
});
|
|
934
1015
|
|
|
@@ -994,6 +1075,7 @@ describe("runOpComponents", () => {
|
|
|
994
1075
|
env: undefined,
|
|
995
1076
|
componentOutputs: {},
|
|
996
1077
|
onProgress: undefined,
|
|
1078
|
+
buildParams: [],
|
|
997
1079
|
});
|
|
998
1080
|
expect(stdoutWrite).not.toHaveBeenCalled();
|
|
999
1081
|
vi.restoreAllMocks();
|
|
@@ -1018,7 +1100,7 @@ describe("runOpComponents", () => {
|
|
|
1018
1100
|
const exit = await runOpComponents({ args: makeArgs({ path: "all", temporal: false }), plugins: [], serializers: [] });
|
|
1019
1101
|
|
|
1020
1102
|
expect(exit).toBe(0);
|
|
1021
|
-
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "all", { env: undefined, componentOutputs: {} });
|
|
1103
|
+
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "all", { env: undefined, componentOutputs: {}, buildParams: [] });
|
|
1022
1104
|
const printed = stderrWrite.mock.calls.map((c) => String(c[0])).join("");
|
|
1023
1105
|
expect(printed).toContain("shared-alb");
|
|
1024
1106
|
expect(printed).toContain("search-service");
|
|
@@ -1269,7 +1351,13 @@ describe("runOpComponents: --temporal routes to the durable path", () => {
|
|
|
1269
1351
|
resolveComponentTargetsMock.mockReset();
|
|
1270
1352
|
findComponentGateMock.mockReset();
|
|
1271
1353
|
loadComponentTemporalCodegenMock.mockReset();
|
|
1272
|
-
|
|
1354
|
+
// chant #1108 — runOpComponents now resolves build-time parameters (which
|
|
1355
|
+
// needs chant.config.ts's declared `buildParams`) BEFORE dispatching to
|
|
1356
|
+
// either the local or --temporal path, so every test in this block hits
|
|
1357
|
+
// loadChantConfig at least once now, even ones that never reach the rest
|
|
1358
|
+
// of the durable path (e.g. "unknown component"). Individual tests below
|
|
1359
|
+
// still override this where they care about a specific config shape.
|
|
1360
|
+
loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
|
|
1273
1361
|
resolveProfileMock.mockReset();
|
|
1274
1362
|
loadTemporalClientMock.mockReset();
|
|
1275
1363
|
spawnChildMock.mockReset();
|
|
@@ -1297,6 +1385,73 @@ describe("runOpComponents: --temporal routes to the durable path", () => {
|
|
|
1297
1385
|
expect(stderr.join("\n")).toContain('Component "missing" not found');
|
|
1298
1386
|
});
|
|
1299
1387
|
|
|
1388
|
+
// ── build-time parameters (chant #1108) — resolved before discovery here too ─
|
|
1389
|
+
|
|
1390
|
+
test("resolved build-time parameters are forwarded into resolveComponentTargets on the --temporal path", async () => {
|
|
1391
|
+
process.env.LOOM_ENV = "staging";
|
|
1392
|
+
resolveComponentTargetsMock.mockResolvedValue({
|
|
1393
|
+
success: true,
|
|
1394
|
+
targets: [{ name: "gated-svc", dependsOn: [], deploy: [] }],
|
|
1395
|
+
});
|
|
1396
|
+
resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
|
|
1397
|
+
loadComponentTemporalCodegenMock.mockResolvedValue({
|
|
1398
|
+
serializeComponent: () => ({ "components/gated-svc/worker.ts": "// worker" }),
|
|
1399
|
+
componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
|
|
1400
|
+
});
|
|
1401
|
+
const mockClient = createMockTemporalClient({
|
|
1402
|
+
describeByWorkflowId: {
|
|
1403
|
+
"chant-component-gated-svc": {
|
|
1404
|
+
workflowId: "chant-component-gated-svc", runId: "r1",
|
|
1405
|
+
status: { name: "COMPLETED" }, startTime: new Date(),
|
|
1406
|
+
taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
|
|
1407
|
+
},
|
|
1408
|
+
},
|
|
1409
|
+
historyByWorkflowId: { "chant-component-gated-svc": [] },
|
|
1410
|
+
});
|
|
1411
|
+
// setupTemporalClient sets its own default loadChantConfigMock resolved
|
|
1412
|
+
// value, so the test's own (buildParams-declaring) config must be set
|
|
1413
|
+
// AFTER calling it, not before.
|
|
1414
|
+
setupTemporalClient(mockClient);
|
|
1415
|
+
loadChantConfigMock.mockResolvedValue({
|
|
1416
|
+
config: { buildParams: { env: { type: "string", env: "LOOM_ENV", default: "dev" } } },
|
|
1417
|
+
});
|
|
1418
|
+
const { proc } = makeFakeChildProcess();
|
|
1419
|
+
spawnChildMock.mockReturnValue(proc);
|
|
1420
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
1421
|
+
|
|
1422
|
+
try {
|
|
1423
|
+
vi.useFakeTimers();
|
|
1424
|
+
const promise = runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true }), plugins: [], serializers: [] });
|
|
1425
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
1426
|
+
const exit = await promise;
|
|
1427
|
+
|
|
1428
|
+
expect(exit).toBe(0);
|
|
1429
|
+
expect(resolveComponentTargetsMock).toHaveBeenCalledWith(
|
|
1430
|
+
expect.any(String),
|
|
1431
|
+
"gated-svc",
|
|
1432
|
+
undefined,
|
|
1433
|
+
[{ name: "env", value: "staging", source: "env" }],
|
|
1434
|
+
);
|
|
1435
|
+
} finally {
|
|
1436
|
+
vi.useRealTimers();
|
|
1437
|
+
vi.restoreAllMocks();
|
|
1438
|
+
delete process.env.LOOM_ENV;
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
test("an unresolved required build-time parameter → exit 1, never reaches resolveComponentTargets", async () => {
|
|
1443
|
+
loadChantConfigMock.mockResolvedValue({
|
|
1444
|
+
config: { buildParams: { tier: { type: "string" } } },
|
|
1445
|
+
});
|
|
1446
|
+
const stderr = makeStderrSpy();
|
|
1447
|
+
|
|
1448
|
+
const exit = await runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true }), plugins: [], serializers: [] });
|
|
1449
|
+
|
|
1450
|
+
expect(exit).toBe(1);
|
|
1451
|
+
expect(stderr.join("\n")).toMatch(/"tier"/);
|
|
1452
|
+
expect(resolveComponentTargetsMock).not.toHaveBeenCalled();
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1300
1455
|
test("compiles the component, spawns the worker, submits the workflow, polls to COMPLETED", async () => {
|
|
1301
1456
|
vi.useFakeTimers();
|
|
1302
1457
|
try {
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -2,12 +2,13 @@ import { resolve, join, dirname } from "node:path";
|
|
|
2
2
|
import { existsSync, writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { createConnection } from "node:net";
|
|
4
4
|
import { spawn as spawnChild, type ChildProcess } from "node:child_process";
|
|
5
|
-
import { loadChantConfig, resolveAutoReleaseDisabled } from "../../config";
|
|
5
|
+
import { loadChantConfig, resolveAutoReleaseDisabled, type ChantConfig } from "../../config";
|
|
6
6
|
import { discoverOps } from "../../op/discover";
|
|
7
7
|
import { loadActivities, loadProfiles } from "../../op/activity-registry";
|
|
8
8
|
import { runOpLocally, findGate, LocalGateUnsupportedError, OpRunFailure } from "../../op/local-executor";
|
|
9
9
|
import { renderHuman, renderJson } from "../../op/local-output";
|
|
10
10
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
11
|
+
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
11
12
|
import type { CommandContext } from "../registry";
|
|
12
13
|
import {
|
|
13
14
|
loadTemporalClient,
|
|
@@ -27,6 +28,7 @@ import { applyConfigDefaults } from "../../components/config-defaults";
|
|
|
27
28
|
import { maybeRecordAutoRelease, extractRunDigestFromPhaseOutputs } from "../../components/auto-release";
|
|
28
29
|
import { maybePersistBuildManifest, extractRunManifestFromPhaseOutputs } from "../../components/manifest-persistence";
|
|
29
30
|
import type { DriverComponentResult } from "../../components/driver";
|
|
31
|
+
import type { BuildParamProvenance } from "../../provenance";
|
|
30
32
|
|
|
31
33
|
function kebabToCamel(s: string): string {
|
|
32
34
|
return s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
@@ -669,6 +671,18 @@ async function recordAutoReleasesForRun(
|
|
|
669
671
|
* Opt out with `--no-release-record` or `chant.config.ts`'s
|
|
670
672
|
* `release.autoRecord: false`; the default is ON. A failed run never reaches
|
|
671
673
|
* this step, so it writes nothing.
|
|
674
|
+
*
|
|
675
|
+
* chant #1108 — resolves this invocation's declared build-time parameters
|
|
676
|
+
* (`chant.config.ts`'s `buildParams`, against `--param`/`--params-file`/a
|
|
677
|
+
* declared `env` mapping) the exact same way `chant build` does
|
|
678
|
+
* (`resolveCliBuildParams`, shared with `buildCommand`), BEFORE either the
|
|
679
|
+
* local or `--temporal` path discovers/imports any `*.component.ts` file.
|
|
680
|
+
* Before this, `params.*` (`@intentius/chant/params`) was always `{}` under
|
|
681
|
+
* this command, no matter what a component's `chant.config.ts` declared or a
|
|
682
|
+
* CI job's environment supplied — see chant #1108. `chant.config.ts` is
|
|
683
|
+
* loaded once here (with the same defensive fallback the post-run
|
|
684
|
+
* auto-release check below used to apply itself) and reused for that check,
|
|
685
|
+
* rather than loaded a second time.
|
|
672
686
|
*/
|
|
673
687
|
export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
674
688
|
const selector = ctx.args.path;
|
|
@@ -680,7 +694,18 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
680
694
|
return 1;
|
|
681
695
|
}
|
|
682
696
|
|
|
683
|
-
|
|
697
|
+
const projectPath = resolve(".");
|
|
698
|
+
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
699
|
+
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
700
|
+
cli: parseParamFlags(ctx.args.param),
|
|
701
|
+
paramsFile: ctx.args.paramsFile,
|
|
702
|
+
});
|
|
703
|
+
if (!paramsResolution.success) {
|
|
704
|
+
for (const message of paramsResolution.errors) console.error(message);
|
|
705
|
+
return 1;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (ctx.args.temporal) return runComponentTemporal(ctx, selector, config, paramsResolution.provenance);
|
|
684
709
|
|
|
685
710
|
const env = ctx.args.env ?? "local";
|
|
686
711
|
// Seed cross-component/cross-stack outputs from upstream jobs' dumped files
|
|
@@ -704,11 +729,12 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
704
729
|
// `undefined` and every `onProgress?.(...)` call in the driver is a no-op —
|
|
705
730
|
// behavior is byte-for-byte unchanged from before this flag existed.
|
|
706
731
|
const onProgress = ctx.args.progressJson ? ndjsonProgressSink() : undefined;
|
|
707
|
-
const result = await runComponents(
|
|
732
|
+
const result = await runComponents(projectPath, selector, {
|
|
708
733
|
env: ctx.args.env,
|
|
709
734
|
componentOutputs: seededOutputs,
|
|
710
735
|
onProgress,
|
|
711
736
|
sandbox: ctx.args.sandbox,
|
|
737
|
+
buildParams: paramsResolution.provenance,
|
|
712
738
|
});
|
|
713
739
|
|
|
714
740
|
// Dump the accumulated outputs for a downstream job to seed from. Written
|
|
@@ -739,7 +765,6 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
739
765
|
}
|
|
740
766
|
|
|
741
767
|
if (result.success && result.run) {
|
|
742
|
-
const { config } = await loadChantConfig(resolve(".")).catch(() => ({ config: {} }));
|
|
743
768
|
const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord);
|
|
744
769
|
await recordAutoReleasesForRun(result.run.results, env, `local-${Date.now()}`, disabled);
|
|
745
770
|
}
|
|
@@ -771,8 +796,19 @@ function componentWorkflowId(componentName: string): string {
|
|
|
771
796
|
* crash and clearable via `chant run signal <name> <signal> --components
|
|
772
797
|
* --temporal` (`runOpSignal` above, extended for components alongside this
|
|
773
798
|
* issue).
|
|
799
|
+
*
|
|
800
|
+
* `config`/`buildParams` are resolved once by the caller (`runOpComponents`,
|
|
801
|
+
* chant #1108) BEFORE this function runs, so `resolveComponentTargets`
|
|
802
|
+
* below — which discovers/imports the target `*.component.ts` file — sees
|
|
803
|
+
* `params.*` (`@intentius/chant/params`) already populated, the same
|
|
804
|
+
* guarantee the local executor path gets.
|
|
774
805
|
*/
|
|
775
|
-
async function runComponentTemporal(
|
|
806
|
+
async function runComponentTemporal(
|
|
807
|
+
ctx: CommandContext,
|
|
808
|
+
selector: string,
|
|
809
|
+
config: ChantConfig,
|
|
810
|
+
buildParams: BuildParamProvenance[],
|
|
811
|
+
): Promise<number> {
|
|
776
812
|
if (selector === "all") {
|
|
777
813
|
console.error(formatError({
|
|
778
814
|
message: "`chant run --components all --temporal` is not supported",
|
|
@@ -782,15 +818,17 @@ async function runComponentTemporal(ctx: CommandContext, selector: string): Prom
|
|
|
782
818
|
}
|
|
783
819
|
|
|
784
820
|
const projectPath = resolve(".");
|
|
785
|
-
const resolved = await resolveComponentTargets(projectPath, selector, ctx.args.sandbox);
|
|
821
|
+
const resolved = await resolveComponentTargets(projectPath, selector, ctx.args.sandbox, buildParams);
|
|
786
822
|
if (!resolved.success || resolved.targets.length === 0) {
|
|
787
823
|
console.error(formatError({ message: resolved.error ?? `Component "${selector}" not found` }));
|
|
788
824
|
return 1;
|
|
789
825
|
}
|
|
790
826
|
const component = resolved.targets[0];
|
|
791
827
|
|
|
792
|
-
//
|
|
793
|
-
|
|
828
|
+
// Config + profile (mirrors runOpTemporal). `config` was already loaded by
|
|
829
|
+
// the caller to resolve build-time parameters (chant #1108); reused here
|
|
830
|
+
// rather than loading chant.config.ts a second time.
|
|
831
|
+
const chantConfig = config;
|
|
794
832
|
let profile;
|
|
795
833
|
try {
|
|
796
834
|
profile = resolveProfile(chantConfig as Record<string, unknown>, ctx.args.profile);
|
package/src/cli/lsp/server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LexiconPlugin } from "../../lexicon";
|
|
1
|
+
import type { LexiconPlugin, IntrinsicDef } from "../../lexicon";
|
|
2
2
|
import type { CompletionContext, HoverContext, CodeActionContext } from "../../lsp/types";
|
|
3
3
|
import { computeCapabilities } from "./capabilities";
|
|
4
4
|
import { toLspDiagnostics } from "./diagnostics";
|
|
@@ -356,13 +356,18 @@ export class LspServer {
|
|
|
356
356
|
try {
|
|
357
357
|
const { runLint } = await import("../../lint/engine");
|
|
358
358
|
const rules = [];
|
|
359
|
+
// chant #1106 — the same active plugins' registered intrinsics, so
|
|
360
|
+
// EVL001 answers exactly like `fold()` does for a registered,
|
|
361
|
+
// opted-in call instead of flagging every call as a violation.
|
|
362
|
+
const intrinsics: IntrinsicDef[] = [];
|
|
359
363
|
for (const plugin of this.plugins) {
|
|
360
364
|
rules.push(...(plugin.lintRules?.() ?? []));
|
|
365
|
+
intrinsics.push(...(plugin.intrinsics?.() ?? []));
|
|
361
366
|
}
|
|
362
367
|
|
|
363
368
|
if (rules.length === 0) return [];
|
|
364
369
|
|
|
365
|
-
const { diagnostics } = await runLint([filePath], rules);
|
|
370
|
+
const { diagnostics } = await runLint([filePath], rules, undefined, intrinsics);
|
|
366
371
|
return toLspDiagnostics(diagnostics);
|
|
367
372
|
} catch {
|
|
368
373
|
return [];
|