deer-flow/frontend/tests/unit/core/threads/run-context.test.ts
Totoro 94e69d6ff7
feat(frontend): reference conversations from the composer (#5465)
* feat(frontend): reference conversations from the composer

Adds a "Reference a conversation" button next to the attachment button,
shown only while GET /api/features reports read_conversation enabled. It
opens a picker over the recent-conversation list (current thread excluded,
capped at max_references) and shows removable chips in the composer.

On send the thread IDs ride SendMessageOptions.conversationReferences into
run context.conversation_references, which the Gateway consumes at
admission; the LangGraph SDK drops unknown top-level body fields. A
display-only copy ({thread_id, title}) on the visible human message lets
the transcript render read-only chips linking to the source.

References are per message: not persisted with the draft and cleared on
send or thread switch; regenerating or editing a turn runs without them
unless they are attached again.

Related to #5398. Depends on #5463.

* fix(frontend): pin the run-context contract and finish the picker states

Both thread.submit paths now build their run context through one exported
buildRunContext helper, tested directly: attached references travel as a
plain string[] under context.conversation_references only when the caller
passed them, a stray key in local settings is dropped instead of forwarded,
and the regenerate/edit replay path never carries references.

The picker shows a loading row while the conversation list is still in
flight instead of claiming there are no conversations, and the transcript
chip group is labelled with the previously unused referencedConversations
translation.

* fix(frontend): route conversation-reference chips to custom-agent sources

The picker offered custom-agent conversations but kept only the thread ID
and title, so transcript chips always linked to /workspace/chats/{id} and
dropped the source's custom-agent context on navigation.

Preserve the agent identity end to end: the picker now attaches
agentNameOfThread() (context first, then metadata.agent_name, mirroring
pathOfThread) to the selection, the display-only additional_kwargs metadata
round-trips it as agent_name, and the transcript chip passes it to
pathOfThread so custom-agent sources resolve to
/workspace/agents/{agent}/chats/{id}.

Tests: agent_name metadata round-trip and malformed-entry tolerance, picker
toggle carrying the metadata agent with run context winning, and a
picker-to-transcript regression pinning the /workspace/agents/writer/chats/
source-1 href.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-09-16 21:19:52 +08:00

70 lines
2.1 KiB
TypeScript

import { describe, expect, it } from "@rstest/core";
import type { LocalSettings } from "@/core/settings";
import { buildRunContext } from "@/core/threads/hooks";
const settings = {
mode: "pro",
model_name: "gemma4",
reasoning_effort: undefined,
} as unknown as LocalSettings["context"];
describe("buildRunContext", () => {
it("sends attached references as a plain string[] under context.conversation_references", () => {
const context = buildRunContext({
settings,
threadId: "t-1",
extraContext: { agent_name: "writer" },
conversationReferences: ["source-a", "source-b"],
});
expect(context.conversation_references).toEqual(["source-a", "source-b"]);
expect(context.agent_name).toBe("writer");
expect(context.thread_id).toBe("t-1");
expect(context.is_plan_mode).toBe(true);
});
it("omits the key when nothing is attached, including on the replay path", () => {
expect(
"conversation_references" in
buildRunContext({ settings, threadId: "t-1" }),
).toBe(false);
expect(
"conversation_references" in
buildRunContext({
settings,
threadId: "t-1",
conversationReferences: [],
}),
).toBe(false);
});
it("never forwards a stray conversation_references key from local settings", () => {
const stale = {
...settings,
conversation_references: ["stale-source"],
} as unknown as LocalSettings["context"];
expect(
"conversation_references" in
buildRunContext({ settings: stale, threadId: "t-1" }),
).toBe(false);
expect(
buildRunContext({
settings: stale,
threadId: "t-1",
conversationReferences: ["source-a"],
}).conversation_references,
).toEqual(["source-a"]);
});
it("copies the list so later mutation of the caller's array cannot change the request", () => {
const references = ["source-a"];
const context = buildRunContext({
settings,
threadId: "t-1",
conversationReferences: references,
});
references.push("source-b");
expect(context.conversation_references).toEqual(["source-a"]);
});
});