@intentius/chant-lexicon-temporal 0.1.5 → 0.1.8
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/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/src/codegen/docs.ts +1 -0
- package/src/composites/composites.test.ts +103 -1
- package/src/composites/watch-op.ts +101 -0
- package/src/describe-resources.test.ts +328 -0
- package/src/describe-resources.ts +300 -0
- package/src/index.ts +19 -0
- package/src/op/activities/build.ts +23 -0
- package/src/op/activities/gitlab.ts +56 -0
- package/src/op/activities/helm.ts +41 -0
- package/src/op/activities/index.ts +23 -0
- package/src/op/activities/kubectl.ts +32 -0
- package/src/op/activities/shell.ts +25 -0
- package/src/op/activities/state.ts +85 -0
- package/src/op/activities/teardown.ts +21 -0
- package/src/op/activities/wait.ts +52 -0
- package/src/op/op-serializer.test.ts +508 -0
- package/src/op/serializer.ts +349 -0
- package/src/plugin.test.ts +2 -2
- package/src/plugin.ts +7 -0
- package/src/serializer.test.ts +39 -0
- package/src/serializer.ts +10 -8
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Op serializer tests — verifies that Temporal::Op entities generate
|
|
3
|
+
* the correct workflow.ts, activities.ts, and worker.ts files.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { serializeOps } from "./serializer";
|
|
8
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
9
|
+
import type { OpConfig } from "@intentius/chant/op";
|
|
10
|
+
|
|
11
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function makeOp(config: OpConfig): [string, Record<string, unknown>] {
|
|
14
|
+
return [
|
|
15
|
+
config.name,
|
|
16
|
+
{
|
|
17
|
+
[DECLARABLE_MARKER]: true,
|
|
18
|
+
entityType: "Temporal::Op",
|
|
19
|
+
lexicon: "temporal",
|
|
20
|
+
kind: "resource",
|
|
21
|
+
props: config,
|
|
22
|
+
attributes: {},
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Basic generation ──────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe("serializeOps()", () => {
|
|
30
|
+
it("returns empty object for empty map", () => {
|
|
31
|
+
expect(serializeOps(new Map())).toEqual({});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("generates workflow.ts, activities.ts, worker.ts for each Op", () => {
|
|
35
|
+
const ops = new Map([
|
|
36
|
+
makeOp({ name: "alb-deploy", overview: "ALB deploy", phases: [] }),
|
|
37
|
+
]);
|
|
38
|
+
const files = serializeOps(ops);
|
|
39
|
+
expect(files["ops/alb-deploy/workflow.ts"]).toBeDefined();
|
|
40
|
+
expect(files["ops/alb-deploy/activities.ts"]).toBeDefined();
|
|
41
|
+
expect(files["ops/alb-deploy/worker.ts"]).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("generates files for multiple Ops under separate directories", () => {
|
|
45
|
+
const ops = new Map([
|
|
46
|
+
makeOp({ name: "op-a", overview: "A", phases: [] }),
|
|
47
|
+
makeOp({ name: "op-b", overview: "B", phases: [] }),
|
|
48
|
+
]);
|
|
49
|
+
const files = serializeOps(ops);
|
|
50
|
+
expect(files["ops/op-a/workflow.ts"]).toBeDefined();
|
|
51
|
+
expect(files["ops/op-b/workflow.ts"]).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ── workflow.ts ─────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
describe("workflow.ts", () => {
|
|
57
|
+
it("exports a camelCase workflow function named after the Op", () => {
|
|
58
|
+
const ops = new Map([
|
|
59
|
+
makeOp({ name: "alb-deploy", overview: "o", phases: [] }),
|
|
60
|
+
]);
|
|
61
|
+
const wf = serializeOps(ops)["ops/alb-deploy/workflow.ts"];
|
|
62
|
+
expect(wf).toContain("export async function albDeployWorkflow()");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("imports proxyActivities, condition, defineSignal, setHandler from @temporalio/workflow", () => {
|
|
66
|
+
const ops = new Map([makeOp({ name: "my-op", overview: "o", phases: [] })]);
|
|
67
|
+
const wf = serializeOps(ops)["ops/my-op/workflow.ts"];
|
|
68
|
+
expect(wf).toContain("from '@temporalio/workflow'");
|
|
69
|
+
expect(wf).toContain("proxyActivities");
|
|
70
|
+
expect(wf).toContain("condition");
|
|
71
|
+
expect(wf).toContain("defineSignal");
|
|
72
|
+
expect(wf).toContain("setHandler");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("imports TEMPORAL_ACTIVITY_PROFILES from @intentius/chant-lexicon-temporal", () => {
|
|
76
|
+
const ops = new Map([makeOp({ name: "my-op", overview: "o", phases: [] })]);
|
|
77
|
+
const wf = serializeOps(ops)["ops/my-op/workflow.ts"];
|
|
78
|
+
expect(wf).toContain("TEMPORAL_ACTIVITY_PROFILES");
|
|
79
|
+
expect(wf).toContain("@intentius/chant-lexicon-temporal");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("groups activities by profile in proxyActivities calls", () => {
|
|
83
|
+
const ops = new Map([
|
|
84
|
+
makeOp({
|
|
85
|
+
name: "deploy", overview: "o",
|
|
86
|
+
phases: [
|
|
87
|
+
{ name: "Build", steps: [{ kind: "activity", fn: "chantBuild", args: { path: "./a" } }] },
|
|
88
|
+
{ name: "Deploy", steps: [{ kind: "activity", fn: "helmInstall", args: { name: "r", chart: "c" }, profile: "longInfra" }] },
|
|
89
|
+
],
|
|
90
|
+
}),
|
|
91
|
+
]);
|
|
92
|
+
const wf = serializeOps(ops)["ops/deploy/workflow.ts"];
|
|
93
|
+
expect(wf).toContain("TEMPORAL_ACTIVITY_PROFILES.fastIdempotent");
|
|
94
|
+
expect(wf).toContain("TEMPORAL_ACTIVITY_PROFILES.longInfra");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("generates sequential await calls for a non-parallel phase", () => {
|
|
98
|
+
const ops = new Map([
|
|
99
|
+
makeOp({
|
|
100
|
+
name: "seq-op", overview: "o",
|
|
101
|
+
phases: [{
|
|
102
|
+
name: "Deploy",
|
|
103
|
+
steps: [
|
|
104
|
+
{ kind: "activity", fn: "chantBuild", args: { path: "./a" } },
|
|
105
|
+
{ kind: "activity", fn: "kubectlApply", args: { manifest: "out.yaml" }, profile: "longInfra" },
|
|
106
|
+
],
|
|
107
|
+
}],
|
|
108
|
+
}),
|
|
109
|
+
]);
|
|
110
|
+
const wf = serializeOps(ops)["ops/seq-op/workflow.ts"];
|
|
111
|
+
expect(wf).toContain("await chantBuild(");
|
|
112
|
+
expect(wf).toContain("await kubectlApply(");
|
|
113
|
+
expect(wf).not.toContain("Promise.all");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("generates Promise.all for a parallel phase", () => {
|
|
117
|
+
const ops = new Map([
|
|
118
|
+
makeOp({
|
|
119
|
+
name: "par-op", overview: "o",
|
|
120
|
+
phases: [{
|
|
121
|
+
name: "Build",
|
|
122
|
+
parallel: true,
|
|
123
|
+
steps: [
|
|
124
|
+
{ kind: "activity", fn: "chantBuild", args: { path: "./a" } },
|
|
125
|
+
{ kind: "activity", fn: "chantBuild", args: { path: "./b" } },
|
|
126
|
+
],
|
|
127
|
+
}],
|
|
128
|
+
}),
|
|
129
|
+
]);
|
|
130
|
+
const wf = serializeOps(ops)["ops/par-op/workflow.ts"];
|
|
131
|
+
expect(wf).toContain("Promise.all");
|
|
132
|
+
expect(wf).toContain("chantBuild({");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("generates gate: defineSignal, setHandler, condition", () => {
|
|
136
|
+
const ops = new Map([
|
|
137
|
+
makeOp({
|
|
138
|
+
name: "gate-op", overview: "o",
|
|
139
|
+
phases: [{
|
|
140
|
+
name: "Approval",
|
|
141
|
+
steps: [{ kind: "gate", signalName: "gate-dns-delegation", timeout: "48h" }],
|
|
142
|
+
}],
|
|
143
|
+
}),
|
|
144
|
+
]);
|
|
145
|
+
const wf = serializeOps(ops)["ops/gate-op/workflow.ts"];
|
|
146
|
+
expect(wf).toContain("defineSignal");
|
|
147
|
+
expect(wf).toContain('"gate-dns-delegation"');
|
|
148
|
+
expect(wf).toContain("setHandler");
|
|
149
|
+
expect(wf).toContain("condition");
|
|
150
|
+
expect(wf).toContain('"48h"');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("uses 48h as default gate timeout when not specified", () => {
|
|
154
|
+
const ops = new Map([
|
|
155
|
+
makeOp({
|
|
156
|
+
name: "gate-op", overview: "o",
|
|
157
|
+
phases: [{ name: "Wait", steps: [{ kind: "gate", signalName: "my-signal" }] }],
|
|
158
|
+
}),
|
|
159
|
+
]);
|
|
160
|
+
const wf = serializeOps(ops)["ops/gate-op/workflow.ts"];
|
|
161
|
+
expect(wf).toContain('"48h"');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("uses kebab-to-camel for signal handler variable name", () => {
|
|
165
|
+
const ops = new Map([
|
|
166
|
+
makeOp({
|
|
167
|
+
name: "op", overview: "o",
|
|
168
|
+
phases: [{ name: "W", steps: [{ kind: "gate", signalName: "gate-dns-delegation" }] }],
|
|
169
|
+
}),
|
|
170
|
+
]);
|
|
171
|
+
const wf = serializeOps(ops)["ops/op/workflow.ts"];
|
|
172
|
+
// "gate-dns-delegation" → resumeDnsDelegation
|
|
173
|
+
expect(wf).toContain("resumeDnsDelegation");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("passes activity args as JSON object", () => {
|
|
177
|
+
const ops = new Map([
|
|
178
|
+
makeOp({
|
|
179
|
+
name: "op", overview: "o",
|
|
180
|
+
phases: [{ name: "P", steps: [{ kind: "activity", fn: "chantBuild", args: { path: "my/path" } }] }],
|
|
181
|
+
}),
|
|
182
|
+
]);
|
|
183
|
+
const wf = serializeOps(ops)["ops/op/workflow.ts"];
|
|
184
|
+
expect(wf).toContain('"path":"my/path"');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("includes phase comment for each phase", () => {
|
|
188
|
+
const ops = new Map([
|
|
189
|
+
makeOp({
|
|
190
|
+
name: "op", overview: "o",
|
|
191
|
+
phases: [{ name: "Build and Test", steps: [{ kind: "activity", fn: "chantBuild", args: { path: "./" } }] }],
|
|
192
|
+
}),
|
|
193
|
+
]);
|
|
194
|
+
const wf = serializeOps(ops)["ops/op/workflow.ts"];
|
|
195
|
+
expect(wf).toContain("// Phase: Build and Test");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("renders onFailure compensation phases after main phases", () => {
|
|
199
|
+
const ops = new Map([
|
|
200
|
+
makeOp({
|
|
201
|
+
name: "safe-op", overview: "o",
|
|
202
|
+
phases: [
|
|
203
|
+
{ name: "Deploy", steps: [{ kind: "activity", fn: "helmInstall", args: { name: "r", chart: "c" } }] },
|
|
204
|
+
],
|
|
205
|
+
onFailure: [
|
|
206
|
+
{ name: "Rollback", steps: [{ kind: "activity", fn: "helmInstall", args: { name: "r", chart: "c" } }] },
|
|
207
|
+
],
|
|
208
|
+
}),
|
|
209
|
+
]);
|
|
210
|
+
const wf = serializeOps(ops)["ops/safe-op/workflow.ts"];
|
|
211
|
+
expect(wf).toContain("onFailure compensation");
|
|
212
|
+
expect(wf).toContain("// Phase: Rollback");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ── activities.ts ───────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
describe("activities.ts", () => {
|
|
219
|
+
it("re-exports from @intentius/chant-lexicon-temporal/op/activities", () => {
|
|
220
|
+
const ops = new Map([makeOp({ name: "op", overview: "o", phases: [] })]);
|
|
221
|
+
const act = serializeOps(ops)["ops/op/activities.ts"];
|
|
222
|
+
expect(act).toContain("export * from '@intentius/chant-lexicon-temporal/op/activities'");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ── worker.ts ───────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
describe("worker.ts", () => {
|
|
229
|
+
it("imports Worker and NativeConnection from @temporalio/worker", () => {
|
|
230
|
+
const ops = new Map([makeOp({ name: "op", overview: "o", phases: [] })]);
|
|
231
|
+
const w = serializeOps(ops)["ops/op/worker.ts"];
|
|
232
|
+
expect(w).toContain("@temporalio/worker");
|
|
233
|
+
expect(w).toContain("Worker");
|
|
234
|
+
expect(w).toContain("NativeConnection");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("reads chant.config.js (relative import from ops/<name>/)", () => {
|
|
238
|
+
const ops = new Map([makeOp({ name: "op", overview: "o", phases: [] })]);
|
|
239
|
+
const w = serializeOps(ops)["ops/op/worker.ts"];
|
|
240
|
+
expect(w).toContain("chant.config.js");
|
|
241
|
+
expect(w).toContain("../../chant.config.js");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("uses op name as default task queue when taskQueue not specified", () => {
|
|
245
|
+
const ops = new Map([makeOp({ name: "alb-deploy", overview: "o", phases: [] })]);
|
|
246
|
+
const w = serializeOps(ops)["ops/alb-deploy/worker.ts"];
|
|
247
|
+
expect(w).toContain("alb-deploy");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("uses custom taskQueue when specified", () => {
|
|
251
|
+
const ops = new Map([makeOp({ name: "my-op", overview: "o", phases: [], taskQueue: "custom-q" })]);
|
|
252
|
+
const w = serializeOps(ops)["ops/my-op/worker.ts"];
|
|
253
|
+
expect(w).toContain("custom-q");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("references workflow.js (compiled JS) not workflow.ts", () => {
|
|
257
|
+
const ops = new Map([makeOp({ name: "op", overview: "o", phases: [] })]);
|
|
258
|
+
const w = serializeOps(ops)["ops/op/worker.ts"];
|
|
259
|
+
expect(w).toContain("./workflow.js");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("imports activities from ./activities.js", () => {
|
|
263
|
+
const ops = new Map([makeOp({ name: "op", overview: "o", phases: [] })]);
|
|
264
|
+
const w = serializeOps(ops)["ops/op/worker.ts"];
|
|
265
|
+
expect(w).toContain("./activities.js");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("resolves TLS and apiKey from profile", () => {
|
|
269
|
+
const ops = new Map([makeOp({ name: "op", overview: "o", phases: [] })]);
|
|
270
|
+
const w = serializeOps(ops)["ops/op/worker.ts"];
|
|
271
|
+
expect(w).toContain("profile.tls");
|
|
272
|
+
expect(w).toContain("apiKey");
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ── depends validation ──────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
describe("depends validation", () => {
|
|
279
|
+
it("accepts depends on known Op names", () => {
|
|
280
|
+
const ops = new Map([
|
|
281
|
+
makeOp({ name: "first", overview: "o", phases: [] }),
|
|
282
|
+
makeOp({ name: "second", overview: "o", phases: [], depends: ["first"] }),
|
|
283
|
+
]);
|
|
284
|
+
expect(() => serializeOps(ops)).not.toThrow();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("throws when depends references an unknown Op name", () => {
|
|
288
|
+
const ops = new Map([
|
|
289
|
+
makeOp({ name: "op", overview: "o", phases: [], depends: ["nonexistent-op"] }),
|
|
290
|
+
]);
|
|
291
|
+
expect(() => serializeOps(ops)).toThrow(/nonexistent-op/);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── Auto-emit upsertSearchAttributes (#28) ──────────────────────────────────
|
|
296
|
+
|
|
297
|
+
describe("upsertSearchAttributes auto-emit", () => {
|
|
298
|
+
function countOccurrences(haystack: string, needle: string): number {
|
|
299
|
+
return haystack.split(needle).length - 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
it("imports upsertSearchAttributes from @temporalio/workflow", () => {
|
|
303
|
+
const ops = new Map([
|
|
304
|
+
makeOp({ name: "op", overview: "o", phases: [{ name: "Phase1", steps: [] }] }),
|
|
305
|
+
]);
|
|
306
|
+
const wf = serializeOps(ops)["ops/op/workflow.ts"];
|
|
307
|
+
expect(wf).toMatch(
|
|
308
|
+
/import \{[^}]*\bupsertSearchAttributes\b[^}]*\} from '@temporalio\/workflow'/,
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("3-phase Op with no searchAttributes emits exactly 4 upsert calls", () => {
|
|
313
|
+
const ops = new Map([
|
|
314
|
+
makeOp({
|
|
315
|
+
name: "deploy",
|
|
316
|
+
overview: "o",
|
|
317
|
+
phases: [
|
|
318
|
+
{ name: "init", steps: [] },
|
|
319
|
+
{ name: "apply", steps: [] },
|
|
320
|
+
{ name: "verify", steps: [] },
|
|
321
|
+
],
|
|
322
|
+
}),
|
|
323
|
+
]);
|
|
324
|
+
const wf = serializeOps(ops)["ops/deploy/workflow.ts"];
|
|
325
|
+
|
|
326
|
+
expect(countOccurrences(wf, "upsertSearchAttributes(")).toBe(4);
|
|
327
|
+
// Initial call: OpName only, no Phase yet
|
|
328
|
+
expect(wf).toContain('upsertSearchAttributes({"OpName":["deploy"]});');
|
|
329
|
+
// Phase upserts at each phase boundary
|
|
330
|
+
expect(wf).toContain('upsertSearchAttributes({ Phase: ["init"] });');
|
|
331
|
+
expect(wf).toContain('upsertSearchAttributes({ Phase: ["apply"] });');
|
|
332
|
+
expect(wf).toContain('upsertSearchAttributes({ Phase: ["verify"] });');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("merges user-provided searchAttributes into the initial call (each value as a 1-element array)", () => {
|
|
336
|
+
const ops = new Map([
|
|
337
|
+
makeOp({
|
|
338
|
+
name: "deploy",
|
|
339
|
+
overview: "o",
|
|
340
|
+
phases: [{ name: "Build", steps: [] }],
|
|
341
|
+
searchAttributes: { Region: "us-east-1", Environment: "prod" },
|
|
342
|
+
}),
|
|
343
|
+
]);
|
|
344
|
+
const wf = serializeOps(ops)["ops/deploy/workflow.ts"];
|
|
345
|
+
|
|
346
|
+
expect(wf).toContain(
|
|
347
|
+
'upsertSearchAttributes({"OpName":["deploy"],"Region":["us-east-1"],"Environment":["prod"]});',
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("single-phase Op produces exactly 2 upsert calls", () => {
|
|
352
|
+
const ops = new Map([
|
|
353
|
+
makeOp({
|
|
354
|
+
name: "tiny",
|
|
355
|
+
overview: "o",
|
|
356
|
+
phases: [{ name: "Only", steps: [] }],
|
|
357
|
+
}),
|
|
358
|
+
]);
|
|
359
|
+
const wf = serializeOps(ops)["ops/tiny/workflow.ts"];
|
|
360
|
+
|
|
361
|
+
expect(countOccurrences(wf, "upsertSearchAttributes(")).toBe(2);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("emits Phase upserts inside onFailure compensation phases", () => {
|
|
365
|
+
const ops = new Map([
|
|
366
|
+
makeOp({
|
|
367
|
+
name: "deploy",
|
|
368
|
+
overview: "o",
|
|
369
|
+
phases: [{ name: "Apply", steps: [] }],
|
|
370
|
+
onFailure: [
|
|
371
|
+
{ name: "Rollback", steps: [] },
|
|
372
|
+
{ name: "Notify", steps: [] },
|
|
373
|
+
],
|
|
374
|
+
}),
|
|
375
|
+
]);
|
|
376
|
+
const wf = serializeOps(ops)["ops/deploy/workflow.ts"];
|
|
377
|
+
|
|
378
|
+
// 1 initial + 1 Apply + 2 onFailure phases = 4
|
|
379
|
+
expect(countOccurrences(wf, "upsertSearchAttributes(")).toBe(4);
|
|
380
|
+
expect(wf).toContain('upsertSearchAttributes({ Phase: ["Rollback"] });');
|
|
381
|
+
expect(wf).toContain('upsertSearchAttributes({ Phase: ["Notify"] });');
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ── outcomeAttribute (#41) ──────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
describe("outcomeAttribute auto-emit", () => {
|
|
388
|
+
it("captures activity result and emits an upsert with from-path", () => {
|
|
389
|
+
const ops = new Map([
|
|
390
|
+
makeOp({
|
|
391
|
+
name: "watch",
|
|
392
|
+
overview: "watch",
|
|
393
|
+
phases: [
|
|
394
|
+
{
|
|
395
|
+
name: "Diff",
|
|
396
|
+
steps: [
|
|
397
|
+
{ kind: "activity", fn: "stateDiff", args: { env: "prod" }, outcomeAttribute: { name: "Drift", from: "drifted" } },
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
}),
|
|
402
|
+
]);
|
|
403
|
+
const wf = serializeOps(ops)["ops/watch/workflow.ts"];
|
|
404
|
+
expect(wf).toContain('const __r0 = await stateDiff({"env":"prod"});');
|
|
405
|
+
expect(wf).toContain('upsertSearchAttributes({ "Drift": [String(__r0?.drifted)] });');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("stringifies whole result when from is omitted", () => {
|
|
409
|
+
const ops = new Map([
|
|
410
|
+
makeOp({
|
|
411
|
+
name: "watch",
|
|
412
|
+
overview: "watch",
|
|
413
|
+
phases: [
|
|
414
|
+
{
|
|
415
|
+
name: "Check",
|
|
416
|
+
steps: [
|
|
417
|
+
{ kind: "activity", fn: "checkSomething", outcomeAttribute: { name: "Status" } },
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
}),
|
|
422
|
+
]);
|
|
423
|
+
const wf = serializeOps(ops)["ops/watch/workflow.ts"];
|
|
424
|
+
expect(wf).toContain('upsertSearchAttributes({ "Status": [String(__r0)] });');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("nested from-path uses optional-chain stringify", () => {
|
|
428
|
+
const ops = new Map([
|
|
429
|
+
makeOp({
|
|
430
|
+
name: "watch",
|
|
431
|
+
overview: "watch",
|
|
432
|
+
phases: [
|
|
433
|
+
{
|
|
434
|
+
name: "Inspect",
|
|
435
|
+
steps: [
|
|
436
|
+
{ kind: "activity", fn: "deepInspect", outcomeAttribute: { name: "Ok", from: "result.healthy" } },
|
|
437
|
+
],
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
}),
|
|
441
|
+
]);
|
|
442
|
+
const wf = serializeOps(ops)["ops/watch/workflow.ts"];
|
|
443
|
+
expect(wf).toContain('upsertSearchAttributes({ "Ok": [String(__r0?.result?.healthy)] });');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("counter is workflow-scoped: multiple outcome attrs use __r0, __r1, ...", () => {
|
|
447
|
+
const ops = new Map([
|
|
448
|
+
makeOp({
|
|
449
|
+
name: "watch",
|
|
450
|
+
overview: "watch",
|
|
451
|
+
phases: [
|
|
452
|
+
{ name: "A", steps: [{ kind: "activity", fn: "first", outcomeAttribute: { name: "FirstResult" } }] },
|
|
453
|
+
{ name: "B", steps: [{ kind: "activity", fn: "second", outcomeAttribute: { name: "SecondResult" } }] },
|
|
454
|
+
],
|
|
455
|
+
}),
|
|
456
|
+
]);
|
|
457
|
+
const wf = serializeOps(ops)["ops/watch/workflow.ts"];
|
|
458
|
+
expect(wf).toContain("const __r0 = await first(");
|
|
459
|
+
expect(wf).toContain("const __r1 = await second(");
|
|
460
|
+
expect(wf).toContain('upsertSearchAttributes({ "FirstResult": [String(__r0)] });');
|
|
461
|
+
expect(wf).toContain('upsertSearchAttributes({ "SecondResult": [String(__r1)] });');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("activities without outcomeAttribute keep the bare-await form", () => {
|
|
465
|
+
const ops = new Map([
|
|
466
|
+
makeOp({
|
|
467
|
+
name: "mixed",
|
|
468
|
+
overview: "mixed",
|
|
469
|
+
phases: [
|
|
470
|
+
{
|
|
471
|
+
name: "Mix",
|
|
472
|
+
steps: [
|
|
473
|
+
{ kind: "activity", fn: "noOutcome" },
|
|
474
|
+
{ kind: "activity", fn: "withOutcome", outcomeAttribute: { name: "Result" } },
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
}),
|
|
479
|
+
]);
|
|
480
|
+
const wf = serializeOps(ops)["ops/mixed/workflow.ts"];
|
|
481
|
+
expect(wf).toContain("await noOutcome({});");
|
|
482
|
+
expect(wf).toContain("const __r0 = await withOutcome({});");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("parallel phase with outcome attrs destructures Promise.all results", () => {
|
|
486
|
+
const ops = new Map([
|
|
487
|
+
makeOp({
|
|
488
|
+
name: "fan",
|
|
489
|
+
overview: "fan",
|
|
490
|
+
phases: [
|
|
491
|
+
{
|
|
492
|
+
name: "Parallel",
|
|
493
|
+
parallel: true,
|
|
494
|
+
steps: [
|
|
495
|
+
{ kind: "activity", fn: "alpha", outcomeAttribute: { name: "AlphaOk", from: "ok" } },
|
|
496
|
+
{ kind: "activity", fn: "beta", outcomeAttribute: { name: "BetaOk", from: "ok" } },
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
}),
|
|
501
|
+
]);
|
|
502
|
+
const wf = serializeOps(ops)["ops/fan/workflow.ts"];
|
|
503
|
+
expect(wf).toContain("const [__r0, __r1] = await Promise.all([");
|
|
504
|
+
expect(wf).toContain('upsertSearchAttributes({ "AlphaOk": [String(__r0?.ok)] });');
|
|
505
|
+
expect(wf).toContain('upsertSearchAttributes({ "BetaOk": [String(__r1?.ok)] });');
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
});
|