@cueloop/client 0.1.0-alpha.4 → 0.1.0-alpha.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +32 -24
- package/src/App.autoclose.test.tsx +137 -0
- package/src/App.diff.test.tsx +158 -22
- package/src/App.export.test.tsx +44 -37
- package/src/App.plan-review.test.tsx +632 -0
- package/src/App.readonly.test.tsx +60 -36
- package/src/App.rendering.test.tsx +132 -0
- package/src/App.review-panel.test.tsx +136 -0
- package/src/App.submit-confirm.test.tsx +220 -0
- package/src/App.test.tsx +128 -69
- package/src/App.toast.test.tsx +75 -0
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +288 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +38 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +30 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +94 -0
- package/src/components/AnnotationCard.tsx +228 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +44 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +83 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +59 -0
- package/src/components/ConfirmCard.stories.tsx +56 -0
- package/src/components/ConfirmCard.tsx +135 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +64 -0
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +78 -0
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +355 -0
- package/src/components/InboxList.stories.tsx +31 -0
- package/src/components/InboxList.tsx +62 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +58 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +102 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +74 -0
- package/src/components/PlanSheet.stories.tsx +113 -0
- package/src/components/PlanSheet.tsx +389 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +78 -0
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +101 -0
- package/src/components/ReviewRail.stories.tsx +220 -0
- package/src/components/ReviewRail.tsx +316 -0
- package/src/components/SettingsDialog.stories.tsx +111 -0
- package/src/components/SettingsDialog.tsx +141 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +108 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +44 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +152 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1829 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +21 -0
- package/src/components/agent-launcher.tsx +123 -0
- package/src/components/diff-sheet-layout.test.ts +127 -0
- package/src/components/diff-sheet-layout.ts +167 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +48 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +59 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +64 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +22 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +98 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/quick-actions-editor.stories.tsx +54 -0
- package/src/components/quick-actions-editor.tsx +136 -0
- package/src/components/stories-app.tsx +123 -0
- package/src/components/stories.test.tsx +62 -0
- package/src/components/story-fixtures.ts +98 -0
- package/src/components/story.ts +69 -0
- package/src/components/syntax-highlight.ts +92 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +36 -0
- package/src/components/truncate-text.ts +6 -0
- package/src/config.test.ts +430 -31
- package/src/config.ts +304 -26
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +233 -0
- package/src/diff-intraline.test.ts +164 -0
- package/src/diff-intraline.ts +132 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +154 -0
- package/src/editor.test.ts +143 -0
- package/src/editor.ts +123 -9
- package/src/herdr.test.ts +41 -0
- package/src/herdr.ts +27 -0
- package/src/index.ts +10 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +30 -0
- package/src/intent-dispatch.test.ts +492 -0
- package/src/intent-dispatch.ts +428 -0
- package/src/key-bindings.test.ts +152 -0
- package/src/key-bindings.ts +424 -0
- package/src/keymap.test.ts +622 -0
- package/src/keymap.ts +339 -0
- package/src/review-panel.test.ts +113 -0
- package/src/review-panel.ts +83 -0
- package/src/run.ts +9 -5
- package/src/serve.test.ts +24 -9
- package/src/serve.ts +7 -7
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +309 -0
- package/src/session-controller.ts +900 -0
- package/src/share.ts +113 -0
- package/src/test-support.test.ts +55 -0
- package/src/test-support.ts +187 -0
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +36 -15
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +19 -8
- package/src/view-diff.ts +34 -23
- package/src/view-plan.test.ts +294 -0
- package/src/view-plan.ts +408 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +70 -0
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -0
package/src/App.test.tsx
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Virtual-terminal component tests (tier 2): the real App over a real
|
|
3
|
-
* in-process daemon in a temp home. Char-frame assertions + mock keys -
|
|
4
|
-
* the whole review loop drivable without a terminal.
|
|
5
|
-
*/
|
|
1
|
+
/** Virtual-terminal component tests (tier 2): the real App over a real in-process daemon in a temp home. Char-frame assertions + mock keys - the whole review loop drivable without a terminal. */
|
|
6
2
|
|
|
7
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
4
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
@@ -13,6 +9,13 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
13
9
|
import { DaemonServer } from "@cueloop/daemon";
|
|
14
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
15
11
|
import { App } from "./App";
|
|
12
|
+
import {
|
|
13
|
+
isolateUserConfig,
|
|
14
|
+
press,
|
|
15
|
+
typeText as type,
|
|
16
|
+
waitForState,
|
|
17
|
+
waitForText,
|
|
18
|
+
} from "./test-support";
|
|
16
19
|
|
|
17
20
|
const PLAN = `# Migration Plan
|
|
18
21
|
|
|
@@ -27,19 +30,26 @@ The daemon persists sessions to disk atomically.
|
|
|
27
30
|
`;
|
|
28
31
|
|
|
29
32
|
let home: string;
|
|
33
|
+
let restoreUserConfig: () => void;
|
|
30
34
|
let server: DaemonServer;
|
|
31
35
|
let session: ReviewSession;
|
|
32
36
|
|
|
33
37
|
beforeEach(() => {
|
|
34
38
|
home = mkdtempSync(join(tmpdir(), "cueloop-app-"));
|
|
39
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
35
40
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
36
41
|
server.start();
|
|
37
42
|
session = server.core.sessionCreate({
|
|
38
43
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
39
|
-
artifact: {
|
|
44
|
+
artifact: {
|
|
45
|
+
type: "plan",
|
|
46
|
+
content: PLAN,
|
|
47
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
48
|
+
},
|
|
40
49
|
});
|
|
41
50
|
});
|
|
42
51
|
afterEach(() => {
|
|
52
|
+
restoreUserConfig();
|
|
43
53
|
server.stop();
|
|
44
54
|
rmSync(home, { recursive: true, force: true });
|
|
45
55
|
});
|
|
@@ -49,78 +59,72 @@ async function renderApp(sessionId?: string) {
|
|
|
49
59
|
width: 120,
|
|
50
60
|
height: 32,
|
|
51
61
|
});
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
await Bun.sleep(25);
|
|
55
|
-
await setup.renderOnce();
|
|
56
|
-
}
|
|
57
|
-
await setup.renderOnce();
|
|
62
|
+
// the async daemon connect + first fetch land within the frame wait
|
|
63
|
+
await waitForText(setup, "cueloop");
|
|
58
64
|
return setup;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
62
|
-
|
|
63
|
-
/** Drive one key press: letters type as text; named keys use KeyCodes ids. */
|
|
64
|
-
async function press(setup: Setup, k: string): Promise<void> {
|
|
65
|
-
if (k === "enter") setup.mockInput.pressKey("RETURN");
|
|
66
|
-
else if (k === "escape") setup.mockInput.pressKey("ESCAPE");
|
|
67
|
-
else if (k === "backspace") setup.mockInput.pressKey("BACKSPACE");
|
|
68
|
-
else if (k === "left") setup.mockInput.pressKey("ARROW_LEFT");
|
|
69
|
-
else if (k === "right") setup.mockInput.pressKey("ARROW_RIGHT");
|
|
70
|
-
else await type(setup, k);
|
|
71
|
-
await Bun.sleep(15);
|
|
72
|
-
await setup.renderOnce();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function type(setup: Setup, text: string): Promise<void> {
|
|
76
|
-
await setup.mockInput.typeText(text);
|
|
77
|
-
await Bun.sleep(15);
|
|
78
|
-
await setup.renderOnce();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
67
|
describe("plan rendering", () => {
|
|
82
68
|
test("renders the plan with headings, list markers, and the rail", async () => {
|
|
69
|
+
// Arrange
|
|
83
70
|
const setup = await renderApp();
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
84
73
|
const frame = setup.captureCharFrame();
|
|
85
74
|
expect(frame).toContain("Migration Plan");
|
|
86
75
|
expect(frame).toContain("Context");
|
|
87
76
|
expect(frame).toContain("persists sessions to disk atomically");
|
|
88
77
|
expect(frame).toContain("- move the store");
|
|
89
|
-
expect(frame).toContain("
|
|
90
|
-
expect(frame).toContain("Submit review
|
|
78
|
+
expect(frame).toContain("Review");
|
|
79
|
+
expect(frame).toContain("Submit review");
|
|
91
80
|
});
|
|
92
81
|
});
|
|
93
82
|
|
|
94
83
|
describe("keyboard grammar", () => {
|
|
95
84
|
test("j/k moves the cursor glyph between blocks", async () => {
|
|
85
|
+
// Arrange
|
|
96
86
|
const setup = await renderApp();
|
|
87
|
+
|
|
88
|
+
// Act
|
|
97
89
|
await press(setup, "j");
|
|
98
90
|
await press(setup, "j");
|
|
99
91
|
await setup.renderOnce();
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
100
94
|
const lines = setup.captureCharFrame().split("\n");
|
|
101
|
-
const cursorLine = lines.find((
|
|
95
|
+
const cursorLine = lines.find((line) => line.includes("▎"))!;
|
|
102
96
|
expect(cursorLine).toContain("persists sessions");
|
|
103
97
|
});
|
|
104
98
|
|
|
105
99
|
test("comment flow: c types a body, ⏎ saves to the daemon", async () => {
|
|
100
|
+
// Arrange
|
|
106
101
|
const setup = await renderApp();
|
|
102
|
+
|
|
103
|
+
// Act
|
|
107
104
|
await press(setup, "j");
|
|
108
105
|
await press(setup, "j");
|
|
109
106
|
await press(setup, "c");
|
|
110
107
|
await setup.renderOnce();
|
|
111
|
-
|
|
108
|
+
|
|
109
|
+
// Assert
|
|
110
|
+
await waitForText(setup, 'comment on "The daemon');
|
|
111
|
+
|
|
112
|
+
// Act
|
|
112
113
|
await type(setup, "Define atomically.");
|
|
113
114
|
await press(setup, "enter");
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
await waitForText(setup, "COMMENT");
|
|
116
118
|
const stored = server.core.sessionGet(session.id);
|
|
117
119
|
expect(stored.annotations.length).toBe(1);
|
|
118
120
|
expect(stored.annotations[0]!.body).toBe("Define atomically.");
|
|
119
|
-
expect(setup.captureCharFrame()).toContain("COMMENT");
|
|
120
121
|
});
|
|
121
122
|
|
|
122
123
|
test("span mode: v + l selects words, c anchors the exact span", async () => {
|
|
124
|
+
// Arrange
|
|
123
125
|
const setup = await renderApp();
|
|
126
|
+
|
|
127
|
+
// Act
|
|
124
128
|
await press(setup, "j");
|
|
125
129
|
await press(setup, "j");
|
|
126
130
|
await press(setup, "v");
|
|
@@ -128,24 +132,31 @@ describe("keyboard grammar", () => {
|
|
|
128
132
|
await press(setup, "c");
|
|
129
133
|
await type(setup, "Which daemon?");
|
|
130
134
|
await press(setup, "enter");
|
|
131
|
-
|
|
135
|
+
|
|
136
|
+
// Assert
|
|
137
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
132
138
|
const stored = server.core.sessionGet(session.id);
|
|
133
139
|
expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
|
|
134
140
|
});
|
|
135
141
|
|
|
136
142
|
test("x cuts a block into the working copy; x restores it", async () => {
|
|
143
|
+
// Arrange
|
|
137
144
|
const setup = await renderApp();
|
|
138
145
|
// move to "- move the store" (h1, h2, p, h2 = 4 steps in)
|
|
139
146
|
for (let i = 0; i < 4; i++) await press(setup, "j");
|
|
147
|
+
|
|
148
|
+
// Act
|
|
140
149
|
await press(setup, "x");
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
|
|
151
|
+
// Assert - the rail shows the cut as a removal card titled CUT · me
|
|
152
|
+
await waitForText(setup, "CUT · me");
|
|
143
153
|
expect(server.core.sessionGet(session.id).workingCopy).not.toContain("move the store");
|
|
144
|
-
|
|
154
|
+
|
|
155
|
+
// Act
|
|
145
156
|
await press(setup, "x");
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
|
|
158
|
+
// Assert
|
|
159
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy === undefined);
|
|
149
160
|
});
|
|
150
161
|
|
|
151
162
|
test("e runs $EDITOR on the working copy and tracks the diff", async () => {
|
|
@@ -154,12 +165,15 @@ describe("keyboard grammar", () => {
|
|
|
154
165
|
Bun.spawnSync(["chmod", "+x", script]);
|
|
155
166
|
process.env.CUELOOP_EDITOR = script;
|
|
156
167
|
try {
|
|
168
|
+
// Arrange
|
|
157
169
|
const setup = await renderApp();
|
|
170
|
+
|
|
171
|
+
// Act
|
|
158
172
|
await press(setup, "e");
|
|
159
|
-
|
|
160
|
-
|
|
173
|
+
|
|
174
|
+
// Assert
|
|
175
|
+
await waitForText(setup, "[edited]");
|
|
161
176
|
expect(server.core.sessionGet(session.id).workingCopy).toContain("very atomically");
|
|
162
|
-
expect(setup.captureCharFrame()).toContain("[edited]");
|
|
163
177
|
} finally {
|
|
164
178
|
delete process.env.CUELOOP_EDITOR;
|
|
165
179
|
}
|
|
@@ -167,51 +181,96 @@ describe("keyboard grammar", () => {
|
|
|
167
181
|
});
|
|
168
182
|
|
|
169
183
|
describe("submit", () => {
|
|
170
|
-
test("⏎ opens the
|
|
184
|
+
test("⏎ opens the rail confirm card; verdict + summary resolve the session", async () => {
|
|
185
|
+
// Arrange
|
|
171
186
|
const setup = await renderApp();
|
|
172
187
|
await press(setup, "j");
|
|
173
188
|
await press(setup, "j");
|
|
174
189
|
await press(setup, "c");
|
|
175
190
|
await type(setup, "Needs a phase list.");
|
|
176
191
|
await press(setup, "enter");
|
|
177
|
-
await
|
|
192
|
+
await waitForText(setup, "Needs a phase list");
|
|
193
|
+
|
|
194
|
+
// Act
|
|
178
195
|
await press(setup, "enter"); // open submit
|
|
179
|
-
|
|
180
|
-
|
|
196
|
+
|
|
197
|
+
// Assert
|
|
198
|
+
await waitForText(setup, "[Changes]");
|
|
199
|
+
|
|
200
|
+
// Act
|
|
181
201
|
await type(setup, "Expand the steps.");
|
|
182
202
|
await press(setup, "enter");
|
|
183
|
-
|
|
184
|
-
|
|
203
|
+
|
|
204
|
+
// Assert
|
|
205
|
+
await waitForText(setup, "feedback sent");
|
|
185
206
|
const stored = server.core.sessionGet(session.id);
|
|
186
207
|
expect(stored.status).toBe("resolved");
|
|
187
208
|
expect(stored.verdict!.kind).toBe("request_changes");
|
|
188
209
|
expect(stored.verdict!.feedback).toContain("Needs a phase list.");
|
|
189
|
-
|
|
210
|
+
// submit hands the reviewer back to the agent via the completion overlay
|
|
211
|
+
expect(setup.captureCharFrame()).toContain("feedback sent");
|
|
190
212
|
});
|
|
191
213
|
|
|
192
214
|
test("approve via ←/→ verdict cycling", async () => {
|
|
215
|
+
// Arrange
|
|
193
216
|
const setup = await renderApp();
|
|
217
|
+
|
|
218
|
+
// Act
|
|
194
219
|
await press(setup, "enter");
|
|
195
|
-
|
|
196
|
-
|
|
220
|
+
|
|
221
|
+
// Assert
|
|
222
|
+
await waitForText(setup, "[Approve]"); // no pending items → approve default
|
|
223
|
+
|
|
224
|
+
// Act
|
|
197
225
|
await press(setup, "enter");
|
|
198
|
-
|
|
226
|
+
|
|
227
|
+
// Assert
|
|
228
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).verdict !== undefined);
|
|
199
229
|
expect(server.core.sessionGet(session.id).verdict!.kind).toBe("approve");
|
|
200
230
|
});
|
|
201
231
|
});
|
|
202
232
|
|
|
203
233
|
describe("inbox", () => {
|
|
204
|
-
test("inbox mode
|
|
234
|
+
test("inbox mode wears the review chrome (header + menu bar) and opens a session", async () => {
|
|
235
|
+
// Arrange
|
|
205
236
|
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
expect(
|
|
211
|
-
expect(
|
|
237
|
+
await waitForText(setup, "cueloop");
|
|
238
|
+
|
|
239
|
+
// Assert - the same header/menu chrome as review, plus the session row
|
|
240
|
+
const frame = setup.captureCharFrame();
|
|
241
|
+
expect(frame).toContain("cueloop · resume");
|
|
242
|
+
expect(frame).toContain("Migration Plan");
|
|
243
|
+
expect(frame).toContain("menu"); // the shared MenuBar sits at the bottom
|
|
244
|
+
expect(frame).not.toContain("inbox ("); // the old inline header is gone
|
|
245
|
+
|
|
246
|
+
// Act
|
|
212
247
|
await press(setup, "enter");
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
248
|
+
|
|
249
|
+
// Assert
|
|
250
|
+
await waitForText(setup, "Submit review");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("the menu opens from the inbox and escape is not a trap", async () => {
|
|
254
|
+
// Arrange
|
|
255
|
+
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
256
|
+
await waitForText(setup, "cueloop · resume");
|
|
257
|
+
const lines = setup.captureCharFrame().split("\n");
|
|
258
|
+
const menuRow = lines.findIndex((line) => line.includes("menu"));
|
|
259
|
+
const menuColumn = lines[menuRow]!.indexOf("menu");
|
|
260
|
+
|
|
261
|
+
// Act - open the drop-up from the shared menu bar
|
|
262
|
+
await setup.mockMouse.click(menuColumn + 1, menuRow);
|
|
263
|
+
|
|
264
|
+
// Assert - the drop-up options appear
|
|
265
|
+
await waitForText(setup, "Keybinds");
|
|
266
|
+
expect(setup.captureCharFrame()).toContain("Settings");
|
|
267
|
+
|
|
268
|
+
// Act - escape closes the menu (not a trap), and the inbox nav still works
|
|
269
|
+
await press(setup, "escape");
|
|
270
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("Keybinds"));
|
|
271
|
+
await press(setup, "enter");
|
|
272
|
+
|
|
273
|
+
// Assert
|
|
274
|
+
await waitForText(setup, "Submit review");
|
|
216
275
|
});
|
|
217
276
|
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** The share toast is non-modal: while it is up, escape still cancels an open overlay. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, mock, test } from "bun:test";
|
|
4
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
9
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
10
|
+
import type { ReviewSession } from "@cueloop/schema";
|
|
11
|
+
|
|
12
|
+
// the controller reaches the gateway through "./share"; stub it so `S` yields a toast
|
|
13
|
+
const publishShare = mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true }));
|
|
14
|
+
mock.module("./share", () => ({
|
|
15
|
+
publishShare,
|
|
16
|
+
pullShare: mock(async () => null),
|
|
17
|
+
pushShare: mock(async () => {}),
|
|
18
|
+
shareIdFromLine: (line: string) => line.match(/^ssh (\S+)@/)?.[1],
|
|
19
|
+
collaboratorAnnotations: () => [],
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const { App } = await import("./App");
|
|
23
|
+
const { isolateUserConfig, press, waitForText, waitForTextGone } = await import("./test-support");
|
|
24
|
+
|
|
25
|
+
const PLAN = `# Migration Plan\n\n## Context\n\nThe daemon persists sessions to disk atomically.\n`;
|
|
26
|
+
|
|
27
|
+
let home: string;
|
|
28
|
+
let restoreUserConfig: () => void;
|
|
29
|
+
let server: DaemonServer;
|
|
30
|
+
let session: ReviewSession;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-toast-"));
|
|
34
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
35
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
36
|
+
server.start();
|
|
37
|
+
session = server.core.sessionCreate({
|
|
38
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
39
|
+
artifact: {
|
|
40
|
+
type: "plan",
|
|
41
|
+
content: PLAN,
|
|
42
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
restoreUserConfig();
|
|
48
|
+
server.stop();
|
|
49
|
+
rmSync(home, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("share toast", () => {
|
|
53
|
+
test("escape cancels an open composer even while the toast is up", async () => {
|
|
54
|
+
// Arrange
|
|
55
|
+
const setup = await testRender(<App home={home} sessionId={session.id} />, {
|
|
56
|
+
width: 120,
|
|
57
|
+
height: 32,
|
|
58
|
+
});
|
|
59
|
+
await waitForText(setup, "cueloop");
|
|
60
|
+
|
|
61
|
+
// Act: share raises the toast, then open a composer under it
|
|
62
|
+
await press(setup, "S");
|
|
63
|
+
await waitForText(setup, "share link copied");
|
|
64
|
+
await press(setup, "j");
|
|
65
|
+
await press(setup, "j");
|
|
66
|
+
await press(setup, "c");
|
|
67
|
+
await waitForText(setup, 'comment on "');
|
|
68
|
+
|
|
69
|
+
// Act: escape must reach the composer, not get eaten by the toast
|
|
70
|
+
await press(setup, "escape");
|
|
71
|
+
|
|
72
|
+
// Assert: the composer closed on the first escape
|
|
73
|
+
await waitForTextGone(setup, 'comment on "');
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/App.tsx
CHANGED
|
Binary file
|