pi-odd-worktree 0.0.0-stage → 0.1.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.
Files changed (60) hide show
  1. package/README.md +223 -2
  2. package/extensions/odd-worktree-tools.ts +7593 -0
  3. package/lib/checkout-facts.js +598 -0
  4. package/lib/clipboard.js +226 -0
  5. package/lib/gh-issues.js +1555 -0
  6. package/lib/issue-preview.js +273 -0
  7. package/lib/issues-view.js +2274 -0
  8. package/lib/odd-docs.js +505 -0
  9. package/lib/odd-markdown.js +413 -0
  10. package/lib/odd-view.js +990 -0
  11. package/lib/open-external.js +106 -0
  12. package/lib/preview-scroll-keys.js +192 -0
  13. package/lib/session-auto-name.js +292 -0
  14. package/lib/session-worktree-register.js +62 -0
  15. package/lib/tui-paint.js +273 -0
  16. package/lib/view-layout.js +368 -0
  17. package/lib/warp-create-panel.js +722 -0
  18. package/lib/warp-create-picker.js +906 -0
  19. package/lib/warp-delete-confirm.js +465 -0
  20. package/lib/warp-selector-view.js +1265 -0
  21. package/lib/worktree-card.js +1117 -0
  22. package/lib/worktree-changes.js +1395 -0
  23. package/lib/worktree-close-herdr.js +73 -0
  24. package/lib/worktree-git.js +1048 -0
  25. package/lib/worktree-hooks.js +738 -0
  26. package/lib/worktree-model-result.js +291 -0
  27. package/lib/worktree-registry.js +886 -0
  28. package/lib/worktree-session-guard.js +595 -0
  29. package/lib/worktree-symlinks.js +336 -0
  30. package/lib/worktree-warp-sessions.js +566 -0
  31. package/package.json +36 -4
  32. package/tools/commands.test.js +176 -0
  33. package/tools/fixtures/gh-api-user.json +1 -0
  34. package/tools/fixtures/gh-issue-list.json +1 -0
  35. package/tools/fixtures/gh-issue-view.json +1 -0
  36. package/tools/fixtures/gh-label-list.json +1 -0
  37. package/tools/fixtures/gh-repo-view.json +1 -0
  38. package/tools/fixtures/load-extension.js +66 -0
  39. package/tools/gh-issues-feedback.test.js +359 -0
  40. package/tools/gh-issues.test.js +1502 -0
  41. package/tools/issue-preview.test.js +216 -0
  42. package/tools/issues-command.test.js +1217 -0
  43. package/tools/issues-view.test.js +4401 -0
  44. package/tools/odd-docs.test.js +803 -0
  45. package/tools/odd-markdown.test.js +236 -0
  46. package/tools/odd-view.test.js +2328 -0
  47. package/tools/package-surface.test.js +138 -0
  48. package/tools/t5-session-identity.test.js +306 -0
  49. package/tools/warp-create-panel.test.js +185 -0
  50. package/tools/warp-create-picker.test.js +1357 -0
  51. package/tools/warp-delete-confirm.test.js +616 -0
  52. package/tools/warp-selector-view.test.js +1463 -0
  53. package/tools/worktree-changes.test.js +951 -0
  54. package/tools/worktree-close-herdr-async.test.js +204 -0
  55. package/tools/worktree-odd-migration.test.js +224 -0
  56. package/tools/worktree-registry-cache.test.js +163 -0
  57. package/tools/worktree-registry-odd.test.js +231 -0
  58. package/tools/worktree-registry.test.js +244 -0
  59. package/tools/worktree-session-guard.test.js +507 -0
  60. package/tools/worktree-tools.test.js +434 -0
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+ // T2 (odd-viewer S5) tests: lib/odd-markdown.js — the pure markdown ->
3
+ // styled-lines renderer for the document pane.
4
+ //
5
+ // Contract under test (observable output only): headings of every level
6
+ // this repo's odd documents use (`#`, `##`, `###`) get a heading mark and
7
+ // keep their text; list items, GitHub task rows (checked and unchecked),
8
+ // ordered items and table rows are recognised; fenced code and blockquotes
9
+ // are recognised; nothing is dropped — every source line's words reach the
10
+ // output — and no rendered line exceeds the requested width, so a long
11
+ // paragraph wraps instead of being truncated.
12
+
13
+ const { test, describe } = require('node:test');
14
+ const assert = require('node:assert/strict');
15
+
16
+ const { renderOddMarkdown } = require('../lib/odd-markdown.js');
17
+
18
+ // The injected paint seam: roles are tagged so tests can assert styling
19
+ // without depending on a theme implementation.
20
+ const tag = (role, text) => `<${role}>${text}</${role}>`;
21
+ const opts = (width) => ({ width, paint: tag, measure: (s) => [...strip(s)].length });
22
+ function strip(value) {
23
+ return String(value).replace(/<\/?[a-z][a-zA-Z]*>/g, '');
24
+ }
25
+ function plain(lines) {
26
+ return lines.map(strip).join('\n');
27
+ }
28
+
29
+ describe('renderOddMarkdown', () => {
30
+ test('renders the document heading and each section heading with its level mark', () => {
31
+ const source = [
32
+ '# Odd viewer: read the documents',
33
+ '',
34
+ '## Specs',
35
+ 'S1. one line.',
36
+ '',
37
+ '### Deep section',
38
+ 'detail.',
39
+ ].join('\n');
40
+ const lines = renderOddMarkdown(source, opts(60));
41
+ const body = plain(lines);
42
+ assert.ok(body.includes('Odd viewer: read the documents'), 'the document title keeps its text');
43
+ assert.ok(!body.includes('# Odd viewer'), 'the raw # marker is replaced by the heading mark');
44
+ assert.ok(body.includes('Specs'), 'the section heading keeps its text');
45
+ assert.ok(body.includes('Deep section'), 'the nested heading keeps its text');
46
+ assert.ok(lines.some((line) => line.startsWith('<mdHeading>')), 'headings use the mdHeading role');
47
+ const specLine = lines.findIndex((line) => strip(line).includes('Specs'));
48
+ const deepLine = lines.findIndex((line) => strip(line).includes('Deep section'));
49
+ assert.ok(specLine >= 0 && deepLine > specLine, 'headings stay in source order');
50
+ });
51
+
52
+ test('renders list items, task rows and table rows', () => {
53
+ const source = [
54
+ '## Tasks',
55
+ 'T1 | S1 | done | a task row.',
56
+ '- a plain bullet',
57
+ '- [ ] an open task',
58
+ '- [x] a done task',
59
+ '1. an ordered item',
60
+ '',
61
+ '| ID | Spec | Route |',
62
+ '| --- | --- | --- |',
63
+ '| T2 | S5 | implement |',
64
+ ].join('\n');
65
+ const lines = renderOddMarkdown(source, opts(60));
66
+ const body = plain(lines);
67
+ for (const copy of [
68
+ 'T1 | S1 | done | a task row.',
69
+ 'a plain bullet',
70
+ 'an open task',
71
+ 'a done task',
72
+ 'an ordered item',
73
+ 'T2 | S5 | implement',
74
+ ]) {
75
+ assert.ok(body.includes(copy), `lost content: ${copy}`);
76
+ }
77
+ assert.ok(/•\s+a plain bullet/.test(body), 'bullets carry a • mark');
78
+ assert.ok(/☐\s+an open task/.test(body), 'an open task row carries an unchecked box');
79
+ assert.ok(/☑\s+a done task/.test(body), 'a done task row carries a checked box');
80
+ assert.ok(/1\.\s+an ordered item/.test(body), 'an ordered item keeps its number');
81
+ });
82
+
83
+ test('renders fenced code and blockquotes without their markers leaking', () => {
84
+ const source = [
85
+ 'text before',
86
+ '',
87
+ '> a quoted line',
88
+ '',
89
+ '```sh',
90
+ 'git worktree list',
91
+ '```',
92
+ '',
93
+ 'text after',
94
+ ].join('\n');
95
+ const lines = renderOddMarkdown(source, opts(60));
96
+ const body = plain(lines);
97
+ assert.ok(body.includes('git worktree list'), 'the code body survives');
98
+ assert.ok(body.includes('a quoted line'), 'the quote text survives');
99
+ assert.ok(!body.includes('```'), 'the fence marker is not rendered as text');
100
+ assert.ok(!/^>/m.test(body), 'the quote marker is not rendered as text');
101
+ });
102
+
103
+ test('wraps a long paragraph to the width instead of cutting it', () => {
104
+ const words = Array.from({ length: 40 }, (_, index) => `word${index}`).join(' ');
105
+ const lines = renderOddMarkdown(words, opts(24));
106
+ assert.ok(lines.length > 1, 'a long paragraph occupies several lines');
107
+ for (const line of lines) {
108
+ assert.ok(strip(line).length <= 24, `line exceeds the width: ${strip(line)}`);
109
+ }
110
+ const joined = plain(lines).replace(/\n/g, ' ');
111
+ for (const word of words.split(' ')) {
112
+ assert.ok(joined.includes(word), `wrapped output lost ${word}`);
113
+ }
114
+ });
115
+
116
+ test('honours an inline emphasis and code span without breaking the width budget', () => {
117
+ const lines = renderOddMarkdown('a **bold** value and `inline code` here', opts(80));
118
+ const body = plain(lines);
119
+ assert.ok(body.includes('bold'), 'bold text survives');
120
+ assert.ok(body.includes('inline code'), 'inline code survives');
121
+ for (const line of lines) assert.ok(strip(line).length <= 80);
122
+ });
123
+
124
+ test('an empty document renders nothing', () => {
125
+ assert.deepEqual(renderOddMarkdown('', opts(40)), []);
126
+ assert.deepEqual(renderOddMarkdown(' \n\n', opts(40)), []);
127
+ });
128
+
129
+ test('keeps punctuation glued to inline spans attached', () => {
130
+ const source = 'see `/x/y`, and **bold**, done.';
131
+ for (const width of [80, 20]) {
132
+ const lines = renderOddMarkdown(source, opts(width));
133
+ const body = plain(lines);
134
+ assert.ok(body.includes('/x/y,'), `width ${width}: code span keeps its glued comma: ${JSON.stringify(body)}`);
135
+ assert.ok(body.includes('bold,'), `width ${width}: bold span keeps its glued comma: ${JSON.stringify(body)}`);
136
+ assert.ok(!body.includes('/x/y ,'), `width ${width}: no space before the code comma: ${JSON.stringify(body)}`);
137
+ assert.ok(!body.includes('bold ,'), `width ${width}: no space before the bold comma: ${JSON.stringify(body)}`);
138
+ }
139
+ });
140
+ });
141
+
142
+ // S12 (Issues preview): GitHub-flavored markdown treats a single newline inside
143
+ // a paragraph as a space, so a body whose lines were hard-wrapped at the author's
144
+ // terminal reads as ONE paragraph. The ODD documents keep their own
145
+ // line-per-line shape, so the joined form is an explicit option and the default
146
+ // is unchanged.
147
+ describe('renderOddMarkdown joins wrapped paragraphs when asked (S12)', () => {
148
+ const flow = (width) => ({ width, paint: tag, measure: (s) => [...strip(s)].length, flowParagraphs: true });
149
+
150
+ test('consecutive plain source lines become one paragraph', () => {
151
+ const source = 'first source line\nsecond source line';
152
+ assert.equal(plain(renderOddMarkdown(source, flow(200))), 'first source line second source line');
153
+ assert.equal(plain(renderOddMarkdown(source, opts(200))), 'first source line\n\nsecond source line',
154
+ 'the ODD document shape keeps one source line per block by default');
155
+ });
156
+
157
+ test('a joined paragraph still wraps to the requested width', () => {
158
+ const source = 'alpha beta gamma delta epsilon zeta eta theta';
159
+ const lines = renderOddMarkdown(source, flow(16));
160
+ assert.ok(lines.length > 1, 'the joined paragraph wraps');
161
+ for (const line of lines) assert.ok(strip(line).length <= 16, `line exceeds the width: ${strip(line)}`);
162
+ assert.equal(plain(lines).replace(/\n/g, ' '), source, 'every word survives the join and the wrap');
163
+ });
164
+
165
+ test('headings, lists, quotes and code blocks keep their own blocks', () => {
166
+ const source = [
167
+ 'lead in',
168
+ 'continues here',
169
+ '## Steps',
170
+ '- one',
171
+ '- two',
172
+ '> quoted',
173
+ '```sh',
174
+ 'ls',
175
+ '```',
176
+ 'trailing text',
177
+ ].join('\n');
178
+ const body = plain(renderOddMarkdown(source, flow(80)));
179
+ assert.ok(body.includes('lead in continues here'), 'plain lines joined into one paragraph');
180
+ assert.ok(!body.includes('continues here ## Steps'), 'a heading ends the paragraph');
181
+ assert.ok(/▌ Steps/.test(body), 'the heading keeps its mark');
182
+ assert.ok(/•\s+one/.test(body), 'list items stay their own rows');
183
+ assert.ok(body.includes('quoted') && !/\n>/.test(body), 'the quote keeps its own block');
184
+ assert.ok(body.includes('ls'), 'the code body survives');
185
+ assert.ok(body.includes('trailing text'), 'text after a fence is not swallowed');
186
+ });
187
+ });
188
+
189
+ // S12: the preview renders text that came from someone else's issue tracker, so
190
+ // the renderer treats control sequences as display data. Roles are asserted
191
+ // through the injected paint seam, never through a theme.
192
+ describe('renderOddMarkdown styles and sanitizes untrusted text (S12)', () => {
193
+ const roles = (width) => ({
194
+ width,
195
+ paint: tag,
196
+ emphasis: (text) => `<strong>${text}</strong>`,
197
+ measure: (s) => [...strip(s)].length,
198
+ });
199
+
200
+ test('inline code and bold take their own roles', () => {
201
+ const body = renderOddMarkdown('use `gh auth` and **today**', roles(80)).join('\n');
202
+ assert.match(body, /<accent><dim>gh<\/dim><\/accent>/, 'inline code is accent on dim');
203
+ assert.match(body, /<strong><text>today<\/text><\/strong>/, 'bold goes through the emphasis seam');
204
+ });
205
+
206
+ test('a fenced block renders indented, dim, and inside the width budget', () => {
207
+ const source = ['```', 'git worktree add /tmp/very-long-branch-name-here ok', '```'].join('\n');
208
+ const lines = renderOddMarkdown(source, roles(24));
209
+ assert.ok(lines.length > 1, 'a long code line wraps');
210
+ for (const line of lines) {
211
+ assert.match(line, /^ {2}<dim>/, `code rows are indented and dim: ${JSON.stringify(line)}`);
212
+ assert.ok(strip(line).length <= 24, `code row exceeds the width: ${strip(line)}`);
213
+ }
214
+ const withoutSpace = (value) => String(value).replace(/\s+/g, '');
215
+ assert.equal(withoutSpace(plain(lines)), withoutSpace('git worktree add /tmp/very-long-branch-name-here ok'),
216
+ 'no code character is dropped by the wrap');
217
+ const wide = renderOddMarkdown(source, roles(200));
218
+ assert.equal(plain(wide), ' git worktree add /tmp/very-long-branch-name-here ok', 'a code line that fits is untouched');
219
+ });
220
+
221
+ test('escape sequences, OSC titles and control characters never reach the output', () => {
222
+ const source = 'plain \x1b[31mred\x1b[0m \x1b]0;owned\x07text\x08\x07 end';
223
+ const lines = renderOddMarkdown(source, roles(80));
224
+ const joined = lines.join('');
225
+ assert.ok(!joined.includes('\x1b'), 'no escape byte reaches the output');
226
+ assert.ok(!joined.includes('\x07'), 'no BEL reaches the output');
227
+ assert.ok(!joined.includes('\x08'), 'no backspace reaches the output');
228
+ assert.equal(plain(lines), 'plain red text end', 'the visible text of the line survives intact');
229
+ });
230
+
231
+ test('a tab is folded to spaces so the width budget matches the terminal', () => {
232
+ const body = plain(renderOddMarkdown('a\tb', roles(80)));
233
+ assert.ok(!body.includes('\t'), 'no raw tab reaches the output');
234
+ assert.equal(body, 'a b');
235
+ });
236
+ });