opengstack 0.13.10 → 0.14.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 (151) hide show
  1. package/{skills/land-and-deploy/SKILL.md → commands/autoplan.md} +0 -16
  2. package/{skills/benchmark/SKILL.md → commands/benchmark.md} +0 -17
  3. package/{skills/browse/SKILL.md → commands/browse.md} +0 -17
  4. package/{skills/ship/SKILL.md → commands/canary.md} +0 -18
  5. package/{skills/careful/SKILL.md → commands/careful.md} +0 -20
  6. package/{skills/canary/SKILL.md → commands/codex.md} +0 -17
  7. package/{skills/connect-chrome/SKILL.md → commands/connect-chrome.md} +0 -15
  8. package/commands/cso.md +72 -0
  9. package/commands/design-consultation.md +72 -0
  10. package/commands/design-review.md +72 -0
  11. package/commands/design-shotgun.md +72 -0
  12. package/commands/document-release.md +72 -0
  13. package/{skills/freeze/SKILL.md → commands/freeze.md} +0 -26
  14. package/{skills/gstack-upgrade/SKILL.md → commands/gstack-upgrade.md} +0 -14
  15. package/{skills/guard/SKILL.md → commands/guard.md} +0 -31
  16. package/commands/investigate.md +72 -0
  17. package/commands/land-and-deploy.md +72 -0
  18. package/commands/office-hours.md +72 -0
  19. package/commands/plan-ceo-review.md +72 -0
  20. package/commands/plan-design-review.md +72 -0
  21. package/commands/plan-eng-review.md +72 -0
  22. package/commands/qa-only.md +72 -0
  23. package/commands/qa.md +72 -0
  24. package/commands/retro.md +72 -0
  25. package/commands/review.md +72 -0
  26. package/{skills/setup-browser-cookies/SKILL.md → commands/setup-browser-cookies.md} +0 -14
  27. package/commands/setup-deploy.md +72 -0
  28. package/commands/ship.md +72 -0
  29. package/{skills/unfreeze/SKILL.md → commands/unfreeze.md} +0 -12
  30. package/package.json +4 -4
  31. package/scripts/install-commands.js +45 -0
  32. package/skills/autoplan/SKILL.md +0 -96
  33. package/skills/autoplan/SKILL.md.tmpl +0 -694
  34. package/skills/benchmark/SKILL.md.tmpl +0 -222
  35. package/skills/browse/SKILL.md.tmpl +0 -131
  36. package/skills/browse/bin/find-browse +0 -21
  37. package/skills/browse/bin/remote-slug +0 -14
  38. package/skills/browse/scripts/build-node-server.sh +0 -48
  39. package/skills/browse/src/activity.ts +0 -208
  40. package/skills/browse/src/browser-manager.ts +0 -959
  41. package/skills/browse/src/buffers.ts +0 -137
  42. package/skills/browse/src/bun-polyfill.cjs +0 -109
  43. package/skills/browse/src/cli.ts +0 -678
  44. package/skills/browse/src/commands.ts +0 -128
  45. package/skills/browse/src/config.ts +0 -150
  46. package/skills/browse/src/cookie-import-browser.ts +0 -625
  47. package/skills/browse/src/cookie-picker-routes.ts +0 -230
  48. package/skills/browse/src/cookie-picker-ui.ts +0 -688
  49. package/skills/browse/src/find-browse.ts +0 -61
  50. package/skills/browse/src/meta-commands.ts +0 -550
  51. package/skills/browse/src/platform.ts +0 -17
  52. package/skills/browse/src/read-commands.ts +0 -358
  53. package/skills/browse/src/server.ts +0 -1192
  54. package/skills/browse/src/sidebar-agent.ts +0 -280
  55. package/skills/browse/src/sidebar-utils.ts +0 -21
  56. package/skills/browse/src/snapshot.ts +0 -407
  57. package/skills/browse/src/url-validation.ts +0 -95
  58. package/skills/browse/src/write-commands.ts +0 -364
  59. package/skills/browse/test/activity.test.ts +0 -120
  60. package/skills/browse/test/adversarial-security.test.ts +0 -32
  61. package/skills/browse/test/browser-manager-unit.test.ts +0 -17
  62. package/skills/browse/test/bun-polyfill.test.ts +0 -72
  63. package/skills/browse/test/commands.test.ts +0 -2075
  64. package/skills/browse/test/compare-board.test.ts +0 -342
  65. package/skills/browse/test/config.test.ts +0 -316
  66. package/skills/browse/test/cookie-import-browser.test.ts +0 -519
  67. package/skills/browse/test/cookie-picker-routes.test.ts +0 -260
  68. package/skills/browse/test/file-drop.test.ts +0 -271
  69. package/skills/browse/test/find-browse.test.ts +0 -50
  70. package/skills/browse/test/findport.test.ts +0 -191
  71. package/skills/browse/test/fixtures/basic.html +0 -33
  72. package/skills/browse/test/fixtures/cursor-interactive.html +0 -22
  73. package/skills/browse/test/fixtures/dialog.html +0 -15
  74. package/skills/browse/test/fixtures/empty.html +0 -2
  75. package/skills/browse/test/fixtures/forms.html +0 -55
  76. package/skills/browse/test/fixtures/iframe.html +0 -30
  77. package/skills/browse/test/fixtures/network-idle.html +0 -30
  78. package/skills/browse/test/fixtures/qa-eval-checkout.html +0 -108
  79. package/skills/browse/test/fixtures/qa-eval-spa.html +0 -98
  80. package/skills/browse/test/fixtures/qa-eval.html +0 -51
  81. package/skills/browse/test/fixtures/responsive.html +0 -49
  82. package/skills/browse/test/fixtures/snapshot.html +0 -55
  83. package/skills/browse/test/fixtures/spa.html +0 -24
  84. package/skills/browse/test/fixtures/states.html +0 -17
  85. package/skills/browse/test/fixtures/upload.html +0 -25
  86. package/skills/browse/test/gstack-config.test.ts +0 -138
  87. package/skills/browse/test/gstack-update-check.test.ts +0 -514
  88. package/skills/browse/test/handoff.test.ts +0 -235
  89. package/skills/browse/test/path-validation.test.ts +0 -91
  90. package/skills/browse/test/platform.test.ts +0 -37
  91. package/skills/browse/test/server-auth.test.ts +0 -65
  92. package/skills/browse/test/sidebar-agent-roundtrip.test.ts +0 -226
  93. package/skills/browse/test/sidebar-agent.test.ts +0 -199
  94. package/skills/browse/test/sidebar-integration.test.ts +0 -320
  95. package/skills/browse/test/sidebar-unit.test.ts +0 -96
  96. package/skills/browse/test/snapshot.test.ts +0 -467
  97. package/skills/browse/test/state-ttl.test.ts +0 -35
  98. package/skills/browse/test/test-server.ts +0 -57
  99. package/skills/browse/test/url-validation.test.ts +0 -72
  100. package/skills/browse/test/watch.test.ts +0 -129
  101. package/skills/canary/SKILL.md.tmpl +0 -212
  102. package/skills/careful/SKILL.md.tmpl +0 -56
  103. package/skills/careful/bin/check-careful.sh +0 -112
  104. package/skills/codex/SKILL.md +0 -90
  105. package/skills/codex/SKILL.md.tmpl +0 -417
  106. package/skills/connect-chrome/SKILL.md.tmpl +0 -195
  107. package/skills/cso/ACKNOWLEDGEMENTS.md +0 -14
  108. package/skills/cso/SKILL.md +0 -93
  109. package/skills/cso/SKILL.md.tmpl +0 -606
  110. package/skills/design-consultation/SKILL.md +0 -94
  111. package/skills/design-consultation/SKILL.md.tmpl +0 -415
  112. package/skills/design-review/SKILL.md +0 -94
  113. package/skills/design-review/SKILL.md.tmpl +0 -290
  114. package/skills/design-shotgun/SKILL.md +0 -91
  115. package/skills/design-shotgun/SKILL.md.tmpl +0 -285
  116. package/skills/document-release/SKILL.md +0 -91
  117. package/skills/document-release/SKILL.md.tmpl +0 -359
  118. package/skills/freeze/SKILL.md.tmpl +0 -77
  119. package/skills/freeze/bin/check-freeze.sh +0 -79
  120. package/skills/gstack-upgrade/SKILL.md.tmpl +0 -222
  121. package/skills/guard/SKILL.md.tmpl +0 -77
  122. package/skills/investigate/SKILL.md +0 -105
  123. package/skills/investigate/SKILL.md.tmpl +0 -194
  124. package/skills/land-and-deploy/SKILL.md.tmpl +0 -881
  125. package/skills/office-hours/SKILL.md +0 -96
  126. package/skills/office-hours/SKILL.md.tmpl +0 -645
  127. package/skills/plan-ceo-review/SKILL.md +0 -94
  128. package/skills/plan-ceo-review/SKILL.md.tmpl +0 -811
  129. package/skills/plan-design-review/SKILL.md +0 -92
  130. package/skills/plan-design-review/SKILL.md.tmpl +0 -446
  131. package/skills/plan-eng-review/SKILL.md +0 -93
  132. package/skills/plan-eng-review/SKILL.md.tmpl +0 -303
  133. package/skills/qa/SKILL.md +0 -95
  134. package/skills/qa/SKILL.md.tmpl +0 -316
  135. package/skills/qa/references/issue-taxonomy.md +0 -85
  136. package/skills/qa/templates/qa-report-template.md +0 -126
  137. package/skills/qa-only/SKILL.md +0 -89
  138. package/skills/qa-only/SKILL.md.tmpl +0 -101
  139. package/skills/retro/SKILL.md +0 -89
  140. package/skills/retro/SKILL.md.tmpl +0 -820
  141. package/skills/review/SKILL.md +0 -92
  142. package/skills/review/SKILL.md.tmpl +0 -281
  143. package/skills/review/TODOS-format.md +0 -62
  144. package/skills/review/checklist.md +0 -220
  145. package/skills/review/design-checklist.md +0 -132
  146. package/skills/review/greptile-triage.md +0 -220
  147. package/skills/setup-browser-cookies/SKILL.md.tmpl +0 -81
  148. package/skills/setup-deploy/SKILL.md +0 -92
  149. package/skills/setup-deploy/SKILL.md.tmpl +0 -215
  150. package/skills/ship/SKILL.md.tmpl +0 -636
  151. package/skills/unfreeze/SKILL.md.tmpl +0 -36
@@ -1,235 +0,0 @@
1
- /**
2
- * Tests for handoff/resume commands — headless-to-headed browser switching.
3
- *
4
- * Unit tests cover saveState/restoreState, failure tracking, and edge cases.
5
- * Integration tests cover the full handoff flow with real Playwright browsers.
6
- */
7
-
8
- import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
9
- import { startTestServer } from './test-server';
10
- import { BrowserManager, type BrowserState } from '../src/browser-manager';
11
- import { handleWriteCommand } from '../src/write-commands';
12
- import { handleMetaCommand } from '../src/meta-commands';
13
-
14
- let testServer: ReturnType<typeof startTestServer>;
15
- let bm: BrowserManager;
16
- let baseUrl: string;
17
-
18
- beforeAll(async () => {
19
- testServer = startTestServer(0);
20
- baseUrl = testServer.url;
21
-
22
- bm = new BrowserManager();
23
- await bm.launch();
24
- });
25
-
26
- afterAll(() => {
27
- try { testServer.server.stop(); } catch {}
28
- setTimeout(() => process.exit(0), 500);
29
- });
30
-
31
- // ─── Unit Tests: Failure Tracking (no browser needed) ────────────
32
-
33
- describe('failure tracking', () => {
34
- test('getFailureHint returns null when below threshold', () => {
35
- const tracker = new BrowserManager();
36
- tracker.incrementFailures();
37
- tracker.incrementFailures();
38
- expect(tracker.getFailureHint()).toBeNull();
39
- });
40
-
41
- test('getFailureHint returns hint after 3 consecutive failures', () => {
42
- const tracker = new BrowserManager();
43
- tracker.incrementFailures();
44
- tracker.incrementFailures();
45
- tracker.incrementFailures();
46
- const hint = tracker.getFailureHint();
47
- expect(hint).not.toBeNull();
48
- expect(hint).toContain('handoff');
49
- expect(hint).toContain('3');
50
- });
51
-
52
- test('hint suppressed when already headed', () => {
53
- const tracker = new BrowserManager();
54
- (tracker as any).isHeaded = true;
55
- tracker.incrementFailures();
56
- tracker.incrementFailures();
57
- tracker.incrementFailures();
58
- expect(tracker.getFailureHint()).toBeNull();
59
- });
60
-
61
- test('resetFailures clears the counter', () => {
62
- const tracker = new BrowserManager();
63
- tracker.incrementFailures();
64
- tracker.incrementFailures();
65
- tracker.incrementFailures();
66
- expect(tracker.getFailureHint()).not.toBeNull();
67
- tracker.resetFailures();
68
- expect(tracker.getFailureHint()).toBeNull();
69
- });
70
-
71
- test('getIsHeaded returns false by default', () => {
72
- const tracker = new BrowserManager();
73
- expect(tracker.getIsHeaded()).toBe(false);
74
- });
75
- });
76
-
77
- // ─── Unit Tests: State Save/Restore (shared browser) ─────────────
78
-
79
- describe('saveState', () => {
80
- test('captures cookies and page URLs', async () => {
81
- await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
82
- await handleWriteCommand('cookie', ['testcookie=testvalue'], bm);
83
-
84
- const state = await bm.saveState();
85
-
86
- expect(state.cookies.length).toBeGreaterThan(0);
87
- expect(state.cookies.some(c => c.name === 'testcookie')).toBe(true);
88
- expect(state.pages.length).toBeGreaterThanOrEqual(1);
89
- expect(state.pages.some(p => p.url.includes('/basic.html'))).toBe(true);
90
- }, 15000);
91
-
92
- test('captures localStorage and sessionStorage', async () => {
93
- await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
94
- const page = bm.getPage();
95
- await page.evaluate(() => {
96
- localStorage.setItem('lsKey', 'lsValue');
97
- sessionStorage.setItem('ssKey', 'ssValue');
98
- });
99
-
100
- const state = await bm.saveState();
101
- const activePage = state.pages.find(p => p.isActive);
102
-
103
- expect(activePage).toBeDefined();
104
- expect(activePage!.storage).not.toBeNull();
105
- expect(activePage!.storage!.localStorage).toHaveProperty('lsKey', 'lsValue');
106
- expect(activePage!.storage!.sessionStorage).toHaveProperty('ssKey', 'ssValue');
107
- }, 15000);
108
-
109
- test('captures multiple tabs', async () => {
110
- while (bm.getTabCount() > 1) {
111
- await bm.closeTab();
112
- }
113
- await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
114
- await handleMetaCommand('newtab', [baseUrl + '/form.html'], bm, () => {});
115
-
116
- const state = await bm.saveState();
117
- expect(state.pages.length).toBe(2);
118
- const activePage = state.pages.find(p => p.isActive);
119
- expect(activePage).toBeDefined();
120
- expect(activePage!.url).toContain('/form.html');
121
-
122
- await bm.closeTab();
123
- }, 15000);
124
- });
125
-
126
- describe('restoreState', () => {
127
- test('state survives recreateContext round-trip', async () => {
128
- await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
129
- await handleWriteCommand('cookie', ['restored=yes'], bm);
130
-
131
- const stateBefore = await bm.saveState();
132
- expect(stateBefore.cookies.some(c => c.name === 'restored')).toBe(true);
133
-
134
- await bm.recreateContext();
135
-
136
- const stateAfter = await bm.saveState();
137
- expect(stateAfter.cookies.some(c => c.name === 'restored')).toBe(true);
138
- expect(stateAfter.pages.length).toBeGreaterThanOrEqual(1);
139
- }, 30000);
140
- });
141
-
142
- // ─── Unit Tests: Handoff Edge Cases ──────────────────────────────
143
-
144
- describe('handoff edge cases', () => {
145
- test('handoff when already headed returns no-op', async () => {
146
- (bm as any).isHeaded = true;
147
- const result = await bm.handoff('test');
148
- expect(result).toContain('Already in headed mode');
149
- (bm as any).isHeaded = false;
150
- }, 10000);
151
-
152
- test('resume clears refs and resets failures', () => {
153
- bm.incrementFailures();
154
- bm.incrementFailures();
155
- bm.incrementFailures();
156
- bm.resume();
157
- expect(bm.getFailureHint()).toBeNull();
158
- expect(bm.getRefCount()).toBe(0);
159
- });
160
-
161
- test('resume without prior handoff works via meta command', async () => {
162
- await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
163
- const result = await handleMetaCommand('resume', [], bm, () => {});
164
- expect(result).toContain('RESUMED');
165
- }, 15000);
166
- });
167
-
168
- // ─── Integration Tests: Full Handoff Flow ────────────────────────
169
- // Each handoff test creates its own BrowserManager since handoff swaps the browser.
170
- // These tests run sequentially (one browser at a time) to avoid resource issues.
171
-
172
- describe('handoff integration', () => {
173
- test('full handoff: cookies preserved, headed mode active, commands work', async () => {
174
- const hbm = new BrowserManager();
175
- await hbm.launch();
176
-
177
- try {
178
- // Set up state
179
- await handleWriteCommand('goto', [baseUrl + '/basic.html'], hbm);
180
- await handleWriteCommand('cookie', ['handoff_test=preserved'], hbm);
181
-
182
- // Handoff
183
- const result = await hbm.handoff('Testing handoff');
184
- expect(result).toContain('HANDOFF:');
185
- expect(result).toContain('Testing handoff');
186
- expect(result).toContain('resume');
187
- expect(hbm.getIsHeaded()).toBe(true);
188
-
189
- // Verify cookies survived
190
- const { handleReadCommand } = await import('../src/read-commands');
191
- const cookiesResult = await handleReadCommand('cookies', [], hbm);
192
- expect(cookiesResult).toContain('handoff_test');
193
-
194
- // Verify commands still work
195
- const text = await handleReadCommand('text', [], hbm);
196
- expect(text.length).toBeGreaterThan(0);
197
-
198
- // Resume
199
- const resumeResult = await handleMetaCommand('resume', [], hbm, () => {});
200
- expect(resumeResult).toContain('RESUMED');
201
- } finally {
202
- await hbm.close();
203
- }
204
- }, 45000);
205
-
206
- test('multi-tab handoff preserves all tabs', async () => {
207
- const hbm = new BrowserManager();
208
- await hbm.launch();
209
-
210
- try {
211
- await handleWriteCommand('goto', [baseUrl + '/basic.html'], hbm);
212
- await handleMetaCommand('newtab', [baseUrl + '/form.html'], hbm, () => {});
213
- expect(hbm.getTabCount()).toBe(2);
214
-
215
- await hbm.handoff('multi-tab test');
216
- expect(hbm.getTabCount()).toBe(2);
217
- expect(hbm.getIsHeaded()).toBe(true);
218
- } finally {
219
- await hbm.close();
220
- }
221
- }, 45000);
222
-
223
- test('handoff meta command joins args as message', async () => {
224
- const hbm = new BrowserManager();
225
- await hbm.launch();
226
-
227
- try {
228
- await handleWriteCommand('goto', [baseUrl + '/basic.html'], hbm);
229
- const result = await handleMetaCommand('handoff', ['CAPTCHA', 'stuck'], hbm, () => {});
230
- expect(result).toContain('CAPTCHA stuck');
231
- } finally {
232
- await hbm.close();
233
- }
234
- }, 45000);
235
- });
@@ -1,91 +0,0 @@
1
- import { describe, it, expect } from 'bun:test';
2
- import { validateOutputPath } from '../src/meta-commands';
3
- import { validateReadPath } from '../src/read-commands';
4
- import { symlinkSync, unlinkSync, writeFileSync } from 'fs';
5
- import { tmpdir } from 'os';
6
- import { join } from 'path';
7
-
8
- describe('validateOutputPath', () => {
9
- it('allows paths within /tmp', () => {
10
- expect(() => validateOutputPath('/tmp/screenshot.png')).not.toThrow();
11
- });
12
-
13
- it('allows paths in subdirectories of /tmp', () => {
14
- expect(() => validateOutputPath('/tmp/browse/output.png')).not.toThrow();
15
- });
16
-
17
- it('allows paths within cwd', () => {
18
- expect(() => validateOutputPath(`${process.cwd()}/output.png`)).not.toThrow();
19
- });
20
-
21
- it('blocks paths outside safe directories', () => {
22
- expect(() => validateOutputPath('/etc/cron.d/backdoor.png')).toThrow(/Path must be within/);
23
- });
24
-
25
- it('blocks /tmpevil prefix collision', () => {
26
- expect(() => validateOutputPath('/tmpevil/file.png')).toThrow(/Path must be within/);
27
- });
28
-
29
- it('blocks home directory paths', () => {
30
- expect(() => validateOutputPath('/Users/someone/file.png')).toThrow(/Path must be within/);
31
- });
32
-
33
- it('blocks path traversal via ..', () => {
34
- expect(() => validateOutputPath('/tmp/../etc/passwd')).toThrow(/Path must be within/);
35
- });
36
- });
37
-
38
- describe('validateReadPath', () => {
39
- it('allows absolute paths within /tmp', () => {
40
- expect(() => validateReadPath('/tmp/script.js')).not.toThrow();
41
- });
42
-
43
- it('allows absolute paths within cwd', () => {
44
- expect(() => validateReadPath(`${process.cwd()}/test.js`)).not.toThrow();
45
- });
46
-
47
- it('allows relative paths without traversal', () => {
48
- expect(() => validateReadPath('src/index.js')).not.toThrow();
49
- });
50
-
51
- it('blocks absolute paths outside safe directories', () => {
52
- expect(() => validateReadPath('/etc/passwd')).toThrow(/Path must be within/);
53
- });
54
-
55
- it('blocks /tmpevil prefix collision', () => {
56
- expect(() => validateReadPath('/tmpevil/file.js')).toThrow(/Path must be within/);
57
- });
58
-
59
- it('blocks path traversal sequences', () => {
60
- expect(() => validateReadPath('../../../etc/passwd')).toThrow(/Path must be within/);
61
- });
62
-
63
- it('blocks nested path traversal', () => {
64
- expect(() => validateReadPath('src/../../etc/passwd')).toThrow(/Path must be within/);
65
- });
66
-
67
- it('blocks symlink inside safe dir pointing outside', () => {
68
- const linkPath = join(tmpdir(), 'test-symlink-bypass-' + Date.now());
69
- try {
70
- symlinkSync('/etc/passwd', linkPath);
71
- expect(() => validateReadPath(linkPath)).toThrow(/Path must be within/);
72
- } finally {
73
- try { unlinkSync(linkPath); } catch {}
74
- }
75
- });
76
-
77
- it('throws clear error on non-ENOENT realpathSync failure', () => {
78
- // Attempting to resolve a path through a non-directory should throw
79
- // a descriptive error (ENOTDIR), not silently pass through.
80
- // Create a regular file, then try to resolve a path through it as if it were a directory.
81
- const filePath = join(tmpdir(), 'test-notdir-' + Date.now());
82
- try {
83
- writeFileSync(filePath, 'not a directory');
84
- // filePath is a file, so filePath + '/subpath' triggers ENOTDIR
85
- const invalidPath = join(filePath, 'subpath');
86
- expect(() => validateReadPath(invalidPath)).toThrow(/Cannot resolve real path|Path must be within/);
87
- } finally {
88
- try { unlinkSync(filePath); } catch {}
89
- }
90
- });
91
- });
@@ -1,37 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { TEMP_DIR, isPathWithin, IS_WINDOWS } from '../src/platform';
3
-
4
- describe('platform constants', () => {
5
- test('TEMP_DIR is /tmp on non-Windows', () => {
6
- if (!IS_WINDOWS) {
7
- expect(TEMP_DIR).toBe('/tmp');
8
- }
9
- });
10
-
11
- test('IS_WINDOWS reflects process.platform', () => {
12
- expect(IS_WINDOWS).toBe(process.platform === 'win32');
13
- });
14
- });
15
-
16
- describe('isPathWithin', () => {
17
- test('path inside directory returns true', () => {
18
- expect(isPathWithin('/tmp/foo', '/tmp')).toBe(true);
19
- });
20
-
21
- test('path outside directory returns false', () => {
22
- expect(isPathWithin('/etc/foo', '/tmp')).toBe(false);
23
- });
24
-
25
- test('exact match returns true', () => {
26
- expect(isPathWithin('/tmp', '/tmp')).toBe(true);
27
- });
28
-
29
- test('partial prefix does not match (path traversal)', () => {
30
- // /tmp-evil should NOT match /tmp
31
- expect(isPathWithin('/tmp-evil/foo', '/tmp')).toBe(false);
32
- });
33
-
34
- test('nested path returns true', () => {
35
- expect(isPathWithin('/tmp/a/b/c', '/tmp')).toBe(true);
36
- });
37
- });
@@ -1,65 +0,0 @@
1
- /**
2
- * Server auth security tests — verify security remediation in server.ts
3
- *
4
- * Tests are source-level: they read server.ts and verify that auth checks,
5
- * CORS restrictions, and token removal are correctly in place.
6
- */
7
-
8
- import { describe, test, expect } from 'bun:test';
9
- import * as fs from 'fs';
10
- import * as path from 'path';
11
-
12
- const SERVER_SRC = fs.readFileSync(path.join(import.meta.dir, '../src/server.ts'), 'utf-8');
13
-
14
- // Helper: extract a block of source between two markers
15
- function sliceBetween(source: string, startMarker: string, endMarker: string): string {
16
- const startIdx = source.indexOf(startMarker);
17
- if (startIdx === -1) throw new Error(`Marker not found: ${startMarker}`);
18
- const endIdx = source.indexOf(endMarker, startIdx + startMarker.length);
19
- if (endIdx === -1) throw new Error(`End marker not found: ${endMarker}`);
20
- return source.slice(startIdx, endIdx);
21
- }
22
-
23
- describe('Server auth security', () => {
24
- // Test 1: /health response must not leak the auth token
25
- test('/health response must not contain token field', () => {
26
- const healthBlock = sliceBetween(SERVER_SRC, "url.pathname === '/health'", "url.pathname === '/refs'");
27
- // The old pattern was: token: AUTH_TOKEN
28
- // The new pattern should have a comment indicating token was removed
29
- expect(healthBlock).not.toContain('token: AUTH_TOKEN');
30
- expect(healthBlock).toContain('token removed');
31
- });
32
-
33
- // Test 2: /refs endpoint requires auth via validateAuth
34
- test('/refs endpoint requires authentication', () => {
35
- const refsBlock = sliceBetween(SERVER_SRC, "url.pathname === '/refs'", "url.pathname === '/activity/stream'");
36
- expect(refsBlock).toContain('validateAuth');
37
- });
38
-
39
- // Test 3: /refs has no wildcard CORS header
40
- test('/refs has no wildcard CORS header', () => {
41
- const refsBlock = sliceBetween(SERVER_SRC, "url.pathname === '/refs'", "url.pathname === '/activity/stream'");
42
- expect(refsBlock).not.toContain("'*'");
43
- });
44
-
45
- // Test 4: /activity/history requires auth via validateAuth
46
- test('/activity/history requires authentication', () => {
47
- const historyBlock = sliceBetween(SERVER_SRC, "url.pathname === '/activity/history'", 'Sidebar endpoints');
48
- expect(historyBlock).toContain('validateAuth');
49
- });
50
-
51
- // Test 5: /activity/history has no wildcard CORS header
52
- test('/activity/history has no wildcard CORS header', () => {
53
- const historyBlock = sliceBetween(SERVER_SRC, "url.pathname === '/activity/history'", 'Sidebar endpoints');
54
- expect(historyBlock).not.toContain("'*'");
55
- });
56
-
57
- // Test 6: /activity/stream requires auth (inline Bearer or ?token= check)
58
- test('/activity/stream requires authentication with inline token check', () => {
59
- const streamBlock = sliceBetween(SERVER_SRC, "url.pathname === '/activity/stream'", "url.pathname === '/activity/history'");
60
- expect(streamBlock).toContain('validateAuth');
61
- expect(streamBlock).toContain('AUTH_TOKEN');
62
- // Should not have wildcard CORS for the SSE stream
63
- expect(streamBlock).not.toContain("Access-Control-Allow-Origin': '*'");
64
- });
65
- });
@@ -1,226 +0,0 @@
1
- /**
2
- * Layer 3: Sidebar agent round-trip tests.
3
- * Starts server + sidebar-agent together. Mocks the `claude` binary with a shell
4
- * script that outputs canned stream-json. Verifies events flow end-to-end:
5
- * POST /sidebar-command → queue → sidebar-agent → mock claude → events → /sidebar-chat
6
- */
7
-
8
- import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
9
- import { spawn, type Subprocess } from 'bun';
10
- import * as fs from 'fs';
11
- import * as os from 'os';
12
- import * as path from 'path';
13
-
14
- let serverProc: Subprocess | null = null;
15
- let agentProc: Subprocess | null = null;
16
- let serverPort: number = 0;
17
- let authToken: string = '';
18
- let tmpDir: string = '';
19
- let stateFile: string = '';
20
- let queueFile: string = '';
21
- let mockBinDir: string = '';
22
-
23
- async function api(pathname: string, opts: RequestInit = {}): Promise<Response> {
24
- const headers: Record<string, string> = {
25
- 'Content-Type': 'application/json',
26
- ...(opts.headers as Record<string, string> || {}),
27
- };
28
- if (!headers['Authorization'] && authToken) {
29
- headers['Authorization'] = `Bearer ${authToken}`;
30
- }
31
- return fetch(`http://127.0.0.1:${serverPort}${pathname}`, { ...opts, headers });
32
- }
33
-
34
- async function resetState() {
35
- await api('/sidebar-session/new', { method: 'POST' });
36
- fs.writeFileSync(queueFile, '');
37
- }
38
-
39
- async function pollChatUntil(
40
- predicate: (entries: any[]) => boolean,
41
- timeoutMs = 10000,
42
- ): Promise<any[]> {
43
- const deadline = Date.now() + timeoutMs;
44
- while (Date.now() < deadline) {
45
- const resp = await api('/sidebar-chat?after=0');
46
- const data = await resp.json();
47
- if (predicate(data.entries)) return data.entries;
48
- await new Promise(r => setTimeout(r, 300));
49
- }
50
- // Return whatever we have on timeout
51
- const resp = await api('/sidebar-chat?after=0');
52
- return (await resp.json()).entries;
53
- }
54
-
55
- function writeMockClaude(script: string) {
56
- const mockPath = path.join(mockBinDir, 'claude');
57
- fs.writeFileSync(mockPath, script, { mode: 0o755 });
58
- }
59
-
60
- beforeAll(async () => {
61
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sidebar-roundtrip-'));
62
- stateFile = path.join(tmpDir, 'browse.json');
63
- queueFile = path.join(tmpDir, 'sidebar-queue.jsonl');
64
- mockBinDir = path.join(tmpDir, 'bin');
65
- fs.mkdirSync(mockBinDir, { recursive: true });
66
- fs.mkdirSync(path.dirname(queueFile), { recursive: true });
67
-
68
- // Write default mock claude that outputs canned events
69
- writeMockClaude(`#!/bin/bash
70
- echo '{"type":"system","session_id":"mock-session-123"}'
71
- echo '{"type":"assistant","message":{"content":[{"type":"text","text":"I can see the page. It looks like a test fixture."}]}}'
72
- echo '{"type":"result","result":"Done."}'
73
- `);
74
-
75
- // Start server (no browser)
76
- const serverScript = path.resolve(__dirname, '..', 'src', 'server.ts');
77
- serverProc = spawn(['bun', 'run', serverScript], {
78
- env: {
79
- ...process.env,
80
- BROWSE_STATE_FILE: stateFile,
81
- BROWSE_HEADLESS_SKIP: '1',
82
- BROWSE_PORT: '0',
83
- SIDEBAR_QUEUE_PATH: queueFile,
84
- BROWSE_IDLE_TIMEOUT: '300',
85
- },
86
- stdio: ['ignore', 'pipe', 'pipe'],
87
- });
88
-
89
- // Wait for server
90
- const deadline = Date.now() + 15000;
91
- while (Date.now() < deadline) {
92
- if (fs.existsSync(stateFile)) {
93
- try {
94
- const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
95
- if (state.port && state.token) {
96
- serverPort = state.port;
97
- authToken = state.token;
98
- break;
99
- }
100
- } catch {}
101
- }
102
- await new Promise(r => setTimeout(r, 100));
103
- }
104
- if (!serverPort) throw new Error('Server did not start in time');
105
-
106
- // Start sidebar-agent with mock claude on PATH
107
- const agentScript = path.resolve(__dirname, '..', 'src', 'sidebar-agent.ts');
108
- agentProc = spawn(['bun', 'run', agentScript], {
109
- env: {
110
- ...process.env,
111
- PATH: `${mockBinDir}:${process.env.PATH}`,
112
- BROWSE_SERVER_PORT: String(serverPort),
113
- BROWSE_STATE_FILE: stateFile,
114
- SIDEBAR_QUEUE_PATH: queueFile,
115
- SIDEBAR_AGENT_TIMEOUT: '10000',
116
- BROWSE_BIN: 'browse', // doesn't matter, mock claude doesn't use it
117
- },
118
- stdio: ['ignore', 'pipe', 'pipe'],
119
- });
120
-
121
- // Give sidebar-agent time to start polling
122
- await new Promise(r => setTimeout(r, 1000));
123
- }, 20000);
124
-
125
- afterAll(() => {
126
- if (agentProc) { try { agentProc.kill(); } catch {} }
127
- if (serverProc) { try { serverProc.kill(); } catch {} }
128
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
129
- });
130
-
131
- describe('sidebar-agent round-trip', () => {
132
- test('full message round-trip with mock claude', async () => {
133
- await resetState();
134
-
135
- // Send a command
136
- const resp = await api('/sidebar-command', {
137
- method: 'POST',
138
- body: JSON.stringify({
139
- message: 'what is on this page?',
140
- activeTabUrl: 'https://example.com/test',
141
- }),
142
- });
143
- expect(resp.status).toBe(200);
144
-
145
- // Wait for mock claude to process and events to arrive
146
- const entries = await pollChatUntil(
147
- (entries) => entries.some((e: any) => e.type === 'agent_done'),
148
- 15000,
149
- );
150
-
151
- // Verify the flow: user message → agent_start → text → agent_done
152
- const userEntry = entries.find((e: any) => e.role === 'user');
153
- expect(userEntry).toBeDefined();
154
- expect(userEntry.message).toBe('what is on this page?');
155
-
156
- // The mock claude outputs text — check for any agent text entry
157
- const textEntries = entries.filter((e: any) => e.role === 'agent' && (e.type === 'text' || e.type === 'result'));
158
- expect(textEntries.length).toBeGreaterThan(0);
159
-
160
- const doneEntry = entries.find((e: any) => e.type === 'agent_done');
161
- expect(doneEntry).toBeDefined();
162
-
163
- // Agent should be back to idle
164
- const session = await (await api('/sidebar-session')).json();
165
- expect(session.agent.status).toBe('idle');
166
- }, 20000);
167
-
168
- test('claude crash produces agent_error', async () => {
169
- await resetState();
170
-
171
- // Replace mock claude with one that crashes
172
- writeMockClaude(`#!/bin/bash
173
- echo '{"type":"system","session_id":"crash-test"}' >&2
174
- exit 1
175
- `);
176
-
177
- await api('/sidebar-command', {
178
- method: 'POST',
179
- body: JSON.stringify({ message: 'crash test' }),
180
- });
181
-
182
- // Wait for agent_done (sidebar-agent sends agent_done even on crash via proc.on('close'))
183
- const entries = await pollChatUntil(
184
- (entries) => entries.some((e: any) => e.type === 'agent_done' || e.type === 'agent_error'),
185
- 15000,
186
- );
187
-
188
- // Agent should recover to idle
189
- const session = await (await api('/sidebar-session')).json();
190
- expect(session.agent.status).toBe('idle');
191
-
192
- // Restore working mock
193
- writeMockClaude(`#!/bin/bash
194
- echo '{"type":"assistant","message":{"content":[{"type":"text","text":"recovered"}]}}'
195
- `);
196
- }, 20000);
197
-
198
- test('sequential queue drain', async () => {
199
- await resetState();
200
-
201
- // Restore working mock
202
- writeMockClaude(`#!/bin/bash
203
- echo '{"type":"assistant","message":{"content":[{"type":"text","text":"response to: '"'"'$*'"'"'"}]}}'
204
- `);
205
-
206
- // Send two messages rapidly — first processes, second queues
207
- await api('/sidebar-command', {
208
- method: 'POST',
209
- body: JSON.stringify({ message: 'first message' }),
210
- });
211
- await api('/sidebar-command', {
212
- method: 'POST',
213
- body: JSON.stringify({ message: 'second message' }),
214
- });
215
-
216
- // Wait for both to complete (two agent_done events)
217
- const entries = await pollChatUntil(
218
- (entries) => entries.filter((e: any) => e.type === 'agent_done').length >= 2,
219
- 20000,
220
- );
221
-
222
- // Both user messages should be in chat
223
- const userEntries = entries.filter((e: any) => e.role === 'user');
224
- expect(userEntries.length).toBeGreaterThanOrEqual(2);
225
- }, 25000);
226
- });