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.
- package/AGENTS.md +4 -4
- package/CLAUDE.md +127 -110
- package/README.md +10 -5
- package/SKILL.md +500 -70
- package/bin/opengstack.js +69 -69
- package/{skills/land-and-deploy/SKILL.md → commands/autoplan.md} +7 -25
- package/{skills/benchmark/SKILL.md → commands/benchmark.md} +84 -108
- package/{skills/browse/SKILL.md → commands/browse.md} +60 -81
- package/{skills/ship/SKILL.md → commands/canary.md} +7 -27
- package/{skills/careful/SKILL.md → commands/careful.md} +2 -22
- package/{skills/canary/SKILL.md → commands/codex.md} +7 -26
- package/{skills/connect-chrome/SKILL.md → commands/connect-chrome.md} +7 -24
- package/commands/cso.md +70 -0
- package/commands/design-consultation.md +70 -0
- package/commands/design-review.md +70 -0
- package/commands/design-shotgun.md +70 -0
- package/commands/document-release.md +70 -0
- package/{skills/freeze/SKILL.md → commands/freeze.md} +3 -29
- package/{skills/guard/SKILL.md → commands/guard.md} +4 -35
- package/commands/investigate.md +70 -0
- package/commands/land-and-deploy.md +70 -0
- package/commands/office-hours.md +70 -0
- package/{skills/gstack-upgrade/SKILL.md → commands/opengstack-upgrade.md} +64 -79
- package/commands/plan-ceo-review.md +70 -0
- package/commands/plan-design-review.md +70 -0
- package/commands/plan-eng-review.md +70 -0
- package/commands/qa-only.md +70 -0
- package/commands/qa.md +70 -0
- package/commands/retro.md +70 -0
- package/commands/review.md +70 -0
- package/{skills/setup-browser-cookies/SKILL.md → commands/setup-browser-cookies.md} +22 -40
- package/commands/setup-deploy.md +70 -0
- package/commands/ship.md +70 -0
- package/commands/unfreeze.md +25 -0
- package/docs/designs/CHROME_VS_CHROMIUM_EXPLORATION.md +9 -9
- package/docs/designs/CONDUCTOR_CHROME_SIDEBAR_INTEGRATION.md +2 -2
- package/docs/designs/CONDUCTOR_SESSION_API.md +16 -16
- package/docs/designs/DESIGN_SHOTGUN.md +74 -74
- package/docs/designs/DESIGN_TOOLS_V1.md +111 -111
- package/docs/skills.md +483 -202
- package/package.json +42 -43
- package/scripts/analytics.ts +188 -0
- package/scripts/dev-skill.ts +83 -0
- package/scripts/discover-skills.ts +39 -0
- package/scripts/eval-compare.ts +97 -0
- package/scripts/eval-list.ts +117 -0
- package/scripts/eval-select.ts +86 -0
- package/scripts/eval-summary.ts +188 -0
- package/scripts/eval-watch.ts +172 -0
- package/scripts/gen-skill-docs.ts +473 -0
- package/scripts/resolvers/browse.ts +129 -0
- package/scripts/resolvers/codex-helpers.ts +133 -0
- package/scripts/resolvers/composition.ts +48 -0
- package/scripts/resolvers/confidence.ts +37 -0
- package/scripts/resolvers/constants.ts +50 -0
- package/scripts/resolvers/design.ts +950 -0
- package/scripts/resolvers/index.ts +59 -0
- package/scripts/resolvers/learnings.ts +96 -0
- package/scripts/resolvers/preamble.ts +505 -0
- package/scripts/resolvers/review.ts +884 -0
- package/scripts/resolvers/testing.ts +573 -0
- package/scripts/resolvers/types.ts +45 -0
- package/scripts/resolvers/utility.ts +421 -0
- package/scripts/skill-check.ts +190 -0
- package/scripts/cleanup.py +0 -100
- package/scripts/filter-skills.sh +0 -114
- package/scripts/filter_skills.py +0 -164
- package/scripts/install-skills.js +0 -60
- package/skills/autoplan/SKILL.md +0 -96
- package/skills/autoplan/SKILL.md.tmpl +0 -694
- package/skills/benchmark/SKILL.md.tmpl +0 -222
- package/skills/browse/SKILL.md.tmpl +0 -131
- package/skills/browse/bin/find-browse +0 -21
- package/skills/browse/bin/remote-slug +0 -14
- package/skills/browse/scripts/build-node-server.sh +0 -48
- package/skills/browse/src/activity.ts +0 -208
- package/skills/browse/src/browser-manager.ts +0 -959
- package/skills/browse/src/buffers.ts +0 -137
- package/skills/browse/src/bun-polyfill.cjs +0 -109
- package/skills/browse/src/cli.ts +0 -678
- package/skills/browse/src/commands.ts +0 -128
- package/skills/browse/src/config.ts +0 -150
- package/skills/browse/src/cookie-import-browser.ts +0 -625
- package/skills/browse/src/cookie-picker-routes.ts +0 -230
- package/skills/browse/src/cookie-picker-ui.ts +0 -688
- package/skills/browse/src/find-browse.ts +0 -61
- package/skills/browse/src/meta-commands.ts +0 -550
- package/skills/browse/src/platform.ts +0 -17
- package/skills/browse/src/read-commands.ts +0 -358
- package/skills/browse/src/server.ts +0 -1192
- package/skills/browse/src/sidebar-agent.ts +0 -280
- package/skills/browse/src/sidebar-utils.ts +0 -21
- package/skills/browse/src/snapshot.ts +0 -407
- package/skills/browse/src/url-validation.ts +0 -95
- package/skills/browse/src/write-commands.ts +0 -364
- package/skills/browse/test/activity.test.ts +0 -120
- package/skills/browse/test/adversarial-security.test.ts +0 -32
- package/skills/browse/test/browser-manager-unit.test.ts +0 -17
- package/skills/browse/test/bun-polyfill.test.ts +0 -72
- package/skills/browse/test/commands.test.ts +0 -2075
- package/skills/browse/test/compare-board.test.ts +0 -342
- package/skills/browse/test/config.test.ts +0 -316
- package/skills/browse/test/cookie-import-browser.test.ts +0 -519
- package/skills/browse/test/cookie-picker-routes.test.ts +0 -260
- package/skills/browse/test/file-drop.test.ts +0 -271
- package/skills/browse/test/find-browse.test.ts +0 -50
- package/skills/browse/test/findport.test.ts +0 -191
- package/skills/browse/test/fixtures/basic.html +0 -33
- package/skills/browse/test/fixtures/cursor-interactive.html +0 -22
- package/skills/browse/test/fixtures/dialog.html +0 -15
- package/skills/browse/test/fixtures/empty.html +0 -2
- package/skills/browse/test/fixtures/forms.html +0 -55
- package/skills/browse/test/fixtures/iframe.html +0 -30
- package/skills/browse/test/fixtures/network-idle.html +0 -30
- package/skills/browse/test/fixtures/qa-eval-checkout.html +0 -108
- package/skills/browse/test/fixtures/qa-eval-spa.html +0 -98
- package/skills/browse/test/fixtures/qa-eval.html +0 -51
- package/skills/browse/test/fixtures/responsive.html +0 -49
- package/skills/browse/test/fixtures/snapshot.html +0 -55
- package/skills/browse/test/fixtures/spa.html +0 -24
- package/skills/browse/test/fixtures/states.html +0 -17
- package/skills/browse/test/fixtures/upload.html +0 -25
- package/skills/browse/test/gstack-config.test.ts +0 -138
- package/skills/browse/test/gstack-update-check.test.ts +0 -514
- package/skills/browse/test/handoff.test.ts +0 -235
- package/skills/browse/test/path-validation.test.ts +0 -91
- package/skills/browse/test/platform.test.ts +0 -37
- package/skills/browse/test/server-auth.test.ts +0 -65
- package/skills/browse/test/sidebar-agent-roundtrip.test.ts +0 -226
- package/skills/browse/test/sidebar-agent.test.ts +0 -199
- package/skills/browse/test/sidebar-integration.test.ts +0 -320
- package/skills/browse/test/sidebar-unit.test.ts +0 -96
- package/skills/browse/test/snapshot.test.ts +0 -467
- package/skills/browse/test/state-ttl.test.ts +0 -35
- package/skills/browse/test/test-server.ts +0 -57
- package/skills/browse/test/url-validation.test.ts +0 -72
- package/skills/browse/test/watch.test.ts +0 -129
- package/skills/canary/SKILL.md.tmpl +0 -212
- package/skills/careful/SKILL.md.tmpl +0 -56
- package/skills/careful/bin/check-careful.sh +0 -112
- package/skills/codex/SKILL.md +0 -90
- package/skills/codex/SKILL.md.tmpl +0 -417
- package/skills/connect-chrome/SKILL.md.tmpl +0 -195
- package/skills/cso/ACKNOWLEDGEMENTS.md +0 -14
- package/skills/cso/SKILL.md +0 -93
- package/skills/cso/SKILL.md.tmpl +0 -606
- package/skills/design-consultation/SKILL.md +0 -94
- package/skills/design-consultation/SKILL.md.tmpl +0 -415
- package/skills/design-review/SKILL.md +0 -94
- package/skills/design-review/SKILL.md.tmpl +0 -290
- package/skills/design-shotgun/SKILL.md +0 -91
- package/skills/design-shotgun/SKILL.md.tmpl +0 -285
- package/skills/document-release/SKILL.md +0 -91
- package/skills/document-release/SKILL.md.tmpl +0 -359
- package/skills/freeze/SKILL.md.tmpl +0 -77
- package/skills/freeze/bin/check-freeze.sh +0 -79
- package/skills/gstack-upgrade/SKILL.md.tmpl +0 -222
- package/skills/guard/SKILL.md.tmpl +0 -77
- package/skills/investigate/SKILL.md +0 -105
- package/skills/investigate/SKILL.md.tmpl +0 -194
- package/skills/land-and-deploy/SKILL.md.tmpl +0 -881
- package/skills/office-hours/SKILL.md +0 -96
- package/skills/office-hours/SKILL.md.tmpl +0 -645
- package/skills/plan-ceo-review/SKILL.md +0 -94
- package/skills/plan-ceo-review/SKILL.md.tmpl +0 -811
- package/skills/plan-design-review/SKILL.md +0 -92
- package/skills/plan-design-review/SKILL.md.tmpl +0 -446
- package/skills/plan-eng-review/SKILL.md +0 -93
- package/skills/plan-eng-review/SKILL.md.tmpl +0 -303
- package/skills/qa/SKILL.md +0 -95
- package/skills/qa/SKILL.md.tmpl +0 -316
- package/skills/qa/references/issue-taxonomy.md +0 -85
- package/skills/qa/templates/qa-report-template.md +0 -126
- package/skills/qa-only/SKILL.md +0 -89
- package/skills/qa-only/SKILL.md.tmpl +0 -101
- package/skills/retro/SKILL.md +0 -89
- package/skills/retro/SKILL.md.tmpl +0 -820
- package/skills/review/SKILL.md +0 -92
- package/skills/review/SKILL.md.tmpl +0 -281
- package/skills/review/TODOS-format.md +0 -62
- package/skills/review/checklist.md +0 -220
- package/skills/review/design-checklist.md +0 -132
- package/skills/review/greptile-triage.md +0 -220
- package/skills/setup-browser-cookies/SKILL.md.tmpl +0 -81
- package/skills/setup-deploy/SKILL.md +0 -92
- package/skills/setup-deploy/SKILL.md.tmpl +0 -215
- package/skills/ship/SKILL.md.tmpl +0 -636
- package/skills/unfreeze/SKILL.md +0 -37
- package/skills/unfreeze/SKILL.md.tmpl +0 -36
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration test for the design comparison board feedback loop.
|
|
3
|
-
*
|
|
4
|
-
* Tests the DOM polling pattern that plan-design-review, office-hours,
|
|
5
|
-
* and design-consultation use to read user feedback from the comparison board.
|
|
6
|
-
*
|
|
7
|
-
* Flow: generate board HTML → open in browser → verify DOM elements →
|
|
8
|
-
* simulate user interaction → verify structured JSON feedback.
|
|
9
|
-
*
|
|
10
|
-
* No LLM involved — this is a deterministic functional test.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
14
|
-
import { BrowserManager } from '../src/browser-manager';
|
|
15
|
-
import { handleReadCommand } from '../src/read-commands';
|
|
16
|
-
import { handleWriteCommand } from '../src/write-commands';
|
|
17
|
-
import { generateCompareHtml } from '../../design/src/compare';
|
|
18
|
-
import * as fs from 'fs';
|
|
19
|
-
import * as path from 'path';
|
|
20
|
-
|
|
21
|
-
let bm: BrowserManager;
|
|
22
|
-
let boardUrl: string;
|
|
23
|
-
let server: ReturnType<typeof Bun.serve>;
|
|
24
|
-
let tmpDir: string;
|
|
25
|
-
|
|
26
|
-
// Create a minimal 1x1 pixel PNG for test variants
|
|
27
|
-
function createTestPng(filePath: string): void {
|
|
28
|
-
// Minimal valid PNG: 1x1 red pixel
|
|
29
|
-
const png = Buffer.from(
|
|
30
|
-
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/58BAwAI/AL+hc2rNAAAAABJRU5ErkJggg==',
|
|
31
|
-
'base64'
|
|
32
|
-
);
|
|
33
|
-
fs.writeFileSync(filePath, png);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
beforeAll(async () => {
|
|
37
|
-
// Create test PNG files
|
|
38
|
-
tmpDir = '/tmp/compare-board-test-' + Date.now();
|
|
39
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
40
|
-
|
|
41
|
-
createTestPng(path.join(tmpDir, 'variant-A.png'));
|
|
42
|
-
createTestPng(path.join(tmpDir, 'variant-B.png'));
|
|
43
|
-
createTestPng(path.join(tmpDir, 'variant-C.png'));
|
|
44
|
-
|
|
45
|
-
// Generate comparison board HTML using the real compare module
|
|
46
|
-
const html = generateCompareHtml([
|
|
47
|
-
path.join(tmpDir, 'variant-A.png'),
|
|
48
|
-
path.join(tmpDir, 'variant-B.png'),
|
|
49
|
-
path.join(tmpDir, 'variant-C.png'),
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
// Serve the board via HTTP (browse blocks file:// URLs for security)
|
|
53
|
-
server = Bun.serve({
|
|
54
|
-
port: 0,
|
|
55
|
-
fetch() {
|
|
56
|
-
return new Response(html, { headers: { 'Content-Type': 'text/html' } });
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
boardUrl = `http://localhost:${server.port}`;
|
|
60
|
-
|
|
61
|
-
// Launch browser and navigate to the board
|
|
62
|
-
bm = new BrowserManager();
|
|
63
|
-
await bm.launch();
|
|
64
|
-
await handleWriteCommand('goto', [boardUrl], bm);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
afterAll(() => {
|
|
68
|
-
try { server.stop(); } catch {}
|
|
69
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
70
|
-
setTimeout(() => process.exit(0), 500);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// ─── DOM Structure ──────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
describe('Comparison board DOM structure', () => {
|
|
76
|
-
test('has hidden status element', async () => {
|
|
77
|
-
const status = await handleReadCommand('js', [
|
|
78
|
-
'document.getElementById("status").textContent'
|
|
79
|
-
], bm);
|
|
80
|
-
expect(status).toBe('');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('has hidden feedback-result element', async () => {
|
|
84
|
-
const result = await handleReadCommand('js', [
|
|
85
|
-
'document.getElementById("feedback-result").textContent'
|
|
86
|
-
], bm);
|
|
87
|
-
expect(result).toBe('');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('has submit button', async () => {
|
|
91
|
-
const exists = await handleReadCommand('js', [
|
|
92
|
-
'!!document.getElementById("submit-btn")'
|
|
93
|
-
], bm);
|
|
94
|
-
expect(exists).toBe('true');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('has regenerate button', async () => {
|
|
98
|
-
const exists = await handleReadCommand('js', [
|
|
99
|
-
'!!document.getElementById("regen-btn")'
|
|
100
|
-
], bm);
|
|
101
|
-
expect(exists).toBe('true');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('has 3 variant cards', async () => {
|
|
105
|
-
const count = await handleReadCommand('js', [
|
|
106
|
-
'document.querySelectorAll(".variant").length'
|
|
107
|
-
], bm);
|
|
108
|
-
expect(count).toBe('3');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('has pick radio buttons for each variant', async () => {
|
|
112
|
-
const count = await handleReadCommand('js', [
|
|
113
|
-
'document.querySelectorAll("input[name=\\"preferred\\"]").length'
|
|
114
|
-
], bm);
|
|
115
|
-
expect(count).toBe('3');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('has star ratings for each variant', async () => {
|
|
119
|
-
const count = await handleReadCommand('js', [
|
|
120
|
-
'document.querySelectorAll(".stars").length'
|
|
121
|
-
], bm);
|
|
122
|
-
expect(count).toBe('3');
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// ─── Submit Flow ────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
describe('Submit feedback flow', () => {
|
|
129
|
-
test('submit without interaction returns empty preferred', async () => {
|
|
130
|
-
// Reset page state
|
|
131
|
-
await handleWriteCommand('goto', [boardUrl], bm);
|
|
132
|
-
|
|
133
|
-
// Click submit without picking anything
|
|
134
|
-
await handleReadCommand('js', [
|
|
135
|
-
'document.getElementById("submit-btn").click()'
|
|
136
|
-
], bm);
|
|
137
|
-
|
|
138
|
-
// Status should be "submitted"
|
|
139
|
-
const status = await handleReadCommand('js', [
|
|
140
|
-
'document.getElementById("status").textContent'
|
|
141
|
-
], bm);
|
|
142
|
-
expect(status).toBe('submitted');
|
|
143
|
-
|
|
144
|
-
// Read feedback JSON
|
|
145
|
-
const raw = await handleReadCommand('js', [
|
|
146
|
-
'document.getElementById("feedback-result").textContent'
|
|
147
|
-
], bm);
|
|
148
|
-
const feedback = JSON.parse(raw);
|
|
149
|
-
expect(feedback.preferred).toBeNull();
|
|
150
|
-
expect(feedback.regenerated).toBe(false);
|
|
151
|
-
expect(feedback.ratings).toBeDefined();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('submit with pick + rating + comment returns structured JSON', async () => {
|
|
155
|
-
// Fresh page
|
|
156
|
-
await handleWriteCommand('goto', [boardUrl], bm);
|
|
157
|
-
|
|
158
|
-
// Pick variant B
|
|
159
|
-
await handleReadCommand('js', [
|
|
160
|
-
'document.querySelectorAll("input[name=\\"preferred\\"]")[1].click()'
|
|
161
|
-
], bm);
|
|
162
|
-
|
|
163
|
-
// Rate variant A: 4 stars (click the 4th star)
|
|
164
|
-
await handleReadCommand('js', [
|
|
165
|
-
'document.querySelectorAll(".stars")[0].querySelectorAll(".star")[3].click()'
|
|
166
|
-
], bm);
|
|
167
|
-
|
|
168
|
-
// Rate variant B: 5 stars
|
|
169
|
-
await handleReadCommand('js', [
|
|
170
|
-
'document.querySelectorAll(".stars")[1].querySelectorAll(".star")[4].click()'
|
|
171
|
-
], bm);
|
|
172
|
-
|
|
173
|
-
// Add comment on variant A
|
|
174
|
-
await handleReadCommand('js', [
|
|
175
|
-
'document.querySelectorAll(".feedback-input")[0].value = "Good spacing but wrong colors"'
|
|
176
|
-
], bm);
|
|
177
|
-
|
|
178
|
-
// Add overall feedback
|
|
179
|
-
await handleReadCommand('js', [
|
|
180
|
-
'document.getElementById("overall-feedback").value = "Go with B, make the CTA bigger"'
|
|
181
|
-
], bm);
|
|
182
|
-
|
|
183
|
-
// Submit
|
|
184
|
-
await handleReadCommand('js', [
|
|
185
|
-
'document.getElementById("submit-btn").click()'
|
|
186
|
-
], bm);
|
|
187
|
-
|
|
188
|
-
// Verify status
|
|
189
|
-
const status = await handleReadCommand('js', [
|
|
190
|
-
'document.getElementById("status").textContent'
|
|
191
|
-
], bm);
|
|
192
|
-
expect(status).toBe('submitted');
|
|
193
|
-
|
|
194
|
-
// Read and verify structured feedback
|
|
195
|
-
const raw = await handleReadCommand('js', [
|
|
196
|
-
'document.getElementById("feedback-result").textContent'
|
|
197
|
-
], bm);
|
|
198
|
-
const feedback = JSON.parse(raw);
|
|
199
|
-
|
|
200
|
-
expect(feedback.preferred).toBe('B');
|
|
201
|
-
expect(feedback.ratings.A).toBe(4);
|
|
202
|
-
expect(feedback.ratings.B).toBe(5);
|
|
203
|
-
expect(feedback.comments.A).toBe('Good spacing but wrong colors');
|
|
204
|
-
expect(feedback.overall).toBe('Go with B, make the CTA bigger');
|
|
205
|
-
expect(feedback.regenerated).toBe(false);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test('submit button is disabled after submission', async () => {
|
|
209
|
-
const disabled = await handleReadCommand('js', [
|
|
210
|
-
'document.getElementById("submit-btn").disabled'
|
|
211
|
-
], bm);
|
|
212
|
-
expect(disabled).toBe('true');
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test('success message is visible after submission', async () => {
|
|
216
|
-
const display = await handleReadCommand('js', [
|
|
217
|
-
'document.getElementById("success-msg").style.display'
|
|
218
|
-
], bm);
|
|
219
|
-
expect(display).toBe('block');
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// ─── Regenerate Flow ────────────────────────────────────────────
|
|
224
|
-
|
|
225
|
-
describe('Regenerate flow', () => {
|
|
226
|
-
test('regenerate button sets status to "regenerate"', async () => {
|
|
227
|
-
// Fresh page
|
|
228
|
-
await handleWriteCommand('goto', [boardUrl], bm);
|
|
229
|
-
|
|
230
|
-
// Click "Totally different" chiclet then regenerate
|
|
231
|
-
await handleReadCommand('js', [
|
|
232
|
-
'document.querySelector(".regen-chiclet[data-action=\\"different\\"]").click()'
|
|
233
|
-
], bm);
|
|
234
|
-
await handleReadCommand('js', [
|
|
235
|
-
'document.getElementById("regen-btn").click()'
|
|
236
|
-
], bm);
|
|
237
|
-
|
|
238
|
-
const status = await handleReadCommand('js', [
|
|
239
|
-
'document.getElementById("status").textContent'
|
|
240
|
-
], bm);
|
|
241
|
-
expect(status).toBe('regenerate');
|
|
242
|
-
|
|
243
|
-
// Verify regenerate action in feedback
|
|
244
|
-
const raw = await handleReadCommand('js', [
|
|
245
|
-
'document.getElementById("feedback-result").textContent'
|
|
246
|
-
], bm);
|
|
247
|
-
const feedback = JSON.parse(raw);
|
|
248
|
-
expect(feedback.regenerated).toBe(true);
|
|
249
|
-
expect(feedback.regenerateAction).toBe('different');
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test('"More like this" sets regenerate with variant reference', async () => {
|
|
253
|
-
// Fresh page
|
|
254
|
-
await handleWriteCommand('goto', [boardUrl], bm);
|
|
255
|
-
|
|
256
|
-
// Click "More like this" on variant B
|
|
257
|
-
await handleReadCommand('js', [
|
|
258
|
-
'document.querySelectorAll(".more-like-this")[1].click()'
|
|
259
|
-
], bm);
|
|
260
|
-
|
|
261
|
-
const status = await handleReadCommand('js', [
|
|
262
|
-
'document.getElementById("status").textContent'
|
|
263
|
-
], bm);
|
|
264
|
-
expect(status).toBe('regenerate');
|
|
265
|
-
|
|
266
|
-
const raw = await handleReadCommand('js', [
|
|
267
|
-
'document.getElementById("feedback-result").textContent'
|
|
268
|
-
], bm);
|
|
269
|
-
const feedback = JSON.parse(raw);
|
|
270
|
-
expect(feedback.regenerated).toBe(true);
|
|
271
|
-
expect(feedback.regenerateAction).toBe('more_like_B');
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
test('regenerate with custom text', async () => {
|
|
275
|
-
// Fresh page
|
|
276
|
-
await handleWriteCommand('goto', [boardUrl], bm);
|
|
277
|
-
|
|
278
|
-
// Type custom regeneration text
|
|
279
|
-
await handleReadCommand('js', [
|
|
280
|
-
'document.getElementById("regen-custom-input").value = "V3 layout with V1 colors"'
|
|
281
|
-
], bm);
|
|
282
|
-
|
|
283
|
-
// Click regenerate (no chiclet selected = custom)
|
|
284
|
-
await handleReadCommand('js', [
|
|
285
|
-
'document.getElementById("regen-btn").click()'
|
|
286
|
-
], bm);
|
|
287
|
-
|
|
288
|
-
const raw = await handleReadCommand('js', [
|
|
289
|
-
'document.getElementById("feedback-result").textContent'
|
|
290
|
-
], bm);
|
|
291
|
-
const feedback = JSON.parse(raw);
|
|
292
|
-
expect(feedback.regenerated).toBe(true);
|
|
293
|
-
expect(feedback.regenerateAction).toBe('V3 layout with V1 colors');
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// ─── Agent Polling Pattern ──────────────────────────────────────
|
|
298
|
-
|
|
299
|
-
describe('Agent polling pattern (simulates what $B eval does)', () => {
|
|
300
|
-
test('status is empty before user action', async () => {
|
|
301
|
-
// Fresh page — simulates agent's first poll
|
|
302
|
-
await handleWriteCommand('goto', [boardUrl], bm);
|
|
303
|
-
|
|
304
|
-
const status = await handleReadCommand('js', [
|
|
305
|
-
'document.getElementById("status").textContent'
|
|
306
|
-
], bm);
|
|
307
|
-
expect(status).toBe('');
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test('full polling cycle: empty → submitted → read JSON', async () => {
|
|
311
|
-
await handleWriteCommand('goto', [boardUrl], bm);
|
|
312
|
-
|
|
313
|
-
// Poll 1: empty (user hasn't acted)
|
|
314
|
-
const poll1 = await handleReadCommand('js', [
|
|
315
|
-
'document.getElementById("status").textContent'
|
|
316
|
-
], bm);
|
|
317
|
-
expect(poll1).toBe('');
|
|
318
|
-
|
|
319
|
-
// User acts: pick A, submit
|
|
320
|
-
await handleReadCommand('js', [
|
|
321
|
-
'document.querySelectorAll("input[name=\\"preferred\\"]")[0].click()'
|
|
322
|
-
], bm);
|
|
323
|
-
await handleReadCommand('js', [
|
|
324
|
-
'document.getElementById("submit-btn").click()'
|
|
325
|
-
], bm);
|
|
326
|
-
|
|
327
|
-
// Poll 2: submitted
|
|
328
|
-
const poll2 = await handleReadCommand('js', [
|
|
329
|
-
'document.getElementById("status").textContent'
|
|
330
|
-
], bm);
|
|
331
|
-
expect(poll2).toBe('submitted');
|
|
332
|
-
|
|
333
|
-
// Read feedback (what the agent does after seeing "submitted")
|
|
334
|
-
const raw = await handleReadCommand('js', [
|
|
335
|
-
'document.getElementById("feedback-result").textContent'
|
|
336
|
-
], bm);
|
|
337
|
-
const feedback = JSON.parse(raw);
|
|
338
|
-
expect(feedback.preferred).toBe('A');
|
|
339
|
-
expect(typeof feedback.ratings).toBe('object');
|
|
340
|
-
expect(typeof feedback.comments).toBe('object');
|
|
341
|
-
});
|
|
342
|
-
});
|
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import { resolveConfig, ensureStateDir, readVersionHash, getGitRoot, getRemoteSlug } from '../src/config';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as os from 'os';
|
|
6
|
-
|
|
7
|
-
describe('config', () => {
|
|
8
|
-
describe('getGitRoot', () => {
|
|
9
|
-
test('returns a path when in a git repo', () => {
|
|
10
|
-
const root = getGitRoot();
|
|
11
|
-
expect(root).not.toBeNull();
|
|
12
|
-
expect(fs.existsSync(path.join(root!, '.git'))).toBe(true);
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
describe('resolveConfig', () => {
|
|
17
|
-
test('uses git root by default', () => {
|
|
18
|
-
const config = resolveConfig({});
|
|
19
|
-
const gitRoot = getGitRoot();
|
|
20
|
-
expect(gitRoot).not.toBeNull();
|
|
21
|
-
expect(config.projectDir).toBe(gitRoot);
|
|
22
|
-
expect(config.stateDir).toBe(path.join(gitRoot!, '.gstack'));
|
|
23
|
-
expect(config.stateFile).toBe(path.join(gitRoot!, '.gstack', 'browse.json'));
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('derives paths from BROWSE_STATE_FILE when set', () => {
|
|
27
|
-
const stateFile = '/tmp/test-config/.gstack/browse.json';
|
|
28
|
-
const config = resolveConfig({ BROWSE_STATE_FILE: stateFile });
|
|
29
|
-
expect(config.stateFile).toBe(stateFile);
|
|
30
|
-
expect(config.stateDir).toBe('/tmp/test-config/.gstack');
|
|
31
|
-
expect(config.projectDir).toBe('/tmp/test-config');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('log paths are in stateDir', () => {
|
|
35
|
-
const config = resolveConfig({});
|
|
36
|
-
expect(config.consoleLog).toBe(path.join(config.stateDir, 'browse-console.log'));
|
|
37
|
-
expect(config.networkLog).toBe(path.join(config.stateDir, 'browse-network.log'));
|
|
38
|
-
expect(config.dialogLog).toBe(path.join(config.stateDir, 'browse-dialog.log'));
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('ensureStateDir', () => {
|
|
43
|
-
test('creates directory if it does not exist', () => {
|
|
44
|
-
const tmpDir = path.join(os.tmpdir(), `browse-config-test-${Date.now()}`);
|
|
45
|
-
const config = resolveConfig({ BROWSE_STATE_FILE: path.join(tmpDir, '.gstack', 'browse.json') });
|
|
46
|
-
expect(fs.existsSync(config.stateDir)).toBe(false);
|
|
47
|
-
ensureStateDir(config);
|
|
48
|
-
expect(fs.existsSync(config.stateDir)).toBe(true);
|
|
49
|
-
// Cleanup
|
|
50
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('is a no-op if directory already exists', () => {
|
|
54
|
-
const tmpDir = path.join(os.tmpdir(), `browse-config-test-${Date.now()}`);
|
|
55
|
-
const stateDir = path.join(tmpDir, '.gstack');
|
|
56
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
57
|
-
const config = resolveConfig({ BROWSE_STATE_FILE: path.join(stateDir, 'browse.json') });
|
|
58
|
-
ensureStateDir(config); // should not throw
|
|
59
|
-
expect(fs.existsSync(config.stateDir)).toBe(true);
|
|
60
|
-
// Cleanup
|
|
61
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('adds .gstack/ to .gitignore if not present', () => {
|
|
65
|
-
const tmpDir = path.join(os.tmpdir(), `browse-gitignore-test-${Date.now()}`);
|
|
66
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
67
|
-
fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'node_modules/\n');
|
|
68
|
-
const config = resolveConfig({ BROWSE_STATE_FILE: path.join(tmpDir, '.gstack', 'browse.json') });
|
|
69
|
-
ensureStateDir(config);
|
|
70
|
-
const content = fs.readFileSync(path.join(tmpDir, '.gitignore'), 'utf-8');
|
|
71
|
-
expect(content).toContain('.gstack/');
|
|
72
|
-
expect(content).toBe('node_modules/\n.gstack/\n');
|
|
73
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('does not duplicate .gstack/ in .gitignore', () => {
|
|
77
|
-
const tmpDir = path.join(os.tmpdir(), `browse-gitignore-test-${Date.now()}`);
|
|
78
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
79
|
-
fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'node_modules/\n.gstack/\n');
|
|
80
|
-
const config = resolveConfig({ BROWSE_STATE_FILE: path.join(tmpDir, '.gstack', 'browse.json') });
|
|
81
|
-
ensureStateDir(config);
|
|
82
|
-
const content = fs.readFileSync(path.join(tmpDir, '.gitignore'), 'utf-8');
|
|
83
|
-
expect(content).toBe('node_modules/\n.gstack/\n');
|
|
84
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('handles .gitignore without trailing newline', () => {
|
|
88
|
-
const tmpDir = path.join(os.tmpdir(), `browse-gitignore-test-${Date.now()}`);
|
|
89
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
90
|
-
fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'node_modules');
|
|
91
|
-
const config = resolveConfig({ BROWSE_STATE_FILE: path.join(tmpDir, '.gstack', 'browse.json') });
|
|
92
|
-
ensureStateDir(config);
|
|
93
|
-
const content = fs.readFileSync(path.join(tmpDir, '.gitignore'), 'utf-8');
|
|
94
|
-
expect(content).toBe('node_modules\n.gstack/\n');
|
|
95
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('logs warning to browse-server.log on non-ENOENT gitignore error', () => {
|
|
99
|
-
const tmpDir = path.join(os.tmpdir(), `browse-gitignore-test-${Date.now()}`);
|
|
100
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
101
|
-
// Create a read-only .gitignore (no .gstack/ entry → would try to append)
|
|
102
|
-
fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'node_modules/\n');
|
|
103
|
-
fs.chmodSync(path.join(tmpDir, '.gitignore'), 0o444);
|
|
104
|
-
const config = resolveConfig({ BROWSE_STATE_FILE: path.join(tmpDir, '.gstack', 'browse.json') });
|
|
105
|
-
ensureStateDir(config); // should not throw
|
|
106
|
-
// Verify warning was written to server log
|
|
107
|
-
const logPath = path.join(config.stateDir, 'browse-server.log');
|
|
108
|
-
expect(fs.existsSync(logPath)).toBe(true);
|
|
109
|
-
const logContent = fs.readFileSync(logPath, 'utf-8');
|
|
110
|
-
expect(logContent).toContain('Warning: could not update .gitignore');
|
|
111
|
-
// .gitignore should remain unchanged
|
|
112
|
-
const gitignoreContent = fs.readFileSync(path.join(tmpDir, '.gitignore'), 'utf-8');
|
|
113
|
-
expect(gitignoreContent).toBe('node_modules/\n');
|
|
114
|
-
// Cleanup
|
|
115
|
-
fs.chmodSync(path.join(tmpDir, '.gitignore'), 0o644);
|
|
116
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('skips if no .gitignore exists', () => {
|
|
120
|
-
const tmpDir = path.join(os.tmpdir(), `browse-gitignore-test-${Date.now()}`);
|
|
121
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
122
|
-
const config = resolveConfig({ BROWSE_STATE_FILE: path.join(tmpDir, '.gstack', 'browse.json') });
|
|
123
|
-
ensureStateDir(config);
|
|
124
|
-
expect(fs.existsSync(path.join(tmpDir, '.gitignore'))).toBe(false);
|
|
125
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('getRemoteSlug', () => {
|
|
130
|
-
test('returns owner-repo format for current repo', () => {
|
|
131
|
-
const slug = getRemoteSlug();
|
|
132
|
-
// This repo has an origin remote — should return a slug
|
|
133
|
-
expect(slug).toBeTruthy();
|
|
134
|
-
expect(slug).toMatch(/^[a-zA-Z0-9._-]+-[a-zA-Z0-9._-]+$/);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('parses SSH remote URLs', () => {
|
|
138
|
-
// Test the regex directly since we can't mock Bun.spawnSync easily
|
|
139
|
-
const url = 'git@github.com:garrytan/gstack.git';
|
|
140
|
-
const match = url.match(/[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
141
|
-
expect(match).not.toBeNull();
|
|
142
|
-
expect(`${match![1]}-${match![2]}`).toBe('garrytan-gstack');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test('parses HTTPS remote URLs', () => {
|
|
146
|
-
const url = 'https://github.com/garrytan/gstack.git';
|
|
147
|
-
const match = url.match(/[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
148
|
-
expect(match).not.toBeNull();
|
|
149
|
-
expect(`${match![1]}-${match![2]}`).toBe('garrytan-gstack');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test('parses HTTPS remote URLs without .git suffix', () => {
|
|
153
|
-
const url = 'https://github.com/garrytan/gstack';
|
|
154
|
-
const match = url.match(/[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
155
|
-
expect(match).not.toBeNull();
|
|
156
|
-
expect(`${match![1]}-${match![2]}`).toBe('garrytan-gstack');
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe('readVersionHash', () => {
|
|
161
|
-
test('returns null when .version file does not exist', () => {
|
|
162
|
-
const result = readVersionHash('/nonexistent/path/browse');
|
|
163
|
-
expect(result).toBeNull();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test('reads version from .version file adjacent to execPath', () => {
|
|
167
|
-
const tmpDir = path.join(os.tmpdir(), `browse-version-test-${Date.now()}`);
|
|
168
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
169
|
-
const versionFile = path.join(tmpDir, '.version');
|
|
170
|
-
fs.writeFileSync(versionFile, 'abc123def\n');
|
|
171
|
-
const result = readVersionHash(path.join(tmpDir, 'browse'));
|
|
172
|
-
expect(result).toBe('abc123def');
|
|
173
|
-
// Cleanup
|
|
174
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
describe('resolveServerScript', () => {
|
|
180
|
-
// Import the function from cli.ts
|
|
181
|
-
const { resolveServerScript } = require('../src/cli');
|
|
182
|
-
|
|
183
|
-
test('uses BROWSE_SERVER_SCRIPT env when set', () => {
|
|
184
|
-
const result = resolveServerScript({ BROWSE_SERVER_SCRIPT: '/custom/server.ts' }, '', '');
|
|
185
|
-
expect(result).toBe('/custom/server.ts');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test('finds server.ts adjacent to cli.ts in dev mode', () => {
|
|
189
|
-
const srcDir = path.resolve(__dirname, '../src');
|
|
190
|
-
const result = resolveServerScript({}, srcDir, '');
|
|
191
|
-
expect(result).toBe(path.join(srcDir, 'server.ts'));
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('throws when server.ts cannot be found', () => {
|
|
195
|
-
expect(() => resolveServerScript({}, '/nonexistent/$bunfs', '/nonexistent/browse'))
|
|
196
|
-
.toThrow('Cannot find server.ts');
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe('resolveNodeServerScript', () => {
|
|
201
|
-
const { resolveNodeServerScript } = require('../src/cli');
|
|
202
|
-
|
|
203
|
-
test('finds server-node.mjs in dist from dev mode', () => {
|
|
204
|
-
const srcDir = path.resolve(__dirname, '../src');
|
|
205
|
-
const distFile = path.resolve(srcDir, '..', 'dist', 'server-node.mjs');
|
|
206
|
-
const fs = require('fs');
|
|
207
|
-
// Only test if the file exists (it may not be built yet)
|
|
208
|
-
if (fs.existsSync(distFile)) {
|
|
209
|
-
const result = resolveNodeServerScript(srcDir, '');
|
|
210
|
-
expect(result).toBe(distFile);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test('returns null when server-node.mjs does not exist', () => {
|
|
215
|
-
const result = resolveNodeServerScript('/nonexistent/$bunfs', '/nonexistent/browse');
|
|
216
|
-
expect(result).toBeNull();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test('finds server-node.mjs adjacent to compiled binary', () => {
|
|
220
|
-
const distDir = path.resolve(__dirname, '../dist');
|
|
221
|
-
const distFile = path.join(distDir, 'server-node.mjs');
|
|
222
|
-
const fs = require('fs');
|
|
223
|
-
if (fs.existsSync(distFile)) {
|
|
224
|
-
const result = resolveNodeServerScript('/$bunfs/something', path.join(distDir, 'browse'));
|
|
225
|
-
expect(result).toBe(distFile);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
describe('version mismatch detection', () => {
|
|
231
|
-
test('detects when versions differ', () => {
|
|
232
|
-
const stateVersion = 'abc123';
|
|
233
|
-
const currentVersion = 'def456';
|
|
234
|
-
expect(stateVersion !== currentVersion).toBe(true);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
test('no mismatch when versions match', () => {
|
|
238
|
-
const stateVersion = 'abc123';
|
|
239
|
-
const currentVersion = 'abc123';
|
|
240
|
-
expect(stateVersion !== currentVersion).toBe(false);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test('no mismatch when either version is null', () => {
|
|
244
|
-
const currentVersion: string | null = null;
|
|
245
|
-
const stateVersion: string | undefined = 'abc123';
|
|
246
|
-
// Version mismatch only triggers when both are present
|
|
247
|
-
const shouldRestart = currentVersion !== null && stateVersion !== undefined && currentVersion !== stateVersion;
|
|
248
|
-
expect(shouldRestart).toBe(false);
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe('isServerHealthy', () => {
|
|
253
|
-
const { isServerHealthy } = require('../src/cli');
|
|
254
|
-
const http = require('http');
|
|
255
|
-
|
|
256
|
-
test('returns true for a healthy server', async () => {
|
|
257
|
-
const server = http.createServer((_req: any, res: any) => {
|
|
258
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
259
|
-
res.end(JSON.stringify({ status: 'healthy' }));
|
|
260
|
-
});
|
|
261
|
-
await new Promise<void>(resolve => server.listen(0, resolve));
|
|
262
|
-
const port = server.address().port;
|
|
263
|
-
try {
|
|
264
|
-
expect(await isServerHealthy(port)).toBe(true);
|
|
265
|
-
} finally {
|
|
266
|
-
server.close();
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
test('returns false for an unhealthy server', async () => {
|
|
271
|
-
const server = http.createServer((_req: any, res: any) => {
|
|
272
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
273
|
-
res.end(JSON.stringify({ status: 'unhealthy' }));
|
|
274
|
-
});
|
|
275
|
-
await new Promise<void>(resolve => server.listen(0, resolve));
|
|
276
|
-
const port = server.address().port;
|
|
277
|
-
try {
|
|
278
|
-
expect(await isServerHealthy(port)).toBe(false);
|
|
279
|
-
} finally {
|
|
280
|
-
server.close();
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test('returns false when server is not running', async () => {
|
|
285
|
-
// Use a port that's almost certainly not in use
|
|
286
|
-
expect(await isServerHealthy(59999)).toBe(false);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
test('returns false on non-200 response', async () => {
|
|
290
|
-
const server = http.createServer((_req: any, res: any) => {
|
|
291
|
-
res.writeHead(500);
|
|
292
|
-
res.end('Internal Server Error');
|
|
293
|
-
});
|
|
294
|
-
await new Promise<void>(resolve => server.listen(0, resolve));
|
|
295
|
-
const port = server.address().port;
|
|
296
|
-
try {
|
|
297
|
-
expect(await isServerHealthy(port)).toBe(false);
|
|
298
|
-
} finally {
|
|
299
|
-
server.close();
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
describe('startup error log', () => {
|
|
305
|
-
test('write and read error log', () => {
|
|
306
|
-
const tmpDir = path.join(os.tmpdir(), `browse-error-log-test-${Date.now()}`);
|
|
307
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
308
|
-
const errorLogPath = path.join(tmpDir, 'browse-startup-error.log');
|
|
309
|
-
const errorMsg = 'Cannot find module playwright';
|
|
310
|
-
fs.writeFileSync(errorLogPath, `2026-03-23T00:00:00.000Z ${errorMsg}\n`);
|
|
311
|
-
const content = fs.readFileSync(errorLogPath, 'utf-8').trim();
|
|
312
|
-
expect(content).toContain(errorMsg);
|
|
313
|
-
expect(content).toMatch(/^\d{4}-\d{2}-\d{2}T/); // ISO timestamp prefix
|
|
314
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
315
|
-
});
|
|
316
|
-
});
|