@t4h.framework/core 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.ai/skills/framework-core/SKILL.md +103 -287
  2. package/.ai/skills/framework-workflow-creation/SKILL.md +108 -60
  3. package/.ai/skills/framework-workflow-testing/SKILL.md +165 -111
  4. package/README.md +123 -37
  5. package/dist/models/AsyncActivity.d.ts +1 -0
  6. package/dist/models/AsyncActivity.d.ts.map +1 -1
  7. package/dist/models/AsyncActivity.js +1 -0
  8. package/dist/models/AsyncActivity.js.map +1 -1
  9. package/dist/models/History.d.ts +1 -0
  10. package/dist/models/History.d.ts.map +1 -1
  11. package/dist/models/History.js +3 -0
  12. package/dist/models/History.js.map +1 -1
  13. package/dist/models/SyncActivity.d.ts +1 -0
  14. package/dist/models/SyncActivity.d.ts.map +1 -1
  15. package/dist/models/SyncActivity.js +1 -0
  16. package/dist/models/SyncActivity.js.map +1 -1
  17. package/dist/testing/ActivityTesting.d.ts +1 -1
  18. package/dist/testing/ActivityTesting.d.ts.map +1 -1
  19. package/dist/testing/ActivityTesting.js.map +1 -1
  20. package/dist/testing/TestClock.d.ts.map +1 -1
  21. package/dist/testing/TestClock.js +1 -2
  22. package/dist/testing/TestClock.js.map +1 -1
  23. package/dist/testing/TestWorkflowEnvironment.d.ts +6 -18
  24. package/dist/testing/TestWorkflowEnvironment.d.ts.map +1 -1
  25. package/dist/testing/TestWorkflowEnvironment.js +35 -93
  26. package/dist/testing/TestWorkflowEnvironment.js.map +1 -1
  27. package/dist/testing/WorkflowTesting.d.ts +1 -1
  28. package/dist/testing/async-activity-results.d.ts +12 -0
  29. package/dist/testing/async-activity-results.d.ts.map +1 -0
  30. package/dist/testing/async-activity-results.js +10 -0
  31. package/dist/testing/async-activity-results.js.map +1 -0
  32. package/dist/testing/create-mock-claim.d.ts +11 -5
  33. package/dist/testing/create-mock-claims.d.ts +12 -8
  34. package/dist/testing/create-test-env.d.ts +10 -4
  35. package/dist/testing/exceptions/NonDeterministicWorkflowError.d.ts +4 -0
  36. package/dist/testing/exceptions/NonDeterministicWorkflowError.d.ts.map +1 -0
  37. package/dist/testing/exceptions/NonDeterministicWorkflowError.js +7 -0
  38. package/dist/testing/exceptions/NonDeterministicWorkflowError.js.map +1 -0
  39. package/dist/testing/index.d.ts +8 -1
  40. package/dist/testing/index.d.ts.map +1 -1
  41. package/dist/testing/index.js +8 -1
  42. package/dist/testing/index.js.map +1 -1
  43. package/dist/testing/mock-async.d.ts +48 -0
  44. package/dist/testing/mock-async.d.ts.map +1 -0
  45. package/dist/testing/mock-async.js +58 -0
  46. package/dist/testing/mock-async.js.map +1 -0
  47. package/dist/testing/outcomes.d.ts +12 -0
  48. package/dist/testing/outcomes.d.ts.map +1 -0
  49. package/dist/testing/outcomes.js +10 -0
  50. package/dist/testing/outcomes.js.map +1 -0
  51. package/dist/testing/run-workflow.d.ts +22 -21
  52. package/dist/testing/run-workflow.d.ts.map +1 -1
  53. package/dist/testing/run-workflow.js +91 -33
  54. package/dist/testing/run-workflow.js.map +1 -1
  55. package/dist/testing/tools/create-mock-claim.d.ts +5 -0
  56. package/dist/testing/tools/create-mock-claim.d.ts.map +1 -0
  57. package/dist/testing/tools/create-mock-claim.js +26 -0
  58. package/dist/testing/tools/create-mock-claim.js.map +1 -0
  59. package/dist/testing/tools/create-mock-claims.d.ts +8 -0
  60. package/dist/testing/tools/create-mock-claims.d.ts.map +1 -0
  61. package/dist/testing/tools/create-mock-claims.js +5 -0
  62. package/dist/testing/tools/create-mock-claims.js.map +1 -0
  63. package/dist/testing/tools/create-test-env.d.ts +4 -0
  64. package/dist/testing/tools/create-test-env.d.ts.map +1 -0
  65. package/dist/testing/tools/create-test-env.js +7 -0
  66. package/dist/testing/tools/create-test-env.js.map +1 -0
  67. package/dist/testing/tools/mock-async.d.ts +20 -0
  68. package/dist/testing/tools/mock-async.d.ts.map +1 -0
  69. package/dist/testing/tools/mock-async.js +57 -0
  70. package/dist/testing/tools/mock-async.js.map +1 -0
  71. package/dist/testing/tools/resolve-activity.d.ts +11 -0
  72. package/dist/testing/tools/resolve-activity.d.ts.map +1 -0
  73. package/dist/testing/tools/resolve-activity.js +23 -0
  74. package/dist/testing/tools/resolve-activity.js.map +1 -0
  75. package/dist/testing/tools/resolve-async-activity.d.ts +15 -0
  76. package/dist/testing/tools/resolve-async-activity.d.ts.map +1 -0
  77. package/dist/testing/tools/resolve-async-activity.js +69 -0
  78. package/dist/testing/tools/resolve-async-activity.js.map +1 -0
  79. package/dist/testing/tools/run-with-clock.d.ts +3 -0
  80. package/dist/testing/tools/run-with-clock.d.ts.map +1 -0
  81. package/dist/testing/tools/run-with-clock.js +7 -0
  82. package/dist/testing/tools/run-with-clock.js.map +1 -0
  83. package/dist/testing/tools/run-workflow-ticks.d.ts +8 -0
  84. package/dist/testing/tools/run-workflow-ticks.d.ts.map +1 -0
  85. package/dist/testing/tools/run-workflow-ticks.js +26 -0
  86. package/dist/testing/tools/run-workflow-ticks.js.map +1 -0
  87. package/dist/testing/tools/run-workflow.d.ts +25 -0
  88. package/dist/testing/tools/run-workflow.d.ts.map +1 -0
  89. package/dist/testing/tools/run-workflow.js +25 -0
  90. package/dist/testing/tools/run-workflow.js.map +1 -0
  91. package/dist/testing/types.d.ts +1 -1
  92. package/dist/testing/types.d.ts.map +1 -1
  93. package/dist/testing/typings/AsyncActivityResult.d.ts +10 -0
  94. package/dist/testing/typings/AsyncActivityResult.d.ts.map +1 -0
  95. package/dist/testing/typings/AsyncActivityResult.js +2 -0
  96. package/dist/testing/typings/AsyncActivityResult.js.map +1 -0
  97. package/dist/testing/typings/ClaimConstructor.d.ts +2 -0
  98. package/dist/testing/typings/ClaimConstructor.d.ts.map +1 -0
  99. package/dist/testing/typings/ClaimConstructor.js +2 -0
  100. package/dist/testing/typings/ClaimConstructor.js.map +1 -0
  101. package/dist/testing/typings/MockAsyncActivity.d.ts +10 -0
  102. package/dist/testing/typings/MockAsyncActivity.d.ts.map +1 -0
  103. package/dist/testing/typings/MockAsyncActivity.js +2 -0
  104. package/dist/testing/typings/MockAsyncActivity.js.map +1 -0
  105. package/dist/tools/Internals.d.ts +1 -0
  106. package/dist/tools/Internals.d.ts.map +1 -1
  107. package/dist/tools/Internals.js +1 -0
  108. package/dist/tools/Internals.js.map +1 -1
  109. package/package.json +3 -2
  110. package/dist/testing/auto-resolve-timeouts.d.ts +0 -3
  111. package/dist/testing/auto-resolve-timeouts.d.ts.map +0 -1
  112. package/dist/testing/auto-resolve-timeouts.js +0 -16
  113. package/dist/testing/auto-resolve-timeouts.js.map +0 -1
  114. package/dist/testing/mock-async-by-activity.d.ts +0 -10
  115. package/dist/testing/mock-async-by-activity.d.ts.map +0 -1
  116. package/dist/testing/mock-async-by-activity.js +0 -15
  117. package/dist/testing/mock-async-by-activity.js.map +0 -1
  118. package/dist/testing/resolve-async-activities-in-order.d.ts +0 -3
  119. package/dist/testing/resolve-async-activities-in-order.d.ts.map +0 -1
  120. package/dist/testing/resolve-async-activities-in-order.js +0 -10
  121. package/dist/testing/resolve-async-activities-in-order.js.map +0 -1
  122. package/dist/tools/runWorkflowToCompletion.d.ts +0 -15
  123. package/dist/tools/runWorkflowToCompletion.d.ts.map +0 -1
  124. package/dist/tools/runWorkflowToCompletion.js +0 -61
  125. package/dist/tools/runWorkflowToCompletion.js.map +0 -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 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.
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 workflow modules that are easy to register, test, and maintain.
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 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
+ - 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
- // orchestration only
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
- import { env } from '@t4h.framework/core'
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
- export const SERVICE_URL = env.SERVICE_URL!
56
- if (!SERVICE_URL) throw new TypeError('SERVICE_URL is not defined')
85
+ ```ts
86
+ import { environments } from '../constants/environments.js'
87
+ // environments.SERVICE_URL -> string, environments.MAX_RETRIES -> number
57
88
  ```
58
89
 
59
- Keep this in a dedicated constants file and import values into workflows/activities.
90
+ Why `EnvDesign` and not raw `env`:
60
91
 
61
- ## Core APIs to prefer while authoring workflows
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
- - `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).
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. 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.
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` directly in workflow modules when the project uses centralized constants via `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
- If a required env key is missing, fail fast with a clear `TypeError` in the constants module.
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
- - Add required keys in a constants module.
90
- - Export each key from `env.<KEY>!`.
91
- - Validate each key immediately with an explicit `TypeError`.
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 required orchestration helpers.
96
- - Import only typed/safe constants (never raw secrets inline).
97
- - Implement the function body as orchestration steps.
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 in App
130
+ ### Step 3: wire it into the App
100
131
 
101
- - Import the workflow module in `main.tsx`.
132
+ - Import the workflow in `main.tsx`.
102
133
  - Add it to `workflows: [...]` in `new App({...})`.
103
- - Keep `App` declaration as default export for runtime loading.
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 where possible.
138
+ - Keep outputs deterministic.
108
139
  - Avoid hidden global state.
109
- - Structure side effects so they can be mocked by claim/activity tests.
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
- return { accepted: true, customerId: input.customerId }
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
- ### B) Workflow orchestrating activities
174
+ ### C) Workflow with a delay
132
175
 
133
176
  ```ts
134
- import { History, Workflow } from '@t4h.framework/core'
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 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 }
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
- ### C) Nested workflow orchestration
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://..."` 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.
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 constants in the central constants module.
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. 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".
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
- ## What this skill optimizes
16
+ Harness APIs live in `@t4h.framework/core/testing`.
16
17
 
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`.
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
- ## When to choose this skill
21
+ ## What this skill optimizes
23
22
 
24
- Choose this skill when the user asks to:
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
- - 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.
28
+ ## Public API
31
29
 
32
- If the request is specifically about testing `AsyncActivity.advance`, use `Internals.runActivity` directly instead of only `WorkflowTesting.run`.
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
- ## Public API available
47
+ ### TestWorkflowEnvironment
35
48
 
36
49
  ```ts
37
- import { WorkflowTesting } from '@t4h.framework/core/testing'
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
- `WorkflowTesting.run(workflow, input?, options?)`
65
+ `TestWorkflowEnvironment.create()` options:
41
66
 
42
- Supported options:
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
- - `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`).
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
- Follow this structure by default:
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
- 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.
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
- Base mock style on this pattern:
109
+ `mockClaim(ClaimClass, impl)` returns a `ClaimProvider`; put them in a plain array:
60
110
 
61
111
  ```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
- }
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
- Why this pattern: it keeps tests close to real wiring while still allowing strict call inspection.
128
+ `createMockClaims` is the same thing from a list of `[ClaimClass, impl]` tuples:
74
129
 
75
- ## Standard recipe for new tests
76
-
77
- ### 1) Create setup helpers
130
+ ```ts
131
+ import { createMockClaims } from '@t4h.framework/core/testing'
78
132
 
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.
133
+ const claims = createMockClaims([
134
+ [HttpClientRequestClaim, { request: mockRequest }],
135
+ [FileSystemClaim, { write: mockWrite, read: vi.fn() }],
136
+ ])
137
+ ```
82
138
 
83
- ### 2) Execute workflow under test
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
- - Use `await WorkflowTesting.run(workflow, input, { claims, logs, ... })`.
86
- - Capture logs with `const logs: Log[] = []` when log behavior matters.
143
+ Unconfigured claim methods throw with a clear error — configure every method the
144
+ workflow may call.
87
145
 
88
- ### 3) Assert behavior and contract
146
+ ## Async activities with `mockAsync()`
89
147
 
90
- Prefer these assertions:
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
- - 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])`).
151
+ ### Outcome helpers
97
152
 
98
- ### 4) Cover async outcomes when applicable
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
- If any async activity can return `pending`, provide `mockAsyncActivity`.
159
+ ### Builder API
101
160
 
102
- Function strategy:
161
+ **Static outcome per type:**
103
162
 
104
163
  ```ts
105
- mockAsyncActivity: (activity, { index, pending }) => {
106
- if (index === 0) return { status: 'fulfilled', output: true }
107
- return { status: 'rejected', reason: 'denied' }
108
- }
164
+ mockAsyncActivity: mockAsync()
165
+ .on(WaitForApprovalActivity, fulfilled(true))
166
+ .on(EmailActivity, fulfilled())
109
167
  ```
110
168
 
111
- Queue strategy:
169
+ **Sequential outcomes per type** (per-type call count, not global):
112
170
 
113
171
  ```ts
114
- mockAsyncActivity: [
115
- { status: 'fulfilled', output: true },
116
- { status: 'timeout' },
117
- ]
172
+ mockAsyncActivity: mockAsync()
173
+ .on(WaitForApprovalActivity, [
174
+ rejected(new Error('transient')),
175
+ fulfilled(true),
176
+ ])
118
177
  ```
119
178
 
120
- Important behavior:
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
- ## Mocking guidelines (do this, avoid that)
181
+ ```ts
182
+ mockAsyncActivity: mockAsync()
183
+ .on(WaitForApprovalActivity, (activity, { callCount }) =>
184
+ callCount === 0 ? rejected() : fulfilled(true)
185
+ )
186
+ ```
127
187
 
128
- Do:
188
+ **Auto-resolve timeout-based activities** (e.g. `SleepActivity`):
129
189
 
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`.
190
+ ```ts
191
+ mockAsyncActivity: mockAsync()
192
+ .on(WaitForApprovalActivity, fulfilled(true))
193
+ .timeout() // resolves any remaining activity that has timeoutAt
194
+ ```
134
195
 
135
- Avoid:
196
+ **Fallback for unregistered types:**
136
197
 
137
- - over-mocking framework internals;
138
- - asserting implementation details unrelated to behavior;
139
- - using `WorkflowTesting.run` to validate real `advance` logic.
198
+ ```ts
199
+ mockAsyncActivity: mockAsync()
200
+ .on(WaitForApprovalActivity, fulfilled(true))
201
+ .fallback(() => timeout())
202
+ ```
140
203
 
141
- ## Minimal template to generate quickly
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
- 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
- })
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 before finishing
219
+ ## Completion checklist
166
220
 
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.
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.