@cueloop/client 0.1.0-alpha.21 → 0.1.0-alpha.22
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 +5 -5
- package/src/App.autoclose.test.tsx +29 -5
- package/src/App.diff.test.tsx +20 -0
- package/src/App.export.test.tsx +12 -8
- package/src/App.plan-review.test.tsx +121 -7
- package/src/App.readonly.test.tsx +19 -5
- package/src/App.rendering.test.tsx +15 -0
- package/src/App.review-panel.test.tsx +29 -6
- package/src/App.submit-confirm.test.tsx +57 -6
- package/src/App.test.tsx +62 -5
- package/src/App.tsx +71 -182
- package/src/App.walk.test.tsx +63 -8
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +26 -0
- package/src/components/ReviewRail.tsx +16 -4
- package/src/components/stories.test.tsx +5 -0
- package/src/config.test.ts +62 -0
- package/src/config.ts +1 -0
- package/src/editor.test.ts +24 -0
- package/src/herdr.test.ts +4 -0
- package/src/index.ts +2 -0
- package/src/integrations.test.ts +12 -0
- package/src/intent-dispatch.test.ts +213 -0
- package/src/intent-dispatch.ts +270 -0
- package/src/key-bindings.test.ts +16 -5
- package/src/keymap.test.ts +97 -5
- package/src/keymap.ts +24 -2
- package/src/serve.test.ts +7 -0
- package/src/session-controller.ts +22 -3
- package/src/share.ts +47 -0
- package/src/view-diff.test.ts +11 -0
- package/src/view-diff.ts +6 -6
- package/src/view-plan.test.ts +81 -0
- package/src/view-plan.ts +3 -0
- package/src/walk.test.ts +6 -0
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The review panel end to end: the b key cycles expanded -> compact -> hidden
|
|
3
|
-
* -> expanded on the live App, and the collapse mode persists to the user
|
|
4
|
-
* config so it survives a restart. Rendering details are locked by the
|
|
5
|
-
* ReviewPanel stories; this suite proves the App wiring and the persistence.
|
|
6
|
-
*/
|
|
1
|
+
/** The review panel end to end: the b key cycles expanded -> compact -> hidden -> expanded on the live App, and the collapse mode persists to the user config so it survives a restart. Rendering details are locked by the ReviewPanel stories; this suite proves the App wiring and the persistence. */
|
|
7
2
|
|
|
8
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
9
4
|
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
@@ -49,44 +44,72 @@ async function renderApp() {
|
|
|
49
44
|
|
|
50
45
|
describe("review panel", () => {
|
|
51
46
|
test("b cycles expanded -> compact -> hidden -> expanded", async () => {
|
|
47
|
+
// Arrange
|
|
52
48
|
const setup = await renderApp();
|
|
49
|
+
|
|
50
|
+
// Assert
|
|
53
51
|
expect(setup.captureCharFrame()).toContain("Submit review"); // expanded rail
|
|
54
52
|
|
|
53
|
+
// Act
|
|
55
54
|
await press(setup, "b");
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
56
57
|
let frame = setup.captureCharFrame();
|
|
57
58
|
expect(frame).toContain("«"); // compact strip shows the expand chevron
|
|
58
59
|
expect(frame).not.toContain("Submit review");
|
|
59
60
|
|
|
61
|
+
// Act
|
|
60
62
|
await press(setup, "b");
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
61
65
|
frame = setup.captureCharFrame();
|
|
62
66
|
expect(frame).not.toContain("«"); // hidden: no strip, no divider
|
|
63
67
|
expect(frame).not.toContain("│");
|
|
64
68
|
|
|
69
|
+
// Act
|
|
65
70
|
await press(setup, "b");
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
66
73
|
expect(setup.captureCharFrame()).toContain("Submit review"); // back to expanded
|
|
67
74
|
});
|
|
68
75
|
|
|
69
76
|
test("the collapse mode persists to the user config", async () => {
|
|
77
|
+
// Arrange
|
|
70
78
|
const setup = await renderApp();
|
|
79
|
+
|
|
80
|
+
// Act
|
|
71
81
|
await press(setup, "b"); // -> compact
|
|
82
|
+
|
|
83
|
+
// Assert
|
|
72
84
|
expect(loadConfig({ userConfigPath: configPath }).ui.reviewState).toBe("compact");
|
|
73
85
|
});
|
|
74
86
|
|
|
75
87
|
test("] widens the rail and persists the new width", async () => {
|
|
88
|
+
// Arrange
|
|
76
89
|
const setup = await renderApp();
|
|
77
90
|
const startWidth = loadConfig({ userConfigPath: configPath }).ui.reviewWidth;
|
|
91
|
+
|
|
92
|
+
// Act
|
|
78
93
|
await press(setup, "]");
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
79
96
|
const widened = loadConfig({ userConfigPath: configPath }).ui.reviewWidth;
|
|
80
97
|
expect(widened).toBeGreaterThan(startWidth);
|
|
81
98
|
});
|
|
82
99
|
|
|
83
100
|
test("submitting from a hidden panel force-opens the rail so the confirm card is reachable", async () => {
|
|
101
|
+
// Arrange
|
|
84
102
|
const setup = await renderApp();
|
|
85
103
|
await press(setup, "b"); // -> compact
|
|
86
104
|
await press(setup, "b"); // -> hidden
|
|
105
|
+
|
|
106
|
+
// Assert
|
|
87
107
|
expect(setup.captureCharFrame()).not.toContain("Submit review"); // rail is gone
|
|
88
108
|
|
|
109
|
+
// Act
|
|
89
110
|
await press(setup, "enter"); // submit while hidden must not trap in an invisible modal
|
|
111
|
+
|
|
112
|
+
// Assert
|
|
90
113
|
const frame = setup.captureCharFrame();
|
|
91
114
|
expect(frame).toContain("submit review"); // the confirm card is visible again
|
|
92
115
|
expect(frame).toContain("[Approve]");
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rail submit confirm (tier 2): pressing submit expands the rail's
|
|
3
|
-
* Submit button into a bordered confirm card - counts, verdict selector,
|
|
4
|
-
* summary input, word-buttons. Char-frame assertions over the real App
|
|
5
|
-
* and an in-process daemon, like App.test.tsx.
|
|
6
|
-
*/
|
|
1
|
+
/** The rail submit confirm (tier 2): pressing submit expands the rail's Submit button into a bordered confirm card - counts, verdict selector, summary input, word-buttons. Char-frame assertions over the real App and an in-process daemon, like App.test.tsx. */
|
|
7
2
|
|
|
8
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
9
4
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
@@ -72,9 +67,16 @@ function seedAnnotations(count: number): void {
|
|
|
72
67
|
|
|
73
68
|
describe("rail submit confirm", () => {
|
|
74
69
|
test("submit expands the Submit button into the confirm card", async () => {
|
|
70
|
+
// Arrange
|
|
75
71
|
const setup = await renderApp();
|
|
72
|
+
|
|
73
|
+
// Assert
|
|
76
74
|
expect(setup.captureCharFrame()).toContain("Submit review (0)");
|
|
75
|
+
|
|
76
|
+
// Act
|
|
77
77
|
await press(setup, "enter");
|
|
78
|
+
|
|
79
|
+
// Assert
|
|
78
80
|
const frame = setup.captureCharFrame();
|
|
79
81
|
// the card: title, honest counts, verdict selector, word-buttons
|
|
80
82
|
expect(frame).toContain("submit review");
|
|
@@ -89,22 +91,44 @@ describe("rail submit confirm", () => {
|
|
|
89
91
|
});
|
|
90
92
|
|
|
91
93
|
test("left/right cycles the verdict selector in the card", async () => {
|
|
94
|
+
// Arrange
|
|
92
95
|
const setup = await renderApp();
|
|
93
96
|
await press(setup, "enter");
|
|
97
|
+
|
|
98
|
+
// Assert
|
|
94
99
|
expect(setup.captureCharFrame()).toContain("[Approve]");
|
|
100
|
+
|
|
101
|
+
// Act
|
|
95
102
|
await press(setup, "right");
|
|
103
|
+
|
|
104
|
+
// Assert
|
|
96
105
|
expect(setup.captureCharFrame()).toContain("[Changes]");
|
|
106
|
+
|
|
107
|
+
// Act
|
|
97
108
|
await press(setup, "right");
|
|
109
|
+
|
|
110
|
+
// Assert
|
|
98
111
|
expect(setup.captureCharFrame()).toContain("[Comment]");
|
|
112
|
+
|
|
113
|
+
// Act
|
|
99
114
|
await press(setup, "left");
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
100
117
|
expect(setup.captureCharFrame()).toContain("[Changes]");
|
|
101
118
|
});
|
|
102
119
|
|
|
103
120
|
test("esc cancels the card and restores the plain Submit button", async () => {
|
|
121
|
+
// Arrange
|
|
104
122
|
const setup = await renderApp();
|
|
105
123
|
await press(setup, "enter");
|
|
124
|
+
|
|
125
|
+
// Assert
|
|
106
126
|
expect(setup.captureCharFrame()).toContain("[Approve]");
|
|
127
|
+
|
|
128
|
+
// Act
|
|
107
129
|
await press(setup, "escape");
|
|
130
|
+
|
|
131
|
+
// Assert
|
|
108
132
|
// a bare ESC settles after the parser's escape-sequence window
|
|
109
133
|
const frame = await waitForTextGone(setup, "[Approve]");
|
|
110
134
|
expect(frame).not.toContain("0 annotations");
|
|
@@ -112,14 +136,23 @@ describe("rail submit confirm", () => {
|
|
|
112
136
|
});
|
|
113
137
|
|
|
114
138
|
test("enter in the card resolves the session through the controller", async () => {
|
|
139
|
+
// Arrange
|
|
115
140
|
seedAnnotations(1);
|
|
116
141
|
const setup = await renderApp();
|
|
142
|
+
|
|
143
|
+
// Act
|
|
117
144
|
await press(setup, "enter");
|
|
145
|
+
|
|
146
|
+
// Assert
|
|
118
147
|
const frame = setup.captureCharFrame();
|
|
119
148
|
expect(frame).toContain("1 annotations · 0 blocking");
|
|
120
149
|
expect(frame).toContain("[Changes]"); // pending items: request changes default
|
|
150
|
+
|
|
151
|
+
// Act
|
|
121
152
|
await setup.mockInput.typeText("Tighten the steps.");
|
|
122
153
|
await press(setup, "enter");
|
|
154
|
+
|
|
155
|
+
// Assert
|
|
123
156
|
// the completion flow after submit is unchanged
|
|
124
157
|
await waitForText(setup, "feedback sent");
|
|
125
158
|
const stored = server.core.sessionGet(session.id);
|
|
@@ -128,23 +161,36 @@ describe("rail submit confirm", () => {
|
|
|
128
161
|
});
|
|
129
162
|
|
|
130
163
|
test("the annotation stack stays scrollable while the card is open", async () => {
|
|
164
|
+
// Arrange
|
|
131
165
|
seedAnnotations(12);
|
|
132
166
|
const setup = await renderApp();
|
|
167
|
+
|
|
168
|
+
// Act
|
|
133
169
|
// walk annotation focus to the last card: the rail scrolls it into view
|
|
134
170
|
for (let index = 0; index < 12; index++) await press(setup, "n");
|
|
171
|
+
|
|
172
|
+
// Assert
|
|
135
173
|
const scrolled = setup.captureCharFrame();
|
|
136
174
|
expect(scrolled).toContain("note 12");
|
|
137
175
|
expect(scrolled).not.toContain("note 01");
|
|
176
|
+
|
|
177
|
+
// Act
|
|
138
178
|
await press(setup, "enter");
|
|
179
|
+
|
|
180
|
+
// Assert
|
|
139
181
|
const opened = setup.captureCharFrame();
|
|
140
182
|
// the card is pinned outside the scrollbox; the stack above stays
|
|
141
183
|
// scrolled away from the top when the card takes its space
|
|
142
184
|
expect(opened).toContain("12 annotations · 0 blocking");
|
|
143
185
|
expect(opened).not.toContain("note 01");
|
|
186
|
+
|
|
187
|
+
// Act
|
|
144
188
|
// the stack still scrolls with the card open: wheel over the rail
|
|
145
189
|
// brings the last card back into view while the confirm card stays put
|
|
146
190
|
for (let turn = 0; turn < 12; turn++) await setup.mockMouse.scroll(100, 10, "down");
|
|
147
191
|
await settle(setup);
|
|
192
|
+
|
|
193
|
+
// Assert
|
|
148
194
|
const scrolledWithCard = setup.captureCharFrame();
|
|
149
195
|
expect(scrolledWithCard).toContain("note 12");
|
|
150
196
|
expect(scrolledWithCard).toContain("12 annotations · 0 blocking");
|
|
@@ -152,9 +198,14 @@ describe("rail submit confirm", () => {
|
|
|
152
198
|
});
|
|
153
199
|
|
|
154
200
|
test("read-only observers never see the confirm card", async () => {
|
|
201
|
+
// Arrange
|
|
155
202
|
seedAnnotations(1);
|
|
156
203
|
const setup = await renderApp({ readOnly: true });
|
|
204
|
+
|
|
205
|
+
// Act
|
|
157
206
|
await press(setup, "enter");
|
|
207
|
+
|
|
208
|
+
// Assert
|
|
158
209
|
const frame = setup.captureCharFrame();
|
|
159
210
|
expect(frame).toContain("observer - read-only");
|
|
160
211
|
expect(frame).not.toContain("1 annotations");
|
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";
|
|
@@ -57,7 +53,10 @@ async function renderApp(sessionId?: string) {
|
|
|
57
53
|
|
|
58
54
|
describe("plan rendering", () => {
|
|
59
55
|
test("renders the plan with headings, list markers, and the rail", async () => {
|
|
56
|
+
// Arrange
|
|
60
57
|
const setup = await renderApp();
|
|
58
|
+
|
|
59
|
+
// Assert
|
|
61
60
|
const frame = setup.captureCharFrame();
|
|
62
61
|
expect(frame).toContain("Migration Plan");
|
|
63
62
|
expect(frame).toContain("Context");
|
|
@@ -70,24 +69,38 @@ describe("plan rendering", () => {
|
|
|
70
69
|
|
|
71
70
|
describe("keyboard grammar", () => {
|
|
72
71
|
test("j/k moves the cursor glyph between blocks", async () => {
|
|
72
|
+
// Arrange
|
|
73
73
|
const setup = await renderApp();
|
|
74
|
+
|
|
75
|
+
// Act
|
|
74
76
|
await press(setup, "j");
|
|
75
77
|
await press(setup, "j");
|
|
76
78
|
await setup.renderOnce();
|
|
79
|
+
|
|
80
|
+
// Assert
|
|
77
81
|
const lines = setup.captureCharFrame().split("\n");
|
|
78
82
|
const cursorLine = lines.find((line) => line.includes("▎"))!;
|
|
79
83
|
expect(cursorLine).toContain("persists sessions");
|
|
80
84
|
});
|
|
81
85
|
|
|
82
86
|
test("comment flow: c types a body, ⏎ saves to the daemon", async () => {
|
|
87
|
+
// Arrange
|
|
83
88
|
const setup = await renderApp();
|
|
89
|
+
|
|
90
|
+
// Act
|
|
84
91
|
await press(setup, "j");
|
|
85
92
|
await press(setup, "j");
|
|
86
93
|
await press(setup, "c");
|
|
87
94
|
await setup.renderOnce();
|
|
95
|
+
|
|
96
|
+
// Assert
|
|
88
97
|
expect(setup.captureCharFrame()).toContain('comment on "The daemon');
|
|
98
|
+
|
|
99
|
+
// Act
|
|
89
100
|
await type(setup, "Define atomically.");
|
|
90
101
|
await press(setup, "enter");
|
|
102
|
+
|
|
103
|
+
// Assert
|
|
91
104
|
await waitForText(setup, "COMMENT");
|
|
92
105
|
const stored = server.core.sessionGet(session.id);
|
|
93
106
|
expect(stored.annotations.length).toBe(1);
|
|
@@ -95,7 +108,10 @@ describe("keyboard grammar", () => {
|
|
|
95
108
|
});
|
|
96
109
|
|
|
97
110
|
test("span mode: v + l selects words, c anchors the exact span", async () => {
|
|
111
|
+
// Arrange
|
|
98
112
|
const setup = await renderApp();
|
|
113
|
+
|
|
114
|
+
// Act
|
|
99
115
|
await press(setup, "j");
|
|
100
116
|
await press(setup, "j");
|
|
101
117
|
await press(setup, "v");
|
|
@@ -103,19 +119,30 @@ describe("keyboard grammar", () => {
|
|
|
103
119
|
await press(setup, "c");
|
|
104
120
|
await type(setup, "Which daemon?");
|
|
105
121
|
await press(setup, "enter");
|
|
122
|
+
|
|
123
|
+
// Assert
|
|
106
124
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
107
125
|
const stored = server.core.sessionGet(session.id);
|
|
108
126
|
expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
|
|
109
127
|
});
|
|
110
128
|
|
|
111
129
|
test("x cuts a block into the working copy; x restores it", async () => {
|
|
130
|
+
// Arrange
|
|
112
131
|
const setup = await renderApp();
|
|
113
132
|
// move to "- move the store" (h1, h2, p, h2 = 4 steps in)
|
|
114
133
|
for (let i = 0; i < 4; i++) await press(setup, "j");
|
|
134
|
+
|
|
135
|
+
// Act
|
|
115
136
|
await press(setup, "x");
|
|
137
|
+
|
|
138
|
+
// Assert
|
|
116
139
|
await waitForText(setup, "[cut]");
|
|
117
140
|
expect(server.core.sessionGet(session.id).workingCopy).not.toContain("move the store");
|
|
141
|
+
|
|
142
|
+
// Act
|
|
118
143
|
await press(setup, "x");
|
|
144
|
+
|
|
145
|
+
// Assert
|
|
119
146
|
await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy === undefined);
|
|
120
147
|
});
|
|
121
148
|
|
|
@@ -125,8 +152,13 @@ describe("keyboard grammar", () => {
|
|
|
125
152
|
Bun.spawnSync(["chmod", "+x", script]);
|
|
126
153
|
process.env.CUELOOP_EDITOR = script;
|
|
127
154
|
try {
|
|
155
|
+
// Arrange
|
|
128
156
|
const setup = await renderApp();
|
|
157
|
+
|
|
158
|
+
// Act
|
|
129
159
|
await press(setup, "e");
|
|
160
|
+
|
|
161
|
+
// Assert
|
|
130
162
|
await waitForText(setup, "[edited]");
|
|
131
163
|
expect(server.core.sessionGet(session.id).workingCopy).toContain("very atomically");
|
|
132
164
|
} finally {
|
|
@@ -137,6 +169,7 @@ describe("keyboard grammar", () => {
|
|
|
137
169
|
|
|
138
170
|
describe("submit", () => {
|
|
139
171
|
test("⏎ opens the rail confirm card; verdict + summary resolve the session", async () => {
|
|
172
|
+
// Arrange
|
|
140
173
|
const setup = await renderApp();
|
|
141
174
|
await press(setup, "j");
|
|
142
175
|
await press(setup, "j");
|
|
@@ -144,10 +177,18 @@ describe("submit", () => {
|
|
|
144
177
|
await type(setup, "Needs a phase list.");
|
|
145
178
|
await press(setup, "enter");
|
|
146
179
|
await waitForText(setup, "Review (1)");
|
|
180
|
+
|
|
181
|
+
// Act
|
|
147
182
|
await press(setup, "enter"); // open submit
|
|
183
|
+
|
|
184
|
+
// Assert
|
|
148
185
|
expect(setup.captureCharFrame()).toContain("[Changes]");
|
|
186
|
+
|
|
187
|
+
// Act
|
|
149
188
|
await type(setup, "Expand the steps.");
|
|
150
189
|
await press(setup, "enter");
|
|
190
|
+
|
|
191
|
+
// Assert
|
|
151
192
|
await waitForText(setup, "feedback sent");
|
|
152
193
|
const stored = server.core.sessionGet(session.id);
|
|
153
194
|
expect(stored.status).toBe("resolved");
|
|
@@ -158,10 +199,19 @@ describe("submit", () => {
|
|
|
158
199
|
});
|
|
159
200
|
|
|
160
201
|
test("approve via ←/→ verdict cycling", async () => {
|
|
202
|
+
// Arrange
|
|
161
203
|
const setup = await renderApp();
|
|
204
|
+
|
|
205
|
+
// Act
|
|
162
206
|
await press(setup, "enter");
|
|
207
|
+
|
|
208
|
+
// Assert
|
|
163
209
|
expect(setup.captureCharFrame()).toContain("[Approve]"); // no pending items → approve default
|
|
210
|
+
|
|
211
|
+
// Act
|
|
164
212
|
await press(setup, "enter");
|
|
213
|
+
|
|
214
|
+
// Assert
|
|
165
215
|
await waitForState(setup, () => server.core.sessionGet(session.id).verdict !== undefined);
|
|
166
216
|
expect(server.core.sessionGet(session.id).verdict!.kind).toBe("approve");
|
|
167
217
|
});
|
|
@@ -169,11 +219,18 @@ describe("submit", () => {
|
|
|
169
219
|
|
|
170
220
|
describe("inbox", () => {
|
|
171
221
|
test("inbox mode renders and opens a session", async () => {
|
|
222
|
+
// Arrange
|
|
172
223
|
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
173
224
|
await waitForText(setup, "inbox");
|
|
225
|
+
|
|
226
|
+
// Assert
|
|
174
227
|
expect(setup.captureCharFrame()).toContain("inbox (1 pending)");
|
|
175
228
|
expect(setup.captureCharFrame()).toContain("Migration Plan");
|
|
229
|
+
|
|
230
|
+
// Act
|
|
176
231
|
await press(setup, "enter");
|
|
232
|
+
|
|
233
|
+
// Assert
|
|
177
234
|
await waitForText(setup, "Submit review (0)");
|
|
178
235
|
});
|
|
179
236
|
});
|