pinokiod 8.1.1 → 8.2.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.
@@ -0,0 +1,1761 @@
1
+ #!/usr/bin/env node
2
+ const assert = require('node:assert/strict')
3
+ const childProcess = require('node:child_process')
4
+ const crypto = require('node:crypto')
5
+ const fs = require('node:fs')
6
+ const fsp = require('node:fs/promises')
7
+ const http = require('node:http')
8
+ const net = require('node:net')
9
+ const os = require('node:os')
10
+ const path = require('node:path')
11
+
12
+ const repoRoot = path.resolve(__dirname, '..')
13
+ const outputRoot = path.resolve(repoRoot, 'output', 'playwright', 'nested-launcher-real-browser')
14
+ const readyPrefix = 'PINOKIO_NESTED_BROWSER_READY '
15
+ const resultPrefix = 'PINOKIO_NESTED_BROWSER_RESULT '
16
+ const appId = 'nested-browser-acceptance'
17
+ const inheritanceId = 'nested-browser-inheritance'
18
+ const nestedChildId = 'nested-child-regression'
19
+ const historicalSession = 'historical-session'
20
+ const historicalSentinel = 'HISTORICAL_NESTED_BROWSER_SENTINEL'
21
+ const currentSentinel = 'CURRENT_NESTED_BROWSER_SENTINEL'
22
+ const adversarialViewerSentinel = 'ADVERSARIAL_ROOT_LOG_VIEWER_SENTINEL'
23
+ const adversarialHelpSentinel = 'ADVERSARIAL_NESTED_GET_HELP_SENTINEL'
24
+ const runtimeSentinel = 'RUNTIME_NESTED_BROWSER_SENTINEL'
25
+ const runtimeMarker = '.pinokio-nested-acceptance-runtime.json'
26
+ const liveControls = {
27
+ root: {
28
+ slug: 'github-com-halr9000-pinokio-hello-world',
29
+ title: 'Hello World Gradio',
30
+ repo: 'https://github.com/halr9000/pinokio-hello-world',
31
+ folder: 'pinokio-hello-world.git',
32
+ layout: 'root'
33
+ },
34
+ nested: {
35
+ slug: 'github-com-bilawalsidhu-gods-eye-view',
36
+ title: "God's Eye View",
37
+ repo: 'https://github.com/bilawalsidhu/gods-eye-view',
38
+ folder: 'gods-eye-view.git',
39
+ layout: 'nested'
40
+ }
41
+ }
42
+
43
+ function parseArgs(argv = process.argv.slice(2)) {
44
+ const args = {}
45
+ for (let index = 0; index < argv.length; index += 1) {
46
+ const item = argv[index]
47
+ if (!item.startsWith('--')) continue
48
+ const key = item.slice(2)
49
+ const next = argv[index + 1]
50
+ if (next && !next.startsWith('--')) {
51
+ args[key] = next
52
+ index += 1
53
+ } else {
54
+ args[key] = true
55
+ }
56
+ }
57
+ return args
58
+ }
59
+
60
+ function run(command, args, options = {}) {
61
+ return childProcess.execFileSync(command, args, { encoding: 'utf8', ...options }).trim()
62
+ }
63
+
64
+ async function copyDirectory(source, target) {
65
+ await fsp.mkdir(path.dirname(target), { recursive: true })
66
+ if (process.platform === 'darwin') {
67
+ childProcess.execFileSync('/bin/cp', ['-cR', source, target])
68
+ } else {
69
+ await fsp.cp(source, target, { recursive: true, preserveTimestamps: true, dereference: false, verbatimSymlinks: true })
70
+ }
71
+ }
72
+
73
+ function safeRuntimePath(value, label) {
74
+ const target = path.resolve(String(value || ''))
75
+ const forbidden = new Set([path.parse(target).root, os.homedir(), repoRoot])
76
+ assert.equal(forbidden.has(target), false, `${label} must not be a filesystem root, the user home, or the repository`)
77
+ return target
78
+ }
79
+
80
+ async function runtimeManifest(root) {
81
+ const entries = []
82
+ async function visit(directory, relative = '') {
83
+ const names = await fsp.readdir(directory)
84
+ names.sort()
85
+ for (const name of names) {
86
+ const absolute = path.resolve(directory, name)
87
+ const itemPath = relative ? `${relative}/${name}` : name
88
+ const stat = await fsp.lstat(absolute)
89
+ if (stat.isSymbolicLink()) {
90
+ entries.push({ path: itemPath, type: 'symlink', target: await fsp.readlink(absolute), mtime_ms: stat.mtimeMs })
91
+ } else if (stat.isDirectory()) {
92
+ entries.push({ path: itemPath, type: 'directory', mtime_ms: stat.mtimeMs })
93
+ await visit(absolute, itemPath)
94
+ } else if (stat.isFile()) {
95
+ const hash = crypto.createHash('sha256')
96
+ await new Promise((resolve, reject) => {
97
+ const stream = fs.createReadStream(absolute)
98
+ stream.on('data', chunk => hash.update(chunk))
99
+ stream.on('error', reject)
100
+ stream.on('end', resolve)
101
+ })
102
+ entries.push({ path: itemPath, type: 'file', size: stat.size, sha256: hash.digest('hex'), mtime_ms: stat.mtimeMs })
103
+ } else {
104
+ entries.push({ path: itemPath, type: 'other', size: stat.size, mtime_ms: stat.mtimeMs })
105
+ }
106
+ }
107
+ }
108
+ await visit(root)
109
+ return entries
110
+ }
111
+
112
+ async function prepareRuntime({ active, snapshot, createSnapshot }) {
113
+ const activeRoot = safeRuntimePath(active, 'runtime active path')
114
+ const snapshotRoot = safeRuntimePath(snapshot, 'runtime snapshot path')
115
+ assert.notEqual(activeRoot, snapshotRoot, 'runtime active and snapshot paths must differ')
116
+ assert.equal(activeRoot.startsWith(`${snapshotRoot}${path.sep}`) || snapshotRoot.startsWith(`${activeRoot}${path.sep}`), false, 'runtime active and snapshot paths must not contain one another')
117
+ const condaRelative = path.join('bin', 'miniforge', 'bin', process.platform === 'win32' ? 'conda.exe' : 'conda')
118
+ assert.equal(fs.existsSync(path.resolve(activeRoot, condaRelative)), true, `runtime is missing ${condaRelative}`)
119
+ if (!fs.existsSync(snapshotRoot)) {
120
+ assert.equal(createSnapshot, true, 'runtime snapshot does not exist; pass --create-runtime-snapshot once to create it')
121
+ await fsp.writeFile(path.resolve(activeRoot, runtimeMarker), JSON.stringify({ active: activeRoot, created_at: new Date().toISOString() }, null, 2))
122
+ await copyDirectory(activeRoot, snapshotRoot)
123
+ }
124
+ const marker = JSON.parse(await fsp.readFile(path.resolve(snapshotRoot, runtimeMarker), 'utf8'))
125
+ assert.equal(marker.active, activeRoot, 'runtime snapshot was provisioned for a different absolute active path')
126
+ assert.equal(fs.existsSync(path.resolve(snapshotRoot, condaRelative)), true, 'runtime snapshot is incomplete')
127
+ const manifest = await runtimeManifest(snapshotRoot)
128
+ async function restore() {
129
+ if (fs.existsSync(activeRoot)) {
130
+ const activeMarker = JSON.parse(await fsp.readFile(path.resolve(activeRoot, runtimeMarker), 'utf8'))
131
+ assert.equal(activeMarker.active, activeRoot, 'refusing to replace an unrecognized runtime directory')
132
+ await fsp.rm(activeRoot, { recursive: true, force: true })
133
+ }
134
+ await copyDirectory(snapshotRoot, activeRoot)
135
+ const restoredMarker = JSON.parse(await fsp.readFile(path.resolve(activeRoot, runtimeMarker), 'utf8'))
136
+ assert.equal(restoredMarker.active, activeRoot, 'restored runtime marker does not match its path')
137
+ const conda = path.resolve(activeRoot, condaRelative)
138
+ const version = run(conda, ['--version'])
139
+ assert.match(version, /^conda\s+\S+/, 'restored runtime failed its conda smoke check')
140
+ const compatibilityAlias = path.resolve(activeRoot, 'bin', 'miniconda')
141
+ if (fs.existsSync(compatibilityAlias) && (await fsp.lstat(compatibilityAlias)).isSymbolicLink()) {
142
+ const target = await fsp.readlink(compatibilityAlias)
143
+ assert.equal(path.isAbsolute(target), true, 'runtime compatibility alias must use its provisioned absolute path')
144
+ assert.equal(target === activeRoot || target.startsWith(`${activeRoot}${path.sep}`), true, 'runtime compatibility alias escapes the active runtime')
145
+ }
146
+ const condaHeader = (await fsp.readFile(conda)).subarray(0, 512).toString('utf8')
147
+ if (condaHeader.startsWith('#!')) assert.equal(condaHeader.split(/\r?\n/, 1)[0].includes(activeRoot), true, 'conda shebang points outside the active runtime')
148
+ const bin = path.resolve(activeRoot, 'bin')
149
+ return {
150
+ root: activeRoot,
151
+ bin,
152
+ conda: version,
153
+ node: run(path.resolve(bin, 'miniforge', 'bin', process.platform === 'win32' ? 'node.exe' : 'node'), ['--version']),
154
+ git: run(path.resolve(bin, 'miniforge', 'bin', process.platform === 'win32' ? 'git.exe' : 'git'), ['--version']),
155
+ uv: run(path.resolve(bin, 'miniforge', 'bin', process.platform === 'win32' ? 'uv.exe' : 'uv'), ['--version'])
156
+ }
157
+ }
158
+ async function verifySnapshot() {
159
+ assert.deepEqual(await runtimeManifest(snapshotRoot), manifest, 'the pristine runtime snapshot was modified')
160
+ }
161
+ return { activeRoot, snapshotRoot, manifest, restore, verifySnapshot }
162
+ }
163
+
164
+ function loadPlaywright() {
165
+ try {
166
+ return require('playwright')
167
+ } catch (_) {}
168
+ try {
169
+ const locator = process.platform === 'win32' ? 'where' : 'which'
170
+ const bin = run(locator, ['playwright']).split(/\r?\n/)[0]
171
+ return require(path.join(path.dirname(path.dirname(bin)), 'playwright'))
172
+ } catch (_) {}
173
+ throw new Error('Playwright is required. Run with: npx --yes --package playwright node test/nested-launcher-real-browser.js')
174
+ }
175
+
176
+ function browserExecutable(playwright) {
177
+ const candidates = [
178
+ process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,
179
+ process.env.BRAVE_EXECUTABLE,
180
+ process.platform === 'darwin' ? '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser' : '',
181
+ process.platform === 'darwin' ? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' : '',
182
+ process.platform === 'win32' && process.env.PROGRAMFILES ? path.join(process.env.PROGRAMFILES, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe') : '',
183
+ process.platform === 'win32' && process.env['PROGRAMFILES(X86)'] ? path.join(process.env['PROGRAMFILES(X86)'], 'Google', 'Chrome', 'Application', 'chrome.exe') : '',
184
+ '/usr/bin/brave-browser',
185
+ '/usr/bin/google-chrome',
186
+ '/usr/bin/chromium',
187
+ playwright.chromium.executablePath()
188
+ ].filter(Boolean)
189
+ return candidates.find(candidate => fs.existsSync(candidate)) || ''
190
+ }
191
+
192
+ async function freePort() {
193
+ return await new Promise((resolve, reject) => {
194
+ const server = net.createServer()
195
+ server.on('error', reject)
196
+ server.listen(0, '127.0.0.1', () => {
197
+ const port = server.address().port
198
+ server.close(() => resolve(port))
199
+ })
200
+ })
201
+ }
202
+
203
+ async function write(root, relative, content = '') {
204
+ const target = path.resolve(root, relative)
205
+ await fsp.mkdir(path.dirname(target), { recursive: true })
206
+ await fsp.writeFile(target, content)
207
+ return target
208
+ }
209
+
210
+ function script(runSteps, daemon = false) {
211
+ return `module.exports = ${JSON.stringify({ daemon, run: runSteps }, null, 2)}\n`
212
+ }
213
+
214
+ function launcherModule(layout) {
215
+ const prefix = layout === 'nested' ? 'pinokio/' : ''
216
+ const runCommand = `node -e "require('fs').writeFileSync('run-cwd.txt', process.cwd())"`
217
+ return `module.exports = {
218
+ version: "1.0.0",
219
+ title: "Nested Browser Acceptance",
220
+ description: "Controlled nested-launcher browser fixture.",
221
+ icon: "icon.png",
222
+ pre: [
223
+ { title: "Local preflight", icon: "pre.png", href: "setup.js" },
224
+ { title: "External preflight", href: "https://example.com/preflight" }
225
+ ],
226
+ menu: [
227
+ { default: true, text: "Start relative", href: "start.js" },
228
+ { text: "Launcher child", href: "child/tool.js" },
229
+ { text: "Absolute launcher", href: "/api/${appId}/${prefix}absolute.js" },
230
+ { text: "Absolute workspace", href: "/api/${appId}/workspace.js" },
231
+ { text: "Run string", run: ${JSON.stringify(runCommand)} },
232
+ { text: "Stop relative", action: { method: "stop", uri: "start.js" } },
233
+ { text: "State marker", when: "start.js", on: "STATE_ON", off: "STATE_OFF" }
234
+ ],
235
+ shortcuts: [
236
+ { text: "Shortcut stop", action: { method: "stop", uri: "start.js" } }
237
+ ]
238
+ }\n`
239
+ }
240
+
241
+ const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=', 'base64')
242
+
243
+ async function writePrimaryFixture(pinokioHome, layout) {
244
+ const workspace = path.resolve(pinokioHome, 'api', appId)
245
+ const launcher = layout === 'nested' ? path.resolve(workspace, 'pinokio') : workspace
246
+ await fsp.mkdir(launcher, { recursive: true })
247
+ await write(launcher, 'pinokio.js', launcherModule(layout))
248
+ await write(launcher, 'pinokio.json', JSON.stringify({
249
+ title: 'Nested Browser Acceptance',
250
+ description: 'Controlled nested-launcher browser fixture.',
251
+ icon: 'icon.png'
252
+ }, null, 2))
253
+ await write(launcher, 'icon.png', png)
254
+ await write(launcher, 'pre.png', png)
255
+ await write(launcher, 'install.js', script([{ method: 'local.set', params: { installed: true } }]))
256
+ await write(launcher, 'start.js', script([
257
+ { method: 'process.wait', params: { sec: 20 } },
258
+ { method: 'local.set', params: { started: true, search: 'SCRIPTSEARCHSENTINEL' } }
259
+ ], true))
260
+ await write(launcher, 'env-set.js', script([{ method: 'env.set', params: { ACTION_VALUE: 'set-by-action' } }]))
261
+ await write(launcher, 'env-switch.js', script([{ method: 'env.switch', params: { SWITCH_VALUE: ['one', 'two'] } }]))
262
+ await write(launcher, 'setup.js', script([{ method: 'local.set', params: { setup: true } }]))
263
+ await write(launcher, 'absolute.js', script([{ method: 'local.set', params: { absolute: true } }]))
264
+ await write(launcher, 'child/tool.js', script([{ method: 'local.set', params: { child: true } }]))
265
+ await write(launcher, 'runtime-probe.js', `const fs = require('fs')
266
+ const result = { sentinel: ${JSON.stringify(runtimeSentinel)}, cwd: process.cwd(), relative: process.env.RELATIVE_VALUE }
267
+ console.log(${JSON.stringify(runtimeSentinel)}, JSON.stringify(result))
268
+ fs.writeFileSync('runtime-result.json', JSON.stringify(result, null, 2))
269
+ `)
270
+ await write(launcher, 'runtime.js', script([{ method: 'shell.run', params: { message: 'node runtime-probe.js' } }]))
271
+ if (layout === 'nested') {
272
+ await write(launcher, 'README.md', 'LAUNCHERREADMESENTINEL\n')
273
+ await write(launcher, 'CLAUDE.md', 'LAUNCHERAISENTINEL\n')
274
+ }
275
+ await write(launcher, '_ENVIRONMENT', 'TEMPLATE_SENTINEL=primary-template\n')
276
+ await write(launcher, 'ENVIRONMENT', [
277
+ 'REQUIRED_VALUE=initial',
278
+ 'ACTION_VALUE=initial',
279
+ 'SWITCH_VALUE=one',
280
+ 'RELATIVE_VALUE=./something',
281
+ 'PINOKIO_SHARE_CLOUDFLARE=false',
282
+ 'PINOKIO_SHARE_LOCAL=false',
283
+ 'PINOKIO_SHARE_LOCAL_PORT=',
284
+ ''
285
+ ].join('\n'))
286
+ await write(workspace, 'workspace.js', script([{ method: 'local.set', params: { workspace: true } }]))
287
+ await write(workspace, 'README.md', layout === 'nested'
288
+ ? 'WORKSPACEREADMESENTINEL\n'
289
+ : 'WORKSPACEREADMESENTINEL\nLAUNCHERREADMESENTINEL\n')
290
+ await write(workspace, 'CLAUDE.md', layout === 'nested'
291
+ ? 'WORKSPACEAISENTINEL\n'
292
+ : 'WORKSPACEAISENTINEL\nLAUNCHERAISENTINEL\n')
293
+ await write(workspace, '.git/config', '[remote "origin"]\n\turl = https://github.com/pinokio/acceptance-fixture.git\n')
294
+ await write(workspace, '.git/info/exclude', 'existing-entry\n')
295
+
296
+ const logRoot = path.resolve(launcher, 'logs')
297
+ await write(logRoot, 'api/seeded.js/latest', `${currentSentinel}\n`)
298
+ const historicalLog = await write(logRoot, 'api/start.js/1000', `${historicalSentinel}\n`)
299
+ await write(logRoot, 'dev/latest', 'SEEDED_DEV_LOG\n')
300
+ await write(logRoot, 'shell/latest', 'SEEDED_SHELL_LOG\n')
301
+ await write(logRoot, 'sessions/index.json', JSON.stringify({
302
+ version: 1,
303
+ latest_session: historicalSession,
304
+ sessions: [{ id: historicalSession, created_at: '2026-01-01T00:00:00.000Z', updated_at: '2026-01-01T00:00:00.000Z', runs: ['start.js'] }]
305
+ }, null, 2))
306
+ await write(logRoot, `sessions/${historicalSession}.json`, JSON.stringify({
307
+ version: 1,
308
+ id: historicalSession,
309
+ created_at: '2026-01-01T00:00:00.000Z',
310
+ updated_at: '2026-01-01T00:00:00.000Z',
311
+ runs: [{
312
+ script: 'start.js',
313
+ started_at: '2026-01-01T00:00:00.000Z',
314
+ ended_at: '2026-01-01T00:00:01.000Z',
315
+ logs: [{ path: path.relative(launcher, historicalLog).split(path.sep).join('/') }]
316
+ }]
317
+ }, null, 2))
318
+ return { workspace, launcher }
319
+ }
320
+
321
+ async function writeInheritanceFixture(pinokioHome, layout) {
322
+ const workspace = path.resolve(pinokioHome, 'api', inheritanceId)
323
+ const launcher = layout === 'nested' ? path.resolve(workspace, 'pinokio') : workspace
324
+ await write(launcher, 'pinokio.js', `module.exports = { title: "Inheritance Fixture", menu: [{ default: true, text: "Start", href: "start.js" }] }\n`)
325
+ await write(launcher, 'start.js', script([{ method: 'local.set', params: { inherited: true } }]))
326
+ await write(launcher, '_ENVIRONMENT', 'INHERITANCE_UNIQUE_SENTINEL=yes\n')
327
+ await write(workspace, '.git/info/exclude', 'existing-entry\n')
328
+ return { workspace, launcher }
329
+ }
330
+
331
+ async function writeNestedChildFixture(pinokioHome, layout) {
332
+ if (layout !== 'nested') return null
333
+ const workspace = path.resolve(pinokioHome, 'api', nestedChildId)
334
+ const launcher = path.resolve(workspace, 'pinokio')
335
+ await write(launcher, 'pinokio.js', `module.exports = {
336
+ title: "Nested Child Regression",
337
+ menu: [
338
+ { text: "Start nested child", href: "pinokio/tool.js" },
339
+ { text: "Stop nested child", action: { method: "stop", uri: "pinokio/tool.js" } }
340
+ ]
341
+ }\n`)
342
+ await write(launcher, 'pinokio/tool.js', script([{ method: 'process.wait', params: { sec: 20 } }], true))
343
+ await write(launcher, 'ENVIRONMENT', '')
344
+ return { workspace, launcher }
345
+ }
346
+
347
+ async function writeAdversarialFixtures(pinokioHome) {
348
+ const rootFolder = path.resolve(pinokioHome, 'api', 'root-with-folder')
349
+ await write(rootFolder, 'pinokio.js', `module.exports = { title: "Root With Folder", menu: [{ text: "Root start", href: "start.js" }] }\n`)
350
+ await write(rootFolder, 'start.js', script([{ method: 'process.wait', params: { sec: 20 } }], true))
351
+ await write(rootFolder, 'install.js', script([{ method: 'local.set', params: { installed: true } }]))
352
+ await write(rootFolder, 'logs/api/seeded.js/latest', `${adversarialViewerSentinel}\n`)
353
+ await write(rootFolder, 'pinokio/unrelated.txt', 'unrelated')
354
+ const nestedLog = await write(rootFolder, 'pinokio/logs/api/help.js/1000', `${adversarialHelpSentinel}\n`)
355
+ await write(rootFolder, 'pinokio/logs/sessions/index.json', JSON.stringify({
356
+ version: 1,
357
+ latest_session: 'adversarial-help-session',
358
+ sessions: [{ id: 'adversarial-help-session', created_at: '2026-01-01T00:00:00.000Z', updated_at: '2026-01-01T00:00:01.000Z', runs: ['help.js'] }]
359
+ }, null, 2))
360
+ await write(rootFolder, 'pinokio/logs/sessions/adversarial-help-session.json', JSON.stringify({
361
+ version: 1,
362
+ id: 'adversarial-help-session',
363
+ created_at: '2026-01-01T00:00:00.000Z',
364
+ updated_at: '2026-01-01T00:00:01.000Z',
365
+ runs: [{ script: 'help.js', started_at: '2026-01-01T00:00:00.000Z', ended_at: '2026-01-01T00:00:01.000Z', logs: [{ path: path.relative(path.resolve(rootFolder, 'pinokio'), nestedLog).split(path.sep).join('/') }] }]
366
+ }, null, 2))
367
+ await write(rootFolder, '.git/config', '[remote "origin"]\n\turl = https://github.com/pinokio/root-with-folder.git\n')
368
+
369
+ const both = path.resolve(pinokioHome, 'api', 'both-launchers')
370
+ await write(both, 'pinokio.js', `module.exports = { title: "Root Winner", menu: [{ text: "Root winner start", href: "start.js" }] }\n`)
371
+ await write(both, 'start.js', script([{ method: 'process.wait', params: { sec: 20 } }], true))
372
+ await write(both, 'install.js', script([{ method: 'local.set', params: { installed: 'root' } }]))
373
+ await write(both, 'pinokio/pinokio.js', `module.exports = { title: "Nested Loser", menu: [{ text: "Nested loser start", href: "start.js" }] }\n`)
374
+ await write(both, 'pinokio/start.js', script([{ method: 'local.set', params: { started: 'nested' } }]))
375
+ await write(both, 'pinokio/install.js', script([{ method: 'local.set', params: { installed: 'nested' } }]))
376
+
377
+ const broken = path.resolve(pinokioHome, 'api', 'broken-root')
378
+ await write(broken, 'pinokio.js', 'throw new Error("intentional broken root")\n')
379
+ await write(broken, 'pinokio/pinokio.js', `module.exports = {
380
+ title: "Legacy Nested Fallback",
381
+ menu: [
382
+ { text: "Fallback start", href: "start.js" },
383
+ { text: "Fallback fs", href: "file.txt", fs: true },
384
+ { text: "Fallback command", href: "file.txt", command: "open" },
385
+ { text: "Fallback run", run: "printf fallback" },
386
+ { text: "Fallback stop", action: { method: "stop", uri: "start.js" } },
387
+ { text: "Fallback state", when: "start.js", on: "FALLBACK_ON", off: "FALLBACK_OFF" }
388
+ ]
389
+ }\n`)
390
+ await write(broken, 'pinokio/start.js', script([{ method: 'process.wait', params: { sec: 20 } }], true))
391
+ await write(broken, 'pinokio/file.txt', 'nested fallback file\n')
392
+
393
+ await write(path.resolve(pinokioHome, 'api', 'empty-pinokio-folder'), 'pinokio/unrelated.txt', 'empty')
394
+ await write(path.resolve(pinokioHome, 'api', 'plain-pinokio-file'), 'pinokio', 'plain file')
395
+ await fsp.mkdir(path.resolve(pinokioHome, 'api', 'no-launcher'), { recursive: true })
396
+ await write(path.resolve(pinokioHome, 'api', 'pinokio'), 'pinokio.js', `module.exports = { title: "App Named Pinokio", menu: [] }\n`)
397
+ await write(path.resolve(pinokioHome, 'api', 'subfolder-project'), 'pinokio.js', `module.exports = { title: "Subfolder Parent", menu: [] }\n`)
398
+ await write(path.resolve(pinokioHome, 'api', 'subfolder-project'), 'child/pinokio.js', `module.exports = { title: "Subfolder Child", icon: "child.png", menu: [{ text: "Child start", href: "start.js" }] }\n`)
399
+ await write(path.resolve(pinokioHome, 'api', 'subfolder-project'), 'child/start.js', script([{ method: 'local.set', params: { child: true } }]))
400
+ await write(path.resolve(pinokioHome, 'api', 'subfolder-project'), 'child/child.png', png)
401
+ }
402
+
403
+ async function createHome(layout, runtimeBin) {
404
+ const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'pinokio-nested-browser-'))
405
+ const fakeUserHome = path.resolve(root, 'user')
406
+ const pinokioHome = path.resolve(root, 'pinokio-home')
407
+ await fsp.mkdir(path.resolve(pinokioHome, 'api'), { recursive: true })
408
+ await fsp.mkdir(fakeUserHome, { recursive: true })
409
+ if (runtimeBin) {
410
+ await fsp.symlink(runtimeBin, path.resolve(pinokioHome, 'bin'), process.platform === 'win32' ? 'junction' : 'dir')
411
+ await write(pinokioHome, 'prototype/system/.acceptance-fixture', '')
412
+ await write(pinokioHome, 'prototype/PINOKIO.md', 'Acceptance fixture\n')
413
+ await write(pinokioHome, 'prototype/PTERM.md', 'Acceptance fixture\n')
414
+ }
415
+ if (process.platform === 'win32') {
416
+ await fsp.mkdir(path.resolve(fakeUserHome, 'AppData', 'Roaming'), { recursive: true })
417
+ await fsp.mkdir(path.resolve(fakeUserHome, 'AppData', 'Local'), { recursive: true })
418
+ }
419
+ const primary = await writePrimaryFixture(pinokioHome, layout)
420
+ const inheritance = await writeInheritanceFixture(pinokioHome, layout)
421
+ const nestedChild = await writeNestedChildFixture(pinokioHome, layout)
422
+ await writeAdversarialFixtures(pinokioHome)
423
+ return { root, fakeUserHome, pinokioHome, primary, inheritance, nestedChild }
424
+ }
425
+
426
+ async function createLiveHome(runtimeBin) {
427
+ const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'pinokio-nested-live-'))
428
+ const fakeUserHome = path.resolve(root, 'user')
429
+ const pinokioHome = path.resolve(root, 'pinokio-home')
430
+ await fsp.mkdir(path.resolve(pinokioHome, 'api'), { recursive: true })
431
+ await fsp.mkdir(fakeUserHome, { recursive: true })
432
+ if (process.platform === 'win32') {
433
+ await fsp.mkdir(path.resolve(fakeUserHome, 'AppData', 'Roaming'), { recursive: true })
434
+ await fsp.mkdir(path.resolve(fakeUserHome, 'AppData', 'Local'), { recursive: true })
435
+ }
436
+ await fsp.symlink(runtimeBin, path.resolve(pinokioHome, 'bin'), process.platform === 'win32' ? 'junction' : 'dir')
437
+ await write(pinokioHome, 'prototype/system/.acceptance-fixture', '')
438
+ await write(pinokioHome, 'prototype/PINOKIO.md', 'Acceptance fixture\n')
439
+ await write(pinokioHome, 'prototype/PTERM.md', 'Acceptance fixture\n')
440
+ return { root, fakeUserHome, pinokioHome }
441
+ }
442
+
443
+ async function runServerMode() {
444
+ const args = parseArgs()
445
+ const checkout = path.resolve(String(args.checkout || ''))
446
+ const home = path.resolve(String(args.home || ''))
447
+ const port = Number(args.port)
448
+ process.chdir(checkout)
449
+ const pkg = require(path.resolve(checkout, 'package.json'))
450
+ const Server = require(path.resolve(checkout, 'server'))
451
+ const server = new Server({
452
+ store: { store: { home, version: pkg.version } },
453
+ agent: 'test',
454
+ newsfeed: '',
455
+ site: 'https://pinokio.co',
456
+ discover_dark: 'https://pinokio.co?embed=1&theme=dark',
457
+ discover_light: 'https://pinokio.co?embed=1&theme=light',
458
+ portal: 'https://pinokio.co'
459
+ })
460
+ server.port = port
461
+ await server.start({ debug: true })
462
+ // The acceptance fixtures need the real app UI, not the unrelated full-dev
463
+ // setup gate. Runtime-backed cases still execute through the real Conda shell.
464
+ server.kernel.bin.check = async () => ({
465
+ requirements: [],
466
+ install_required: false,
467
+ requirements_pending: false,
468
+ error: null
469
+ })
470
+ process.stdout.write(`${readyPrefix}${JSON.stringify({ checkout, home, port, pid: process.pid })}\n`)
471
+ setInterval(() => {}, 1000)
472
+ }
473
+
474
+ async function waitForReady(child, timeoutMs = 90000) {
475
+ return await new Promise((resolve, reject) => {
476
+ let stdout = ''
477
+ let stderr = ''
478
+ const timer = setTimeout(() => reject(new Error(`server readiness timed out\nstdout:\n${stdout}\nstderr:\n${stderr}`)), timeoutMs)
479
+ const onExit = (code, signal) => {
480
+ clearTimeout(timer)
481
+ reject(new Error(`server exited before ready: code=${code} signal=${signal}\nstdout:\n${stdout}\nstderr:\n${stderr}`))
482
+ }
483
+ child.once('exit', onExit)
484
+ child.stdout.on('data', chunk => {
485
+ const text = chunk.toString()
486
+ stdout += text
487
+ for (const line of text.split(/\r?\n/)) {
488
+ if (!line.startsWith(readyPrefix)) continue
489
+ clearTimeout(timer)
490
+ child.off('exit', onExit)
491
+ resolve({ ready: JSON.parse(line.slice(readyPrefix.length)), stdout, stderr })
492
+ }
493
+ })
494
+ child.stderr.on('data', chunk => { stderr += chunk.toString() })
495
+ })
496
+ }
497
+
498
+ async function stopChild(child) {
499
+ if (!child || child.exitCode !== null) return
500
+ child.kill('SIGTERM')
501
+ await new Promise(resolve => {
502
+ const timer = setTimeout(resolve, 3000)
503
+ child.once('exit', () => {
504
+ clearTimeout(timer)
505
+ resolve()
506
+ })
507
+ })
508
+ if (child.exitCode === null) child.kill('SIGKILL')
509
+ }
510
+
511
+ async function waitForHttp(url, timeoutMs = 45000) {
512
+ const started = Date.now()
513
+ while (Date.now() - started < timeoutMs) {
514
+ try {
515
+ const response = await fetch(url)
516
+ if (response.ok) return
517
+ } catch (_) {}
518
+ await new Promise(resolve => setTimeout(resolve, 200))
519
+ }
520
+ throw new Error(`timed out waiting for ${url}`)
521
+ }
522
+
523
+ async function waitForFile(file, predicate, timeoutMs = 15000) {
524
+ const started = Date.now()
525
+ while (Date.now() - started < timeoutMs) {
526
+ try {
527
+ const content = await fsp.readFile(file, 'utf8')
528
+ if (predicate(content)) return content
529
+ } catch (_) {}
530
+ await new Promise(resolve => setTimeout(resolve, 150))
531
+ }
532
+ throw new Error(`timed out waiting for ${file}`)
533
+ }
534
+
535
+ async function waitForOptionalFile(file, predicate, timeoutMs = 15000) {
536
+ try {
537
+ return await waitForFile(file, predicate, timeoutMs)
538
+ } catch (_) {
539
+ return null
540
+ }
541
+ }
542
+
543
+ async function readOptional(file) {
544
+ try {
545
+ return await fsp.readFile(file, 'utf8')
546
+ } catch (error) {
547
+ if (error.code === 'ENOENT') return null
548
+ throw error
549
+ }
550
+ }
551
+
552
+ async function waitForBodyText(page, value, timeoutMs = 15000) {
553
+ const started = Date.now()
554
+ while (Date.now() - started < timeoutMs) {
555
+ const body = await page.locator('body').innerText().catch(() => '')
556
+ if (body.includes(value)) return true
557
+ await page.waitForTimeout(150)
558
+ }
559
+ return false
560
+ }
561
+
562
+ async function startRegistryStub() {
563
+ const received = []
564
+ const server = http.createServer((req, res) => {
565
+ const chunks = []
566
+ req.on('data', chunk => chunks.push(chunk))
567
+ req.on('end', () => {
568
+ const body = Buffer.concat(chunks).toString('utf8')
569
+ const match = body.match(/name="metadata_b64"\r\n\r\n([^\r\n]+)/)
570
+ let metadata = null
571
+ if (match) {
572
+ try { metadata = JSON.parse(Buffer.from(match[1], 'base64').toString('utf8')) } catch (_) {}
573
+ }
574
+ received.push({ method: req.method, url: req.url, authorization: req.headers.authorization || '', metadata })
575
+ res.writeHead(200, { 'Content-Type': 'application/json' })
576
+ res.end(JSON.stringify({ ok: true, draft: 'local-stub' }))
577
+ })
578
+ })
579
+ await new Promise((resolve, reject) => {
580
+ server.once('error', reject)
581
+ server.listen(0, '127.0.0.1', resolve)
582
+ })
583
+ return {
584
+ url: `http://127.0.0.1:${server.address().port}`,
585
+ received,
586
+ close: () => new Promise(resolve => server.close(resolve))
587
+ }
588
+ }
589
+
590
+ async function browserRequest(page, pathname, options = {}) {
591
+ for (let attempt = 0; attempt < 3; attempt += 1) {
592
+ try {
593
+ return await page.evaluate(async ({ pathname, options }) => {
594
+ const response = await fetch(pathname, options)
595
+ const text = await response.text()
596
+ let json = null
597
+ try { json = JSON.parse(text) } catch (_) {}
598
+ return { status: response.status, ok: response.ok, text, json }
599
+ }, { pathname, options })
600
+ } catch (error) {
601
+ const interrupted = /Execution context was destroyed|Cannot find context with specified id|Inspected target navigated or closed/i.test(String(error && error.message || error))
602
+ if (!interrupted || attempt === 2) throw error
603
+ await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {})
604
+ }
605
+ }
606
+ }
607
+
608
+ async function waitForBrowserRequest(page, pathname, predicate, timeoutMs = 15000) {
609
+ const started = Date.now()
610
+ let response
611
+ while (Date.now() - started < timeoutMs) {
612
+ response = await browserRequest(page, pathname)
613
+ if (predicate(response)) return response
614
+ await page.waitForTimeout(150)
615
+ }
616
+ return response
617
+ }
618
+
619
+ function stableStatus(payload) {
620
+ if (!payload) return null
621
+ return {
622
+ app_id: payload.app_id,
623
+ title: payload.title,
624
+ icon: payload.icon,
625
+ install_script: payload.install_script,
626
+ start_script: payload.start_script,
627
+ default_script: payload.default_script,
628
+ default_target: payload.default_target,
629
+ state: payload.state
630
+ }
631
+ }
632
+
633
+ async function menuSnapshot(page, selector = '.m.h.menu > .frame-link') {
634
+ return await page.locator(selector).evaluateAll(nodes => nodes.map(node => {
635
+ let action = null
636
+ try { action = node.dataset.action ? JSON.parse(node.dataset.action) : null } catch (_) {}
637
+ return {
638
+ text: (node.innerText || '').replace(/\s+/g, ' ').trim(),
639
+ href: node.getAttribute('href'),
640
+ run: node.dataset.run || null,
641
+ cwd: node.dataset.cwd || null,
642
+ command: node.dataset.command || null,
643
+ filepath: node.dataset.filepath || null,
644
+ action,
645
+ running: node.classList.contains('running') || !!node.querySelector('.fa-circle')
646
+ }
647
+ }).filter(item => !/^Settings/.test(item.text)))
648
+ }
649
+
650
+ async function adversarialPageSnapshot(page, baseUrl, id) {
651
+ const navigation = await goto(page, baseUrl, `/p/${id}`)
652
+ const body = await page.locator('body').innerText().catch(() => '')
653
+ const status = await browserRequest(page, `/apps/status/${id}`)
654
+ return {
655
+ navigation,
656
+ markers: ['Root With Folder', 'Root Winner', 'Nested Loser', 'Legacy Nested Fallback', 'App Named Pinokio']
657
+ .filter(marker => body.includes(marker)),
658
+ menu: await page.locator('.m.h.menu').count() ? await menuSnapshot(page) : [],
659
+ status: { status: status.status, value: stableStatus(status.json) }
660
+ }
661
+ }
662
+
663
+ async function adversarialRouteSnapshot(page, pathname, markers = []) {
664
+ const response = await browserRequest(page, pathname)
665
+ return {
666
+ status: response.status,
667
+ markers: markers.filter(marker => response.text.includes(marker))
668
+ }
669
+ }
670
+
671
+ async function exerciseAdversarial(page, baseUrl) {
672
+ const rootWithFolder = await adversarialPageSnapshot(page, baseUrl, 'root-with-folder')
673
+ const viewer = await browserRequest(page, '/apps/logs/root-with-folder?script=seeded.js')
674
+ const help = await browserRequest(page, '/apps/logs/root-with-folder/report?session=adversarial-help-session&redaction=none')
675
+ rootWithFolder.logViewer = {
676
+ status: viewer.status,
677
+ script: viewer.json && viewer.json.script,
678
+ hasRootSentinel: !!(viewer.json && String(viewer.json.text || '').includes(adversarialViewerSentinel)),
679
+ hasNestedSentinel: !!(viewer.json && String(viewer.json.text || '').includes(adversarialHelpSentinel))
680
+ }
681
+ rootWithFolder.getHelp = {
682
+ status: help.status,
683
+ hasRootSentinel: !!(help.json && String(help.json.markdown || '').includes(adversarialViewerSentinel)),
684
+ hasNestedSentinel: !!(help.json && String(help.json.markdown || '').includes(adversarialHelpSentinel))
685
+ }
686
+
687
+ const subfolderUi = await adversarialRouteSnapshot(page, '/p/subfolder-project/child', ['Subfolder Parent', 'Subfolder Child'])
688
+ const subfolderDirectory = await adversarialRouteSnapshot(page, '/api/subfolder-project/child', ['Subfolder Child', 'Child start'])
689
+ const subfolderIcon = await browserRequest(page, '/asset/api/subfolder-project/child/child.png')
690
+ const brokenRoot = await adversarialPageSnapshot(page, baseUrl, 'broken-root')
691
+ brokenRoot.sidebarNavigation = await goto(page, baseUrl, '/pinokio/sidebar/broken-root')
692
+ brokenRoot.sidebarMenu = await menuSnapshot(page, '.frame-link')
693
+
694
+ return {
695
+ rootWithFolder,
696
+ bothLaunchers: await adversarialPageSnapshot(page, baseUrl, 'both-launchers'),
697
+ brokenRoot,
698
+ emptyPinokioFolder: await adversarialRouteSnapshot(page, '/p/empty-pinokio-folder', ['empty-pinokio-folder']),
699
+ plainPinokioFile: await adversarialRouteSnapshot(page, '/p/plain-pinokio-file', ['plain-pinokio-file']),
700
+ noLauncher: await adversarialRouteSnapshot(page, '/p/no-launcher', ['no-launcher']),
701
+ appNamedPinokio: await adversarialPageSnapshot(page, baseUrl, 'pinokio'),
702
+ subfolder: {
703
+ ui: subfolderUi,
704
+ directory: subfolderDirectory,
705
+ iconStatus: subfolderIcon.status
706
+ }
707
+ }
708
+ }
709
+
710
+ async function waitForRunning(page, expected, running, timeoutMs = 12000, targetApp = appId) {
711
+ const started = Date.now()
712
+ while (Date.now() - started < timeoutMs) {
713
+ const response = await browserRequest(page, `/pinokio/home_status?t=${Date.now()}`)
714
+ const entries = response.json && Array.isArray(response.json.running_scripts) ? response.json.running_scripts : []
715
+ const found = entries.some(entry => entry && entry.app === targetApp && String(entry.script_path || entry.name || '').replace(/\\/g, '/') === expected)
716
+ if (found === running) return true
717
+ await page.waitForTimeout(150)
718
+ }
719
+ return false
720
+ }
721
+
722
+ async function goto(page, baseUrl, pathname) {
723
+ const response = await page.goto(`${baseUrl}${pathname}`, { waitUntil: 'load' })
724
+ return { status: response ? response.status() : 0, url: new URL(page.url()).pathname + new URL(page.url()).search }
725
+ }
726
+
727
+ function normalizedTreeEntries(entries) {
728
+ return entries.map(entry => ({ name: entry.name, path: entry.path, type: entry.type })).sort((a, b) => a.path.localeCompare(b.path))
729
+ }
730
+
731
+ async function exerciseRun({ page, baseUrl, home, build, layout, stub, artifacts, runtimeBacked }) {
732
+ const prefix = layout === 'nested' ? 'pinokio/' : ''
733
+ const envPath = path.resolve(home.primary.launcher, 'ENVIRONMENT')
734
+ const otherEnvPath = layout === 'nested' ? path.resolve(home.primary.workspace, 'ENVIRONMENT') : path.resolve(home.primary.workspace, 'pinokio', 'ENVIRONMENT')
735
+ const envWritePath = build === 'baseline' && layout === 'nested' ? otherEnvPath : envPath
736
+ const trace = { build, layout, runtime_backed: runtimeBacked
737
+ ? { status: 'EXECUTED' }
738
+ : { 4: 'UNVERIFIED: no test-owned runtime supplied', 16: 'UNVERIFIED: no test-owned runtime supplied', 23: 'UNVERIFIED: no test-owned runtime supplied', tier6: 'UNVERIFIED: live Explore installation is a separate release test' } }
739
+
740
+ trace.home = await goto(page, baseUrl, '/home')
741
+ await page.locator(`.home-app-line[data-uri="${appId}"]`).waitFor()
742
+ trace.homeCard = await page.locator(`.home-app-line[data-uri="${appId}"]`).evaluate(node => ({
743
+ href: node.getAttribute('data-href'),
744
+ icon: node.getAttribute('data-icon'),
745
+ iconpath: node.getAttribute('data-iconpath'),
746
+ name: node.getAttribute('data-name')
747
+ }))
748
+
749
+ trace.app = await goto(page, baseUrl, `/p/${appId}`)
750
+ await page.locator('.m.h.menu').waitFor()
751
+ trace.menuBefore = await menuSnapshot(page)
752
+ const startItem = trace.menuBefore.find(item => item.text.includes('Start relative'))
753
+ const runItem = trace.menuBefore.find(item => item.text.includes('Run string'))
754
+ trace.runCwd = runItem ? runItem.cwd : null
755
+ if (runtimeBacked) {
756
+ const runFile = path.resolve(home.primary.launcher, 'run-cwd.txt')
757
+ const runButton = page.locator('.m.h.menu [data-run]', { hasText: 'Run string' }).first()
758
+ if (await runButton.count()) await runButton.click()
759
+ trace.runtime_backed.run_string = {
760
+ clicked: !!(await runButton.count()),
761
+ output: await waitForOptionalFile(runFile, value => !!value.trim(), 30000)
762
+ }
763
+ }
764
+
765
+ trace.status = await waitForBrowserRequest(page, `/apps/status/${appId}`, response => response.json && response.json.state === 'starting')
766
+ assert.equal(trace.status.json && trace.status.json.state, 'starting', 'opening the app did not start its default script')
767
+ trace.status = { status: trace.status.status, value: stableStatus(trace.status.json) }
768
+ const expectedStart = trace.status.value && trace.status.value.start_script ? trace.status.value.start_script : `${prefix}start.js`
769
+ trace.autolaunch = await browserRequest(page, `/autolaunch/candidates?app=${encodeURIComponent(appId)}`)
770
+ if (trace.autolaunch.json) {
771
+ trace.autolaunch = {
772
+ status: trace.autolaunch.status,
773
+ menu: (trace.autolaunch.json.menu || []).map(item => item.script),
774
+ other: (trace.autolaunch.json.other || []).map(item => item.script)
775
+ }
776
+ }
777
+
778
+ trace.startNavigation = startItem ? await goto(page, baseUrl, startItem.href) : { status: 0, url: '' }
779
+ trace.startRunning = await waitForRunning(page, expectedStart, true)
780
+ await goto(page, baseUrl, `/p/${appId}`)
781
+ trace.menuRunning = await menuSnapshot(page)
782
+ const stopItem = page.locator('.m.h.menu [data-action]', { hasText: 'Stop relative' }).first()
783
+ if (await stopItem.count()) await stopItem.click()
784
+ trace.menuStopWorked = await waitForRunning(page, expectedStart, false)
785
+
786
+ if (startItem) await goto(page, baseUrl, startItem.href)
787
+ trace.shortcutStartRunning = await waitForRunning(page, expectedStart, true)
788
+ await goto(page, baseUrl, '/home')
789
+ const shortcut = page.locator(`.home-app-line[data-uri="${appId}"] .menu-btn[data-action]`).first()
790
+ trace.shortcutAction = await shortcut.count() ? await shortcut.getAttribute('data-action') : null
791
+ trace.shortcutElement = await shortcut.count() ? await shortcut.evaluate(node => node.outerHTML) : null
792
+ await page.waitForTimeout(250)
793
+ if (await shortcut.count()) await shortcut.click()
794
+ trace.shortcutFirstClickWorked = await waitForRunning(page, expectedStart, false, 750)
795
+ if (!trace.shortcutFirstClickWorked && await shortcut.count()) await shortcut.click()
796
+ trace.shortcutStopWorked = trace.shortcutFirstClickWorked || await waitForRunning(page, expectedStart, false)
797
+
798
+ trace.envPage = await goto(page, baseUrl, `/env/api/${appId}?init=true`)
799
+ await page.locator('#customize').click()
800
+ await page.locator('#REQUIRED_VALUE').fill('saved-through-browser')
801
+ const envResponsePromise = page.waitForResponse(response => response.url() === `${baseUrl}/env` && response.request().method() === 'POST')
802
+ await page.locator('#save').click()
803
+ const envResponse = await envResponsePromise
804
+ trace.envPost = envResponse.status()
805
+ const baselineNestedRepair = build === 'baseline' && layout === 'nested'
806
+ if (!baselineNestedRepair) {
807
+ await waitForFile(envWritePath, content => content.includes('REQUIRED_VALUE=saved-through-browser'))
808
+ }
809
+ trace.env = { launcher: await readOptional(envPath), other: await readOptional(otherEnvPath) }
810
+
811
+ trace.envSetNavigation = await goto(page, baseUrl, `/api/${appId}/${prefix}env-set.js`)
812
+ if (!baselineNestedRepair) {
813
+ await waitForFile(envWritePath, content => content.includes('ACTION_VALUE=set-by-action'))
814
+ }
815
+ trace.envSwitchNavigation = await goto(page, baseUrl, `/api/${appId}/${prefix}env-switch.js`)
816
+ if (!baselineNestedRepair) {
817
+ await waitForFile(envWritePath, content => content.includes('SWITCH_VALUE=two'))
818
+ }
819
+ trace.envAfterActions = await readOptional(envWritePath)
820
+
821
+ if (runtimeBacked) {
822
+ const runtimeResultPath = path.resolve(home.primary.launcher, 'runtime-result.json')
823
+ trace.runtime_backed.script_navigation = await goto(page, baseUrl, `/api/${appId}/${prefix}runtime.js`)
824
+ const runtimeResult = await waitForOptionalFile(runtimeResultPath, value => value.includes(runtimeSentinel), 30000)
825
+ trace.runtime_backed.terminal_has_sentinel = await waitForBodyText(page, runtimeSentinel, 5000)
826
+ trace.runtime_backed.result = runtimeResult ? JSON.parse(runtimeResult) : null
827
+ const runtimeLog = await waitForBrowserRequest(
828
+ page,
829
+ `/apps/logs/${appId}?script=${encodeURIComponent(`${prefix}runtime.js`)}`,
830
+ response => !!(response.json && String(response.json.text || '').includes(runtimeSentinel)),
831
+ build === 'baseline' && layout === 'nested' ? 1000 : 15000
832
+ )
833
+ trace.runtime_backed.log = runtimeLog.json ? {
834
+ status: runtimeLog.status,
835
+ script: runtimeLog.json.script,
836
+ has_sentinel: String(runtimeLog.json.text || '').includes(runtimeSentinel)
837
+ } : { status: runtimeLog.status, has_sentinel: false, error: runtimeLog.text }
838
+ }
839
+
840
+ trace.share = await goto(page, baseUrl, `/share/${appId}`)
841
+ trace.shareValues = await page.locator('.env-item').evaluateAll(nodes => nodes.map(node => ({
842
+ key: (node.querySelector('label') || {}).innerText || '',
843
+ value: (node.querySelector('input') || {}).value || ''
844
+ })))
845
+
846
+ trace.inheritancePage = await goto(page, baseUrl, `/env/api/${inheritanceId}?init=true`)
847
+ trace.inheritance = {
848
+ launcher: await readOptional(path.resolve(home.inheritance.launcher, 'ENVIRONMENT')),
849
+ other: await readOptional(layout === 'nested' ? path.resolve(home.inheritance.workspace, 'ENVIRONMENT') : path.resolve(home.inheritance.workspace, 'pinokio', 'ENVIRONMENT'))
850
+ }
851
+
852
+ trace.iconBefore = await browserRequest(page, trace.status.value.icon)
853
+ trace.iconBefore = { status: trace.iconBefore.status, size: trace.iconBefore.text.length }
854
+ trace.iconUpload = await page.evaluate(async ({ appId, pngBase64 }) => {
855
+ const form = new FormData()
856
+ form.append('avatar', new File([Uint8Array.from(atob(pngBase64), value => value.charCodeAt(0))], 'uploaded.png', { type: 'image/png' }))
857
+ form.append('icon_dirty', 'true')
858
+ form.append('icon_path', 'uploaded.png')
859
+ form.append('title', 'Nested Browser Acceptance')
860
+ form.append('description', 'Controlled nested-launcher browser fixture.')
861
+ form.append('old_path', appId)
862
+ form.append('new_path', appId)
863
+ form.append('edit', 'true')
864
+ const response = await fetch('/pinokio/upload', { method: 'POST', body: form })
865
+ return { status: response.status, text: await response.text() }
866
+ }, { appId, pngBase64: png.toString('base64') })
867
+ trace.uploadedIcon = {
868
+ launcher: fs.existsSync(path.resolve(home.primary.launcher, 'uploaded.png')),
869
+ other: fs.existsSync(layout === 'nested' ? path.resolve(home.primary.workspace, 'uploaded.png') : path.resolve(home.primary.workspace, 'pinokio', 'uploaded.png'))
870
+ }
871
+ const refreshedStatus = await browserRequest(page, `/apps/status/${appId}`)
872
+ trace.iconAfter = { url: refreshedStatus.json && refreshedStatus.json.icon }
873
+ if (trace.iconAfter.url) {
874
+ const iconAfterResponse = await browserRequest(page, trace.iconAfter.url)
875
+ trace.iconAfter.status = iconAfterResponse.status
876
+ }
877
+
878
+ trace.preFirst = await goto(page, baseUrl, `/pre/api/${appId}`)
879
+ trace.preFirst.items = await page.locator('.item').evaluateAll(nodes => nodes.map(node => ({
880
+ title: (node.querySelector('.title') || {}).innerText || '',
881
+ icon: (node.querySelector('img') || {}).getAttribute ? node.querySelector('img').getAttribute('src') : null,
882
+ href: (node.querySelector('a') || {}).getAttribute ? node.querySelector('a').getAttribute('href') : null,
883
+ filepath: (node.querySelector('button') || {}).dataset ? node.querySelector('button').dataset.filepath || null : null
884
+ })))
885
+ trace.preSecond = await goto(page, baseUrl, `/pre/api/${appId}`)
886
+ trace.preSecond.items = await page.locator('.item').evaluateAll(nodes => nodes.map(node => ({
887
+ title: (node.querySelector('.title') || {}).innerText || '',
888
+ icon: (node.querySelector('img') || {}).getAttribute ? node.querySelector('img').getAttribute('src') : null,
889
+ href: (node.querySelector('a') || {}).getAttribute ? node.querySelector('a').getAttribute('href') : null,
890
+ filepath: (node.querySelector('button') || {}).dataset ? node.querySelector('button').dataset.filepath || null : null
891
+ })))
892
+
893
+ trace.search = {}
894
+ for (const token of ['WORKSPACEREADMESENTINEL', 'LAUNCHERREADMESENTINEL', 'SCRIPTSEARCHSENTINEL']) {
895
+ const response = await browserRequest(page, `/apps/search?q=${encodeURIComponent(token)}&mode=strict&min_match=1`)
896
+ trace.search[token] = {
897
+ status: response.status,
898
+ files: response.json && Array.isArray(response.json.apps)
899
+ ? response.json.apps.flatMap(app => (app.matches || []).map(match => match.file)).sort()
900
+ : []
901
+ }
902
+ }
903
+ const aiResponse = await browserRequest(page, `/pinokio/ai/${appId}`)
904
+ trace.ai = { status: aiResponse.status }
905
+ await page.setContent(aiResponse.text)
906
+ trace.ai.links = await page.locator('a').evaluateAll(nodes => nodes.map(node => ({ text: node.innerText.trim(), href: node.getAttribute('href') })).sort((a, b) => a.href.localeCompare(b.href)))
907
+ for (const link of trace.ai.links) {
908
+ const response = await browserRequest(page, link.href)
909
+ link.status = response.status
910
+ link.body = response.text.trim()
911
+ }
912
+
913
+ await goto(page, baseUrl, `/p/${appId}`)
914
+ const logQuery = layout === 'nested' ? 'pinokio/seeded.js' : 'seeded.js'
915
+ const currentLog = await browserRequest(page, `/apps/logs/${appId}?script=${encodeURIComponent(logQuery)}`)
916
+ trace.currentLog = currentLog.json ? {
917
+ status: currentLog.status,
918
+ script: currentLog.json.script,
919
+ file: currentLog.json.file,
920
+ text: currentLog.json.text
921
+ } : { status: currentLog.status, error: currentLog.text }
922
+ const logsPage = await goto(page, baseUrl, `/logs?workspace=${appId}`)
923
+ trace.logsPage = { ...logsPage, root: await page.locator('[data-logs-root]').count() ? await page.locator('[data-logs-root]').getAttribute('data-logs-root') : null }
924
+ const tree = await browserRequest(page, `/api/logs/tree?workspace=${appId}`)
925
+ trace.logTree = tree.json ? { status: tree.status, path: tree.json.path, entries: normalizedTreeEntries(tree.json.entries || []) } : { status: tree.status, error: tree.text }
926
+
927
+ const reportResponse = await browserRequest(page, `/apps/logs/${appId}/report?session=${historicalSession}&redaction=none`)
928
+ trace.report = reportResponse.json ? {
929
+ status: reportResponse.status,
930
+ session: reportResponse.json.session,
931
+ sections: (reportResponse.json.sections || []).map(section => ({ source: section.source, script: section.script, file: section.file, text: section.text })),
932
+ hasHistorical: String(reportResponse.json.markdown || '').includes(historicalSentinel),
933
+ hasCurrent: String(reportResponse.json.markdown || '').includes(currentSentinel),
934
+ repo: reportResponse.json.repo_url || null
935
+ } : { status: reportResponse.status, error: reportResponse.text }
936
+ if (reportResponse.json) {
937
+ const metadata = Buffer.from(JSON.stringify({
938
+ body: reportResponse.json.markdown,
939
+ appRepoUrl: reportResponse.json.repo_url,
940
+ session: historicalSession
941
+ }), 'utf8').toString('base64')
942
+ trace.draft = await page.evaluate(async ({ appId, registry, metadata }) => {
943
+ const form = new FormData()
944
+ form.append('token', 'acceptance-token')
945
+ form.append('registry', registry)
946
+ form.append('metadata_b64', metadata)
947
+ const response = await fetch(`/apps/logs/${appId}/drafts`, { method: 'POST', body: form })
948
+ return { status: response.status, text: await response.text() }
949
+ }, { appId, registry: stub.url, metadata })
950
+ } else {
951
+ trace.draft = { status: 0, text: '' }
952
+ }
953
+ trace.registryDraft = stub.received.map(item => ({
954
+ method: item.method,
955
+ url: item.url,
956
+ authorization: item.authorization,
957
+ hasHistorical: !!(item.metadata && item.metadata.body && item.metadata.body.includes(historicalSentinel)),
958
+ hasCurrent: !!(item.metadata && item.metadata.body && item.metadata.body.includes(currentSentinel)),
959
+ repo: item.metadata && (item.metadata.appRepoUrl || item.metadata.repoUrl)
960
+ }))
961
+
962
+ trace.gitExclude = await readOptional(path.resolve(home.primary.workspace, '.git', 'info', 'exclude'))
963
+ trace.inheritanceGitExclude = await readOptional(path.resolve(home.inheritance.workspace, '.git', 'info', 'exclude'))
964
+ trace.locations = {
965
+ env: path.relative(home.pinokioHome, envPath).split(path.sep).join('/'),
966
+ logs: path.relative(home.pinokioHome, path.resolve(home.primary.launcher, 'logs')).split(path.sep).join('/'),
967
+ icon: trace.uploadedIcon.launcher ? path.relative(home.pinokioHome, path.resolve(home.primary.launcher, 'uploaded.png')).split(path.sep).join('/') : null
968
+ }
969
+ trace.nestedChildRegression = null
970
+ if (layout === 'nested') {
971
+ await goto(page, baseUrl, `/p/${nestedChildId}`)
972
+ const nestedChildMenu = await menuSnapshot(page)
973
+ const start = nestedChildMenu.find(item => item.text.includes('Start nested child'))
974
+ const stop = nestedChildMenu.find(item => item.text.includes('Stop nested child'))
975
+ const candidates = await browserRequest(page, `/autolaunch/candidates?app=${nestedChildId}`)
976
+ if (start) await goto(page, baseUrl, start.href)
977
+ const started = await waitForRunning(page, 'pinokio/pinokio/tool.js', true, 12000, nestedChildId)
978
+ await goto(page, baseUrl, `/p/${nestedChildId}`)
979
+ const stopButton = page.locator('.m.h.menu [data-action]', { hasText: 'Stop nested child' }).first()
980
+ if (await stopButton.count()) await stopButton.click()
981
+ trace.nestedChildRegression = {
982
+ startHref: start && start.href,
983
+ stopUri: stop && stop.action && stop.action.uri,
984
+ candidates: candidates.json && (candidates.json.menu || []).map(item => item.script),
985
+ started,
986
+ stopped: await waitForRunning(page, 'pinokio/pinokio/tool.js', false, 12000, nestedChildId)
987
+ }
988
+ }
989
+ trace.adversarial = await exerciseAdversarial(page, baseUrl)
990
+ await page.screenshot({ path: path.resolve(artifacts, 'final.png'), fullPage: true })
991
+ return trace
992
+ }
993
+
994
+ async function executeCase({ checkout, build, layout, playwright, executablePath, artifactsRoot, runtime }) {
995
+ const home = await createHome(layout, runtime && runtime.bin)
996
+ const artifacts = path.resolve(artifactsRoot, `${build}-${layout}`)
997
+ await fsp.mkdir(artifacts, { recursive: true })
998
+ const port = await freePort()
999
+ const stub = await startRegistryStub()
1000
+ const env = {
1001
+ ...process.env,
1002
+ HOME: home.fakeUserHome,
1003
+ PINOKIO_HOME: home.pinokioHome,
1004
+ PINOKIO_DISABLE_WATCH: '1',
1005
+ NODE_PATH: [path.resolve(repoRoot, 'node_modules'), process.env.NODE_PATH || ''].filter(Boolean).join(path.delimiter)
1006
+ }
1007
+ if (process.platform === 'win32') {
1008
+ env.USERPROFILE = home.fakeUserHome
1009
+ env.APPDATA = path.resolve(home.fakeUserHome, 'AppData', 'Roaming')
1010
+ env.LOCALAPPDATA = path.resolve(home.fakeUserHome, 'AppData', 'Local')
1011
+ }
1012
+ const serverArgs = [
1013
+ __filename,
1014
+ '--server',
1015
+ '--checkout', checkout,
1016
+ '--home', home.pinokioHome,
1017
+ '--port', String(port)
1018
+ ]
1019
+ if (!runtime) serverArgs.push('--runtime-free')
1020
+ const child = childProcess.spawn(process.execPath, serverArgs, { cwd: checkout, env, stdio: ['ignore', 'pipe', 'pipe'], detached: false })
1021
+ const logs = []
1022
+ child.stdout.on('data', chunk => logs.push(chunk.toString()))
1023
+ child.stderr.on('data', chunk => logs.push(chunk.toString()))
1024
+ let browser
1025
+ let page
1026
+ try {
1027
+ await waitForReady(child)
1028
+ const baseUrl = `http://127.0.0.1:${port}`
1029
+ await waitForHttp(`${baseUrl}/home`)
1030
+ browser = await playwright.chromium.launch({ headless: true, executablePath })
1031
+ page = await browser.newPage()
1032
+ page.setDefaultTimeout(20000)
1033
+ const diagnostics = { pageErrors: [], consoleErrors: [], requestFailures: [], httpErrors: [] }
1034
+ page.on('pageerror', error => diagnostics.pageErrors.push(error.message || String(error)))
1035
+ page.on('console', message => {
1036
+ if (message.type() === 'error') diagnostics.consoleErrors.push(message.text())
1037
+ })
1038
+ page.on('requestfailed', request => {
1039
+ if (request.url().startsWith(baseUrl)) diagnostics.requestFailures.push({ url: request.url(), error: request.failure() && request.failure().errorText })
1040
+ })
1041
+ page.on('response', response => {
1042
+ if (response.url().startsWith(baseUrl) && response.status() >= 400) diagnostics.httpErrors.push({ url: response.url(), status: response.status() })
1043
+ })
1044
+ const trace = await exerciseRun({ page, baseUrl, home, build, layout, stub, artifacts, runtimeBacked: !!runtime })
1045
+ trace.diagnostics = diagnostics
1046
+ await fsp.writeFile(path.resolve(artifacts, 'trace.json'), JSON.stringify(trace, null, 2))
1047
+ await fsp.writeFile(path.resolve(artifacts, 'server.log'), logs.join(''))
1048
+ const unexpectedPageErrors = diagnostics.pageErrors.filter(message => {
1049
+ return !(build === 'baseline' && layout === 'nested' && String(message).startsWith("Unexpected token '<'"))
1050
+ })
1051
+ assert.deepEqual(unexpectedPageErrors, [], 'unexpected uncaught page errors')
1052
+ assert.deepEqual(diagnostics.requestFailures.filter(entry => entry.error !== 'net::ERR_ABORTED'), [], 'failed local browser requests')
1053
+ const knownBaselineNestedError = entry => {
1054
+ if (!(build === 'baseline' && layout === 'nested')) return false
1055
+ const url = new URL(entry.url)
1056
+ if (entry.status === 500 && url.pathname === '/env') return true
1057
+ if (entry.status === 404 && url.pathname === `/api/${appId}/pre.png`) return true
1058
+ if (entry.status !== 404 || url.pathname !== `/apps/logs/${appId}`) return false
1059
+ return ['pinokio/start.js', 'pinokio/seeded.js', 'pinokio/runtime.js'].includes(url.searchParams.get('script'))
1060
+ }
1061
+ const knownTier4Error = entry => {
1062
+ const url = new URL(entry.url)
1063
+ return (entry.status === 404 && url.pathname === '/p/subfolder-project/child') ||
1064
+ (entry.status === 422 && url.pathname === '/p/broken-root') ||
1065
+ (entry.status === 500 && url.pathname === '/p/plain-pinokio-file')
1066
+ }
1067
+ assert.deepEqual(diagnostics.httpErrors.filter(entry => !knownBaselineNestedError(entry) && !knownTier4Error(entry)), [], 'unexpected local 4xx/5xx responses')
1068
+ if (build === 'baseline' && layout === 'nested') {
1069
+ assert.equal(diagnostics.consoleErrors.every(message => /^Failed to load resource: the server responded with a status of (404|422|500)/.test(message)), true, 'unexpected baseline nested console error')
1070
+ } else {
1071
+ assert.equal(diagnostics.consoleErrors.every(message => /^Failed to load resource: the server responded with a status of (404|422|500)/.test(message)), true, 'unexpected browser console error')
1072
+ }
1073
+ return { trace, artifacts, temporaryHome: home.root, browserVersion: browser.version(), server: { pid: child.pid, port } }
1074
+ } catch (error) {
1075
+ if (page) await page.screenshot({ path: path.resolve(artifacts, 'failure.png'), fullPage: true }).catch(() => {})
1076
+ await fsp.writeFile(path.resolve(artifacts, 'server.log'), logs.join('')).catch(() => {})
1077
+ await fsp.writeFile(path.resolve(artifacts, 'failure.txt'), error && error.stack ? error.stack : String(error)).catch(() => {})
1078
+ error.artifacts = artifacts
1079
+ error.temporaryHome = home.root
1080
+ throw error
1081
+ } finally {
1082
+ if (page) await page.close().catch(() => {})
1083
+ if (browser) await browser.close().catch(() => {})
1084
+ await stopChild(child)
1085
+ await stub.close().catch(() => {})
1086
+ }
1087
+ }
1088
+
1089
+ async function waitForScriptCompletion(page, app, scriptPath, logFile, initiallyRunning = false, timeoutMs = 600000) {
1090
+ const started = Date.now()
1091
+ let sawRunning = initiallyRunning
1092
+ while (Date.now() - started < timeoutMs) {
1093
+ const response = await browserRequest(page, `/pinokio/home_status?t=${Date.now()}`)
1094
+ const entries = response.json && Array.isArray(response.json.running_scripts) ? response.json.running_scripts : []
1095
+ const running = entries.some(entry => entry && entry.app === app && String(entry.script_path || '').replace(/\\/g, '/') === scriptPath)
1096
+ sawRunning ||= running
1097
+ if (!running && sawRunning) return { completed: true, sawRunning, logCreated: fs.existsSync(logFile) }
1098
+ await page.waitForTimeout(500)
1099
+ }
1100
+ return { completed: false, sawRunning }
1101
+ }
1102
+
1103
+ async function openPublishedControl(page, baseUrl, control) {
1104
+ await goto(page, baseUrl, '/home')
1105
+ const exploreButton = page.locator('[data-pinokio-explore-nav="true"]').first()
1106
+ await exploreButton.waitFor()
1107
+ await exploreButton.click()
1108
+ await page.waitForURL(url => url.origin === baseUrl && url.searchParams.get('mode') === 'explore', { timeout: 30000 })
1109
+ const frameElement = page.locator('iframe[name="pinokio-explore"]')
1110
+ await frameElement.waitFor()
1111
+ const frame = page.frame({ name: 'pinokio-explore' })
1112
+ assert.ok(frame, 'Explore iframe was not created')
1113
+ await frame.waitForLoadState('domcontentloaded')
1114
+ const toggle = frame.locator('button.search-toggle').first()
1115
+ if (await toggle.count()) await toggle.click()
1116
+ const search = frame.locator('form[role="search"] input[name="q"]').first()
1117
+ await search.waitFor({ timeout: 60000 })
1118
+ await search.fill(control.repo)
1119
+ await search.press('Enter')
1120
+ const result = frame.locator(`a[href="/apps/${control.slug}"]`).first()
1121
+ await Promise.race([
1122
+ frame.waitForURL(url => url.pathname === `/apps/${control.slug}`, { timeout: 60000 }),
1123
+ result.waitFor({ timeout: 60000 })
1124
+ ])
1125
+ if (new URL(frame.url()).pathname !== `/apps/${control.slug}`) await result.click()
1126
+ await frame.waitForURL(url => url.pathname === `/apps/${control.slug}`, { timeout: 60000 })
1127
+ const installTrigger = frame.locator('button.install-trigger:visible').first()
1128
+ await installTrigger.waitFor()
1129
+ await installTrigger.click()
1130
+ const installAction = frame.locator('.install-drawer-backdrop.is-open a[href^="http://localhost:42000/pinokio/download?uri="]', { hasText: 'Install latest' }).first()
1131
+ await installAction.waitFor()
1132
+ const handoff = await installAction.getAttribute('href')
1133
+ await installAction.click()
1134
+ await page.waitForURL(url => url.origin === baseUrl && url.searchParams.get('mode') === 'download', { timeout: 30000 })
1135
+ return {
1136
+ explorePath: new URL(frame.url()).pathname,
1137
+ handoff,
1138
+ parentPath: new URL(page.url()).pathname + new URL(page.url()).search
1139
+ }
1140
+ }
1141
+
1142
+ async function latestSession(launcher) {
1143
+ const index = await readOptional(path.resolve(launcher, 'logs', 'sessions', 'index.json'))
1144
+ if (!index) return null
1145
+ try {
1146
+ const parsed = JSON.parse(index)
1147
+ return parsed.latest_session || (parsed.sessions && parsed.sessions[0] && parsed.sessions[0].id) || null
1148
+ } catch (_) {
1149
+ return null
1150
+ }
1151
+ }
1152
+
1153
+ async function executeLiveCase({ checkout, build, controlName, control, playwright, executablePath, artifactsRoot, runtime }) {
1154
+ const home = await createLiveHome(runtime.bin)
1155
+ const artifacts = path.resolve(artifactsRoot, `live-${build}-${controlName}`)
1156
+ await fsp.mkdir(artifacts, { recursive: true })
1157
+ const port = await freePort()
1158
+ const env = {
1159
+ ...process.env,
1160
+ HOME: home.fakeUserHome,
1161
+ PINOKIO_HOME: home.pinokioHome,
1162
+ PINOKIO_DISABLE_WATCH: '1',
1163
+ NODE_PATH: [path.resolve(repoRoot, 'node_modules'), process.env.NODE_PATH || ''].filter(Boolean).join(path.delimiter)
1164
+ }
1165
+ if (process.platform === 'win32') {
1166
+ env.USERPROFILE = home.fakeUserHome
1167
+ env.APPDATA = path.resolve(home.fakeUserHome, 'AppData', 'Roaming')
1168
+ env.LOCALAPPDATA = path.resolve(home.fakeUserHome, 'AppData', 'Local')
1169
+ }
1170
+ const child = childProcess.spawn(process.execPath, [
1171
+ __filename,
1172
+ '--server',
1173
+ '--checkout', checkout,
1174
+ '--home', home.pinokioHome,
1175
+ '--port', String(port)
1176
+ ], { cwd: checkout, env, stdio: ['ignore', 'pipe', 'pipe'], detached: false })
1177
+ const logs = []
1178
+ child.stdout.on('data', chunk => logs.push(chunk.toString()))
1179
+ child.stderr.on('data', chunk => logs.push(chunk.toString()))
1180
+ let browser
1181
+ let page
1182
+ const trace = { build, control: controlName, published: control, runtime: { conda: runtime.conda, node: runtime.node, git: runtime.git, uv: runtime.uv } }
1183
+ try {
1184
+ await waitForReady(child)
1185
+ const baseUrl = `http://127.0.0.1:${port}`
1186
+ await waitForHttp(`${baseUrl}/home`)
1187
+ browser = await playwright.chromium.launch({ headless: true, executablePath })
1188
+ page = await browser.newPage()
1189
+ page.setDefaultTimeout(30000)
1190
+ const diagnostics = { pageErrors: [], consoleErrors: [], requestFailures: [], localHttpErrors: [] }
1191
+ page.on('pageerror', error => diagnostics.pageErrors.push(error.message || String(error)))
1192
+ page.on('console', message => {
1193
+ if (message.type() === 'error') diagnostics.consoleErrors.push(message.text())
1194
+ })
1195
+ page.on('requestfailed', request => diagnostics.requestFailures.push({ url: request.url(), error: request.failure() && request.failure().errorText }))
1196
+ page.on('response', response => {
1197
+ if (response.url().startsWith(baseUrl) && response.status() >= 400) diagnostics.localHttpErrors.push({ url: response.url(), status: response.status() })
1198
+ })
1199
+
1200
+ trace.explore = await openPublishedControl(page, baseUrl, control)
1201
+ const modalInput = page.locator('.swal2-input').first()
1202
+ await modalInput.waitFor({ timeout: 30000 })
1203
+ trace.downloadName = await modalInput.inputValue()
1204
+ assert.equal(trace.downloadName, control.folder)
1205
+ await page.locator('.swal2-confirm').click()
1206
+ await page.waitForURL(url => url.pathname === `/env/api/${control.folder}` && url.searchParams.get('init') === 'true', { timeout: 240000 })
1207
+
1208
+ const workspace = path.resolve(home.pinokioHome, 'api', control.folder)
1209
+ const launcher = control.layout === 'nested' ? path.resolve(workspace, 'pinokio') : workspace
1210
+ trace.clone = {
1211
+ remote: run('git', ['config', '--get', 'remote.origin.url'], { cwd: workspace }).replace(/\.git$/, ''),
1212
+ commit: run('git', ['rev-parse', 'HEAD'], { cwd: workspace }),
1213
+ rootManifest: fs.existsSync(path.resolve(workspace, 'pinokio.js')),
1214
+ nestedManifest: fs.existsSync(path.resolve(workspace, 'pinokio', 'pinokio.js'))
1215
+ }
1216
+ assert.equal(trace.clone.remote, control.repo)
1217
+ assert.equal(trace.clone.rootManifest, control.layout === 'root')
1218
+ assert.equal(trace.clone.nestedManifest, control.layout === 'nested')
1219
+
1220
+ trace.confirmation = {
1221
+ path: new URL(page.url()).pathname + new URL(page.url()).search,
1222
+ title: (await page.locator('body').innerText()).includes(control.title),
1223
+ remote: await page.locator('.warning a').getAttribute('href')
1224
+ }
1225
+ const envResponsePromise = page.waitForResponse(response => response.url() === `${baseUrl}/env` && response.request().method() === 'POST')
1226
+ await page.locator('#save').click()
1227
+ const envResponse = await envResponsePromise
1228
+ trace.environmentPost = envResponse.status()
1229
+ trace.environment = {
1230
+ root: await readOptional(path.resolve(workspace, 'ENVIRONMENT')),
1231
+ nested: await readOptional(path.resolve(workspace, 'pinokio', 'ENVIRONMENT'))
1232
+ }
1233
+ if (!envResponse.ok()) {
1234
+ trace.expectedReproduction = build === 'baseline' && control.layout === 'nested'
1235
+ trace.diagnostics = diagnostics
1236
+ await page.screenshot({ path: path.resolve(artifacts, 'environment-failure.png'), fullPage: true })
1237
+ await fsp.writeFile(path.resolve(artifacts, 'trace.json'), JSON.stringify(trace, null, 2))
1238
+ await fsp.writeFile(path.resolve(artifacts, 'server.log'), logs.join(''))
1239
+ assert.equal(trace.expectedReproduction, true, `unexpected environment POST failure: ${envResponse.status()}`)
1240
+ return { trace, artifacts, temporaryHome: home.root, browserVersion: browser.version() }
1241
+ }
1242
+
1243
+ await page.waitForURL(url => url.pathname === `/p/${control.folder}`, { timeout: 30000 })
1244
+ await page.locator('.m.h.menu').waitFor()
1245
+ trace.menuBeforeInstall = await menuSnapshot(page)
1246
+ let status = await browserRequest(page, `/apps/status/${control.folder}`)
1247
+ trace.statusBeforeInstall = { status: status.status, value: stableStatus(status.json) }
1248
+ const installScript = `${control.layout === 'nested' ? 'pinokio/' : ''}install.js`
1249
+ const installLog = path.resolve(launcher, 'logs', 'api', 'install.js', 'latest')
1250
+ const autoStarted = await waitForRunning(page, installScript, true, 5000, control.folder)
1251
+ if (!autoStarted) {
1252
+ const installItem = trace.menuBeforeInstall.find(item => /^Install(?:\s|$)/.test(item.text))
1253
+ assert.ok(installItem && installItem.href, 'published app did not expose an Install action')
1254
+ await goto(page, baseUrl, installItem.href)
1255
+ }
1256
+ trace.install = {
1257
+ trigger: autoStarted ? 'environment-install-auto-start' : 'menu-click',
1258
+ ...await waitForScriptCompletion(page, control.folder, installScript, installLog, autoStarted)
1259
+ }
1260
+ assert.equal(trace.install.completed, true, 'published install script did not complete')
1261
+
1262
+ await goto(page, baseUrl, `/p/${control.folder}`)
1263
+ await page.locator('.m.h.menu').waitFor()
1264
+ trace.menuAfterInstall = await menuSnapshot(page)
1265
+ status = await browserRequest(page, `/apps/status/${control.folder}`)
1266
+ trace.statusAfterInstall = { status: status.status, value: stableStatus(status.json) }
1267
+ const startItem = trace.menuAfterInstall.find(item => /^(Start|Open)/.test(item.text))
1268
+ assert.ok(startItem && startItem.href, 'published app did not expose a Start action after install')
1269
+ await goto(page, baseUrl, startItem.href)
1270
+ const startScript = `${control.layout === 'nested' ? 'pinokio/' : ''}start.js`
1271
+ trace.start = { running: await waitForRunning(page, startScript, true, 180000, control.folder) }
1272
+ assert.equal(trace.start.running, true, 'published start script did not enter the running state')
1273
+ const shellInfo = await browserRequest(page, '/info/shells')
1274
+ trace.start.shells = shellInfo.json
1275
+ ? Object.values(shellInfo.json).map(shell => ({ path: shell.path, cmd: shell.cmd, done: shell.done })).filter(shell => shell.path && shell.path.includes(control.folder))
1276
+ : []
1277
+ const stop = page.locator('.stop:not(.hidden)').first()
1278
+ await stop.waitFor({ state: 'visible', timeout: 180000 })
1279
+ await stop.click()
1280
+ trace.start.stopped = await waitForRunning(page, startScript, false, 30000, control.folder)
1281
+ assert.equal(trace.start.stopped, true, 'published start script did not stop')
1282
+
1283
+ await goto(page, baseUrl, '/home')
1284
+ const card = page.locator(`.home-app-line[data-uri="${control.folder}"]`)
1285
+ await card.waitFor()
1286
+ trace.homeCard = {
1287
+ href: await card.getAttribute('data-href'),
1288
+ icon: await card.getAttribute('data-icon'),
1289
+ name: await card.getAttribute('data-name')
1290
+ }
1291
+ const icon = trace.statusAfterInstall.value && trace.statusAfterInstall.value.icon
1292
+ const iconResponse = icon ? await browserRequest(page, icon) : null
1293
+ trace.icon = { url: icon, status: iconResponse && iconResponse.status }
1294
+ const session = await latestSession(launcher)
1295
+ const report = session ? await browserRequest(page, `/apps/logs/${control.folder}/report?session=${encodeURIComponent(session)}&redaction=none`) : null
1296
+ trace.logs = {
1297
+ root: path.relative(home.pinokioHome, path.resolve(launcher, 'logs')).split(path.sep).join('/'),
1298
+ installLatest: fs.existsSync(installLog),
1299
+ session,
1300
+ reportStatus: report && report.status
1301
+ }
1302
+ trace.filesystem = {
1303
+ rootEnvironment: fs.existsSync(path.resolve(workspace, 'ENVIRONMENT')),
1304
+ nestedEnvironment: fs.existsSync(path.resolve(workspace, 'pinokio', 'ENVIRONMENT')),
1305
+ gitExclude: await readOptional(path.resolve(workspace, '.git', 'info', 'exclude')),
1306
+ installedMarker: fs.existsSync(path.resolve(launcher, '.installed')),
1307
+ helloVenv: fs.existsSync(path.resolve(workspace, 'app', '.venv'))
1308
+ }
1309
+ trace.diagnostics = diagnostics
1310
+ await page.screenshot({ path: path.resolve(artifacts, 'final.png'), fullPage: true })
1311
+ await fsp.writeFile(path.resolve(artifacts, 'trace.json'), JSON.stringify(trace, null, 2))
1312
+ await fsp.writeFile(path.resolve(artifacts, 'server.log'), logs.join(''))
1313
+ const unexpectedLocal = diagnostics.localHttpErrors.filter(entry => !(entry.status === 404 && new URL(entry.url).pathname.startsWith('/apps/logs/')))
1314
+ assert.deepEqual(unexpectedLocal, [], 'unexpected local 4xx/5xx during published-app flow')
1315
+ return { trace, artifacts, temporaryHome: home.root, browserVersion: browser.version() }
1316
+ } catch (error) {
1317
+ if (page) await page.screenshot({ path: path.resolve(artifacts, 'failure.png'), fullPage: true }).catch(() => {})
1318
+ await fsp.writeFile(path.resolve(artifacts, 'trace.json'), JSON.stringify(trace, null, 2)).catch(() => {})
1319
+ await fsp.writeFile(path.resolve(artifacts, 'server.log'), logs.join('')).catch(() => {})
1320
+ await fsp.writeFile(path.resolve(artifacts, 'failure.txt'), error && error.stack ? error.stack : String(error)).catch(() => {})
1321
+ error.artifacts = artifacts
1322
+ error.temporaryHome = home.root
1323
+ throw error
1324
+ } finally {
1325
+ if (page) await page.close().catch(() => {})
1326
+ if (browser) await browser.close().catch(() => {})
1327
+ await stopChild(child)
1328
+ }
1329
+ }
1330
+
1331
+ function normalize(value, trace, parity = false) {
1332
+ if (Array.isArray(value)) return value.map(item => normalize(item, trace, parity))
1333
+ if (value && typeof value === 'object') {
1334
+ const next = {}
1335
+ for (const [key, item] of Object.entries(value)) {
1336
+ if (['diagnostics', 'runtime_backed'].includes(key)) continue
1337
+ next[key] = normalize(item, trace, parity)
1338
+ }
1339
+ return next
1340
+ }
1341
+ if (typeof value !== 'string') return value
1342
+ let text = value.replace(/\\/g, '/')
1343
+ const home = trace.__home.replace(/\\/g, '/')
1344
+ const homes = [...new Set([home, canonicalPath(home).replace(/\\/g, '/')])].sort((a, b) => b.length - a.length)
1345
+ for (const candidate of homes) {
1346
+ text = text.replaceAll(candidate, '<PINOKIO_HOME>')
1347
+ const encoded = encodeURIComponent(candidate)
1348
+ text = text.replaceAll(encoded, '<PINOKIO_HOME_ENCODED>')
1349
+ text = text.replaceAll(encoded.replaceAll('%2F', '%5C'), '<PINOKIO_HOME_ENCODED>')
1350
+ }
1351
+ if (parity && trace.layout === 'nested') {
1352
+ text = text.replaceAll(`/api/${appId}/pinokio/`, `/api/${appId}/`)
1353
+ text = text.replaceAll(`/asset/api/${appId}/pinokio/`, `/asset/api/${appId}/`)
1354
+ text = text.replaceAll(`api/${appId}/pinokio/`, `api/${appId}/`)
1355
+ text = text.replaceAll(`/api/${appId}/pinokio`, `/api/${appId}`)
1356
+ text = text.replaceAll(`api/${appId}/pinokio`, `api/${appId}`)
1357
+ text = text.replace(/^pinokio\//, '')
1358
+ }
1359
+ return text
1360
+ }
1361
+
1362
+ function comparable(trace, parity = false) {
1363
+ const copy = JSON.parse(JSON.stringify(trace))
1364
+ delete copy.__home
1365
+ delete copy.build
1366
+ if (parity) {
1367
+ delete copy.layout
1368
+ delete copy.nestedChildRegression
1369
+ copy.ai = {
1370
+ status: copy.ai.status,
1371
+ linksOk: copy.ai.links.every(link => link.status === 200),
1372
+ workspace: copy.ai.links.some(link => link.body.includes('WORKSPACEAISENTINEL')),
1373
+ launcher: copy.ai.links.some(link => link.body.includes('LAUNCHERAISENTINEL'))
1374
+ }
1375
+ const excludeSummary = value => String(value || '').split(/\r?\n/)
1376
+ .map(line => line.replace(/^\/pinokio\//, '').replace(/^\//, ''))
1377
+ .filter(Boolean)
1378
+ .sort()
1379
+ copy.gitExclude = excludeSummary(copy.gitExclude)
1380
+ copy.inheritanceGitExclude = excludeSummary(copy.inheritanceGitExclude)
1381
+ }
1382
+ return normalize(copy, trace, parity)
1383
+ }
1384
+
1385
+ function comparableRuntime(trace, parity = false) {
1386
+ return normalize(trace.runtime_backed, trace, parity)
1387
+ }
1388
+
1389
+ function assertCurrentSemantics(trace, layout) {
1390
+ const prefix = layout === 'nested' ? 'pinokio/' : ''
1391
+ assert.equal(trace.home.status, 200)
1392
+ assert.equal(trace.app.status, 200)
1393
+ assert.equal(trace.envPage.status, 200)
1394
+ assert.equal(trace.envPost, 200)
1395
+ assert.equal(trace.startRunning, true)
1396
+ assert.equal(trace.menuStopWorked, true)
1397
+ assert.equal(trace.shortcutStartRunning, true)
1398
+ assert.equal(trace.shortcutStopWorked, true)
1399
+ assert.equal(JSON.parse(trace.shortcutAction).uri, `~/api/${appId}/${prefix}start.js`)
1400
+ assert.equal(trace.status.value.install_script, `${prefix}install.js`)
1401
+ assert.equal(trace.status.value.start_script, `${prefix}start.js`)
1402
+ assert.equal(trace.status.value.default_script, `${prefix}start.js`)
1403
+ assert.equal(trace.runCwd.replace(/\\/g, '/').endsWith(`/api/${appId}/${prefix}`.replace(/\/$/, '')), true)
1404
+ assert.equal(trace.env.launcher.includes('REQUIRED_VALUE=saved-through-browser'), true)
1405
+ assert.equal(trace.env.other, null)
1406
+ assert.equal(trace.envAfterActions.includes('ACTION_VALUE=set-by-action'), true)
1407
+ assert.equal(trace.envAfterActions.includes('SWITCH_VALUE=two'), true)
1408
+ assert.equal(trace.inheritance.launcher.includes('INHERITANCE_UNIQUE_SENTINEL=yes'), true)
1409
+ assert.equal(trace.inheritance.other, null)
1410
+ assert.equal(trace.iconBefore.status, 200)
1411
+ assert.equal(trace.iconUpload.status, 200)
1412
+ assert.equal(trace.uploadedIcon.launcher, true)
1413
+ assert.equal(trace.uploadedIcon.other, false)
1414
+ assert.equal(trace.iconAfter.status, 200)
1415
+ assert.deepEqual(trace.preSecond.items, trace.preFirst.items)
1416
+ assert.equal(trace.search.WORKSPACEREADMESENTINEL.files.includes('README.md'), true)
1417
+ assert.equal(trace.search.LAUNCHERREADMESENTINEL.files.includes(`${prefix}README.md`), true)
1418
+ assert.equal(trace.search.SCRIPTSEARCHSENTINEL.files.includes(`${prefix}start.js`), true)
1419
+ assert.equal(trace.ai.links.some(link => link.href === `/_api/${appId}/CLAUDE.md` && link.body.includes('WORKSPACEAISENTINEL')), true)
1420
+ assert.equal(trace.ai.links.some(link => link.href === `/_api/${appId}/${prefix}CLAUDE.md` && link.body.includes('LAUNCHERAISENTINEL')), true)
1421
+ assert.equal(trace.currentLog.status, 200)
1422
+ assert.equal(trace.currentLog.text.includes(currentSentinel), true)
1423
+ assert.equal(trace.logTree.status, 200)
1424
+ assert.equal(trace.report.status, 200)
1425
+ assert.equal(trace.report.hasHistorical, true)
1426
+ assert.equal(trace.report.hasCurrent, false)
1427
+ assert.equal(trace.draft.status, 200)
1428
+ assert.equal(trace.registryDraft.length, 1)
1429
+ assert.equal(trace.registryDraft[0].hasHistorical, true)
1430
+ assert.equal(trace.registryDraft[0].hasCurrent, false)
1431
+ assert.deepEqual(trace.adversarial.rootWithFolder.markers, ['Root With Folder'])
1432
+ assert.equal(trace.adversarial.rootWithFolder.logViewer.status, 200)
1433
+ assert.equal(trace.adversarial.rootWithFolder.logViewer.hasRootSentinel, true)
1434
+ assert.equal(trace.adversarial.rootWithFolder.logViewer.hasNestedSentinel, false)
1435
+ assert.equal(trace.adversarial.rootWithFolder.getHelp.status, 200)
1436
+ assert.equal(trace.adversarial.rootWithFolder.getHelp.hasRootSentinel, false)
1437
+ assert.equal(trace.adversarial.rootWithFolder.getHelp.hasNestedSentinel, true)
1438
+ assert.deepEqual(trace.adversarial.bothLaunchers.markers, ['Root Winner'])
1439
+ assert.equal(trace.adversarial.bothLaunchers.menu.some(item => item.text.includes('Root winner start')), true)
1440
+ assert.equal(trace.adversarial.bothLaunchers.menu.some(item => item.text.includes('Nested loser start')), false)
1441
+ assert.equal(trace.adversarial.brokenRoot.navigation.status, 422)
1442
+ assert.equal(trace.adversarial.brokenRoot.sidebarNavigation.status, 200)
1443
+ assert.equal(trace.adversarial.brokenRoot.sidebarMenu.some(item => item.text.includes('Fallback fs')), true)
1444
+ assert.equal(trace.adversarial.brokenRoot.sidebarMenu.some(item => item.text.includes('Fallback command')), true)
1445
+ assert.deepEqual(trace.adversarial.appNamedPinokio.markers, ['App Named Pinokio'])
1446
+ if (trace.runtime_backed.status === 'EXECUTED') {
1447
+ assert.equal(trace.runtime_backed.run_string.clicked, true)
1448
+ assert.equal(canonicalPath(trace.runtime_backed.run_string.output), canonicalPath(homePath(trace, `${appId}/${prefix}`)))
1449
+ assert.equal(trace.runtime_backed.script_navigation.status, 200)
1450
+ assert.equal(trace.runtime_backed.terminal_has_sentinel, true)
1451
+ assert.equal(trace.runtime_backed.result.sentinel, runtimeSentinel)
1452
+ assert.equal(canonicalPath(trace.runtime_backed.result.cwd), canonicalPath(homePath(trace, `${appId}/${prefix}`)))
1453
+ assert.equal(canonicalPath(trace.runtime_backed.result.relative), canonicalPath(homePath(trace, `${appId}/${prefix}something`)))
1454
+ assert.equal(trace.runtime_backed.log.status, 200)
1455
+ assert.equal(trace.runtime_backed.log.has_sentinel, true)
1456
+ }
1457
+ if (layout === 'nested') {
1458
+ assert.match(trace.gitExclude, /^\/pinokio\/ENVIRONMENT$/m)
1459
+ assert.deepEqual(trace.nestedChildRegression, {
1460
+ startHref: `/api/${nestedChildId}/pinokio/pinokio/tool.js`,
1461
+ stopUri: `~/api/${nestedChildId}/pinokio/pinokio/tool.js`,
1462
+ candidates: ['pinokio/pinokio/tool.js'],
1463
+ started: true,
1464
+ stopped: true
1465
+ })
1466
+ }
1467
+ }
1468
+
1469
+ function homePath(trace, relative) {
1470
+ return path.resolve(trace.__home, 'api', relative)
1471
+ }
1472
+
1473
+ function canonicalPath(value) {
1474
+ let current = path.resolve(value)
1475
+ const suffix = []
1476
+ while (!fs.existsSync(current)) {
1477
+ const parent = path.dirname(current)
1478
+ assert.notEqual(parent, current, `cannot canonicalize ${value}`)
1479
+ suffix.unshift(path.basename(current))
1480
+ current = parent
1481
+ }
1482
+ return path.resolve(fs.realpathSync.native(current), ...suffix)
1483
+ }
1484
+
1485
+ function stableNestedCompatibility(trace) {
1486
+ return {
1487
+ home: trace.home.status,
1488
+ app: trace.app.status,
1489
+ envPage: trace.envPage.status,
1490
+ inheritanceSentinel: !!(trace.inheritance.launcher && trace.inheritance.launcher.includes('INHERITANCE_UNIQUE_SENTINEL=yes')),
1491
+ reportStatus: trace.report.status,
1492
+ reportHistorical: trace.report.hasHistorical,
1493
+ reportCurrent: trace.report.hasCurrent,
1494
+ draftStatus: trace.draft.status,
1495
+ draftHistorical: trace.registryDraft[0] && trace.registryDraft[0].hasHistorical,
1496
+ nestedChild: trace.nestedChildRegression && {
1497
+ startHref: trace.nestedChildRegression.startHref,
1498
+ candidates: trace.nestedChildRegression.candidates,
1499
+ started: trace.nestedChildRegression.started
1500
+ }
1501
+ }
1502
+ }
1503
+
1504
+ function stableLiveRoot(trace) {
1505
+ const menu = items => (items || []).map(item => ({ text: item.text, href: item.href, run: item.run, cwd: item.cwd, action: item.action }))
1506
+ const status = response => response && ({
1507
+ ...response,
1508
+ value: response.value && {
1509
+ ...response.value,
1510
+ default_target: response.value.default_target
1511
+ ? path.relative(trace.__home, response.value.default_target).split(path.sep).join('/')
1512
+ : response.value.default_target
1513
+ }
1514
+ })
1515
+ return {
1516
+ explore: trace.explore,
1517
+ downloadName: trace.downloadName,
1518
+ clone: trace.clone,
1519
+ confirmation: trace.confirmation,
1520
+ environmentPost: trace.environmentPost,
1521
+ environment: {
1522
+ root: trace.environment && trace.environment.root !== null,
1523
+ nested: trace.environment && trace.environment.nested !== null
1524
+ },
1525
+ menuBeforeInstall: menu(trace.menuBeforeInstall),
1526
+ menuAfterInstall: menu(trace.menuAfterInstall),
1527
+ statusBeforeInstall: status(trace.statusBeforeInstall),
1528
+ statusAfterInstall: status(trace.statusAfterInstall),
1529
+ install: trace.install,
1530
+ start: trace.start && { running: trace.start.running, stopped: trace.start.stopped },
1531
+ homeCard: trace.homeCard,
1532
+ icon: trace.icon,
1533
+ logs: trace.logs && { root: trace.logs.root, installLatest: trace.logs.installLatest, reportStatus: trace.logs.reportStatus },
1534
+ filesystem: trace.filesystem
1535
+ }
1536
+ }
1537
+
1538
+ function assertLiveNested(trace) {
1539
+ assert.equal(trace.expectedReproduction, undefined)
1540
+ assert.equal(trace.environmentPost, 200)
1541
+ assert.equal(trace.environment.root, null)
1542
+ assert.ok(trace.environment.nested)
1543
+ assert.equal(trace.statusAfterInstall.status, 200)
1544
+ assert.equal(trace.statusAfterInstall.value.install_script, 'pinokio/install.js')
1545
+ assert.equal(trace.statusAfterInstall.value.start_script, 'pinokio/start.js')
1546
+ assert.equal(trace.install.completed, true)
1547
+ assert.equal(trace.start.running, true)
1548
+ assert.equal(trace.start.stopped, true)
1549
+ assert.equal(trace.icon.status, 200)
1550
+ assert.equal(trace.logs.root, `api/${liveControls.nested.folder}/pinokio/logs`)
1551
+ assert.equal(trace.logs.installLatest, true)
1552
+ assert.equal(trace.logs.reportStatus, 200)
1553
+ assert.equal(trace.filesystem.rootEnvironment, false)
1554
+ assert.equal(trace.filesystem.nestedEnvironment, true)
1555
+ assert.equal(trace.filesystem.installedMarker, true)
1556
+ assert.match(trace.filesystem.gitExclude, /^\/pinokio\/ENVIRONMENT$/m)
1557
+ }
1558
+
1559
+ async function materializeCheckout(value, tempRoot) {
1560
+ const candidate = path.resolve(String(value || ''))
1561
+ if (value && fs.existsSync(path.resolve(candidate, 'package.json'))) return { path: candidate, cleanup: async () => {} }
1562
+ const ref = value && value !== true ? String(value) : 'HEAD'
1563
+ const checkout = path.resolve(tempRoot, 'baseline-checkout')
1564
+ run('git', ['worktree', 'add', '--detach', checkout, ref], { cwd: repoRoot })
1565
+ return {
1566
+ path: checkout,
1567
+ cleanup: async () => {
1568
+ try { run('git', ['worktree', 'remove', '--force', checkout], { cwd: repoRoot }) } catch (_) {}
1569
+ }
1570
+ }
1571
+ }
1572
+
1573
+ function checkoutIdentity(checkout) {
1574
+ const revision = run('git', ['rev-parse', 'HEAD'], { cwd: checkout })
1575
+ const status = run('git', ['status', '--short'], { cwd: checkout })
1576
+ const diff = run('git', ['diff', '--binary', 'HEAD'], { cwd: checkout })
1577
+ const untracked = run('git', ['ls-files', '--others', '--exclude-standard'], { cwd: checkout }).split(/\r?\n/).filter(Boolean)
1578
+ const digest = crypto.createHash('sha256').update(`${revision}\n${status}\n${diff}`)
1579
+ for (const relative of untracked) {
1580
+ digest.update(`\nuntracked:${relative}\n`)
1581
+ digest.update(fs.readFileSync(path.resolve(checkout, relative)))
1582
+ }
1583
+ return { checkout, revision, dirty: !!status, status, untracked, diff_hash: digest.digest('hex') }
1584
+ }
1585
+
1586
+ async function parentMode() {
1587
+ const args = parseArgs()
1588
+ const playwright = loadPlaywright()
1589
+ const executablePath = browserExecutable(playwright)
1590
+ assert.ok(executablePath, 'No supported Chromium or Brave executable is available')
1591
+ const started = new Date().toISOString().replace(/[:.]/g, '-')
1592
+ const artifactsRoot = path.resolve(outputRoot, started)
1593
+ const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'pinokio-nested-checkouts-'))
1594
+ await fsp.mkdir(artifactsRoot, { recursive: true })
1595
+ const baseline = await materializeCheckout(args.baseline || args['baseline-ref'] || 'HEAD', tempRoot)
1596
+ const current = path.resolve(String(args.current || repoRoot))
1597
+ const runtimeActive = args['runtime-active'] || process.env.PINOKIO_ACCEPTANCE_RUNTIME
1598
+ const runtimeSnapshot = args['runtime-snapshot'] || process.env.PINOKIO_ACCEPTANCE_RUNTIME_SNAPSHOT
1599
+ assert.equal(!!runtimeActive, !!runtimeSnapshot, 'runtime-active and runtime-snapshot must be supplied together')
1600
+ const runtimeManager = runtimeActive ? await prepareRuntime({
1601
+ active: runtimeActive,
1602
+ snapshot: runtimeSnapshot,
1603
+ createSnapshot: !!args['create-runtime-snapshot']
1604
+ }) : null
1605
+ const runtimeManifestHash = runtimeManager
1606
+ ? crypto.createHash('sha256').update(JSON.stringify(runtimeManager.manifest)).digest('hex')
1607
+ : null
1608
+ const evidence = {
1609
+ started_at: new Date().toISOString(),
1610
+ platform: `${process.platform} ${process.arch}`,
1611
+ node: process.version,
1612
+ browser: null,
1613
+ baseline: checkoutIdentity(baseline.path),
1614
+ current: checkoutIdentity(current),
1615
+ runtime_backed: runtimeManager ? {
1616
+ status: 'RUNNING',
1617
+ active: runtimeManager.activeRoot,
1618
+ snapshot: runtimeManager.snapshotRoot,
1619
+ snapshot_entries: runtimeManager.manifest.length,
1620
+ snapshot_manifest_sha256: runtimeManifestHash
1621
+ } : 'UNVERIFIED: PINOKIO_ACCEPTANCE_RUNTIME was not supplied; this execution intentionally runs the runtime-free subset only.',
1622
+ unverified: {
1623
+ runtime_backed_steps: runtimeManager ? null : 'Steps 4, 16, and 23 require a test-owned runtime.',
1624
+ tier6_live_explore: args.tier6 || args['tier6-only'] ? null : 'The published-app Explore installation matrix is a separate release test.',
1625
+ windows: process.platform === 'win32' ? null : 'This run does not provide Windows execution evidence.'
1626
+ }
1627
+ }
1628
+ evidence.browser = { executable: executablePath, version: null }
1629
+ const cases = args['tier6-only'] ? [] : [
1630
+ { build: 'baseline', checkout: baseline.path, layout: 'root' },
1631
+ { build: 'baseline', checkout: baseline.path, layout: 'nested' },
1632
+ { build: 'current', checkout: current, layout: 'root' },
1633
+ { build: 'current', checkout: current, layout: 'nested' }
1634
+ ]
1635
+ const results = {}
1636
+ let failed = false
1637
+ try {
1638
+ for (const item of cases) {
1639
+ const key = `${item.build}-${item.layout}`
1640
+ process.stdout.write(`${resultPrefix}${JSON.stringify({ state: 'running', case: key })}\n`)
1641
+ try {
1642
+ const runtime = runtimeManager ? await runtimeManager.restore() : null
1643
+ const result = await executeCase({ ...item, playwright, executablePath, artifactsRoot, runtime })
1644
+ result.trace.__home = result.trace.layout === item.layout ? result.temporaryHome.replace(/\\/g, '/') + '/pinokio-home' : ''
1645
+ results[key] = result
1646
+ evidence.browser.version ||= result.browserVersion
1647
+ process.stdout.write(`${resultPrefix}${JSON.stringify({ state: 'collected', case: key, artifacts: result.artifacts })}\n`)
1648
+ } catch (error) {
1649
+ failed = true
1650
+ results[key] = { error: error && error.stack ? error.stack : String(error), artifacts: error.artifacts || null, temporaryHome: error.temporaryHome || null }
1651
+ process.stdout.write(`${resultPrefix}${JSON.stringify({ state: 'failed', case: key, error: results[key].error, artifacts: results[key].artifacts })}\n`)
1652
+ break
1653
+ }
1654
+ }
1655
+
1656
+ if (!failed && cases.length > 0) {
1657
+ const baselineRoot = results['baseline-root'].trace
1658
+ const baselineNested = results['baseline-nested'].trace
1659
+ const currentRoot = results['current-root'].trace
1660
+ const currentNested = results['current-nested'].trace
1661
+ assertCurrentSemantics(currentRoot, 'root')
1662
+ assertCurrentSemantics(currentNested, 'nested')
1663
+ assert.deepEqual(comparable(currentRoot), comparable(baselineRoot), 'strict additivity failed: baseline root and current root traces differ')
1664
+ assert.deepEqual(stableNestedCompatibility(currentNested), stableNestedCompatibility(baselineNested), 'existing nested behavior regressed')
1665
+ assert.deepEqual(comparable(currentNested, true), comparable(currentRoot, true), 'layout parity failed after translating the nested launcher root')
1666
+ if (runtimeManager) {
1667
+ assert.deepEqual(comparableRuntime(currentRoot), comparableRuntime(baselineRoot), 'runtime-backed strict additivity failed for the root layout')
1668
+ assert.deepEqual(comparableRuntime(currentNested, true), comparableRuntime(currentRoot, true), 'runtime-backed root/nested parity failed')
1669
+ }
1670
+ evidence.comparisons = {
1671
+ strict_additivity: 'PASS',
1672
+ existing_nested_behavior: 'PASS',
1673
+ current_layout_parity: 'PASS',
1674
+ tier4_adversarial: 'PASS',
1675
+ runtime_backed: runtimeManager ? 'PASS' : 'UNVERIFIED'
1676
+ }
1677
+ if (runtimeManager) evidence.runtime_backed.status = 'PASS'
1678
+ }
1679
+ if (!failed && (args.tier6 || args['tier6-only'])) {
1680
+ assert.ok(runtimeManager, 'Tier 6 requires runtime-active and runtime-snapshot')
1681
+ const liveCases = [
1682
+ { build: 'baseline', checkout: baseline.path, controlName: 'root', control: liveControls.root },
1683
+ { build: 'baseline', checkout: baseline.path, controlName: 'nested', control: liveControls.nested },
1684
+ { build: 'current', checkout: current, controlName: 'root', control: liveControls.root },
1685
+ { build: 'current', checkout: current, controlName: 'nested', control: liveControls.nested }
1686
+ ]
1687
+ for (const item of liveCases) {
1688
+ const key = `live-${item.build}-${item.controlName}`
1689
+ process.stdout.write(`${resultPrefix}${JSON.stringify({ state: 'running', case: key })}\n`)
1690
+ try {
1691
+ const runtime = await runtimeManager.restore()
1692
+ const result = await executeLiveCase({ ...item, playwright, executablePath, artifactsRoot, runtime })
1693
+ result.trace.__home = `${result.temporaryHome.replace(/\\/g, '/')}/pinokio-home`
1694
+ results[key] = result
1695
+ evidence.browser.version ||= result.browserVersion
1696
+ process.stdout.write(`${resultPrefix}${JSON.stringify({ state: 'collected', case: key, artifacts: result.artifacts })}\n`)
1697
+ } catch (error) {
1698
+ failed = true
1699
+ results[key] = { error: error && error.stack ? error.stack : String(error), artifacts: error.artifacts || null, temporaryHome: error.temporaryHome || null }
1700
+ process.stdout.write(`${resultPrefix}${JSON.stringify({ state: 'failed', case: key, error: results[key].error, artifacts: results[key].artifacts })}\n`)
1701
+ break
1702
+ }
1703
+ }
1704
+ if (!failed) {
1705
+ const baselineLiveRoot = results['live-baseline-root'].trace
1706
+ const baselineLiveNested = results['live-baseline-nested'].trace
1707
+ const currentLiveRoot = results['live-current-root'].trace
1708
+ const currentLiveNested = results['live-current-nested'].trace
1709
+ assert.equal(baselineLiveRoot.clone.commit, currentLiveRoot.clone.commit, 'root control changed commits between baseline and current')
1710
+ assert.equal(baselineLiveNested.clone.commit, currentLiveNested.clone.commit, 'nested control changed commits between baseline and current')
1711
+ assert.deepEqual(stableLiveRoot(currentLiveRoot), stableLiveRoot(baselineLiveRoot), 'published root-layout behavior changed')
1712
+ assert.equal(baselineLiveNested.expectedReproduction, true, 'baseline did not reproduce the published nested environment failure')
1713
+ assertLiveNested(currentLiveNested)
1714
+ evidence.comparisons ||= {}
1715
+ evidence.comparisons.tier6_live_root_additivity = 'PASS'
1716
+ evidence.comparisons.tier6_live_nested_reproduction_and_repair = 'PASS'
1717
+ evidence.runtime_backed.status = 'PASS'
1718
+ }
1719
+ }
1720
+ } catch (error) {
1721
+ failed = true
1722
+ evidence.failure = error && error.stack ? error.stack : String(error)
1723
+ } finally {
1724
+ if (runtimeManager) {
1725
+ try {
1726
+ await runtimeManager.verifySnapshot()
1727
+ evidence.runtime_backed.snapshot_unchanged = true
1728
+ } catch (error) {
1729
+ failed = true
1730
+ evidence.runtime_backed.snapshot_unchanged = false
1731
+ evidence.runtime_backed.snapshot_error = error && error.stack ? error.stack : String(error)
1732
+ }
1733
+ }
1734
+ evidence.finished_at = new Date().toISOString()
1735
+ evidence.results = Object.fromEntries(Object.entries(results).map(([key, value]) => [key, {
1736
+ artifacts: value.artifacts || null,
1737
+ error: value.error || null,
1738
+ server: value.server || null
1739
+ }]))
1740
+ await fsp.writeFile(path.resolve(artifactsRoot, 'evidence.json'), JSON.stringify(evidence, null, 2))
1741
+ for (const value of Object.values(results)) {
1742
+ if (value.temporaryHome) await fsp.rm(value.temporaryHome, { recursive: true, force: true }).catch(() => {})
1743
+ }
1744
+ await baseline.cleanup()
1745
+ await fsp.rm(tempRoot, { recursive: true, force: true }).catch(() => {})
1746
+ }
1747
+ process.stdout.write(`${resultPrefix}${JSON.stringify({ ok: !failed, artifacts: artifactsRoot, comparisons: evidence.comparisons || null, failure: evidence.failure || null })}\n`)
1748
+ if (failed) process.exitCode = 1
1749
+ }
1750
+
1751
+ if (process.argv.includes('--server')) {
1752
+ runServerMode().catch(error => {
1753
+ console.error(error && error.stack ? error.stack : error)
1754
+ process.exit(1)
1755
+ })
1756
+ } else {
1757
+ parentMode().catch(error => {
1758
+ console.error(error && error.stack ? error.stack : error)
1759
+ process.exit(1)
1760
+ })
1761
+ }