opengstack 0.13.9 → 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 (152) 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/scripts/install-skills.js +4 -7
  33. package/skills/autoplan/SKILL.md +0 -96
  34. package/skills/autoplan/SKILL.md.tmpl +0 -694
  35. package/skills/benchmark/SKILL.md.tmpl +0 -222
  36. package/skills/browse/SKILL.md.tmpl +0 -131
  37. package/skills/browse/bin/find-browse +0 -21
  38. package/skills/browse/bin/remote-slug +0 -14
  39. package/skills/browse/scripts/build-node-server.sh +0 -48
  40. package/skills/browse/src/activity.ts +0 -208
  41. package/skills/browse/src/browser-manager.ts +0 -959
  42. package/skills/browse/src/buffers.ts +0 -137
  43. package/skills/browse/src/bun-polyfill.cjs +0 -109
  44. package/skills/browse/src/cli.ts +0 -678
  45. package/skills/browse/src/commands.ts +0 -128
  46. package/skills/browse/src/config.ts +0 -150
  47. package/skills/browse/src/cookie-import-browser.ts +0 -625
  48. package/skills/browse/src/cookie-picker-routes.ts +0 -230
  49. package/skills/browse/src/cookie-picker-ui.ts +0 -688
  50. package/skills/browse/src/find-browse.ts +0 -61
  51. package/skills/browse/src/meta-commands.ts +0 -550
  52. package/skills/browse/src/platform.ts +0 -17
  53. package/skills/browse/src/read-commands.ts +0 -358
  54. package/skills/browse/src/server.ts +0 -1192
  55. package/skills/browse/src/sidebar-agent.ts +0 -280
  56. package/skills/browse/src/sidebar-utils.ts +0 -21
  57. package/skills/browse/src/snapshot.ts +0 -407
  58. package/skills/browse/src/url-validation.ts +0 -95
  59. package/skills/browse/src/write-commands.ts +0 -364
  60. package/skills/browse/test/activity.test.ts +0 -120
  61. package/skills/browse/test/adversarial-security.test.ts +0 -32
  62. package/skills/browse/test/browser-manager-unit.test.ts +0 -17
  63. package/skills/browse/test/bun-polyfill.test.ts +0 -72
  64. package/skills/browse/test/commands.test.ts +0 -2075
  65. package/skills/browse/test/compare-board.test.ts +0 -342
  66. package/skills/browse/test/config.test.ts +0 -316
  67. package/skills/browse/test/cookie-import-browser.test.ts +0 -519
  68. package/skills/browse/test/cookie-picker-routes.test.ts +0 -260
  69. package/skills/browse/test/file-drop.test.ts +0 -271
  70. package/skills/browse/test/find-browse.test.ts +0 -50
  71. package/skills/browse/test/findport.test.ts +0 -191
  72. package/skills/browse/test/fixtures/basic.html +0 -33
  73. package/skills/browse/test/fixtures/cursor-interactive.html +0 -22
  74. package/skills/browse/test/fixtures/dialog.html +0 -15
  75. package/skills/browse/test/fixtures/empty.html +0 -2
  76. package/skills/browse/test/fixtures/forms.html +0 -55
  77. package/skills/browse/test/fixtures/iframe.html +0 -30
  78. package/skills/browse/test/fixtures/network-idle.html +0 -30
  79. package/skills/browse/test/fixtures/qa-eval-checkout.html +0 -108
  80. package/skills/browse/test/fixtures/qa-eval-spa.html +0 -98
  81. package/skills/browse/test/fixtures/qa-eval.html +0 -51
  82. package/skills/browse/test/fixtures/responsive.html +0 -49
  83. package/skills/browse/test/fixtures/snapshot.html +0 -55
  84. package/skills/browse/test/fixtures/spa.html +0 -24
  85. package/skills/browse/test/fixtures/states.html +0 -17
  86. package/skills/browse/test/fixtures/upload.html +0 -25
  87. package/skills/browse/test/gstack-config.test.ts +0 -138
  88. package/skills/browse/test/gstack-update-check.test.ts +0 -514
  89. package/skills/browse/test/handoff.test.ts +0 -235
  90. package/skills/browse/test/path-validation.test.ts +0 -91
  91. package/skills/browse/test/platform.test.ts +0 -37
  92. package/skills/browse/test/server-auth.test.ts +0 -65
  93. package/skills/browse/test/sidebar-agent-roundtrip.test.ts +0 -226
  94. package/skills/browse/test/sidebar-agent.test.ts +0 -199
  95. package/skills/browse/test/sidebar-integration.test.ts +0 -320
  96. package/skills/browse/test/sidebar-unit.test.ts +0 -96
  97. package/skills/browse/test/snapshot.test.ts +0 -467
  98. package/skills/browse/test/state-ttl.test.ts +0 -35
  99. package/skills/browse/test/test-server.ts +0 -57
  100. package/skills/browse/test/url-validation.test.ts +0 -72
  101. package/skills/browse/test/watch.test.ts +0 -129
  102. package/skills/canary/SKILL.md.tmpl +0 -212
  103. package/skills/careful/SKILL.md.tmpl +0 -56
  104. package/skills/careful/bin/check-careful.sh +0 -112
  105. package/skills/codex/SKILL.md +0 -90
  106. package/skills/codex/SKILL.md.tmpl +0 -417
  107. package/skills/connect-chrome/SKILL.md.tmpl +0 -195
  108. package/skills/cso/ACKNOWLEDGEMENTS.md +0 -14
  109. package/skills/cso/SKILL.md +0 -93
  110. package/skills/cso/SKILL.md.tmpl +0 -606
  111. package/skills/design-consultation/SKILL.md +0 -94
  112. package/skills/design-consultation/SKILL.md.tmpl +0 -415
  113. package/skills/design-review/SKILL.md +0 -94
  114. package/skills/design-review/SKILL.md.tmpl +0 -290
  115. package/skills/design-shotgun/SKILL.md +0 -91
  116. package/skills/design-shotgun/SKILL.md.tmpl +0 -285
  117. package/skills/document-release/SKILL.md +0 -91
  118. package/skills/document-release/SKILL.md.tmpl +0 -359
  119. package/skills/freeze/SKILL.md.tmpl +0 -77
  120. package/skills/freeze/bin/check-freeze.sh +0 -79
  121. package/skills/gstack-upgrade/SKILL.md.tmpl +0 -222
  122. package/skills/guard/SKILL.md.tmpl +0 -77
  123. package/skills/investigate/SKILL.md +0 -105
  124. package/skills/investigate/SKILL.md.tmpl +0 -194
  125. package/skills/land-and-deploy/SKILL.md.tmpl +0 -881
  126. package/skills/office-hours/SKILL.md +0 -96
  127. package/skills/office-hours/SKILL.md.tmpl +0 -645
  128. package/skills/plan-ceo-review/SKILL.md +0 -94
  129. package/skills/plan-ceo-review/SKILL.md.tmpl +0 -811
  130. package/skills/plan-design-review/SKILL.md +0 -92
  131. package/skills/plan-design-review/SKILL.md.tmpl +0 -446
  132. package/skills/plan-eng-review/SKILL.md +0 -93
  133. package/skills/plan-eng-review/SKILL.md.tmpl +0 -303
  134. package/skills/qa/SKILL.md +0 -95
  135. package/skills/qa/SKILL.md.tmpl +0 -316
  136. package/skills/qa/references/issue-taxonomy.md +0 -85
  137. package/skills/qa/templates/qa-report-template.md +0 -126
  138. package/skills/qa-only/SKILL.md +0 -89
  139. package/skills/qa-only/SKILL.md.tmpl +0 -101
  140. package/skills/retro/SKILL.md +0 -89
  141. package/skills/retro/SKILL.md.tmpl +0 -820
  142. package/skills/review/SKILL.md +0 -92
  143. package/skills/review/SKILL.md.tmpl +0 -281
  144. package/skills/review/TODOS-format.md +0 -62
  145. package/skills/review/checklist.md +0 -220
  146. package/skills/review/design-checklist.md +0 -132
  147. package/skills/review/greptile-triage.md +0 -220
  148. package/skills/setup-browser-cookies/SKILL.md.tmpl +0 -81
  149. package/skills/setup-deploy/SKILL.md +0 -92
  150. package/skills/setup-deploy/SKILL.md.tmpl +0 -215
  151. package/skills/ship/SKILL.md.tmpl +0 -636
  152. 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
- });