Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions examples/chat/aimock-e2e/aimock-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;

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<AimockHandle> {
Expand All @@ -57,8 +57,8 @@ export async function startAimock(opts: AimockStartOptions): Promise<AimockHandl
// Phase 1 unit-variance tables; the e2e harness is for final-state
// invariants and cross-stack integration.
const mock = new LLMock({ port: 0, chunkSize: 4096 });
for (const fx of entries) {
mock.onMessage(fx.match.userMessage, fx.response);
if (entries.length > 0) {
mock.addFixturesFromJSON(entries as never);
}
await mock.start();

Expand Down
37 changes: 37 additions & 0 deletions examples/chat/aimock-e2e/fixtures/research-subagent.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
]
}
33 changes: 33 additions & 0 deletions examples/chat/aimock-e2e/research-subagent.spec.ts
Original file line number Diff line number Diff line change
@@ -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
// <chat-subagents> 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);
});
Loading