|
1 | 1 | --- |
2 | | -description: Test AG-UI components with provideFakeAgent(), the neutral mockAgent(), or a scripted AbstractAgent, and know which double covers which surface. |
| 2 | +description: Test AG-UI components with provideFakeAgent() and its event script, the neutral mockAgent(), or a hand-written AbstractAgent, and know which double covers which surface. |
3 | 3 | --- |
4 | 4 |
|
5 | 5 | # Testing |
@@ -53,13 +53,14 @@ export class ChatHost { |
53 | 53 | </Tab> |
54 | 54 | </Tabs> |
55 | 55 |
|
56 | | -The component is byte-identical to production — only the provider changes. `FakeAgentConfig` (`{ tokens?, reasoningTokens?, delayMs? }`) lives in `@threadplane/chat/testing`: |
| 56 | +The component is byte-identical to production — only the provider changes. `provideFakeAgent()` accepts `AgUiFakeAgentConfig`: the shared `FakeAgentConfig` from `@threadplane/chat/testing` plus the AG-UI-only `script`. |
57 | 57 |
|
58 | 58 | | Option | Type | Notes | |
59 | 59 | | --- | --- | --- | |
60 | 60 | | `tokens` | `string[]` | Emitted as streamed text deltas, in order. | |
61 | 61 | | `reasoningTokens` | `string[]` | Emitted before text deltas to exercise reasoning UI. | |
62 | 62 | | `delayMs` | `number` | Delay between streamed events. | |
| 63 | +| `script` | `FakeAgentScript` | Raw AG-UI event branches that replace the canned reply. See [Testing tool calls, state, and custom events](#testing-tool-calls-state-and-custom-events). | |
63 | 64 |
|
64 | 65 | For the underlying `FakeAgent` class and its canned event sequence, see [Fake Agent](/docs/ag-ui/guides/fake-agent). |
65 | 66 |
|
@@ -109,7 +110,64 @@ expect(m.status()).toBe('running'); |
109 | 110 |
|
110 | 111 | ## Testing tool calls, state, and custom events |
111 | 112 |
|
112 | | -`provideFakeAgent()` only produces `RUN_*`, `REASONING_MESSAGE_*`, and `TEXT_MESSAGE_*` events — it never emits `TOOL_CALL_*`, `STATE_SNAPSHOT`/`STATE_DELTA`, or `CUSTOM`. To exercise the reducer's headline non-text features — tool-call rendering, shared state, citations, custom events — either pass a `script` of raw events to a directly-constructed `FakeAgent`, or script your own `AbstractAgent` and feed it through `toAgent()`. The adapter reduces your scripted events into `toolCalls()`, `state()`, and `customEvents()` exactly as it would real wire events. |
| 113 | +By default `provideFakeAgent()` produces only `RUN_*`, `REASONING_MESSAGE_*`, and `TEXT_MESSAGE_*` events. To exercise the reducer's headline non-text features — tool-call rendering, shared state, citations, custom events, interrupts — pass a `script` of raw events. It is part of the config `provideFakeAgent()` accepts, so the whole surface stays reachable through DI: |
| 114 | + |
| 115 | +```typescript |
| 116 | +import { describe, it, expect } from 'vitest'; |
| 117 | +import { TestBed } from '@angular/core/testing'; |
| 118 | +import { EventType, type BaseEvent } from '@ag-ui/client'; |
| 119 | +import { provideFakeAgent, injectAgent } from '@threadplane/ag-ui'; |
| 120 | + |
| 121 | +describe('scripted provideFakeAgent()', () => { |
| 122 | + it('reduces tool calls, state, and custom events', async () => { |
| 123 | + TestBed.configureTestingModule({ |
| 124 | + providers: [ |
| 125 | + provideFakeAgent({ |
| 126 | + delayMs: 0, |
| 127 | + script: [ |
| 128 | + { |
| 129 | + when: 'initial', |
| 130 | + events: [ |
| 131 | + { |
| 132 | + type: EventType.TOOL_CALL_START, |
| 133 | + toolCallId: 'search-1', |
| 134 | + toolCallName: 'search', |
| 135 | + } as BaseEvent, |
| 136 | + { |
| 137 | + type: EventType.TOOL_CALL_ARGS, |
| 138 | + toolCallId: 'search-1', |
| 139 | + delta: '{"q":"Angular"}', |
| 140 | + } as BaseEvent, |
| 141 | + { type: EventType.TOOL_CALL_END, toolCallId: 'search-1' } as BaseEvent, |
| 142 | + { |
| 143 | + type: EventType.STATE_SNAPSHOT, |
| 144 | + snapshot: { topic: 'billing' }, |
| 145 | + } as BaseEvent, |
| 146 | + { |
| 147 | + type: EventType.CUSTOM, |
| 148 | + name: 'analysis_progress', |
| 149 | + value: { pct: 100 }, |
| 150 | + } as BaseEvent, |
| 151 | + ], |
| 152 | + }, |
| 153 | + ], |
| 154 | + }), |
| 155 | + ], |
| 156 | + }); |
| 157 | + |
| 158 | + const agent = TestBed.runInInjectionContext(() => injectAgent()); |
| 159 | + await agent.submit({ message: 'find docs' }); |
| 160 | + |
| 161 | + expect(agent.toolCalls()[0]).toMatchObject({ name: 'search', args: { q: 'Angular' } }); |
| 162 | + expect(agent.state()).toMatchObject({ topic: 'billing' }); |
| 163 | + expect(agent.customEvents()).toContainEqual({ name: 'analysis_progress', data: { pct: 100 } }); |
| 164 | + }); |
| 165 | +}); |
| 166 | +``` |
| 167 | + |
| 168 | +A `CUSTOM` event named `on_interrupt` populates `agent.interrupt()` instead of `customEvents()`; see the [Interrupts guide](/docs/ag-ui/guides/interrupts). A second branch keyed `{ toolMessageFor: 'search-1' }` plays on the follow-up run that resolving a client tool starts, so an approve-then-continue turn is scriptable end to end. |
| 169 | + |
| 170 | +When you need control the script does not give you — per-event timing, mid-stream errors, an observable you drive by hand — script your own `AbstractAgent` and feed it through `toAgent()`. The adapter reduces those events the same way. |
113 | 171 |
|
114 | 172 | ```typescript |
115 | 173 | import { describe, it, expect } from 'vitest'; |
@@ -153,4 +211,4 @@ describe('scripted AG-UI events', () => { |
153 | 211 | }); |
154 | 212 | ``` |
155 | 213 |
|
156 | | -`customEvents()` is the AG-UI-specific signal — `toAgent()` returns an `AgUiAgent`, so it is reachable directly here without a cast. A `CUSTOM` event named `on_interrupt` would instead populate `agent.interrupt()`; see the [Interrupts guide](/docs/ag-ui/guides/interrupts). |
| 214 | +`customEvents()` is the AG-UI-specific signal — `toAgent()` returns an `AgUiAgent`, so it is reachable directly here without a cast, exactly as it is through `injectAgent()`. |
0 commit comments