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
14 changes: 0 additions & 14 deletions cockpit/chat/debug/angular/e2e/README.md

This file was deleted.

99 changes: 99 additions & 0 deletions cockpit/chat/debug/angular/e2e/c-debug.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { test, expect, type Page } from '@playwright/test';
import { submitAndWaitForResponse } from '@threadplane-internal/e2e-harness';

const PROMPT = 'What is a jet bridge?';

/**
* The checkpoint rows the c-debug pipeline writes, newest first.
*
* Every label is structural, not model prose: `toDebugCheckpoint` reads
* `state.next[0]` off each LangGraph checkpoint, so the list is exactly the
* graph's wiring (`__start__` → generate → process → summarize →
* generate_title) read backwards, plus the terminal checkpoint whose `next`
* is empty and therefore falls back to the positional `Step 1` label.
*
* Asserting the whole list rather than a count is deliberate. The row set IS
* what this capability exists to demonstrate, so a node added to or dropped
* from the pipeline should fail here and be re-stated, not silently pass.
*/
const EXPECTED_CHECKPOINT_ROWS = [
'Step 1',
'generate_title',
'summarize',
'process',
'generate',
'__start__',
];

const openDock = (page: Page) =>
page.getByRole('button', { name: /open chat devtools/i }).click();

test('c-debug: the Timeline tab shows its empty state before any run', async ({ page }) => {
await page.goto('/');
await openDock(page);

// The dock opens on the Timeline tab. With no run on the thread there is
// nothing to inspect, and this is the state the demo was stuck in for as
// long as it mounted <chat-debug> with no composer beside it.
await expect(page.locator('chat-debug-timeline-inspector')).toBeVisible();
await expect(page.getByText('No checkpoints yet.')).toBeVisible();
await expect(page.locator('chat-debug-checkpoint-card')).toHaveCount(0);
});

test('c-debug: a run through the composer fills the Timeline tab', async ({ page }) => {
// Sending through <chat>'s composer is the whole point of the pairing:
// the chat produces the run, the dock inspects it.
await submitAndWaitForResponse(page, PROMPT);
await openDock(page);

const cards = page.locator('chat-debug-checkpoint-card');
await expect(cards.first()).toBeVisible({ timeout: 30_000 });
await expect(cards).toHaveCount(EXPECTED_CHECKPOINT_ROWS.length);
expect((await cards.allTextContents()).map((t) => t.trim())).toEqual(
EXPECTED_CHECKPOINT_ROWS,
);
await expect(page.getByText('No checkpoints yet.')).toHaveCount(0);
});

test('c-debug: selecting a checkpoint diffs that step of the run', async ({ page }) => {
await submitAndWaitForResponse(page, PROMPT);
await openDock(page);

const cards = page.locator('chat-debug-checkpoint-card');
await expect(cards.first()).toBeVisible({ timeout: 30_000 });
await cards.first().click();

// The newest checkpoint has no predecessor in the list, so its diff is the
// whole of that checkpoint's values added at once. `messages` is the only
// key on this graph's MessagesState, and it is read from the checkpoint the
// server persisted — so a diff naming it proves the panel is rendering real
// run state rather than a placeholder.
const diff = page.locator('chat-debug-state-diff');
await expect(diff).toBeVisible();
await expect(diff).toContainText('+ messages');
});

test('c-debug: the State tab swaps in the live state inspector', async ({ page }) => {
await submitAndWaitForResponse(page, PROMPT);
await openDock(page);
await expect(page.locator('chat-debug-checkpoint-card').first()).toBeVisible({
timeout: 30_000,
});

await page.getByRole('tab', { name: 'State' }).click();

const stateTab = page.locator('chat-debug-state-tab');
await expect(stateTab).toBeVisible();
await expect(stateTab).toContainText('Current state');
// The tab owns the panel body — the timeline is torn down, not stacked.
await expect(page.locator('chat-debug-checkpoint-card')).toHaveCount(0);
// `agent.state()` is the LangGraph values bag with `messages` projected out
// into the transcript, so on this MessagesState graph the inspector renders
// an empty object today. Assert the shape the JsonPipe produces rather than
// that exact literal: the claim is that the inspector is mounted and bound
// to the agent, and a graph that carries state beyond its messages should
// widen this tab's coverage, not fail it.
await expect(stateTab.locator('chat-debug-state-inspector pre')).toHaveText(
/^\{[\s\S]*\}$/,
);
});
31 changes: 31 additions & 0 deletions cockpit/chat/debug/angular/e2e/fixtures/c-debug.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"fixtures": [
{
"match": {
"systemMessage": "Aviation Assistant",
"userMessage": "What is a jet bridge?"
},
"response": {
"content": "A jet bridge is the enclosed, movable walkway that connects an airport gate to an aircraft door, so passengers board without crossing the ramp."
}
},
{
"match": {
"systemMessage": "brief one-sentence summary",
"userMessage": "What is a jet bridge?"
},
"response": {
"content": "The traveler asked what a jet bridge is and received a short definition of the boarding walkway."
}
},
{
"match": {
"systemMessage": "In 3-5 words",
"userMessage": "What is a jet bridge?"
},
"response": {
"content": "Jet bridge basics"
}
}
]
}
15 changes: 15 additions & 0 deletions cockpit/chat/debug/angular/e2e/global-setup-impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { resolve } from 'node:path';
import { portsFor } from '../../../../../cockpit/ports.mjs';
import { createGlobalSetup } from '@threadplane-internal/e2e-harness';

const ports = portsFor('cockpit-chat-debug-angular');

export default createGlobalSetup({
// Each chat cap runs its OWN standalone backend (cockpit/chat/<name>/python)
// on `<angular_port> + 1000`. The proxy.conf.mjs target matches.
langgraphCwd: 'cockpit/chat/debug/python',
langgraphPort: ports.langgraph,
angularProject: 'cockpit-chat-debug-angular',
angularPort: ports.angular,
fixturesDir: resolve(__dirname, 'fixtures'),
});
20 changes: 20 additions & 0 deletions cockpit/chat/debug/angular/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { defineConfig, devices } from '@playwright/test';
import { portsFor } from '../../../../../cockpit/ports.mjs';

const { angular: angularPort } = portsFor('cockpit-chat-debug-angular');

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:${angularPort}`,
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'),
});
32 changes: 32 additions & 0 deletions cockpit/chat/debug/angular/e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": [
"node"
],
"baseUrl": "../../../../..",
"paths": {
"@threadplane-internal/e2e-harness": [
"libs/e2e-harness/src/index.ts"
],
"@threadplane-internal/e2e-harness/global-teardown": [
"libs/e2e-harness/src/global-teardown.ts"
]
},
"allowJs": true
},
"include": [
"**/*.ts"
],
"exclude": [
"node_modules",
"test-results",
"playwright-report"
]
}
6 changes: 6 additions & 0 deletions cockpit/chat/debug/angular/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@
"cwd": "cockpit/chat/debug/angular",
"command": "npx tsx -e \"import { chatDebugAngularModule } from './src/index.ts'; const module = chatDebugAngularModule; if (module.id !== 'chat-debug-angular' || module.title !== 'Chat Debug (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\""
}
},
"e2e": {
"executor": "@nx/playwright:playwright",
"options": {
"config": "cockpit/chat/debug/angular/e2e/playwright.config.ts"
}
}
},
"tags": [
Expand Down
2 changes: 1 addition & 1 deletion cockpit/render/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ Existing coverage:

If a render-specific e2e harness is desired in the future (visual diffs, interactive scrubbing), it would be a separate cycle. The aimock pattern from cockpit-chat / cockpit-langgraph caps does not fit.

This file is the deliberate "no e2e" marker matching the c-debug README (`cockpit/chat/debug/angular/e2e/README.md`).
This file is the deliberate "no e2e" marker for the render caps. It is the only one left: `c-debug` carried the same marker until it gained a real aimock suite at `cockpit/chat/debug/angular/e2e/`.
3 changes: 2 additions & 1 deletion scripts/rerecord-all-aimock.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ declare -A CAP_PROMPTS=(
["c-subagents"]="Plan a trip from LAX to JFK"
["c-generative-ui"]="Show me a dashboard of airline operations.|Filter to only the cancelled flights."
["c-a2ui"]="I want to fly LAX to JFK|I want to fly SFO to SEA"
["c-debug"]="What is a jet bridge?"
["streaming"]="Tell me one quick fact about Angular signals in two sentences."
)

# Discover aimock-eligible caps by walking fixture files. Excludes
# documented-N/A caps (render, ag-ui, c-debug) which have no fixtures.
# documented-N/A caps (render, ag-ui) which have no fixtures.
CAPS=()
while IFS= read -r f; do
cap_id=$(basename "$f" .json)
Expand Down
Loading