pinokiod 7.3.14 → 7.4.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 (51) hide show
  1. package/kernel/api/index.js +111 -15
  2. package/kernel/api/script/index.js +10 -0
  3. package/kernel/autolaunch.js +111 -0
  4. package/kernel/environment.js +89 -1
  5. package/kernel/git.js +9 -19
  6. package/kernel/index.js +142 -43
  7. package/kernel/launch_requirements.js +1115 -0
  8. package/kernel/ready.js +231 -0
  9. package/kernel/script.js +16 -0
  10. package/kernel/shells.js +9 -1
  11. package/kernel/workspace_status.js +111 -45
  12. package/package.json +1 -1
  13. package/server/autolaunch.js +725 -0
  14. package/server/index.js +244 -411
  15. package/server/public/logs.js +15 -1
  16. package/server/public/style.css +99 -31
  17. package/server/views/app.ejs +263 -159
  18. package/server/views/autolaunch.ejs +363 -75
  19. package/server/views/index.ejs +550 -26
  20. package/server/views/logs.ejs +14 -12
  21. package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
  22. package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
  23. package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
  24. package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
  25. package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
  26. package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
  27. package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
  28. package/server/views/partials/home_action_modal.ejs +4 -1
  29. package/server/views/partials/launch_requirements_status_client.ejs +271 -0
  30. package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
  31. package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
  32. package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
  33. package/server/views/terminal.ejs +196 -2
  34. package/test/home-autolaunch-live-ui.test.js +455 -0
  35. package/test/launch-requirements-browser.test.js +579 -0
  36. package/test/launch-requirements-contract-coverage.test.js +627 -0
  37. package/test/launch-requirements-real-browser.js +625 -0
  38. package/test/launch-requirements-status-client.test.js +132 -0
  39. package/test/launch-requirements.test.js +1806 -0
  40. package/test/launch-settings-ui.test.js +370 -0
  41. package/test/ready-state.test.js +49 -0
  42. package/test/server-autolaunch.test.js +1052 -0
  43. package/test/startup-git-index-benchmark.js +409 -0
  44. package/test/startup-git-index-browser.js +320 -0
  45. package/test/startup-git-index-performance.test.js +380 -0
  46. package/test/startup-git-index-refactor.test.js +450 -0
  47. package/test/startup-git-index-route.test.js +588 -0
  48. package/test/universal-launcher.smoke.spec.js +10 -9
  49. package/test/workspace-gitignore-benchmark.js +815 -0
  50. package/test/workspace-gitignore-path-scoped.test.js +256 -0
  51. package/spec/INSTRUCTION_SYNC.md +0 -432
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env node
2
+
3
+ const assert = require('node:assert/strict')
4
+ const childProcess = require('node:child_process')
5
+ const fs = require('node:fs')
6
+ const fsp = require('node:fs/promises')
7
+ const net = require('node:net')
8
+ const os = require('node:os')
9
+ const path = require('node:path')
10
+
11
+ const repoRoot = path.resolve(__dirname, '..')
12
+ const outputRoot = path.resolve(repoRoot, 'output/playwright/startup-git-index')
13
+ const readyPrefix = 'PINOKIO_STARTUP_BROWSER_READY '
14
+ const serverMode = process.argv.includes('--server')
15
+
16
+ function parseArgs(argv = process.argv.slice(2)) {
17
+ const args = {}
18
+ for (let i = 0; i < argv.length; i += 1) {
19
+ const item = argv[i]
20
+ if (!item.startsWith('--')) continue
21
+ const key = item.slice(2)
22
+ const next = argv[i + 1]
23
+ if (next && !next.startsWith('--')) {
24
+ args[key] = next
25
+ i += 1
26
+ } else {
27
+ args[key] = true
28
+ }
29
+ }
30
+ return args
31
+ }
32
+
33
+ function loadPlaywright() {
34
+ try {
35
+ return require('playwright')
36
+ } catch (_) {}
37
+
38
+ try {
39
+ const bin = childProcess.execFileSync('which', ['playwright'], { encoding: 'utf8' }).trim()
40
+ if (bin) {
41
+ return require(path.join(path.dirname(path.dirname(bin)), 'playwright'))
42
+ }
43
+ } catch (_) {}
44
+
45
+ throw new Error('Playwright is required. Run: npx -y -p playwright node test/startup-git-index-browser.js')
46
+ }
47
+
48
+ function chromiumExecutablePath(playwright) {
49
+ const candidates = [
50
+ process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,
51
+ playwright.chromium.executablePath(),
52
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
53
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
54
+ ].filter(Boolean)
55
+ return candidates.find((candidate) => fs.existsSync(candidate)) || ''
56
+ }
57
+
58
+ function run(cmd, args, cwd, options = {}) {
59
+ return childProcess.execFileSync(cmd, args, {
60
+ cwd,
61
+ encoding: 'utf8',
62
+ env: {
63
+ ...process.env,
64
+ GIT_AUTHOR_NAME: 'Codex Browser Test',
65
+ GIT_AUTHOR_EMAIL: 'codex-browser-test@example.com',
66
+ GIT_COMMITTER_NAME: 'Codex Browser Test',
67
+ GIT_COMMITTER_EMAIL: 'codex-browser-test@example.com',
68
+ GIT_TERMINAL_PROMPT: '0',
69
+ ...options.env,
70
+ },
71
+ stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
72
+ }).trim()
73
+ }
74
+
75
+ function git(args, cwd) {
76
+ return run('git', args, cwd)
77
+ }
78
+
79
+ function initAppRepo(repoPath, { remote }) {
80
+ fs.mkdirSync(repoPath, { recursive: true })
81
+ git(['init'], repoPath)
82
+ git(['config', 'user.name', 'Codex Browser Test'], repoPath)
83
+ git(['config', 'user.email', 'codex-browser-test@example.com'], repoPath)
84
+ fs.writeFileSync(path.join(repoPath, 'pinokio.js'), [
85
+ 'module.exports = {',
86
+ ' title: "Startup Browser Fixture",',
87
+ ' description: "Fixture for startup git index Browser verification.",',
88
+ ' menu: []',
89
+ '}',
90
+ '',
91
+ ].join('\n'))
92
+ fs.writeFileSync(path.join(repoPath, 'tracked.txt'), 'top clean\n')
93
+ git(['add', '.'], repoPath)
94
+ git(['commit', '-m', 'top baseline'], repoPath)
95
+ git(['branch', '-M', 'main'], repoPath)
96
+ git(['remote', 'add', 'origin', remote], repoPath)
97
+ }
98
+
99
+ function initNestedRepo(repoPath, { remote }) {
100
+ fs.mkdirSync(repoPath, { recursive: true })
101
+ git(['init'], repoPath)
102
+ git(['config', 'user.name', 'Codex Browser Test'], repoPath)
103
+ git(['config', 'user.email', 'codex-browser-test@example.com'], repoPath)
104
+ fs.writeFileSync(path.join(repoPath, 'nested.txt'), 'nested clean\n')
105
+ git(['add', '.'], repoPath)
106
+ git(['commit', '-m', 'nested baseline'], repoPath)
107
+ git(['branch', '-M', 'main'], repoPath)
108
+ git(['remote', 'add', 'origin', remote], repoPath)
109
+ }
110
+
111
+ async function freePort() {
112
+ return await new Promise((resolve, reject) => {
113
+ const server = net.createServer()
114
+ server.listen(0, '127.0.0.1', () => {
115
+ const port = server.address().port
116
+ server.close(() => resolve(port))
117
+ })
118
+ server.on('error', reject)
119
+ })
120
+ }
121
+
122
+ async function createHome() {
123
+ const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'pinokio-startup-browser-'))
124
+ const fakeUserHome = path.join(root, 'user')
125
+ const pinokioHome = path.join(root, 'pinokio')
126
+ const apiRoot = path.join(pinokioHome, 'api')
127
+ const workspaceName = 'browser-fixture'
128
+ const workspace = path.join(apiRoot, workspaceName)
129
+ const nested = path.join(workspace, 'nested')
130
+ await fsp.mkdir(fakeUserHome, { recursive: true })
131
+ await fsp.mkdir(apiRoot, { recursive: true })
132
+ const runtimeBin = path.resolve(process.env.PINOKIO_REAL_BROWSER_BIN || path.join(os.homedir(), 'pinokio', 'bin'))
133
+ if (fs.existsSync(runtimeBin)) {
134
+ await fsp.symlink(runtimeBin, path.join(pinokioHome, 'bin'), 'dir')
135
+ }
136
+ initAppRepo(workspace, { remote: 'https://example.com/pinokio-browser-fixture.git' })
137
+ initNestedRepo(nested, { remote: 'https://example.com/pinokio-browser-fixture-nested.git' })
138
+ return { root, fakeUserHome, pinokioHome, workspaceName, workspace, nested }
139
+ }
140
+
141
+ async function runServerMode() {
142
+ const args = parseArgs()
143
+ const port = Number(args.port)
144
+ const home = path.resolve(String(args.home || ''))
145
+ const pkg = require(path.join(repoRoot, 'package.json'))
146
+ const Server = require(path.join(repoRoot, 'server'))
147
+ const server = new Server({
148
+ store: { store: { home, version: pkg.version } },
149
+ agent: 'test',
150
+ newsfeed: '',
151
+ portal: '',
152
+ })
153
+ server.port = port
154
+ await server.start({ debug: true })
155
+ process.stdout.write(`${readyPrefix}${JSON.stringify({ port, home, pid: process.pid })}\n`)
156
+ setInterval(() => {}, 1000)
157
+ }
158
+
159
+ function waitForReady(child, timeoutMs = 60000) {
160
+ return new Promise((resolve, reject) => {
161
+ let stdout = ''
162
+ let stderr = ''
163
+ const timer = setTimeout(() => {
164
+ reject(new Error(`server did not become ready in ${timeoutMs}ms\nstdout:\n${stdout}\nstderr:\n${stderr}`))
165
+ }, timeoutMs)
166
+ child.stdout.on('data', (chunk) => {
167
+ const text = chunk.toString()
168
+ stdout += text
169
+ for (const line of text.split(/\r?\n/)) {
170
+ if (line.startsWith(readyPrefix)) {
171
+ clearTimeout(timer)
172
+ resolve({ ready: JSON.parse(line.slice(readyPrefix.length)), stdout, stderr })
173
+ }
174
+ }
175
+ })
176
+ child.stderr.on('data', (chunk) => {
177
+ stderr += chunk.toString()
178
+ })
179
+ child.on('exit', (code, signal) => {
180
+ clearTimeout(timer)
181
+ reject(new Error(`server exited before ready: code=${code} signal=${signal}\nstdout:\n${stdout}\nstderr:\n${stderr}`))
182
+ })
183
+ })
184
+ }
185
+
186
+ async function withBrowserServer(callback) {
187
+ const home = await createHome()
188
+ const port = await freePort()
189
+ const artifacts = path.join(outputRoot, `${Date.now()}-changes-ui`)
190
+ await fsp.mkdir(artifacts, { recursive: true })
191
+ const child = childProcess.spawn(process.execPath, [
192
+ __filename,
193
+ '--server',
194
+ '--port',
195
+ String(port),
196
+ '--home',
197
+ home.pinokioHome,
198
+ ], {
199
+ cwd: repoRoot,
200
+ env: {
201
+ ...process.env,
202
+ HOME: home.fakeUserHome,
203
+ PINOKIO_HOME: home.pinokioHome,
204
+ },
205
+ detached: true,
206
+ stdio: ['ignore', 'pipe', 'pipe'],
207
+ })
208
+ const logs = []
209
+ child.stdout.on('data', (chunk) => logs.push(chunk.toString()))
210
+ child.stderr.on('data', (chunk) => logs.push(chunk.toString()))
211
+
212
+ const playwright = loadPlaywright()
213
+ const executablePath = chromiumExecutablePath(playwright)
214
+ assert.ok(executablePath, 'No Chromium executable is available for Browser verification')
215
+ let browser
216
+ let page
217
+ try {
218
+ await waitForReady(child)
219
+ browser = await playwright.chromium.launch({ headless: true, executablePath })
220
+ page = await browser.newPage()
221
+ page.setDefaultTimeout(20000)
222
+ const pageErrors = []
223
+ page.on('pageerror', (error) => pageErrors.push(error.message || String(error)))
224
+ await callback({
225
+ ...home,
226
+ baseUrl: `http://127.0.0.1:${port}`,
227
+ artifacts,
228
+ page,
229
+ })
230
+ assert.deepEqual(pageErrors, [])
231
+ await fsp.writeFile(path.join(artifacts, 'server.log'), logs.join(''))
232
+ } catch (error) {
233
+ if (page) {
234
+ await page.screenshot({ path: path.join(artifacts, 'failure.png'), fullPage: true }).catch(() => {})
235
+ await fsp.writeFile(path.join(artifacts, 'body.txt'), await page.locator('body').innerText().catch(() => '')).catch(() => {})
236
+ }
237
+ await fsp.writeFile(path.join(artifacts, 'server.log'), logs.join('')).catch(() => {})
238
+ error.message = `${error.message}\nartifacts: ${artifacts}`
239
+ throw error
240
+ } finally {
241
+ if (browser) {
242
+ await browser.close().catch(() => {})
243
+ }
244
+ if (child.pid) {
245
+ try {
246
+ process.kill(-child.pid, 'SIGTERM')
247
+ } catch (_) {
248
+ child.kill('SIGTERM')
249
+ }
250
+ }
251
+ await new Promise((resolve) => {
252
+ const timer = setTimeout(resolve, 3000)
253
+ child.once('exit', () => {
254
+ clearTimeout(timer)
255
+ resolve()
256
+ })
257
+ })
258
+ await fsp.rm(home.root, { recursive: true, force: true })
259
+ }
260
+ }
261
+
262
+ async function main() {
263
+ await withBrowserServer(async ({ page, baseUrl, workspaceName, workspace, nested, artifacts }) => {
264
+ await page.goto(`${baseUrl}/p/${workspaceName}/files`, { waitUntil: 'domcontentloaded' })
265
+ await page.locator('#fs-status').waitFor({ state: 'visible' })
266
+ await page.screenshot({ path: path.join(artifacts, 'initial.png'), fullPage: true })
267
+
268
+ await fsp.writeFile(path.join(workspace, 'tracked.txt'), 'top clean\ntop dirty\n')
269
+ await fsp.writeFile(path.join(nested, 'nested.txt'), 'nested clean\nnested dirty\n')
270
+
271
+ await page.locator('#fs-changes-btn').click()
272
+ await page.locator('#fs-changes-btn .badge').waitFor({ state: 'visible' })
273
+ await page.waitForFunction(() => {
274
+ const badge = document.querySelector('#fs-changes-btn .badge')
275
+ return badge && badge.textContent && badge.textContent.trim() === '2'
276
+ }, null, { timeout: 15000 })
277
+
278
+ const topRepoItem = page.locator(`#fs-changes-menu .git-changes-item[data-repo="${workspaceName}"]`)
279
+ const nestedRepoItem = page.locator(`#fs-changes-menu .git-changes-item[data-repo="${workspaceName}/nested"]`)
280
+ await topRepoItem.waitFor()
281
+ await nestedRepoItem.waitFor()
282
+ await page.screenshot({ path: path.join(artifacts, 'changes-expanded.png'), fullPage: true })
283
+
284
+ await topRepoItem.click()
285
+ await page.locator('.pinokio-diff-modal').waitFor({ state: 'visible' })
286
+ await page.locator('.pinokio-git-diff-file-item-row[data-filepath="tracked.txt"]').click()
287
+ await page.locator('.pinokio-git-diff-viewer-panel').getByText('top dirty').waitFor()
288
+ await page.screenshot({ path: path.join(artifacts, 'top-diff.png'), fullPage: true })
289
+
290
+ const closeButton = page.locator('.swal2-close')
291
+ if (await closeButton.count()) {
292
+ await closeButton.click()
293
+ } else {
294
+ await page.keyboard.press('Escape')
295
+ }
296
+ await page.locator('.pinokio-diff-modal').waitFor({ state: 'hidden' }).catch(() => {})
297
+ await page.locator('#fs-changes-btn').click()
298
+ const nestedRepoItemAfterModal = page.locator(`#fs-changes-menu .git-changes-item[data-repo="${workspaceName}/nested"]`)
299
+ await nestedRepoItemAfterModal.waitFor({ state: 'visible' })
300
+ await nestedRepoItemAfterModal.click()
301
+ await page.locator('.pinokio-diff-modal').waitFor({ state: 'visible' })
302
+ await page.locator('.pinokio-git-diff-file-item-row[data-filepath="nested.txt"]').click()
303
+ await page.locator('.pinokio-git-diff-viewer-panel').getByText('nested dirty').waitFor()
304
+ await page.screenshot({ path: path.join(artifacts, 'nested-diff.png'), fullPage: true })
305
+
306
+ console.log(JSON.stringify({ ok: true, artifacts }))
307
+ })
308
+ }
309
+
310
+ if (serverMode) {
311
+ runServerMode().catch((error) => {
312
+ console.error(error && error.stack ? error.stack : error)
313
+ process.exitCode = 1
314
+ })
315
+ } else {
316
+ main().catch((error) => {
317
+ console.error(error && error.stack ? error.stack : error)
318
+ process.exitCode = 1
319
+ })
320
+ }
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+
3
+ const assert = require('node:assert/strict')
4
+ const childProcess = require('node:child_process')
5
+ const fs = require('node:fs')
6
+ const fsp = require('node:fs/promises')
7
+ const net = require('node:net')
8
+ const os = require('node:os')
9
+ const path = require('node:path')
10
+ const test = require('node:test')
11
+
12
+ const repoRoot = path.resolve(__dirname, '..')
13
+ const readyPrefix = 'PINOKIO_STARTUP_PERF_READY '
14
+ const serverMode = process.argv.includes('--server')
15
+
16
+ function parseArgs(argv = process.argv.slice(2)) {
17
+ const args = {}
18
+ for (let i = 0; i < argv.length; i += 1) {
19
+ const item = argv[i]
20
+ if (!item.startsWith('--')) continue
21
+ const key = item.slice(2)
22
+ const next = argv[i + 1]
23
+ if (next && !next.startsWith('--')) {
24
+ args[key] = next
25
+ i += 1
26
+ } else {
27
+ args[key] = true
28
+ }
29
+ }
30
+ return args
31
+ }
32
+
33
+ function run(cmd, args, cwd, options = {}) {
34
+ return childProcess.execFileSync(cmd, args, {
35
+ cwd,
36
+ encoding: 'utf8',
37
+ env: {
38
+ ...process.env,
39
+ GIT_AUTHOR_NAME: 'Codex Perf Test',
40
+ GIT_AUTHOR_EMAIL: 'codex-perf-test@example.com',
41
+ GIT_COMMITTER_NAME: 'Codex Perf Test',
42
+ GIT_COMMITTER_EMAIL: 'codex-perf-test@example.com',
43
+ GIT_TERMINAL_PROMPT: '0',
44
+ ...options.env,
45
+ },
46
+ stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
47
+ }).trim()
48
+ }
49
+
50
+ function git(args, cwd) {
51
+ return run('git', args, cwd)
52
+ }
53
+
54
+ function initRepo(repoPath, { remote, file, content, message }) {
55
+ fs.mkdirSync(repoPath, { recursive: true })
56
+ git(['init'], repoPath)
57
+ git(['config', 'user.name', 'Codex Perf Test'], repoPath)
58
+ git(['config', 'user.email', 'codex-perf-test@example.com'], repoPath)
59
+ fs.writeFileSync(path.join(repoPath, file), content)
60
+ git(['add', '.'], repoPath)
61
+ git(['commit', '-m', message], repoPath)
62
+ git(['branch', '-M', 'main'], repoPath)
63
+ git(['remote', 'add', 'origin', remote], repoPath)
64
+ }
65
+
66
+ async function freePort() {
67
+ return await new Promise((resolve, reject) => {
68
+ const server = net.createServer()
69
+ server.listen(0, '127.0.0.1', () => {
70
+ const port = server.address().port
71
+ server.close(() => resolve(port))
72
+ })
73
+ server.on('error', reject)
74
+ })
75
+ }
76
+
77
+ async function createFixtureHome() {
78
+ const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'pinokio-startup-perf-'))
79
+ const fakeUserHome = path.join(root, 'user')
80
+ const pinokioHome = path.join(root, 'pinokio')
81
+ const apiRoot = path.join(pinokioHome, 'api')
82
+ const workspaceName = 'perf-fixture'
83
+ const workspace = path.join(apiRoot, workspaceName)
84
+ const nested = path.join(workspace, 'nested')
85
+ await fsp.mkdir(fakeUserHome, { recursive: true })
86
+ await fsp.mkdir(apiRoot, { recursive: true })
87
+ initRepo(workspace, {
88
+ remote: 'https://example.com/pinokio-perf-fixture.git',
89
+ file: 'tracked.txt',
90
+ content: 'top clean\n',
91
+ message: 'top baseline',
92
+ })
93
+ fs.writeFileSync(path.join(workspace, '.gitignore'), 'tracked-hidden.txt\n')
94
+ fs.writeFileSync(path.join(workspace, 'tracked-hidden.txt'), 'hidden clean\n')
95
+ git(['add', '.gitignore'], workspace)
96
+ git(['add', '-f', 'tracked-hidden.txt'], workspace)
97
+ git(['commit', '-m', 'hidden baseline'], workspace)
98
+ initRepo(nested, {
99
+ remote: 'https://example.com/pinokio-perf-fixture-nested.git',
100
+ file: 'nested.txt',
101
+ content: 'nested clean\n',
102
+ message: 'nested baseline',
103
+ })
104
+ return { root, fakeUserHome, pinokioHome, apiRoot, workspaceName, workspace, nested }
105
+ }
106
+
107
+ function createMetrics(home) {
108
+ const apiRoot = path.resolve(home, 'api')
109
+ const empty = () => ({
110
+ startedAt: Date.now(),
111
+ gitIndexCalls: 0,
112
+ gitReposCalls: 0,
113
+ apiRootReposCalls: 0,
114
+ workspaceReposCalls: 0,
115
+ rootWatcherCalls: 0,
116
+ workspaceWatcherCalls: 0,
117
+ gitIgnorePreScanCalls: 0,
118
+ fs: {
119
+ readdir: 0,
120
+ readFile: 0,
121
+ stat: 0,
122
+ lstat: 0,
123
+ access: 0,
124
+ readdirEntries: 0,
125
+ readFileBytes: 0,
126
+ },
127
+ })
128
+ let current = empty()
129
+ return {
130
+ apiRoot,
131
+ reset() {
132
+ current = empty()
133
+ },
134
+ snapshot() {
135
+ return JSON.parse(JSON.stringify(current))
136
+ },
137
+ inc(key, amount = 1) {
138
+ current[key] += amount
139
+ },
140
+ incFs(key, amount = 1) {
141
+ current.fs[key] += amount
142
+ },
143
+ }
144
+ }
145
+
146
+ function installInstrumentation(home) {
147
+ const metrics = createMetrics(home)
148
+ const fsMethods = ['readdir', 'readFile', 'stat', 'lstat', 'access']
149
+ const originals = {}
150
+ for (const method of fsMethods) {
151
+ originals[method] = fs.promises[method]
152
+ fs.promises[method] = async function instrumentedFsMethod(...args) {
153
+ const result = await originals[method].apply(this, args)
154
+ metrics.incFs(method)
155
+ if (method === 'readdir' && Array.isArray(result)) {
156
+ metrics.incFs('readdirEntries', result.length)
157
+ } else if (method === 'readFile') {
158
+ metrics.incFs('readFileBytes', Buffer.isBuffer(result) ? result.length : Buffer.byteLength(String(result)))
159
+ }
160
+ return result
161
+ }
162
+ }
163
+
164
+ const Git = require(path.join(repoRoot, 'kernel/git'))
165
+ const WorkspaceStatusManager = require(path.join(repoRoot, 'kernel/workspace_status'))
166
+ const originalIndex = Git.prototype.index
167
+ const originalRepos = Git.prototype.repos
168
+ const originalEnsureWatcher = WorkspaceStatusManager.prototype.ensureWatcher
169
+ const originalEnsureGitIgnoreEngine = WorkspaceStatusManager.prototype.ensureGitIgnoreEngine
170
+
171
+ Git.prototype.index = async function instrumentedGitIndex(...args) {
172
+ metrics.inc('gitIndexCalls')
173
+ return originalIndex.apply(this, args)
174
+ }
175
+ Git.prototype.repos = async function instrumentedGitRepos(root, ...args) {
176
+ metrics.inc('gitReposCalls')
177
+ const resolvedRoot = root ? path.resolve(root) : ''
178
+ if (resolvedRoot === metrics.apiRoot) {
179
+ metrics.inc('apiRootReposCalls')
180
+ } else if (resolvedRoot.startsWith(`${metrics.apiRoot}${path.sep}`)) {
181
+ metrics.inc('workspaceReposCalls')
182
+ }
183
+ return originalRepos.call(this, root, ...args)
184
+ }
185
+ WorkspaceStatusManager.prototype.ensureWatcher = async function instrumentedEnsureWatcher(workspaceName, workspaceRoot, ...args) {
186
+ const resolvedRoot = workspaceRoot ? path.resolve(workspaceRoot) : ''
187
+ if (workspaceName === 'api' && resolvedRoot === metrics.apiRoot) {
188
+ metrics.inc('rootWatcherCalls')
189
+ } else {
190
+ metrics.inc('workspaceWatcherCalls')
191
+ }
192
+ return originalEnsureWatcher.call(this, workspaceName, workspaceRoot, ...args)
193
+ }
194
+ WorkspaceStatusManager.prototype.ensureGitIgnoreEngine = async function instrumentedEnsureGitIgnoreEngine(...args) {
195
+ metrics.inc('gitIgnorePreScanCalls')
196
+ return originalEnsureGitIgnoreEngine.apply(this, args)
197
+ }
198
+
199
+ return metrics
200
+ }
201
+
202
+ async function runServerMode() {
203
+ const args = parseArgs()
204
+ const port = Number(args.port)
205
+ const home = path.resolve(String(args.home || ''))
206
+ const metrics = installInstrumentation(home)
207
+ const pkg = require(path.join(repoRoot, 'package.json'))
208
+ const Server = require(path.join(repoRoot, 'server'))
209
+ const server = new Server({
210
+ store: { store: { home, version: pkg.version } },
211
+ agent: 'test',
212
+ newsfeed: '',
213
+ portal: '',
214
+ })
215
+ server.port = port
216
+ await server.start({ debug: true })
217
+ server.app.get('/__startup-git-index-test/metrics', (req, res) => {
218
+ res.json({ ok: true, metrics: metrics.snapshot() })
219
+ })
220
+ server.app.post('/__startup-git-index-test/reset', (req, res) => {
221
+ metrics.reset()
222
+ res.json({ ok: true })
223
+ })
224
+ process.stdout.write(`${readyPrefix}${JSON.stringify({ port, home, pid: process.pid })}\n`)
225
+ setInterval(() => {}, 1000)
226
+ }
227
+
228
+ function waitForReady(child, timeoutMs = 60000) {
229
+ return new Promise((resolve, reject) => {
230
+ let stdout = ''
231
+ let stderr = ''
232
+ const timer = setTimeout(() => {
233
+ reject(new Error(`server did not become ready in ${timeoutMs}ms\nstdout:\n${stdout}\nstderr:\n${stderr}`))
234
+ }, timeoutMs)
235
+ child.stdout.on('data', (chunk) => {
236
+ const text = chunk.toString()
237
+ stdout += text
238
+ for (const line of text.split(/\r?\n/)) {
239
+ if (line.startsWith(readyPrefix)) {
240
+ clearTimeout(timer)
241
+ resolve({ ready: JSON.parse(line.slice(readyPrefix.length)), stdout, stderr })
242
+ }
243
+ }
244
+ })
245
+ child.stderr.on('data', (chunk) => {
246
+ stderr += chunk.toString()
247
+ })
248
+ child.on('exit', (code, signal) => {
249
+ clearTimeout(timer)
250
+ reject(new Error(`server exited before ready: code=${code} signal=${signal}\nstdout:\n${stdout}\nstderr:\n${stderr}`))
251
+ })
252
+ })
253
+ }
254
+
255
+ async function fetchJson(baseUrl, route, options = {}) {
256
+ const response = await fetch(`${baseUrl}${route}`, {
257
+ method: options.method || 'GET',
258
+ headers: options.body ? { 'content-type': 'application/json' } : undefined,
259
+ body: options.body ? JSON.stringify(options.body) : undefined,
260
+ })
261
+ const text = await response.text()
262
+ let json = null
263
+ try {
264
+ json = text ? JSON.parse(text) : null
265
+ } catch (_) {}
266
+ assert.ok(response.ok, `${route} failed with ${response.status}: ${text}`)
267
+ return json
268
+ }
269
+
270
+ async function sleep(ms) {
271
+ await new Promise((resolve) => setTimeout(resolve, ms))
272
+ }
273
+
274
+ async function withPerfServer(callback, options = {}) {
275
+ const fixture = await createFixtureHome()
276
+ const port = await freePort()
277
+ const env = {
278
+ ...process.env,
279
+ HOME: fixture.fakeUserHome,
280
+ PINOKIO_HOME: fixture.pinokioHome,
281
+ }
282
+ if (options.disableWatch) {
283
+ env.PINOKIO_DISABLE_WATCH = '1'
284
+ }
285
+ const child = childProcess.spawn(process.execPath, [
286
+ __filename,
287
+ '--server',
288
+ '--port',
289
+ String(port),
290
+ '--home',
291
+ fixture.pinokioHome,
292
+ ], {
293
+ cwd: repoRoot,
294
+ env,
295
+ detached: true,
296
+ stdio: ['ignore', 'pipe', 'pipe'],
297
+ })
298
+ const logs = []
299
+ child.stdout.on('data', (chunk) => logs.push(chunk.toString()))
300
+ child.stderr.on('data', (chunk) => logs.push(chunk.toString()))
301
+ try {
302
+ await waitForReady(child)
303
+ await callback({
304
+ ...fixture,
305
+ baseUrl: `http://127.0.0.1:${port}`,
306
+ })
307
+ } catch (error) {
308
+ error.message = `${error.message}\nserver log:\n${logs.join('')}`
309
+ throw error
310
+ } finally {
311
+ if (child.pid) {
312
+ try {
313
+ process.kill(-child.pid, 'SIGTERM')
314
+ } catch (_) {
315
+ child.kill('SIGTERM')
316
+ }
317
+ }
318
+ await new Promise((resolve) => {
319
+ const timer = setTimeout(resolve, 3000)
320
+ child.once('exit', () => {
321
+ clearTimeout(timer)
322
+ resolve()
323
+ })
324
+ })
325
+ await fsp.rm(fixture.root, { recursive: true, force: true })
326
+ }
327
+ }
328
+
329
+ if (!serverMode) test('startup path does not run global git index or root API watcher pre-scan', async () => {
330
+ await withPerfServer(async ({ baseUrl }) => {
331
+ await sleep(5000)
332
+ const { metrics } = await fetchJson(baseUrl, '/__startup-git-index-test/metrics')
333
+ assert.equal(metrics.gitIndexCalls, 0, 'startup must not call kernel.git.index()')
334
+ assert.equal(metrics.rootWatcherCalls, 0, 'startup must not call root ensureWatcher("api")')
335
+ assert.equal(metrics.apiRootReposCalls, 0, 'startup must not recursively scan the API root with kernel.git.repos(apiRoot)')
336
+ })
337
+ })
338
+
339
+ if (!serverMode) test('/info/gitstatus stays selected-workspace scoped and avoids whole API scans', async () => {
340
+ await withPerfServer(async ({ baseUrl, workspaceName, workspace }) => {
341
+ await sleep(5000)
342
+ await fetchJson(baseUrl, '/__startup-git-index-test/reset', { method: 'POST', body: {} })
343
+ await fsp.writeFile(path.join(workspace, 'tracked.txt'), 'top clean\ntop dirty\n')
344
+ await fsp.writeFile(path.join(workspace, 'tracked-hidden.txt'), 'hidden clean\nhidden dirty\n')
345
+ const status = await fetchJson(baseUrl, `/info/gitstatus/${workspaceName}`)
346
+ assert.equal(status.totalChanges, 1)
347
+ assert.equal(status.repos.length, 2)
348
+ assert.equal(
349
+ status.repos.some((repo) => Array.isArray(repo.changes) && repo.changes.some((change) => change.file === 'tracked-hidden.txt')),
350
+ false,
351
+ 'tracked paths hidden by .gitignore should not appear in /info/gitstatus'
352
+ )
353
+ const { metrics } = await fetchJson(baseUrl, '/__startup-git-index-test/metrics')
354
+ assert.equal(metrics.gitIgnorePreScanCalls, 0, '/info/gitstatus must not call recursive ensureGitIgnoreEngine()')
355
+ assert.equal(metrics.apiRootReposCalls, 0, '/info/gitstatus must not call kernel.git.repos(apiRoot)')
356
+ assert.ok(metrics.workspaceReposCalls >= 1, '/info/gitstatus should scan the selected workspace')
357
+ assert.ok(metrics.fs.readdir < 10000, `/info/gitstatus should stay below whole-tree scale; readdir=${metrics.fs.readdir}`)
358
+ }, { disableWatch: true })
359
+ })
360
+
361
+ if (!serverMode) test('/info/gitstatus first watched request avoids recursive gitignore pre-scan', async () => {
362
+ await withPerfServer(async ({ baseUrl, workspaceName, workspace }) => {
363
+ await sleep(5000)
364
+ await fetchJson(baseUrl, '/__startup-git-index-test/reset', { method: 'POST', body: {} })
365
+ await fsp.writeFile(path.join(workspace, 'tracked.txt'), 'top clean\ntop dirty\n')
366
+ await fsp.writeFile(path.join(workspace, 'tracked-hidden.txt'), 'hidden clean\nhidden dirty\n')
367
+ const status = await fetchJson(baseUrl, `/info/gitstatus/${workspaceName}`)
368
+ assert.equal(status.totalChanges, 1)
369
+ const { metrics } = await fetchJson(baseUrl, '/__startup-git-index-test/metrics')
370
+ assert.equal(metrics.gitIgnorePreScanCalls, 0, 'first watched /info/gitstatus request must not call recursive ensureGitIgnoreEngine()')
371
+ assert.ok(metrics.workspaceWatcherCalls >= 1, '/info/gitstatus should still initialize the selected workspace watcher')
372
+ })
373
+ })
374
+
375
+ if (serverMode) {
376
+ runServerMode().catch((error) => {
377
+ console.error(error && error.stack ? error.stack : error)
378
+ process.exitCode = 1
379
+ })
380
+ }