opengstack 0.13.10 → 0.14.2

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 (189) hide show
  1. package/AGENTS.md +4 -4
  2. package/CLAUDE.md +127 -110
  3. package/README.md +10 -5
  4. package/SKILL.md +500 -70
  5. package/bin/opengstack.js +69 -69
  6. package/{skills/land-and-deploy/SKILL.md → commands/autoplan.md} +7 -25
  7. package/{skills/benchmark/SKILL.md → commands/benchmark.md} +84 -108
  8. package/{skills/browse/SKILL.md → commands/browse.md} +60 -81
  9. package/{skills/ship/SKILL.md → commands/canary.md} +7 -27
  10. package/{skills/careful/SKILL.md → commands/careful.md} +2 -22
  11. package/{skills/canary/SKILL.md → commands/codex.md} +7 -26
  12. package/{skills/connect-chrome/SKILL.md → commands/connect-chrome.md} +7 -24
  13. package/commands/cso.md +70 -0
  14. package/commands/design-consultation.md +70 -0
  15. package/commands/design-review.md +70 -0
  16. package/commands/design-shotgun.md +70 -0
  17. package/commands/document-release.md +70 -0
  18. package/{skills/freeze/SKILL.md → commands/freeze.md} +3 -29
  19. package/{skills/guard/SKILL.md → commands/guard.md} +4 -35
  20. package/commands/investigate.md +70 -0
  21. package/commands/land-and-deploy.md +70 -0
  22. package/commands/office-hours.md +70 -0
  23. package/{skills/gstack-upgrade/SKILL.md → commands/opengstack-upgrade.md} +64 -79
  24. package/commands/plan-ceo-review.md +70 -0
  25. package/commands/plan-design-review.md +70 -0
  26. package/commands/plan-eng-review.md +70 -0
  27. package/commands/qa-only.md +70 -0
  28. package/commands/qa.md +70 -0
  29. package/commands/retro.md +70 -0
  30. package/commands/review.md +70 -0
  31. package/{skills/setup-browser-cookies/SKILL.md → commands/setup-browser-cookies.md} +22 -40
  32. package/commands/setup-deploy.md +70 -0
  33. package/commands/ship.md +70 -0
  34. package/commands/unfreeze.md +25 -0
  35. package/docs/designs/CHROME_VS_CHROMIUM_EXPLORATION.md +9 -9
  36. package/docs/designs/CONDUCTOR_CHROME_SIDEBAR_INTEGRATION.md +2 -2
  37. package/docs/designs/CONDUCTOR_SESSION_API.md +16 -16
  38. package/docs/designs/DESIGN_SHOTGUN.md +74 -74
  39. package/docs/designs/DESIGN_TOOLS_V1.md +111 -111
  40. package/docs/skills.md +483 -202
  41. package/package.json +42 -43
  42. package/scripts/analytics.ts +188 -0
  43. package/scripts/dev-skill.ts +83 -0
  44. package/scripts/discover-skills.ts +39 -0
  45. package/scripts/eval-compare.ts +97 -0
  46. package/scripts/eval-list.ts +117 -0
  47. package/scripts/eval-select.ts +86 -0
  48. package/scripts/eval-summary.ts +188 -0
  49. package/scripts/eval-watch.ts +172 -0
  50. package/scripts/gen-skill-docs.ts +473 -0
  51. package/scripts/resolvers/browse.ts +129 -0
  52. package/scripts/resolvers/codex-helpers.ts +133 -0
  53. package/scripts/resolvers/composition.ts +48 -0
  54. package/scripts/resolvers/confidence.ts +37 -0
  55. package/scripts/resolvers/constants.ts +50 -0
  56. package/scripts/resolvers/design.ts +950 -0
  57. package/scripts/resolvers/index.ts +59 -0
  58. package/scripts/resolvers/learnings.ts +96 -0
  59. package/scripts/resolvers/preamble.ts +505 -0
  60. package/scripts/resolvers/review.ts +884 -0
  61. package/scripts/resolvers/testing.ts +573 -0
  62. package/scripts/resolvers/types.ts +45 -0
  63. package/scripts/resolvers/utility.ts +421 -0
  64. package/scripts/skill-check.ts +190 -0
  65. package/scripts/cleanup.py +0 -100
  66. package/scripts/filter-skills.sh +0 -114
  67. package/scripts/filter_skills.py +0 -164
  68. package/scripts/install-skills.js +0 -60
  69. package/skills/autoplan/SKILL.md +0 -96
  70. package/skills/autoplan/SKILL.md.tmpl +0 -694
  71. package/skills/benchmark/SKILL.md.tmpl +0 -222
  72. package/skills/browse/SKILL.md.tmpl +0 -131
  73. package/skills/browse/bin/find-browse +0 -21
  74. package/skills/browse/bin/remote-slug +0 -14
  75. package/skills/browse/scripts/build-node-server.sh +0 -48
  76. package/skills/browse/src/activity.ts +0 -208
  77. package/skills/browse/src/browser-manager.ts +0 -959
  78. package/skills/browse/src/buffers.ts +0 -137
  79. package/skills/browse/src/bun-polyfill.cjs +0 -109
  80. package/skills/browse/src/cli.ts +0 -678
  81. package/skills/browse/src/commands.ts +0 -128
  82. package/skills/browse/src/config.ts +0 -150
  83. package/skills/browse/src/cookie-import-browser.ts +0 -625
  84. package/skills/browse/src/cookie-picker-routes.ts +0 -230
  85. package/skills/browse/src/cookie-picker-ui.ts +0 -688
  86. package/skills/browse/src/find-browse.ts +0 -61
  87. package/skills/browse/src/meta-commands.ts +0 -550
  88. package/skills/browse/src/platform.ts +0 -17
  89. package/skills/browse/src/read-commands.ts +0 -358
  90. package/skills/browse/src/server.ts +0 -1192
  91. package/skills/browse/src/sidebar-agent.ts +0 -280
  92. package/skills/browse/src/sidebar-utils.ts +0 -21
  93. package/skills/browse/src/snapshot.ts +0 -407
  94. package/skills/browse/src/url-validation.ts +0 -95
  95. package/skills/browse/src/write-commands.ts +0 -364
  96. package/skills/browse/test/activity.test.ts +0 -120
  97. package/skills/browse/test/adversarial-security.test.ts +0 -32
  98. package/skills/browse/test/browser-manager-unit.test.ts +0 -17
  99. package/skills/browse/test/bun-polyfill.test.ts +0 -72
  100. package/skills/browse/test/commands.test.ts +0 -2075
  101. package/skills/browse/test/compare-board.test.ts +0 -342
  102. package/skills/browse/test/config.test.ts +0 -316
  103. package/skills/browse/test/cookie-import-browser.test.ts +0 -519
  104. package/skills/browse/test/cookie-picker-routes.test.ts +0 -260
  105. package/skills/browse/test/file-drop.test.ts +0 -271
  106. package/skills/browse/test/find-browse.test.ts +0 -50
  107. package/skills/browse/test/findport.test.ts +0 -191
  108. package/skills/browse/test/fixtures/basic.html +0 -33
  109. package/skills/browse/test/fixtures/cursor-interactive.html +0 -22
  110. package/skills/browse/test/fixtures/dialog.html +0 -15
  111. package/skills/browse/test/fixtures/empty.html +0 -2
  112. package/skills/browse/test/fixtures/forms.html +0 -55
  113. package/skills/browse/test/fixtures/iframe.html +0 -30
  114. package/skills/browse/test/fixtures/network-idle.html +0 -30
  115. package/skills/browse/test/fixtures/qa-eval-checkout.html +0 -108
  116. package/skills/browse/test/fixtures/qa-eval-spa.html +0 -98
  117. package/skills/browse/test/fixtures/qa-eval.html +0 -51
  118. package/skills/browse/test/fixtures/responsive.html +0 -49
  119. package/skills/browse/test/fixtures/snapshot.html +0 -55
  120. package/skills/browse/test/fixtures/spa.html +0 -24
  121. package/skills/browse/test/fixtures/states.html +0 -17
  122. package/skills/browse/test/fixtures/upload.html +0 -25
  123. package/skills/browse/test/gstack-config.test.ts +0 -138
  124. package/skills/browse/test/gstack-update-check.test.ts +0 -514
  125. package/skills/browse/test/handoff.test.ts +0 -235
  126. package/skills/browse/test/path-validation.test.ts +0 -91
  127. package/skills/browse/test/platform.test.ts +0 -37
  128. package/skills/browse/test/server-auth.test.ts +0 -65
  129. package/skills/browse/test/sidebar-agent-roundtrip.test.ts +0 -226
  130. package/skills/browse/test/sidebar-agent.test.ts +0 -199
  131. package/skills/browse/test/sidebar-integration.test.ts +0 -320
  132. package/skills/browse/test/sidebar-unit.test.ts +0 -96
  133. package/skills/browse/test/snapshot.test.ts +0 -467
  134. package/skills/browse/test/state-ttl.test.ts +0 -35
  135. package/skills/browse/test/test-server.ts +0 -57
  136. package/skills/browse/test/url-validation.test.ts +0 -72
  137. package/skills/browse/test/watch.test.ts +0 -129
  138. package/skills/canary/SKILL.md.tmpl +0 -212
  139. package/skills/careful/SKILL.md.tmpl +0 -56
  140. package/skills/careful/bin/check-careful.sh +0 -112
  141. package/skills/codex/SKILL.md +0 -90
  142. package/skills/codex/SKILL.md.tmpl +0 -417
  143. package/skills/connect-chrome/SKILL.md.tmpl +0 -195
  144. package/skills/cso/ACKNOWLEDGEMENTS.md +0 -14
  145. package/skills/cso/SKILL.md +0 -93
  146. package/skills/cso/SKILL.md.tmpl +0 -606
  147. package/skills/design-consultation/SKILL.md +0 -94
  148. package/skills/design-consultation/SKILL.md.tmpl +0 -415
  149. package/skills/design-review/SKILL.md +0 -94
  150. package/skills/design-review/SKILL.md.tmpl +0 -290
  151. package/skills/design-shotgun/SKILL.md +0 -91
  152. package/skills/design-shotgun/SKILL.md.tmpl +0 -285
  153. package/skills/document-release/SKILL.md +0 -91
  154. package/skills/document-release/SKILL.md.tmpl +0 -359
  155. package/skills/freeze/SKILL.md.tmpl +0 -77
  156. package/skills/freeze/bin/check-freeze.sh +0 -79
  157. package/skills/gstack-upgrade/SKILL.md.tmpl +0 -222
  158. package/skills/guard/SKILL.md.tmpl +0 -77
  159. package/skills/investigate/SKILL.md +0 -105
  160. package/skills/investigate/SKILL.md.tmpl +0 -194
  161. package/skills/land-and-deploy/SKILL.md.tmpl +0 -881
  162. package/skills/office-hours/SKILL.md +0 -96
  163. package/skills/office-hours/SKILL.md.tmpl +0 -645
  164. package/skills/plan-ceo-review/SKILL.md +0 -94
  165. package/skills/plan-ceo-review/SKILL.md.tmpl +0 -811
  166. package/skills/plan-design-review/SKILL.md +0 -92
  167. package/skills/plan-design-review/SKILL.md.tmpl +0 -446
  168. package/skills/plan-eng-review/SKILL.md +0 -93
  169. package/skills/plan-eng-review/SKILL.md.tmpl +0 -303
  170. package/skills/qa/SKILL.md +0 -95
  171. package/skills/qa/SKILL.md.tmpl +0 -316
  172. package/skills/qa/references/issue-taxonomy.md +0 -85
  173. package/skills/qa/templates/qa-report-template.md +0 -126
  174. package/skills/qa-only/SKILL.md +0 -89
  175. package/skills/qa-only/SKILL.md.tmpl +0 -101
  176. package/skills/retro/SKILL.md +0 -89
  177. package/skills/retro/SKILL.md.tmpl +0 -820
  178. package/skills/review/SKILL.md +0 -92
  179. package/skills/review/SKILL.md.tmpl +0 -281
  180. package/skills/review/TODOS-format.md +0 -62
  181. package/skills/review/checklist.md +0 -220
  182. package/skills/review/design-checklist.md +0 -132
  183. package/skills/review/greptile-triage.md +0 -220
  184. package/skills/setup-browser-cookies/SKILL.md.tmpl +0 -81
  185. package/skills/setup-deploy/SKILL.md +0 -92
  186. package/skills/setup-deploy/SKILL.md.tmpl +0 -215
  187. package/skills/ship/SKILL.md.tmpl +0 -636
  188. package/skills/unfreeze/SKILL.md +0 -37
  189. package/skills/unfreeze/SKILL.md.tmpl +0 -36
@@ -1,199 +0,0 @@
1
- /**
2
- * Tests for sidebar agent queue parsing and inbox writing.
3
- *
4
- * sidebar-agent.ts functions are not exported (it's an entry-point script),
5
- * so we test the same logic inline: JSONL parsing, writeToInbox filesystem
6
- * behavior, and edge cases.
7
- */
8
-
9
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
10
- import * as fs from 'fs';
11
- import * as path from 'path';
12
- import * as os from 'os';
13
-
14
- // ─── Helpers: replicate sidebar-agent logic for unit testing ──────
15
-
16
- /** Parse a single JSONL line — same logic as sidebar-agent poll() */
17
- function parseQueueLine(line: string): any | null {
18
- if (!line.trim()) return null;
19
- try {
20
- const entry = JSON.parse(line);
21
- if (!entry.message && !entry.prompt) return null;
22
- return entry;
23
- } catch {
24
- return null;
25
- }
26
- }
27
-
28
- /** Read all valid entries from a JSONL string — same as countLines + readLine loop */
29
- function parseQueueFile(content: string): any[] {
30
- const entries: any[] = [];
31
- const lines = content.split('\n').filter(Boolean);
32
- for (const line of lines) {
33
- const entry = parseQueueLine(line);
34
- if (entry) entries.push(entry);
35
- }
36
- return entries;
37
- }
38
-
39
- /** Write to inbox — extracted logic from sidebar-agent.ts writeToInbox() */
40
- function writeToInbox(
41
- gitRoot: string,
42
- message: string,
43
- pageUrl?: string,
44
- sessionId?: string,
45
- ): string | null {
46
- if (!gitRoot) return null;
47
-
48
- const inboxDir = path.join(gitRoot, '.context', 'sidebar-inbox');
49
- fs.mkdirSync(inboxDir, { recursive: true });
50
-
51
- const now = new Date();
52
- const timestamp = now.toISOString().replace(/:/g, '-');
53
- const filename = `${timestamp}-observation.json`;
54
- const tmpFile = path.join(inboxDir, `.${filename}.tmp`);
55
- const finalFile = path.join(inboxDir, filename);
56
-
57
- const inboxMessage = {
58
- type: 'observation',
59
- timestamp: now.toISOString(),
60
- page: { url: pageUrl || 'unknown', title: '' },
61
- userMessage: message,
62
- sidebarSessionId: sessionId || 'unknown',
63
- };
64
-
65
- fs.writeFileSync(tmpFile, JSON.stringify(inboxMessage, null, 2));
66
- fs.renameSync(tmpFile, finalFile);
67
- return finalFile;
68
- }
69
-
70
- // ─── Test setup ──────────────────────────────────────────────────
71
-
72
- let tmpDir: string;
73
-
74
- beforeEach(() => {
75
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sidebar-agent-test-'));
76
- });
77
-
78
- afterEach(() => {
79
- fs.rmSync(tmpDir, { recursive: true, force: true });
80
- });
81
-
82
- // ─── Queue File Parsing ─────────────────────────────────────────
83
-
84
- describe('queue file parsing', () => {
85
- test('valid JSONL line parsed correctly', () => {
86
- const line = JSON.stringify({ message: 'hello', prompt: 'check this', pageUrl: 'https://example.com' });
87
- const entry = parseQueueLine(line);
88
- expect(entry).not.toBeNull();
89
- expect(entry.message).toBe('hello');
90
- expect(entry.prompt).toBe('check this');
91
- expect(entry.pageUrl).toBe('https://example.com');
92
- });
93
-
94
- test('malformed JSON line skipped without crash', () => {
95
- const entry = parseQueueLine('this is not json {{{');
96
- expect(entry).toBeNull();
97
- });
98
-
99
- test('valid JSON without message or prompt is skipped', () => {
100
- const line = JSON.stringify({ foo: 'bar' });
101
- const entry = parseQueueLine(line);
102
- expect(entry).toBeNull();
103
- });
104
-
105
- test('empty file returns no entries', () => {
106
- const entries = parseQueueFile('');
107
- expect(entries).toEqual([]);
108
- });
109
-
110
- test('file with blank lines returns no entries', () => {
111
- const entries = parseQueueFile('\n\n\n');
112
- expect(entries).toEqual([]);
113
- });
114
-
115
- test('mixed valid and invalid lines', () => {
116
- const content = [
117
- JSON.stringify({ message: 'first' }),
118
- 'not json',
119
- JSON.stringify({ unrelated: true }),
120
- JSON.stringify({ message: 'second', prompt: 'do stuff' }),
121
- ].join('\n');
122
-
123
- const entries = parseQueueFile(content);
124
- expect(entries.length).toBe(2);
125
- expect(entries[0].message).toBe('first');
126
- expect(entries[1].message).toBe('second');
127
- });
128
- });
129
-
130
- // ─── writeToInbox ────────────────────────────────────────────────
131
-
132
- describe('writeToInbox', () => {
133
- test('creates .context/sidebar-inbox/ directory', () => {
134
- writeToInbox(tmpDir, 'test message');
135
- const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
136
- expect(fs.existsSync(inboxDir)).toBe(true);
137
- expect(fs.statSync(inboxDir).isDirectory()).toBe(true);
138
- });
139
-
140
- test('writes valid JSON file', () => {
141
- const filePath = writeToInbox(tmpDir, 'test message', 'https://example.com', 'session-123');
142
- expect(filePath).not.toBeNull();
143
- expect(fs.existsSync(filePath!)).toBe(true);
144
-
145
- const data = JSON.parse(fs.readFileSync(filePath!, 'utf-8'));
146
- expect(data.type).toBe('observation');
147
- expect(data.userMessage).toBe('test message');
148
- expect(data.page.url).toBe('https://example.com');
149
- expect(data.sidebarSessionId).toBe('session-123');
150
- expect(data.timestamp).toBeTruthy();
151
- });
152
-
153
- test('atomic write — final file exists, no .tmp left', () => {
154
- const filePath = writeToInbox(tmpDir, 'atomic test');
155
- expect(filePath).not.toBeNull();
156
- expect(fs.existsSync(filePath!)).toBe(true);
157
-
158
- // Check no .tmp files remain in the inbox directory
159
- const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
160
- const files = fs.readdirSync(inboxDir);
161
- const tmpFiles = files.filter(f => f.endsWith('.tmp'));
162
- expect(tmpFiles.length).toBe(0);
163
-
164
- // Final file should end with -observation.json
165
- const jsonFiles = files.filter(f => f.endsWith('-observation.json') && !f.startsWith('.'));
166
- expect(jsonFiles.length).toBe(1);
167
- });
168
-
169
- test('handles missing git root gracefully', () => {
170
- const result = writeToInbox('', 'test');
171
- expect(result).toBeNull();
172
- });
173
-
174
- test('defaults pageUrl to unknown when not provided', () => {
175
- const filePath = writeToInbox(tmpDir, 'no url provided');
176
- expect(filePath).not.toBeNull();
177
- const data = JSON.parse(fs.readFileSync(filePath!, 'utf-8'));
178
- expect(data.page.url).toBe('unknown');
179
- });
180
-
181
- test('defaults sessionId to unknown when not provided', () => {
182
- const filePath = writeToInbox(tmpDir, 'no session');
183
- expect(filePath).not.toBeNull();
184
- const data = JSON.parse(fs.readFileSync(filePath!, 'utf-8'));
185
- expect(data.sidebarSessionId).toBe('unknown');
186
- });
187
-
188
- test('multiple writes create separate files', () => {
189
- writeToInbox(tmpDir, 'message 1');
190
- // Tiny delay to ensure different timestamps
191
- const t = Date.now();
192
- while (Date.now() === t) {} // spin until next ms
193
- writeToInbox(tmpDir, 'message 2');
194
-
195
- const inboxDir = path.join(tmpDir, '.context', 'sidebar-inbox');
196
- const files = fs.readdirSync(inboxDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
197
- expect(files.length).toBe(2);
198
- });
199
- });
@@ -1,320 +0,0 @@
1
- /**
2
- * Layer 2: Server HTTP integration tests for sidebar endpoints.
3
- * Starts the browse server as a subprocess (no browser via BROWSE_HEADLESS_SKIP),
4
- * exercises sidebar HTTP endpoints with fetch(). No Chrome, no Claude, no sidebar-agent.
5
- */
6
-
7
- import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
8
- import { spawn, type Subprocess } from 'bun';
9
- import * as fs from 'fs';
10
- import * as os from 'os';
11
- import * as path from 'path';
12
-
13
- let serverProc: Subprocess | null = null;
14
- let serverPort: number = 0;
15
- let authToken: string = '';
16
- let tmpDir: string = '';
17
- let stateFile: string = '';
18
- let queueFile: string = '';
19
-
20
- async function api(pathname: string, opts: RequestInit & { noAuth?: boolean } = {}): Promise<Response> {
21
- const { noAuth, ...fetchOpts } = opts;
22
- const headers: Record<string, string> = {
23
- 'Content-Type': 'application/json',
24
- ...(fetchOpts.headers as Record<string, string> || {}),
25
- };
26
- if (!noAuth && !headers['Authorization'] && authToken) {
27
- headers['Authorization'] = `Bearer ${authToken}`;
28
- }
29
- return fetch(`http://127.0.0.1:${serverPort}${pathname}`, { ...fetchOpts, headers });
30
- }
31
-
32
- beforeAll(async () => {
33
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sidebar-integ-'));
34
- stateFile = path.join(tmpDir, 'browse.json');
35
- queueFile = path.join(tmpDir, 'sidebar-queue.jsonl');
36
-
37
- // Ensure queue dir exists
38
- fs.mkdirSync(path.dirname(queueFile), { recursive: true });
39
-
40
- const serverScript = path.resolve(__dirname, '..', 'src', 'server.ts');
41
- serverProc = spawn(['bun', 'run', serverScript], {
42
- env: {
43
- ...process.env,
44
- BROWSE_STATE_FILE: stateFile,
45
- BROWSE_HEADLESS_SKIP: '1',
46
- BROWSE_PORT: '0',
47
- SIDEBAR_QUEUE_PATH: queueFile,
48
- BROWSE_IDLE_TIMEOUT: '300',
49
- },
50
- stdio: ['ignore', 'pipe', 'pipe'],
51
- });
52
-
53
- // Wait for state file
54
- const deadline = Date.now() + 15000;
55
- while (Date.now() < deadline) {
56
- if (fs.existsSync(stateFile)) {
57
- try {
58
- const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
59
- if (state.port && state.token) {
60
- serverPort = state.port;
61
- authToken = state.token;
62
- break;
63
- }
64
- } catch {}
65
- }
66
- await new Promise(r => setTimeout(r, 100));
67
- }
68
- if (!serverPort) throw new Error('Server did not start in time');
69
- }, 20000);
70
-
71
- afterAll(() => {
72
- if (serverProc) { try { serverProc.kill(); } catch {} }
73
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
74
- });
75
-
76
- // Reset state between tests — creates a fresh session, clears all queues
77
- async function resetState() {
78
- await api('/sidebar-session/new', { method: 'POST' });
79
- fs.writeFileSync(queueFile, '');
80
- }
81
-
82
- describe('sidebar auth', () => {
83
- test('rejects request without auth token', async () => {
84
- const resp = await api('/sidebar-command', {
85
- method: 'POST',
86
- noAuth: true,
87
- body: JSON.stringify({ message: 'test' }),
88
- });
89
- expect(resp.status).toBe(401);
90
- });
91
-
92
- test('rejects request with wrong token', async () => {
93
- const resp = await api('/sidebar-command', {
94
- method: 'POST',
95
- headers: { 'Authorization': 'Bearer wrong-token' },
96
- body: JSON.stringify({ message: 'test' }),
97
- });
98
- expect(resp.status).toBe(401);
99
- });
100
-
101
- test('accepts request with correct token', async () => {
102
- const resp = await api('/sidebar-command', {
103
- method: 'POST',
104
- body: JSON.stringify({ message: 'hello' }),
105
- });
106
- expect(resp.status).toBe(200);
107
- // Clean up
108
- await api('/sidebar-agent/kill', { method: 'POST' });
109
- });
110
- });
111
-
112
- describe('sidebar-command → queue', () => {
113
- test('writes queue entry with activeTabUrl', async () => {
114
- await resetState();
115
-
116
- const resp = await api('/sidebar-command', {
117
- method: 'POST',
118
- body: JSON.stringify({
119
- message: 'what is on this page?',
120
- activeTabUrl: 'https://example.com/test-page',
121
- }),
122
- });
123
- expect(resp.status).toBe(200);
124
- const data = await resp.json();
125
- expect(data.ok).toBe(true);
126
-
127
- // Give server a moment to write queue
128
- await new Promise(r => setTimeout(r, 100));
129
-
130
- const content = fs.readFileSync(queueFile, 'utf-8').trim();
131
- const lines = content.split('\n').filter(Boolean);
132
- expect(lines.length).toBeGreaterThan(0);
133
- const entry = JSON.parse(lines[lines.length - 1]);
134
- expect(entry.pageUrl).toBe('https://example.com/test-page');
135
- expect(entry.prompt).toContain('https://example.com/test-page');
136
-
137
- await api('/sidebar-agent/kill', { method: 'POST' });
138
- });
139
-
140
- test('falls back when activeTabUrl is null', async () => {
141
- await resetState();
142
-
143
- await api('/sidebar-command', {
144
- method: 'POST',
145
- body: JSON.stringify({ message: 'test', activeTabUrl: null }),
146
- });
147
- await new Promise(r => setTimeout(r, 100));
148
-
149
- const lines = fs.readFileSync(queueFile, 'utf-8').trim().split('\n').filter(Boolean);
150
- expect(lines.length).toBeGreaterThan(0);
151
- const entry = JSON.parse(lines[lines.length - 1]);
152
- // No browser → playwright URL is 'about:blank'
153
- expect(entry.pageUrl).toBe('about:blank');
154
-
155
- await api('/sidebar-agent/kill', { method: 'POST' });
156
- });
157
-
158
- test('rejects chrome:// activeTabUrl and falls back', async () => {
159
- await resetState();
160
-
161
- await api('/sidebar-command', {
162
- method: 'POST',
163
- body: JSON.stringify({ message: 'test', activeTabUrl: 'chrome://extensions' }),
164
- });
165
- await new Promise(r => setTimeout(r, 100));
166
-
167
- const lines = fs.readFileSync(queueFile, 'utf-8').trim().split('\n').filter(Boolean);
168
- expect(lines.length).toBeGreaterThan(0);
169
- const entry = JSON.parse(lines[lines.length - 1]);
170
- expect(entry.pageUrl).toBe('about:blank');
171
-
172
- await api('/sidebar-agent/kill', { method: 'POST' });
173
- });
174
-
175
- test('rejects empty message', async () => {
176
- const resp = await api('/sidebar-command', {
177
- method: 'POST',
178
- body: JSON.stringify({ message: '' }),
179
- });
180
- expect(resp.status).toBe(400);
181
- });
182
- });
183
-
184
- describe('sidebar-agent/event → chat buffer', () => {
185
- test('agent events appear in /sidebar-chat', async () => {
186
- await resetState();
187
-
188
- // Post mock agent events using Claude's streaming format
189
- await api('/sidebar-agent/event', {
190
- method: 'POST',
191
- body: JSON.stringify({
192
- type: 'assistant',
193
- message: { content: [{ type: 'text', text: 'Hello from mock agent' }] },
194
- }),
195
- });
196
-
197
- const chatData = await (await api('/sidebar-chat?after=0')).json();
198
- const textEntry = chatData.entries.find((e: any) => e.type === 'text');
199
- expect(textEntry).toBeDefined();
200
- expect(textEntry.text).toBe('Hello from mock agent');
201
- });
202
-
203
- test('agent_done transitions status to idle', async () => {
204
- await resetState();
205
- // Start a command so agent is processing
206
- await api('/sidebar-command', {
207
- method: 'POST',
208
- body: JSON.stringify({ message: 'test' }),
209
- });
210
-
211
- // Verify processing
212
- let session = await (await api('/sidebar-session')).json();
213
- expect(session.agent.status).toBe('processing');
214
-
215
- // Send agent_done
216
- await api('/sidebar-agent/event', {
217
- method: 'POST',
218
- body: JSON.stringify({ type: 'agent_done' }),
219
- });
220
-
221
- session = await (await api('/sidebar-session')).json();
222
- expect(session.agent.status).toBe('idle');
223
- });
224
- });
225
-
226
- describe('message queuing', () => {
227
- test('queues message when agent is processing', async () => {
228
- await resetState();
229
-
230
- // First message starts processing
231
- await api('/sidebar-command', {
232
- method: 'POST',
233
- body: JSON.stringify({ message: 'first' }),
234
- });
235
-
236
- // Second message gets queued
237
- const resp = await api('/sidebar-command', {
238
- method: 'POST',
239
- body: JSON.stringify({ message: 'second' }),
240
- });
241
- const data = await resp.json();
242
- expect(data.ok).toBe(true);
243
- expect(data.queued).toBe(true);
244
- expect(data.position).toBe(1);
245
-
246
- await api('/sidebar-agent/kill', { method: 'POST' });
247
- });
248
-
249
- test('returns 429 when queue is full', async () => {
250
- await resetState();
251
-
252
- // First message starts processing
253
- await api('/sidebar-command', {
254
- method: 'POST',
255
- body: JSON.stringify({ message: 'first' }),
256
- });
257
-
258
- // Fill queue (max 5)
259
- for (let i = 0; i < 5; i++) {
260
- await api('/sidebar-command', {
261
- method: 'POST',
262
- body: JSON.stringify({ message: `fill-${i}` }),
263
- });
264
- }
265
-
266
- // 7th message should be rejected
267
- const resp = await api('/sidebar-command', {
268
- method: 'POST',
269
- body: JSON.stringify({ message: 'overflow' }),
270
- });
271
- expect(resp.status).toBe(429);
272
-
273
- await api('/sidebar-agent/kill', { method: 'POST' });
274
- });
275
- });
276
-
277
- describe('chat clear', () => {
278
- test('clears chat buffer', async () => {
279
- await resetState();
280
- // Add some entries
281
- await api('/sidebar-agent/event', {
282
- method: 'POST',
283
- body: JSON.stringify({ type: 'text', text: 'to be cleared' }),
284
- });
285
-
286
- await api('/sidebar-chat/clear', { method: 'POST' });
287
-
288
- const data = await (await api('/sidebar-chat?after=0')).json();
289
- expect(data.entries.length).toBe(0);
290
- expect(data.total).toBe(0);
291
- });
292
- });
293
-
294
- describe('agent kill', () => {
295
- test('kill adds error entry and returns to idle', async () => {
296
- await resetState();
297
-
298
- // Start a command so agent is processing
299
- await api('/sidebar-command', {
300
- method: 'POST',
301
- body: JSON.stringify({ message: 'kill me' }),
302
- });
303
-
304
- let session = await (await api('/sidebar-session')).json();
305
- expect(session.agent.status).toBe('processing');
306
-
307
- // Kill the agent
308
- const killResp = await api('/sidebar-agent/kill', { method: 'POST' });
309
- expect(killResp.status).toBe(200);
310
-
311
- // Check chat for error entry
312
- const chatData = await (await api('/sidebar-chat?after=0')).json();
313
- const errorEntry = chatData.entries.find((e: any) => e.error === 'Killed by user');
314
- expect(errorEntry).toBeDefined();
315
-
316
- // Agent should be idle (no queue items to auto-process)
317
- session = await (await api('/sidebar-session')).json();
318
- expect(session.agent.status).toBe('idle');
319
- });
320
- });
@@ -1,96 +0,0 @@
1
- /**
2
- * Layer 1: Unit tests for sidebar utilities.
3
- * Tests pure functions — no server, no processes, no network.
4
- */
5
-
6
- import { describe, test, expect } from 'bun:test';
7
- import { sanitizeExtensionUrl } from '../src/sidebar-utils';
8
-
9
- describe('sanitizeExtensionUrl', () => {
10
- test('passes valid http URL', () => {
11
- expect(sanitizeExtensionUrl('http://example.com')).toBe('http://example.com/');
12
- });
13
-
14
- test('passes valid https URL', () => {
15
- expect(sanitizeExtensionUrl('https://example.com/page?q=1')).toBe('https://example.com/page?q=1');
16
- });
17
-
18
- test('rejects chrome:// URLs', () => {
19
- expect(sanitizeExtensionUrl('chrome://extensions')).toBeNull();
20
- });
21
-
22
- test('rejects chrome-extension:// URLs', () => {
23
- expect(sanitizeExtensionUrl('chrome-extension://abcdef/popup.html')).toBeNull();
24
- });
25
-
26
- test('rejects javascript: URLs', () => {
27
- expect(sanitizeExtensionUrl('javascript:alert(1)')).toBeNull();
28
- });
29
-
30
- test('rejects file:// URLs', () => {
31
- expect(sanitizeExtensionUrl('file:///etc/passwd')).toBeNull();
32
- });
33
-
34
- test('rejects data: URLs', () => {
35
- expect(sanitizeExtensionUrl('data:text/html,<h1>hi</h1>')).toBeNull();
36
- });
37
-
38
- test('strips raw control characters from URL', () => {
39
- // URL constructor percent-encodes \x00 as %00, which is safe
40
- // The regex strips any remaining raw control chars after .href normalization
41
- const result = sanitizeExtensionUrl('https://example.com/\x00page\x1f');
42
- expect(result).not.toBeNull();
43
- expect(result!).not.toMatch(/[\x00-\x1f\x7f]/);
44
- });
45
-
46
- test('strips newlines (prompt injection vector)', () => {
47
- const result = sanitizeExtensionUrl('https://evil.com/%0AUser:%20ignore');
48
- // URL constructor normalizes %0A, control char stripping removes any raw newlines
49
- expect(result).not.toBeNull();
50
- expect(result!).not.toContain('\n');
51
- });
52
-
53
- test('truncates URLs longer than 2048 chars', () => {
54
- const longUrl = 'https://example.com/' + 'a'.repeat(3000);
55
- const result = sanitizeExtensionUrl(longUrl);
56
- expect(result).not.toBeNull();
57
- expect(result!.length).toBeLessThanOrEqual(2048);
58
- });
59
-
60
- test('returns null for null input', () => {
61
- expect(sanitizeExtensionUrl(null)).toBeNull();
62
- });
63
-
64
- test('returns null for undefined input', () => {
65
- expect(sanitizeExtensionUrl(undefined)).toBeNull();
66
- });
67
-
68
- test('returns null for empty string', () => {
69
- expect(sanitizeExtensionUrl('')).toBeNull();
70
- });
71
-
72
- test('returns null for invalid URL string', () => {
73
- expect(sanitizeExtensionUrl('not a url at all')).toBeNull();
74
- });
75
-
76
- test('does not crash on weird input', () => {
77
- expect(sanitizeExtensionUrl(':///')).toBeNull();
78
- expect(sanitizeExtensionUrl(' ')).toBeNull();
79
- expect(sanitizeExtensionUrl('\x00\x01\x02')).toBeNull();
80
- });
81
-
82
- test('preserves query parameters and fragments', () => {
83
- const url = 'https://example.com/search?q=test&page=2#results';
84
- expect(sanitizeExtensionUrl(url)).toBe(url);
85
- });
86
-
87
- test('preserves port numbers', () => {
88
- expect(sanitizeExtensionUrl('http://localhost:3000/api')).toBe('http://localhost:3000/api');
89
- });
90
-
91
- test('handles URL with auth (user:pass@host)', () => {
92
- const result = sanitizeExtensionUrl('https://user:pass@example.com/');
93
- expect(result).not.toBeNull();
94
- expect(result).toContain('example.com');
95
- });
96
- });