revspec 0.4.0 → 0.6.0
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/CLAUDE.md +3 -1
- package/README.md +48 -10
- package/bun.lock +3 -0
- package/package.json +6 -3
- package/src/state/review-state.ts +5 -0
- package/src/tui/app.ts +65 -34
- package/src/tui/comment-input.ts +15 -3
- package/src/tui/help.ts +123 -38
- package/src/tui/pager.ts +36 -14
- package/src/tui/search.ts +9 -4
- package/src/tui/status-bar.ts +17 -7
- package/src/tui/thread-list.ts +1 -1
- package/src/tui/ui/keybinds.ts +4 -2
- package/src/tui/ui/markdown.ts +50 -9
- package/test/e2e/__snapshots__/snapshot.test.ts.snap +31 -0
- package/test/e2e/fixtures/spec.md +36 -0
- package/test/e2e/harness.ts +80 -0
- package/test/e2e/snapshot.test.ts +182 -0
- package/test/{cli-reply.test.ts → integration/cli-reply.test.ts} +2 -2
- package/test/{cli-watch.test.ts → integration/cli-watch.test.ts} +2 -2
- package/test/{cli.test.ts → integration/cli.test.ts} +3 -3
- package/test/{e2e-live.test.ts → integration/e2e-live.test.ts} +4 -4
- package/test/{live-interaction.test.ts → integration/live-interaction.test.ts} +4 -4
- package/test/{protocol → unit/protocol}/live-events.test.ts +1 -1
- package/test/{protocol → unit/protocol}/live-merge.test.ts +3 -3
- package/test/{protocol → unit/protocol}/merge.test.ts +2 -2
- package/test/{protocol → unit/protocol}/read.test.ts +2 -2
- package/test/{protocol → unit/protocol}/types.test.ts +1 -1
- package/test/{protocol → unit/protocol}/write.test.ts +2 -2
- package/test/{state → unit/state}/review-state.test.ts +2 -2
- package/test/{tui → unit/tui}/pager.test.ts +3 -3
- package/test/{tui → unit/tui}/ui/keybinds.test.ts +1 -1
- /package/test/{opentui-smoke.test.ts → integration/opentui-smoke.test.ts} +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
|
2
|
+
|
|
3
|
+
exports[`revspec E2E snapshots initial render 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20"`;
|
|
4
|
+
|
|
5
|
+
exports[`revspec E2E snapshots G scrolls to bottom 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 37/37 17 │ Key │ Format │ TTL │ 18├──────────────┼───────────┼─────┤ 19│ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ 20 21Tasks 22 23☑ Implement login 24☐ Add rate limitg 25 26│ Note: tokens are not encrypted at rest. 27█28Code Example █29█30\`\`\`typescript █31const token: string = await auth.login();█ 32 \`\`\` █33 █34────────────────────────────────────────█35 █ 36 End of spec. █> 37 █"`;
|
|
6
|
+
|
|
7
|
+
exports[`revspec E2E snapshots j/k moves cursor 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 6 1 Auth System Design> 6"`;
|
|
8
|
+
|
|
9
|
+
exports[`revspec E2E snapshots Ctrl+D half page down 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 2/37 1 Auth System Design> 12"`;
|
|
10
|
+
|
|
11
|
+
exports[`revspec E2E snapshots / search highlights matches 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 / Search..."`;
|
|
12
|
+
|
|
13
|
+
exports[`revspec E2E snapshots n jumps to next match 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 / Search... n"`;
|
|
14
|
+
|
|
15
|
+
exports[`revspec E2E snapshots ? opens help 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 ┌─ Help ───────────────────────────────────────┐│ ││ ▀ ││ revspec vX.Y.Z ││ ││ Quick Start ││ Navigate to a line and press c to comment ││ The AI replies in real-time via the threa ││ Press r to resolve threads, a to approve ││ Use :wq to save and quit when done review ││ ││ Thread Popup ││ Opens in INSERT mode — type and press Tab ││ Press Esc for NORMAL mode — scroll with j ││ c to reply, r to resolve, q to close. ││ ││ Navigation ││ j/k Down/up ││ gg/G Top/bottom ││ Ctrl+d/u Half page down/up ││ [j/k] navigate [q/?/Esc] close │└──────────────────────────────────────────────┘"`;
|
|
16
|
+
|
|
17
|
+
exports[`revspec E2E snapshots c opens comment input 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘"`;
|
|
18
|
+
|
|
19
|
+
exports[`revspec E2E snapshots Tab submits comment 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘ 1 open · L4/37 Thread #t1 (line 4)──│You YYYY-MM-DD HH:MM:SS▌ │This is a test commentNORMALc] reply [r] resolve [q] closerrsolve [?] help"`;
|
|
20
|
+
|
|
21
|
+
exports[`revspec E2E snapshots Esc switches to normal mode 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘ NORMALc] reply [r] resolve [q] close"`;
|
|
22
|
+
|
|
23
|
+
exports[`revspec E2E snapshots thread gutter indicator 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘ 1 open · L4/37 Thread #t1 (line 4)──│You YYYY-MM-DD HH:MM:SS▌ │Test threadNORMALc] reply [r] resolve [q] closerrsolve [?] helputh System Design verview « Test thread sers authenticate via OAuth2 with Google and GitHub providers. ndpoints POST /auth/login — initiates OAuth flow GET /auth/callback — handles provider callback POST /auth/refresh — refreshes expired tokens oken Storage okens are stored in Redis as JSON blobs. See Redis docs. ──────────────┬───────────┬─────┐ Key │ Format │ TTL │ ──────────────┼───────────┼─────┤ session:{id} │ JSON blob │ 24h │ ──────────────┴───────────┴─────┘"`;
|
|
24
|
+
|
|
25
|
+
exports[`revspec E2E snapshots T opens thread list 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 ┌─ Threads (0 active) ─────────────────────────────────┐│ ││ No active threads. Press [Esc] to close. █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [j/k] navigate [Enter] jump [Esc] close │└──────────────────────────────────────────────────────┘"`;
|
|
26
|
+
|
|
27
|
+
exports[`revspec E2E snapshots r resolves thread 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘ 1 open · L4/37 Thread #t1 (line 4)──│You YYYY-MM-DD HH:MM:SS▌ │Comment to resolveNORMALc] reply [r] resolve [q] closerrsolve [?] helputh System Design verview « Comment to resolve sers authenticate via OAuth2 with Google and GitHub providers. ndpoints POST /auth/login — initiates OAuth flow GET /auth/callback — handles provider callback POST /auth/refresh — refreshes expired tokens oken Storage okens are stored in Redis as JSON blobs. See Redis docs. ──────────────┬───────────┬─────┐ Key │ Format │ TTL │ ──────────────┼───────────┼─────┤ session:{id} │ JSON blob │ 24h │ ──────────────┴───────────┴─────┘ No active threads · L4/37✓✔ Resolved thread #t1"`;
|
|
28
|
+
|
|
29
|
+
exports[`revspec E2E snapshots dd y deletes thread 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘ 1 open · L4/37 Thread #t1 (line 4)──│You YYYY-MM-DD HH:MM:SS▌ │Comment to deleteNORMALc] reply [r] resolve [q] closerrsolve [?] helputh System Design verview « Comment to delete sers authenticate via OAuth2 with Google and GitHub providers. ndpoints POST /auth/login — initiates OAuth flow GET /auth/callback — handles provider callback POST /auth/refresh — refreshes expired tokens oken Storage okens are stored in Redis as JSON blobs. See Redis docs. ──────────────┬───────────┬─────┐ Key │ Format │ TTL │ ──────────────┼───────────┼─────┤ session:{id} │ JSON blob │ 24h │ ──────────────┴───────────┴─────┘ ┌─ Delete Thread ──────────────────────┐│ ││ Delete thread #t1 on line 4? ││ ││ ││ ││ ││ [y] yes [n/Esc] no │└──────────────────────────────────────┘d... No active threads · L4/37 login — initiates OAuth flow allback — handles provider callback refresh — refreshes expired tokens ored in Redis as JSON blobs. See Redis d ✔ Deleted thread #t1"`;
|
|
30
|
+
|
|
31
|
+
exports[`revspec E2E snapshots :w merges review 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 :w"`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Auth System Design
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Users authenticate via **OAuth2** with `Google` and *GitHub* providers.
|
|
6
|
+
|
|
7
|
+
## Endpoints
|
|
8
|
+
|
|
9
|
+
- POST /auth/login — initiates OAuth flow
|
|
10
|
+
- GET /auth/callback — handles provider callback
|
|
11
|
+
- POST /auth/refresh — refreshes ~~expired~~ tokens
|
|
12
|
+
|
|
13
|
+
## Token Storage
|
|
14
|
+
|
|
15
|
+
Tokens are stored in Redis as JSON blobs. See [Redis docs](https://redis.io).
|
|
16
|
+
|
|
17
|
+
| Key | Format | TTL |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `session:{id}` | JSON blob | 24h |
|
|
20
|
+
|
|
21
|
+
## Tasks
|
|
22
|
+
|
|
23
|
+
- [x] Implement login
|
|
24
|
+
- [ ] Add rate limiting
|
|
25
|
+
|
|
26
|
+
> Note: tokens are **not encrypted** at rest.
|
|
27
|
+
|
|
28
|
+
### Code Example
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
const token: string = await auth.login();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
End of spec.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { spawn, type IPty } from "bun-pty";
|
|
2
|
+
import { resolve, dirname, basename, join } from "path";
|
|
3
|
+
import { unlinkSync, existsSync } from "fs";
|
|
4
|
+
|
|
5
|
+
const CLI = resolve(import.meta.dir, "../../bin/revspec.ts");
|
|
6
|
+
|
|
7
|
+
function stripAnsi(str: string): string {
|
|
8
|
+
return str
|
|
9
|
+
.replace(/\x1bP(?:[^\x1b]|\x1b[^\\])*\x1b\\/g, "")
|
|
10
|
+
.replace(/\x1b\[[0-9;?>=!]*[ -/]*[A-Za-z@`~]/g, "")
|
|
11
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
12
|
+
.replace(/\x1b[()][0-9A-Z]/g, "")
|
|
13
|
+
.replace(/\x1b[>=<]/g, "")
|
|
14
|
+
.replace(/\x1b./g, "")
|
|
15
|
+
.replace(/\r/g, "")
|
|
16
|
+
.replace(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/g, "YYYY-MM-DD HH:MM:SS")
|
|
17
|
+
.replace(/revspec v\d+\.\d+\.\d+/g, "revspec vX.Y.Z");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TuiHarness {
|
|
21
|
+
sendKeys: (keys: string) => void;
|
|
22
|
+
wait: (ms?: number) => Promise<void>;
|
|
23
|
+
capture: () => string;
|
|
24
|
+
contains: (text: string) => boolean;
|
|
25
|
+
quit: () => Promise<void>;
|
|
26
|
+
cleanReviewFiles: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function createHarness(specFile: string, opts?: { cols?: number; rows?: number }): Promise<TuiHarness> {
|
|
30
|
+
const cols = opts?.cols ?? 80;
|
|
31
|
+
const rows = opts?.rows ?? 24;
|
|
32
|
+
const specPath = resolve(specFile);
|
|
33
|
+
let buffer = "";
|
|
34
|
+
|
|
35
|
+
const pty = spawn("bun", ["run", CLI, specPath], {
|
|
36
|
+
name: "xterm-256color",
|
|
37
|
+
cols,
|
|
38
|
+
rows,
|
|
39
|
+
env: { ...process.env, TERM: "xterm-256color", NO_COLOR: undefined, FORCE_COLOR: undefined },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
pty.onData((data: string) => { buffer += data; });
|
|
43
|
+
|
|
44
|
+
function sendKeys(keys: string): void { pty.write(keys); }
|
|
45
|
+
|
|
46
|
+
function wait(ms = 50): Promise<void> {
|
|
47
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function capture(): string {
|
|
51
|
+
const clean = stripAnsi(buffer);
|
|
52
|
+
const lines = clean.split("\n");
|
|
53
|
+
return lines.slice(Math.max(0, lines.length - rows), lines.length).join("\n").trimEnd();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function contains(text: string): boolean { return capture().includes(text); }
|
|
57
|
+
|
|
58
|
+
function cleanReviewFiles(): void {
|
|
59
|
+
const dir = dirname(specPath);
|
|
60
|
+
const base = basename(specPath, ".md");
|
|
61
|
+
for (const ext of [".review.json", ".review.live.jsonl", ".review.live.offset", ".review.live.lock", ".review.draft.json"]) {
|
|
62
|
+
const f = join(dir, `${base}${ext}`);
|
|
63
|
+
try { if (existsSync(f)) unlinkSync(f); } catch {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function quit(): Promise<void> {
|
|
68
|
+
sendKeys("\x1b");
|
|
69
|
+
await wait(30);
|
|
70
|
+
sendKeys(":q!\n");
|
|
71
|
+
await wait(100);
|
|
72
|
+
try { pty.kill(); } catch {}
|
|
73
|
+
cleanReviewFiles();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Wait for initial render
|
|
77
|
+
await wait(150);
|
|
78
|
+
|
|
79
|
+
return { sendKeys, wait, capture, contains, quit, cleanReviewFiles };
|
|
80
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, test, expect, afterEach } from "bun:test";
|
|
2
|
+
import { createHarness, type TuiHarness } from "./harness";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
|
|
5
|
+
const SPEC = resolve(import.meta.dir, "fixtures/spec.md");
|
|
6
|
+
const S = 350; // sequence wait (gg/dd need 300ms timeout)
|
|
7
|
+
|
|
8
|
+
describe("revspec E2E snapshots", () => {
|
|
9
|
+
let harness: TuiHarness | null = null;
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
if (harness) { await harness.quit(); harness = null; }
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("initial render", async () => {
|
|
16
|
+
harness = await createHarness(SPEC);
|
|
17
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("G scrolls to bottom", async () => {
|
|
21
|
+
harness = await createHarness(SPEC);
|
|
22
|
+
harness.sendKeys("G");
|
|
23
|
+
await harness.wait();
|
|
24
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("j/k moves cursor", async () => {
|
|
28
|
+
harness = await createHarness(SPEC);
|
|
29
|
+
harness.sendKeys("jjjjj");
|
|
30
|
+
await harness.wait();
|
|
31
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("Ctrl+D half page down", async () => {
|
|
35
|
+
harness = await createHarness(SPEC);
|
|
36
|
+
harness.sendKeys("\x04");
|
|
37
|
+
await harness.wait();
|
|
38
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("/ search highlights matches", async () => {
|
|
42
|
+
harness = await createHarness(SPEC);
|
|
43
|
+
harness.sendKeys("/token\n");
|
|
44
|
+
await harness.wait();
|
|
45
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("n jumps to next match", async () => {
|
|
49
|
+
harness = await createHarness(SPEC);
|
|
50
|
+
harness.sendKeys("/token\n");
|
|
51
|
+
await harness.wait();
|
|
52
|
+
harness.sendKeys("n");
|
|
53
|
+
await harness.wait();
|
|
54
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("? opens help", async () => {
|
|
58
|
+
harness = await createHarness(SPEC);
|
|
59
|
+
harness.sendKeys("?");
|
|
60
|
+
await harness.wait();
|
|
61
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("c opens comment input", async () => {
|
|
65
|
+
harness = await createHarness(SPEC);
|
|
66
|
+
harness.sendKeys("jjj");
|
|
67
|
+
await harness.wait();
|
|
68
|
+
harness.sendKeys("c");
|
|
69
|
+
await harness.wait();
|
|
70
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("Tab submits comment", async () => {
|
|
74
|
+
harness = await createHarness(SPEC);
|
|
75
|
+
harness.sendKeys("jjj");
|
|
76
|
+
await harness.wait();
|
|
77
|
+
harness.sendKeys("c");
|
|
78
|
+
await harness.wait();
|
|
79
|
+
harness.sendKeys("This is a test comment\t");
|
|
80
|
+
await harness.wait();
|
|
81
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("Esc switches to normal mode", async () => {
|
|
85
|
+
harness = await createHarness(SPEC);
|
|
86
|
+
harness.sendKeys("jjj");
|
|
87
|
+
await harness.wait();
|
|
88
|
+
harness.sendKeys("c");
|
|
89
|
+
await harness.wait();
|
|
90
|
+
harness.sendKeys("\x1b");
|
|
91
|
+
await harness.wait();
|
|
92
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("thread gutter indicator", async () => {
|
|
96
|
+
harness = await createHarness(SPEC);
|
|
97
|
+
harness.sendKeys("jjj");
|
|
98
|
+
await harness.wait();
|
|
99
|
+
harness.sendKeys("c");
|
|
100
|
+
await harness.wait();
|
|
101
|
+
harness.sendKeys("Test thread\t");
|
|
102
|
+
await harness.wait();
|
|
103
|
+
harness.sendKeys("\x1b");
|
|
104
|
+
await harness.wait();
|
|
105
|
+
harness.sendKeys("q");
|
|
106
|
+
await harness.wait();
|
|
107
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("T opens thread list", async () => {
|
|
111
|
+
harness = await createHarness(SPEC);
|
|
112
|
+
harness.sendKeys("T");
|
|
113
|
+
await harness.wait();
|
|
114
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("r resolves thread", async () => {
|
|
118
|
+
harness = await createHarness(SPEC);
|
|
119
|
+
harness.sendKeys("jjj");
|
|
120
|
+
await harness.wait();
|
|
121
|
+
harness.sendKeys("c");
|
|
122
|
+
await harness.wait();
|
|
123
|
+
harness.sendKeys("Comment to resolve\t");
|
|
124
|
+
await harness.wait();
|
|
125
|
+
harness.sendKeys("\x1b");
|
|
126
|
+
await harness.wait();
|
|
127
|
+
harness.sendKeys("q");
|
|
128
|
+
await harness.wait();
|
|
129
|
+
harness.sendKeys("r");
|
|
130
|
+
await harness.wait();
|
|
131
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("dd y deletes thread", async () => {
|
|
135
|
+
harness = await createHarness(SPEC);
|
|
136
|
+
harness.sendKeys("jjj");
|
|
137
|
+
await harness.wait();
|
|
138
|
+
harness.sendKeys("c");
|
|
139
|
+
await harness.wait();
|
|
140
|
+
harness.sendKeys("Comment to delete\t");
|
|
141
|
+
await harness.wait();
|
|
142
|
+
harness.sendKeys("\x1b");
|
|
143
|
+
await harness.wait();
|
|
144
|
+
harness.sendKeys("q");
|
|
145
|
+
await harness.wait();
|
|
146
|
+
harness.sendKeys("dd");
|
|
147
|
+
await harness.wait(S);
|
|
148
|
+
harness.sendKeys("y");
|
|
149
|
+
await harness.wait();
|
|
150
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test(":w merges review", async () => {
|
|
154
|
+
harness = await createHarness(SPEC);
|
|
155
|
+
harness.sendKeys(":w\n");
|
|
156
|
+
await harness.wait();
|
|
157
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("bottom bar shows resolve on thread line", async () => {
|
|
161
|
+
harness = await createHarness(SPEC);
|
|
162
|
+
harness.sendKeys("jjj");
|
|
163
|
+
await harness.wait();
|
|
164
|
+
harness.sendKeys("c");
|
|
165
|
+
await harness.wait();
|
|
166
|
+
harness.sendKeys("hint test\t");
|
|
167
|
+
await harness.wait();
|
|
168
|
+
harness.sendKeys("\x1b");
|
|
169
|
+
await harness.wait();
|
|
170
|
+
harness.sendKeys("q");
|
|
171
|
+
await harness.wait();
|
|
172
|
+
expect(harness.contains("resolve")).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("bottom bar hides resolve on non-thread line", async () => {
|
|
176
|
+
harness = await createHarness(SPEC);
|
|
177
|
+
const lines = harness.capture().split("\n");
|
|
178
|
+
const bottom = lines[lines.length - 1] || "";
|
|
179
|
+
expect(bottom).toContain("navigate");
|
|
180
|
+
expect(bottom).not.toContain("resolve");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -2,9 +2,9 @@ import { describe, it, expect, afterEach } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
|
-
import { appendEvent, readEventsFromOffset } from "
|
|
5
|
+
import { appendEvent, readEventsFromOffset } from "../../src/protocol/live-events";
|
|
6
6
|
|
|
7
|
-
const CLI = resolve(import.meta.dir, "
|
|
7
|
+
const CLI = resolve(import.meta.dir, "../../bin/revspec.ts");
|
|
8
8
|
|
|
9
9
|
interface SpawnResult {
|
|
10
10
|
exitCode: number;
|
|
@@ -2,9 +2,9 @@ import { describe, it, expect, afterEach } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
|
-
import { appendEvent, readEventsFromOffset } from "
|
|
5
|
+
import { appendEvent, readEventsFromOffset } from "../../src/protocol/live-events";
|
|
6
6
|
|
|
7
|
-
const CLI = resolve(import.meta.dir, "
|
|
7
|
+
const CLI = resolve(import.meta.dir, "../../bin/revspec.ts");
|
|
8
8
|
|
|
9
9
|
interface SpawnResult {
|
|
10
10
|
exitCode: number;
|
|
@@ -2,10 +2,10 @@ import { describe, it, expect, afterEach } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
|
-
import { appendEvent } from "
|
|
6
|
-
import { writeReviewFile } from "
|
|
5
|
+
import { appendEvent } from "../../src/protocol/live-events";
|
|
6
|
+
import { writeReviewFile } from "../../src/protocol/write";
|
|
7
7
|
|
|
8
|
-
const CLI = resolve(import.meta.dir, "
|
|
8
|
+
const CLI = resolve(import.meta.dir, "../../bin/revspec.ts");
|
|
9
9
|
|
|
10
10
|
interface SpawnResult {
|
|
11
11
|
exitCode: number;
|
|
@@ -2,11 +2,11 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync, existsSync } from "fs"
|
|
3
3
|
import { join } from "path"
|
|
4
4
|
import { tmpdir } from "os"
|
|
5
|
-
import { appendEvent, readEventsFromOffset } from "
|
|
6
|
-
import { mergeJsonlIntoReview } from "
|
|
7
|
-
import { writeReviewFile } from "
|
|
5
|
+
import { appendEvent, readEventsFromOffset } from "../../src/protocol/live-events"
|
|
6
|
+
import { mergeJsonlIntoReview } from "../../src/protocol/live-merge"
|
|
7
|
+
import { writeReviewFile } from "../../src/protocol/write"
|
|
8
8
|
|
|
9
|
-
const CLI = join(import.meta.dir, "..", "bin", "revspec.ts")
|
|
9
|
+
const CLI = join(import.meta.dir, "..", "..", "bin", "revspec.ts")
|
|
10
10
|
|
|
11
11
|
describe("E2E: live review loop", () => {
|
|
12
12
|
let dir: string
|
|
@@ -2,11 +2,11 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync } from "fs"
|
|
3
3
|
import { join, resolve } from "path"
|
|
4
4
|
import { tmpdir } from "os"
|
|
5
|
-
import { appendEvent, readEventsFromOffset, replayEventsToThreads } from "
|
|
6
|
-
import { mergeJsonlIntoReview } from "
|
|
7
|
-
import { ReviewState } from "
|
|
5
|
+
import { appendEvent, readEventsFromOffset, replayEventsToThreads } from "../../src/protocol/live-events"
|
|
6
|
+
import { mergeJsonlIntoReview } from "../../src/protocol/live-merge"
|
|
7
|
+
import { ReviewState } from "../../src/state/review-state"
|
|
8
8
|
|
|
9
|
-
const CLI = resolve(import.meta.dir, "
|
|
9
|
+
const CLI = resolve(import.meta.dir, "../../bin/revspec.ts")
|
|
10
10
|
|
|
11
11
|
interface SpawnResult {
|
|
12
12
|
exitCode: number
|
|
@@ -2,9 +2,9 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
|
-
import { mergeJsonlIntoReview } from "
|
|
6
|
-
import { appendEvent } from "
|
|
7
|
-
import type { ReviewFile } from "
|
|
5
|
+
import { mergeJsonlIntoReview } from "../../../src/protocol/live-merge";
|
|
6
|
+
import { appendEvent } from "../../../src/protocol/live-events";
|
|
7
|
+
import type { ReviewFile } from "../../../src/protocol/types";
|
|
8
8
|
|
|
9
9
|
function tmpDir() {
|
|
10
10
|
return mkdtempSync(join(tmpdir(), "revspec-test-"));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { mergeDraftIntoReview } from "
|
|
3
|
-
import type { ReviewFile, DraftFile, Thread } from "
|
|
2
|
+
import { mergeDraftIntoReview } from "../../../src/protocol/merge";
|
|
3
|
+
import type { ReviewFile, DraftFile, Thread } from "../../../src/protocol/types";
|
|
4
4
|
|
|
5
5
|
const baseReview: ReviewFile = {
|
|
6
6
|
file: "spec.md",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it, afterEach } from "bun:test";
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { readReviewFile, readDraftFile } from "
|
|
5
|
-
import type { ReviewFile, DraftFile } from "
|
|
4
|
+
import { readReviewFile, readDraftFile } from "../../../src/protocol/read";
|
|
5
|
+
import type { ReviewFile, DraftFile } from "../../../src/protocol/types";
|
|
6
6
|
|
|
7
7
|
function tmpDir() {
|
|
8
8
|
return mkdtempSync("/tmp/revspec-test-");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { mkdtempSync, rmSync, readFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { writeReviewFile, writeDraftFile } from "
|
|
5
|
-
import type { ReviewFile, DraftFile } from "
|
|
4
|
+
import { writeReviewFile, writeDraftFile } from "../../../src/protocol/write";
|
|
5
|
+
import type { ReviewFile, DraftFile } from "../../../src/protocol/types";
|
|
6
6
|
|
|
7
7
|
function tmpDir() {
|
|
8
8
|
return mkdtempSync("/tmp/revspec-test-");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, beforeEach } from "bun:test";
|
|
2
|
-
import { ReviewState } from "
|
|
3
|
-
import type { Thread } from "
|
|
2
|
+
import { ReviewState } from "../../../src/state/review-state";
|
|
3
|
+
import type { Thread } from "../../../src/protocol/types";
|
|
4
4
|
|
|
5
5
|
const SPEC = ["line one", "line two", "line three", "line four", "line five"];
|
|
6
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { buildPagerContent } from "
|
|
3
|
-
import { ReviewState } from "
|
|
4
|
-
import type { Thread } from "
|
|
2
|
+
import { buildPagerContent } from "../../../src/tui/pager";
|
|
3
|
+
import { ReviewState } from "../../../src/state/review-state";
|
|
4
|
+
import type { Thread } from "../../../src/protocol/types";
|
|
5
5
|
|
|
6
6
|
const SPEC = ["# Title", "Some text", "More text", "Final line"];
|
|
7
7
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { createKeybindRegistry } from "
|
|
2
|
+
import { createKeybindRegistry } from "../../../../src/tui/ui/keybinds";
|
|
3
3
|
|
|
4
4
|
function makeKey(name: string, opts: { ctrl?: boolean; shift?: boolean; sequence?: string } = {}): any {
|
|
5
5
|
return { name, ctrl: opts.ctrl ?? false, shift: opts.shift ?? false, sequence: opts.sequence ?? name };
|
|
File without changes
|