diff --git a/examples/chat/aimock-e2e/aimock-runner.ts b/examples/chat/aimock-e2e/aimock-runner.ts index bc5e7e613..5392cb777 100644 --- a/examples/chat/aimock-e2e/aimock-runner.ts +++ b/examples/chat/aimock-e2e/aimock-runner.ts @@ -18,30 +18,30 @@ export interface AimockStartOptions { fixturePath: string; } -interface FixtureFile { - fixtures: ReadonlyArray<{ - match: { userMessage: string }; - response: { content: string }; - }>; -} +// Raw JSON entry shape passes through to aimock's FixtureFileEntry — the +// `match` block can carry richer discriminators (toolName, hasToolResult, +// turnIndex, etc.) that are needed to distinguish a parent LLM's first call +// from its continuation after a tool round. We don't narrow the shape here; +// aimock's `addFixturesFromJSON` validates structure at load time. +type FixtureFileEntry = Record; -function loadFixtureEntries(fixturePath: string): FixtureFile['fixtures'] { +function loadFixtureEntries(fixturePath: string): FixtureFileEntry[] { const stats = statSync(fixturePath); + const out: FixtureFileEntry[] = []; + const readFile = (full: string): void => { + const raw = readFileSync(full, 'utf-8'); + const parsed = JSON.parse(raw) as { fixtures: FixtureFileEntry[] }; + for (const fx of parsed.fixtures) out.push(fx); + }; if (stats.isDirectory()) { - const merged: FixtureFile['fixtures'][number][] = []; const files = readdirSync(fixturePath) .filter((f) => f.endsWith('.json')) .sort(); - for (const file of files) { - const raw = readFileSync(join(fixturePath, file), 'utf-8'); - const parsed = JSON.parse(raw) as FixtureFile; - for (const fx of parsed.fixtures) merged.push(fx); - } - return merged; + for (const file of files) readFile(join(fixturePath, file)); + return out; } - const raw = readFileSync(fixturePath, 'utf-8'); - const parsed = JSON.parse(raw) as FixtureFile; - return parsed.fixtures; + readFile(fixturePath); + return out; } export async function startAimock(opts: AimockStartOptions): Promise { @@ -57,8 +57,8 @@ export async function startAimock(opts: AimockStartOptions): Promise 0) { + mock.addFixturesFromJSON(entries as never); } await mock.start(); diff --git a/examples/chat/aimock-e2e/fixtures/research-subagent.json b/examples/chat/aimock-e2e/fixtures/research-subagent.json new file mode 100644 index 000000000..193863789 --- /dev/null +++ b/examples/chat/aimock-e2e/fixtures/research-subagent.json @@ -0,0 +1,37 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "Use the research subagent to investigate the history and motivation behind Angular standalone components, then report back with a concise summary.", + "hasToolResult": true + }, + "response": { + "content": "I ran the research subagent on \"History and motivation behind Angular standalone components\" and here is a concise summary.\n\n- Historical context\n - Angular originally required NgModules to declare components, directives, pipes, imports, and providers. This module-centric model was introduced early in Angular to organize and scope features.\n - The Ivy compiler/rendering pipeline (around Angular 9, 2019) decoupled component compilation and runtime from many NgModule responsibilities, making the module layer less technically necessary for many tasks.\n - Standalone components were introduced as an opt-in preview in Angular 14 (2022) and stabilized in Angular 15 (late 2022), with companion APIs (e.g., bootstrapApplication, loadComponent) enabling apps to run and lazy-load without NgModules.\n\n- What standalone components are\n - A component (or directive/pipe) marked with standalone: true that can declare its own imports and providers.\n - Standalone components can be bootstrapped directly, referenced in routes, and lazy-loaded with loadComponent \u2014 without being declared inside an NgModule.\n\n- Primary motivations\n - Reduce boilerplate and mental overhead associated with creating and maintaining NgModules.\n - Simplify app structure and make code more ergonomic and approachable, particularly for newcomers and small apps.\n - Enable better tree-shaking and potentially improved compilation/runtime performance by removing an extra indirection layer.\n - Improve ergonomics for lazy-loading, microfrontends, and unit testing (components can be used directly).\n - Allow incremental adoption: NgModules remain supported so teams can migrate at their own pace.\n\n- Practical implications\n - Developers can migrate incrementally to standalone components; existing NgModule-based apps continue to work.\n - New APIs (bootstrapApplication, loadComponent) support module-free bootstrapping and route-level lazy loading.\n - Simpler test setup and smaller, more self-contained component boundaries.\n\nIf you want, I can:\n- Show brief code examples (bootstrapping with bootstrapApplication, a standalone component declaration, using loadComponent in a route).\n- Summarize migration strategies from NgModules to standalone components." + } + }, + { + "match": { + "userMessage": "Use the research subagent to investigate the history and motivation behind Angular standalone components, then report back with a concise summary." + }, + "response": { + "toolCalls": [ + { + "name": "research", + "arguments": { + "topic": "History and motivation behind Angular standalone components", + "subagent_type": "research" + } + } + ] + } + }, + { + "match": { + "userMessage": "Topic: History and motivation behind Angular standalone components" + }, + "response": { + "content": "- Background: Angular originally required NgModules to declare components, imports and providers; Ivy (Angular 9, 2019) decoupled the rendering/compilation pipeline from NgModules, making many NgModule responsibilities redundant. \n- Timeline: the standalone-components idea was introduced as an opt-in preview in Angular 14 (2022) and stabilized in Angular 15 (late 2022), with supporting APIs (e.g., bootstrapApplication) to run apps without NgModules. \n- What they are: a component marked standalone: true that can carry its own imports/providers and be used directly in routes or bootstrapped without being declared in an NgModule (also applies to directives and pipes). \n- Motivation: reduce boilerplate and mental overhead, simplify app structure, improve tree-shaking and compilation speed, and make lazy-loading/microfrontend patterns and unit testing easier and more ergonomic. \n- Practical effect: developers can incrementally adopt standalone components (NgModules remain supported), use loadComponent for route-level lazy loading, and simplify bootstrapping and test setup while preserving backward compatibility." + } + } + ] +} diff --git a/examples/chat/aimock-e2e/research-subagent.spec.ts b/examples/chat/aimock-e2e/research-subagent.spec.ts new file mode 100644 index 000000000..f2bfb64a1 --- /dev/null +++ b/examples/chat/aimock-e2e/research-subagent.spec.ts @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; + +const PROMPT = + 'Use the research subagent to investigate the history and motivation behind ' + + 'Angular standalone components, then report back with a concise summary.'; + +test('research subagent: parent dispatches research, subagent content surfaces in the bubble', async ({ + page, +}) => { + await page.goto('/embed'); + + const input = page.getByRole('textbox', { name: /message|prompt/i }); + await input.fill(PROMPT); + await page.getByRole('button', { name: /send/i }).click(); + + // The chat-tool-calls primitive renders a button/chip labeled "Called research" + // (or similar) once the parent dispatches the tool. With aimock the subagent + // runs essentially instantly, so we don't try to catch the transient + // panel — instead we assert on the durable + // tool-call-completion chip and on subagent-emitted content reaching the bubble. + const researchChip = page.getByRole('button', { name: /research/i }).first(); + await expect(researchChip).toBeVisible({ timeout: 45_000 }); + + // The captured subagent summary mentions standalone components and NgModule. + // Assert one of those terms appears in the conversation body — proves the + // subagent's LLM response made it through the graph back into the chat. + const conversation = page.locator('chat-message-list, chat-window').first(); + await expect.poll( + async () => (await conversation.innerText()).toLowerCase(), + { timeout: 45_000 }, + ).toMatch(/standalone components|ngmodule/i); +});