Skip to content

Commit 3aa64b0

Browse files
authored
fix(showcase/harness): probe-side DOM-signal alignments — flips 5 more D4 cells to D5 (CopilotKit#4732)
## Summary Six probe-side fixes landing **5 additional D5 flips** on the langgraph-python column. None of the fixes touch demo or runtime code — just the harness-side selectors / signal predicates so each probe asserts on what the published langgraph-python image actually renders today. ## D5 impact Stacks on PR CopilotKit#4728 (merged). After this branch (live in PocketBase): | Cell | Before this PR | After | |---|---|---| | `tool-rendering-default-catchall` | red | **green** | | `beautiful-chat` | red | **green** | | `reasoning-default` | red | **green** (rolled with reasoning-custom on the same `d5:reasoning-display` row) | | `frontend-tools` | regressed in earlier replay sweep | **green** (toolCallId disambiguation in PR CopilotKit#4728) | **Net dashboard count: 11 → 5 D4 cells in the langgraph-python column** (including PR CopilotKit#4724 + CopilotKit#4728's flips). ## Per-probe changes - **`d5-tool-rendering-default-catchall`** — add a fallback that scans `[data-testid="copilot-assistant-message"]` bubbles for the literal tool name + a `done`/`running` status label, alongside the strict testid contract. Strict path (the `[data-testid="copilot-tool-render"]` wrapper added in commit `ba60df5d3`) re-engages once `@copilotkit/react-core` ships a release with it; until then the bubble-text scan is the only stable hook on 1.56.5. - **`d5-reasoning-display`** — published built-in `CopilotChatReasoningMessage` carries no testid. Accept the verbatim `Thought for` / `Thinking…` header text the slot emits, in addition to the four pre-existing testid selectors used by the `reasoning-custom` override. - **`d5-gen-ui-headless-complete`** — the `SuggestionBar` renders agent-generated chip phrasing alternately with the static `useConfigureSuggestions` titles. Switch from `aria-label="Suggestion: <Title>"` to a per-chip alias list (e.g. `["stock", "aapl"]`) that matches BOTH forms via case-insensitive CSS attribute substring (`button[aria-label*="..." i]`). - **`d5-gen-ui-interrupt`** — widen the post-pick assertion to accept any of (a) the `time-picker-picked` testid, (b) the visible `Booked` badge text, or (c) the agent's `scheduled` / `confirmed` resume continuation. The picked-state Card unmounts as soon as langgraph resumes after `resolve`, so a 5s testid wait races the unmount; the OR catches whichever signal lands first. - **`_beautiful-chat-shared` (toggle-theme)** — two-track signal: pass on EITHER the `html.dark` class flipping from its initial reading OR the visible `Theme toggled` assistant content rendering. The class-flip is the strongest signal but `useFrontendTool`'s dispatch occasionally drops the handler call without dropping the agent's follow-up content message; track (b) catches the "tool semantics reached the UI" state in the meantime. - **`_beautiful-chat-shared` (search-flights)** — swap the literal `United Airlines` wait for the short brand label `United`, which appears in BOTH the FlightCard a2ui surface (when it paints) and the assistant's `$349 / $289` narration. Price literals stay as the canonical 2-flight fingerprint. ## Conversation-runner adjustment Reverse the partial-fix from PR CopilotKit#4724: try chat-input cascade resolution AT BOOT first (and read baseline at boot too); only defer to post-`preFill` on auth-shape demos where the cascade fails. Without this, demos where `preFill` itself fires the message (chip clicks in headless-complete) had baseline read AFTER the assistant bubble already appeared and the settle waited indefinitely for further growth that never came. ## Remaining 5 D4 cells | Cell | Class | |---|---| | `agent-config` | Bucket A — runtime/streaming defect | | `shared-state-streaming` | Bucket A — same defect class | | `open-gen-ui-advanced` | Bucket A — same defect class | | `gen-ui-headless-complete` | First 3 of 4 chip turns now pass; 4th turn (`revenue` chart) still fails. Suggestion drift across 4 sequential turns means the 4th chip's aria-label sometimes drifts outside both alias forms. Worth narrowing once dev-debug-mode confirms what the 4th-turn chip rendered. | | `gen-ui-interrupt` | Post-pick signal still elusive — `Booked` badge / `scheduled` continuation are not landing in the rendered DOM after a slot click in the local environment. Further investigation needed via debug-mode trace. | Bucket A is deferred per prior discussion; the other two would benefit from `debug: true` on the runtime/provider to see exactly what events fire after the slot click / 4th chip click. ## Test plan - [x] Full d5 sweep against local replay confirms 5 → 5 red d5 keys (down from 8 pre-PR; -3 net flips on this branch alone) - [x] PocketBase reflects current state — 5 D4 cells visible on dashboard - [ ] CI's langgraph-python D5 cron passes the same 8 cells on a deploy of this branch - [ ] `dashboard.showcase.copilotkit.ai` shows 11 → 5 D4 transitions on the langgraph-python column once Railway picks this up
2 parents d182797 + e1137bb commit 3aa64b0

12 files changed

Lines changed: 739 additions & 95 deletions

showcase/aimock/d5-all.json

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,15 @@
15521552
"content": "Agent context is a typed payload published from the frontend on every turn through the useAgentContext hook. The payload is forwarded into the agent's runtime context (LangGraph 0.6 introduced the `context` channel as the supported relay for per-run frontend-supplied data; legacy `properties` flowed via `forwardedProps` and did not land in `RunnableConfig`). On the Python side, CopilotKitMiddleware reads the value off the runtime context, then routes it into the system-prompt builder so the model sees the user's tone, expertise, and length preferences before each call. The result is per-turn behavior change without a model swap."
15531553
}
15541554
},
1555+
{
1556+
"match": {
1557+
"userMessage": "Make the background a sunset gradient",
1558+
"toolCallId": "call_d5_change_background_sunset"
1559+
},
1560+
"response": {
1561+
"content": "Done — sunset gradient is live."
1562+
}
1563+
},
15551564
{
15561565
"match": {
15571566
"userMessage": "Make the background a sunset gradient"
@@ -1560,6 +1569,7 @@
15601569
"content": "Sunset gradient applied — warm orange-to-rose blend.",
15611570
"toolCalls": [
15621571
{
1572+
"id": "call_d5_change_background_sunset",
15631573
"name": "change_background",
15641574
"arguments": {
15651575
"background": "linear-gradient(135deg, #ff7e5f 0%, #feb47b 50%, #ff6b6b 100%)"
@@ -1568,6 +1578,15 @@
15681578
]
15691579
}
15701580
},
1581+
{
1582+
"match": {
1583+
"userMessage": "deep green forest gradient",
1584+
"toolCallId": "call_d5_change_background_forest"
1585+
},
1586+
"response": {
1587+
"content": "Done — forest gradient is live."
1588+
}
1589+
},
15711590
{
15721591
"match": {
15731592
"userMessage": "deep green forest gradient"
@@ -1576,6 +1595,7 @@
15761595
"content": "Forest gradient applied — deep green hues.",
15771596
"toolCalls": [
15781597
{
1598+
"id": "call_d5_change_background_forest",
15791599
"name": "change_background",
15801600
"arguments": {
15811601
"background": "linear-gradient(135deg, #0a3d2e 0%, #166534 50%, #059669 100%)"
@@ -1584,6 +1604,15 @@
15841604
]
15851605
}
15861606
},
1607+
{
1608+
"match": {
1609+
"userMessage": "navy → magenta cosmic gradient",
1610+
"toolCallId": "call_d5_change_background_cosmic"
1611+
},
1612+
"response": {
1613+
"content": "Done — cosmic gradient is live."
1614+
}
1615+
},
15871616
{
15881617
"match": {
15891618
"userMessage": "navy → magenta cosmic gradient"
@@ -1592,6 +1621,7 @@
15921621
"content": "Cosmic gradient applied — navy fading to magenta.",
15931622
"toolCalls": [
15941623
{
1624+
"id": "call_d5_change_background_cosmic",
15951625
"name": "change_background",
15961626
"arguments": {
15971627
"background": "linear-gradient(135deg, #1e3a8a 0%, #6b21a8 50%, #9333ea 100%)"
@@ -1799,10 +1829,22 @@
17991829
"title": "Sales by region",
18001830
"description": "Q4 — share of total revenue",
18011831
"data": [
1802-
{ "label": "North America", "value": 540 },
1803-
{ "label": "EMEA", "value": 320 },
1804-
{ "label": "APAC", "value": 210 },
1805-
{ "label": "LATAM", "value": 90 }
1832+
{
1833+
"label": "North America",
1834+
"value": 540
1835+
},
1836+
{
1837+
"label": "EMEA",
1838+
"value": 320
1839+
},
1840+
{
1841+
"label": "APAC",
1842+
"value": 210
1843+
},
1844+
{
1845+
"label": "LATAM",
1846+
"value": 90
1847+
}
18061848
]
18071849
}
18081850
}
@@ -1834,10 +1876,22 @@
18341876
"title": "Quarterly revenue",
18351877
"description": "FY24 — USD thousands",
18361878
"data": [
1837-
{ "label": "Q1", "value": 820 },
1838-
{ "label": "Q2", "value": 940 },
1839-
{ "label": "Q3", "value": 1080 },
1840-
{ "label": "Q4", "value": 1240 }
1879+
{
1880+
"label": "Q1",
1881+
"value": 820
1882+
},
1883+
{
1884+
"label": "Q2",
1885+
"value": 940
1886+
},
1887+
{
1888+
"label": "Q3",
1889+
"value": 1080
1890+
},
1891+
{
1892+
"label": "Q4",
1893+
"value": 1240
1894+
}
18411895
]
18421896
}
18431897
}
@@ -1874,17 +1928,26 @@
18741928
{
18751929
"id": "status-api",
18761930
"type": "StatusBadge",
1877-
"props": { "text": "API: healthy", "variant": "success" }
1931+
"props": {
1932+
"text": "API: healthy",
1933+
"variant": "success"
1934+
}
18781935
},
18791936
{
18801937
"id": "status-db",
18811938
"type": "StatusBadge",
1882-
"props": { "text": "Database: healthy", "variant": "success" }
1939+
"props": {
1940+
"text": "Database: healthy",
1941+
"variant": "success"
1942+
}
18831943
},
18841944
{
18851945
"id": "status-workers",
18861946
"type": "StatusBadge",
1887-
"props": { "text": "Workers: degraded", "variant": "warning" }
1947+
"props": {
1948+
"text": "Workers: degraded",
1949+
"variant": "warning"
1950+
}
18881951
}
18891952
],
18901953
"data": {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# aimock writes per-call fixture files to `recorded/` while it runs in
2+
# `--record` mode. The `record-d5-fixtures.mjs` orchestrator merges those
3+
# into the per-demo `<slug>.json` files at the parent level and deletes
4+
# the per-call files. Anything left in `recorded/` is recording-session
5+
# scratch and must not be committed.
6+
recorded/

showcase/docker-compose.record.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Record-mode override for aimock. Layer on top of docker-compose.local.yml
2+
# to flip aimock from replay-only into record+replay against real OpenAI /
3+
# Anthropic. Recorded fixtures land on the host at
4+
# `showcase/aimock/d5-recorded/recorded/` (one file per LLM call).
5+
#
6+
# Usage:
7+
# docker compose -f showcase/docker-compose.local.yml \
8+
# -f showcase/docker-compose.record.yml \
9+
# --profile infra up -d aimock langgraph-python
10+
#
11+
# When recording is finished, switch back to base compose (no override) and
12+
# load any new fixtures from `d5-recorded/` via an additional `--fixtures`
13+
# entry on the aimock command.
14+
#
15+
# Authorization headers from the integration container are forwarded to the
16+
# real provider — no key needs to be set on aimock itself.
17+
services:
18+
aimock:
19+
volumes:
20+
# RW mount so aimock can write `recorded/<provider>-<ts>-<uuid>.json`
21+
# under this dir. Existing baseline fixtures stay :ro from the base
22+
# compose; only the recording dir is writable.
23+
- ./aimock/d5-recorded:/showcase-fixtures/d5-recorded:rw
24+
command: [
25+
"--port",
26+
"4010",
27+
"--host",
28+
"0.0.0.0",
29+
"--record",
30+
"--provider-openai",
31+
"https://api.openai.com",
32+
"--provider-anthropic",
33+
"https://api.anthropic.com",
34+
# Load ONLY the recorded dir as the fixture source. The baseline
35+
# d5-all.json / feature-parity.json fixtures are intentionally
36+
# NOT loaded — if they were, aimock would short-circuit on
37+
# already-matched prompts and never proxy them to the real
38+
# provider, defeating the recording pass.
39+
#
40+
# smoke.json is also dropped here; the langgraph-python container
41+
# doesn't use it during d5 probes, only the dedicated /api/smoke
42+
# route which we aren't exercising.
43+
#
44+
# First --fixtures path is the recording destination base; aimock
45+
# writes to `<base>/recorded/<provider>-<ts>-<uuid>.json`.
46+
"--fixtures",
47+
"/showcase-fixtures/d5-recorded",
48+
]

showcase/docker-compose.replay.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Replay-mode override for aimock — load existing baseline fixtures PLUS
2+
# the freshly-recorded per-demo files in `aimock/d5-recorded/*.json`. No
3+
# `--record` flag, no provider URLs: every request must match a loaded
4+
# fixture or aimock returns 404.
5+
#
6+
# Usage:
7+
# docker compose -f showcase/docker-compose.local.yml \
8+
# -f showcase/docker-compose.replay.yml \
9+
# --profile infra up -d aimock langgraph-python
10+
services:
11+
aimock:
12+
volumes:
13+
# Keep the recorded directory mounted (read-only is fine here — we're
14+
# not recording in this mode) so aimock loads the per-demo files.
15+
- ./aimock/d5-recorded:/showcase-fixtures/d5-recorded:ro
16+
command: [
17+
"--port",
18+
"4010",
19+
"--host",
20+
"0.0.0.0",
21+
# Recorded per-demo fixtures first so they take precedence over the
22+
# baseline files when match criteria collide. aimock's matcher
23+
# iterates fixtures in registration order.
24+
"--fixtures",
25+
"/showcase-fixtures/d5-recorded",
26+
"--fixtures",
27+
"/showcase-fixtures/d5-all.json",
28+
"--fixtures",
29+
"/showcase-fixtures/smoke.json",
30+
"--fixtures",
31+
"/showcase-fixtures/feature-parity.json",
32+
]

showcase/harness/fixtures/d5/frontend-tools.json

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
{
2-
"_comment": "D5 fixture for /demos/frontend-tools. Three per-pill prompts (Sunset/Forest/Cosmic) drive the `change_background` frontend tool. Each fixture emits a tool call with a gradient containing the pill family's signature hex tokens, so the probe can assert the gradient mutated to the right family. Substrings 'Make the background a sunset gradient', 'deep green forest gradient', 'navy → magenta cosmic gradient' are unique per pill.",
2+
"_comment": "D5 fixture for /demos/frontend-tools. Three per-pill prompts (Sunset/Forest/Cosmic) drive the `change_background` frontend tool. Each pill has TWO entries: a first-turn (matches by userMessage substring AND no toolCall-id present in the most recent tool message) and a follow-up (matches by userMessage substring AND a static toolCallId). Both criteria are combined so a follow-up never cross-matches a different pill (the matcher iterates top-to-bottom and uses first-match-wins; a follow-up keyed only on toolCallId would re-match the next pill's first-turn request because the prior tool result is still the latest tool message in history). Substrings 'Make the background a sunset gradient', 'deep green forest gradient', 'navy → magenta cosmic gradient' are unique per pill. See showcase/harness/fixtures/d5/README.md (\"Mid-loop re-invocations after a tool call\").",
33
"fixtures": [
44
{
5-
"match": { "userMessage": "Make the background a sunset gradient" },
5+
"match": {
6+
"userMessage": "Make the background a sunset gradient",
7+
"toolCallId": "call_d5_change_background_sunset"
8+
},
9+
"response": {
10+
"content": "Done — sunset gradient is live."
11+
}
12+
},
13+
{
14+
"match": {
15+
"userMessage": "Make the background a sunset gradient"
16+
},
617
"response": {
718
"content": "Sunset gradient applied — warm orange-to-rose blend.",
819
"toolCalls": [
920
{
21+
"id": "call_d5_change_background_sunset",
1022
"name": "change_background",
1123
"arguments": {
1224
"background": "linear-gradient(135deg, #ff7e5f 0%, #feb47b 50%, #ff6b6b 100%)"
@@ -16,11 +28,23 @@
1628
}
1729
},
1830
{
19-
"match": { "userMessage": "deep green forest gradient" },
31+
"match": {
32+
"userMessage": "deep green forest gradient",
33+
"toolCallId": "call_d5_change_background_forest"
34+
},
35+
"response": {
36+
"content": "Done — forest gradient is live."
37+
}
38+
},
39+
{
40+
"match": {
41+
"userMessage": "deep green forest gradient"
42+
},
2043
"response": {
2144
"content": "Forest gradient applied — deep green hues.",
2245
"toolCalls": [
2346
{
47+
"id": "call_d5_change_background_forest",
2448
"name": "change_background",
2549
"arguments": {
2650
"background": "linear-gradient(135deg, #0a3d2e 0%, #166534 50%, #059669 100%)"
@@ -30,11 +54,23 @@
3054
}
3155
},
3256
{
33-
"match": { "userMessage": "navy → magenta cosmic gradient" },
57+
"match": {
58+
"userMessage": "navy → magenta cosmic gradient",
59+
"toolCallId": "call_d5_change_background_cosmic"
60+
},
61+
"response": {
62+
"content": "Done — cosmic gradient is live."
63+
}
64+
},
65+
{
66+
"match": {
67+
"userMessage": "navy → magenta cosmic gradient"
68+
},
3469
"response": {
3570
"content": "Cosmic gradient applied — navy fading to magenta.",
3671
"toolCalls": [
3772
{
73+
"id": "call_d5_change_background_cosmic",
3874
"name": "change_background",
3975
"arguments": {
4076
"background": "linear-gradient(135deg, #1e3a8a 0%, #6b21a8 50%, #9333ea 100%)"

showcase/harness/src/probes/helpers/conversation-runner.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -274,18 +274,35 @@ export async function runConversation(
274274
// ever got a chance to dismiss it. Subsequent turns reuse the cached
275275
// selector so we don't re-probe per turn.
276276
let chatInputSelector: string | null = null;
277-
// Initial assistant-message count is also computed lazily (after
278-
// preFill) for the same reason: on idiomatic-shape auth demos the
279-
// chat tree isn't mounted until preFill clicks sign-in, so reading
280-
// before preFill would always observe 0 (correct here, but the read
281-
// belongs alongside the resolution it pairs with).
277+
// Try to resolve the chat input AT BOOT first — when it works
278+
// (every demo except idiomatic-shape auth), capture the baseline
279+
// assistant-message count BEFORE turn 1's preFill runs. Demos like
280+
// /demos/headless-complete fire the user message inside preFill (a
281+
// chip click), so reading the baseline AFTER preFill would observe
282+
// the assistant's response already appended and the settle would
283+
// wait forever for further growth that never comes. The deferred
284+
// path is only used when boot-time resolution fails — that's
285+
// specifically the auth shape, where the chat tree mounts later.
282286
let baselineCount = 0;
283-
console.debug(
284-
"[conversation-runner] initial baseline assistant message count",
285-
{
286-
baselineCount,
287-
},
288-
);
287+
try {
288+
chatInputSelector = await resolveChatInputSelector(
289+
page,
290+
opts.chatInputSelector,
291+
);
292+
console.debug(
293+
"[conversation-runner] resolved chat input selector at boot",
294+
{ selector: chatInputSelector },
295+
);
296+
baselineCount = await readMessageCount(page);
297+
console.debug(
298+
"[conversation-runner] initial baseline assistant message count (boot)",
299+
{ baselineCount },
300+
);
301+
} catch {
302+
console.debug(
303+
"[conversation-runner] chat input cascade did not resolve at boot — deferring to post-preFill (auth shape)",
304+
);
305+
}
289306

290307
for (let idx = 0; idx < total; idx++) {
291308
const turn = turns[idx]!;

0 commit comments

Comments
 (0)