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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ jobs:
- { angular: cockpit-chat-threads-angular, python: cockpit/chat/threads/python }
- { angular: cockpit-chat-timeline-angular, python: cockpit/chat/timeline/python }
- { angular: cockpit-chat-theming-angular, python: cockpit/chat/theming/python }
- { angular: cockpit-chat-generative-ui-angular, python: cockpit/chat/generative-ui/python }
- { angular: cockpit-chat-a2ui-angular, python: cockpit/chat/a2ui/python }
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
Expand Down
18 changes: 18 additions & 0 deletions cockpit/chat/a2ui/angular/e2e/c-a2ui.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
import { test, expect } from '@playwright/test';
import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src';

test('c-a2ui: LAX-JFK prompt renders a2ui-surface with form spec', async ({ page }) => {
await submitAndWaitForResponse(page, 'I want to fly LAX to JFK');

// The BookingFormSpec tool call returns an A2UI form spec which the
// chat-lib content-classifier mounts as a <a2ui-surface> with the
// booking surface_id. Presence proves the envelope parsed and the
// A2UI host wired up against the cap's catalog views.
await expect(page.locator('a2ui-surface')).toBeVisible();
});

test('c-a2ui: SFO-SEA prompt also renders a2ui-surface', async ({ page }) => {
await submitAndWaitForResponse(page, 'I want to fly SFO to SEA');
await expect(page.locator('a2ui-surface')).toBeVisible();
});
32 changes: 32 additions & 0 deletions cockpit/chat/a2ui/angular/e2e/fixtures/c-a2ui.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"fixtures": [
{
"match": {
"userMessage": "I want to fly LAX to JFK"
},
"response": {
"toolCalls": [
{
"name": "BookingFormSpec",
"arguments": "{\"surface_id\":\"booking\",\"data_model\":{\"origin\":\"LAX\",\"dest\":\"JFK\",\"date\":\"\",\"passengers\":1,\"fare_class\":\"Economy\"},\"components\":[{\"id\":\"root\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"card\"]}}}},{\"id\":\"card\",\"component\":{\"Card\":{\"child\":\"card_col\"}}},{\"id\":\"card_col\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"title\",\"origin\",\"dest\",\"date\",\"passengers\",\"fare\",\"submit\"]}}}},{\"id\":\"title\",\"component\":{\"Text\":{\"text\":\"Book a flight\",\"usageHint\":\"h2\"}}},{\"id\":\"origin\",\"component\":{\"MultipleChoice\":{\"label\":\"Origin\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/origin\"},\"maxAllowedSelections\":1}}},{\"id\":\"dest\",\"component\":{\"MultipleChoice\":{\"label\":\"Destination\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/dest\"},\"maxAllowedSelections\":1}}},{\"id\":\"date\",\"component\":{\"TextField\":{\"label\":\"Departure date (YYYY-MM-DD)\",\"text\":{\"path\":\"/date\"},\"textFieldType\":\"date\"}}},{\"id\":\"passengers\",\"component\":{\"TextField\":{\"label\":\"Passengers\",\"text\":{\"path\":\"/passengers\"},\"textFieldType\":\"number\"}}},{\"id\":\"fare\",\"component\":{\"MultipleChoice\":{\"label\":\"Fare class\",\"options\":[{\"label\":\"Economy\",\"value\":\"Economy\"},{\"label\":\"Premium\",\"value\":\"Premium\"},{\"label\":\"Business\",\"value\":\"Business\"},{\"label\":\"First\",\"value\":\"First\"}],\"selections\":{\"path\":\"/fare_class\"},\"maxAllowedSelections\":1}}},{\"id\":\"submit_label\",\"component\":{\"Text\":{\"text\":\"Search flights\"}}},{\"id\":\"submit\",\"component\":{\"Button\":{\"child\":\"submit_label\",\"primary\":true,\"action\":{\"name\":\"bookingSubmit\",\"context\":[{\"key\":\"formId\",\"value\":\"booking\"},{\"key\":\"origin\",\"value\":{\"path\":\"/origin\"}},{\"key\":\"dest\",\"value\":{\"path\":\"/dest\"}},{\"key\":\"date\",\"value\":{\"path\":\"/date\"}},{\"key\":\"passengers\",\"value\":{\"path\":\"/passengers\"}},{\"key\":\"fare_class\",\"value\":{\"path\":\"/fare_class\"}}]}}}}]}",
"id": "call_DiKu9KXnOKtbRcNBtDlXwTzx"
}
]
}
},
{
"match": {
"userMessage": "I want to fly SFO to SEA"
},
"response": {
"toolCalls": [
{
"name": "BookingFormSpec",
"arguments": "{\"surface_id\":\"booking\",\"data_model\":{\"origin\":\"SFO\",\"dest\":\"SEA\",\"date\":\"\",\"passengers\":1,\"fare_class\":\"Economy\"},\"components\":[{\"id\":\"root\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"card\"]}}}},{\"id\":\"card\",\"component\":{\"Card\":{\"child\":\"card_col\"}}},{\"id\":\"card_col\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"title\",\"origin\",\"dest\",\"date\",\"passengers\",\"fare\",\"submit\"]}}}},{\"id\":\"title\",\"component\":{\"Text\":{\"text\":\"Book a flight\",\"usageHint\":\"h2\"}}},{\"id\":\"origin\",\"component\":{\"MultipleChoice\":{\"label\":\"Origin\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/origin\"},\"maxAllowedSelections\":1}}},{\"id\":\"dest\",\"component\":{\"MultipleChoice\":{\"label\":\"Destination\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/dest\"},\"maxAllowedSelections\":1}}},{\"id\":\"date\",\"component\":{\"TextField\":{\"label\":\"Departure date (YYYY-MM-DD)\",\"text\":{\"path\":\"/date\"},\"textFieldType\":\"date\"}}},{\"id\":\"passengers\",\"component\":{\"TextField\":{\"label\":\"Passengers\",\"text\":{\"path\":\"/passengers\"},\"textFieldType\":\"number\"}}},{\"id\":\"fare\",\"component\":{\"MultipleChoice\":{\"label\":\"Fare class\",\"options\":[{\"label\":\"Economy\",\"value\":\"Economy\"},{\"label\":\"Premium\",\"value\":\"Premium\"},{\"label\":\"Business\",\"value\":\"Business\"},{\"label\":\"First\",\"value\":\"First\"}],\"selections\":{\"path\":\"/fare_class\"},\"maxAllowedSelections\":1}}},{\"id\":\"submit\",\"component\":{\"Button\":{\"child\":\"submit_label\",\"primary\":true,\"action\":{\"name\":\"bookingSubmit\",\"context\":[{\"key\":\"formId\",\"value\":\"booking\"},{\"key\":\"origin\",\"value\":{\"path\":\"/origin\"}},{\"key\":\"dest\",\"value\":{\"path\":\"/dest\"}},{\"key\":\"date\",\"value\":{\"path\":\"/date\"}},{\"key\":\"passengers\",\"value\":{\"path\":\"/passengers\"}},{\"key\":\"fare_class\",\"value\":{\"path\":\"/fare_class\"}}]}}}},{\"id\":\"submit_label\",\"component\":{\"Text\":{\"text\":\"Search flights\"}}}]}",
"id": "call_Miv5QrUKT95KeUOO7xzPGSEl"
}
]
}
}
]
}
11 changes: 11 additions & 0 deletions cockpit/chat/a2ui/angular/e2e/global-setup-impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
import { resolve } from 'node:path';
import { createGlobalSetup } from '../../../../../libs/e2e-harness/src';

export default createGlobalSetup({
langgraphCwd: 'cockpit/chat/a2ui/python',
langgraphPort: 5511,
angularProject: 'cockpit-chat-a2ui-angular',
angularPort: 4511,
fixturesDir: resolve(__dirname, 'fixtures'),
});
18 changes: 18 additions & 0 deletions cockpit/chat/a2ui/angular/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: '.',
testMatch: '**/*.spec.ts',
fullyParallel: false,
workers: 1,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
use: {
baseURL: 'http://localhost:4511',
trace: 'retain-on-failure',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
globalSetup: './global-setup-impl.ts',
globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'),
});
14 changes: 14 additions & 0 deletions cockpit/chat/a2ui/angular/e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "test-results", "playwright-report"]
}
6 changes: 6 additions & 0 deletions cockpit/chat/a2ui/angular/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@
"cwd": "cockpit/chat/a2ui/angular",
"command": "npx tsx -e \"import { chatA2uiAngularModule } from './src/index.ts'; const module = chatA2uiAngularModule; if (module.id !== 'chat-a2ui-angular' || module.title !== 'Chat A2UI (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\""
}
},
"e2e": {
"executor": "@nx/playwright:playwright",
"options": {
"config": "cockpit/chat/a2ui/angular/e2e/playwright.config.ts"
}
}
}
}
27 changes: 27 additions & 0 deletions cockpit/chat/generative-ui/angular/e2e/c-generative-ui.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
import { test, expect } from '@playwright/test';
import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src';

test('c-generative-ui: dashboard prompt renders chat-generative-ui surface', async ({ page }) => {
await submitAndWaitForResponse(page, 'Show me a dashboard of airline operations.');

// The render_spec tool call returns a dashboard JSON spec which the
// content-classifier in @ngaf/chat mounts as a tree of
// <chat-generative-ui> hosts (one per node in the dashboard view).
// Multiple matches expected (≥5 for the standard dashboard layout);
// assert the count proves the GenUI tree wired up. .first() unblocks
// toBeVisible's strict-mode requirement.
await expect(page.locator('chat-generative-ui').first()).toBeVisible();
await expect(page.locator('chat-generative-ui')).not.toHaveCount(0);
});

test('c-generative-ui: filter prompt produces assistant turn', async ({ page }) => {
await submitAndWaitForResponse(page, 'Filter to only the cancelled flights.');

// The query_recent_disruptions tool returns data the assistant uses to
// narrow the dashboard. Distinctive surface here is just that the
// assistant turn finalized — the dashboard view update is internal state.
await expect(
page.locator('chat-message[data-role="assistant"]').last(),
).toBeVisible();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"fixtures": [
{
"match": {
"userMessage": "Show me a dashboard of airline operations."
},
"response": {
"toolCalls": [
{
"name": "render_spec",
"arguments": "{\"elements\":{\"root\":{\"type\":\"dashboard_grid\",\"children\":[\"stats_row\",\"charts_row\",\"table_section\"]},\"stats_row\":{\"type\":\"container\",\"props\":{\"direction\":\"row\"},\"children\":[\"on_time_card\",\"flights_card\",\"delay_card\",\"load_card\"]},\"on_time_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"On-time %\",\"value\":{\"$state\":\"/on_time/value\"},\"delta\":{\"$state\":\"/on_time/delta\"}}},\"flights_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Flights Today\",\"value\":{\"$state\":\"/flights_today/value\"},\"delta\":{\"$state\":\"/flights_today/delta\"}}},\"delay_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Avg Delay (min)\",\"value\":{\"$state\":\"/avg_delay/value\"},\"delta\":{\"$state\":\"/avg_delay/delta\"}}},\"load_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Load Factor\",\"value\":{\"$state\":\"/load_factor/value\"},\"delta\":{\"$state\":\"/load_factor/delta\"}}},\"charts_row\":{\"type\":\"container\",\"props\":{\"direction\":\"row\"},\"children\":[\"trend_chart\",\"airline_chart\"]},\"trend_chart\":{\"type\":\"line_chart\",\"props\":{\"title\":\"On-time % Trend\",\"data\":{\"$state\":\"/on_time_trend\"},\"xKey\":\"month\",\"yKey\":\"on_time_pct\"}},\"airline_chart\":{\"type\":\"bar_chart\",\"props\":{\"title\":\"Flights by Airline (Daily)\",\"data\":{\"$state\":\"/flights_by_airline\"},\"labelKey\":\"airline\",\"valueKey\":\"count\"}},\"table_section\":{\"type\":\"data_grid\",\"props\":{\"title\":\"Recent Disruptions\",\"rows\":{\"$state\":\"/recent_disruptions\"},\"columns\":[\"flight_number\",\"type\",\"minutes\",\"route\",\"date\"]}}},\"root\":\"root\"}",
"id": "call_offZhaDPm8tbblqv1sNHarnc"
}
]
}
},
{
"match": {
"userMessage": "Filter to only the cancelled flights."
},
"response": {
"toolCalls": [
{
"name": "query_recent_disruptions",
"arguments": "{\"limit\":5,\"type\":\"cancelled\"}",
"id": "call_ivGTFO6CX8sl4bkOprs633gH"
}
]
}
}
]
}
11 changes: 11 additions & 0 deletions cockpit/chat/generative-ui/angular/e2e/global-setup-impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
import { resolve } from 'node:path';
import { createGlobalSetup } from '../../../../../libs/e2e-harness/src';

export default createGlobalSetup({
langgraphCwd: 'cockpit/chat/generative-ui/python',
langgraphPort: 5508,
angularProject: 'cockpit-chat-generative-ui-angular',
angularPort: 4508,
fixturesDir: resolve(__dirname, 'fixtures'),
});
18 changes: 18 additions & 0 deletions cockpit/chat/generative-ui/angular/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: '.',
testMatch: '**/*.spec.ts',
fullyParallel: false,
workers: 1,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
use: {
baseURL: 'http://localhost:4508',
trace: 'retain-on-failure',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
globalSetup: './global-setup-impl.ts',
globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'),
});
14 changes: 14 additions & 0 deletions cockpit/chat/generative-ui/angular/e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "test-results", "playwright-report"]
}
6 changes: 6 additions & 0 deletions cockpit/chat/generative-ui/angular/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@
"cwd": "cockpit/chat/generative-ui/angular",
"command": "npx tsx -e \"import { chatGenerativeUiAngularModule } from './src/index.ts'; const module = chatGenerativeUiAngularModule; if (module.id !== 'chat-generative-ui-angular' || module.title !== 'Chat Generative UI (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\""
}
},
"e2e": {
"executor": "@nx/playwright:playwright",
"options": {
"config": "cockpit/chat/generative-ui/angular/e2e/playwright.config.ts"
}
}
}
}
Loading