Files
2026-07-13 12:58:18 +08:00

118 lines
5.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { runBubbleRaceRepro } from "./bubble-race-repro.js";
/**
* Phase 1 Task 1.2 — defect 2 (un-turn-scoped bubble selection).
*
* This file pairs the defect-RED test with a mechanism-GREEN test for
* `messagesOverrideFromEnv()` (the Node-side helper in
* `harness/src/probes/helpers/init-scripts.ts` consumed at the
* `runConversation` callsite in `d6-all-pills.ts:1697`). The
* mechanism-GREEN exists so a wiring regression of the override
* channel surfaces immediately rather than masquerading as a
* defect-2 failure. Per the user's tight-TDD-loop directive, the
* mechanism test is asserted to PASS against the current state
* (Phase-1 baseline at s4 commit 69b383d0c) before the defect-RED
* test is permitted to run.
*
* Inputs use the canonical 3-turn sequence from
* `aimock/d6/langgraph-python/agentic-chat.json`:
* 1. "good name for a goldfish" -> response contains "Bubbles"
* 2. "name for its tank" -> response contains "Bubble Bowl"
* 3. "what we named the goldfish" -> recall, response contains
* BOTH "Bubbles" AND "Bubble Bowl"
*
* Defect-2 manifests because `readLastAssistantText` in
* `_gen-ui-shared.ts` reads `list[list.length - 1]` GLOBALLY (i.e.
* the last bubble in the DOM at the moment of read), not the bubble
* for the just-settled turn. On a 3-turn fixture with no mid-stream
* cascade flicker, turn N's "last bubble" is the right one once
* the COUNT settles, but the defect surfaces in two ways:
* (a) any cascade flicker between tiers mid-stream causes the
* "last" index to point at the wrong tier's list.
* (b) more importantly: turn N is supposed to read the bubble at
* INDEX N-1 (0-based), not the last index. The current
* implementation conflates "count grew past baseline" with
* "last bubble is mine"; under the new turn-indexed contract,
* a per-turn substring assertion will pin the correct read.
*
* Even on the happy path, turn-1's read can race the SSE stream
* for turn-2 if the count-baseline settle window closes before
* turn-2's bubble appears: turn-1's "last" suddenly becomes
* turn-2's content. The substring assertion fails for the right
* reason: turn 1 reads "Bubble Bowl" (turn-2's marker) instead of
* "Bubbles" (turn-1's marker).
*/
describe("bubble-race repro (defect 2: mechanism-GREEN — messagesOverrideFromEnv)", () => {
it("BUBBLE_RACE_MESSAGES drives the canonical 3-turn sequence through the harness end-to-end", async () => {
const messages = [
"good name for a goldfish",
"name for its tank",
"what we named the goldfish",
];
const result = await runBubbleRaceRepro({
slug: "langgraph-python:agentic-chat",
level: "d5",
messages,
});
expect(result.exitCode).toBe(0);
// 3 settled turns means messagesOverrideFromEnv() got past the
// env parse, became 3 ConversationTurns, was selected at the
// override callsite, and each turn drove a settled assistant
// response. This is the end-to-end wiring proof.
expect(result.turns).toHaveLength(3);
// Each of the 3 user inputs was echoed by the runner's
// `[conversation-runner] turn N/total — sending message
// { input: '<msg>', ... }` log immediately before the send.
// util.inspect renders the object across multiple lines, so the
// regex uses [\s\S]*? to span the structured-2nd-arg block. The
// sending-message log is emitted BEFORE the settle wait, so it
// is present even on turns that would later mis-settle — making
// this assertion strictly about the SEND-channel wiring, not
// about settled assistant text.
for (const msg of messages) {
const escaped = msg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const sendRe = new RegExp(
`\\[conversation-runner\\] turn \\d+/\\d+ — sending message[\\s\\S]*?input:\\s*'${escaped}'`,
"m",
);
expect(
result.stdout,
`expected stdout to contain a sending-message log echoing input='${msg}'`,
).toMatch(sendRe);
}
}, 240_000);
});
describe("bubble-race repro (defect 2: un-turn-scoped bubble selection)", () => {
it("reads each turn's distinct text in order across the canonical 3-turn fixture", async () => {
const result = await runBubbleRaceRepro({
slug: "langgraph-python:agentic-chat",
level: "d5",
messages: [
"good name for a goldfish",
"name for its tank",
"what we named the goldfish",
],
});
expect(result.exitCode).toBe(0);
expect(result.turns).toHaveLength(3);
// Turn 1's canonical response: "How about Bubbles? It is friendly, ..."
// Defect-2 manifests when turn-1's read picks up turn-2's or
// turn-3's content via the global `list[last]` selector.
expect(result.turns[0].assistantText).toContain("Bubbles");
expect(result.turns[0].assistantText).not.toContain("Bubble Bowl");
// Turn 2's canonical response: "Following the Bubbles theme, you
// could call the tank The Bubble Bowl. ..."
expect(result.turns[1].assistantText).toContain("Bubble Bowl");
// Turn 3's canonical response is the recall confirmation:
// "We named the goldfish Bubbles, and the tank The Bubble Bowl."
// The recall is distinguished by containing BOTH names AND the
// "named" verb (turn 1's "Bubbles" line never uses "named").
expect(result.turns[2].assistantText).toContain("Bubbles");
expect(result.turns[2].assistantText).toContain("Bubble Bowl");
expect(result.turns[2].assistantText).toMatch(/named/i);
}, 240_000);
});