返回 DeepSeek-Reasonix
edit-replay.test.ts
根目录 / desktop / frontend / src / __tests__ / edit-replay.test.ts
1 // Run: tsx src/__tests__/edit-replay.test.ts
2
3 import { replaySubmitText, replaySubmitTextPreservingSelectedContext } from "../lib/editReplay";
4 import { invocationSegmentsFromMessage, replaceInvocationTextRange, serializeInvocationSubmit, type ComposerInvocation } from "../lib/invocationDisplay";
5 import { formatSelectedTextContext, formatSelectionLabel, stripSelectionLabels } from "../lib/selectedTextContext";
6
7 let passed = 0;
8 let failed = 0;
9
10 function eq(a: unknown, b: unknown, label: string) {
11 if (a === b) {
12 process.stdout.write(` PASS ${label}\n`);
13 passed += 1;
14 } else {
15 process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}\n`);
16 failed += 1;
17 }
18 }
19
20 console.log("\nedit replay");
21
22 eq(
23 replaySubmitText("hidden session context\nvisible prompt", "visible prompt", "visible prompt", "visible prompt"),
24 "hidden session context\nvisible prompt",
25 "unchanged edits preserve the original submitted text",
26 );
27
28 eq(
29 replaySubmitText("hidden session context\nvisible prompt @.reasonix/attachments/a.png", "visible prompt @[a.png](.reasonix/attachments/a.png)", "updated prompt @[a.png](.reasonix/attachments/a.png)", "updated prompt @.reasonix/attachments/a.png"),
30 "hidden session context\nupdated prompt @.reasonix/attachments/a.png",
31 "edited visible text preserves submit-only prefix and raw attachment refs",
32 );
33
34 eq(
35 replaySubmitText(undefined, "visible prompt", "updated prompt", "updated prompt"),
36 "updated prompt",
37 "messages without hidden submit context use the rebuilt submit text",
38 );
39
40 const selectedReferences = [{ id: "chat-1", text: "selected assistant response" }];
41 const selectedLabel = formatSelectionLabel(selectedReferences[0]);
42 const selectedContext = formatSelectedTextContext(selectedReferences);
43 const selectedDisplay = `visible prompt ${selectedLabel}`;
44 const selectedEditableDisplay = stripSelectionLabels(selectedDisplay, selectedReferences);
45 const replayedSelectedEdit = replaySubmitTextPreservingSelectedContext(
46 `visible prompt\n\n${selectedContext}`,
47 selectedEditableDisplay,
48 "updated prompt",
49 "updated prompt",
50 );
51 eq(
52 replayedSelectedEdit,
53 `updated prompt\n\n${selectedContext}`,
54 "editing a selected-context message preserves the quoted context suffix without submitting its display label",
55 );
56
57 eq(
58 replaySubmitText("/reasonix-develop review this change", "review this change", "review the updated change", "review the updated change"),
59 "/reasonix-develop review the updated change",
60 "editing a structured skill message preserves its slash invocation",
61 );
62
63 const mixedInvocations: ComposerInvocation[] = [
64 { id: "primary", offset: 0, command: { name: "general-purpose", description: "", kind: "subagent" } },
65 { id: "skill", offset: 2, command: { name: "activity-dynamic-debug", description: "", kind: "skill" } },
66 { id: "explore", offset: 3, command: { name: "explore", description: "", kind: "subagent" } },
67 ];
68 const mixedSubmit = serializeInvocationSubmit("你是再做", mixedInvocations);
69 eq(
70 mixedSubmit,
71 "/general-purpose 你是 /activity-dynamic-debug 再 /explore 做",
72 "multiple abilities serialize in visual order",
73 );
74 const mixedSegments = invocationSegmentsFromMessage("你是再做", mixedSubmit);
75 eq(
76 mixedSegments.filter((segment) => segment.type === "invocation").map((segment) => segment.type === "invocation" ? segment.invocation.name : "").join(","),
77 "general-purpose,activity-dynamic-debug,explore",
78 "multiple abilities restore from display and submit text",
79 );
80 eq(
81 mixedSegments.filter((segment) => segment.type === "text").map((segment) => segment.type === "text" ? segment.content : "").join(""),
82 "你是再做",
83 "restored ability segments preserve visible task text",
84 );
85
86 eq(
87 replaySubmitText(mixedSubmit, "你是再做", "请开发并检查", "请开发并检查"),
88 "/general-purpose 请开发 /activity-dynamic-debug 并检 /explore 查",
89 "editing a mixed ability message preserves every invocation",
90 );
91
92 const boundaryInvocations: ComposerInvocation[] = [
93 { id: "first", offset: 0, command: { name: "general-purpose", description: "", kind: "subagent" } },
94 { id: "second", offset: 0, command: { name: "explore", description: "", kind: "subagent" } },
95 ];
96 const insertedAfterFirst = replaceInvocationTextRange("", boundaryInvocations, 0, 0, "task", "first");
97 eq(
98 insertedAfterFirst.invocations.map((invocation) => `${invocation.id}:${invocation.offset}`).join(","),
99 "first:0,second:4",
100 "inserting after an invocation preserves the selected entity boundary",
101 );
102 eq(
103 serializeInvocationSubmit(insertedAfterFirst.text, insertedAfterFirst.invocations),
104 "/general-purpose task /explore",
105 "same-offset entities serialize around inserted text in visual order",
106 );
107 eq(
108 invocationSegmentsFromMessage("develop x", "/my-formatter develop /explore x")
109 .filter((segment) => segment.type === "invocation")
110 .map((segment) => segment.type === "invocation" ? segment.invocation.name : "")
111 .join(","),
112 "my-formatter,explore",
113 "spaced English messages restore every inline invocation",
114 );
115
116 const sessionPrefix = "以下是用户引用的历史会话上下文:\n\n[会话:Earlier]\n...\n\n---\n\n当前用户问题:\n";
117 eq(
118 replaySubmitText(
119 `${sessionPrefix}/reasonix-develop review this change`,
120 "review this change",
121 "review the updated change",
122 "review the updated change",
123 ),
124 `${sessionPrefix}/reasonix-develop review the updated change`,
125 "editing a structured message keeps the hidden referenced-session prefix",
126 );
127
128 // Hydrated structured messages: reload resolves the recorded display (the
129 // serialized slash form) while submit is the composed model text, so badge
130 // restoration relies on the leading-known-token fallback.
131 const segmentNames = (display: string, submit: string, metadata: Record<string, { kind: "skill" | "subagent" }>) =>
132 invocationSegmentsFromMessage(display, submit, metadata)
133 .map((segment) => segment.type === "invocation" ? `[${segment.invocation.name}]` : segment.content)
134 .join("|");
135 eq(
136 segmentNames("/my-formatter fix the tests", "plan-wrapped composed body", { "my-formatter": { kind: "subagent" } }),
137 "[my-formatter]|fix the tests",
138 "hydrated slash-form display restores its leading known invocation",
139 );
140 eq(
141 segmentNames("/my-formatter /explore fix", "composed body", { "my-formatter": { kind: "subagent" }, explore: { kind: "subagent" } }),
142 "[my-formatter]|[explore]|fix",
143 "hydrated display restores consecutive leading invocations",
144 );
145 eq(
146 segmentNames("/unknown-name fix", "composed body", {}),
147 "/unknown-name fix",
148 "unknown leading slash names stay plain text after reload",
149 );
150 eq(
151 segmentNames("see /my-formatter later", "composed body", { "my-formatter": { kind: "subagent" } }),
152 "see /my-formatter later",
153 "mid-text slash tokens stay plain text after reload",
154 );
155
156 console.log(`\n${passed} passed, ${failed} failed`);
157 if (failed > 0) process.exit(1);
158
158 lines TYPESCRIPT