@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,23 +1,32 @@
1
1
  ---
2
2
  name: framework-core
3
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.
4
+ Build, register, and test T4H Framework workflows with @t4h.framework/core:
5
+ the Workflow and App author API, env/EnvDesign configuration, nested workflows
6
+ via startWorkflow, Serializable/Authorization manifests, and the in-memory
7
+ TestWorkflowEnvironment. Use when importing @t4h.framework/core to create a
8
+ Workflow or App, wire env, orchestrate nested workflows, or test workflows
9
+ with @t4h.framework/core/testing. Core composes with the other framework
10
+ packages (@t4h.framework/http, @t4h.framework/sleep, …) that do the real work.
11
11
  ---
12
12
 
13
13
  # @t4h.framework/core
14
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.
15
+ Core is the **orchestration layer** of the T4H Framework. A workflow is a deterministic,
16
+ replay-safe async function you register in an **App**. This package gives you the author
17
+ API (`Workflow`, `App`, `env`, `startWorkflow`) and an in-memory test harness. The actual
18
+ side effects — HTTP requests, delays, storage — come from the companion packages you
19
+ compose inside the workflow body.
20
+
21
+ > **Building a workflow?** You only need the primitives on this page plus the package(s)
22
+ > that do the real work — `@t4h.framework/http` for HTTP, `@t4h.framework/sleep` for
23
+ > delays, and so on. You do **not** implement any of the framework's internal building
24
+ > blocks to write a workflow.
16
25
 
17
26
  | Import | Purpose |
18
27
  |--------|---------|
19
- | `@t4h.framework/core` | Workflows, activities, claims, app, env, internals |
20
- | `@t4h.framework/core/testing` | `WorkflowTesting` — tests/local only |
28
+ | `@t4h.framework/core` | `Workflow`, `App`, `env`, `EnvDesign`, `startWorkflow`, `Serializable`/`Binary`, `Authorization` |
29
+ | `@t4h.framework/core/testing` | `TestWorkflowEnvironment` — tests/local only |
21
30
 
22
31
  Node `>=22`. Schemas use TypeBox (`typebox`). See [README.md](../../../README.md) for diagrams.
23
32
 
@@ -25,12 +34,11 @@ Node `>=22`. Schemas use TypeBox (`typebox`). See [README.md](../../../README.md
25
34
 
26
35
  ## Concepts
27
36
 
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()`.
37
+ - **Workflow** — an orchestration entrypoint: a plain async function wrapped in `new Workflow(...)`. It coordinates steps; it does not implement the steps.
38
+ - **App** — registers workflows so the runtime can discover and run them.
39
+ - **Replay & determinism** — the runtime may re-run a workflow from its recorded history. Keep the body deterministic: branch on inputs and on the results of the steps you call, never on `Date.now()`, `Math.random()`, or ambient state. Use framework primitives (e.g. `sleep`) when you need time to pass.
40
+ - **env** — configuration values supplied by the runtime; the single source for URLs, tokens, and tenants.
41
+ - **Composition** — external work is performed by calling primitives from the other framework packages inside the workflow body.
34
42
 
35
43
  ---
36
44
 
@@ -38,7 +46,7 @@ Async helpers on `AsyncActivity`: `this.pending()`, `this.done()`, `this.rejecte
38
46
 
39
47
  ```typescript
40
48
  import Type from 'typebox'
41
- import { Workflow, env } from '@t4h.framework/core'
49
+ import { Workflow } from '@t4h.framework/core'
42
50
 
43
51
  const bare = new Workflow(async () => ({ ok: true }))
44
52
 
@@ -53,141 +61,26 @@ const validated = new Workflow({ id: 'cashback/v1', schema }, async input => ({
53
61
  }))
54
62
  ```
55
63
 
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`.
64
+ - `new Workflow(fn)` or `new Workflow(options, fn)` — a bad overload throws `Invalid constructor arguments`.
65
+ - `WorkflowInput<T>` — derived from `schema`; `undefined` when no schema is set.
66
+ - `workflow.validate` — an Ajv validator when `schema` is set; otherwise `null`.
67
+ - Optional `authorization` on a workflow or app surfaces in `toManifest()`.
178
68
 
179
69
  ---
180
70
 
181
71
  ## Nested workflows
182
72
 
183
73
  ```typescript
184
- import { startWorkflow, WorkflowClaim } from '@t4h.framework/core'
74
+ import { Workflow, startWorkflow } from '@t4h.framework/core'
75
+ import { childWorkflow } from './child-workflow.js'
185
76
 
186
- const ref = await startWorkflow(childWorkflow, { ...input, parallel?: boolean })
187
- // WorkflowRef: status 'fulfilled' | 'rejected', output Serializable
77
+ export const parent = new Workflow({ id: 'parent' }, async input => {
78
+ const ref = await startWorkflow(childWorkflow, { ...input, parallel: false })
79
+ return ref.output // WorkflowRef: status 'fulfilled' | 'rejected', output Serializable
80
+ })
188
81
  ```
189
82
 
190
- `StartWorkflowActivity`: `parallel: true` → immediate `done`; else `pending` until `advance`. Requires `WorkflowClaim` provider.
83
+ `parallel: true` returns as soon as the child starts; otherwise the parent waits for the child to settle.
191
84
 
192
85
  ---
193
86
 
@@ -204,200 +97,123 @@ const app = new App({
204
97
  app.addWorkflow(anotherWorkflow) // throws on duplicate workflow.id
205
98
  ```
206
99
 
207
- `toManifest()` includes workflows + registered `EnvDesign` entries.
100
+ `toManifest()` includes the registered workflows plus any `EnvDesign` entries.
208
101
 
209
102
  ---
210
103
 
211
104
  ## Environment
212
105
 
213
- ```typescript
214
- import { env, EnvDesign } from '@t4h.framework/core'
106
+ **Always configure environment access through `EnvDesign`** — not the raw `env` proxy. `EnvDesign` gives you typed, validated, self-documenting configuration that also feeds the app manifest automatically. Reach for raw `env` only for a one-off, dynamic key that can't be declared ahead of time.
215
107
 
216
- // Under Env.run — string values
217
- console.log(env.API_URL)
108
+ ### The standard: one `constants/environments.ts` with the `EnvDesign`
109
+
110
+ Declare **every** env var the project needs in a single `EnvDesign` and export it. Import that one object anywhere you need configuration.
111
+
112
+ ```typescript
113
+ // src/constants/environments.ts
114
+ import { EnvDesign } from '@t4h.framework/core'
218
115
 
219
- // Typed parsing (registers for app manifest)
220
- const config = EnvDesign({
116
+ export const environments = EnvDesign({
221
117
  API_URL: { type: String },
222
118
  RETRIES: { type: Number, defaultValue: '3' },
223
119
  DEBUG: { type: Boolean, isOptional: true },
120
+ // custom parser: raw string -> your type
121
+ REAJUSTE_INDICES: { type: (raw: string) => JSON.parse(raw) as Record<string, number> },
224
122
  })
225
123
  ```
226
124
 
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
125
  ```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:
126
+ // anywhere — a workflow, a helper, etc.
127
+ import { environments } from '../constants/environments.js'
260
128
 
261
- ```typescript
262
- import { WorkflowTesting } from '@t4h.framework/core/testing'
129
+ const res = await http.get(`${environments.API_URL}/status`) // string
130
+ if (environments.RETRIES > 0) { /* number, parsed */ }
263
131
  ```
264
132
 
265
- `package.json` maps `"./testing"` → `dist/testing/index.js`. The public API is **`WorkflowTesting.run`** only.
133
+ **Why this is safe and preferred:**
266
134
 
267
- ### `WorkflowTesting.run`
135
+ - `EnvDesign({...})` does **not** read `env` at construction — only when you access a key (`environments.API_URL`). So the top-level `export const environments = EnvDesign(...)` is safe at import time; the read + parse + validation happen lazily, inside a running workflow.
136
+ - Access **keys inside the workflow body / functions**, never at module top level — reading a key outside a run throws (`env` has no storage until the runtime executes the workflow).
137
+ - Each key is parsed to its declared `type` (`String` | `Number` | `Boolean` | a custom `(raw: string) => T` parser), `defaultValue` fills in when unset, and `isOptional: true` allows `undefined` instead of throwing.
138
+ - A required key that is unset throws `Environment variable X is not set` on access — fail-fast, no silent `undefined`.
139
+ - Declaring via `EnvDesign` registers the entry in the **app manifest** (`app.toManifest().env`) automatically — the runtime knows exactly which vars the app requires.
268
140
 
269
- ```typescript
270
- const output = await WorkflowTesting.run(workflow, input?, options?)
271
- ```
141
+ Never inline URLs, secrets, tokens, or tenants in workflow code; never read `process.env` directly.
272
142
 
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 |
143
+ ---
280
144
 
281
- Typed input follows `WorkflowInput<T>` when the workflow has a `schema`; omit `input` for workflows without one.
145
+ ## Serializable & Authorization
282
146
 
283
- ### How the driver works
147
+ **Serializable:** primitives, `null`, `undefined`, `Binary`, `SerializableClass`, and arrays/objects thereof. Workflow input, output, and every value that crosses a step boundary must be `Serializable`. `Binary`: `contentType`, `contentLength`, `stream()`.
284
148
 
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.
149
+ **Authorization:** subclass with `toManifest(): AuthorizationManifest` (`pathToModule`, optional `context`); attach to a `Workflow` or `App`.
290
150
 
291
- This drives sync activities end-to-end and async activities through bootstrap + mocked resolution — **not** through real `advance`.
151
+ ---
292
152
 
293
- ### Wiring claims in tests
153
+ ## Testing (`@t4h.framework/core/testing`)
294
154
 
295
- Pass explicit `{ provide, value }` providers — same shape as `Internals.runWorkflow`:
155
+ Test-only entrypoint — **not** part of the production runtime contract. `package.json` maps `"./testing"` → `dist/testing/index.js`.
296
156
 
297
157
  ```typescript
298
- import { WorkflowTesting } from '@t4h.framework/core/testing'
299
- import { History, type Log } from '@t4h.framework/core'
158
+ import { TestWorkflowEnvironment } from '@t4h.framework/core/testing'
300
159
 
301
- const logs: Log[] = []
302
- const output = await WorkflowTesting.run(workflow, input, {
160
+ const env = TestWorkflowEnvironment.create({ now: 0 })
161
+ const output = await env.execute(workflow, input, {
303
162
  env: { API_URL: 'https://example.com' },
304
- claims: [{ provide: UserRepositoryClaim, value: mockRepo }],
305
- mockAsyncActivity: () => ({ status: 'fulfilled', output: true }),
306
- logs,
163
+ claims: [/* provider implementations required by the packages your workflow uses */],
307
164
  })
308
165
  ```
309
166
 
310
- For a single activity, prefer `Claim.run([...], fn)` or `Internals.runActivity`. Use `WorkflowTesting.run` for full workflow integration tests.
167
+ `create()` options:
311
168
 
312
- ### `mockAsyncActivity`
169
+ | Option | Default | Purpose |
170
+ |--------|---------|---------|
171
+ | `now` | `0` | Pins `Date.now()` to this value at run start |
172
+ | `resolveActivityTimeouts` | `true` | Auto-advances the clock through time-based steps (e.g. `sleep`) and resolves them |
313
173
 
314
- Required when `bootstrap` returns `pending`:
174
+ `execute()` options:
315
175
 
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) |
176
+ | Option | Type | Purpose |
177
+ |--------|------|---------|
178
+ | `env` | `Env.Entries` | String env vars for the run |
179
+ | `claims` | `readonly ClaimProvider[]` | Provider implementations required by the packages the workflow calls (see each package's skill) |
180
+ | `mockAsyncActivity` | `MockAsyncActivity` | Controls the outcome of a time-/event-based step when auto-resolution doesn't cover it |
321
181
 
322
- **Array** — FIFO, one entry per pending async:
182
+ `execute()` returns the workflow output directly, then advances `env.clock` to reflect time consumed.
323
183
 
324
- ```typescript
325
- mockAsyncActivity: [
326
- { status: 'fulfilled', output: true },
327
- { status: 'rejected', reason: false },
328
- ]
329
- ```
184
+ **Replay validation is automatic.** After each `execute()`, the harness re-runs the workflow from the recorded history and from a fresh replay; a mismatch throws `NonDeterministicWorkflowError`.
330
185
 
331
- **Function** — `(activity, { input, pending, index }) => outcome`:
186
+ Clock helpers:
332
187
 
333
188
  ```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
- }
189
+ env.clock.now() // current pinned timestamp (ms)
190
+ env.clock.advance(ms) // advance the pinned clock
191
+ env.clock.set(now) // set the pinned clock to a Date | number
340
192
  ```
341
193
 
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`.
194
+ > Wiring specific providers or controlling a specific step's outcome (e.g. an HTTP response or a `sleep` timeout) is documented in that package's skill — **framework-http**, **framework-sleep**, etc. `@t4h.framework/core/testing` exposes helpers those skills reference; you don't wire them by hand here.
343
195
 
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.
196
+ Do **not** import `@t4h.framework/core/testing` in production app code.
385
197
 
386
198
  ---
387
199
 
388
200
  ## Anti-patterns
389
201
 
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`.
202
+ - Reading `process.env` directly, or reading the raw `env` proxy when the project should declare an `EnvDesign` in `constants/environments.ts`.
203
+ - Inlining URLs/secrets/tenants in workflow code instead of pulling them from the shared `EnvDesign`.
204
+ - Accessing an `EnvDesign` key at module top level (throws — no env storage outside a run); read keys inside the workflow body/functions.
205
+ - Non-deterministic workflow bodies (`Date.now()`, `Math.random()`, ambient state) — they break replay.
206
+ - Non-`Serializable` workflow input, output, or step values.
207
+ - Duplicate `workflow.id` within an `App`.
208
+ - Importing `@t4h.framework/core/testing` from production code.
209
+ - Building low-level infrastructure inside a workflow instead of composing the framework packages that already provide it.
398
210
 
399
211
  ---
400
212
 
401
- ## Exports (main)
213
+ ## Exports
214
+
215
+ **Main (`@t4h.framework/core`):** `Workflow`, `App`, `startWorkflow`, `WorkflowRef`, `env`, `EnvDesign`, `Authorization`, `Binary`, `Serializable`.
216
+
217
+ **Testing (`@t4h.framework/core/testing`):** `TestWorkflowEnvironment`, `TestClock`, `createTestEnv`, `createTestEnvEntries`, `NonDeterministicWorkflowError`.
402
218
 
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.
219
+ > Core also exports lower-level building blocks used to **author framework packages** themselves. Those are out of scope for building workflows and are not covered here — reach for the primitives above and the companion packages instead.