@temporal-architect/claude-plugin 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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/package.json +37 -0
  4. package/skills/MANIFEST.json +373 -0
  5. package/skills/MANIFEST.md +121 -0
  6. package/skills/temporal-architect/SKILL.md +99 -0
  7. package/skills/temporal-architect/reference/decomposition.md +78 -0
  8. package/skills/temporal-architect-author-go/README.md +16 -0
  9. package/skills/temporal-architect-author-go/SKILL.md +191 -0
  10. package/skills/temporal-architect-author-go/SUBAGENT_ADOPTION.md +161 -0
  11. package/skills/temporal-architect-author-go/reference/activity-call.md +73 -0
  12. package/skills/temporal-architect-author-go/reference/activity-def.md +54 -0
  13. package/skills/temporal-architect-author-go/reference/assignment.md +36 -0
  14. package/skills/temporal-architect-author-go/reference/await-all.md +104 -0
  15. package/skills/temporal-architect-author-go/reference/await-one.md +193 -0
  16. package/skills/temporal-architect-author-go/reference/await-timer.md +35 -0
  17. package/skills/temporal-architect-author-go/reference/close.md +71 -0
  18. package/skills/temporal-architect-author-go/reference/composite-patterns.md +176 -0
  19. package/skills/temporal-architect-author-go/reference/condition.md +56 -0
  20. package/skills/temporal-architect-author-go/reference/control-flow.md +151 -0
  21. package/skills/temporal-architect-author-go/reference/dependency-resolution.md +29 -0
  22. package/skills/temporal-architect-author-go/reference/detach.md +52 -0
  23. package/skills/temporal-architect-author-go/reference/heartbeat.md +84 -0
  24. package/skills/temporal-architect-author-go/reference/nexus-service-def.md +73 -0
  25. package/skills/temporal-architect-author-go/reference/nexus.md +35 -0
  26. package/skills/temporal-architect-author-go/reference/options.md +138 -0
  27. package/skills/temporal-architect-author-go/reference/promise.md +73 -0
  28. package/skills/temporal-architect-author-go/reference/proto-driven.md +197 -0
  29. package/skills/temporal-architect-author-go/reference/query-handler.md +34 -0
  30. package/skills/temporal-architect-author-go/reference/signal-handler.md +73 -0
  31. package/skills/temporal-architect-author-go/reference/three-layer-testing.md +173 -0
  32. package/skills/temporal-architect-author-go/reference/types.md +72 -0
  33. package/skills/temporal-architect-author-go/reference/update-handler.md +64 -0
  34. package/skills/temporal-architect-author-go/reference/worker.md +215 -0
  35. package/skills/temporal-architect-author-go/reference/workflow-call.md +37 -0
  36. package/skills/temporal-architect-author-go/reference/workflow-def.md +45 -0
  37. package/skills/temporal-architect-author-infra/README.md +16 -0
  38. package/skills/temporal-architect-author-infra/SKILL.md +132 -0
  39. package/skills/temporal-architect-author-infra/reference/tcld.md +112 -0
  40. package/skills/temporal-architect-author-infra/reference/terraform.md +125 -0
  41. package/skills/temporal-architect-design/README.md +16 -0
  42. package/skills/temporal-architect-design/SKILL.md +224 -0
  43. package/skills/temporal-architect-design/reference/LANGUAGE.md +5 -0
  44. package/skills/temporal-architect-design/reference/anti-patterns.md +332 -0
  45. package/skills/temporal-architect-design/reference/common-errors.md +88 -0
  46. package/skills/temporal-architect-design/reference/core-principles.md +52 -0
  47. package/skills/temporal-architect-design/reference/design-checklist.md +59 -0
  48. package/skills/temporal-architect-design/reference/namespaces.md +84 -0
  49. package/skills/temporal-architect-design/reference/notation-examples.md +304 -0
  50. package/skills/temporal-architect-design/reference/notation-reference.md +70 -0
  51. package/skills/temporal-architect-design/reference/primitives-reference.md +65 -0
  52. package/skills/temporal-architect-design/reference/project-discovery-subagent.md +80 -0
  53. package/skills/temporal-architect-design/reference/reverse-engineering.md +53 -0
  54. package/skills/temporal-architect-design/reference/twf-conventions.md +43 -0
  55. package/skills/temporal-architect-design/reference/workflow-boundaries.md +43 -0
  56. package/skills/temporal-architect-design/topics/activities-advanced.md +358 -0
  57. package/skills/temporal-architect-design/topics/activities-advanced.twf +107 -0
  58. package/skills/temporal-architect-design/topics/child-workflows.md +347 -0
  59. package/skills/temporal-architect-design/topics/child-workflows.twf +171 -0
  60. package/skills/temporal-architect-design/topics/long-running.md +230 -0
  61. package/skills/temporal-architect-design/topics/long-running.twf +100 -0
  62. package/skills/temporal-architect-design/topics/nexus.md +248 -0
  63. package/skills/temporal-architect-design/topics/nexus.twf +148 -0
  64. package/skills/temporal-architect-design/topics/patterns.md +469 -0
  65. package/skills/temporal-architect-design/topics/patterns.twf +346 -0
  66. package/skills/temporal-architect-design/topics/promises-conditions.md +179 -0
  67. package/skills/temporal-architect-design/topics/promises-conditions.twf +213 -0
  68. package/skills/temporal-architect-design/topics/signals-queries-updates.md +319 -0
  69. package/skills/temporal-architect-design/topics/signals-queries-updates.twf +234 -0
  70. package/skills/temporal-architect-design/topics/task-queues.md +205 -0
  71. package/skills/temporal-architect-design/topics/task-queues.twf +184 -0
  72. package/skills/temporal-architect-design/topics/testing.md +437 -0
  73. package/skills/temporal-architect-design/topics/testing.twf +177 -0
  74. package/skills/temporal-architect-design/topics/timers-scheduling.md +131 -0
  75. package/skills/temporal-architect-design/topics/timers-scheduling.twf +129 -0
  76. package/skills/temporal-architect-design/topics/versioning.md +434 -0
  77. package/skills/temporal-architect-design/topics/versioning.twf +174 -0
@@ -0,0 +1,73 @@
1
+ # promise
2
+
3
+ ## DSL
4
+
5
+ ```twf
6
+ promise handleA <- activity ProcessA(items.a)
7
+ # ... do other work ...
8
+ await handleA -> resultA
9
+ ```
10
+
11
+ ## Go
12
+
13
+ ```go
14
+ futureA := workflow.ExecuteActivity(ctx, ProcessA, items.A)
15
+ // ... do other work ...
16
+ var resultA ResultA
17
+ err := futureA.Get(ctx, &resultA)
18
+ if err != nil {
19
+ return Result{}, err
20
+ }
21
+ ```
22
+
23
+ ## Variants
24
+
25
+ ```twf
26
+ promise childHandle <- workflow SlowChild(input.data)
27
+ ```
28
+
29
+ ```go
30
+ childFuture := workflow.ExecuteChildWorkflow(ctx, SlowChild, input.Data)
31
+ ```
32
+
33
+ ```twf
34
+ promise timeout <- timer(5m)
35
+ ```
36
+
37
+ ```go
38
+ timerFuture := workflow.NewTimer(ctx, 5*time.Minute)
39
+ ```
40
+
41
+ ```twf
42
+ promise approved <- signal Approved
43
+ ```
44
+
45
+ ```go
46
+ approvedCh := workflow.GetSignalChannel(ctx, "Approved")
47
+ // Use approvedCh.Receive later, or add to selector
48
+ ```
49
+
50
+ ```twf
51
+ promise payHandle <- nexus BillingEndpoint BillingService.ChargePayment(payment)
52
+ ```
53
+
54
+ ```go
55
+ c := workflow.NewNexusClient("BillingEndpoint", "BillingService")
56
+ payFuture := c.ExecuteOperation(ctx, "ChargePayment", payment, workflow.NexusOperationOptions{})
57
+ ```
58
+
59
+ ## Notes
60
+
61
+ - A promise is just a future — the call starts immediately, `.Get` defers the blocking
62
+ - Activity/workflow promises → `workflow.Future` from `ExecuteActivity`/`ExecuteChildWorkflow`
63
+ - Timer promises → `workflow.Future` from `workflow.NewTimer`
64
+ - Signal promises → `workflow.ReceiveChannel` from `workflow.GetSignalChannel` — **not a `Future`**. Use `.Receive()` to block or add to a selector with `AddReceive`. Do not call `.Get()` on a channel
65
+ - Nexus promises → `NexusOperationFuture` from `NexusClient.ExecuteOperation`; same `.Get()` pattern as activity/workflow futures. Also has `GetNexusOperationExecution()` to optionally wait for the operation to start (not finish)
66
+ - Updates are handler-driven, not future-driven — they don't produce futures directly. To race an update completion, use a channel set by the update handler (see [update-handler.md](./update-handler.md))
67
+ - Promises used in `await one:` are added as selector cases — see [await-one.md](./await-one.md)
68
+
69
+ ## When to use
70
+
71
+ - Use a promise (deferred `.Get`) when other work can proceed before the result is needed — the call starts immediately and runs concurrently with subsequent workflow code
72
+ - Use an inline blocking call (`.Get()` immediately) when the result is needed before any further work. This is the simpler pattern and should be the default
73
+ - Promises are essential for `await all:` (parallel fan-out) and `await one:` (racing) patterns
@@ -0,0 +1,197 @@
1
+ # proto-driven
2
+
3
+ Codegen variant where **protobuf is the single source of truth** for workflow and activity interfaces. Generators produce the Temporal framework — typed interfaces, registration helpers, activity futures, Nexus stubs — leaving only business logic hand-written. Route here when the [Orient](../SKILL.md#orient) signals fire (`buf.gen.yaml` → `protoc-gen-go_temporal`, generated `*_temporal.pb.go`, `(temporal.v1.activity|workflow)` annotations).
4
+
5
+ Sometimes called **PFI** (proto-first interfaces). Detect it from those repo signals, never from the name.
6
+
7
+ In an existing proto-first repo, the layout, tooling, and naming are **requirements to match** — conform to what discovery found; do not impose this skeleton over a different one.
8
+
9
+ ---
10
+
11
+ ## Contract + layout
12
+
13
+ Three directories, one direction of flow: `proto/` (source of truth) → `gen/` (generated, never edit) → `lib/` (hand-written).
14
+
15
+ ```
16
+ <project>/
17
+ ├── proto/<service>/<version>/<service>.proto # interface definitions (source of truth)
18
+ ├── gen/<service>/<version>/
19
+ │ ├── *.pb.go # generated: message types
20
+ │ └── *_temporal.pb.go # generated: interfaces, registration helpers, futures
21
+ └── lib/<service>/<version>/
22
+ ├── activities.go # hand-written: implements generated Activities interface
23
+ ├── workflows.go # hand-written: implements generated Workflows interface (if any)
24
+ ├── client.go # hand-written: external system client
25
+ └── fx.go # hand-written: dependency injection wiring
26
+ ```
27
+
28
+ Everything under `gen/` is generated — never hand-edit it.
29
+
30
+ ---
31
+
32
+ ## Tools
33
+
34
+ [buf](https://buf.build) drives generation; plugins are installed from `go.mod` so versions are pinned to the module:
35
+
36
+ ```bash
37
+ go install google.golang.org/protobuf/cmd/protoc-gen-go
38
+ go install github.com/alta/protopatch/cmd/protoc-gen-go-patch
39
+ go install github.com/cludden/protoc-gen-go-temporal/cmd/protoc-gen-go_temporal
40
+ # Optional: Nexus support
41
+ go install github.com/bergundy/protoc-gen-go-nexus/cmd/protoc-gen-go-nexus
42
+ go install github.com/bergundy/protoc-gen-go-nexus-temporal/cmd/protoc-gen-go-nexus-temporal
43
+ ```
44
+
45
+ These must be on `$PATH` when `buf generate` runs.
46
+
47
+ ### `buf.yaml` skeleton
48
+
49
+ ```yaml
50
+ version: v2
51
+ modules:
52
+ - path: proto/<service>
53
+ deps:
54
+ - buf.build/cludden/protoc-gen-go-temporal # Temporal annotations
55
+ - buf.build/alta/protopatch # struct tag patching
56
+ lint:
57
+ use:
58
+ - STANDARD
59
+ except:
60
+ # Temporal uses XxxInput/XxxOutput, not XxxRequest/XxxResponse
61
+ - RPC_REQUEST_STANDARD_NAME
62
+ - RPC_RESPONSE_STANDARD_NAME
63
+ - RPC_REQUEST_RESPONSE_UNIQUE
64
+ breaking:
65
+ use:
66
+ - FILE
67
+ ```
68
+
69
+ ### `buf.gen.yaml` skeleton
70
+
71
+ ```yaml
72
+ version: v2
73
+ managed:
74
+ enabled: true
75
+ plugins:
76
+ - local: protoc-gen-go-patch # Go message types (with struct tag patching)
77
+ out: gen/<service>
78
+ opt: [paths=source_relative, plugin=go]
79
+ - local: protoc-gen-go_temporal # Temporal workflow/activity stubs and interfaces
80
+ out: gen/<service>
81
+ opt: [paths=source_relative, enable-patch-support=true]
82
+ strategy: all
83
+ inputs:
84
+ - directory: proto/<service>
85
+ ```
86
+
87
+ Run generation with `buf generate`.
88
+
89
+ ---
90
+
91
+ ## Proto annotations
92
+
93
+ ```proto
94
+ service MyService {
95
+ option (temporal.v1.service) = {task_queue: "my-task-queue"};
96
+
97
+ rpc CreateThing(CreateThingInput) returns (CreateThingOutput) {
98
+ option (temporal.v1.activity) = {
99
+ name: "myservice.v1.CreateThing"
100
+ schedule_to_close_timeout: {seconds: 300}
101
+ };
102
+ }
103
+
104
+ rpc DeployCluster(DeployClusterInput) returns (DeployClusterOutput) {
105
+ option (temporal.v1.workflow) = {
106
+ name: "myservice.v1.DeployCluster"
107
+ id: "deploy-cluster-{{.Input.ClusterName}}"
108
+ };
109
+ }
110
+ }
111
+
112
+ message CreateThingInput {
113
+ string name = 1 [(go.field).tags = 'json:"Name" validate:"required"'];
114
+ }
115
+ ```
116
+
117
+ - `(temporal.v1.service)` sets the task queue for all RPCs in the service.
118
+ - `(temporal.v1.activity)` / `(temporal.v1.workflow)` mark an RPC; set a unique `name` and timeouts. Workflows and activities coexist in one service.
119
+ - `(go.field).tags` injects struct tags on the generated Go type (requires `protopatch`).
120
+ - Use `XxxInput` / `XxxOutput` naming — the Temporal ecosystem expects these, not `Request`/`Response`.
121
+
122
+ ---
123
+
124
+ ## Generated-symbol table
125
+
126
+ For each annotated service, `protoc-gen-go_temporal` emits these in `*_temporal.pb.go`. This is the Rosetta Stone — the same table is read **backward** during [reverse engineering](../../temporal-architect-design/reference/reverse-engineering.md) to recover intent from generated code.
127
+
128
+ | Generated symbol | Purpose |
129
+ |---|---|
130
+ | `XxxActivities` interface | implement with your business logic |
131
+ | `RegisterXxxActivities(worker, impl)` | register all activities at once |
132
+ | `XxxActivityName` constant | activity name string for Temporal |
133
+ | `XxxFuture` struct | typed future for async activity calls from workflows |
134
+ | `XxxWorkflows` interface | implement for workflows (if RPCs are annotated as workflows) |
135
+ | `RegisterXxxWorkflows(worker, impl)` | register all workflows |
136
+ | `XxxClient` struct | call activities/workflows from outside Temporal |
137
+
138
+ You never write these — only the implementations.
139
+
140
+ ---
141
+
142
+ ## Implement, register, client
143
+
144
+ **Implement** the generated interface; each method is validate → call → return:
145
+
146
+ ```go
147
+ // activities.go
148
+ type Activities struct {
149
+ client MyServiceClient // hand-written external system client
150
+ }
151
+
152
+ func (a *Activities) CreateThing(ctx context.Context, req *pb.CreateThingInput) (*pb.CreateThingOutput, error) {
153
+ if req.Name == "" { // 1. validate
154
+ return nil, errors.New("name is required")
155
+ }
156
+ id, err := a.client.Create(ctx, req.Name) // 2. call external system
157
+ if err != nil {
158
+ return nil, fmt.Errorf("create thing: %w", err)
159
+ }
160
+ return &pb.CreateThingOutput{Id: id}, nil // 3. return typed output
161
+ }
162
+ ```
163
+
164
+ **Register** the generated helper at worker startup (commonly via `fx`):
165
+
166
+ ```go
167
+ // fx.go
168
+ var Module = fx.Options(
169
+ fx.Provide(NewMyServiceClient),
170
+ fx.Provide(func(c MyServiceClient) pb.MyServiceActivities { return NewActivities(c) }),
171
+ )
172
+
173
+ func RegisterActivities(w worker.Worker, a pb.MyServiceActivities) {
174
+ pb.RegisterMyServiceActivities(w, a) // generated; missing registration → "activity not found" at runtime
175
+ }
176
+ ```
177
+
178
+ **Client interface** — define a hand-written interface for the external system in the implementation package so activities depend on the interface, not the concrete type (and it can be mocked in tests):
179
+
180
+ ```go
181
+ // client.go
182
+ type MyServiceClient interface {
183
+ Create(ctx context.Context, name string) (id string, err error)
184
+ Delete(ctx context.Context, id string) error
185
+ }
186
+ ```
187
+
188
+ For the testing seam on these generated and hand-written interfaces, see [three-layer-testing.md](./three-layer-testing.md).
189
+
190
+ ---
191
+
192
+ ## References
193
+
194
+ - [protoc-gen-go-temporal](https://github.com/cludden/protoc-gen-go-temporal) — the core Temporal code generator
195
+ - [protopatch](https://github.com/alta/protopatch) — struct tag injection for Go proto types
196
+ - [buf](https://buf.build/docs) — proto dependency management and generation pipeline
197
+ - [three-layer-testing.md](./three-layer-testing.md) — testing strategy; the generated activities interface is the proto seam for mocks
@@ -0,0 +1,34 @@
1
+ # query handler
2
+
3
+ ## DSL
4
+
5
+ ```twf
6
+ query GetStatus() -> (string):
7
+ return status
8
+ ```
9
+
10
+ ## Go
11
+
12
+ ```go
13
+ err := workflow.SetQueryHandler(ctx, "GetStatus", func() (string, error) {
14
+ return status, nil
15
+ })
16
+ if err != nil {
17
+ return Result{}, err
18
+ }
19
+ ```
20
+
21
+ ## Notes
22
+
23
+ - Query handlers always return `(ReturnType, error)`
24
+ - Query handlers must not modify workflow state — read-only
25
+ - Query handlers have no `workflow.Context` parameter in their signature
26
+ - With params: `func(param1 Type1, param2 Type2) (ReturnType, error)`
27
+ - Non-deterministic operations (e.g., ranging over a map) are permitted in query handlers — they execute outside the replay path
28
+ - Register queries at the very start of the workflow function, before any blocking calls. If a query arrives before the handler is registered during replay, the caller receives an `unknown queryType` error
29
+
30
+ ## Pitfalls
31
+
32
+ - **No blocking operations.** Query handlers must not call `workflow.Go()`, `workflow.NewChannel()`, `Channel.Get()`, `Future.Get()`, or `workflow.Await()`. Violation causes a panic caught by the SDK, returning `QueryFailedError` to the caller
33
+ - **No activities or commands.** Query handlers cannot execute activities, schedule timers, start child workflows, or produce any workflow command. This also produces `QueryFailedError`
34
+ - These restrictions exist because queries are dispatched outside the normal workflow execution context — they cannot participate in the event history
@@ -0,0 +1,73 @@
1
+ # signal handler
2
+
3
+ ## DSL
4
+
5
+ ```twf
6
+ workflow OrderWorkflow(orderId: string) -> (OrderResult):
7
+ signal PaymentReceived(transactionId: string, amount: decimal):
8
+ status = "payment_received"
9
+ lastTransactionId = transactionId
10
+
11
+ # ... body uses await signal PaymentReceived or await one with signal case
12
+ ```
13
+
14
+ ## Go
15
+
16
+ ```go
17
+ func OrderWorkflow(ctx workflow.Context, orderId string) (OrderResult, error) {
18
+ var status string
19
+ var lastTransactionId string
20
+
21
+ // Signal struct
22
+ type PaymentReceivedSignal struct {
23
+ TransactionId string
24
+ Amount float64
25
+ }
26
+
27
+ // Signal channel
28
+ paymentReceivedCh := workflow.GetSignalChannel(ctx, "PaymentReceived")
29
+
30
+ // Register handler via goroutine that loops on the channel
31
+ workflow.Go(ctx, func(gCtx workflow.Context) {
32
+ for {
33
+ var sig PaymentReceivedSignal
34
+ paymentReceivedCh.Receive(gCtx, &sig)
35
+ status = "payment_received"
36
+ lastTransactionId = sig.TransactionId
37
+ }
38
+ })
39
+
40
+ // ... workflow body
41
+ }
42
+ ```
43
+
44
+ ## Notes
45
+
46
+ - Signal params become a struct; signal name becomes the channel name string
47
+ - The handler goroutine loops forever — it processes every signal arrival, not just the first
48
+ - Handler body mutates workflow-scoped variables (closures over workflow state)
49
+ - Signals with no params: use `paymentReceivedCh.Receive(gCtx, nil)`
50
+ - When a signal is also used in `await one:`, the selector reads from the same channel — see [await-one.md](./await-one.md)
51
+ - `decimal` type in DSL maps to `float64` by default. For financial values where precision matters, consider `shopspring/decimal` or integer-cents representation instead
52
+
53
+ ## When to use each pattern
54
+
55
+ - **Goroutine loop** (`workflow.Go` + `Receive` in a loop, as shown above): use when every signal must be processed as it arrives, independent of the main workflow flow. Most common pattern
56
+ - **Blocking `Receive` inline**: use when the workflow must pause and wait for exactly one signal before continuing. Simpler but blocks the main flow
57
+ - **`Selector.AddReceive`**: use when racing a signal against other events (timers, activities, other signals) — see [await-one.md](./await-one.md)
58
+
59
+ ## Pitfalls
60
+
61
+ - **Signal drain before Continue-As-New (Go SDK only).** Signals are lost if the channel is not drained before `workflow.NewContinueAsNewError`. Drain with `ReceiveAsync` immediately before CAN:
62
+ ```go
63
+ for {
64
+ var sig SignalType
65
+ if !signalCh.ReceiveAsync(&sig) {
66
+ break
67
+ }
68
+ // process or forward to next run via workflow input
69
+ }
70
+ return workflow.NewContinueAsNewError(ctx, MyWorkflow, state)
71
+ ```
72
+ - When using a `Selector` for signal handling, drain with `selector.HasPending()` + `selector.Select(ctx)` loop before CAN
73
+ - Do not trigger Continue-As-New from inside a signal handler — call it from the main workflow thread to avoid signal loss
@@ -0,0 +1,173 @@
1
+ # three-layer-testing
2
+
3
+ Temporal Go code divides into three testable layers. **Each layer mocks only its direct dependency** — never anything deeper. This is good practice for any clean Go Temporal project, not just proto-driven ones.
4
+
5
+ | Layer | File | Mocks | Build tag | Speed |
6
+ |---|---|---|---|---|
7
+ | Workflows | `workflows_test.go` | activities interface | *(none)* | fast, no Docker |
8
+ | Activities | `activities_test.go` | client interface | *(none)* | fast, no Docker |
9
+ | Clients | `client_test.go` | nothing (real system) | `integration` | slow, needs Docker |
10
+
11
+ **Why three layers:** workflow tests check orchestration (right activities, right order, failures propagate) but can't see bugs inside activities; activity tests check validation and error handling but can't see bugs in the real client; client tests check the real protocol/error codes that mocks would hide. Each layer catches bugs the others cannot.
12
+
13
+ ---
14
+
15
+ ## Layer 1: Workflow tests
16
+
17
+ Use `testsuite.WorkflowTestSuite`; mock the activities interface so the workflow can be driven to any outcome without real systems. A fresh environment per table case:
18
+
19
+ ```go
20
+ func TestProcessOrderWorkflow(t *testing.T) {
21
+ tests := []struct {
22
+ name string
23
+ createErr error
24
+ wantErr bool
25
+ }{
26
+ {name: "success - order processed", wantErr: false},
27
+ {name: "error - charge fails", createErr: errors.New("declined"), wantErr: true},
28
+ }
29
+ for _, tt := range tests {
30
+ t.Run(tt.name, func(t *testing.T) {
31
+ env := (&testsuite.WorkflowTestSuite{}).NewTestWorkflowEnvironment()
32
+ mockActivities := mocks.NewMockActivities(t)
33
+ env.RegisterActivity(mockActivities)
34
+
35
+ // Conditional mock: only set expectations on the path the case reaches.
36
+ mockActivities.EXPECT().
37
+ ChargePayment(mock.Anything, mock.Anything).
38
+ Return(&pb.ChargeOutput{Id: "ch-1"}, tt.createErr)
39
+
40
+ env.ExecuteWorkflow(ProcessOrder, &pb.OrderInput{})
41
+
42
+ err := env.GetWorkflowError()
43
+ if tt.wantErr {
44
+ require.Error(t, err)
45
+ return
46
+ }
47
+ require.NoError(t, err)
48
+ })
49
+ }
50
+ }
51
+ ```
52
+
53
+ **Split validation tests from execution tests.** A constructor that validates input is tested directly, with no workflow environment — those run in milliseconds:
54
+
55
+ ```go
56
+ func TestNewProcessOrder(t *testing.T) {
57
+ _, err := newProcessOrder(&pb.OrderInput{}) // empty → invalid
58
+ require.ErrorContains(t, err, "order_id")
59
+ }
60
+ ```
61
+
62
+ Workflow tests also guard **replay safety**: Temporal replays from event history, so if changed code executes activities in a different order, replay breaks. These tests pin the activity-call sequence for given inputs.
63
+
64
+ ---
65
+
66
+ ## Layer 2: Activity tests
67
+
68
+ Mock the client interface. Cover, in order: validation errors (caught before any client call), success, then each client error type.
69
+
70
+ ```go
71
+ func TestCreateThingActivity(t *testing.T) {
72
+ tests := []struct {
73
+ name string
74
+ input *pb.CreateThingInput
75
+ clientErr error
76
+ wantErr bool
77
+ }{
78
+ {name: "error - empty name", input: &pb.CreateThingInput{}, wantErr: true}, // validation
79
+ {name: "success", input: &pb.CreateThingInput{Name: "x"}, wantErr: false}, // success
80
+ {name: "error - already exists", input: &pb.CreateThingInput{Name: "x"}, clientErr: errExists, wantErr: true}, // client error
81
+ }
82
+ for _, tt := range tests {
83
+ t.Run(tt.name, func(t *testing.T) {
84
+ env := (&testsuite.WorkflowTestSuite{}).NewTestActivityEnvironment()
85
+ mockClient := mocks.NewMockClient(t)
86
+
87
+ // Conditional mock: only expect a client call if validation would pass.
88
+ if tt.input.Name != "" {
89
+ mockClient.EXPECT().Create(mock.Anything, tt.input.Name).Return("id-123", tt.clientErr)
90
+ }
91
+
92
+ acts := NewActivities(mockClient)
93
+ env.RegisterActivity(acts.CreateThing)
94
+ _, err := env.ExecuteActivity(acts.CreateThing, tt.input)
95
+
96
+ if tt.wantErr {
97
+ require.Error(t, err)
98
+ return
99
+ }
100
+ require.NoError(t, err)
101
+ })
102
+ }
103
+ }
104
+ ```
105
+
106
+ **Conditional mocks** are the key pattern: when input is invalid, set no expectations — mockery fails the test if the client is called anyway, which proves the validation layer works.
107
+
108
+ ---
109
+
110
+ ## Layer 3: Client tests (integration)
111
+
112
+ Real system in a container via [testcontainers-go](https://golang.testcontainers.org), gated by a build tag so normal `go test ./...` skips them:
113
+
114
+ ```go
115
+ //go:build integration
116
+
117
+ func TestMyServiceClient(t *testing.T) {
118
+ if testing.Short() {
119
+ t.Skip("skipping integration test in short mode")
120
+ }
121
+ ctx := context.Background()
122
+ container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
123
+ ContainerRequest: testcontainers.ContainerRequest{
124
+ Image: "myservice:latest",
125
+ ExposedPorts: []string{"8080/tcp"},
126
+ WaitingFor: wait.ForHTTP("/health").WithPort("8080/tcp"),
127
+ },
128
+ Started: true,
129
+ })
130
+ require.NoError(t, err)
131
+ defer container.Terminate(ctx)
132
+
133
+ host, _ := container.Host(ctx)
134
+ port, _ := container.MappedPort(ctx, "8080")
135
+ client := NewMyServiceClient(fmt.Sprintf("http://%s:%s", host, port.Port()))
136
+
137
+ id, err := client.Create(ctx, "my-thing")
138
+ require.NoError(t, err)
139
+ require.NotEmpty(t, id)
140
+ }
141
+ ```
142
+
143
+ - The `//go:build integration` tag keeps these out of normal runs; include with `go test -tags=integration ./...`.
144
+ - Start a fresh container per test function; always `defer container.Terminate(ctx)`.
145
+ - Assert against the client's **real error types** (`ErrNotFound`), not test sentinels — this verifies the client translates system error codes correctly.
146
+
147
+ ---
148
+
149
+ ## Mock generation (the proto seam)
150
+
151
+ Hand-written interfaces can be mocked by hand or with a generator. When the project is **proto-driven**, the activities interface is generated (`XxxActivities` — see [proto-driven.md](./proto-driven.md)), so its mock must be generated too. [mockery](https://vektra.github.io/mockery/) generates both kinds from a `.mockery.yaml`:
152
+
153
+ ```yaml
154
+ with-expecter: true
155
+ packages:
156
+ github.com/myorg/myproject/gen/myservice/v1: # generated activities interface → workflow tests
157
+ interfaces:
158
+ MyServiceActivities:
159
+ github.com/myorg/myproject/lib/myservice/v1: # hand-written client interface → activity tests
160
+ interfaces:
161
+ MyServiceClient:
162
+ ```
163
+
164
+ Generated mocks are an **option within** testing — the one proto seam — not a requirement of the three-layer approach.
165
+
166
+ ---
167
+
168
+ ## Key principles
169
+
170
+ - **Only mock your direct dependency.** Workflow → activities; activity → client. Never reach two layers deep (a workflow must not know about HTTP).
171
+ - **Conditional mocks mirror code flow.** No expectation set for inputs that validation rejects.
172
+ - **Fresh setup per case.** Never share `env` or mock state across table-driven cases.
173
+ - **Test names are documentation.** `"error - empty role name"`, not `"test1"`.
@@ -0,0 +1,72 @@
1
+ # types
2
+
3
+ ## Strategy
4
+
5
+ Types in generated code come from two sources. Each has a different resolution method.
6
+
7
+ **Types you define** — derived from the `.twf` file. You control the shape; the TWF tells you what fields exist.
8
+
9
+ **Dependency types** — from external packages. The shape is fixed; you discover it at the call site.
10
+
11
+ ### Defined types
12
+
13
+ Resolve in priority order:
14
+
15
+ 1. **Explicit signatures** — workflow/activity params and return types name the type directly
16
+ 2. **Constructor usage** — `Result{field: value}` reveals struct fields and their types (inferred from the values assigned)
17
+ 3. **Field access** — `order.items` implies `Order` has an `items` field; the accessed type propagates from usage context
18
+ 4. **Generate** — only for application-specific types with no existing match
19
+
20
+ ### Dependency types
21
+
22
+ The ground truth is the call site — the method you will call in the activity body.
23
+
24
+ 1. **Identify the method** — which function or method on the dependency will the activity call?
25
+ 2. **Read the method signature** — `go doc <package>.Method` gives you the exact parameter and return types. If `go doc` is unavailable (dependency not yet in `go.mod`, docs sparse, or offline), fall back to: read source on pkg.go.dev, inspect the dependency source directly, or ask the user
26
+ 3. **Read each parameter type** — `go doc <package>.ParamType` gives you the fields you need to populate. Verify every field type the same way — the field name alone can be misleading (e.g., a `Tools` field may accept a union wrapper type, not the type you'd guess from the name)
27
+ 4. **Follow the chain until you reach primitives or types you recognize** — stop when every type in the call is verified
28
+
29
+ Dependency APIs are version-specific — verify types against the version in `go.mod`, not trained knowledge. A dependency is resolved when you can write the full call expression with concrete types:
30
+ ```
31
+ client.Messages.New(ctx, anthropic.MessageNewParams{
32
+ Model: anthropic.Model(model), // verified: Model is a string typedef
33
+ Messages: []anthropic.MessageParam{}, // verified: not []Message
34
+ Tools: []anthropic.ToolUnionParam{}, // verified: not []ToolParam
35
+ })
36
+ ```
37
+
38
+ ### Serialization boundaries
39
+
40
+ Workflow and activity parameters pass through Temporal's data converter (JSON by default). Types you define are safe — you control their fields and they round-trip cleanly. Dependency types may have custom marshaling, unexported fields, or non-JSON-safe constructs.
41
+
42
+ **Keep dependency types inside activity bodies.** Expose your own types in workflow/activity signatures and convert to dependency types within the activity implementation. This keeps the serialization boundary clean and decouples your workflow logic from any single dependency.
43
+
44
+ ## Go
45
+
46
+ ```go
47
+ // From explicit signature: activity ValidateOrder(order: Order) -> (ValidateResult)
48
+ type Order struct { /* fields derived from usage */ }
49
+ type ValidateResult struct { /* fields derived from constructor */ }
50
+
51
+ // From constructor: Result{status: "completed", trackingId: reservation.trackingId}
52
+ type Result struct {
53
+ Status string
54
+ TrackingId string
55
+ }
56
+
57
+ // Primitive mapping
58
+ // string → string
59
+ // int → int
60
+ // decimal → float64
61
+ // bool → bool
62
+ // []Type → []Type
63
+ // duration → time.Duration (e.g., 5m → 5*time.Minute)
64
+ // time → time.Time
65
+ ```
66
+
67
+ ## Notes
68
+
69
+ - Collect all constructor sites and field accesses across the `.twf` file before defining a type — a type used in multiple places may reveal different fields in each
70
+ - When a field's type is ambiguous (e.g., assigned from an untyped expression), leave a `// TODO` comment and ask the user
71
+ - Generic containers: `Map[K]V` → `map[K]V`, `[]Item` → `[]Item`
72
+ - Export all struct fields (uppercase) — these cross workflow/activity boundaries via serialization