@intentius/chant-lexicon-aws 0.14.0 → 0.15.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.
Files changed (85) hide show
  1. package/dist/components/__tests__/mock-cloud-executor.d.ts +106 -0
  2. package/dist/components/__tests__/mock-cloud-executor.d.ts.map +1 -0
  3. package/dist/components/apply.d.ts +242 -0
  4. package/dist/components/apply.d.ts.map +1 -0
  5. package/dist/components/builders.d.ts +37 -0
  6. package/dist/components/builders.d.ts.map +1 -0
  7. package/dist/components/capability-plugin.d.ts +23 -0
  8. package/dist/components/capability-plugin.d.ts.map +1 -0
  9. package/dist/components/cloud-executor.d.ts +350 -0
  10. package/dist/components/cloud-executor.d.ts.map +1 -0
  11. package/dist/components/config-bom.d.ts +135 -0
  12. package/dist/components/config-bom.d.ts.map +1 -0
  13. package/dist/components/host-delivery.d.ts +110 -0
  14. package/dist/components/host-delivery.d.ts.map +1 -0
  15. package/dist/components/index.d.ts +19 -0
  16. package/dist/components/index.d.ts.map +1 -0
  17. package/dist/components/job-submission.d.ts +89 -0
  18. package/dist/components/job-submission.d.ts.map +1 -0
  19. package/dist/components/publish.d.ts +194 -0
  20. package/dist/components/publish.d.ts.map +1 -0
  21. package/dist/components/safety.d.ts +49 -0
  22. package/dist/components/safety.d.ts.map +1 -0
  23. package/dist/components/wait-aws.d.ts +65 -0
  24. package/dist/components/wait-aws.d.ts.map +1 -0
  25. package/dist/generated/index.d.ts +7 -4
  26. package/dist/generated/index.d.ts.map +1 -1
  27. package/dist/import/live-export.d.ts.map +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/integrity.json +4 -4
  31. package/dist/lint/audit-catalog.d.ts +10 -0
  32. package/dist/lint/audit-catalog.d.ts.map +1 -0
  33. package/dist/manifest.json +1 -1
  34. package/dist/meta.json +141 -30
  35. package/dist/op/activities/floci.d.ts +54 -0
  36. package/dist/op/activities/floci.d.ts.map +1 -0
  37. package/dist/op/activities/index.d.ts +8 -0
  38. package/dist/op/activities/index.d.ts.map +1 -0
  39. package/dist/ownership.d.ts +10 -0
  40. package/dist/ownership.d.ts.map +1 -0
  41. package/dist/plugin.d.ts.map +1 -1
  42. package/dist/serializer.d.ts.map +1 -1
  43. package/dist/types/index.d.ts +145 -38
  44. package/package.json +12 -2
  45. package/src/components/__fixtures__/schemas/cyclonedx-1.5.schema.json +3800 -0
  46. package/src/components/__fixtures__/schemas/cyclonedx-spdx-license.schema.json +621 -0
  47. package/src/components/__fixtures__/schemas/jsf-0.82.schema.json +240 -0
  48. package/src/components/__fixtures__/schemas/spdx-2.3.schema.json +740 -0
  49. package/src/components/__fixtures__/synthesized-template.json +53 -0
  50. package/src/components/__tests__/mock-cloud-executor.ts +425 -0
  51. package/src/components/apply.test.ts +382 -0
  52. package/src/components/apply.ts +499 -0
  53. package/src/components/builders.test.ts +49 -0
  54. package/src/components/builders.ts +59 -0
  55. package/src/components/capability-plugin.ts +73 -0
  56. package/src/components/cloud-executor.test.ts +25 -0
  57. package/src/components/cloud-executor.ts +814 -0
  58. package/src/components/config-bom.test.ts +273 -0
  59. package/src/components/config-bom.ts +310 -0
  60. package/src/components/host-delivery.test.ts +121 -0
  61. package/src/components/host-delivery.ts +193 -0
  62. package/src/components/index.ts +19 -0
  63. package/src/components/job-submission.test.ts +97 -0
  64. package/src/components/job-submission.ts +138 -0
  65. package/src/components/pilots-e2e.test.ts +460 -0
  66. package/src/components/presets-e2e.test.ts +166 -0
  67. package/src/components/publish.test.ts +318 -0
  68. package/src/components/publish.ts +356 -0
  69. package/src/components/safety.test.ts +47 -0
  70. package/src/components/safety.ts +78 -0
  71. package/src/components/wait-aws.test.ts +79 -0
  72. package/src/components/wait-aws.ts +132 -0
  73. package/src/generated/index.d.ts +145 -38
  74. package/src/generated/index.ts +12 -9
  75. package/src/generated/lexicon-aws.json +141 -30
  76. package/src/import/live-export.ts +2 -1
  77. package/src/index.ts +5 -0
  78. package/src/lint/audit-catalog.ts +45 -0
  79. package/src/op/activities/floci.test.ts +67 -0
  80. package/src/op/activities/floci.ts +149 -0
  81. package/src/op/activities/index.ts +16 -0
  82. package/src/ownership.test.ts +18 -0
  83. package/src/ownership.ts +15 -0
  84. package/src/plugin.ts +3 -0
  85. package/src/serializer.ts +2 -1
@@ -0,0 +1,53 @@
1
+ {
2
+ "AWSTemplateFormatVersion": "2010-09-09",
3
+ "Description": "Fixture: search-service stack with a nested network stack, an ECS task referencing a published image digest, and an EC2 launch template referencing an AMI.",
4
+ "Resources": {
5
+ "network": {
6
+ "Type": "AWS::CloudFormation::Stack",
7
+ "Properties": {
8
+ "TemplateURL": { "Fn::Sub": "${TemplateBasePath}/network.template.json" },
9
+ "Parameters": {
10
+ "TemplateBasePath": { "Ref": "TemplateBasePath" }
11
+ }
12
+ }
13
+ },
14
+ "SearchBucket": {
15
+ "Type": "AWS::S3::Bucket",
16
+ "Properties": {
17
+ "BucketName": "search-service-assets"
18
+ }
19
+ },
20
+ "SearchTaskDefinition": {
21
+ "Type": "AWS::ECS::TaskDefinition",
22
+ "Properties": {
23
+ "Family": "search-service",
24
+ "ContainerDefinitions": [
25
+ {
26
+ "Name": "search",
27
+ "Image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/search-service@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
28
+ }
29
+ ]
30
+ }
31
+ },
32
+ "SearchLaunchTemplate": {
33
+ "Type": "AWS::EC2::LaunchTemplate",
34
+ "Properties": {
35
+ "LaunchTemplateData": {
36
+ "ImageId": "ami-0abcdef1234567890",
37
+ "InstanceType": "m5.large"
38
+ }
39
+ }
40
+ },
41
+ "SearchServiceComponentMetadata": {
42
+ "Type": "Custom::ComponentMetadata",
43
+ "Properties": {
44
+ "LexiconVersion": "0.13.1"
45
+ }
46
+ }
47
+ },
48
+ "Outputs": {
49
+ "BucketName": {
50
+ "Value": { "Ref": "SearchBucket" }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,425 @@
1
+ /**
2
+ * `MockCloudExecutor` — an in-memory fake of `CloudExecutor` (../cloud-executor.ts)
3
+ * for tests. No live AWS, no live docker: every method records its call and
4
+ * returns/derives a canned result, with enough state (a fake stack registry, a
5
+ * fake deployment registry, a fake cluster registry) to exercise realistic
6
+ * scenarios — a changeset that proposes a replacement, a stack that takes a
7
+ * few polls to go terminal, a CodeDeploy deployment that fails then rolls back,
8
+ * a cluster that becomes healthy after enough members join.
9
+ *
10
+ * Every capability factory in `../*.ts` accepts a `CloudExecutor`, so a test
11
+ * builds one `MockCloudExecutor` and passes it to every `create*Capability`
12
+ * under test — never touching the real, `child_process`/`net`-backed
13
+ * executor in `../cloud-executor.ts`.
14
+ */
15
+
16
+ import type {
17
+ CfnChange,
18
+ CfnCreateChangeSetArgs,
19
+ CfnExecuteChangeSetArgs,
20
+ CfnStackStatus,
21
+ CloudExecutor,
22
+ CodeDeployCreateArgs,
23
+ CodeDeployStatus,
24
+ EcsClient,
25
+ EcsRunTaskArgs,
26
+ EcsServiceState,
27
+ EcsUpdateServiceArgs,
28
+ EmrAddStepArgs,
29
+ EmrClient,
30
+ EmrStartJobRunArgs,
31
+ HostClient,
32
+ HostCopyFileArgs,
33
+ HostDockerLoadArgs,
34
+ LambdaClient,
35
+ LambdaUpdateAliasArgs,
36
+ LambdaUpdateCodeArgs,
37
+ } from "../cloud-executor";
38
+ // The docker + neo4j clients are agnostic and owned by core; this mock provides
39
+ // them too (its executor is a superset) so the pilots-e2e suite can drive both
40
+ // core-agnostic capabilities (`docker-build`, `wait-cluster-healthy`) and the
41
+ // aws leaves off one shared mock.
42
+ import type {
43
+ DockerBuildArgs,
44
+ DockerLoadArgs,
45
+ DockerPushArgs,
46
+ DockerSaveArgs,
47
+ DockerTagArgs,
48
+ Neo4jClusterClient,
49
+ Neo4jProbeArgs,
50
+ } from "@intentius/chant/components/verbs/cloud-executor";
51
+
52
+ /** The mock's executor is a superset of the aws `CloudExecutor` plus the agnostic Neo4j client, so it satisfies both aws-leaf and core-agnostic capability factories in the e2e suites. */
53
+ type MockExecutor = CloudExecutor & { neo4j: Neo4jClusterClient };
54
+
55
+ export interface RecordedCall {
56
+ client: string;
57
+ method: string;
58
+ args: unknown;
59
+ }
60
+
61
+ /** Scripted behavior for one fake CloudFormation stack. */
62
+ export interface FakeStackConfig {
63
+ /** Changes CloudFormation would propose for the next changeset created against this stack. Default: no changes (no-op update). */
64
+ changes?: CfnChange[];
65
+ /** Outputs the stack reports once terminal. */
66
+ outputs?: Record<string, string>;
67
+ /** Terminal status reported after `executeChangeSet` (or from the start, if the stack pre-exists). Default: "UPDATE_COMPLETE". */
68
+ terminalStatus?: string;
69
+ /** True if this is a brand-new stack (no prior stack) — affects `isCreate`. Default: false. */
70
+ isCreate?: boolean;
71
+ }
72
+
73
+ export interface FakeDeploymentConfig {
74
+ /** Terminal status CodeDeploy reports for this deployment. Default: "Succeeded". */
75
+ terminalStatus?: "Succeeded" | "Failed" | "Stopped";
76
+ }
77
+
78
+ export interface FakeClusterConfig {
79
+ /** Number of bolt endpoints (out of however many `cluster` lists) that report healthy. Default: all of them. */
80
+ healthyCount?: number;
81
+ }
82
+
83
+ export interface FakeLambdaConfig {
84
+ /** Alias -> version this function's alias currently resolves to, before any deploy in the test runs. */
85
+ aliasVersions?: Record<string, string>;
86
+ /** Force `waitForUpdate` to report a failed code update (simulates a bad image). */
87
+ failUpdate?: boolean;
88
+ }
89
+
90
+ /** Scripted behavior for one fake EMR job run. */
91
+ export interface FakeJobRunConfig {
92
+ /** Terminal state reported once the run "completes". Default: "COMPLETED". */
93
+ terminalState?: "COMPLETED" | "FAILED" | "CANCELLED";
94
+ }
95
+
96
+ export interface MockCloudExecutorOptions {
97
+ stacks?: Record<string, FakeStackConfig>;
98
+ deployments?: Record<string, FakeDeploymentConfig>;
99
+ clusters?: Record<string, FakeClusterConfig>;
100
+ /** ECS service states keyed by `cluster/service`, evolved by `updateService`/`rollbackService` calls. */
101
+ ecsServices?: Record<string, EcsServiceState>;
102
+ /** Force every docker/ecr call to fail (simulates a build/push failure). */
103
+ failDocker?: boolean;
104
+ /** Force every `host` (registry-less `load-image-on-host`) call to fail (simulates an unreachable host). */
105
+ failHost?: boolean;
106
+ /** Lambda functions keyed by function name. */
107
+ lambdas?: Record<string, FakeLambdaConfig>;
108
+ /** Scripted job runs keyed by the run id the test expects (see `MockCloudExecutor.setJobRun` for post-construction control, e.g. before the run id is known). */
109
+ jobRuns?: Record<string, FakeJobRunConfig>;
110
+ /** Object counts `s3.sync` reports (uploaded always; deleted only when the call passes `delete: true`). */
111
+ s3Sync?: { uploaded?: number; deleted?: number };
112
+ /** Canned stdout `host.exec` returns (e.g. a byte count for `copy-to-host`, or command output for `remote-exec`). */
113
+ hostExecStdout?: string;
114
+ /** Result `ecs.waitForTask` reports for a one-off `run-task` (e.g. a migration task). Default: clean exit (`STOPPED`, exit 0). */
115
+ ecsTask?: { lastStatus?: string; exitCode?: number; stoppedReason?: string };
116
+ /** Result `lambda.invoke` returns. Default: `{ statusCode: 200, payload: "" }`. */
117
+ lambdaInvoke?: { statusCode?: number; payload?: string; functionError?: string };
118
+ }
119
+
120
+ /** An injected `CloudExecutor` plus the call log and stack-status controls tests use to script scenarios and assert on I/O. */
121
+ export interface MockCloudExecutor {
122
+ executor: MockExecutor;
123
+ calls: RecordedCall[];
124
+ /** Change a stack's terminal status/outputs/changes after construction (e.g. to simulate a later poll succeeding). */
125
+ setStack(name: string, config: FakeStackConfig): void;
126
+ /** Change a deployment's terminal status after construction. */
127
+ setDeployment(id: string, config: FakeDeploymentConfig): void;
128
+ /** Change how many cluster members report healthy after construction (simulates a follower catching up). */
129
+ setClusterHealth(cluster: string, healthyCount: number): void;
130
+ /** Change a job run's terminal state after construction (e.g. once its runId is known from a prior `startJobRun` call). */
131
+ setJobRun(runId: string, config: FakeJobRunConfig): void;
132
+ }
133
+
134
+ /** Build a fresh mock `CloudExecutor`. Every method is deterministic and synchronous-fast — no real polling delay. */
135
+ export function createMockCloudExecutor(options: MockCloudExecutorOptions = {}): MockCloudExecutor {
136
+ const calls: RecordedCall[] = [];
137
+ const record = (client: string, method: string, args: unknown) => calls.push({ client, method, args });
138
+
139
+ const stacks = new Map<string, FakeStackConfig>(Object.entries(options.stacks ?? {}));
140
+ const deployments = new Map<string, FakeDeploymentConfig>(Object.entries(options.deployments ?? {}));
141
+ const clusters = new Map<string, number>(
142
+ Object.entries(options.clusters ?? {}).map(([k, v]) => [k, v.healthyCount ?? Number.MAX_SAFE_INTEGER]),
143
+ );
144
+ const ecsServices = new Map<string, EcsServiceState>(Object.entries(options.ecsServices ?? {}));
145
+ const pendingChangeSets = new Map<string, { stackName: string; changes: CfnChange[] }>();
146
+ const lambdaConfigs = new Map<string, FakeLambdaConfig>(Object.entries(options.lambdas ?? {}));
147
+ const lambdaAliasVersions = new Map<string, Map<string, string>>(
148
+ Object.entries(options.lambdas ?? {}).map(([fn, cfg]) => [fn, new Map(Object.entries(cfg.aliasVersions ?? {}))]),
149
+ );
150
+ const jobRuns = new Map<string, FakeJobRunConfig>(Object.entries(options.jobRuns ?? {}));
151
+ let changeSetCounter = 0;
152
+ let deploymentCounter = 0;
153
+ let lambdaVersionCounter = 0;
154
+ let jobRunCounter = 0;
155
+ let taskCounter = 0;
156
+ let stepCounter = 0;
157
+
158
+ function stackStatus(name: string): CfnStackStatus {
159
+ const config = stacks.get(name);
160
+ return { stackStatus: config?.terminalStatus ?? "UPDATE_COMPLETE", outputs: config?.outputs ?? {} };
161
+ }
162
+
163
+ const docker: CloudExecutor["docker"] = {
164
+ async build(args: DockerBuildArgs) {
165
+ record("docker", "build", args);
166
+ if (options.failDocker) throw new Error(`docker build failed for ${args.context}`);
167
+ return { digest: `sha256:${fakeDigest("build", args.context, args.tag)}` };
168
+ },
169
+ async save(args: DockerSaveArgs) {
170
+ record("docker", "save", args);
171
+ },
172
+ async load(args: DockerLoadArgs) {
173
+ record("docker", "load", args);
174
+ return { digest: `sha256:${fakeDigest("load", args.inFile)}` };
175
+ },
176
+ async tag(args: DockerTagArgs) {
177
+ record("docker", "tag", args);
178
+ },
179
+ async push(args: DockerPushArgs) {
180
+ record("docker", "push", args);
181
+ if (options.failDocker) throw new Error(`docker push failed for ${args.image}`);
182
+ return { digest: `sha256:${fakeDigest("push", args.image)}` };
183
+ },
184
+ async remoteDigest(image: string) {
185
+ record("docker", "remoteDigest", image);
186
+ return `sha256:${fakeDigest("remote", image)}`;
187
+ },
188
+ };
189
+
190
+ const ecr: CloudExecutor["ecr"] = {
191
+ async login(registry: string) {
192
+ record("ecr", "login", registry);
193
+ },
194
+ };
195
+
196
+ const cloudformation: CloudExecutor["cloudformation"] = {
197
+ async createChangeSet(args: CfnCreateChangeSetArgs) {
198
+ record("cloudformation", "createChangeSet", args);
199
+ changeSetCounter += 1;
200
+ const changeSetName = `mock-changeset-${changeSetCounter}`;
201
+ const config = stacks.get(args.stackName);
202
+ const changes = config?.changes ?? [];
203
+ pendingChangeSets.set(changeSetName, { stackName: args.stackName, changes });
204
+ return {
205
+ changeSetName,
206
+ stackName: args.stackName,
207
+ status: "CREATE_COMPLETE",
208
+ isCreate: config?.isCreate ?? false,
209
+ changes,
210
+ };
211
+ },
212
+ async executeChangeSet(args: CfnExecuteChangeSetArgs) {
213
+ record("cloudformation", "executeChangeSet", args);
214
+ pendingChangeSets.delete(args.changeSetName);
215
+ },
216
+ async deleteChangeSet(args: CfnExecuteChangeSetArgs) {
217
+ record("cloudformation", "deleteChangeSet", args);
218
+ pendingChangeSets.delete(args.changeSetName);
219
+ },
220
+ async describeStack(stackName: string) {
221
+ record("cloudformation", "describeStack", stackName);
222
+ return stackStatus(stackName);
223
+ },
224
+ async waitForStack(stackName: string) {
225
+ record("cloudformation", "waitForStack", stackName);
226
+ return stackStatus(stackName);
227
+ },
228
+ async rollbackStack(stackName: string) {
229
+ record("cloudformation", "rollbackStack", stackName);
230
+ stacks.set(stackName, { ...stacks.get(stackName), terminalStatus: "ROLLBACK_COMPLETE" });
231
+ },
232
+ };
233
+
234
+ const ecs: EcsClient = {
235
+ async updateService(args: EcsUpdateServiceArgs) {
236
+ record("ecs", "updateService", args);
237
+ const key = `${args.cluster}/${args.service}`;
238
+ const desired = args.desiredCount ?? ecsServices.get(key)?.desiredCount ?? 1;
239
+ ecsServices.set(key, { runningCount: desired, desiredCount: desired, stable: true });
240
+ return { deploymentId: `mock-deployment-${key}` };
241
+ },
242
+ async describeService(cluster: string, service: string) {
243
+ record("ecs", "describeService", { cluster, service });
244
+ return ecsServices.get(`${cluster}/${service}`) ?? { runningCount: 1, desiredCount: 1, stable: true };
245
+ },
246
+ async rollbackService(args: EcsUpdateServiceArgs) {
247
+ record("ecs", "rollbackService", args);
248
+ const key = `${args.cluster}/${args.service}`;
249
+ const current = ecsServices.get(key);
250
+ ecsServices.set(key, { runningCount: current?.desiredCount ?? 1, desiredCount: current?.desiredCount ?? 1, stable: true });
251
+ },
252
+ async runTask(args: EcsRunTaskArgs) {
253
+ record("ecs", "runTask", args);
254
+ taskCounter += 1;
255
+ return { taskArn: `arn:aws:ecs:us-east-1:123456789012:task/${args.cluster}/mock-task-${taskCounter}` };
256
+ },
257
+ async waitForTask(cluster: string, taskArn: string) {
258
+ record("ecs", "waitForTask", { cluster, taskArn });
259
+ return {
260
+ lastStatus: options.ecsTask?.lastStatus ?? "STOPPED",
261
+ exitCode: options.ecsTask?.exitCode ?? 0,
262
+ ...(options.ecsTask?.stoppedReason ? { stoppedReason: options.ecsTask.stoppedReason } : {}),
263
+ };
264
+ },
265
+ };
266
+
267
+ const codeDeploy: CloudExecutor["codeDeploy"] = {
268
+ async createDeployment(args: CodeDeployCreateArgs) {
269
+ record("codeDeploy", "createDeployment", args);
270
+ deploymentCounter += 1;
271
+ return { deploymentId: `mock-deployment-${deploymentCounter}` };
272
+ },
273
+ async waitForDeployment(deploymentId: string) {
274
+ record("codeDeploy", "waitForDeployment", deploymentId);
275
+ const config = deployments.get(deploymentId);
276
+ const status = config?.terminalStatus ?? "Succeeded";
277
+ const result: CodeDeployStatus = { status, terminal: true };
278
+ return result;
279
+ },
280
+ async stopAndRollback(deploymentId: string) {
281
+ record("codeDeploy", "stopAndRollback", deploymentId);
282
+ deployments.set(deploymentId, { terminalStatus: "Stopped" });
283
+ },
284
+ };
285
+
286
+ const neo4j: Neo4jClusterClient = {
287
+ async probe(args: Neo4jProbeArgs) {
288
+ record("neo4j", "probe", args);
289
+ const endpoints = args.cluster
290
+ .split(",")
291
+ .map((s) => s.trim())
292
+ .filter(Boolean);
293
+ const healthy = clusters.get(args.cluster) ?? endpoints.length;
294
+ return { healthyCount: Math.min(healthy, endpoints.length), total: endpoints.length };
295
+ },
296
+ };
297
+
298
+ const lambda: LambdaClient = {
299
+ async updateFunctionCode(args: LambdaUpdateCodeArgs) {
300
+ record("lambda", "updateFunctionCode", args);
301
+ return { functionArn: `arn:aws:lambda:us-east-1:123456789012:function:${args.functionName}` };
302
+ },
303
+ async waitForUpdate(functionName: string) {
304
+ record("lambda", "waitForUpdate", functionName);
305
+ const failed = lambdaConfigs.get(functionName)?.failUpdate;
306
+ return { status: failed ? "Failed" : "Successful" };
307
+ },
308
+ async publishVersion(args) {
309
+ record("lambda", "publishVersion", args);
310
+ lambdaVersionCounter += 1;
311
+ return {
312
+ version: String(lambdaVersionCounter),
313
+ functionArn: `arn:aws:lambda:us-east-1:123456789012:function:${args.functionName}:${lambdaVersionCounter}`,
314
+ };
315
+ },
316
+ async updateAlias(args: LambdaUpdateAliasArgs) {
317
+ record("lambda", "updateAlias", args);
318
+ const versions = lambdaAliasVersions.get(args.functionName) ?? new Map<string, string>();
319
+ versions.set(args.alias, args.version);
320
+ lambdaAliasVersions.set(args.functionName, versions);
321
+ return { aliasArn: `arn:aws:lambda:us-east-1:123456789012:function:${args.functionName}:${args.alias}` };
322
+ },
323
+ async getAliasVersion(functionName: string, alias: string) {
324
+ record("lambda", "getAliasVersion", { functionName, alias });
325
+ return lambdaAliasVersions.get(functionName)?.get(alias);
326
+ },
327
+ async invoke(args: { functionName: string; payload?: string }) {
328
+ record("lambda", "invoke", args);
329
+ return {
330
+ statusCode: options.lambdaInvoke?.statusCode ?? 200,
331
+ payload: options.lambdaInvoke?.payload ?? "",
332
+ ...(options.lambdaInvoke?.functionError ? { functionError: options.lambdaInvoke.functionError } : {}),
333
+ };
334
+ },
335
+ };
336
+
337
+ const emr: EmrClient = {
338
+ async startJobRun(args: EmrStartJobRunArgs) {
339
+ record("emr", "startJobRun", args);
340
+ jobRunCounter += 1;
341
+ return { runId: `mock-job-run-${jobRunCounter}` };
342
+ },
343
+ async addStep(args: EmrAddStepArgs) {
344
+ record("emr", "addStep", args);
345
+ stepCounter += 1;
346
+ return { stepId: `s-MOCK${String(stepCounter).padStart(4, "0")}` };
347
+ },
348
+ async describeJobRun(runId: string) {
349
+ record("emr", "describeJobRun", runId);
350
+ return { state: jobRuns.get(runId)?.terminalState ?? "COMPLETED" };
351
+ },
352
+ async waitForJobRun(runId: string) {
353
+ record("emr", "waitForJobRun", runId);
354
+ return { state: jobRuns.get(runId)?.terminalState ?? "COMPLETED" };
355
+ },
356
+ async cancelJobRun(runId: string) {
357
+ record("emr", "cancelJobRun", runId);
358
+ jobRuns.set(runId, { terminalState: "CANCELLED" });
359
+ },
360
+ };
361
+
362
+ const host: HostClient = {
363
+ async copyFile(args: HostCopyFileArgs) {
364
+ record("host", "copyFile", args);
365
+ if (options.failHost) throw new Error(`host copy failed for ${args.host}`);
366
+ },
367
+ async dockerLoad(args: HostDockerLoadArgs) {
368
+ record("host", "dockerLoad", args);
369
+ if (options.failHost) throw new Error(`host docker load failed for ${args.host}`);
370
+ return { digest: `sha256:${fakeDigest("host-load", args.host, args.path)}` };
371
+ },
372
+ async exec(args) {
373
+ record("host", "exec", args);
374
+ if (options.failHost) throw new Error(`host exec failed for ${args.host}`);
375
+ return { stdout: options.hostExecStdout ?? "", exitCode: 0 };
376
+ },
377
+ };
378
+
379
+ const s3: CloudExecutor["s3"] = {
380
+ async sync(args) {
381
+ record("s3", "sync", args);
382
+ return {
383
+ uploaded: options.s3Sync?.uploaded ?? 0,
384
+ deleted: args.delete ? (options.s3Sync?.deleted ?? 0) : 0,
385
+ };
386
+ },
387
+ async cp(args) {
388
+ record("s3", "cp", args);
389
+ },
390
+ };
391
+ const cloudfront: CloudExecutor["cloudfront"] = {
392
+ async createInvalidation(args) {
393
+ record("cloudfront", "createInvalidation", args);
394
+ return { invalidationId: "I-MOCK0001" };
395
+ },
396
+ };
397
+ const snapshot: CloudExecutor["snapshot"] = {
398
+ async create(args) {
399
+ record("snapshot", "create", args);
400
+ return { snapshotId: `snap-mock-${args.resourceKind}-${args.resource}` };
401
+ },
402
+ async restore(args) {
403
+ record("snapshot", "restore", args);
404
+ },
405
+ };
406
+
407
+ return {
408
+ executor: { docker, ecr, cloudformation, ecs, codeDeploy, neo4j, lambda, emr, host, s3, cloudfront, snapshot },
409
+ calls,
410
+ setStack: (name, config) => stacks.set(name, { ...stacks.get(name), ...config }),
411
+ setDeployment: (id, config) => deployments.set(id, { ...deployments.get(id), ...config }),
412
+ setClusterHealth: (cluster, healthyCount) => clusters.set(cluster, healthyCount),
413
+ setJobRun: (runId, config) => jobRuns.set(runId, { ...jobRuns.get(runId), ...config }),
414
+ };
415
+ }
416
+
417
+ /** Deterministic fake digest suffix derived from its inputs — stable across calls with the same args, distinct otherwise. */
418
+ function fakeDigest(...parts: string[]): string {
419
+ let hash = 0;
420
+ const input = parts.join("|");
421
+ for (let i = 0; i < input.length; i++) {
422
+ hash = (hash * 31 + input.charCodeAt(i)) | 0;
423
+ }
424
+ return Math.abs(hash).toString(16).padStart(8, "0").repeat(4).slice(0, 64);
425
+ }