Skip to content

Commit 2cba7b4

Browse files
bloveclaude
andauthored
test(cockpit-chat-debug): aimock e2e suite for the c-debug devtools dock (#1063)
* test(cockpit-chat-debug): aimock e2e suite for the c-debug devtools dock The c-debug demo had no e2e because it used to mount `<chat-debug>` alone — a devtools dock with no composer, so nothing could produce a run and the Timeline tab showed its empty state forever. The demo now mounts `<chat>` beside the dock, so the capability is drivable and gets the same aimock-replay suite every sibling chat cap has. Four specs, wired the way the siblings wire theirs (playwright.config.ts + global-setup-impl.ts + fixtures/, and an `e2e` target in project.json, which is also what puts the cap in the CI matrix — scripts/cockpit-matrix.mjs derives that from `targets.e2e`): - the Timeline tab shows its empty state before any run; - a run sent through the composer fills it with the pipeline's checkpoint rows (asserted as the exact list, newest first, since the row set is precisely what this capability exists to demonstrate); - selecting a checkpoint diffs that step of the run; - the State tab swaps in the live state inspector. No assertion reads model prose. Every checkpoint label is structural — `toDebugCheckpoint` reads `state.next[0]`, so the rows are the graph wiring read backwards. The fixture is three entries, one per LLM call the graph makes in a turn (`generate`, `summarize`, `generate_title`), discriminated by `systemMessage` because all three carry the same user message. No `hasToolResult` ordering constraint applies: the graph binds no tools. Mutation-checked by removing `<chat>` from the demo template: the three run-driven specs fail on the missing composer and only the empty-state spec passes, which is exactly the state the deleted e2e/README.md described. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(cockpit): retire the c-debug "no aimock e2e" cross-references Two files pointed at the c-debug README as the marker for a cap that deliberately ships without an aimock suite. That is no longer true. `scripts/rerecord-all-aimock.sh` is the load-bearing one: it discovers caps by walking `*/e2e/fixtures/*.json`, so c-debug is picked up automatically now that it has a fixture, and an unlisted cap defaults to the prompt "Hello". Re-recording would have quietly replaced the fixture with one whose user message no cap spec sends. Register c-debug's actual prompt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 845fd53 commit 2cba7b4

9 files changed

Lines changed: 206 additions & 16 deletions

File tree

cockpit/chat/debug/angular/e2e/README.md

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { test, expect, type Page } from '@playwright/test';
2+
import { submitAndWaitForResponse } from '@threadplane-internal/e2e-harness';
3+
4+
const PROMPT = 'What is a jet bridge?';
5+
6+
/**
7+
* The checkpoint rows the c-debug pipeline writes, newest first.
8+
*
9+
* Every label is structural, not model prose: `toDebugCheckpoint` reads
10+
* `state.next[0]` off each LangGraph checkpoint, so the list is exactly the
11+
* graph's wiring (`__start__` → generate → process → summarize →
12+
* generate_title) read backwards, plus the terminal checkpoint whose `next`
13+
* is empty and therefore falls back to the positional `Step 1` label.
14+
*
15+
* Asserting the whole list rather than a count is deliberate. The row set IS
16+
* what this capability exists to demonstrate, so a node added to or dropped
17+
* from the pipeline should fail here and be re-stated, not silently pass.
18+
*/
19+
const EXPECTED_CHECKPOINT_ROWS = [
20+
'Step 1',
21+
'generate_title',
22+
'summarize',
23+
'process',
24+
'generate',
25+
'__start__',
26+
];
27+
28+
const openDock = (page: Page) =>
29+
page.getByRole('button', { name: /open chat devtools/i }).click();
30+
31+
test('c-debug: the Timeline tab shows its empty state before any run', async ({ page }) => {
32+
await page.goto('/');
33+
await openDock(page);
34+
35+
// The dock opens on the Timeline tab. With no run on the thread there is
36+
// nothing to inspect, and this is the state the demo was stuck in for as
37+
// long as it mounted <chat-debug> with no composer beside it.
38+
await expect(page.locator('chat-debug-timeline-inspector')).toBeVisible();
39+
await expect(page.getByText('No checkpoints yet.')).toBeVisible();
40+
await expect(page.locator('chat-debug-checkpoint-card')).toHaveCount(0);
41+
});
42+
43+
test('c-debug: a run through the composer fills the Timeline tab', async ({ page }) => {
44+
// Sending through <chat>'s composer is the whole point of the pairing:
45+
// the chat produces the run, the dock inspects it.
46+
await submitAndWaitForResponse(page, PROMPT);
47+
await openDock(page);
48+
49+
const cards = page.locator('chat-debug-checkpoint-card');
50+
await expect(cards.first()).toBeVisible({ timeout: 30_000 });
51+
await expect(cards).toHaveCount(EXPECTED_CHECKPOINT_ROWS.length);
52+
expect((await cards.allTextContents()).map((t) => t.trim())).toEqual(
53+
EXPECTED_CHECKPOINT_ROWS,
54+
);
55+
await expect(page.getByText('No checkpoints yet.')).toHaveCount(0);
56+
});
57+
58+
test('c-debug: selecting a checkpoint diffs that step of the run', async ({ page }) => {
59+
await submitAndWaitForResponse(page, PROMPT);
60+
await openDock(page);
61+
62+
const cards = page.locator('chat-debug-checkpoint-card');
63+
await expect(cards.first()).toBeVisible({ timeout: 30_000 });
64+
await cards.first().click();
65+
66+
// The newest checkpoint has no predecessor in the list, so its diff is the
67+
// whole of that checkpoint's values added at once. `messages` is the only
68+
// key on this graph's MessagesState, and it is read from the checkpoint the
69+
// server persisted — so a diff naming it proves the panel is rendering real
70+
// run state rather than a placeholder.
71+
const diff = page.locator('chat-debug-state-diff');
72+
await expect(diff).toBeVisible();
73+
await expect(diff).toContainText('+ messages');
74+
});
75+
76+
test('c-debug: the State tab swaps in the live state inspector', async ({ page }) => {
77+
await submitAndWaitForResponse(page, PROMPT);
78+
await openDock(page);
79+
await expect(page.locator('chat-debug-checkpoint-card').first()).toBeVisible({
80+
timeout: 30_000,
81+
});
82+
83+
await page.getByRole('tab', { name: 'State' }).click();
84+
85+
const stateTab = page.locator('chat-debug-state-tab');
86+
await expect(stateTab).toBeVisible();
87+
await expect(stateTab).toContainText('Current state');
88+
// The tab owns the panel body — the timeline is torn down, not stacked.
89+
await expect(page.locator('chat-debug-checkpoint-card')).toHaveCount(0);
90+
// `agent.state()` is the LangGraph values bag with `messages` projected out
91+
// into the transcript, so on this MessagesState graph the inspector renders
92+
// an empty object today. Assert the shape the JsonPipe produces rather than
93+
// that exact literal: the claim is that the inspector is mounted and bound
94+
// to the agent, and a graph that carries state beyond its messages should
95+
// widen this tab's coverage, not fail it.
96+
await expect(stateTab.locator('chat-debug-state-inspector pre')).toHaveText(
97+
/^\{[\s\S]*\}$/,
98+
);
99+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"fixtures": [
3+
{
4+
"match": {
5+
"systemMessage": "Aviation Assistant",
6+
"userMessage": "What is a jet bridge?"
7+
},
8+
"response": {
9+
"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."
10+
}
11+
},
12+
{
13+
"match": {
14+
"systemMessage": "brief one-sentence summary",
15+
"userMessage": "What is a jet bridge?"
16+
},
17+
"response": {
18+
"content": "The traveler asked what a jet bridge is and received a short definition of the boarding walkway."
19+
}
20+
},
21+
{
22+
"match": {
23+
"systemMessage": "In 3-5 words",
24+
"userMessage": "What is a jet bridge?"
25+
},
26+
"response": {
27+
"content": "Jet bridge basics"
28+
}
29+
}
30+
]
31+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { resolve } from 'node:path';
2+
import { portsFor } from '../../../../../cockpit/ports.mjs';
3+
import { createGlobalSetup } from '@threadplane-internal/e2e-harness';
4+
5+
const ports = portsFor('cockpit-chat-debug-angular');
6+
7+
export default createGlobalSetup({
8+
// Each chat cap runs its OWN standalone backend (cockpit/chat/<name>/python)
9+
// on `<angular_port> + 1000`. The proxy.conf.mjs target matches.
10+
langgraphCwd: 'cockpit/chat/debug/python',
11+
langgraphPort: ports.langgraph,
12+
angularProject: 'cockpit-chat-debug-angular',
13+
angularPort: ports.angular,
14+
fixturesDir: resolve(__dirname, 'fixtures'),
15+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
import { portsFor } from '../../../../../cockpit/ports.mjs';
3+
4+
const { angular: angularPort } = portsFor('cockpit-chat-debug-angular');
5+
6+
export default defineConfig({
7+
testDir: '.',
8+
testMatch: '**/*.spec.ts',
9+
fullyParallel: false,
10+
workers: 1,
11+
retries: process.env.CI ? 2 : 0,
12+
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
13+
use: {
14+
baseURL: `http://localhost:${angularPort}`,
15+
trace: 'retain-on-failure',
16+
},
17+
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
18+
globalSetup: './global-setup-impl.ts',
19+
globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'),
20+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ES2022",
5+
"moduleResolution": "Bundler",
6+
"esModuleInterop": true,
7+
"strict": true,
8+
"skipLibCheck": true,
9+
"noEmit": true,
10+
"types": [
11+
"node"
12+
],
13+
"baseUrl": "../../../../..",
14+
"paths": {
15+
"@threadplane-internal/e2e-harness": [
16+
"libs/e2e-harness/src/index.ts"
17+
],
18+
"@threadplane-internal/e2e-harness/global-teardown": [
19+
"libs/e2e-harness/src/global-teardown.ts"
20+
]
21+
},
22+
"allowJs": true
23+
},
24+
"include": [
25+
"**/*.ts"
26+
],
27+
"exclude": [
28+
"node_modules",
29+
"test-results",
30+
"playwright-report"
31+
]
32+
}

cockpit/chat/debug/angular/project.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@
8787
"cwd": "cockpit/chat/debug/angular",
8888
"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); }\""
8989
}
90+
},
91+
"e2e": {
92+
"executor": "@nx/playwright:playwright",
93+
"options": {
94+
"config": "cockpit/chat/debug/angular/e2e/playwright.config.ts"
95+
}
9096
}
9197
},
9298
"tags": [

cockpit/render/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ Existing coverage:
1717

1818
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.
1919

20-
This file is the deliberate "no e2e" marker matching the c-debug README (`cockpit/chat/debug/angular/e2e/README.md`).
20+
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/`.

scripts/rerecord-all-aimock.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ declare -A CAP_PROMPTS=(
4242
["c-subagents"]="Plan a trip from LAX to JFK"
4343
["c-generative-ui"]="Show me a dashboard of airline operations.|Filter to only the cancelled flights."
4444
["c-a2ui"]="I want to fly LAX to JFK|I want to fly SFO to SEA"
45+
["c-debug"]="What is a jet bridge?"
4546
["streaming"]="Tell me one quick fact about Angular signals in two sentences."
4647
)
4748

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

0 commit comments

Comments
 (0)