@t4h.framework/core 0.4.0 → 0.6.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.
@@ -0,0 +1,403 @@
1
+ ---
2
+ name: framework-core
3
+ description: >-
4
+ Author and test T4H Framework workflows, SyncActivity/AsyncActivity classes,
5
+ Claim dependencies, App manifests, env access, and in-memory workflow runs.
6
+ Use when importing @t4h.framework/core, building automation apps, orchestrating
7
+ with History.reconciler, wiring Claim providers, testing with
8
+ @t4h.framework/core/testing (WorkflowTesting, mockAsyncActivity), nested
9
+ startWorkflow, EnvDesign, Authorization manifests, or Internals.runWorkflow /
10
+ Internals.runActivity.
11
+ ---
12
+
13
+ # @t4h.framework/core
14
+
15
+ Core primitives for **Workflows** (orchestration), **Activities** (work units), and **Claims** (runtime dependencies). The runtime supplies `env`, `claims`, and `history`; this package defines the author API and in-memory test harness.
16
+
17
+ | Import | Purpose |
18
+ |--------|---------|
19
+ | `@t4h.framework/core` | Workflows, activities, claims, app, env, internals |
20
+ | `@t4h.framework/core/testing` | `WorkflowTesting` — tests/local only |
21
+
22
+ Node `>=22`. Schemas use TypeBox (`typebox`). See [README.md](../../../README.md) for diagrams.
23
+
24
+ ---
25
+
26
+ ## Concepts
27
+
28
+ - **Workflow** — orchestration; composes activities via `History.reconciler` (or helpers like `startWorkflow`).
29
+ - **Activity** — `SyncActivity` (`toInput` → `run` → `toOutput`) or `AsyncActivity` (`toInput` → `bootstrap` → `advance` → `toOutput`, optional `onTimeout`).
30
+ - **Claim** — abstract class; activity maps keys → constructors; runtime provides `{ provide, value }`.
31
+ - **History** — replay: existing history entries skip execution; missing entries run activity and record result (`PushActivity`).
32
+
33
+ Async helpers on `AsyncActivity`: `this.pending()`, `this.done()`, `this.rejected()`.
34
+
35
+ ---
36
+
37
+ ## Workflows
38
+
39
+ ```typescript
40
+ import Type from 'typebox'
41
+ import { Workflow, env } from '@t4h.framework/core'
42
+
43
+ const bare = new Workflow(async () => ({ ok: true }))
44
+
45
+ const withMeta = new Workflow(
46
+ { id: 'cashback/v1', description: 'Process cashback' },
47
+ async input => ({ value: input.amount * 0.05 }),
48
+ )
49
+
50
+ const schema = Type.Object({ userId: Type.String(), amount: Type.Number() })
51
+ const validated = new Workflow({ id: 'cashback/v1', schema }, async input => ({
52
+ value: input.amount * 0.05,
53
+ }))
54
+ ```
55
+
56
+ - `new Workflow(fn)` or `new Workflow(options, fn)` — bad overload throws `Invalid constructor arguments`.
57
+ - `WorkflowInput<T>` — from schema; `undefined` without schema.
58
+ - `workflow.validate` — Ajv when `schema` set; else `null`.
59
+ - Optional `authorization` on workflow/app → `toManifest()`.
60
+
61
+ ---
62
+
63
+ ## Invoking activities
64
+
65
+ ```typescript
66
+ import { History } from '@t4h.framework/core'
67
+
68
+ const syncResult = await History.reconciler(MySyncActivity, arg1)
69
+
70
+ const handle = await History.reconciler(WaitAsyncActivity, 'order-1')
71
+ const out = await handle.wait()
72
+ ```
73
+
74
+ - No history: `toInput` then `PushActivity` → runtime runs `run`/`bootstrap`.
75
+ - Replay: uses stored output; async returns `AsyncActivityAwaitable`; `wait()` throws `PushWaitAsyncActivity` if still `pending`.
76
+ - Must run inside `History.run` (workflow runtime / `WorkflowTesting` provide this).
77
+
78
+ ---
79
+
80
+ ## SyncActivity
81
+
82
+ ```typescript
83
+ import { SyncActivity } from '@t4h.framework/core'
84
+
85
+ class CalculateCashbackActivity extends SyncActivity<
86
+ [transaction: { amount: number }], // TArgs
87
+ { amount: number }, // TInput (Serializable)
88
+ number, // TSerializableOutput
89
+ number // TOutput
90
+ > {
91
+ public async toInput(transaction: { amount: number }) {
92
+ return { amount: transaction.amount }
93
+ }
94
+ public run(input: { amount: number }) {
95
+ return Math.floor(input.amount * 0.05)
96
+ }
97
+ public toOutput(output: number) {
98
+ return output
99
+ }
100
+ }
101
+ ```
102
+
103
+ `toOutput(output, ...args)` receives original designer args.
104
+
105
+ ---
106
+
107
+ ## AsyncActivity
108
+
109
+ ```typescript
110
+ import Type from 'typebox'
111
+ import { AsyncActivity, AsyncActivityResultPending } from '@t4h.framework/core'
112
+
113
+ const ApprovalSchema = Type.Object({ approved: Type.Boolean() })
114
+
115
+ class WaitForApprovalActivity extends AsyncActivity<
116
+ [orderId: string],
117
+ { orderId: string },
118
+ { approved: boolean },
119
+ { approved: boolean },
120
+ { requestId: string; orderId: string },
121
+ typeof ApprovalSchema
122
+ > {
123
+ public async toInput(orderId: string) {
124
+ return { orderId }
125
+ }
126
+ public async bootstrap(input: { orderId: string }) {
127
+ return this.pending({
128
+ payload: { requestId: 'req-123', orderId: input.orderId },
129
+ schema: ApprovalSchema,
130
+ timeoutAt: new Date(Date.now() + 3_600_000),
131
+ })
132
+ }
133
+ public async advance(
134
+ payload: Type.Static<typeof ApprovalSchema>,
135
+ bootstrap: AsyncActivityResultPending<{ requestId: string; orderId: string }>,
136
+ ) {
137
+ return payload.approved
138
+ ? this.done({ approved: true })
139
+ : this.rejected({ approved: false, requestId: bootstrap.payload?.requestId })
140
+ }
141
+ public toOutput(output: { approved: boolean }) {
142
+ return output
143
+ }
144
+ }
145
+ ```
146
+
147
+ Results: `AsyncActivityResultPending` | `AsyncActivityResultFulfilled` | `AsyncActivityResultRejected`. Default `onTimeout` → `rejected({ reason: 'timeout' })`.
148
+
149
+ ---
150
+
151
+ ## Claims
152
+
153
+ ```typescript
154
+ import { SyncActivity, Claim } from '@t4h.framework/core'
155
+
156
+ abstract class UserRepositoryClaim {
157
+ abstract findById(id: string): Promise<unknown>
158
+ }
159
+
160
+ class FetchUserActivity extends SyncActivity<[id: string], { id: string }, unknown, unknown> {
161
+ private readonly claims = new Claim({ repo: UserRepositoryClaim })
162
+
163
+ public async toInput(id: string) {
164
+ return { id }
165
+ }
166
+ public async run(input: { id: string }) {
167
+ return await this.claims.repo.findById(input.id)
168
+ }
169
+ public toOutput(output: unknown) {
170
+ return output
171
+ }
172
+ }
173
+ ```
174
+
175
+ Provider: `{ provide: UserRepositoryClaim, value: impl }`. Built-ins: `GetWorkflowUniqueIdentifierClaim`, `GetActivityUniqueIdentifierClaim`, `WorkflowClaim` (for `startWorkflow`).
176
+
177
+ Exceptions: `ClaimKeyNotFoundException`, `ClaimConstructorNotFoundException`, `ClaimStorageNotActiveException`, `ClaimProviderNotFoundException`. Access `claims.*` only inside `Claim.run`.
178
+
179
+ ---
180
+
181
+ ## Nested workflows
182
+
183
+ ```typescript
184
+ import { startWorkflow, WorkflowClaim } from '@t4h.framework/core'
185
+
186
+ const ref = await startWorkflow(childWorkflow, { ...input, parallel?: boolean })
187
+ // WorkflowRef: status 'fulfilled' | 'rejected', output Serializable
188
+ ```
189
+
190
+ `StartWorkflowActivity`: `parallel: true` → immediate `done`; else `pending` until `advance`. Requires `WorkflowClaim` provider.
191
+
192
+ ---
193
+
194
+ ## App
195
+
196
+ ```typescript
197
+ import { App, Workflow } from '@t4h.framework/core'
198
+
199
+ const app = new App({
200
+ id: 'my-automation',
201
+ description: 'Cashback automation',
202
+ workflows: [myWorkflow],
203
+ })
204
+ app.addWorkflow(anotherWorkflow) // throws on duplicate workflow.id
205
+ ```
206
+
207
+ `toManifest()` includes workflows + registered `EnvDesign` entries.
208
+
209
+ ---
210
+
211
+ ## Environment
212
+
213
+ ```typescript
214
+ import { env, EnvDesign } from '@t4h.framework/core'
215
+
216
+ // Under Env.run — string values
217
+ console.log(env.API_URL)
218
+
219
+ // Typed parsing (registers for app manifest)
220
+ const config = EnvDesign({
221
+ API_URL: { type: String },
222
+ RETRIES: { type: Number, defaultValue: '3' },
223
+ DEBUG: { type: Boolean, isOptional: true },
224
+ })
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Serializable & Authorization
230
+
231
+ **Serializable:** primitives, `null`, `undefined`, `Binary`, `SerializableClass`, arrays/objects thereof. `Binary`: `contentType`, `contentLength`, `stream()`.
232
+
233
+ **Authorization:** subclass with `toManifest(): AuthorizationManifest` (`pathToModule`, optional `context`); attach to `Workflow` or `App`.
234
+
235
+ ---
236
+
237
+ ## Internals (runtime / unit tests)
238
+
239
+ ```typescript
240
+ import { Internals } from '@t4h.framework/core'
241
+
242
+ Internals.runWorkflow(workflow, {
243
+ input,
244
+ env: { API_URL: 'https://api.example.com' },
245
+ claims: [{ provide: UserRepositoryClaim, value: impl }],
246
+ history: [],
247
+ logs: [],
248
+ })
249
+
250
+ Internals.runActivity(FetchUserActivity, { env, claims }, a => a.run({ id: '123' }))
251
+ ```
252
+
253
+ Stack: `Env.run` → `Claim.run` → `History.run` → `Logger.run` (workflows). **Test `advance` here** — not via `WorkflowTesting`.
254
+
255
+ ---
256
+
257
+ ## Testing (`@t4h.framework/core/testing`)
258
+
259
+ Test-only entrypoint — **not** part of the production runtime contract. Import from the subpath export:
260
+
261
+ ```typescript
262
+ import { WorkflowTesting } from '@t4h.framework/core/testing'
263
+ ```
264
+
265
+ `package.json` maps `"./testing"` → `dist/testing/index.js`. The public API is **`WorkflowTesting.run`** only.
266
+
267
+ ### `WorkflowTesting.run`
268
+
269
+ ```typescript
270
+ const output = await WorkflowTesting.run(workflow, input?, options?)
271
+ ```
272
+
273
+ | Option | Type | Default | Purpose |
274
+ |--------|------|---------|---------|
275
+ | `claims` | `readonly ClaimProvider[]` | `[]` | Concrete claim implementations for the run |
276
+ | `env` | `Env.Entries` | `{}` | String env vars (`Env.run`) |
277
+ | `mockAsyncActivity` | function \| outcome array | — | Required when any async activity returns `pending` |
278
+ | `logs` | `Log[]` | internal | Capture `console.log` / `console.error` once per run |
279
+ | `maxTicks` | `number` | `10_000` | Max activity-resolution loops before error |
280
+
281
+ Typed input follows `WorkflowInput<T>` when the workflow has a `schema`; omit `input` for workflows without one.
282
+
283
+ ### How the driver works
284
+
285
+ 1. Calls `Internals.runWorkflow` with an empty `history`.
286
+ 2. On **`PushActivity`**, runs the pushed activity in-process:
287
+ - **Sync:** `Internals.runActivity` → `run(input)` → append `{ type: 'sync', output }` to history.
288
+ - **Async:** real `bootstrap`; if already `done`/`rejected`, record and continue; if **`pending`**, apply `mockAsyncActivity` (throws if missing).
289
+ 3. Re-runs the workflow with accumulated history until it returns normally or `maxTicks` is exceeded.
290
+
291
+ This drives sync activities end-to-end and async activities through bootstrap + mocked resolution — **not** through real `advance`.
292
+
293
+ ### Wiring claims in tests
294
+
295
+ Pass explicit `{ provide, value }` providers — same shape as `Internals.runWorkflow`:
296
+
297
+ ```typescript
298
+ import { WorkflowTesting } from '@t4h.framework/core/testing'
299
+ import { History, type Log } from '@t4h.framework/core'
300
+
301
+ const logs: Log[] = []
302
+ const output = await WorkflowTesting.run(workflow, input, {
303
+ env: { API_URL: 'https://example.com' },
304
+ claims: [{ provide: UserRepositoryClaim, value: mockRepo }],
305
+ mockAsyncActivity: () => ({ status: 'fulfilled', output: true }),
306
+ logs,
307
+ })
308
+ ```
309
+
310
+ For a single activity, prefer `Claim.run([...], fn)` or `Internals.runActivity`. Use `WorkflowTesting.run` for full workflow integration tests.
311
+
312
+ ### `mockAsyncActivity`
313
+
314
+ Required when `bootstrap` returns `pending`:
315
+
316
+ | `status` | Effect |
317
+ |----------|--------|
318
+ | `fulfilled` | `{ output }` recorded as async fulfilled |
319
+ | `rejected` | `{ reason }` recorded as async rejected |
320
+ | `timeout` | runs the activity's real `onTimeout(pending)` (throws if undefined) |
321
+
322
+ **Array** — FIFO, one entry per pending async:
323
+
324
+ ```typescript
325
+ mockAsyncActivity: [
326
+ { status: 'fulfilled', output: true },
327
+ { status: 'rejected', reason: false },
328
+ ]
329
+ ```
330
+
331
+ **Function** — `(activity, { input, pending, index }) => outcome`:
332
+
333
+ ```typescript
334
+ mockAsyncActivity: (activity, { index }) => {
335
+ if (activity instanceof WaitForApprovalActivity)
336
+ return { status: 'fulfilled', output: true }
337
+ if (index === 1) return { status: 'timeout' }
338
+ return { status: 'rejected', reason: 'denied' }
339
+ }
340
+ ```
341
+
342
+ Bootstrap that already returns `done`/`rejected` needs no mock. Missing mock on pending throws: `pending and no resolveAsyncActivity`. Exhausted array throws: `No more async outcomes`.
343
+
344
+ ### Examples (from `WorkflowTesting.spec.ts`)
345
+
346
+ **Sync workflow:**
347
+
348
+ ```typescript
349
+ const workflow = new Workflow({ schema }, async input => {
350
+ const n = await History.reconciler(AddOneActivity, input.value)
351
+ return n * 10
352
+ })
353
+ await expect(WorkflowTesting.run(workflow, { value: 4 })).resolves.toBe(50)
354
+ ```
355
+
356
+ **Capture logs:**
357
+
358
+ ```typescript
359
+ const logs: Log[] = []
360
+ await WorkflowTesting.run(workflow, undefined, { logs })
361
+ expect(logs.map(entry => entry.args[0])).toEqual(['before activity', 'after activity'])
362
+ ```
363
+
364
+ **Async fulfilled / rejected / timeout:**
365
+
366
+ ```typescript
367
+ await WorkflowTesting.run(workflow, undefined, {
368
+ mockAsyncActivity: () => ({ status: 'fulfilled', output: true }),
369
+ })
370
+ await WorkflowTesting.run(workflow, undefined, {
371
+ mockAsyncActivity: () => ({ status: 'rejected', reason: false }),
372
+ })
373
+ await WorkflowTesting.run(workflow, undefined, {
374
+ mockAsyncActivity: () => ({ status: 'timeout' }),
375
+ })
376
+ ```
377
+
378
+ **Vitest setup** — add `@t4h.framework/core` and `@t4h.framework/core/testing` as devDependencies; run with Vitest (`vitest run`). See `packages/core/src/testing/__tests__/WorkflowTesting.spec.ts`.
379
+
380
+ ### Limits (use `Internals` instead)
381
+
382
+ - No real **`advance`** — test `advance` via `Internals.runActivity` on the async class.
383
+ - No **`PushWaitAsyncActivity`** / resuming pending `handle.wait()` across ticks.
384
+ - Do not import `@t4h.framework/core/testing` in production app code.
385
+
386
+ ---
387
+
388
+ ## Anti-patterns
389
+
390
+ - Claims or `env` outside `Claim.run` / `Env.run`.
391
+ - `History.reconciler` outside `History.run`.
392
+ - Pending async tests without `mockAsyncActivity`.
393
+ - `advance` coverage only via `WorkflowTesting` (use `Internals.runActivity`).
394
+ - Non-`Serializable` activity I/O.
395
+ - Duplicate `workflow.id` in `App`.
396
+ - `@t4h.framework/core/testing` in production.
397
+ - APIs not exported from `core.ts` / `testing/index.ts`.
398
+
399
+ ---
400
+
401
+ ## Exports (main)
402
+
403
+ `Workflow`, `App`, `SyncActivity`, `AsyncActivity`, `Claim`, `History`, `env`, `EnvDesign`, `Authorization`, `WorkflowRef`, `Binary`, `Serializable`, async result types, `startWorkflow`, `WorkflowClaim`, `Internals`, `isActivity`, `Logger`, `Activity` types, exceptions. **Testing:** `WorkflowTesting` only.
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: framework-workflow-creation
3
+ description: >-
4
+ Design and implement T4H Framework workflows with @t4h.framework/core using
5
+ the project's standard structure for workflow files, app registration, and
6
+ environment handling. Use this whenever the user asks to create a new
7
+ workflow, wire it into an App, define orchestration with History.reconciler,
8
+ or refactor workflow code to remove hardcoded URLs/secrets/client values and
9
+ move configuration to env constants.
10
+ ---
11
+
12
+ # Create workflows with `@t4h.framework/core`
13
+
14
+ Use this skill to build production-ready workflow modules that are easy to register, test, and maintain.
15
+
16
+ ## Primary goal
17
+
18
+ Create workflows that:
19
+
20
+ - expose a `Workflow` constant with explicit metadata (`id`, optional `description`, optional `schema`);
21
+ - orchestrate work through framework primitives (`History.reconciler`, `startWorkflow`) instead of ad-hoc infrastructure code;
22
+ - register cleanly in `App` with `export default new App({...})`;
23
+ - never hardcode URLs, secrets, tokens, client IDs, or tenants in workflow files.
24
+
25
+ ## Expected project shape
26
+
27
+ ### Workflow module
28
+
29
+ ```ts
30
+ import { Workflow } from '@t4h.framework/core'
31
+
32
+ export const myWorkflow = new Workflow({ id: 'my-workflow' }, async input => {
33
+ // orchestration only
34
+ })
35
+ ```
36
+
37
+ ### App declaration
38
+
39
+ ```ts
40
+ import { App } from '@t4h.framework/core'
41
+ import { myWorkflow } from './workflows/my-workflow.js'
42
+
43
+ export default new App({
44
+ id: 'my-app',
45
+ description: 'My app',
46
+ workflows: [myWorkflow],
47
+ })
48
+ ```
49
+
50
+ ### Environment constants module
51
+
52
+ ```ts
53
+ import { env } from '@t4h.framework/core'
54
+
55
+ export const SERVICE_URL = env.SERVICE_URL!
56
+ if (!SERVICE_URL) throw new TypeError('SERVICE_URL is not defined')
57
+ ```
58
+
59
+ Keep this in a dedicated constants file and import values into workflows/activities.
60
+
61
+ ## Core APIs to prefer while authoring workflows
62
+
63
+ - `Workflow` for orchestration entrypoints.
64
+ - `History.reconciler(ActivityClass, ...args)` to execute activities deterministically.
65
+ - `startWorkflow(childWorkflow, { ...input, parallel? })` for nested workflow orchestration.
66
+ - `App` to register workflow modules for runtime discovery.
67
+ - `env` / `EnvDesign` for configuration contracts (not inline literal secrets).
68
+
69
+ ## Authoring rules
70
+
71
+ 1. Keep workflows orchestration-focused. Put low-level operations in activities and claims.
72
+ 2. Define workflow metadata with stable `id`.
73
+ 3. Use schema when input validation is needed.
74
+ 4. Import configuration from environment constants.
75
+ 5. Keep side effects explicit and easy to assert in tests.
76
+
77
+ ## Hard constraints
78
+
79
+ - Do not hardcode URLs, client IDs, client secrets, API keys, tenants, or tokens.
80
+ - Do not initialize third-party clients with inline credentials inside workflow files.
81
+ - Do not read `process.env` directly in workflow modules when the project uses centralized constants via `env`.
82
+
83
+ If a required env key is missing, fail fast with a clear `TypeError` in the constants module.
84
+
85
+ ## Workflow implementation recipe
86
+
87
+ ### Step 1: define/extend env constants
88
+
89
+ - Add required keys in a constants module.
90
+ - Export each key from `env.<KEY>!`.
91
+ - Validate each key immediately with an explicit `TypeError`.
92
+
93
+ ### Step 2: create workflow file
94
+
95
+ - Import `Workflow` and required orchestration helpers.
96
+ - Import only typed/safe constants (never raw secrets inline).
97
+ - Implement the function body as orchestration steps.
98
+
99
+ ### Step 3: wire in App
100
+
101
+ - Import the workflow module in `main.tsx`.
102
+ - Add it to `workflows: [...]` in `new App({...})`.
103
+ - Keep `App` declaration as default export for runtime loading.
104
+
105
+ ### Step 4: make it test-friendly
106
+
107
+ - Keep outputs deterministic where possible.
108
+ - Avoid hidden global state.
109
+ - Structure side effects so they can be mocked by claim/activity tests.
110
+
111
+ ## Recommended patterns
112
+
113
+ ### A) Workflow with validated input schema
114
+
115
+ ```ts
116
+ import Type from 'typebox'
117
+ import { Workflow } from '@t4h.framework/core'
118
+
119
+ const schema = Type.Object({
120
+ customerId: Type.String(),
121
+ })
122
+
123
+ export const customerSync = new Workflow(
124
+ { id: 'customer-sync', schema },
125
+ async input => {
126
+ return { accepted: true, customerId: input.customerId }
127
+ },
128
+ )
129
+ ```
130
+
131
+ ### B) Workflow orchestrating activities
132
+
133
+ ```ts
134
+ import { History, Workflow } from '@t4h.framework/core'
135
+
136
+ export const processOrder = new Workflow({ id: 'process-order' }, async input => {
137
+ const prepared = await History.reconciler(PrepareOrderActivity, input)
138
+ const charged = await History.reconciler(ChargeOrderActivity, prepared)
139
+ return { charged }
140
+ })
141
+ ```
142
+
143
+ ### C) Nested workflow orchestration
144
+
145
+ ```ts
146
+ import { Workflow, startWorkflow } from '@t4h.framework/core'
147
+ import { childWorkflow } from './child-workflow.js'
148
+
149
+ export const parentWorkflow = new Workflow({ id: 'parent-workflow' }, async input => {
150
+ const ref = await startWorkflow(childWorkflow, input)
151
+ return ref.output
152
+ })
153
+ ```
154
+
155
+ ## Anti-patterns to remove on sight
156
+
157
+ - Inline `"https://..."` service endpoints in workflow body.
158
+ - Inline `"secret"` / `"clientId"` string literals in workflow code.
159
+ - Creating SDK/API clients with literal credentials in the workflow function.
160
+ - Large workflow functions doing infrastructure plumbing instead of orchestration.
161
+ - Registering workflows in scattered places instead of centralized `App` declaration.
162
+
163
+ ## Output expectations when executing this skill
164
+
165
+ When asked to create a workflow, produce:
166
+
167
+ 1. A new workflow file exporting a named `Workflow` constant.
168
+ 2. Any required env constants in the central constants module.
169
+ 3. `App` registration update with the workflow included.
170
+ 4. No hardcoded client, secret, token, tenant, or URL values in workflow code.
@@ -0,0 +1,172 @@
1
+ ---
2
+ name: framework-workflow-testing
3
+ description: >-
4
+ Build and maintain high-quality unit tests for T4H Framework workflows using
5
+ @t4h.framework/core/testing. Use this whenever the user asks to test a
6
+ workflow, add Vitest coverage for History.reconciler flows, mock claims,
7
+ validate logs, or simulate async activity outcomes (fulfilled/rejected/timeout),
8
+ even when they only mention "workflow test", "mock claim", or "run workflow in memory".
9
+ ---
10
+
11
+ # Workflow unit testing with `@t4h.framework/core/testing`
12
+
13
+ Use this skill to create or improve **workflow-focused unit tests** in T4H Framework projects.
14
+
15
+ ## What this skill optimizes
16
+
17
+ - Fast in-memory workflow execution with `WorkflowTesting.run`.
18
+ - Reliable claim mocking with real claim instances + `vi.spyOn`.
19
+ - Clear assertions for behavior (calls, payload, order, and logs).
20
+ - Predictable handling of async activities with `mockAsyncActivity`.
21
+
22
+ ## When to choose this skill
23
+
24
+ Choose this skill when the user asks to:
25
+
26
+ - test a `Workflow` end-to-end (without infra);
27
+ - mock outbound dependencies used by activities (HTTP, DB, etc.);
28
+ - assert emitted logs for workflow execution paths;
29
+ - simulate async activity outcomes in tests;
30
+ - add or fix `vitest` tests for workflow modules.
31
+
32
+ If the request is specifically about testing `AsyncActivity.advance`, use `Internals.runActivity` directly instead of only `WorkflowTesting.run`.
33
+
34
+ ## Public API available
35
+
36
+ ```ts
37
+ import { WorkflowTesting } from '@t4h.framework/core/testing'
38
+ ```
39
+
40
+ `WorkflowTesting.run(workflow, input?, options?)`
41
+
42
+ Supported options:
43
+
44
+ - `claims`: claim providers used during execution.
45
+ - `env`: environment entries for `Env.run`.
46
+ - `mockAsyncActivity`: function or queue for pending async activities.
47
+ - `logs`: capture runtime logs.
48
+ - `maxTicks`: protect against infinite replay loops (default `10_000`).
49
+
50
+ ## Canonical workflow test pattern
51
+
52
+ Follow this structure by default:
53
+
54
+ 1. Arrange claims and spies in helper functions.
55
+ 2. Initialize setup in `beforeEach`.
56
+ 3. Execute workflow with `WorkflowTesting.run`.
57
+ 4. Assert calls, payload structure, ordering, and logs.
58
+
59
+ Base mock style on this pattern:
60
+
61
+ ```ts
62
+ function createClaimsWithHttpSpy() {
63
+ const http = findClaimByConstructor(DevHttpClientRequestClaim)
64
+ const mockRequest = vi.spyOn(http, 'request').mockResolvedValue({
65
+ status: 200,
66
+ headers: { 'content-type': 'application/json' },
67
+ body: Readable.from(Buffer.from('{}')),
68
+ })
69
+ return { claims, mockRequest }
70
+ }
71
+ ```
72
+
73
+ Why this pattern: it keeps tests close to real wiring while still allowing strict call inspection.
74
+
75
+ ## Standard recipe for new tests
76
+
77
+ ### 1) Create setup helpers
78
+
79
+ - Put claim setup and spies in dedicated functions (`createClaimsWith...`).
80
+ - Return both `claims` and the spy object(s) for assertions.
81
+ - Keep data decoders (e.g. request-body parser) as small utility functions.
82
+
83
+ ### 2) Execute workflow under test
84
+
85
+ - Use `await WorkflowTesting.run(workflow, input, { claims, logs, ... })`.
86
+ - Capture logs with `const logs: Log[] = []` when log behavior matters.
87
+
88
+ ### 3) Assert behavior and contract
89
+
90
+ Prefer these assertions:
91
+
92
+ - call count (`toHaveBeenCalledTimes`);
93
+ - important call arguments (URL, method, payload);
94
+ - ordering (`mock.calls[0]`, `mock.calls[1]`);
95
+ - final workflow output;
96
+ - log messages and sequence (`logs.map(entry => entry.args[0])`).
97
+
98
+ ### 4) Cover async outcomes when applicable
99
+
100
+ If any async activity can return `pending`, provide `mockAsyncActivity`.
101
+
102
+ Function strategy:
103
+
104
+ ```ts
105
+ mockAsyncActivity: (activity, { index, pending }) => {
106
+ if (index === 0) return { status: 'fulfilled', output: true }
107
+ return { status: 'rejected', reason: 'denied' }
108
+ }
109
+ ```
110
+
111
+ Queue strategy:
112
+
113
+ ```ts
114
+ mockAsyncActivity: [
115
+ { status: 'fulfilled', output: true },
116
+ { status: 'timeout' },
117
+ ]
118
+ ```
119
+
120
+ Important behavior:
121
+
122
+ - no resolver + pending async => throws;
123
+ - timeout outcome runs real `onTimeout`;
124
+ - exhausted queue => throws `No more async outcomes`.
125
+
126
+ ## Mocking guidelines (do this, avoid that)
127
+
128
+ Do:
129
+
130
+ - mock concrete claim instances used by runtime providers;
131
+ - use `vi.spyOn` on dependency methods (e.g. `request`);
132
+ - return realistic shapes (status/headers/body) for external call mocks;
133
+ - isolate each test with `beforeEach`.
134
+
135
+ Avoid:
136
+
137
+ - over-mocking framework internals;
138
+ - asserting implementation details unrelated to behavior;
139
+ - using `WorkflowTesting.run` to validate real `advance` logic.
140
+
141
+ ## Minimal template to generate quickly
142
+
143
+ ```ts
144
+ import { WorkflowTesting } from '@t4h.framework/core/testing'
145
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
146
+
147
+ describe('my workflow', () => {
148
+ let claims: ReturnType<typeof createClaimsWithSpy>['claims']
149
+ let mockCall: ReturnType<typeof vi.spyOn>
150
+
151
+ beforeEach(() => {
152
+ const setup = createClaimsWithSpy()
153
+ claims = setup.claims
154
+ mockCall = setup.mockCall
155
+ })
156
+
157
+ it('runs to completion with expected side effects', async () => {
158
+ const logs = []
159
+ await WorkflowTesting.run(myWorkflow, undefined, { claims, logs })
160
+ expect(mockCall).toHaveBeenCalledTimes(1)
161
+ })
162
+ })
163
+ ```
164
+
165
+ ## Completion checklist before finishing
166
+
167
+ - Test imports `WorkflowTesting` from `@t4h.framework/core/testing`.
168
+ - Claim providers are passed via `options.claims`.
169
+ - Pending async paths have `mockAsyncActivity`.
170
+ - Assertions cover side effects and expected ordering.
171
+ - Logs are asserted when they are part of the workflow contract.
172
+ - Test file uses Vitest idioms consistently.
package/README.md CHANGED
@@ -190,10 +190,7 @@ For long-running operations that need a bootstrap/advance cycle (e.g. waiting fo
190
190
 
191
191
  ```typescript
192
192
  import Type from 'typebox'
193
- import {
194
- AsyncActivity,
195
- AsyncActivityResultPending,
196
- } from '@t4h.framework/core'
193
+ import { AsyncActivity, AsyncActivityResultPending } from '@t4h.framework/core'
197
194
 
198
195
  const ApprovalSchema = Type.Object({
199
196
  approved: Type.Boolean(),
@@ -243,7 +240,10 @@ class WaitForApprovalActivity extends AsyncActivity<
243
240
  }
244
241
 
245
242
  public async onTimeout(
246
- bootstrap: AsyncActivityResultPending<{ requestId: string; orderId: string }>,
243
+ bootstrap: AsyncActivityResultPending<{
244
+ requestId: string
245
+ orderId: string
246
+ }>,
247
247
  ) {
248
248
  return this.rejected({
249
249
  approved: false,
@@ -346,3 +346,76 @@ Env.run → Claim.run → fn(activity)
346
346
  ```
347
347
 
348
348
  **History** handles reconciliation: when an activity has been executed before, the previous result is returned directly (deterministic replay). When there is no history, the activity runs and the result is stored.
349
+
350
+ ---
351
+
352
+ ## Testing
353
+
354
+ For running a workflow fully in-memory (driving its activities to completion in a
355
+ single process), use the dedicated `testing` entrypoint. It is meant for tests and
356
+ local execution only and is not part of the runtime contract.
357
+
358
+ ```typescript
359
+ import { WorkflowTesting } from '@t4h.framework/core/testing'
360
+
361
+ const output = await WorkflowTesting.run<MyWorkflow>(myWorkflow, input, {
362
+ env: { API_URL: 'https://api.example.com' },
363
+ claims: [
364
+ { provide: UserRepositoryClaim, value: new UserRepositoryClaimImpl() },
365
+ ],
366
+ mockAsyncActivity: () => ({ status: 'fulfilled', output: true }),
367
+ })
368
+ ```
369
+
370
+ ### Driving async activities
371
+
372
+ The harness runs the real `bootstrap`. If it already settles (`done`/`rejected`),
373
+ that result is honored. If it returns `pending`, you **must** provide a
374
+ `mockAsyncActivity` mock that declares the outcome for that activity — otherwise
375
+ the run throws. This forces each test to consciously pick the scenario it covers.
376
+
377
+ The mock returns an outcome (not an `advance` payload):
378
+
379
+ ```typescript
380
+ // success
381
+ mockAsyncActivity: () => ({ status: 'fulfilled', output: true })
382
+
383
+ // rejection
384
+ mockAsyncActivity: () => ({ status: 'rejected', reason: 'denied' })
385
+
386
+ // timeout — runs the activity's real onTimeout(pending) policy
387
+ mockAsyncActivity: () => ({ status: 'timeout' })
388
+ ```
389
+
390
+ For workflows with several async activities in a fixed order, pass an array of
391
+ outcomes — each pending async consumes the next entry (FIFO):
392
+
393
+ ```typescript
394
+ mockAsyncActivity: [
395
+ { status: 'fulfilled', output: true },
396
+ { status: 'rejected', reason: 'denied' },
397
+ { status: 'timeout' },
398
+ ]
399
+ ```
400
+
401
+ The mock receives the activity instance and `{ input, pending, index }`, so you
402
+ can branch per activity or per resolution order:
403
+
404
+ ```typescript
405
+ mockAsyncActivity: (activity, { index }) => {
406
+ if (activity instanceof WaitForApprovalActivity)
407
+ return { status: 'fulfilled', output: true }
408
+
409
+ if (index === 1) return { status: 'timeout' }
410
+
411
+ return { status: 'rejected', reason: 'denied' }
412
+ }
413
+ ```
414
+
415
+ You can also build the same FIFO behavior explicitly with
416
+ `resolveAsyncActivitiesInOrder([...])` from `@t4h.framework/core/testing`.
417
+
418
+ > The real `advance` logic is intentionally **not** exercised here — drive it in a
419
+ > dedicated activity-level test via `Internals.runActivity`. The pending/resume
420
+ > boundary (`PushWaitAsyncActivity`) is also not modeled: an async activity is a
421
+ > single decision point per run.
@@ -0,0 +1,6 @@
1
+ import { type RunWorkflowOptions } from './run-workflow.js';
2
+ import type { Workflow, WorkflowInput } from '../models/Workflow.js';
3
+ export declare class WorkflowTesting {
4
+ static run<T extends Workflow<any, any>>(workflow: T, input?: WorkflowInput<T>, options?: RunWorkflowOptions): Promise<any>;
5
+ }
6
+ //# sourceMappingURL=WorkflowTesting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkflowTesting.d.ts","sourceRoot":"","sources":["../../src/testing/WorkflowTesting.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AACxE,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAEpE,qBAAa,eAAe;WACN,GAAG,CAAC,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,EAClD,QAAQ,EAAE,CAAC,EACX,KAAK,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,EACxB,OAAO,CAAC,EAAE,kBAAkB;CAI/B"}
@@ -0,0 +1,7 @@
1
+ import { runWorkflow } from './run-workflow.js';
2
+ export class WorkflowTesting {
3
+ static async run(workflow, input, options) {
4
+ return runWorkflow(workflow, input, options);
5
+ }
6
+ }
7
+ //# sourceMappingURL=WorkflowTesting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkflowTesting.js","sourceRoot":"","sources":["../../src/testing/WorkflowTesting.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAA2B,MAAM,mBAAmB,CAAA;AAGxE,MAAM,OAAO,eAAe;IACnB,MAAM,CAAC,KAAK,CAAC,GAAG,CACrB,QAAW,EACX,KAAwB,EACxB,OAA4B;QAE5B,OAAO,WAAW,CAAC,QAAQ,EAAE,KAAyB,EAAE,OAAO,CAAC,CAAA;IAClE,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ export * from './WorkflowTesting.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export * from './WorkflowTesting.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA"}
@@ -0,0 +1,31 @@
1
+ import { type AnyAsyncActivity } from '../models/AsyncActivity.js';
2
+ import { AsyncActivityResultPending } from '../models/AsyncActivityAdvanceResult.js';
3
+ import type { ClaimProvider } from '../models/Claim.js';
4
+ import { Env } from '../models/Env.js';
5
+ import type { Log } from '../models/Logger.js';
6
+ import type { Workflow, WorkflowInput } from '../models/Workflow.js';
7
+ interface MockAsyncActivityContext {
8
+ input: unknown;
9
+ pending: AsyncActivityResultPending;
10
+ index: number;
11
+ }
12
+ export type AsyncActivityOutcome = {
13
+ status: 'fulfilled';
14
+ output?: unknown;
15
+ } | {
16
+ status: 'rejected';
17
+ reason?: unknown;
18
+ } | {
19
+ status: 'timeout';
20
+ };
21
+ type MockAsyncActivity = (activity: AnyAsyncActivity, context: MockAsyncActivityContext) => AsyncActivityOutcome | Promise<AsyncActivityOutcome>;
22
+ export interface RunWorkflowOptions {
23
+ claims?: readonly ClaimProvider<any>[];
24
+ env?: Env.Entries;
25
+ mockAsyncActivity?: MockAsyncActivity | readonly AsyncActivityOutcome[];
26
+ maxTicks?: number;
27
+ logs?: Log[];
28
+ }
29
+ export declare function runWorkflow<T extends Workflow<any, any>>(workflow: T, input: WorkflowInput<T>, options?: RunWorkflowOptions): Promise<any>;
30
+ export {};
31
+ //# sourceMappingURL=run-workflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-workflow.d.ts","sourceRoot":"","sources":["../../src/testing/run-workflow.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAEL,0BAA0B,EAE3B,MAAM,yCAAyC,CAAA;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAA;AAEtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAA;AAG9C,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAGpE,UAAU,wBAAwB;IAChC,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,0BAA0B,CAAA;IACnC,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACzC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACxC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAA;AAEzB,KAAK,iBAAiB,GAAG,CACvB,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,wBAAwB,KAC9B,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAA;AAEzD,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,SAAS,aAAa,CAAC,GAAG,CAAC,EAAE,CAAA;IACtC,GAAG,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACjB,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,SAAS,oBAAoB,EAAE,CAAA;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;CACb;AA6BD,wBAAsB,WAAW,CAAC,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,EAC5D,QAAQ,EAAE,CAAC,EACX,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACvB,OAAO,GAAE,kBAAuB,gBAmCjC"}
@@ -0,0 +1,102 @@
1
+ import { AsyncActivity, } from '../models/AsyncActivity.js';
2
+ import { AsyncActivityResultFulfilled, AsyncActivityResultPending, AsyncActivityResultRejected, } from '../models/AsyncActivityAdvanceResult.js';
3
+ import { Env } from '../models/Env.js';
4
+ import { PushActivity } from '../models/PushActivity.js';
5
+ import { SyncActivity } from '../models/SyncActivity.js';
6
+ import { Internals } from '../tools/Internals.js';
7
+ function mockAsyncActivities(outcomes) {
8
+ const queue = [...outcomes];
9
+ return (activity, context) => {
10
+ const next = queue.shift();
11
+ if (!next)
12
+ throw new Error(`No more async outcomes (activity: "${activity.constructor.name}", index: ${context.index}, input: ${JSON.stringify(context.input)})`);
13
+ return next;
14
+ };
15
+ }
16
+ function normalizeMockAsyncActivity(mock) {
17
+ if (!mock)
18
+ return undefined;
19
+ if (Array.isArray(mock))
20
+ return mockAsyncActivities(mock);
21
+ return mock;
22
+ }
23
+ export async function runWorkflow(workflow, input, options = {}) {
24
+ const claims = options.claims ?? [];
25
+ const env = options.env ?? {};
26
+ const maxTicks = options.maxTicks ?? 10_000;
27
+ const history = [];
28
+ const logs = options.logs ?? [];
29
+ const mocks = normalizeMockAsyncActivity(options.mockAsyncActivity);
30
+ const resolutionState = { index: 0 };
31
+ for (let tick = 0; tick < maxTicks; tick++) {
32
+ try {
33
+ return await Internals.runWorkflow(workflow, {
34
+ input,
35
+ claims,
36
+ env,
37
+ history,
38
+ logs,
39
+ });
40
+ }
41
+ catch (pushOrError) {
42
+ if (!(pushOrError instanceof PushActivity))
43
+ throw pushOrError;
44
+ const item = await resolveActivity(pushOrError, { claims, env }, mocks, resolutionState);
45
+ history.push(item);
46
+ }
47
+ }
48
+ throw new Error(`Workflow did not settle within ${maxTicks} ticks`);
49
+ }
50
+ async function resolveActivity(push, context, mockAsyncActivity, resolutionState) {
51
+ const activity = push.activity;
52
+ if (activity instanceof SyncActivity) {
53
+ const output = await Internals.runActivity(activity, context, instance => instance.run(push.input));
54
+ return { type: 'sync', output };
55
+ }
56
+ if (activity instanceof AsyncActivity) {
57
+ const bootstrap = await Internals.runActivity(activity, context, instance => instance.bootstrap(push.input));
58
+ return resolveAsyncActivity(activity, bootstrap, context, push.input, mockAsyncActivity, resolutionState);
59
+ }
60
+ throw new Error('Unknown activity type');
61
+ }
62
+ async function resolveAsyncActivity(activity, bootstrap, context, input, mockAsyncActivity, resolutionState) {
63
+ if (bootstrap instanceof AsyncActivityResultFulfilled)
64
+ return { type: 'async', status: 'fulfilled', output: bootstrap.payload };
65
+ if (bootstrap instanceof AsyncActivityResultRejected)
66
+ return { type: 'async', status: 'rejected', output: bootstrap.payload };
67
+ if (!(bootstrap instanceof AsyncActivityResultPending))
68
+ throw new Error('Unknown async activity result');
69
+ const name = activity.constructor.name;
70
+ if (!mockAsyncActivity)
71
+ throw new Error(`Async activity "${name}" is pending and no resolveAsyncActivity was provided`);
72
+ const outcome = await mockAsyncActivity(activity, {
73
+ input,
74
+ pending: bootstrap,
75
+ index: resolutionState?.index ?? 0,
76
+ });
77
+ if (resolutionState)
78
+ resolutionState.index++;
79
+ return outcomeToHistoryItem(activity, outcome, bootstrap, context, name);
80
+ }
81
+ async function outcomeToHistoryItem(activity, outcome, pending, context, name) {
82
+ switch (outcome.status) {
83
+ case 'fulfilled':
84
+ return { type: 'async', status: 'fulfilled', output: outcome.output };
85
+ case 'rejected':
86
+ return { type: 'async', status: 'rejected', output: outcome.reason };
87
+ case 'timeout': {
88
+ const onTimeout = activity.onTimeout?.bind(activity);
89
+ if (!onTimeout)
90
+ throw new Error(`Async activity "${name}" timed out but defines no onTimeout handler`);
91
+ const result = await Internals.runActivity(activity, context, () => onTimeout(pending));
92
+ if (result instanceof AsyncActivityResultFulfilled)
93
+ return { type: 'async', status: 'fulfilled', output: result.payload };
94
+ if (result instanceof AsyncActivityResultRejected)
95
+ return { type: 'async', status: 'rejected', output: result.payload };
96
+ throw new Error(`Async activity "${name}" stayed pending after onTimeout`);
97
+ }
98
+ default:
99
+ throw new Error(`Unknown async activity outcome for "${name}"`);
100
+ }
101
+ }
102
+ //# sourceMappingURL=run-workflow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-workflow.js","sourceRoot":"","sources":["../../src/testing/run-workflow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { AsyncActivity } from '../models/AsyncActivity.js';
2
+ import type { ClaimProvider } from '../models/Claim.js';
3
+ import { Env } from '../models/Env.js';
4
+ import type { Log } from '../models/Logger.js';
5
+ import type { Workflow, WorkflowInput } from '../models/Workflow.js';
6
+ export type AsyncActivityResolver = (activity: AsyncActivity<any, any, any, any, any, any>, input: unknown) => unknown | Promise<unknown>;
7
+ export interface RunWorkflowToCompletionOptions {
8
+ claims?: readonly ClaimProvider<any>[];
9
+ env?: Env.Entries;
10
+ resolveAsyncActivity?: AsyncActivityResolver;
11
+ maxTicks?: number;
12
+ logs?: Log[];
13
+ }
14
+ export declare function runWorkflowToCompletion<T extends Workflow<any, any>>(workflow: T, input: WorkflowInput<T>, options?: RunWorkflowToCompletionOptions): Promise<any>;
15
+ //# sourceMappingURL=runWorkflowToCompletion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runWorkflowToCompletion.d.ts","sourceRoot":"","sources":["../../src/tools/runWorkflowToCompletion.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAM1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAA;AAEtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAA;AAG9C,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAGpE,MAAM,MAAM,qBAAqB,GAAG,CAClC,QAAQ,EAAE,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EACrD,KAAK,EAAE,OAAO,KACX,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;AAE/B,MAAM,WAAW,8BAA8B;IAC7C,MAAM,CAAC,EAAE,SAAS,aAAa,CAAC,GAAG,CAAC,EAAE,CAAA;IACtC,GAAG,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACjB,oBAAoB,CAAC,EAAE,qBAAqB,CAAA;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;CACb;AAED,wBAAsB,uBAAuB,CAAC,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,EACxE,QAAQ,EAAE,CAAC,EACX,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACvB,OAAO,GAAE,8BAAmC,gBAgC7C"}
@@ -0,0 +1,61 @@
1
+ import { AsyncActivity } from '../models/AsyncActivity.js';
2
+ import { AsyncActivityResultFulfilled, AsyncActivityResultPending, AsyncActivityResultRejected, } from '../models/AsyncActivityAdvanceResult.js';
3
+ import { Env } from '../models/Env.js';
4
+ import { PushActivity } from '../models/PushActivity.js';
5
+ import { SyncActivity } from '../models/SyncActivity.js';
6
+ import { Internals } from './Internals.js';
7
+ export async function runWorkflowToCompletion(workflow, input, options = {}) {
8
+ const claims = options.claims ?? [];
9
+ const env = options.env ?? {};
10
+ const maxTicks = options.maxTicks ?? 10_000;
11
+ const history = [];
12
+ const logs = options.logs ?? [];
13
+ for (let tick = 0; tick < maxTicks; tick++) {
14
+ try {
15
+ return await Internals.runWorkflow(workflow, {
16
+ input,
17
+ claims,
18
+ env,
19
+ history,
20
+ logs,
21
+ });
22
+ }
23
+ catch (pushOrError) {
24
+ if (!(pushOrError instanceof PushActivity))
25
+ throw pushOrError;
26
+ const item = await resolveActivity(pushOrError, { claims, env }, options.resolveAsyncActivity);
27
+ history.push(item);
28
+ }
29
+ }
30
+ throw new Error(`Workflow did not settle within ${maxTicks} ticks`);
31
+ }
32
+ async function resolveActivity(push, context, resolver) {
33
+ const activity = push.activity;
34
+ if (activity instanceof SyncActivity) {
35
+ const output = await Internals.runActivity(activity, context, instance => instance.run(push.input));
36
+ return { type: 'sync', output };
37
+ }
38
+ if (activity instanceof AsyncActivity) {
39
+ const bootstrap = await Internals.runActivity(activity, context, instance => instance.bootstrap(push.input));
40
+ const settled = await settleAsyncActivity(activity, bootstrap, context, push.input, resolver);
41
+ return settled;
42
+ }
43
+ throw new Error('Unknown activity type');
44
+ }
45
+ async function settleAsyncActivity(activity, result, context, input, resolver) {
46
+ if (result instanceof AsyncActivityResultFulfilled)
47
+ return { type: 'async', status: 'fulfilled', output: result.payload };
48
+ if (result instanceof AsyncActivityResultRejected)
49
+ return { type: 'async', status: 'rejected', output: result.payload };
50
+ if (!(result instanceof AsyncActivityResultPending))
51
+ throw new Error('Unknown async activity result');
52
+ const name = activity.constructor.name;
53
+ if (!resolver)
54
+ throw new Error(`Async activity "${name}" is pending and no resolveAsyncActivity was provided`);
55
+ const payload = await resolver(activity, input);
56
+ const advanced = await Internals.runActivity(activity, context, instance => instance.advance(payload, result));
57
+ if (advanced instanceof AsyncActivityResultPending)
58
+ throw new Error(`Async activity "${name}" stayed pending after advance`);
59
+ return settleAsyncActivity(activity, advanced, context, input, resolver);
60
+ }
61
+ //# sourceMappingURL=runWorkflowToCompletion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runWorkflowToCompletion.js","sourceRoot":"","sources":["../../src/tools/runWorkflowToCompletion.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t4h.framework/core",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Core module for the T4H Framework",
5
5
  "homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/core",
6
6
  "bugs": "https://github.com/tech4humans-brasil/framework/issues",
@@ -13,14 +13,21 @@
13
13
  "author": "Tech4Humans <contact@tech4h.com.br> (https://tech4h.com.br)",
14
14
  "type": "module",
15
15
  "exports": {
16
- "types": "./dist/core.d.ts",
17
- "import": "./dist/core.js"
16
+ ".": {
17
+ "types": "./dist/core.d.ts",
18
+ "import": "./dist/core.js"
19
+ },
20
+ "./testing": {
21
+ "types": "./dist/testing/index.d.ts",
22
+ "import": "./dist/testing/index.js"
23
+ }
18
24
  },
19
25
  "module": "./dist/core.js",
20
26
  "types": "./dist/core.d.ts",
21
27
  "files": [
22
28
  "dist",
23
- "LICENSE"
29
+ "LICENSE",
30
+ ".ai"
24
31
  ],
25
32
  "scripts": {
26
33
  "build": "tsc --project tsconfig.build.json",