Skip to content

Commit 8253b43

Browse files
committed
Added ADR
1 parent 2469175 commit 8253b43

1 file changed

Lines changed: 289 additions & 0 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
# ADR-001: Deterministic Simulation Testing Architecture for Motoko
2+
3+
Date: 2026-06-27
4+
Status: Proposed
5+
6+
## Context
7+
8+
Motoko failures increasingly occur across boundaries rather than inside single pure functions. Recent compaction and harness regressions showed the same pattern:
9+
10+
- provider telemetry from one step influences the next loop decision
11+
- compaction is applied to the provider-call payload but not persisted to loop history
12+
- extension hooks can observe or rewrite the wrong slice of conversation state
13+
- child process sandboxing can make configured files unreadable
14+
- environment forwarding can silently disable runtime behavior
15+
16+
Real-provider tests are insufficient as the primary regression mechanism. They are expensive, slow, model-dependent, network-dependent, and difficult to reproduce. Unit tests are also insufficient when correctness depends on multi-step state transitions and process-boundary setup.
17+
18+
We need a deterministic test system that drives production transition code where practical, records boundary traces, and asserts structural invariants rather than final model prose.
19+
20+
This ADR is based on:
21+
22+
- `.agent/research/DST/motoko-dst-generalized-system.md`
23+
- `.agent/research/DST/deterministic-simulation-testing-for-agent-loop-compaction.md`
24+
- `.agent/plans/DST_v1_Motoko_Core.md`
25+
26+
## Decision
27+
28+
Build Motoko DST as a layered scenario-and-invariant system.
29+
30+
DST will mean:
31+
32+
1. Deterministically model external contracts around Motoko.
33+
2. Drive real production transition code where feasible.
34+
3. Record boundary observations into normalized traces.
35+
4. Assert reusable structural invariants over those traces.
36+
5. Report scenario id, seed, and trace on failure.
37+
38+
The system will use explicit scripted fakes and recorders rather than assuming mature AILANG effect-handler mocking. AILANG latest docs describe effect mocking as planned, while the local repo currently uses explicit stubs such as `run_v2_with_stub`.
39+
40+
## Layers
41+
42+
### Layer 0: Pure Policy DST
43+
44+
Tests pure helpers and policies:
45+
46+
- compaction thresholds
47+
- token estimates
48+
- message elision
49+
- parser helpers
50+
- cost arithmetic
51+
- path/config normalization
52+
53+
This layer can run through `ailang test` or `ailang run` smokes.
54+
55+
### Layer 1: Loop-State DST
56+
57+
Tests `loop_v2` state transitions with scripted provider, extension, and tool behavior.
58+
59+
The first load-bearing seam is provider-call recording in the existing scripted provider path. Compaction is ephemeral: compacted messages are sent to the provider, while the loop recurses on full uncompacted history. Therefore tests must inspect provider-call payloads, not just returned messages.
60+
61+
### Layer 2: Harness-Boundary DST
62+
63+
Tests TypeScript harness behavior before AILANG starts:
64+
65+
- child env preparation
66+
- sandbox-readable path materialization
67+
- profile mirroring
68+
- env allowlist/forwarding
69+
- spawn arguments
70+
71+
This layer catches bugs like external `SYSTEM_MD` being unreadable under `AILANG_FS_SANDBOX`.
72+
73+
### Layer 3: End-to-End Deterministic Harness DST
74+
75+
Composes the TypeScript harness with a deterministic child/runtime probe or scripted AILANG loop. It must not use real providers or live network as its oracle.
76+
77+
## Core Components
78+
79+
DST will use:
80+
81+
- Scenario fixtures with stable ids.
82+
- A normalized trace recorder.
83+
- Reusable invariant functions.
84+
- Scripted provider fakes returning `std/ai.StepResult`-shaped values.
85+
- Extension hook fakes that can record inputs and return `PassThrough` or `Compacted`.
86+
- Tool fakes for deterministic stdout/stderr/exit-code behavior.
87+
- Harness-boundary fixtures for temp workspaces, external files, and env layouts.
88+
- Seeded generators for fuzzing scenario parameters after fixed scenarios are stable.
89+
90+
Canonical trace events should include:
91+
92+
```text
93+
scenario_start
94+
harness_env_prepared
95+
child_spawn_prepared
96+
system_prompt_built
97+
extension_pre_step_called
98+
provider_call_prepared
99+
provider_result
100+
tool_policy_decision
101+
tool_result_appended
102+
loop_totals_updated
103+
scenario_end
104+
```
105+
106+
## Initial Scenario Families
107+
108+
### Compaction
109+
110+
The compaction scenarios prove actual-token behavior and system-message pinning.
111+
112+
Canonical ids:
113+
114+
- `compaction.actual_tokens_drive_next_step`
115+
- `compaction.system_messages_hidden_from_compactors`
116+
- `compaction.emergency_exhaustion_estimate_gated`
117+
- `compaction.actual_tokens_small_context_fail_open`
118+
- `compaction.provider_payload_vs_uncompacted_history_pressure`
119+
120+
Important invariants:
121+
122+
- provider-call payload contains the pinned system prefix
123+
- extension `on_pre_step` never receives system messages
124+
- `last_input_tokens` carries forward from provider result
125+
- actual-token path uses 60/75/85 tiers only when effective context is positive
126+
- estimate fallback uses 70/85/95 tiers when `last_input_tokens <= 0`
127+
- emergency entry is actual-token-gated, but exhaustion is estimate-gated
128+
- tool-call IDs and tool-result IDs survive elision
129+
130+
### Harness Prompt Materialization
131+
132+
The harness scenarios prove runtime inputs are readable inside the sandbox.
133+
134+
Canonical ids:
135+
136+
- `harness.external_system_md_materialized`
137+
- `harness.workspace_system_md_not_rewritten`
138+
- `harness.missing_system_md_fails_loudly`
139+
- `harness.system_md_forwarded_to_child_env`
140+
141+
Important invariants:
142+
143+
- child env `SYSTEM_MD` points to a sandbox-readable path
144+
- materialized prompt content equals the original external file
145+
- child env includes `AILANG_FS_SANDBOX=WORKDIR`
146+
- non-empty configured system prompts produce non-empty runtime prompt observations
147+
148+
## Preconditions
149+
150+
Do not add generalized DST on top of known-red tests.
151+
152+
Before Layer 1 or Layer 3 DST is enabled:
153+
154+
1. Hydrate AILANG registry dependencies in CI and local setup.
155+
2. Ensure every package imported by `src/core/ext/registry_generated.ail` is available.
156+
3. Fix or retire stale compaction tests that still assume no output headroom, especially tests using `test/tiny`.
157+
4. Update compaction source comments to document both tier tables.
158+
159+
## Implementation Plan
160+
161+
### Phase 0: Restore Test Preconditions
162+
163+
- Add dependency hydration to the test workflow.
164+
- Repair stale compaction tests around `context_limit - 75000`.
165+
- Keep current pure compaction checks green.
166+
167+
### Phase 1: Make Existing Boundaries Inspectable
168+
169+
Start from existing infrastructure:
170+
171+
- `src/core/test/stub_step.ail`
172+
- `src/core/agent_loop_v2.ail#run_v2_with_stub`
173+
- existing `scripts/smoke_v2_*.ail`
174+
- existing TypeScript runtime-process tests
175+
176+
Add provider-call recording to the scripted provider path. This is the first required seam.
177+
178+
### Phase 2: Add Scenario Runners
179+
180+
Initial files should stay small:
181+
182+
```text
183+
scripts/smoke_v2_compaction_actual_dst.ail
184+
src/tui/src/harness-dst.test.ts
185+
src/tui/src/runtime-process-env.test.ts
186+
```
187+
188+
Only introduce `src/core/test/dst/` after the smaller seams prove useful.
189+
190+
### Phase 3: Add Reusable Invariants
191+
192+
Implement common invariant helpers, including:
193+
194+
- `system_prompt_non_empty`
195+
- `system_prompt_stable`
196+
- `provider_calls_have_system_prefix`
197+
- `system_messages_not_sent_to_pre_step`
198+
- `tool_call_ids_preserved`
199+
- `actual_tokens_select_compaction_tier`
200+
- `provider_payload_compacted_but_history_uncompacted`
201+
- `emergency_compaction_exhaustion_uses_estimate`
202+
- `small_effective_context_fails_open`
203+
- `sandbox_paths_readable_by_child`
204+
205+
### Phase 4: Add Seeded Fuzzing
206+
207+
Add seeded generators around fixed scenarios:
208+
209+
- message history sizes
210+
- tool output sizes
211+
- tool-call argument sizes
212+
- token counts near thresholds
213+
- extension decisions
214+
- `SYSTEM_MD` path layouts
215+
- profile mirror layouts
216+
217+
Every failure must print scenario id, seed, first failed invariant, and normalized trace.
218+
219+
## CI Shape
220+
221+
Fast PR gate:
222+
223+
```bash
224+
ailang lock
225+
make test_core
226+
ailang run --caps IO --entry main scripts/smoke_v2_compaction_actual_dst.ail
227+
cd src/tui && bun test src/harness-dst.test.ts
228+
```
229+
230+
If `ailang lock` does not hydrate every registry dependency, CI must explicitly install missing packages before full-loop DST.
231+
232+
Expanded gate:
233+
234+
```bash
235+
ailang lock
236+
make test_dst
237+
```
238+
239+
Nightly:
240+
241+
```bash
242+
DST_SEEDS=500 make test_dst_seeded
243+
```
244+
245+
Provider smokes remain optional and supplemental.
246+
247+
## Consequences
248+
249+
Positive:
250+
251+
- Regressions become reproducible without Ollama/OpenRouter.
252+
- Known bug classes become reusable scenario families.
253+
- The provider-call payload becomes observable, closing the main loop-state test gap.
254+
- Harness-boundary failures become testable without a full agent run.
255+
- Fuzzing can explore scenario parameters without losing deterministic replay.
256+
257+
Negative:
258+
259+
- Some production-safe seams must be added for observability.
260+
- CI must reliably hydrate AILANG registry dependencies.
261+
- Tests will need care to avoid duplicating policy constants that should be exported from source.
262+
- Layer 1 and Layer 3 DST can be blocked by unrelated generated-registry imports until package hydration is fixed.
263+
264+
## Rejected Alternatives
265+
266+
### Real-provider E2E as the primary gate
267+
268+
Rejected because it is flaky, slow, expensive, and not deterministic.
269+
270+
### Pure helper tests only
271+
272+
Rejected because they cannot catch loop-state and harness-boundary failures.
273+
274+
### Extract a pure pre-provider helper first
275+
276+
Deferred. Extension `on_pre_step` carries broad effects, and the most important observation is the actual payload consumed by `dispatch_step`. Provider-call recording is the better first seam.
277+
278+
### Arbitrary black-box fuzzing
279+
280+
Rejected for v1. Fuzzing should be seeded, scenario-shaped, and invariant-driven.
281+
282+
## Open Questions
283+
284+
- Should compaction thresholds and output headroom be exported constants?
285+
- Should scenario fixtures be AILANG records, JSON files, TypeScript objects, or layer-specific?
286+
- Should traces be normalized JSONL everywhere, or should AILANG `std/trace` be used for Layer 1?
287+
- How should virtual time be enabled for `std/clock`, if needed?
288+
- Can provider-call recording be added without changing production provider behavior?
289+

0 commit comments

Comments
 (0)