@cueloop/client 0.1.0-alpha.21 → 0.1.0-alpha.23

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.23",
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.23",
11
+ "@cueloop/extension-api": "0.1.0-alpha.23",
12
+ "@cueloop/integration-obsidian": "0.1.0-alpha.23",
13
+ "@cueloop/schema": "0.1.0-alpha.23",
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,81 @@ 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
+
88
+ describe("edit affordance", () => {
89
+ test("the owner sees the Edit button", async () => {
90
+ // Arrange / Act
91
+ const owner = await renderApp();
92
+
93
+ // Assert
94
+ expect(owner.captureCharFrame()).toContain("Edit");
95
+ });
96
+
97
+ test("a read-only viewer (a plan shared over ssh) never sees the Edit button", async () => {
98
+ // Arrange / Act
99
+ const viewer = await testRender(<App home={home} sessionId={session.id} readOnly />, { width: 120, height: 32 });
100
+ await waitForText(viewer, "cueloop");
101
+
102
+ // Assert
103
+ expect(viewer.captureCharFrame()).not.toContain("Edit");
104
+ });
105
+ });
106
+
84
107
  describe("selection feeds the annotation quote", () => {
85
108
  test("v anchors, l extends, c composes with the char-precise quote", async () => {
109
+ // Arrange
86
110
  const setup = await renderApp();
87
111
  await toContextParagraph(setup);
112
+
113
+ // Act
88
114
  await press(setup, "v");
89
115
  await press(setup, "l"); // "The daemon"
90
116
  await press(setup, "c");
117
+
118
+ // Assert
91
119
  expect(setup.captureCharFrame()).toContain('comment on "The daemon"');
120
+
121
+ // Act
92
122
  await type(setup, "Which daemon?");
93
123
  await press(setup, "enter");
124
+
125
+ // Assert
94
126
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
95
127
  const stored = server.core.sessionGet(session.id);
96
128
  expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
97
129
  });
98
130
 
99
131
  test("a mouse drag over the text becomes the compose quote", async () => {
132
+ // Arrange
100
133
  const setup = await renderApp();
101
134
  const lines = setup.captureCharFrame().split("\n");
102
135
  const row = lines.findIndex((line) => line.includes("persists sessions"));
103
136
  const startColumn = lines[row]!.indexOf("persists");
104
137
  expect(row).toBeGreaterThan(-1);
138
+
139
+ // Act
105
140
  // the drag's end cell is exclusive: release one past the last character
106
141
  await setup.mockMouse.drag(startColumn, row, startColumn + "persists sessions".length, row);
107
142
  await settle(setup);
108
143
  await press(setup, "c");
144
+
145
+ // Assert
109
146
  expect(setup.captureCharFrame()).toContain('comment on "persists sessions"');
147
+
148
+ // Act
110
149
  await type(setup, "sessions plural?");
111
150
  await press(setup, "enter");
151
+
152
+ // Assert
112
153
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
113
154
  const stored = server.core.sessionGet(session.id);
114
155
  expect(stored.annotations[0]!.anchor.quote).toBe("persists sessions");
@@ -117,25 +158,38 @@ describe("selection feeds the annotation quote", () => {
117
158
 
118
159
  describe("inline compose keeps the anchor painted", () => {
119
160
  test("the selection stays painted while composing; cancel un-paints; save converts to the comment highlight", async () => {
161
+ // Arrange
120
162
  const setup = await renderApp();
121
163
  await toContextParagraph(setup);
164
+
165
+ // Act
122
166
  await press(setup, "v");
123
167
  await press(setup, "l");
124
168
  await press(setup, "c");
169
+
170
+ // Assert
125
171
  // compose open: the anchor is painted selection-style. The box can render
126
172
  // a frame before the anchor repaint settles, so wait on the color itself.
127
173
  expect(setup.captureCharFrame()).toContain("Save ⏎");
128
174
  expect(setup.captureCharFrame()).toContain("Cancel esc");
129
175
  await waitForState(setup, () => backgroundsOf(setup, "The daemon").includes(T.accent));
130
176
  expect(backgroundsOf(setup, "The daemon")).toContain(T.accent);
177
+
178
+ // Act
131
179
  // cancel un-paints (a bare ESC settles after the parser's escape window)
132
180
  await press(setup, "escape");
181
+
182
+ // Assert
133
183
  await waitForTextGone(setup, 'comment on "');
134
184
  expect(backgroundsOf(setup, "The daemon")).not.toContain(T.accent);
185
+
186
+ // Act
135
187
  // save converts the paint to the kind-colored annotation highlight
136
188
  await press(setup, "c");
137
189
  await type(setup, "Which daemon?");
138
190
  await press(setup, "enter");
191
+
192
+ // Assert
139
193
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
140
194
  await settle(setup);
141
195
  // the whole cursor block is the anchor here; check the highlight landed
@@ -167,42 +221,65 @@ describe("compose newline convention", () => {
167
221
  }
168
222
 
169
223
  test("option+return inserts a newline; a bare return submits the multiline body", async () => {
224
+ // Arrange
170
225
  const setup = await renderModifierAwareApp();
171
226
  await toContextParagraph(setup);
172
227
  await press(setup, "v");
173
228
  await press(setup, "l");
174
229
  await press(setup, "c");
175
230
  await waitForText(setup, 'comment on "The daemon"');
231
+
232
+ // Act
176
233
  await type(setup, "first line");
177
234
  await pressReturnWith(setup, { meta: true }); // option/alt+return -> newline
178
235
  await type(setup, "second line");
236
+
237
+ // Assert
179
238
  // still composing: the newline did not submit
180
239
  expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
240
+
241
+ // Act
181
242
  await press(setup, "enter"); // bare return -> save
243
+
244
+ // Assert
182
245
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
183
246
  expect(server.core.sessionGet(session.id).annotations[0]!.body).toBe("first line\nsecond line");
184
247
  });
185
248
 
186
249
  test("shift+return still inserts a newline (existing muscle memory)", async () => {
250
+ // Arrange
187
251
  const setup = await renderModifierAwareApp();
188
252
  await toContextParagraph(setup);
189
253
  await press(setup, "c");
254
+
255
+ // Act
190
256
  await type(setup, "alpha");
191
257
  await pressReturnWith(setup, { shift: true }); // shift+return -> newline
192
258
  await type(setup, "beta");
259
+
260
+ // Assert
193
261
  expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
262
+
263
+ // Act
194
264
  await press(setup, "enter");
265
+
266
+ // Assert
195
267
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
196
268
  expect(server.core.sessionGet(session.id).annotations[0]!.body).toBe("alpha\nbeta");
197
269
  });
198
270
 
199
271
  test("escape cancels the composer without saving", async () => {
272
+ // Arrange
200
273
  const setup = await renderModifierAwareApp();
201
274
  await toContextParagraph(setup);
202
275
  await press(setup, "c");
203
276
  await waitForText(setup, 'comment on "');
204
277
  await type(setup, "never saved");
278
+
279
+ // Act
205
280
  await press(setup, "escape");
281
+
282
+ // Assert
206
283
  await waitForTextGone(setup, 'comment on "');
207
284
  expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
208
285
  });
@@ -210,16 +287,21 @@ describe("compose newline convention", () => {
210
287
 
211
288
  describe("the document selects, the rail edits", () => {
212
289
  test("e edits the selected card in place; enter saves through the controller", async () => {
290
+ // Arrange
213
291
  const setup = await renderApp();
214
292
  await toContextParagraph(setup);
215
293
  await press(setup, "c");
216
294
  await type(setup, "Needs a citation.");
217
295
  await press(setup, "enter");
218
296
  await waitForText(setup, "COMMENT · pending");
297
+
298
+ // Act
219
299
  // the saved card is selected; e turns its body into an input in place
220
300
  await press(setup, "e");
221
301
  await type(setup, " And a link.");
222
302
  await press(setup, "enter");
303
+
304
+ // Assert
223
305
  await waitForState(
224
306
  setup,
225
307
  () => server.core.sessionGet(session.id).annotations[0]!.body === "Needs a citation. And a link.",
@@ -227,6 +309,7 @@ describe("the document selects, the rail edits", () => {
227
309
  });
228
310
 
229
311
  test("x deletes the selected card and un-paints the document highlight", async () => {
312
+ // Arrange
230
313
  const setup = await renderApp();
231
314
  await toContextParagraph(setup);
232
315
  await press(setup, "c");
@@ -235,28 +318,73 @@ describe("the document selects, the rail edits", () => {
235
318
  await waitForText(setup, "COMMENT · pending");
236
319
  expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
237
320
  expect(backgroundsOf(setup, "persists sessions")).toContain(T.markCommentBg);
321
+
322
+ // Act
238
323
  await press(setup, "x");
324
+
325
+ // Assert
239
326
  await waitForTextGone(setup, "COMMENT · pending");
240
327
  expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
241
328
  expect(backgroundsOf(setup, "persists sessions")).not.toContain(T.markCommentBg);
242
329
  });
243
330
  });
244
331
 
332
+ describe("addressed annotations leave the open list", () => {
333
+ test("a revision that addresses one card hides it, un-paints it, and shows the count", async () => {
334
+ // Arrange: two annotations through the daemon, then a revision addressing one
335
+ const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
336
+ const blocks = parseBlocks(PLAN);
337
+ const contextBlockIndex = blocks.findIndex((block) => block.text.startsWith("The daemon persists"));
338
+ server.core.sessionAnnotate(session.id, {
339
+ id: "a_settled",
340
+ kind: "comment",
341
+ anchor: makeAnchor(blocks, contextBlockIndex, 0, 10), // "The daemon"
342
+ body: "settled note",
343
+ });
344
+ server.core.sessionAnnotate(session.id, {
345
+ id: "a_open",
346
+ kind: "comment",
347
+ anchor: makeAnchor(blocks, contextBlockIndex, 11, 19), // "persists"
348
+ body: "still open note",
349
+ });
350
+ server.core.sessionSubmitRevision(session.id, PLAN, ["a_settled"]);
351
+
352
+ // Act
353
+ const setup = await renderApp();
354
+ await waitForText(setup, "still open note");
355
+
356
+ // Assert: the open card renders, the addressed one is gone behind the count
357
+ const frame = setup.captureCharFrame();
358
+ expect(frame).toContain("✓ 1 addressed by revision");
359
+ expect(frame).not.toContain("settled note");
360
+ expect(frame).toContain("Review (1)"); // the addressed card no longer counts as pending
361
+ expect(backgroundsOf(setup, "The daemon")).not.toContain(T.markCommentBg); // no highlight paint
362
+ });
363
+ });
364
+
245
365
  describe("sheet header", () => {
246
366
  test("shows the submitting agent, the revision, and the Edit word-button", async () => {
367
+ // Arrange
247
368
  const setup = await renderApp();
369
+
370
+ // Assert
248
371
  const frame = setup.captureCharFrame();
249
372
  expect(frame).toContain("submitted by agent/worker-3 · revision 1");
250
373
  expect(frame).toContain("Edit");
251
374
  });
252
375
 
253
376
  test("the Agent tab shows the submitter, status, and revision", async () => {
377
+ // Arrange
254
378
  const setup = await renderApp();
255
379
  const lines = setup.captureCharFrame().split("\n");
256
380
  const tabRow = lines.findIndex((line) => line.includes("Review (0)"));
257
381
  const tabColumn = lines[tabRow]!.indexOf("Agent");
382
+
383
+ // Act
258
384
  await setup.mockMouse.click(tabColumn + 1, tabRow);
259
385
  await setup.renderOnce();
386
+
387
+ // Assert
260
388
  const frame = setup.captureCharFrame();
261
389
  expect(frame).toContain("agent/worker-3");
262
390
  expect(frame).toContain("status: pending");
@@ -271,16 +399,21 @@ describe("edit-exit reconciliation", () => {
271
399
  Bun.spawnSync(["chmod", "+x", script]);
272
400
  process.env.CUELOOP_EDITOR = script;
273
401
  try {
402
+ // Arrange
274
403
  const setup = await renderApp();
275
404
  await toContextParagraph(setup);
276
405
  await press(setup, "c");
277
406
  await type(setup, "Anchor me to the doomed passage.");
278
407
  await press(setup, "enter");
279
408
  await waitForText(setup, "· pending");
409
+
410
+ // Act
280
411
  // deselect the card so e reaches the editor hand-off, then edit
281
412
  await press(setup, "escape");
282
413
  await waitForTextGone(setup, "▸ COMMENT");
283
414
  await press(setup, "e");
415
+
416
+ // Assert
284
417
  await waitForText(setup, "1 annotation no longer match - the passage was removed.");
285
418
  // the rail card flips to ORPHANED once the working-copy refresh lands
286
419
  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");