opengstack 0.13.6 → 0.13.8

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 (135) hide show
  1. package/bin/opengstack.js +35 -90
  2. package/package.json +2 -3
  3. package/scripts/install-skills.js +29 -58
  4. package/skills/browse/bin/find-browse +21 -0
  5. package/skills/browse/bin/remote-slug +14 -0
  6. package/skills/browse/scripts/build-node-server.sh +48 -0
  7. package/skills/browse/src/activity.ts +208 -0
  8. package/skills/browse/src/browser-manager.ts +959 -0
  9. package/skills/browse/src/buffers.ts +137 -0
  10. package/skills/browse/src/bun-polyfill.cjs +109 -0
  11. package/skills/browse/src/cli.ts +678 -0
  12. package/skills/browse/src/commands.ts +128 -0
  13. package/skills/browse/src/config.ts +150 -0
  14. package/skills/browse/src/cookie-import-browser.ts +625 -0
  15. package/skills/browse/src/cookie-picker-routes.ts +230 -0
  16. package/skills/browse/src/cookie-picker-ui.ts +688 -0
  17. package/skills/browse/src/find-browse.ts +61 -0
  18. package/skills/browse/src/meta-commands.ts +550 -0
  19. package/skills/browse/src/platform.ts +17 -0
  20. package/skills/browse/src/read-commands.ts +358 -0
  21. package/skills/browse/src/server.ts +1192 -0
  22. package/skills/browse/src/sidebar-agent.ts +280 -0
  23. package/skills/browse/src/sidebar-utils.ts +21 -0
  24. package/skills/browse/src/snapshot.ts +407 -0
  25. package/skills/browse/src/url-validation.ts +95 -0
  26. package/skills/browse/src/write-commands.ts +364 -0
  27. package/skills/browse/test/activity.test.ts +120 -0
  28. package/skills/browse/test/adversarial-security.test.ts +32 -0
  29. package/skills/browse/test/browser-manager-unit.test.ts +17 -0
  30. package/skills/browse/test/bun-polyfill.test.ts +72 -0
  31. package/skills/browse/test/commands.test.ts +2075 -0
  32. package/skills/browse/test/compare-board.test.ts +342 -0
  33. package/skills/browse/test/config.test.ts +316 -0
  34. package/skills/browse/test/cookie-import-browser.test.ts +519 -0
  35. package/skills/browse/test/cookie-picker-routes.test.ts +260 -0
  36. package/skills/browse/test/file-drop.test.ts +271 -0
  37. package/skills/browse/test/find-browse.test.ts +50 -0
  38. package/skills/browse/test/findport.test.ts +191 -0
  39. package/skills/browse/test/fixtures/basic.html +33 -0
  40. package/skills/browse/test/fixtures/cursor-interactive.html +22 -0
  41. package/skills/browse/test/fixtures/dialog.html +15 -0
  42. package/skills/browse/test/fixtures/empty.html +2 -0
  43. package/skills/browse/test/fixtures/forms.html +55 -0
  44. package/skills/browse/test/fixtures/iframe.html +30 -0
  45. package/skills/browse/test/fixtures/network-idle.html +30 -0
  46. package/skills/browse/test/fixtures/qa-eval-checkout.html +108 -0
  47. package/skills/browse/test/fixtures/qa-eval-spa.html +98 -0
  48. package/skills/browse/test/fixtures/qa-eval.html +51 -0
  49. package/skills/browse/test/fixtures/responsive.html +49 -0
  50. package/skills/browse/test/fixtures/snapshot.html +55 -0
  51. package/skills/browse/test/fixtures/spa.html +24 -0
  52. package/skills/browse/test/fixtures/states.html +17 -0
  53. package/skills/browse/test/fixtures/upload.html +25 -0
  54. package/skills/browse/test/gstack-config.test.ts +138 -0
  55. package/skills/browse/test/gstack-update-check.test.ts +514 -0
  56. package/skills/browse/test/handoff.test.ts +235 -0
  57. package/skills/browse/test/path-validation.test.ts +91 -0
  58. package/skills/browse/test/platform.test.ts +37 -0
  59. package/skills/browse/test/server-auth.test.ts +65 -0
  60. package/skills/browse/test/sidebar-agent-roundtrip.test.ts +226 -0
  61. package/skills/browse/test/sidebar-agent.test.ts +199 -0
  62. package/skills/browse/test/sidebar-integration.test.ts +320 -0
  63. package/skills/browse/test/sidebar-unit.test.ts +96 -0
  64. package/skills/browse/test/snapshot.test.ts +467 -0
  65. package/skills/browse/test/state-ttl.test.ts +35 -0
  66. package/skills/browse/test/test-server.ts +57 -0
  67. package/skills/browse/test/url-validation.test.ts +72 -0
  68. package/skills/browse/test/watch.test.ts +129 -0
  69. package/skills/careful/bin/check-careful.sh +112 -0
  70. package/skills/cso/ACKNOWLEDGEMENTS.md +14 -0
  71. package/skills/freeze/bin/check-freeze.sh +79 -0
  72. package/skills/qa/references/issue-taxonomy.md +85 -0
  73. package/skills/qa/templates/qa-report-template.md +126 -0
  74. package/skills/review/TODOS-format.md +62 -0
  75. package/skills/review/checklist.md +220 -0
  76. package/skills/review/design-checklist.md +132 -0
  77. package/skills/review/greptile-triage.md +220 -0
  78. /package/{autoplan → skills/autoplan}/SKILL.md +0 -0
  79. /package/{autoplan → skills/autoplan}/SKILL.md.tmpl +0 -0
  80. /package/{benchmark → skills/benchmark}/SKILL.md +0 -0
  81. /package/{benchmark → skills/benchmark}/SKILL.md.tmpl +0 -0
  82. /package/{browse → skills/browse}/SKILL.md +0 -0
  83. /package/{browse → skills/browse}/SKILL.md.tmpl +0 -0
  84. /package/{canary → skills/canary}/SKILL.md +0 -0
  85. /package/{canary → skills/canary}/SKILL.md.tmpl +0 -0
  86. /package/{careful → skills/careful}/SKILL.md +0 -0
  87. /package/{careful → skills/careful}/SKILL.md.tmpl +0 -0
  88. /package/{codex → skills/codex}/SKILL.md +0 -0
  89. /package/{codex → skills/codex}/SKILL.md.tmpl +0 -0
  90. /package/{connect-chrome → skills/connect-chrome}/SKILL.md +0 -0
  91. /package/{connect-chrome → skills/connect-chrome}/SKILL.md.tmpl +0 -0
  92. /package/{cso → skills/cso}/SKILL.md +0 -0
  93. /package/{cso → skills/cso}/SKILL.md.tmpl +0 -0
  94. /package/{design-consultation → skills/design-consultation}/SKILL.md +0 -0
  95. /package/{design-consultation → skills/design-consultation}/SKILL.md.tmpl +0 -0
  96. /package/{design-review → skills/design-review}/SKILL.md +0 -0
  97. /package/{design-review → skills/design-review}/SKILL.md.tmpl +0 -0
  98. /package/{design-shotgun → skills/design-shotgun}/SKILL.md +0 -0
  99. /package/{design-shotgun → skills/design-shotgun}/SKILL.md.tmpl +0 -0
  100. /package/{document-release → skills/document-release}/SKILL.md +0 -0
  101. /package/{document-release → skills/document-release}/SKILL.md.tmpl +0 -0
  102. /package/{freeze → skills/freeze}/SKILL.md +0 -0
  103. /package/{freeze → skills/freeze}/SKILL.md.tmpl +0 -0
  104. /package/{gstack-upgrade → skills/gstack-upgrade}/SKILL.md +0 -0
  105. /package/{gstack-upgrade → skills/gstack-upgrade}/SKILL.md.tmpl +0 -0
  106. /package/{guard → skills/guard}/SKILL.md +0 -0
  107. /package/{guard → skills/guard}/SKILL.md.tmpl +0 -0
  108. /package/{investigate → skills/investigate}/SKILL.md +0 -0
  109. /package/{investigate → skills/investigate}/SKILL.md.tmpl +0 -0
  110. /package/{land-and-deploy → skills/land-and-deploy}/SKILL.md +0 -0
  111. /package/{land-and-deploy → skills/land-and-deploy}/SKILL.md.tmpl +0 -0
  112. /package/{office-hours → skills/office-hours}/SKILL.md +0 -0
  113. /package/{office-hours → skills/office-hours}/SKILL.md.tmpl +0 -0
  114. /package/{plan-ceo-review → skills/plan-ceo-review}/SKILL.md +0 -0
  115. /package/{plan-ceo-review → skills/plan-ceo-review}/SKILL.md.tmpl +0 -0
  116. /package/{plan-design-review → skills/plan-design-review}/SKILL.md +0 -0
  117. /package/{plan-design-review → skills/plan-design-review}/SKILL.md.tmpl +0 -0
  118. /package/{plan-eng-review → skills/plan-eng-review}/SKILL.md +0 -0
  119. /package/{plan-eng-review → skills/plan-eng-review}/SKILL.md.tmpl +0 -0
  120. /package/{qa → skills/qa}/SKILL.md +0 -0
  121. /package/{qa → skills/qa}/SKILL.md.tmpl +0 -0
  122. /package/{qa-only → skills/qa-only}/SKILL.md +0 -0
  123. /package/{qa-only → skills/qa-only}/SKILL.md.tmpl +0 -0
  124. /package/{retro → skills/retro}/SKILL.md +0 -0
  125. /package/{retro → skills/retro}/SKILL.md.tmpl +0 -0
  126. /package/{review → skills/review}/SKILL.md +0 -0
  127. /package/{review → skills/review}/SKILL.md.tmpl +0 -0
  128. /package/{setup-browser-cookies → skills/setup-browser-cookies}/SKILL.md +0 -0
  129. /package/{setup-browser-cookies → skills/setup-browser-cookies}/SKILL.md.tmpl +0 -0
  130. /package/{setup-deploy → skills/setup-deploy}/SKILL.md +0 -0
  131. /package/{setup-deploy → skills/setup-deploy}/SKILL.md.tmpl +0 -0
  132. /package/{ship → skills/ship}/SKILL.md +0 -0
  133. /package/{ship → skills/ship}/SKILL.md.tmpl +0 -0
  134. /package/{unfreeze → skills/unfreeze}/SKILL.md +0 -0
  135. /package/{unfreeze → skills/unfreeze}/SKILL.md.tmpl +0 -0
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Tests for cookie-picker route handler
3
+ *
4
+ * Tests the HTTP glue layer directly with mock BrowserManager objects.
5
+ * Verifies that all routes return valid JSON (not HTML) with correct CORS headers.
6
+ */
7
+
8
+ import { describe, test, expect } from 'bun:test';
9
+ import { handleCookiePickerRoute } from '../src/cookie-picker-routes';
10
+
11
+ // ─── Mock BrowserManager ──────────────────────────────────────
12
+
13
+ function mockBrowserManager() {
14
+ const addedCookies: any[] = [];
15
+ const clearedDomains: string[] = [];
16
+ return {
17
+ bm: {
18
+ getPage: () => ({
19
+ context: () => ({
20
+ addCookies: (cookies: any[]) => { addedCookies.push(...cookies); },
21
+ clearCookies: (opts: { domain: string }) => { clearedDomains.push(opts.domain); },
22
+ }),
23
+ }),
24
+ } as any,
25
+ addedCookies,
26
+ clearedDomains,
27
+ };
28
+ }
29
+
30
+ function makeUrl(path: string, port = 9470): URL {
31
+ return new URL(`http://127.0.0.1:${port}${path}`);
32
+ }
33
+
34
+ function makeReq(method: string, body?: any): Request {
35
+ const opts: RequestInit = { method };
36
+ if (body) {
37
+ opts.body = JSON.stringify(body);
38
+ opts.headers = { 'Content-Type': 'application/json' };
39
+ }
40
+ return new Request('http://127.0.0.1:9470', opts);
41
+ }
42
+
43
+ // ─── Tests ──────────────────────────────────────────────────────
44
+
45
+ describe('cookie-picker-routes', () => {
46
+ describe('CORS', () => {
47
+ test('OPTIONS returns 204 with correct CORS headers', async () => {
48
+ const { bm } = mockBrowserManager();
49
+ const url = makeUrl('/cookie-picker/browsers');
50
+ const req = new Request('http://127.0.0.1:9470', { method: 'OPTIONS' });
51
+
52
+ const res = await handleCookiePickerRoute(url, req, bm);
53
+
54
+ expect(res.status).toBe(204);
55
+ expect(res.headers.get('Access-Control-Allow-Origin')).toBe('http://127.0.0.1:9470');
56
+ expect(res.headers.get('Access-Control-Allow-Methods')).toContain('POST');
57
+ });
58
+
59
+ test('JSON responses include correct CORS origin with port', async () => {
60
+ const { bm } = mockBrowserManager();
61
+ const url = makeUrl('/cookie-picker/browsers', 9450);
62
+ const req = new Request('http://127.0.0.1:9450', { method: 'GET' });
63
+
64
+ const res = await handleCookiePickerRoute(url, req, bm);
65
+
66
+ expect(res.headers.get('Access-Control-Allow-Origin')).toBe('http://127.0.0.1:9450');
67
+ });
68
+ });
69
+
70
+ describe('JSON responses (not HTML)', () => {
71
+ test('GET /cookie-picker/browsers returns JSON', async () => {
72
+ const { bm } = mockBrowserManager();
73
+ const url = makeUrl('/cookie-picker/browsers');
74
+ const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
75
+
76
+ const res = await handleCookiePickerRoute(url, req, bm);
77
+
78
+ expect(res.status).toBe(200);
79
+ expect(res.headers.get('Content-Type')).toBe('application/json');
80
+ const body = await res.json();
81
+ expect(body).toHaveProperty('browsers');
82
+ expect(Array.isArray(body.browsers)).toBe(true);
83
+ });
84
+
85
+ test('GET /cookie-picker/domains without browser param returns JSON error', async () => {
86
+ const { bm } = mockBrowserManager();
87
+ const url = makeUrl('/cookie-picker/domains');
88
+ const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
89
+
90
+ const res = await handleCookiePickerRoute(url, req, bm);
91
+
92
+ expect(res.status).toBe(400);
93
+ expect(res.headers.get('Content-Type')).toBe('application/json');
94
+ const body = await res.json();
95
+ expect(body).toHaveProperty('error');
96
+ expect(body).toHaveProperty('code', 'missing_param');
97
+ });
98
+
99
+ test('POST /cookie-picker/import with invalid JSON returns JSON error', async () => {
100
+ const { bm } = mockBrowserManager();
101
+ const url = makeUrl('/cookie-picker/import');
102
+ const req = new Request('http://127.0.0.1:9470', {
103
+ method: 'POST',
104
+ body: 'not json',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ });
107
+
108
+ const res = await handleCookiePickerRoute(url, req, bm);
109
+
110
+ expect(res.status).toBe(400);
111
+ expect(res.headers.get('Content-Type')).toBe('application/json');
112
+ const body = await res.json();
113
+ expect(body.code).toBe('bad_request');
114
+ });
115
+
116
+ test('POST /cookie-picker/import missing browser field returns JSON error', async () => {
117
+ const { bm } = mockBrowserManager();
118
+ const url = makeUrl('/cookie-picker/import');
119
+ const req = makeReq('POST', { domains: ['.example.com'] });
120
+
121
+ const res = await handleCookiePickerRoute(url, req, bm);
122
+
123
+ expect(res.status).toBe(400);
124
+ const body = await res.json();
125
+ expect(body.code).toBe('missing_param');
126
+ });
127
+
128
+ test('POST /cookie-picker/import missing domains returns JSON error', async () => {
129
+ const { bm } = mockBrowserManager();
130
+ const url = makeUrl('/cookie-picker/import');
131
+ const req = makeReq('POST', { browser: 'Chrome' });
132
+
133
+ const res = await handleCookiePickerRoute(url, req, bm);
134
+
135
+ expect(res.status).toBe(400);
136
+ const body = await res.json();
137
+ expect(body.code).toBe('missing_param');
138
+ });
139
+
140
+ test('POST /cookie-picker/remove with invalid JSON returns JSON error', async () => {
141
+ const { bm } = mockBrowserManager();
142
+ const url = makeUrl('/cookie-picker/remove');
143
+ const req = new Request('http://127.0.0.1:9470', {
144
+ method: 'POST',
145
+ body: '{bad',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ });
148
+
149
+ const res = await handleCookiePickerRoute(url, req, bm);
150
+
151
+ expect(res.status).toBe(400);
152
+ expect(res.headers.get('Content-Type')).toBe('application/json');
153
+ });
154
+
155
+ test('POST /cookie-picker/remove missing domains returns JSON error', async () => {
156
+ const { bm } = mockBrowserManager();
157
+ const url = makeUrl('/cookie-picker/remove');
158
+ const req = makeReq('POST', {});
159
+
160
+ const res = await handleCookiePickerRoute(url, req, bm);
161
+
162
+ expect(res.status).toBe(400);
163
+ const body = await res.json();
164
+ expect(body.code).toBe('missing_param');
165
+ });
166
+
167
+ test('GET /cookie-picker/imported returns JSON with domain list', async () => {
168
+ const { bm } = mockBrowserManager();
169
+ const url = makeUrl('/cookie-picker/imported');
170
+ const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
171
+
172
+ const res = await handleCookiePickerRoute(url, req, bm);
173
+
174
+ expect(res.status).toBe(200);
175
+ expect(res.headers.get('Content-Type')).toBe('application/json');
176
+ const body = await res.json();
177
+ expect(body).toHaveProperty('domains');
178
+ expect(body).toHaveProperty('totalDomains');
179
+ expect(body).toHaveProperty('totalCookies');
180
+ });
181
+ });
182
+
183
+ describe('routing', () => {
184
+ test('GET /cookie-picker returns HTML', async () => {
185
+ const { bm } = mockBrowserManager();
186
+ const url = makeUrl('/cookie-picker');
187
+ const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
188
+
189
+ const res = await handleCookiePickerRoute(url, req, bm);
190
+
191
+ expect(res.status).toBe(200);
192
+ expect(res.headers.get('Content-Type')).toContain('text/html');
193
+ });
194
+
195
+ test('unknown path returns 404', async () => {
196
+ const { bm } = mockBrowserManager();
197
+ const url = makeUrl('/cookie-picker/nonexistent');
198
+ const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
199
+
200
+ const res = await handleCookiePickerRoute(url, req, bm);
201
+
202
+ expect(res.status).toBe(404);
203
+ });
204
+ });
205
+
206
+ describe('auth gate security', () => {
207
+ test('GET /cookie-picker HTML page works without auth token', async () => {
208
+ const { bm } = mockBrowserManager();
209
+ const url = makeUrl('/cookie-picker');
210
+ // Request with no Authorization header, but authToken is set on the server
211
+ const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
212
+
213
+ const res = await handleCookiePickerRoute(url, req, bm, 'test-secret-token');
214
+
215
+ expect(res.status).toBe(200);
216
+ expect(res.headers.get('Content-Type')).toContain('text/html');
217
+ });
218
+
219
+ test('GET /cookie-picker/browsers returns 401 without auth', async () => {
220
+ const { bm } = mockBrowserManager();
221
+ const url = makeUrl('/cookie-picker/browsers');
222
+ // No Authorization header
223
+ const req = new Request('http://127.0.0.1:9470', { method: 'GET' });
224
+
225
+ const res = await handleCookiePickerRoute(url, req, bm, 'test-secret-token');
226
+
227
+ expect(res.status).toBe(401);
228
+ const body = await res.json();
229
+ expect(body.error).toBe('Unauthorized');
230
+ });
231
+
232
+ test('POST /cookie-picker/import returns 401 without auth', async () => {
233
+ const { bm } = mockBrowserManager();
234
+ const url = makeUrl('/cookie-picker/import');
235
+ const req = makeReq('POST', { browser: 'Chrome', domains: ['.example.com'] });
236
+
237
+ const res = await handleCookiePickerRoute(url, req, bm, 'test-secret-token');
238
+
239
+ expect(res.status).toBe(401);
240
+ const body = await res.json();
241
+ expect(body.error).toBe('Unauthorized');
242
+ });
243
+
244
+ test('GET /cookie-picker/browsers works with valid auth', async () => {
245
+ const { bm } = mockBrowserManager();
246
+ const url = makeUrl('/cookie-picker/browsers');
247
+ const req = new Request('http://127.0.0.1:9470', {
248
+ method: 'GET',
249
+ headers: { 'Authorization': 'Bearer test-secret-token' },
250
+ });
251
+
252
+ const res = await handleCookiePickerRoute(url, req, bm, 'test-secret-token');
253
+
254
+ expect(res.status).toBe(200);
255
+ expect(res.headers.get('Content-Type')).toBe('application/json');
256
+ const body = await res.json();
257
+ expect(body).toHaveProperty('browsers');
258
+ });
259
+ });
260
+ });
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Tests for the inbox meta-command handler (file drop relay).
3
+ *
4
+ * Tests the inbox display, --clear flag, and edge cases by creating
5
+ * temp directories with test JSON files and calling handleMetaCommand.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import * as os from 'os';
12
+ import { handleMetaCommand } from '../src/meta-commands';
13
+ import { BrowserManager } from '../src/browser-manager';
14
+
15
+ let tmpDir: string;
16
+ let bm: BrowserManager;
17
+
18
+ // We need a BrowserManager instance for handleMetaCommand, but inbox
19
+ // doesn't use it. We also need to mock git rev-parse to point to our
20
+ // temp directory. We'll test the inbox logic directly by manipulating
21
+ // the filesystem and using child_process.execSync override.
22
+
23
+ // ─── Direct filesystem tests (bypassing handleMetaCommand) ──────
24
+ // The inbox handler in meta-commands.ts calls `git rev-parse --show-toplevel`
25
+ // to find the inbox directory. Since we can't easily mock that in unit tests,
26
+ // we test the inbox parsing logic directly.
27
+
28
+ interface InboxMessage {
29
+ timestamp: string;
30
+ url: string;
31
+ userMessage: string;
32
+ }
33
+
34
+ /** Replicate the inbox file reading logic from meta-commands.ts */
35
+ function readInbox(inboxDir: string): InboxMessage[] {
36
+ if (!fs.existsSync(inboxDir)) return [];
37
+
38
+ const files = fs.readdirSync(inboxDir)
39
+ .filter(f => f.endsWith('.json') && !f.startsWith('.'))
40
+ .sort()
41
+ .reverse();
42
+
43
+ if (files.length === 0) return [];
44
+
45
+ const messages: InboxMessage[] = [];
46
+ for (const file of files) {
47
+ try {
48
+ const data = JSON.parse(fs.readFileSync(path.join(inboxDir, file), 'utf-8'));
49
+ messages.push({
50
+ timestamp: data.timestamp || '',
51
+ url: data.page?.url || 'unknown',
52
+ userMessage: data.userMessage || '',
53
+ });
54
+ } catch {
55
+ // Skip malformed files
56
+ }
57
+ }
58
+ return messages;
59
+ }
60
+
61
+ /** Replicate the inbox formatting logic from meta-commands.ts */
62
+ function formatInbox(messages: InboxMessage[]): string {
63
+ if (messages.length === 0) return 'Inbox empty.';
64
+
65
+ const lines: string[] = [];
66
+ lines.push(`SIDEBAR INBOX (${messages.length} message${messages.length === 1 ? '' : 's'})`);
67
+ lines.push('────────────────────────────────');
68
+
69
+ for (const msg of messages) {
70
+ const ts = msg.timestamp ? `[${msg.timestamp}]` : '[unknown]';
71
+ lines.push(`${ts} ${msg.url}`);
72
+ lines.push(` "${msg.userMessage}"`);
73
+ lines.push('');
74
+ }
75
+
76
+ lines.push('────────────────────────────────');
77
+ return lines.join('\n');
78
+ }
79
+
80
+ /** Replicate the --clear logic from meta-commands.ts */
81
+ function clearInbox(inboxDir: string): number {
82
+ const files = fs.readdirSync(inboxDir)
83
+ .filter(f => f.endsWith('.json') && !f.startsWith('.'));
84
+ for (const file of files) {
85
+ try { fs.unlinkSync(path.join(inboxDir, file)); } catch {}
86
+ }
87
+ return files.length;
88
+ }
89
+
90
+ function writeTestInboxFile(
91
+ inboxDir: string,
92
+ message: string,
93
+ pageUrl: string,
94
+ timestamp: string,
95
+ ): string {
96
+ fs.mkdirSync(inboxDir, { recursive: true });
97
+ const filename = `${timestamp.replace(/:/g, '-')}-observation.json`;
98
+ const filePath = path.join(inboxDir, filename);
99
+ fs.writeFileSync(filePath, JSON.stringify({
100
+ type: 'observation',
101
+ timestamp,
102
+ page: { url: pageUrl, title: '' },
103
+ userMessage: message,
104
+ sidebarSessionId: 'test-session',
105
+ }, null, 2));
106
+ return filePath;
107
+ }
108
+
109
+ beforeEach(() => {
110
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'file-drop-test-'));
111
+ });
112
+
113
+ afterEach(() => {
114
+ fs.rmSync(tmpDir, { recursive: true, force: true });
115
+ });
116
+
117
+ // ─── Empty Inbox ─────────────────────────────────────────────────
118
+
119
+ describe('inbox — empty states', () => {
120
+ test('no .context/sidebar-inbox directory returns empty', () => {
121
+ const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
122
+ const messages = readInbox(inboxDir);
123
+ expect(messages.length).toBe(0);
124
+ expect(formatInbox(messages)).toBe('Inbox empty.');
125
+ });
126
+
127
+ test('empty inbox directory returns empty', () => {
128
+ const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
129
+ fs.mkdirSync(inboxDir, { recursive: true });
130
+ const messages = readInbox(inboxDir);
131
+ expect(messages.length).toBe(0);
132
+ expect(formatInbox(messages)).toBe('Inbox empty.');
133
+ });
134
+
135
+ test('directory with only dotfiles returns empty', () => {
136
+ const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
137
+ fs.mkdirSync(inboxDir, { recursive: true });
138
+ fs.writeFileSync(path.join(inboxDir, '.tmp-file.json'), '{}');
139
+ const messages = readInbox(inboxDir);
140
+ expect(messages.length).toBe(0);
141
+ });
142
+ });
143
+
144
+ // ─── Valid Messages ──────────────────────────────────────────────
145
+
146
+ describe('inbox — valid messages', () => {
147
+ test('displays formatted output with timestamps and URLs', () => {
148
+ const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
149
+ writeTestInboxFile(inboxDir, 'This button is broken', 'https://example.com/page', '2024-06-15T10:30:00.000Z');
150
+ writeTestInboxFile(inboxDir, 'Login form fails', 'https://example.com/login', '2024-06-15T10:31:00.000Z');
151
+
152
+ const messages = readInbox(inboxDir);
153
+ expect(messages.length).toBe(2);
154
+
155
+ const output = formatInbox(messages);
156
+ expect(output).toContain('SIDEBAR INBOX (2 messages)');
157
+ expect(output).toContain('https://example.com/page');
158
+ expect(output).toContain('https://example.com/login');
159
+ expect(output).toContain('"This button is broken"');
160
+ expect(output).toContain('"Login form fails"');
161
+ expect(output).toContain('[2024-06-15T10:30:00.000Z]');
162
+ expect(output).toContain('[2024-06-15T10:31:00.000Z]');
163
+ });
164
+
165
+ test('single message uses singular form', () => {
166
+ const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
167
+ writeTestInboxFile(inboxDir, 'Just one', 'https://example.com', '2024-06-15T10:30:00.000Z');
168
+
169
+ const messages = readInbox(inboxDir);
170
+ const output = formatInbox(messages);
171
+ expect(output).toContain('1 message)');
172
+ expect(output).not.toContain('messages)');
173
+ });
174
+
175
+ test('messages sorted newest first', () => {
176
+ const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
177
+ writeTestInboxFile(inboxDir, 'older', 'https://example.com', '2024-06-15T10:00:00.000Z');
178
+ writeTestInboxFile(inboxDir, 'newer', 'https://example.com', '2024-06-15T11:00:00.000Z');
179
+
180
+ const messages = readInbox(inboxDir);
181
+ // Filenames sort lexicographically, reversed = newest first
182
+ expect(messages[0].userMessage).toBe('newer');
183
+ expect(messages[1].userMessage).toBe('older');
184
+ });
185
+ });
186
+
187
+ // ─── Malformed Files ─────────────────────────────────────────────
188
+
189
+ describe('inbox — malformed files', () => {
190
+ test('malformed JSON files are skipped gracefully', () => {
191
+ const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
192
+ fs.mkdirSync(inboxDir, { recursive: true });
193
+
194
+ // Write a valid message
195
+ writeTestInboxFile(inboxDir, 'valid message', 'https://example.com', '2024-06-15T10:30:00.000Z');
196
+
197
+ // Write a malformed JSON file
198
+ fs.writeFileSync(
199
+ path.join(inboxDir, '2024-06-15T10-35-00.000Z-observation.json'),
200
+ 'this is not valid json {{{',
201
+ );
202
+
203
+ const messages = readInbox(inboxDir);
204
+ expect(messages.length).toBe(1);
205
+ expect(messages[0].userMessage).toBe('valid message');
206
+ });
207
+
208
+ test('JSON file missing fields uses defaults', () => {
209
+ const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
210
+ fs.mkdirSync(inboxDir, { recursive: true });
211
+
212
+ // Write a JSON file with missing fields
213
+ fs.writeFileSync(
214
+ path.join(inboxDir, '2024-06-15T10-30-00.000Z-observation.json'),
215
+ JSON.stringify({ type: 'observation' }),
216
+ );
217
+
218
+ const messages = readInbox(inboxDir);
219
+ expect(messages.length).toBe(1);
220
+ expect(messages[0].timestamp).toBe('');
221
+ expect(messages[0].url).toBe('unknown');
222
+ expect(messages[0].userMessage).toBe('');
223
+ });
224
+ });
225
+
226
+ // ─── Clear Flag ──────────────────────────────────────────────────
227
+
228
+ describe('inbox — --clear flag', () => {
229
+ test('files deleted after clear', () => {
230
+ const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
231
+ writeTestInboxFile(inboxDir, 'message 1', 'https://example.com', '2024-06-15T10:30:00.000Z');
232
+ writeTestInboxFile(inboxDir, 'message 2', 'https://example.com', '2024-06-15T10:31:00.000Z');
233
+
234
+ // Verify files exist
235
+ const filesBefore = fs.readdirSync(inboxDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
236
+ expect(filesBefore.length).toBe(2);
237
+
238
+ // Clear
239
+ const cleared = clearInbox(inboxDir);
240
+ expect(cleared).toBe(2);
241
+
242
+ // Verify files deleted
243
+ const filesAfter = fs.readdirSync(inboxDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
244
+ expect(filesAfter.length).toBe(0);
245
+ });
246
+
247
+ test('clear on empty directory does nothing', () => {
248
+ const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
249
+ fs.mkdirSync(inboxDir, { recursive: true });
250
+
251
+ const cleared = clearInbox(inboxDir);
252
+ expect(cleared).toBe(0);
253
+ });
254
+
255
+ test('clear preserves dotfiles', () => {
256
+ const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
257
+ fs.mkdirSync(inboxDir, { recursive: true });
258
+
259
+ // Write a dotfile and a regular file
260
+ fs.writeFileSync(path.join(inboxDir, '.keep'), '');
261
+ writeTestInboxFile(inboxDir, 'to be cleared', 'https://example.com', '2024-06-15T10:30:00.000Z');
262
+
263
+ clearInbox(inboxDir);
264
+
265
+ // Dotfile should remain
266
+ expect(fs.existsSync(path.join(inboxDir, '.keep'))).toBe(true);
267
+ // Regular file should be gone
268
+ const jsonFiles = fs.readdirSync(inboxDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
269
+ expect(jsonFiles.length).toBe(0);
270
+ });
271
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Tests for find-browse binary locator.
3
+ */
4
+
5
+ import { describe, test, expect } from 'bun:test';
6
+ import { locateBinary } from '../src/find-browse';
7
+ import { existsSync } from 'fs';
8
+
9
+ describe('locateBinary', () => {
10
+ test('returns null when no binary exists at known paths', () => {
11
+ // This test depends on the test environment — if a real binary exists at
12
+ // ~/.claude/skills/gstack/browse/dist/browse, it will find it.
13
+ // We mainly test that the function doesn't throw.
14
+ const result = locateBinary();
15
+ expect(result === null || typeof result === 'string').toBe(true);
16
+ });
17
+
18
+ test('returns string path when binary exists', () => {
19
+ const result = locateBinary();
20
+ if (result !== null) {
21
+ expect(existsSync(result)).toBe(true);
22
+ }
23
+ });
24
+
25
+ test('priority chain checks .codex, .agents, .claude markers', () => {
26
+ // Verify the source code implements the correct priority order.
27
+ // We read the function source to confirm the markers array order.
28
+ const src = require('fs').readFileSync(require('path').join(__dirname, '../src/find-browse.ts'), 'utf-8');
29
+ // The markers array should list .codex first, then .agents, then .claude
30
+ const markersMatch = src.match(/const markers = \[([^\]]+)\]/);
31
+ expect(markersMatch).not.toBeNull();
32
+ const markers = markersMatch![1];
33
+ const codexIdx = markers.indexOf('.codex');
34
+ const agentsIdx = markers.indexOf('.agents');
35
+ const claudeIdx = markers.indexOf('.claude');
36
+ // All three must be present
37
+ expect(codexIdx).toBeGreaterThanOrEqual(0);
38
+ expect(agentsIdx).toBeGreaterThanOrEqual(0);
39
+ expect(claudeIdx).toBeGreaterThanOrEqual(0);
40
+ // .codex before .agents before .claude
41
+ expect(codexIdx).toBeLessThan(agentsIdx);
42
+ expect(agentsIdx).toBeLessThan(claudeIdx);
43
+ });
44
+
45
+ test('function signature accepts no arguments', () => {
46
+ // locateBinary should be callable with no arguments
47
+ expect(typeof locateBinary).toBe('function');
48
+ expect(locateBinary.length).toBe(0);
49
+ });
50
+ });