@t4h.framework/core 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai/skills/framework-core/SKILL.md +103 -287
- package/.ai/skills/framework-workflow-creation/SKILL.md +108 -60
- package/.ai/skills/framework-workflow-testing/SKILL.md +165 -111
- package/README.md +123 -37
- package/dist/activities/StartWorkflowActivity.d.ts +52 -9
- package/dist/activities/StartWorkflowActivity.d.ts.map +1 -1
- package/dist/activities/StartWorkflowActivity.js +45 -5
- package/dist/activities/StartWorkflowActivity.js.map +1 -1
- package/dist/core.d.ts +2 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +2 -0
- package/dist/core.js.map +1 -1
- package/dist/models/ContinueAsNew.d.ts +24 -0
- package/dist/models/ContinueAsNew.d.ts.map +1 -0
- package/dist/models/ContinueAsNew.js +28 -0
- package/dist/models/ContinueAsNew.js.map +1 -0
- package/dist/models/ExecutionContext.d.ts +8 -0
- package/dist/models/ExecutionContext.d.ts.map +1 -0
- package/dist/models/ExecutionContext.js +22 -0
- package/dist/models/ExecutionContext.js.map +1 -0
- package/dist/models/WorkflowBatchClaim.d.ts +10 -0
- package/dist/models/WorkflowBatchClaim.d.ts.map +1 -0
- package/dist/models/WorkflowBatchClaim.js +3 -0
- package/dist/models/WorkflowBatchClaim.js.map +1 -0
- package/dist/models/WorkflowBatchRef.d.ts +6 -0
- package/dist/models/WorkflowBatchRef.d.ts.map +1 -0
- package/dist/models/WorkflowBatchRef.js +9 -0
- package/dist/models/WorkflowBatchRef.js.map +1 -0
- package/dist/models/WorkflowRef.d.ts +2 -2
- package/dist/models/WorkflowRef.d.ts.map +1 -1
- package/dist/models/WorkflowRef.js.map +1 -1
- package/dist/testing/ActivityTesting.d.ts +25 -0
- package/dist/testing/ActivityTesting.d.ts.map +1 -0
- package/dist/testing/ActivityTesting.js +74 -0
- package/dist/testing/ActivityTesting.js.map +1 -0
- package/dist/testing/NonDeterministicWorkflowError.d.ts +4 -0
- package/dist/testing/NonDeterministicWorkflowError.d.ts.map +1 -0
- package/dist/testing/NonDeterministicWorkflowError.js +7 -0
- package/dist/testing/NonDeterministicWorkflowError.js.map +1 -0
- package/dist/testing/StrictGuard.d.ts +4 -0
- package/dist/testing/StrictGuard.d.ts.map +1 -0
- package/dist/testing/StrictGuard.js +26 -0
- package/dist/testing/StrictGuard.js.map +1 -0
- package/dist/testing/TestClock.d.ts +12 -0
- package/dist/testing/TestClock.d.ts.map +1 -0
- package/dist/testing/TestClock.js +64 -0
- package/dist/testing/TestClock.js.map +1 -0
- package/dist/testing/TestWorkflowEnvironment.d.ts +27 -0
- package/dist/testing/TestWorkflowEnvironment.d.ts.map +1 -0
- package/dist/testing/TestWorkflowEnvironment.js +64 -0
- package/dist/testing/TestWorkflowEnvironment.js.map +1 -0
- package/dist/testing/WorkflowTesting.d.ts +1 -1
- package/dist/testing/async-activity-results.d.ts +12 -0
- package/dist/testing/async-activity-results.d.ts.map +1 -0
- package/dist/testing/async-activity-results.js +10 -0
- package/dist/testing/async-activity-results.js.map +1 -0
- package/dist/testing/create-mock-claim.d.ts +11 -0
- package/dist/testing/create-mock-claim.d.ts.map +1 -0
- package/dist/testing/create-mock-claim.js +26 -0
- package/dist/testing/create-mock-claim.js.map +1 -0
- package/dist/testing/create-mock-claims.d.ts +13 -0
- package/dist/testing/create-mock-claims.d.ts.map +1 -0
- package/dist/testing/create-mock-claims.js +8 -0
- package/dist/testing/create-mock-claims.js.map +1 -0
- package/dist/testing/create-test-env.d.ts +10 -0
- package/dist/testing/create-test-env.d.ts.map +1 -0
- package/dist/testing/create-test-env.js +7 -0
- package/dist/testing/create-test-env.js.map +1 -0
- package/dist/testing/exceptions/NonDeterministicWorkflowError.d.ts +4 -0
- package/dist/testing/exceptions/NonDeterministicWorkflowError.d.ts.map +1 -0
- package/dist/testing/exceptions/NonDeterministicWorkflowError.js +7 -0
- package/dist/testing/exceptions/NonDeterministicWorkflowError.js.map +1 -0
- package/dist/testing/index.d.ts +8 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +8 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock-async.d.ts +48 -0
- package/dist/testing/mock-async.d.ts.map +1 -0
- package/dist/testing/mock-async.js +58 -0
- package/dist/testing/mock-async.js.map +1 -0
- package/dist/testing/outcomes.d.ts +12 -0
- package/dist/testing/outcomes.d.ts.map +1 -0
- package/dist/testing/outcomes.js +10 -0
- package/dist/testing/outcomes.js.map +1 -0
- package/dist/testing/run-workflow.d.ts +22 -21
- package/dist/testing/run-workflow.d.ts.map +1 -1
- package/dist/testing/run-workflow.js +47 -23
- package/dist/testing/run-workflow.js.map +1 -1
- package/dist/testing/tools/clone.d.ts +2 -0
- package/dist/testing/tools/clone.d.ts.map +1 -0
- package/dist/testing/tools/clone.js +42 -0
- package/dist/testing/tools/clone.js.map +1 -0
- package/dist/testing/tools/create-mock-claim.d.ts +5 -0
- package/dist/testing/tools/create-mock-claim.d.ts.map +1 -0
- package/dist/testing/tools/create-mock-claim.js +26 -0
- package/dist/testing/tools/create-mock-claim.js.map +1 -0
- package/dist/testing/tools/create-mock-claims.d.ts +8 -0
- package/dist/testing/tools/create-mock-claims.d.ts.map +1 -0
- package/dist/testing/tools/create-mock-claims.js +5 -0
- package/dist/testing/tools/create-mock-claims.js.map +1 -0
- package/dist/testing/tools/create-test-env.d.ts +4 -0
- package/dist/testing/tools/create-test-env.d.ts.map +1 -0
- package/dist/testing/tools/create-test-env.js +7 -0
- package/dist/testing/tools/create-test-env.js.map +1 -0
- package/dist/testing/tools/mock-async.d.ts +20 -0
- package/dist/testing/tools/mock-async.d.ts.map +1 -0
- package/dist/testing/tools/mock-async.js +57 -0
- package/dist/testing/tools/mock-async.js.map +1 -0
- package/dist/testing/tools/resolve-activity.d.ts +11 -0
- package/dist/testing/tools/resolve-activity.d.ts.map +1 -0
- package/dist/testing/tools/resolve-activity.js +23 -0
- package/dist/testing/tools/resolve-activity.js.map +1 -0
- package/dist/testing/tools/resolve-async-activity.d.ts +15 -0
- package/dist/testing/tools/resolve-async-activity.d.ts.map +1 -0
- package/dist/testing/tools/resolve-async-activity.js +69 -0
- package/dist/testing/tools/resolve-async-activity.js.map +1 -0
- package/dist/testing/tools/run-with-clock.d.ts +3 -0
- package/dist/testing/tools/run-with-clock.d.ts.map +1 -0
- package/dist/testing/tools/run-with-clock.js +7 -0
- package/dist/testing/tools/run-with-clock.js.map +1 -0
- package/dist/testing/tools/run-workflow-ticks.d.ts +8 -0
- package/dist/testing/tools/run-workflow-ticks.d.ts.map +1 -0
- package/dist/testing/tools/run-workflow-ticks.js +26 -0
- package/dist/testing/tools/run-workflow-ticks.js.map +1 -0
- package/dist/testing/tools/run-workflow.d.ts +25 -0
- package/dist/testing/tools/run-workflow.d.ts.map +1 -0
- package/dist/testing/tools/run-workflow.js +25 -0
- package/dist/testing/tools/run-workflow.js.map +1 -0
- package/dist/testing/tools/serializable-clone.d.ts +21 -0
- package/dist/testing/tools/serializable-clone.d.ts.map +1 -0
- package/dist/testing/tools/serializable-clone.js +77 -0
- package/dist/testing/tools/serializable-clone.js.map +1 -0
- package/dist/testing/types.d.ts +19 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/testing/typings/AsyncActivityResult.d.ts +10 -0
- package/dist/testing/typings/AsyncActivityResult.d.ts.map +1 -0
- package/dist/testing/typings/AsyncActivityResult.js +2 -0
- package/dist/testing/typings/AsyncActivityResult.js.map +1 -0
- package/dist/testing/typings/ClaimConstructor.d.ts +2 -0
- package/dist/testing/typings/ClaimConstructor.d.ts.map +1 -0
- package/dist/testing/typings/ClaimConstructor.js +2 -0
- package/dist/testing/typings/ClaimConstructor.js.map +1 -0
- package/dist/testing/typings/MockAsyncActivity.d.ts +10 -0
- package/dist/testing/typings/MockAsyncActivity.d.ts.map +1 -0
- package/dist/testing/typings/MockAsyncActivity.js +2 -0
- package/dist/testing/typings/MockAsyncActivity.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: framework-workflow-creation
|
|
3
3
|
description: >-
|
|
4
|
-
Design and implement T4H Framework workflows with @t4h.framework/core
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
Design and implement T4H Framework workflows with @t4h.framework/core: expose
|
|
5
|
+
a Workflow constant with metadata/schema, orchestrate with the framework's
|
|
6
|
+
package primitives (http, sleep, startWorkflow) plus plain TypeScript, and
|
|
7
|
+
read configuration from a shared EnvDesign in constants/environments.ts.
|
|
8
|
+
Register it in an App. Use whenever the user asks to create a new workflow,
|
|
9
|
+
wire it into an App, or refactor workflow code to remove hardcoded
|
|
10
|
+
URLs/secrets/client values and move configuration to an EnvDesign.
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# Create workflows with `@t4h.framework/core`
|
|
13
14
|
|
|
14
|
-
Use this skill to build production-ready
|
|
15
|
+
Use this skill to build production-ready workflows that are easy to register, test, and maintain.
|
|
16
|
+
|
|
17
|
+
A **workflow is orchestration only**: an async function that coordinates steps. The steps themselves — HTTP calls, delays, storage — come from the framework's packages. You compose those primitives; you do **not** build low-level infrastructure inside a workflow.
|
|
15
18
|
|
|
16
19
|
## Primary goal
|
|
17
20
|
|
|
18
21
|
Create workflows that:
|
|
19
22
|
|
|
20
23
|
- expose a `Workflow` constant with explicit metadata (`id`, optional `description`, optional `schema`);
|
|
21
|
-
- orchestrate
|
|
22
|
-
- register cleanly in `App`
|
|
23
|
-
- never hardcode URLs, secrets, tokens, client IDs, or tenants
|
|
24
|
+
- orchestrate through framework primitives (`http`, `sleep`, `startWorkflow`) plus plain TypeScript and `env` — never ad-hoc infrastructure code;
|
|
25
|
+
- register cleanly in an `App` (`export default new App({...})`);
|
|
26
|
+
- never hardcode URLs, secrets, tokens, client IDs, or tenants.
|
|
27
|
+
|
|
28
|
+
## What you orchestrate with
|
|
29
|
+
|
|
30
|
+
| Need | Use | From |
|
|
31
|
+
|------|-----|------|
|
|
32
|
+
| External HTTP call | `http.get/post/put/...` | `@t4h.framework/http` (see **framework-http**) |
|
|
33
|
+
| Delay / wait | `sleep('30 seconds')` | `@t4h.framework/sleep` (see **framework-sleep**) |
|
|
34
|
+
| Nested workflow | `startWorkflow(child, input)` | `@t4h.framework/core` |
|
|
35
|
+
| Configuration | `environments.MY_KEY` from a shared `EnvDesign` | `@t4h.framework/core` |
|
|
36
|
+
| Branching / mapping | plain TypeScript | — |
|
|
37
|
+
|
|
38
|
+
Keep the body **deterministic** so it replays safely: branch on inputs and step results, never on `Date.now()`, `Math.random()`, or ambient state. Use `sleep` when you need time to pass.
|
|
24
39
|
|
|
25
40
|
## Expected project shape
|
|
26
41
|
|
|
@@ -28,9 +43,12 @@ Create workflows that:
|
|
|
28
43
|
|
|
29
44
|
```ts
|
|
30
45
|
import { Workflow } from '@t4h.framework/core'
|
|
46
|
+
import { http } from '@t4h.framework/http'
|
|
47
|
+
import { environments } from '../constants/environments.js'
|
|
31
48
|
|
|
32
49
|
export const myWorkflow = new Workflow({ id: 'my-workflow' }, async input => {
|
|
33
|
-
|
|
50
|
+
const res = await http.post(`${environments.SERVICE_URL}/events`, { json: { input } })
|
|
51
|
+
return { status: res.status }
|
|
34
52
|
})
|
|
35
53
|
```
|
|
36
54
|
|
|
@@ -47,66 +65,79 @@ export default new App({
|
|
|
47
65
|
})
|
|
48
66
|
```
|
|
49
67
|
|
|
50
|
-
### Environment constants module
|
|
68
|
+
### Environment constants module — `constants/environments.ts`
|
|
69
|
+
|
|
70
|
+
**Standard pattern: declare every env var in a single `EnvDesign` inside `src/constants/environments.ts`, export it, and import it wherever configuration is needed.** Do not use the raw `env` proxy or `process.env`.
|
|
51
71
|
|
|
52
72
|
```ts
|
|
53
|
-
|
|
73
|
+
// src/constants/environments.ts
|
|
74
|
+
import { EnvDesign } from '@t4h.framework/core'
|
|
75
|
+
|
|
76
|
+
export const environments = EnvDesign({
|
|
77
|
+
SERVICE_URL: { type: String },
|
|
78
|
+
MAX_RETRIES: { type: Number, defaultValue: '3' },
|
|
79
|
+
DEBUG: { type: Boolean, isOptional: true },
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then, inside a workflow (or any helper), read typed values from it:
|
|
54
84
|
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
```ts
|
|
86
|
+
import { environments } from '../constants/environments.js'
|
|
87
|
+
// environments.SERVICE_URL -> string, environments.MAX_RETRIES -> number
|
|
57
88
|
```
|
|
58
89
|
|
|
59
|
-
|
|
90
|
+
Why `EnvDesign` and not raw `env`:
|
|
60
91
|
|
|
61
|
-
|
|
92
|
+
- **Typed & validated:** each key is parsed to its declared `type` (`String` | `Number` | `Boolean` | a custom `(raw: string) => T` parser); a missing required key throws on access (fail-fast), `defaultValue` fills gaps, `isOptional: true` allows `undefined`.
|
|
93
|
+
- **Lazy & safe at import:** `EnvDesign({...})` does not read `env` at construction, so the top-level `export const environments = ...` is safe. Values are read/parsed only when a key is accessed — so **access keys inside the workflow body/functions, never at module top level**.
|
|
94
|
+
- **Manifest for free:** declaring vars via `EnvDesign` registers them in `app.toManifest().env`, so the runtime knows which vars the app requires.
|
|
62
95
|
|
|
63
|
-
|
|
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).
|
|
96
|
+
One `environments` object is the single source of configuration for the whole project — import it anywhere instead of re-declaring env access.
|
|
68
97
|
|
|
69
98
|
## Authoring rules
|
|
70
99
|
|
|
71
|
-
1. Keep workflows orchestration-focused
|
|
72
|
-
2.
|
|
73
|
-
3.
|
|
74
|
-
4.
|
|
75
|
-
5.
|
|
100
|
+
1. Keep workflows orchestration-focused — coordinate steps, don't implement them.
|
|
101
|
+
2. Perform every external effect through a framework package primitive (`http`, `sleep`, …), not `fetch`/`axios`/raw SDKs.
|
|
102
|
+
3. Define workflow metadata with a stable `id`.
|
|
103
|
+
4. Add a `schema` when input validation is needed.
|
|
104
|
+
5. Import all configuration from the shared `EnvDesign` in `constants/environments.ts` — never raw `env` or `process.env`.
|
|
105
|
+
6. Keep the body deterministic so replay produces the same result.
|
|
76
106
|
|
|
77
107
|
## Hard constraints
|
|
78
108
|
|
|
79
109
|
- Do not hardcode URLs, client IDs, client secrets, API keys, tenants, or tokens.
|
|
80
110
|
- Do not initialize third-party clients with inline credentials inside workflow files.
|
|
81
|
-
- Do not read `process.env`
|
|
111
|
+
- Do not read `process.env` or the raw `env` proxy directly — declare configuration in the shared `EnvDesign` (`constants/environments.ts`).
|
|
112
|
+
- Do not call `fetch`/`axios` or other non-framework I/O directly — that breaks replay safety.
|
|
82
113
|
|
|
83
|
-
|
|
114
|
+
Required keys declared in the `EnvDesign` fail fast automatically: accessing an unset required key throws `Environment variable X is not set`. Use `isOptional`/`defaultValue` when a key may be absent.
|
|
84
115
|
|
|
85
116
|
## Workflow implementation recipe
|
|
86
117
|
|
|
87
118
|
### Step 1: define/extend env constants
|
|
88
119
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
120
|
+
- Open (or create) `src/constants/environments.ts` and add the required keys to the exported `EnvDesign`.
|
|
121
|
+
- Give each key a `type` (`String`/`Number`/`Boolean`/custom parser) and, when applicable, `defaultValue` or `isOptional`.
|
|
122
|
+
- Import `environments` where needed; do not add per-key getters or read raw `env`.
|
|
92
123
|
|
|
93
|
-
### Step 2: create workflow file
|
|
124
|
+
### Step 2: create the workflow file
|
|
94
125
|
|
|
95
|
-
- Import `Workflow` and
|
|
96
|
-
- Import
|
|
97
|
-
- Implement the
|
|
126
|
+
- Import `Workflow` and the package primitives you need (`http`, `sleep`, `startWorkflow`).
|
|
127
|
+
- Import typed/safe constants only (never raw secrets inline).
|
|
128
|
+
- Implement the body as orchestration steps.
|
|
98
129
|
|
|
99
|
-
### Step 3: wire
|
|
130
|
+
### Step 3: wire it into the App
|
|
100
131
|
|
|
101
|
-
- Import the workflow
|
|
132
|
+
- Import the workflow in `main.tsx`.
|
|
102
133
|
- Add it to `workflows: [...]` in `new App({...})`.
|
|
103
|
-
- Keep `App`
|
|
134
|
+
- Keep the `App` as the default export for runtime loading.
|
|
104
135
|
|
|
105
136
|
### Step 4: make it test-friendly
|
|
106
137
|
|
|
107
|
-
- Keep outputs deterministic
|
|
138
|
+
- Keep outputs deterministic.
|
|
108
139
|
- Avoid hidden global state.
|
|
109
|
-
-
|
|
140
|
+
- Drive external effects through package primitives so tests can supply their providers (see **framework-core** testing and each package's skill).
|
|
110
141
|
|
|
111
142
|
## Recommended patterns
|
|
112
143
|
|
|
@@ -116,31 +147,45 @@ If a required env key is missing, fail fast with a clear `TypeError` in the cons
|
|
|
116
147
|
import Type from 'typebox'
|
|
117
148
|
import { Workflow } from '@t4h.framework/core'
|
|
118
149
|
|
|
119
|
-
const schema = Type.Object({
|
|
120
|
-
customerId: Type.String(),
|
|
121
|
-
})
|
|
150
|
+
const schema = Type.Object({ customerId: Type.String() })
|
|
122
151
|
|
|
123
152
|
export const customerSync = new Workflow(
|
|
124
153
|
{ id: 'customer-sync', schema },
|
|
125
|
-
async input => {
|
|
126
|
-
|
|
154
|
+
async input => ({ accepted: true, customerId: input.customerId }),
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### B) Workflow orchestrating an external call
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { Workflow } from '@t4h.framework/core'
|
|
162
|
+
import { http } from '@t4h.framework/http'
|
|
163
|
+
|
|
164
|
+
export const notifyLong = new Workflow(
|
|
165
|
+
{ id: 'notify-long', schema: Type.String() },
|
|
166
|
+
async (input: string) => {
|
|
167
|
+
if (input.length > 5) {
|
|
168
|
+
await http.post('https://example.com/webhook', { json: { input } })
|
|
169
|
+
}
|
|
127
170
|
},
|
|
128
171
|
)
|
|
129
172
|
```
|
|
130
173
|
|
|
131
|
-
###
|
|
174
|
+
### C) Workflow with a delay
|
|
132
175
|
|
|
133
176
|
```ts
|
|
134
|
-
import {
|
|
177
|
+
import { Workflow } from '@t4h.framework/core'
|
|
178
|
+
import { http } from '@t4h.framework/http'
|
|
179
|
+
import { sleep } from '@t4h.framework/sleep'
|
|
135
180
|
|
|
136
|
-
export const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
181
|
+
export const retryLater = new Workflow({ id: 'retry-later' }, async input => {
|
|
182
|
+
await http.post('https://example.com/start', { json: input })
|
|
183
|
+
await sleep('5 minutes')
|
|
184
|
+
await http.post('https://example.com/finish', { json: input })
|
|
140
185
|
})
|
|
141
186
|
```
|
|
142
187
|
|
|
143
|
-
###
|
|
188
|
+
### D) Nested workflow orchestration
|
|
144
189
|
|
|
145
190
|
```ts
|
|
146
191
|
import { Workflow, startWorkflow } from '@t4h.framework/core'
|
|
@@ -154,17 +199,20 @@ export const parentWorkflow = new Workflow({ id: 'parent-workflow' }, async inpu
|
|
|
154
199
|
|
|
155
200
|
## Anti-patterns to remove on sight
|
|
156
201
|
|
|
157
|
-
- Inline `"https://..."`
|
|
158
|
-
-
|
|
159
|
-
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
202
|
+
- Inline `"https://..."` endpoints or `"secret"` / `"clientId"` literals in the workflow body — pull them from the shared `EnvDesign`.
|
|
203
|
+
- Reading raw `env`/`process.env`, or scattering per-key env getters instead of one `EnvDesign` in `constants/environments.ts`.
|
|
204
|
+
- Accessing an `EnvDesign` key at module top level (throws — no env storage outside a run).
|
|
205
|
+
- Creating SDK/API clients with literal credentials inside the workflow function.
|
|
206
|
+
- Calling `fetch`/`axios` or other raw I/O instead of the framework's `http` primitive.
|
|
207
|
+
- Building custom low-level infrastructure inside a consuming app to make a workflow work — use the framework packages that already provide it.
|
|
208
|
+
- Large workflow functions doing plumbing instead of orchestration.
|
|
209
|
+
- Registering workflows in scattered places instead of the centralized `App` declaration.
|
|
162
210
|
|
|
163
211
|
## Output expectations when executing this skill
|
|
164
212
|
|
|
165
213
|
When asked to create a workflow, produce:
|
|
166
214
|
|
|
167
215
|
1. A new workflow file exporting a named `Workflow` constant.
|
|
168
|
-
2. Any required env
|
|
169
|
-
3. `App` registration update with the workflow included.
|
|
216
|
+
2. Any required env keys added to the shared `EnvDesign` in `constants/environments.ts`.
|
|
217
|
+
3. An `App` registration update with the workflow included.
|
|
170
218
|
4. No hardcoded client, secret, token, tenant, or URL values in workflow code.
|
|
@@ -2,171 +2,225 @@
|
|
|
2
2
|
name: framework-workflow-testing
|
|
3
3
|
description: >-
|
|
4
4
|
Build and maintain high-quality unit tests for T4H Framework workflows using
|
|
5
|
-
@t4h.framework/core/testing
|
|
6
|
-
workflow, add Vitest coverage for
|
|
7
|
-
validate logs, or simulate async
|
|
8
|
-
even when they only mention
|
|
5
|
+
@t4h.framework/core/testing and per-package testing entrypoints. Use this
|
|
6
|
+
whenever the user asks to test a workflow, add Vitest coverage for
|
|
7
|
+
History.reconciler flows, mock claims, validate logs, or simulate async
|
|
8
|
+
activity outcomes (fulfilled/rejected/timeout), even when they only mention
|
|
9
|
+
"workflow test", "mock claim", or "run workflow in memory".
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
# Workflow unit testing with `@t4h.framework/core/testing`
|
|
12
13
|
|
|
13
14
|
Use this skill to create or improve **workflow-focused unit tests** in T4H Framework projects.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
Harness APIs live in `@t4h.framework/core/testing`.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
- Clear assertions for behavior (calls, payload, order, and logs).
|
|
20
|
-
- Predictable handling of async activities with `mockAsyncActivity`.
|
|
18
|
+
Use `TestWorkflowEnvironment` as the primary test harness. It runs the workflow
|
|
19
|
+
in-memory and automatically validates replay determinism after every `execute()`.
|
|
21
20
|
|
|
22
|
-
##
|
|
21
|
+
## What this skill optimizes
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
- Fast in-memory workflow execution with `TestWorkflowEnvironment.execute()`.
|
|
24
|
+
- Typed claim mocks with `mockClaim` / `createMockClaims` (no manual casts).
|
|
25
|
+
- Predictable async activity mocking with `mockAsync()` builder.
|
|
26
|
+
- Clear assertions for behavior (calls, payload, order).
|
|
25
27
|
|
|
26
|
-
|
|
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.
|
|
28
|
+
## Public API
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
```ts
|
|
31
|
+
import {
|
|
32
|
+
createMockClaim,
|
|
33
|
+
createMockClaims,
|
|
34
|
+
createTestEnv,
|
|
35
|
+
createTestEnvEntries,
|
|
36
|
+
fulfilled,
|
|
37
|
+
mockAsync,
|
|
38
|
+
mockClaim,
|
|
39
|
+
NonDeterministicWorkflowError,
|
|
40
|
+
rejected,
|
|
41
|
+
TestClock,
|
|
42
|
+
TestWorkflowEnvironment,
|
|
43
|
+
timeout,
|
|
44
|
+
} from '@t4h.framework/core/testing'
|
|
45
|
+
```
|
|
33
46
|
|
|
34
|
-
|
|
47
|
+
### TestWorkflowEnvironment
|
|
35
48
|
|
|
36
49
|
```ts
|
|
37
|
-
|
|
50
|
+
const env = TestWorkflowEnvironment.create({ now: 0 })
|
|
51
|
+
// resolveActivityTimeouts defaults to true
|
|
52
|
+
|
|
53
|
+
const output = await env.execute(workflow, input, {
|
|
54
|
+
claims,
|
|
55
|
+
env: envEntries,
|
|
56
|
+
mockAsyncActivity,
|
|
57
|
+
})
|
|
58
|
+
// Replay determinism is validated automatically after each execute()
|
|
59
|
+
|
|
60
|
+
env.clock.now() // current pinned timestamp (ms)
|
|
61
|
+
env.clock.advance(ms) // advance pinned clock
|
|
62
|
+
env.clock.set(now) // set pinned clock to a Date | number
|
|
38
63
|
```
|
|
39
64
|
|
|
40
|
-
`
|
|
65
|
+
`TestWorkflowEnvironment.create()` options:
|
|
41
66
|
|
|
42
|
-
|
|
67
|
+
| Option | Default | Purpose |
|
|
68
|
+
|--------|---------|---------|
|
|
69
|
+
| `now` | `0` | Pins `Date.now()` to this value at start |
|
|
70
|
+
| `resolveActivityTimeouts` | `true` | Auto-advances clock to each activity's `timeoutAt` and resolves it via `onTimeout` |
|
|
43
71
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
`execute()` options:
|
|
73
|
+
|
|
74
|
+
| Option | Type | Purpose |
|
|
75
|
+
|--------|------|---------|
|
|
76
|
+
| `claims` | `readonly ClaimProvider[]` | Claim implementations |
|
|
77
|
+
| `env` | `Env.Entries` | String env vars |
|
|
78
|
+
| `mockAsyncActivity` | `MockAsyncActivity` | Required when `bootstrap` returns `pending` and `resolveActivityTimeouts` doesn't cover it |
|
|
79
|
+
|
|
80
|
+
After `execute()` returns, the clock is advanced to wherever the run left it.
|
|
81
|
+
Subsequent `env.clock.advance()` calls build on that value between tests.
|
|
82
|
+
|
|
83
|
+
Async activities are resolved in tests through `mockAsyncActivity` (which returns
|
|
84
|
+
a final outcome directly). Workflow tests never drive an activity's internal
|
|
85
|
+
`advance` method — you only assert the workflow's observable output and effects.
|
|
86
|
+
|
|
87
|
+
## Test file location
|
|
88
|
+
|
|
89
|
+
Place each test in a `__tests__` folder next to the file under test — not in a
|
|
90
|
+
separate top-level tests directory. For example, a test for `src/workflows/approval.ts`
|
|
91
|
+
lives at `src/workflows/__tests__/approval.test.ts`.
|
|
49
92
|
|
|
50
93
|
## Canonical workflow test pattern
|
|
51
94
|
|
|
52
|
-
|
|
95
|
+
1. Arrange claims with `mockClaim` or package mocks in helper functions.
|
|
96
|
+
2. Create `TestWorkflowEnvironment` in `beforeEach`.
|
|
97
|
+
3. Execute workflow with `env.execute()`.
|
|
98
|
+
4. Assert on output and mock calls.
|
|
99
|
+
|
|
100
|
+
### Mock the claims each activity uses
|
|
53
101
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
102
|
+
A workflow never talks to a claim directly — it calls a package primitive
|
|
103
|
+
(`http.get`, `smtp.send`, `Sftp`, `orm.find`, …) which runs an activity, and the
|
|
104
|
+
**activity** resolves the claims. So a workflow test provides one mock per claim
|
|
105
|
+
that the triggered activities depend on. Each package skill lists its claim(s)
|
|
106
|
+
(e.g. HTTP → `HttpClientRequestClaim` + `FileSystemClaim`; SFTP → `SftpClaim`;
|
|
107
|
+
ORM → `ORMClaim`; OAuth → also `CacheClaim`).
|
|
58
108
|
|
|
59
|
-
|
|
109
|
+
`mockClaim(ClaimClass, impl)` returns a `ClaimProvider`; put them in a plain array:
|
|
60
110
|
|
|
61
111
|
```ts
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
}
|
|
112
|
+
import { mockClaim } from '@t4h.framework/core/testing'
|
|
113
|
+
import { HttpClientRequestClaim } from '@t4h.framework/http'
|
|
114
|
+
import { FileSystemClaim } from '@t4h.framework/fs'
|
|
115
|
+
|
|
116
|
+
const mockRequest = vi.fn().mockImplementation(async () => ({
|
|
117
|
+
status: 200,
|
|
118
|
+
headers: { 'content-type': 'application/json' },
|
|
119
|
+
body: Readable.from(Buffer.from('{"ok":true}')),
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
const claims = [
|
|
123
|
+
mockClaim(HttpClientRequestClaim, { request: mockRequest }),
|
|
124
|
+
mockClaim(FileSystemClaim, { write: mockWrite, read: vi.fn() }),
|
|
125
|
+
]
|
|
71
126
|
```
|
|
72
127
|
|
|
73
|
-
|
|
128
|
+
`createMockClaims` is the same thing from a list of `[ClaimClass, impl]` tuples:
|
|
74
129
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
### 1) Create setup helpers
|
|
130
|
+
```ts
|
|
131
|
+
import { createMockClaims } from '@t4h.framework/core/testing'
|
|
78
132
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
133
|
+
const claims = createMockClaims([
|
|
134
|
+
[HttpClientRequestClaim, { request: mockRequest }],
|
|
135
|
+
[FileSystemClaim, { write: mockWrite, read: vi.fn() }],
|
|
136
|
+
])
|
|
137
|
+
```
|
|
82
138
|
|
|
83
|
-
|
|
139
|
+
Use `mockImplementation` (not `mockResolvedValue`) for mocks that return streams or
|
|
140
|
+
objects that can only be consumed once — replay runs the workflow a second time and
|
|
141
|
+
needs fresh values.
|
|
84
142
|
|
|
85
|
-
|
|
86
|
-
|
|
143
|
+
Unconfigured claim methods throw with a clear error — configure every method the
|
|
144
|
+
workflow may call.
|
|
87
145
|
|
|
88
|
-
|
|
146
|
+
## Async activities with `mockAsync()`
|
|
89
147
|
|
|
90
|
-
|
|
148
|
+
`mockAsync()` returns a builder that is also a valid `MockAsyncActivity`. Chain
|
|
149
|
+
`.on()` calls to declare how each activity type should resolve.
|
|
91
150
|
|
|
92
|
-
|
|
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])`).
|
|
151
|
+
### Outcome helpers
|
|
97
152
|
|
|
98
|
-
|
|
153
|
+
```ts
|
|
154
|
+
fulfilled(output?) // { status: 'fulfilled', output }
|
|
155
|
+
rejected(reason?) // { status: 'rejected', reason }
|
|
156
|
+
timeout() // { status: 'timeout' } — runs the activity's real onTimeout()
|
|
157
|
+
```
|
|
99
158
|
|
|
100
|
-
|
|
159
|
+
### Builder API
|
|
101
160
|
|
|
102
|
-
|
|
161
|
+
**Static outcome per type:**
|
|
103
162
|
|
|
104
163
|
```ts
|
|
105
|
-
mockAsyncActivity: (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
164
|
+
mockAsyncActivity: mockAsync()
|
|
165
|
+
.on(WaitForApprovalActivity, fulfilled(true))
|
|
166
|
+
.on(EmailActivity, fulfilled())
|
|
109
167
|
```
|
|
110
168
|
|
|
111
|
-
|
|
169
|
+
**Sequential outcomes per type** (per-type call count, not global):
|
|
112
170
|
|
|
113
171
|
```ts
|
|
114
|
-
mockAsyncActivity:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
172
|
+
mockAsyncActivity: mockAsync()
|
|
173
|
+
.on(WaitForApprovalActivity, [
|
|
174
|
+
rejected(new Error('transient')),
|
|
175
|
+
fulfilled(true),
|
|
176
|
+
])
|
|
118
177
|
```
|
|
119
178
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
- no resolver + pending async => throws;
|
|
123
|
-
- timeout outcome runs real `onTimeout`;
|
|
124
|
-
- exhausted queue => throws `No more async outcomes`.
|
|
179
|
+
**Callback with per-type `callCount`:**
|
|
125
180
|
|
|
126
|
-
|
|
181
|
+
```ts
|
|
182
|
+
mockAsyncActivity: mockAsync()
|
|
183
|
+
.on(WaitForApprovalActivity, (activity, { callCount }) =>
|
|
184
|
+
callCount === 0 ? rejected() : fulfilled(true)
|
|
185
|
+
)
|
|
186
|
+
```
|
|
127
187
|
|
|
128
|
-
|
|
188
|
+
**Auto-resolve timeout-based activities** (e.g. `SleepActivity`):
|
|
129
189
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
190
|
+
```ts
|
|
191
|
+
mockAsyncActivity: mockAsync()
|
|
192
|
+
.on(WaitForApprovalActivity, fulfilled(true))
|
|
193
|
+
.timeout() // resolves any remaining activity that has timeoutAt
|
|
194
|
+
```
|
|
134
195
|
|
|
135
|
-
|
|
196
|
+
**Fallback for unregistered types:**
|
|
136
197
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
198
|
+
```ts
|
|
199
|
+
mockAsyncActivity: mockAsync()
|
|
200
|
+
.on(WaitForApprovalActivity, fulfilled(true))
|
|
201
|
+
.fallback(() => timeout())
|
|
202
|
+
```
|
|
140
203
|
|
|
141
|
-
|
|
204
|
+
**Combine with `TestWorkflowEnvironment`** — `resolveActivityTimeouts: true` (the default)
|
|
205
|
+
handles all `timeoutAt`-based activities automatically; only provide `mockAsyncActivity`
|
|
206
|
+
when you need finer control:
|
|
142
207
|
|
|
143
208
|
```ts
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
})
|
|
209
|
+
const env = TestWorkflowEnvironment.create({ now: 0 })
|
|
210
|
+
|
|
211
|
+
await env.execute(workflow, undefined, {
|
|
212
|
+
claims,
|
|
213
|
+
mockAsyncActivity: mockAsync()
|
|
214
|
+
.on(WaitForApprovalActivity, fulfilled(true))
|
|
215
|
+
// SleepActivity resolves automatically via resolveActivityTimeouts default
|
|
162
216
|
})
|
|
163
217
|
```
|
|
164
218
|
|
|
165
|
-
## Completion checklist
|
|
219
|
+
## Completion checklist
|
|
166
220
|
|
|
167
|
-
- Test
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
221
|
+
- Test file lives in a `__tests__` folder next to the file under test.
|
|
222
|
+
- Harness imported from `@t4h.framework/core/testing`.
|
|
223
|
+
- Claims built with `mockClaim` / `createMockClaims` — avoid `as unknown as`.
|
|
224
|
+
- Pending async paths have `mockAsyncActivity` via `mockAsync()` builder.
|
|
225
|
+
- Mocks returning streams use `mockImplementation`, not `mockResolvedValue`.
|
|
226
|
+
- Assertions cover output, side effects, and expected ordering.
|