Skip to content

Commit 95eb196

Browse files
romanlutzCopilot
andauthored
FIX: Clear stale conversation_id when opening another attack from history (#1859)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 83caadd commit 95eb196

2 files changed

Lines changed: 78 additions & 1 deletion

File tree

frontend/src/App.test.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,27 @@ jest.mock("./components/Chat/ChatWindow", () => {
8888
const MockChatWindow = ({
8989
onNewAttack,
9090
activeTarget,
91+
attackResultId,
9192
conversationId,
93+
activeConversationId,
9294
onConversationCreated,
9395
onSelectConversation,
9496
labels,
9597
}: {
9698
onNewAttack: () => void;
9799
activeTarget: unknown;
100+
attackResultId: string | null;
98101
conversationId: string | null;
102+
activeConversationId: string | null;
99103
onConversationCreated: (attackResultId: string, conversationId: string) => void;
100104
onSelectConversation: (convId: string) => void;
101105
labels: Record<string, string>;
102106
}) => {
103107
return (
104108
<div data-testid="chat-window">
109+
<span data-testid="attack-result-id">{attackResultId ?? "none"}</span>
105110
<span data-testid="conversation-id">{conversationId ?? "none"}</span>
111+
<span data-testid="active-conversation-id">{activeConversationId ?? "none"}</span>
106112
<span data-testid="has-target">{activeTarget ? "yes" : "no"}</span>
107113
<span data-testid="labels-operator">{labels.operator ?? ""}</span>
108114
<span data-testid="labels-json">{JSON.stringify(labels)}</span>
@@ -181,6 +187,12 @@ jest.mock("./components/History/AttackHistory", () => {
181187
>
182188
Open Attack
183189
</button>
190+
<button
191+
onClick={() => onOpenAttack("ar-attack-2")}
192+
data-testid="open-attack-2"
193+
>
194+
Open Attack 2
195+
</button>
184196
</div>
185197
);
186198
};
@@ -419,6 +431,58 @@ describe("App", () => {
419431
await waitFor(() => expect(screen.getByTestId("conversation-id")).toHaveTextContent("none"));
420432
});
421433

434+
it("clears activeConversationId synchronously before fetching a new attack", async () => {
435+
// Repro: in attack A the user branched into a related conversation, so
436+
// activeConversationId points to a conv that does NOT belong to attack B.
437+
// When the user clicks Open Attack on B, App.tsx must clear the stale
438+
// conv id *before* flipping attackResultId — otherwise ChatWindow renders
439+
// with (attackResultId=B, activeConversationId=A_conv) during the in-flight
440+
// getAttack and issues GET /messages?conversation_id=A_conv → 400.
441+
442+
// Defer getAttack so we can inspect the intermediate render before it resolves.
443+
let resolveGetAttack: (value: unknown) => void = () => {};
444+
mockGetAttack.mockImplementation(
445+
() => new Promise((resolve) => { resolveGetAttack = resolve })
446+
);
447+
448+
render(<App />);
449+
450+
// Simulate: user is already on attack A with a branched conv selected.
451+
fireEvent.click(screen.getByTestId("nav-chat"));
452+
fireEvent.click(screen.getByTestId("set-conversation")); // attack A, main conv-123
453+
// Resolve the (unrelated) getAttack triggered earlier to keep state quiet
454+
// — actually nothing called it yet because set-conversation routes through
455+
// onConversationCreated, not handleOpenAttack. Proceed.
456+
fireEvent.click(screen.getByTestId("select-conversation")); // branched conv-456 in attack A
457+
expect(screen.getByTestId("attack-result-id")).toHaveTextContent("ar-123");
458+
expect(screen.getByTestId("active-conversation-id")).toHaveTextContent("conv-456");
459+
460+
// User clicks Open Attack on attack B in history.
461+
fireEvent.click(screen.getByTestId("nav-history"));
462+
fireEvent.click(screen.getByTestId("open-attack-2")); // ar-attack-2
463+
464+
// BEFORE getAttack resolves: ChatWindow must NOT see the stale conv id
465+
// alongside the new attack id. This is the invariant the fix establishes.
466+
expect(screen.getByTestId("main-layout")).toHaveAttribute(
467+
"data-current-view",
468+
"chat"
469+
);
470+
expect(screen.getByTestId("attack-result-id")).toHaveTextContent("ar-attack-2");
471+
expect(screen.getByTestId("active-conversation-id")).toHaveTextContent("none");
472+
expect(screen.getByTestId("conversation-id")).toHaveTextContent("none");
473+
474+
// After getAttack resolves: the conv id belonging to attack B is committed.
475+
resolveGetAttack({
476+
attack_result_id: "ar-attack-2",
477+
conversation_id: "attack-conv-2",
478+
labels: {},
479+
});
480+
await waitFor(() =>
481+
expect(screen.getByTestId("active-conversation-id")).toHaveTextContent("attack-conv-2")
482+
);
483+
expect(screen.getByTestId("conversation-id")).toHaveTextContent("attack-conv-2");
484+
});
485+
422486
it("merges default labels from backend version API", async () => {
423487
mockedVersionApi.getVersion.mockResolvedValueOnce({
424488
version: "2.0.0",

frontend/src/App.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,19 @@ function App() {
143143
}, [])
144144

145145
const handleOpenAttack = useCallback(async (openAttackResultId: string) => {
146+
// Synchronously clear per-attack state before flipping attackResultId so
147+
// ChatWindow does not fetch /messages with a conv_id that belonged to the
148+
// previously loaded attack while getAttack is in flight. The branched-
149+
// conversation case (activeConversationId pointing to a related conv of
150+
// the old attack) would otherwise produce a 400 from the backend.
151+
// Skip clearing when re-opening the same attack to avoid a redundant reload.
152+
if (openAttackResultId !== attackResultId) {
153+
setConversationId(null)
154+
setActiveConversationId(null)
155+
setAttackLabels(null)
156+
setAttackTarget(null)
157+
setRelatedConversationCount(0)
158+
}
146159
setAttackResultId(openAttackResultId)
147160
setIsLoadingAttack(true)
148161
setCurrentView('chat')
@@ -159,7 +172,7 @@ function App() {
159172
} finally {
160173
setIsLoadingAttack(false)
161174
}
162-
}, [clearAttackState])
175+
}, [attackResultId, clearAttackState])
163176

164177
const toggleTheme = () => {
165178
setIsDarkMode(!isDarkMode)

0 commit comments

Comments
 (0)