@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 CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.21",
3
+ "version": "0.1.0-alpha.22",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.ts"
8
8
  },
9
9
  "dependencies": {
10
- "@cueloop/daemon": "0.1.0-alpha.21",
11
- "@cueloop/extension-api": "0.1.0-alpha.21",
12
- "@cueloop/integration-obsidian": "0.1.0-alpha.21",
13
- "@cueloop/schema": "0.1.0-alpha.21",
10
+ "@cueloop/daemon": "0.1.0-alpha.22",
11
+ "@cueloop/extension-api": "0.1.0-alpha.22",
12
+ "@cueloop/integration-obsidian": "0.1.0-alpha.22",
13
+ "@cueloop/schema": "0.1.0-alpha.22",
14
14
  "@opentui/core": "^0.5.1",
15
15
  "@opentui/keymap": "0.5.1",
16
16
  "@opentui/react": "^0.5.1",
@@ -1,8 +1,4 @@
1
- /**
2
- * Post-submit completion overlay: prompt, opt-in persistence, countdown,
3
- * dismissal. Countdown seconds tick on an injected ManualClock, so the
4
- * tests advance time instead of waiting it out.
5
- */
1
+ /** Post-submit completion overlay: prompt, opt-in persistence, countdown, dismissal. Countdown seconds tick on an injected ManualClock, so tests advance time instead of waiting it out. */
6
2
 
7
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
8
4
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -61,49 +57,77 @@ async function submitApprove(setup: Setup): Promise<void> {
61
57
 
62
58
  describe("completion overlay", () => {
63
59
  test("submit with auto-close off counts down from 5; ⏎ exits now", async () => {
60
+ // Arrange
64
61
  let exited = -1;
65
62
  const setup = await renderApp((code) => (exited = code));
63
+
64
+ // Act
66
65
  await submitApprove(setup);
66
+
67
+ // Assert
67
68
  const frame = await waitForText(setup, "closing in 5s");
68
69
  expect(frame).toContain("review approved");
69
70
  expect(frame).toContain("close [return]");
70
71
  expect(frame).toContain("return to plan [esc]");
72
+
73
+ // Act
71
74
  await press(setup, "enter");
75
+
76
+ // Assert
72
77
  expect(exited).toBe(0);
73
78
  });
74
79
 
75
80
  test("a remembers the countdown as the persisted default", async () => {
81
+ // Arrange
76
82
  const setup = await renderApp();
77
83
  await submitApprove(setup);
78
84
  await waitForText(setup, "closing in 5s");
85
+
86
+ // Act
79
87
  await press(setup, "a");
88
+
89
+ // Assert
80
90
  expect(existsSync(configPath)).toBe(true);
81
91
  expect(readFileSync(configPath, "utf8")).toContain("auto_close = 5");
92
+
93
+ // Act
82
94
  // the countdown keeps running; each second is one manual-clock tick
83
95
  clock.advance(1000);
96
+
97
+ // Assert
84
98
  await waitForText(setup, "closing in 4s");
85
99
  });
86
100
 
87
101
  test("esc dismisses to the resolved read-only view", async () => {
102
+ // Arrange
88
103
  const setup = await renderApp();
89
104
  await submitApprove(setup);
90
105
  await waitForText(setup, "closing in 5s");
106
+
107
+ // Act
91
108
  // a bare ESC sits in the input parser's escape-sequence disambiguation
92
109
  // window before it is delivered - the frame wait absorbs it
93
110
  await press(setup, "escape");
111
+
112
+ // Assert
94
113
  const frame = await waitForText(setup, "resolved: approve");
95
114
  expect(frame).not.toContain("closing in");
96
115
  });
97
116
 
98
117
  test("configured auto_close counts down and exits without interaction", async () => {
118
+ // Arrange
99
119
  writeFileSync(configPath, "[ui]\nauto_close = 1\n");
100
120
  let exited = -1;
101
121
  const setup = await renderApp((code) => (exited = code));
102
122
  await submitApprove(setup);
103
123
  await waitForText(setup, "closing in 1s");
104
124
  expect(exited).toBe(-1);
125
+
126
+ // Act
105
127
  clock.advance(1000);
106
128
  await settle(setup);
129
+
130
+ // Assert
107
131
  expect(exited).toBe(0);
108
132
  });
109
133
  });
@@ -48,7 +48,10 @@ async function renderApp() {
48
48
 
49
49
  describe("diff review", () => {
50
50
  test("renders file header, hunks, and signed lines", async () => {
51
+ // Arrange
51
52
  const setup = await renderApp();
53
+
54
+ // Assert
52
55
  const frame = setup.captureCharFrame();
53
56
  expect(frame).toContain("■ src/store.ts");
54
57
  expect(frame).toContain("@@ -1,4 +1,4 @@");
@@ -57,21 +60,33 @@ describe("diff review", () => {
57
60
  });
58
61
 
59
62
  test("line comment: quote-anchored, lands in feedback with the code line", async () => {
63
+ // Arrange
60
64
  const setup = await renderApp();
65
+
66
+ // Act
61
67
  // rows: file(0), hunk(1), ctx(2), del(3), add(4)
62
68
  for (let i = 0; i < 4; i++) await press(setup, "j");
63
69
  await press(setup, "c");
70
+
71
+ // Assert
64
72
  expect(setup.captureCharFrame()).toContain("COMMENT ON");
73
+
74
+ // Act
65
75
  await setup.mockInput.typeText("Map needs an eviction story.");
66
76
  await press(setup, "enter");
77
+
78
+ // Assert
67
79
  // inline annotation card rendered under the line
68
80
  await waitForText(setup, "◆ Map needs an eviction story.");
69
81
  const stored = server.core.sessionGet(session.id);
70
82
  expect(stored.annotations.length).toBe(1);
71
83
  expect(stored.annotations[0]!.anchor.quote).toContain("new Map()");
72
84
 
85
+ // Act
73
86
  await press(setup, "enter"); // submit
74
87
  await press(setup, "enter"); // confirm request_changes
88
+
89
+ // Assert
75
90
  await waitForText(setup, "feedback sent");
76
91
  const resolved = server.core.sessionGet(session.id);
77
92
  expect(resolved.verdict!.feedback).toContain("new Map()");
@@ -79,8 +94,13 @@ describe("diff review", () => {
79
94
  });
80
95
 
81
96
  test("plan verbs are guarded in diff sessions", async () => {
97
+ // Arrange
82
98
  const setup = await renderApp();
99
+
100
+ // Act
83
101
  await press(setup, "x");
102
+
103
+ // Assert
84
104
  expect(setup.captureCharFrame()).toContain("plan-only verb");
85
105
  });
86
106
  });
@@ -1,8 +1,4 @@
1
- /**
2
- * Tier-2 wiring test for the notes-vault export: with [integrations.obsidian]
3
- * configured to export on resolve, submitting a review writes the note into
4
- * the vault and surfaces the path in the status line.
5
- */
1
+ /** Tier-2 wiring for the notes-vault export: with [integrations.obsidian] set to export on resolve, submitting a review writes the note into the vault and surfaces the path in the status line. */
6
2
 
7
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
8
4
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
@@ -50,12 +46,20 @@ afterEach(() => {
50
46
 
51
47
  describe("obsidian export on resolve", () => {
52
48
  test("submitting a review writes the plan into the vault and shows the path", async () => {
49
+ // Arrange
53
50
  const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 32 });
54
51
  await waitForText(setup, "cueloop");
55
- // open the submit overlay, keep the default verdict (approve), submit
56
- await press(setup, "enter");
52
+
53
+ // Act
54
+ await press(setup, "enter"); // open the submit overlay, keep the default verdict (approve)
55
+
56
+ // Assert
57
57
  expect(setup.captureCharFrame()).toContain("verdict");
58
- await press(setup, "enter");
58
+
59
+ // Act
60
+ await press(setup, "enter"); // submit
61
+
62
+ // Assert
59
63
  // the export is an async round-trip after resolve; the status line lands
60
64
  // on a later render than the file write
61
65
  const dir = join(vault, "cueloop");
@@ -1,10 +1,4 @@
1
- /**
2
- * The plan review surface v2: native selection feeds the annotation
3
- * quote, the inline compose box keeps its anchor painted, the rail edits what
4
- * the document selects, and edit-exit reconciliation orphans annotations
5
- * whose passage was removed. Char-frame + styled-span assertions over the
6
- * real App and a real in-process daemon.
7
- */
1
+ /** The plan review surface v2: native selection feeds the annotation quote, the inline compose box keeps its anchor painted, the rail edits what the document selects, and edit-exit reconciliation orphans annotations whose passage was removed. Char-frame + styled-span assertions over the real App and a real in-process daemon. */
8
2
 
9
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
10
4
  import { mkdtempSync, rmSync } from "node:fs";
@@ -81,34 +75,62 @@ async function toContextParagraph(setup: Setup): Promise<void> {
81
75
  await press(setup, "j");
82
76
  }
83
77
 
78
+ describe("share button", () => {
79
+ test("the owner's rail shows the one-click Share button", async () => {
80
+ // Arrange / Act
81
+ const setup = await renderApp();
82
+
83
+ // Assert
84
+ expect(setup.captureCharFrame()).toContain("Share plan");
85
+ });
86
+ });
87
+
84
88
  describe("selection feeds the annotation quote", () => {
85
89
  test("v anchors, l extends, c composes with the char-precise quote", async () => {
90
+ // Arrange
86
91
  const setup = await renderApp();
87
92
  await toContextParagraph(setup);
93
+
94
+ // Act
88
95
  await press(setup, "v");
89
96
  await press(setup, "l"); // "The daemon"
90
97
  await press(setup, "c");
98
+
99
+ // Assert
91
100
  expect(setup.captureCharFrame()).toContain('comment on "The daemon"');
101
+
102
+ // Act
92
103
  await type(setup, "Which daemon?");
93
104
  await press(setup, "enter");
105
+
106
+ // Assert
94
107
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
95
108
  const stored = server.core.sessionGet(session.id);
96
109
  expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
97
110
  });
98
111
 
99
112
  test("a mouse drag over the text becomes the compose quote", async () => {
113
+ // Arrange
100
114
  const setup = await renderApp();
101
115
  const lines = setup.captureCharFrame().split("\n");
102
116
  const row = lines.findIndex((line) => line.includes("persists sessions"));
103
117
  const startColumn = lines[row]!.indexOf("persists");
104
118
  expect(row).toBeGreaterThan(-1);
119
+
120
+ // Act
105
121
  // the drag's end cell is exclusive: release one past the last character
106
122
  await setup.mockMouse.drag(startColumn, row, startColumn + "persists sessions".length, row);
107
123
  await settle(setup);
108
124
  await press(setup, "c");
125
+
126
+ // Assert
109
127
  expect(setup.captureCharFrame()).toContain('comment on "persists sessions"');
128
+
129
+ // Act
110
130
  await type(setup, "sessions plural?");
111
131
  await press(setup, "enter");
132
+
133
+ // Assert
112
134
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
113
135
  const stored = server.core.sessionGet(session.id);
114
136
  expect(stored.annotations[0]!.anchor.quote).toBe("persists sessions");
@@ -117,25 +139,38 @@ describe("selection feeds the annotation quote", () => {
117
139
 
118
140
  describe("inline compose keeps the anchor painted", () => {
119
141
  test("the selection stays painted while composing; cancel un-paints; save converts to the comment highlight", async () => {
142
+ // Arrange
120
143
  const setup = await renderApp();
121
144
  await toContextParagraph(setup);
145
+
146
+ // Act
122
147
  await press(setup, "v");
123
148
  await press(setup, "l");
124
149
  await press(setup, "c");
150
+
151
+ // Assert
125
152
  // compose open: the anchor is painted selection-style. The box can render
126
153
  // a frame before the anchor repaint settles, so wait on the color itself.
127
154
  expect(setup.captureCharFrame()).toContain("Save ⏎");
128
155
  expect(setup.captureCharFrame()).toContain("Cancel esc");
129
156
  await waitForState(setup, () => backgroundsOf(setup, "The daemon").includes(T.accent));
130
157
  expect(backgroundsOf(setup, "The daemon")).toContain(T.accent);
158
+
159
+ // Act
131
160
  // cancel un-paints (a bare ESC settles after the parser's escape window)
132
161
  await press(setup, "escape");
162
+
163
+ // Assert
133
164
  await waitForTextGone(setup, 'comment on "');
134
165
  expect(backgroundsOf(setup, "The daemon")).not.toContain(T.accent);
166
+
167
+ // Act
135
168
  // save converts the paint to the kind-colored annotation highlight
136
169
  await press(setup, "c");
137
170
  await type(setup, "Which daemon?");
138
171
  await press(setup, "enter");
172
+
173
+ // Assert
139
174
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
140
175
  await settle(setup);
141
176
  // the whole cursor block is the anchor here; check the highlight landed
@@ -167,42 +202,65 @@ describe("compose newline convention", () => {
167
202
  }
168
203
 
169
204
  test("option+return inserts a newline; a bare return submits the multiline body", async () => {
205
+ // Arrange
170
206
  const setup = await renderModifierAwareApp();
171
207
  await toContextParagraph(setup);
172
208
  await press(setup, "v");
173
209
  await press(setup, "l");
174
210
  await press(setup, "c");
175
211
  await waitForText(setup, 'comment on "The daemon"');
212
+
213
+ // Act
176
214
  await type(setup, "first line");
177
215
  await pressReturnWith(setup, { meta: true }); // option/alt+return -> newline
178
216
  await type(setup, "second line");
217
+
218
+ // Assert
179
219
  // still composing: the newline did not submit
180
220
  expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
221
+
222
+ // Act
181
223
  await press(setup, "enter"); // bare return -> save
224
+
225
+ // Assert
182
226
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
183
227
  expect(server.core.sessionGet(session.id).annotations[0]!.body).toBe("first line\nsecond line");
184
228
  });
185
229
 
186
230
  test("shift+return still inserts a newline (existing muscle memory)", async () => {
231
+ // Arrange
187
232
  const setup = await renderModifierAwareApp();
188
233
  await toContextParagraph(setup);
189
234
  await press(setup, "c");
235
+
236
+ // Act
190
237
  await type(setup, "alpha");
191
238
  await pressReturnWith(setup, { shift: true }); // shift+return -> newline
192
239
  await type(setup, "beta");
240
+
241
+ // Assert
193
242
  expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
243
+
244
+ // Act
194
245
  await press(setup, "enter");
246
+
247
+ // Assert
195
248
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
196
249
  expect(server.core.sessionGet(session.id).annotations[0]!.body).toBe("alpha\nbeta");
197
250
  });
198
251
 
199
252
  test("escape cancels the composer without saving", async () => {
253
+ // Arrange
200
254
  const setup = await renderModifierAwareApp();
201
255
  await toContextParagraph(setup);
202
256
  await press(setup, "c");
203
257
  await waitForText(setup, 'comment on "');
204
258
  await type(setup, "never saved");
259
+
260
+ // Act
205
261
  await press(setup, "escape");
262
+
263
+ // Assert
206
264
  await waitForTextGone(setup, 'comment on "');
207
265
  expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
208
266
  });
@@ -210,16 +268,21 @@ describe("compose newline convention", () => {
210
268
 
211
269
  describe("the document selects, the rail edits", () => {
212
270
  test("e edits the selected card in place; enter saves through the controller", async () => {
271
+ // Arrange
213
272
  const setup = await renderApp();
214
273
  await toContextParagraph(setup);
215
274
  await press(setup, "c");
216
275
  await type(setup, "Needs a citation.");
217
276
  await press(setup, "enter");
218
277
  await waitForText(setup, "COMMENT · pending");
278
+
279
+ // Act
219
280
  // the saved card is selected; e turns its body into an input in place
220
281
  await press(setup, "e");
221
282
  await type(setup, " And a link.");
222
283
  await press(setup, "enter");
284
+
285
+ // Assert
223
286
  await waitForState(
224
287
  setup,
225
288
  () => server.core.sessionGet(session.id).annotations[0]!.body === "Needs a citation. And a link.",
@@ -227,6 +290,7 @@ describe("the document selects, the rail edits", () => {
227
290
  });
228
291
 
229
292
  test("x deletes the selected card and un-paints the document highlight", async () => {
293
+ // Arrange
230
294
  const setup = await renderApp();
231
295
  await toContextParagraph(setup);
232
296
  await press(setup, "c");
@@ -235,28 +299,73 @@ describe("the document selects, the rail edits", () => {
235
299
  await waitForText(setup, "COMMENT · pending");
236
300
  expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
237
301
  expect(backgroundsOf(setup, "persists sessions")).toContain(T.markCommentBg);
302
+
303
+ // Act
238
304
  await press(setup, "x");
305
+
306
+ // Assert
239
307
  await waitForTextGone(setup, "COMMENT · pending");
240
308
  expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
241
309
  expect(backgroundsOf(setup, "persists sessions")).not.toContain(T.markCommentBg);
242
310
  });
243
311
  });
244
312
 
313
+ describe("addressed annotations leave the open list", () => {
314
+ test("a revision that addresses one card hides it, un-paints it, and shows the count", async () => {
315
+ // Arrange: two annotations through the daemon, then a revision addressing one
316
+ const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
317
+ const blocks = parseBlocks(PLAN);
318
+ const contextBlockIndex = blocks.findIndex((block) => block.text.startsWith("The daemon persists"));
319
+ server.core.sessionAnnotate(session.id, {
320
+ id: "a_settled",
321
+ kind: "comment",
322
+ anchor: makeAnchor(blocks, contextBlockIndex, 0, 10), // "The daemon"
323
+ body: "settled note",
324
+ });
325
+ server.core.sessionAnnotate(session.id, {
326
+ id: "a_open",
327
+ kind: "comment",
328
+ anchor: makeAnchor(blocks, contextBlockIndex, 11, 19), // "persists"
329
+ body: "still open note",
330
+ });
331
+ server.core.sessionSubmitRevision(session.id, PLAN, ["a_settled"]);
332
+
333
+ // Act
334
+ const setup = await renderApp();
335
+ await waitForText(setup, "still open note");
336
+
337
+ // Assert: the open card renders, the addressed one is gone behind the count
338
+ const frame = setup.captureCharFrame();
339
+ expect(frame).toContain("✓ 1 addressed by revision");
340
+ expect(frame).not.toContain("settled note");
341
+ expect(frame).toContain("Review (1)"); // the addressed card no longer counts as pending
342
+ expect(backgroundsOf(setup, "The daemon")).not.toContain(T.markCommentBg); // no highlight paint
343
+ });
344
+ });
345
+
245
346
  describe("sheet header", () => {
246
347
  test("shows the submitting agent, the revision, and the Edit word-button", async () => {
348
+ // Arrange
247
349
  const setup = await renderApp();
350
+
351
+ // Assert
248
352
  const frame = setup.captureCharFrame();
249
353
  expect(frame).toContain("submitted by agent/worker-3 · revision 1");
250
354
  expect(frame).toContain("Edit");
251
355
  });
252
356
 
253
357
  test("the Agent tab shows the submitter, status, and revision", async () => {
358
+ // Arrange
254
359
  const setup = await renderApp();
255
360
  const lines = setup.captureCharFrame().split("\n");
256
361
  const tabRow = lines.findIndex((line) => line.includes("Review (0)"));
257
362
  const tabColumn = lines[tabRow]!.indexOf("Agent");
363
+
364
+ // Act
258
365
  await setup.mockMouse.click(tabColumn + 1, tabRow);
259
366
  await setup.renderOnce();
367
+
368
+ // Assert
260
369
  const frame = setup.captureCharFrame();
261
370
  expect(frame).toContain("agent/worker-3");
262
371
  expect(frame).toContain("status: pending");
@@ -271,16 +380,21 @@ describe("edit-exit reconciliation", () => {
271
380
  Bun.spawnSync(["chmod", "+x", script]);
272
381
  process.env.CUELOOP_EDITOR = script;
273
382
  try {
383
+ // Arrange
274
384
  const setup = await renderApp();
275
385
  await toContextParagraph(setup);
276
386
  await press(setup, "c");
277
387
  await type(setup, "Anchor me to the doomed passage.");
278
388
  await press(setup, "enter");
279
389
  await waitForText(setup, "· pending");
390
+
391
+ // Act
280
392
  // deselect the card so e reaches the editor hand-off, then edit
281
393
  await press(setup, "escape");
282
394
  await waitForTextGone(setup, "▸ COMMENT");
283
395
  await press(setup, "e");
396
+
397
+ // Assert
284
398
  await waitForText(setup, "1 annotation no longer match - the passage was removed.");
285
399
  // the rail card flips to ORPHANED once the working-copy refresh lands
286
400
  await waitForText(setup, "· ORPHANED");
@@ -1,8 +1,4 @@
1
- /**
2
- * Observer mode (tier 2): the App with readOnly renders and navigates like the
3
- * controller's, but every mutating verb answers "observer - read-only" and
4
- * leaves daemon state untouched.
5
- */
1
+ /** Observer mode (tier 2): the App with readOnly renders and navigates like the controller's, but every mutating verb answers "observer - read-only" and leaves daemon state untouched. */
6
2
 
7
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
8
4
  import { mkdtempSync, rmSync } from "node:fs";
@@ -62,7 +58,10 @@ function snapshot() {
62
58
 
63
59
  describe("observer rendering", () => {
64
60
  test("shows the observer badge and read-only hint bar", async () => {
61
+ // Arrange
65
62
  const setup = await renderObserver();
63
+
64
+ // Assert
66
65
  const frame = setup.captureCharFrame();
67
66
  expect(frame).toContain("· observer");
68
67
  expect(frame).toContain("observer - read-only · j/k move");
@@ -94,12 +93,17 @@ describe("observer verbs are blocked", () => {
94
93
  }
95
94
 
96
95
  test("span mode c/s is blocked too", async () => {
96
+ // Arrange
97
97
  const setup = await renderObserver();
98
+
99
+ // Act
98
100
  await press(setup, "j");
99
101
  await press(setup, "j");
100
102
  await press(setup, "v");
101
103
  await press(setup, "c");
102
104
  await setup.renderOnce();
105
+
106
+ // Assert
103
107
  const frame = setup.captureCharFrame();
104
108
  expect(frame).toContain("observer - read-only");
105
109
  expect(frame).not.toContain("COMMENT ON");
@@ -109,16 +113,22 @@ describe("observer verbs are blocked", () => {
109
113
 
110
114
  describe("observer navigation still works", () => {
111
115
  test("j/k moves the cursor between blocks", async () => {
116
+ // Arrange
112
117
  const setup = await renderObserver();
118
+
119
+ // Act
113
120
  await press(setup, "j");
114
121
  await press(setup, "j");
115
122
  await setup.renderOnce();
123
+
124
+ // Assert
116
125
  const lines = setup.captureCharFrame().split("\n");
117
126
  const cursorLine = lines.find((line) => line.includes("▎"))!;
118
127
  expect(cursorLine).toContain("persists sessions");
119
128
  });
120
129
 
121
130
  test("n focuses annotations made by the controller", async () => {
131
+ // Arrange
122
132
  server.core.sessionAnnotate(session.id, {
123
133
  id: "a_obs1",
124
134
  kind: "comment",
@@ -126,8 +136,12 @@ describe("observer navigation still works", () => {
126
136
  body: "From the controller.",
127
137
  });
128
138
  const setup = await renderObserver();
139
+
140
+ // Act
129
141
  await press(setup, "n");
130
142
  await setup.renderOnce();
143
+
144
+ // Assert
131
145
  expect(setup.captureCharFrame()).toContain("▸ COMMENT");
132
146
  });
133
147
  });
@@ -57,13 +57,19 @@ async function frame(): Promise<string> {
57
57
 
58
58
  describe("code blocks", () => {
59
59
  test("lines render verbatim: indentation preserved, no word-wrap", async () => {
60
+ // Arrange
60
61
  const rendered = await frame();
62
+
63
+ // Assert
61
64
  expect(rendered).toContain(" const blocked = full < 0.6;");
62
65
  expect(rendered).toContain(' return <text>{blocked ? "blocked" : "clear"}</text>;');
63
66
  });
64
67
 
65
68
  test("the block carries its language tag", async () => {
69
+ // Arrange
66
70
  const rendered = await frame();
71
+
72
+ // Assert
67
73
  expect(rendered).toContain("tsx");
68
74
  });
69
75
  });
@@ -74,22 +80,29 @@ describe("block spacing", () => {
74
80
  const planColumn = (line: string): string => line.split("│")[0]!.trimEnd();
75
81
 
76
82
  test("a code block never glues to the list item above it", async () => {
83
+ // Arrange
77
84
  const lines = (await frame()).split("\n").map(planColumn);
78
85
  const listItemLineIndex = lines.findIndex((line) => line.includes("wire the duck over MQTT"));
79
86
  const codeHeader = lines.findIndex((line) => line.trim().startsWith("tsx"));
87
+
88
+ // Assert
80
89
  expect(listItemLineIndex).toBeGreaterThan(-1);
81
90
  expect(codeHeader).toBeGreaterThan(listItemLineIndex);
82
91
  expect(lines.slice(listItemLineIndex + 1, codeHeader).some((line) => line === "")).toBe(true);
83
92
  });
84
93
 
85
94
  test("a heading never sits directly on the previous block", async () => {
95
+ // Arrange
86
96
  const lines = (await frame()).split("\n").map(planColumn);
87
97
  const gate = lines.findIndex((line) => line.trim() === "Gate");
98
+
99
+ // Assert
88
100
  expect(gate).toBeGreaterThan(0);
89
101
  expect(lines[gate - 1]).toBe("");
90
102
  });
91
103
 
92
104
  test("consecutive list items stay tight", async () => {
105
+ // Arrange
93
106
  const listPlan = "# T\n\n- one\n- two\n";
94
107
  const s2 = server.core.sessionCreate({
95
108
  workspace: { repoRoot: "/repo", branch: "main" },
@@ -97,6 +110,8 @@ describe("block spacing", () => {
97
110
  });
98
111
  const setup = await testRender(<App home={home} sessionId={s2.id} />, { width: 120, height: 30 });
99
112
  await waitForText(setup, "one");
113
+
114
+ // Assert
100
115
  const lines = setup.captureCharFrame().split("\n").map((line) => line.trimEnd());
101
116
  const one = lines.findIndex((line) => line.includes("- one"));
102
117
  expect(lines[one + 1]).toContain("- two");