revspec 0.1.0 → 0.2.1

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.
@@ -23,7 +23,7 @@ describe("readReviewFile", () => {
23
23
  id: "t1",
24
24
  line: 10,
25
25
  status: "open",
26
- messages: [{ author: "human", text: "Looks good?" }],
26
+ messages: [{ author: "reviewer", text: "Looks good?" }],
27
27
  },
28
28
  ],
29
29
  };
@@ -73,7 +73,7 @@ describe("readDraftFile", () => {
73
73
  id: "t2",
74
74
  line: 5,
75
75
  status: "pending",
76
- messages: [{ author: "ai", text: "Response here" }],
76
+ messages: [{ author: "owner", text: "Response here" }],
77
77
  },
78
78
  ],
79
79
  };
@@ -22,7 +22,7 @@ describe("isValidThread", () => {
22
22
  id: "t1",
23
23
  line: 5,
24
24
  status: "open",
25
- messages: [{ author: "human", text: "hello" }],
25
+ messages: [{ author: "reviewer", text: "hello" }],
26
26
  })
27
27
  ).toBe(true);
28
28
  });
@@ -69,7 +69,7 @@ describe("isValidReviewFile", () => {
69
69
  id: "t1",
70
70
  line: 1,
71
71
  status: "open",
72
- messages: [{ author: "human", text: "comment" }],
72
+ messages: [{ author: "reviewer", text: "comment" }],
73
73
  },
74
74
  ],
75
75
  })
@@ -19,7 +19,7 @@ describe("writeReviewFile", () => {
19
19
  id: "t1",
20
20
  line: 3,
21
21
  status: "open",
22
- messages: [{ author: "human", text: "Is this right?" }],
22
+ messages: [{ author: "reviewer", text: "Is this right?" }],
23
23
  },
24
24
  ],
25
25
  };
@@ -60,7 +60,7 @@ describe("writeDraftFile", () => {
60
60
  id: "t2",
61
61
  line: 7,
62
62
  status: "resolved",
63
- messages: [{ author: "ai", text: "Fixed." }],
63
+ messages: [{ author: "owner", text: "Fixed." }],
64
64
  },
65
65
  ],
66
66
  };
@@ -8,7 +8,7 @@ function makeThread(
8
8
  id: string,
9
9
  line: number,
10
10
  status: Thread["status"],
11
- messages: Thread["messages"] = [{ author: "human", text: "comment" }]
11
+ messages: Thread["messages"] = [{ author: "reviewer", text: "comment" }]
12
12
  ): Thread {
13
13
  return { id, line, status, messages };
14
14
  }
@@ -39,10 +39,9 @@ describe("ReviewState", () => {
39
39
  expect(state.threads[0].line).toBe(3);
40
40
  expect(state.threads[0].status).toBe("open");
41
41
  expect(state.threads[0].messages).toHaveLength(1);
42
- expect(state.threads[0].messages[0]).toEqual({
43
- author: "human",
44
- text: "needs clarification",
45
- });
42
+ expect(state.threads[0].messages[0].author).toBe("reviewer");
43
+ expect(state.threads[0].messages[0].text).toBe("needs clarification");
44
+ expect(state.threads[0].messages[0].ts).toBeGreaterThan(0);
46
45
  });
47
46
 
48
47
  it("assigns auto-incremented id", () => {
@@ -57,10 +56,9 @@ describe("ReviewState", () => {
57
56
  const state = new ReviewState(SPEC, [makeThread("t1", 2, "pending")]);
58
57
  state.replyToThread("t1", "my reply");
59
58
  expect(state.threads[0].messages).toHaveLength(2);
60
- expect(state.threads[0].messages[1]).toEqual({
61
- author: "human",
62
- text: "my reply",
63
- });
59
+ expect(state.threads[0].messages[1].author).toBe("reviewer");
60
+ expect(state.threads[0].messages[1].text).toBe("my reply");
61
+ expect(state.threads[0].messages[1].ts).toBeGreaterThan(0);
64
62
  });
65
63
 
66
64
  it("flips status to open", () => {
@@ -235,9 +233,9 @@ describe("ReviewState", () => {
235
233
  expect(state.canApprove()).toBe(false);
236
234
  });
237
235
 
238
- it("returns false when there are no threads", () => {
236
+ it("returns true when there are no threads (clean approval)", () => {
239
237
  const state = new ReviewState(SPEC, []);
240
- expect(state.canApprove()).toBe(false);
238
+ expect(state.canApprove()).toBe(true);
241
239
  });
242
240
  });
243
241
 
@@ -265,14 +263,14 @@ describe("ReviewState", () => {
265
263
  line: 1,
266
264
  status: "open",
267
265
  messages: [
268
- { author: "ai", text: "AI response" },
269
- { author: "human", text: "my draft" },
266
+ { author: "owner", text: "AI response" },
267
+ { author: "reviewer", text: "my draft" },
270
268
  ],
271
269
  },
272
270
  ]);
273
271
  state.deleteLastDraftMessage("t1");
274
272
  expect(state.threads[0].messages).toHaveLength(1);
275
- expect(state.threads[0].messages[0].author).toBe("ai");
273
+ expect(state.threads[0].messages[0].author).toBe("owner");
276
274
  });
277
275
 
278
276
  it("removes the thread entirely when it becomes empty", () => {
@@ -281,7 +279,7 @@ describe("ReviewState", () => {
281
279
  id: "t1",
282
280
  line: 1,
283
281
  status: "open",
284
- messages: [{ author: "human", text: "only message" }],
282
+ messages: [{ author: "reviewer", text: "only message" }],
285
283
  },
286
284
  ]);
287
285
  state.deleteLastDraftMessage("t1");
@@ -323,4 +321,79 @@ describe("ReviewState", () => {
323
321
  expect(state.activeThreadCount()).toEqual({ open: 0, pending: 0 });
324
322
  });
325
323
  });
324
+
325
+ describe("unread tracking", () => {
326
+ it("tracks unread owner replies", () => {
327
+ const state = new ReviewState(["line1", "line2"], []);
328
+ state.addComment(1, "fix this");
329
+ state.addOwnerReply("t1", "done", 1001);
330
+ expect(state.unreadCount()).toBe(1);
331
+ });
332
+
333
+ it("markRead clears unread for a thread", () => {
334
+ const state = new ReviewState(["line1"], []);
335
+ state.addComment(1, "fix");
336
+ state.addOwnerReply("t1", "done", 1001);
337
+ state.markRead("t1");
338
+ expect(state.unreadCount()).toBe(0);
339
+ });
340
+
341
+ it("nextUnreadThread returns line of next unread thread after cursor", () => {
342
+ const state = new ReviewState(["a", "b", "c", "d", "e"], []);
343
+ state.addComment(2, "fix");
344
+ state.addComment(4, "fix too");
345
+ state.addOwnerReply("t1", "done", 1001);
346
+ state.addOwnerReply("t2", "done", 1002);
347
+ state.cursorLine = 1;
348
+ expect(state.nextUnreadThread()).toBe(2);
349
+ });
350
+
351
+ it("prevUnreadThread returns line of prev unread thread before cursor", () => {
352
+ const state = new ReviewState(["a", "b", "c", "d", "e"], []);
353
+ state.addComment(2, "fix");
354
+ state.addComment(4, "fix too");
355
+ state.addOwnerReply("t1", "done", 1001);
356
+ state.addOwnerReply("t2", "done", 1002);
357
+ state.cursorLine = 5;
358
+ expect(state.prevUnreadThread()).toBe(4);
359
+ });
360
+
361
+ it("nextUnreadThread returns null when no unread", () => {
362
+ const state = new ReviewState(["a"], []);
363
+ expect(state.nextUnreadThread()).toBeNull();
364
+ });
365
+
366
+ it("isThreadUnread returns correct state", () => {
367
+ const state = new ReviewState(["a"], []);
368
+ state.addComment(1, "fix");
369
+ expect(state.isThreadUnread("t1")).toBe(false);
370
+ state.addOwnerReply("t1", "done", 1001);
371
+ expect(state.isThreadUnread("t1")).toBe(true);
372
+ state.markRead("t1");
373
+ expect(state.isThreadUnread("t1")).toBe(false);
374
+ });
375
+
376
+ it("nextUnreadThread wraps around", () => {
377
+ const state = new ReviewState(["a", "b", "c", "d", "e"], []);
378
+ state.addComment(2, "fix");
379
+ state.addOwnerReply("t1", "done", 1001);
380
+ state.cursorLine = 4;
381
+ expect(state.nextUnreadThread()).toBe(2); // wraps to beginning
382
+ });
383
+
384
+ it("addOwnerReply sets thread status to pending", () => {
385
+ const state = new ReviewState(["a"], []);
386
+ state.addComment(1, "fix");
387
+ expect(state.threads[0].status).toBe("open");
388
+ state.addOwnerReply("t1", "done", 1001);
389
+ expect(state.threads[0].status).toBe("pending");
390
+ });
391
+
392
+ it("addOwnerReply preserves timestamp on message", () => {
393
+ const state = new ReviewState(["a"], []);
394
+ state.addComment(1, "fix");
395
+ state.addOwnerReply("t1", "done", 1234567890);
396
+ expect(state.threads[0].messages[1].ts).toBe(1234567890);
397
+ });
398
+ });
326
399
  });
@@ -9,7 +9,7 @@ function makeThread(
9
9
  id: string,
10
10
  line: number,
11
11
  status: Thread["status"],
12
- messages: Thread["messages"] = [{ author: "human", text: "comment" }]
12
+ messages: Thread["messages"] = [{ author: "reviewer", text: "comment" }]
13
13
  ): Thread {
14
14
  return { id, line, status, messages };
15
15
  }
@@ -21,10 +21,8 @@ describe("buildPagerContent", () => {
21
21
  const lines = content.split("\n");
22
22
 
23
23
  expect(lines).toHaveLength(4);
24
- // Line 1 is the cursor line, gets ">" prefix
25
24
  expect(lines[0]).toContain(" 1");
26
25
  expect(lines[0]).toContain("# Title");
27
- // Other lines get " " prefix
28
26
  expect(lines[1]).toContain(" 2");
29
27
  expect(lines[1]).toContain("Some text");
30
28
  expect(lines[2]).toContain(" 3");
@@ -37,45 +35,42 @@ describe("buildPagerContent", () => {
37
35
  const content = buildPagerContent(state);
38
36
  const lines = content.split("\n");
39
37
 
40
- // Line 3 (index 2) should have ">" prefix
41
38
  expect(lines[2]).toMatch(/^>/);
42
- // Other lines should have " " prefix
43
39
  expect(lines[0]).toMatch(/^ /);
44
40
  expect(lines[1]).toMatch(/^ /);
45
41
  expect(lines[3]).toMatch(/^ /);
46
42
  });
47
43
 
48
- it("shows status indicator for open threads", () => {
44
+ it("shows gutter indicator for open threads", () => {
49
45
  const state = new ReviewState(SPEC, [makeThread("t1", 2, "open")]);
50
46
  const content = buildPagerContent(state);
51
47
  const lines = content.split("\n");
52
48
 
53
- // Line 2 should contain the open icon (*)
54
- expect(lines[1]).toContain("*");
49
+ expect(lines[1]).toContain("\u258c");
55
50
  });
56
51
 
57
- it("shows status indicator for pending threads", () => {
52
+ it("shows gutter indicator for pending threads", () => {
58
53
  const state = new ReviewState(SPEC, [makeThread("t1", 2, "pending")]);
59
54
  const content = buildPagerContent(state);
60
55
  const lines = content.split("\n");
61
56
 
62
- expect(lines[1]).toContain("~");
57
+ expect(lines[1]).toContain("\u258c");
63
58
  });
64
59
 
65
- it("shows status indicator for resolved threads", () => {
60
+ it("shows checkmark for resolved threads", () => {
66
61
  const state = new ReviewState(SPEC, [makeThread("t1", 2, "resolved")]);
67
62
  const content = buildPagerContent(state);
68
63
  const lines = content.split("\n");
69
64
 
70
- expect(lines[1]).toContain("\u2714");
65
+ expect(lines[1]).toContain("\u2713");
71
66
  });
72
67
 
73
- it("shows status indicator for outdated threads", () => {
68
+ it("shows gutter indicator for outdated threads", () => {
74
69
  const state = new ReviewState(SPEC, [makeThread("t1", 2, "outdated")]);
75
70
  const content = buildPagerContent(state);
76
71
  const lines = content.split("\n");
77
72
 
78
- expect(lines[1]).toContain("\u26A0");
73
+ expect(lines[1]).toContain("\u258c");
79
74
  });
80
75
 
81
76
  it("shows different icons for different statuses", () => {
@@ -88,38 +83,24 @@ describe("buildPagerContent", () => {
88
83
  const content = buildPagerContent(state);
89
84
  const lines = content.split("\n");
90
85
 
91
- expect(lines[0]).toContain("*");
92
- expect(lines[1]).toContain("~");
93
- expect(lines[2]).toContain("\u2714");
94
- expect(lines[3]).toContain("\u26A0");
86
+ expect(lines[0]).toContain("\u258c");
87
+ expect(lines[1]).toContain("\u258c");
88
+ expect(lines[2]).toContain("\u2713");
89
+ expect(lines[3]).toContain("\u258c");
95
90
  });
96
91
 
97
- it("shows thread hint with latest message text", () => {
92
+ it("does not show inline comment preview (removed)", () => {
98
93
  const state = new ReviewState(SPEC, [
99
94
  makeThread("t1", 2, "open", [
100
- { author: "ai", text: "AI said something" },
101
- { author: "human", text: "My response" },
95
+ { author: "reviewer", text: "My response" },
102
96
  ]),
103
97
  ]);
104
98
  const content = buildPagerContent(state);
105
99
  const lines = content.split("\n");
106
100
 
107
- expect(lines[1]).toContain("My response");
108
- });
109
-
110
- it("truncates long comment text to 40 chars", () => {
111
- const longText =
112
- "This is a very long comment that should be truncated because it exceeds the maximum hint length";
113
- const state = new ReviewState(SPEC, [
114
- makeThread("t1", 2, "open", [{ author: "human", text: longText }]),
115
- ]);
116
- const content = buildPagerContent(state);
117
- const lines = content.split("\n");
118
-
119
- // Should be truncated to 39 chars + ellipsis
120
- expect(lines[1]).toContain("\u2026");
121
- // The full text should NOT appear
122
- expect(lines[1]).not.toContain(longText);
101
+ // Only gutter indicator, no inline text preview
102
+ expect(lines[1]).toContain("\u258c");
103
+ expect(lines[1]).not.toContain("My response");
123
104
  });
124
105
 
125
106
  it("does not show status indicator for lines without threads", () => {
@@ -127,11 +108,8 @@ describe("buildPagerContent", () => {
127
108
  const content = buildPagerContent(state);
128
109
  const lines = content.split("\n");
129
110
 
130
- // Line 1 (no thread) should not contain any status icons
131
- expect(lines[0]).not.toContain("\u{1F4AC}");
132
- expect(lines[0]).not.toContain("\u{1F535}");
133
- expect(lines[0]).not.toContain("\u2714");
134
- expect(lines[0]).not.toContain("\u26A0");
111
+ expect(lines[0]).not.toContain("\u258c");
112
+ expect(lines[0]).not.toContain("\u2713");
135
113
  });
136
114
 
137
115
  it("pads line numbers to 4 characters", () => {
@@ -139,7 +117,6 @@ describe("buildPagerContent", () => {
139
117
  const content = buildPagerContent(state);
140
118
  const lines = content.split("\n");
141
119
 
142
- // Line numbers are wrapped in ANSI gray codes, so check the raw number is present
143
120
  expect(lines[0]).toContain(" 1");
144
121
  expect(lines[0]).toContain("# Title");
145
122
  });
@@ -155,10 +132,8 @@ describe("buildPagerContent", () => {
155
132
  const content = buildPagerContent(state, "text");
156
133
  const lines = content.split("\n");
157
134
 
158
- // Lines 2 and 3 contain "text" — should be highlighted
159
135
  expect(lines[1]).toContain("Some >>text<<");
160
136
  expect(lines[2]).toContain("More >>text<<");
161
- // Lines 1 and 4 do not contain "text"
162
137
  expect(lines[0]).not.toContain(">>");
163
138
  expect(lines[3]).not.toContain(">>");
164
139
  });
@@ -180,5 +155,5 @@ describe("buildPagerContent", () => {
180
155
  expect(lines[1]).toContain("Some text");
181
156
  expect(lines[1]).not.toContain(">>");
182
157
  });
183
- });
184
158
 
159
+ });