pinokiod 7.5.7 → 7.5.9

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,558 @@
1
+ const assert = require('node:assert/strict')
2
+ const os = require('node:os')
3
+ const path = require('node:path')
4
+ const test = require('node:test')
5
+
6
+ const CondaRuntimeGuard = require('../kernel/shell_conda_runtime_guard')
7
+ const Shell = require('../kernel/shell')
8
+
9
+ const NOOP = 'echo Pinokio skipped a Conda setup command. Pinokio is continuing.'
10
+
11
+ function createContext(platform = 'darwin') {
12
+ const root = platform === 'win32'
13
+ ? 'C:\\pinokio'
14
+ : path.join(os.tmpdir(), 'pinokio-conda-runtime-guard')
15
+ const appPath = platform === 'win32'
16
+ ? 'C:\\pinokio\\api\\demo'
17
+ : path.join(root, 'api', 'demo')
18
+ const events = []
19
+ return {
20
+ appPath,
21
+ context: {
22
+ appPath,
23
+ cwd: appPath,
24
+ managedBasePrefix: platform === 'win32'
25
+ ? 'C:\\pinokio\\bin\\miniforge'
26
+ : path.join(root, 'bin', 'miniforge'),
27
+ ondata: (stream, type) => events.push({ stream, type }),
28
+ platform,
29
+ sessionKey: `test-${Math.random()}`,
30
+ shellName: platform === 'win32' ? 'cmd.exe' : 'bash',
31
+ },
32
+ events,
33
+ root,
34
+ }
35
+ }
36
+
37
+ function assertNoSkip(params, context) {
38
+ const original = structuredClone(params.message)
39
+ const result = applyShellRunGuard(params, context)
40
+ assert.equal(result.skipped.length, 0)
41
+ assert.deepEqual(params.message, original)
42
+ }
43
+
44
+ function markShellRun(params) {
45
+ Object.defineProperty(params, CondaRuntimeGuard.SHELL_RUN_GUARD, {
46
+ configurable: true,
47
+ value: true,
48
+ })
49
+ return params
50
+ }
51
+
52
+ function applyShellRunGuard(params, context) {
53
+ return CondaRuntimeGuard.applyCondaRuntimeGuard(markShellRun(params), context)
54
+ }
55
+
56
+ function assertNoUnmarkedInspection(params, context) {
57
+ const original = structuredClone(params.message)
58
+ const result = CondaRuntimeGuard.applyCondaRuntimeGuard(params, context)
59
+ assert.equal(result.skipped.length, 0)
60
+ assert.deepEqual(params.message, original)
61
+ }
62
+
63
+ test('guard skips protected package mutations and preserves unrelated package mutations in managed base', () => {
64
+ CondaRuntimeGuard.resetNoticeSessionsForTest()
65
+ const { context, events } = createContext()
66
+ const params = {
67
+ path: context.appPath,
68
+ message: [
69
+ 'conda install conda=25.5.1 --yes',
70
+ 'conda install some-package -y',
71
+ ],
72
+ }
73
+
74
+ const result = applyShellRunGuard(params, context)
75
+
76
+ assert.equal(result.skipped.length, 1)
77
+ assert.deepEqual(params.message, [NOOP, 'conda install some-package -y'])
78
+ assert.equal(events.some((event) => event.type === 'notify'), true)
79
+ assert.equal(events.some((event) => /conda install conda=25\.5\.1 --yes/.test(event.stream.raw || '')), true)
80
+ })
81
+
82
+ test('guard exits before inspection when conda.skip is true', () => {
83
+ CondaRuntimeGuard.resetNoticeSessionsForTest()
84
+ const { context, events } = createContext()
85
+ const params = {
86
+ conda: { skip: true },
87
+ path: context.appPath,
88
+ message: [
89
+ 'conda install python=3.12 -y',
90
+ 'conda update --all',
91
+ 'conda env update -n base',
92
+ ],
93
+ }
94
+
95
+ assertNoSkip(params, context)
96
+ assert.equal(events.length, 0)
97
+ })
98
+
99
+ test('guard preserves app environment wrappers and string conda paths', () => {
100
+ const { context } = createContext()
101
+ assertNoSkip({
102
+ conda: { path: '.env' },
103
+ path: context.appPath,
104
+ message: 'conda install python=3.12 -y',
105
+ }, context)
106
+ assertNoSkip({
107
+ conda: { name: 'app-env' },
108
+ path: context.appPath,
109
+ message: 'conda install python=3.12 -y',
110
+ }, context)
111
+ assertNoSkip({
112
+ conda: 'base',
113
+ path: context.appPath,
114
+ message: 'conda install python=3.12 -y',
115
+ }, context)
116
+ })
117
+
118
+ test('guard skips listed broad mutations when named wrapper selects base', () => {
119
+ const { context } = createContext()
120
+ const params = {
121
+ conda: { name: 'base' },
122
+ path: context.appPath,
123
+ message: [
124
+ 'conda update --all',
125
+ 'conda upgrade --all',
126
+ ],
127
+ }
128
+
129
+ const result = applyShellRunGuard(params, context)
130
+
131
+ assert.equal(result.skipped.length, 2)
132
+ assert.deepEqual(params.message, [NOOP, NOOP])
133
+ })
134
+
135
+ test('guard skips protected package specs for current-environment mutating subcommands', () => {
136
+ const { context } = createContext()
137
+ const params = {
138
+ path: context.appPath,
139
+ message: [
140
+ 'conda install --solver libmamba -c conda-forge conda=25.5.1 -y',
141
+ 'conda install conda-forge::conda-libmamba-solver>=25.4.0 -y',
142
+ 'conda remove python -y',
143
+ 'conda uninstall conda-libmamba-solver -y',
144
+ 'conda update python>=3.10 -y',
145
+ 'conda upgrade conda -y',
146
+ ],
147
+ }
148
+
149
+ const result = applyShellRunGuard(params, context)
150
+
151
+ assert.equal(result.skipped.length, params.message.length)
152
+ assert.deepEqual(params.message, Array(6).fill(NOOP))
153
+ })
154
+
155
+ test('guard preserves dry-run protected package and broad mutation commands', () => {
156
+ const { context } = createContext()
157
+ assertNoSkip({
158
+ path: context.appPath,
159
+ message: [
160
+ 'conda install conda=25.5.1 --dry-run -y',
161
+ 'conda install python=3.12 --dry-run -y',
162
+ 'conda update --all --dry-run',
163
+ ],
164
+ }, context)
165
+ })
166
+
167
+ test('guard preserves unprotected package specs for current-environment mutating subcommands', () => {
168
+ const { context } = createContext()
169
+ assertNoSkip({
170
+ path: context.appPath,
171
+ message: [
172
+ 'conda install some-package -y',
173
+ 'conda install ffmpeg -y',
174
+ 'conda install --file requirements.txt -y',
175
+ 'conda install -f specs.txt -y',
176
+ 'conda remove some-package -y',
177
+ 'conda uninstall some-package -y',
178
+ 'conda update some-package -y',
179
+ 'conda upgrade some-package -y',
180
+ ],
181
+ }, context)
182
+ })
183
+
184
+ test('command target arguments override wrapper target', () => {
185
+ const { context } = createContext()
186
+ const params = {
187
+ conda: { path: '.env' },
188
+ path: context.appPath,
189
+ message: 'conda remove -n base python',
190
+ }
191
+ const nonBaseTargets = {
192
+ path: context.appPath,
193
+ message: [
194
+ 'conda install -p ./env python=3.12 -y',
195
+ 'conda install -n app python=3.12 -y',
196
+ 'conda install --prefix ./env python=3.12 -y',
197
+ 'conda install --prefix=./env python=3.12 -y',
198
+ 'conda install --name app-env python=3.12 -y',
199
+ `conda install -p ${path.join(context.managedBasePrefix, 'envs', 'app')} python=3.12 -y`,
200
+ ],
201
+ }
202
+
203
+ const result = applyShellRunGuard(params, context)
204
+
205
+ assert.equal(result.skipped.length, 1)
206
+ assert.equal(params.message, NOOP)
207
+ assertNoSkip(nonBaseTargets, context)
208
+ })
209
+
210
+ test('explicit base target flags select managed base', () => {
211
+ const { context } = createContext()
212
+ const params = {
213
+ path: context.appPath,
214
+ message: [
215
+ 'conda install --name base python=3.12 -y',
216
+ 'conda install --name=base python=3.12 -y',
217
+ `conda install -p ${context.managedBasePrefix} python=3.12 -y`,
218
+ `conda install --prefix ${context.managedBasePrefix} python=3.12 -y`,
219
+ `conda install --prefix=${context.managedBasePrefix} python=3.12 -y`,
220
+ ],
221
+ }
222
+
223
+ const result = applyShellRunGuard(params, context)
224
+
225
+ assert.equal(result.skipped.length, 5)
226
+ assert.deepEqual(params.message, Array(5).fill(NOOP))
227
+ })
228
+
229
+ test('environment-targeting commands require explicit managed-base target', () => {
230
+ const { context } = createContext()
231
+ const params = {
232
+ path: context.appPath,
233
+ message: [
234
+ 'conda env create -f environment.yml',
235
+ 'conda env update -f environment.yml',
236
+ 'conda env remove',
237
+ 'conda create',
238
+ 'conda create -p ./env python=3.12 -y',
239
+ 'conda env update -n base -f environment.yml',
240
+ 'conda env create --name base',
241
+ 'conda create -n base python=3.12 -y',
242
+ 'conda env update -n base python=3.12',
243
+ 'conda env remove -n base',
244
+ `conda env remove -p ${context.managedBasePrefix}`,
245
+ ],
246
+ }
247
+
248
+ const result = applyShellRunGuard(params, context)
249
+
250
+ assert.equal(result.skipped.length, 4)
251
+ assert.deepEqual(params.message.slice(0, 7), [
252
+ 'conda env create -f environment.yml',
253
+ 'conda env update -f environment.yml',
254
+ 'conda env remove',
255
+ 'conda create',
256
+ 'conda create -p ./env python=3.12 -y',
257
+ 'conda env update -n base -f environment.yml',
258
+ 'conda env create --name base',
259
+ ])
260
+ assert.deepEqual(params.message.slice(7), [NOOP, NOOP, NOOP, NOOP])
261
+ })
262
+
263
+ test('guard preserves read-only conda commands', () => {
264
+ const { context } = createContext()
265
+ assertNoSkip({
266
+ path: context.appPath,
267
+ message: [
268
+ 'conda list',
269
+ 'conda info',
270
+ 'conda config --show',
271
+ 'conda search python',
272
+ ],
273
+ }, context)
274
+ })
275
+
276
+ test('direct executable paths identify conda commands but do not override app wrapper target', () => {
277
+ const { context, root } = createContext()
278
+ const managedConda = path.join(root, 'bin', 'miniforge', 'bin', 'conda')
279
+ const params = {
280
+ conda: { path: '.env' },
281
+ path: context.appPath,
282
+ message: [
283
+ `${managedConda} install python=3.12 -y`,
284
+ `${managedConda} install -n base python=3.12 -y`,
285
+ ],
286
+ }
287
+
288
+ const result = applyShellRunGuard(params, context)
289
+
290
+ assert.equal(result.skipped.length, 1)
291
+ assert.equal(params.message[0], `${managedConda} install python=3.12 -y`)
292
+ assert.equal(params.message[1], NOOP)
293
+ })
294
+
295
+ test('guard recognizes Windows conda wrapper executable names', () => {
296
+ const { context } = createContext('win32')
297
+ const params = {
298
+ path: context.appPath,
299
+ message: [
300
+ '"C:\\tools\\conda.exe" install conda=25.5.1 -y',
301
+ '"C:\\tools\\conda.bat" install python=3.12 -y',
302
+ '"C:\\tools\\conda.cmd" install conda-libmamba-solver -y',
303
+ ],
304
+ }
305
+
306
+ const result = applyShellRunGuard(params, context)
307
+
308
+ assert.equal(result.skipped.length, 3)
309
+ assert.deepEqual(params.message, [NOOP, NOOP, NOOP])
310
+ })
311
+
312
+ test('guard treats quoted bare conda executable forms as conda commands', () => {
313
+ const { context } = createContext()
314
+ const params = {
315
+ path: context.appPath,
316
+ message: '"conda" install python=3.12 -y',
317
+ }
318
+
319
+ const result = applyShellRunGuard(params, context)
320
+
321
+ assert.equal(result.skipped.length, 1)
322
+ assert.equal(params.message, NOOP)
323
+ })
324
+
325
+ test('guard rewrites only skippable segments in chained raw strings', () => {
326
+ const { context } = createContext()
327
+ const cases = [
328
+ [
329
+ 'echo before && conda update --all && echo after',
330
+ `echo before && ${NOOP} && echo after`,
331
+ ],
332
+ [
333
+ 'echo before || conda update --all || echo after',
334
+ `echo before || ${NOOP} || echo after`,
335
+ ],
336
+ [
337
+ 'echo before; conda update --all; echo after',
338
+ `echo before; ${NOOP}; echo after`,
339
+ ],
340
+ ]
341
+
342
+ for (const [message, expected] of cases) {
343
+ const params = {
344
+ path: context.appPath,
345
+ message,
346
+ }
347
+
348
+ const result = applyShellRunGuard(params, context)
349
+
350
+ assert.equal(result.skipped.length, 1)
351
+ assert.equal(params.message, expected)
352
+ }
353
+ })
354
+
355
+ test('guard does not corrupt redirection when rewriting raw strings', () => {
356
+ const { context } = createContext()
357
+ const params = {
358
+ path: context.appPath,
359
+ message: 'conda install python=3.12 -y 2>&1',
360
+ }
361
+
362
+ const result = applyShellRunGuard(params, context)
363
+
364
+ assert.equal(result.skipped.length, 1)
365
+ assert.equal(params.message, NOOP)
366
+ assert.equal(params.message.endsWith('&1'), false)
367
+ })
368
+
369
+ test('guard replaces whole raw string when unsafe control syntax cannot be safely separated', () => {
370
+ const { context } = createContext()
371
+ for (const message of [
372
+ 'conda install python=3.12 -y & echo after',
373
+ 'echo before & conda update --all',
374
+ 'echo before | conda update --all',
375
+ ]) {
376
+ const params = {
377
+ path: context.appPath,
378
+ message,
379
+ }
380
+
381
+ const result = applyShellRunGuard(params, context)
382
+
383
+ assert.equal(result.skipped.length, 1)
384
+ assert.equal(params.message, NOOP)
385
+ }
386
+ })
387
+
388
+ test('guard does not treat quoted ampersands or pipe characters as unsafe control syntax', () => {
389
+ const { context } = createContext()
390
+ const params = {
391
+ path: context.appPath,
392
+ message: 'echo "before & after" && conda update --all && echo "left | right"',
393
+ }
394
+
395
+ const result = applyShellRunGuard(params, context)
396
+
397
+ assert.equal(result.skipped.length, 1)
398
+ assert.equal(params.message, `echo "before & after" && ${NOOP} && echo "left | right"`)
399
+ })
400
+
401
+ test('guard emits one non-modal notice per session and terminal details per skip', () => {
402
+ CondaRuntimeGuard.resetNoticeSessionsForTest()
403
+ const { context, events } = createContext()
404
+ context.sessionKey = 'same-app-flow'
405
+
406
+ applyShellRunGuard({
407
+ path: context.appPath,
408
+ message: 'conda install conda=25.5.1 -y',
409
+ }, context)
410
+ applyShellRunGuard({
411
+ path: context.appPath,
412
+ message: 'conda install python=3.12 -y',
413
+ }, context)
414
+
415
+ const notifyEvents = events.filter((event) => event.type === 'notify')
416
+ const rawEvents = events.filter((event) => /\[Pinokio\] Command:/.test(event.stream.raw || ''))
417
+
418
+ assert.equal(notifyEvents.length, 1)
419
+ assert.equal(rawEvents.length, 2)
420
+ assert.equal(notifyEvents[0].stream.silent, true)
421
+ assert.equal(notifyEvents[0].stream.type, 'warning')
422
+ assert.match(notifyEvents[0].stream.html, /Pinokio skipped a Conda setup command/)
423
+ assert.match(notifyEvents[0].stream.html, /Pinokio already manages base Conda/)
424
+ assert.match(notifyEvents[0].stream.html, /Pinokio is continuing/)
425
+ assert.match(notifyEvents[0].stream.html, /conda install conda=25\.5\.1 -y/)
426
+ assert.match(notifyEvents[0].stream.html, /Details are in the terminal\/log/)
427
+ assert.match(rawEvents[0].stream.raw, /conda install conda=25\.5\.1 -y/)
428
+ assert.match(rawEvents[0].stream.raw, /targets Pinokio's protected base Conda setup/)
429
+ })
430
+
431
+ test('guard escapes skipped command text in notification html', () => {
432
+ CondaRuntimeGuard.resetNoticeSessionsForTest()
433
+ const { context, events } = createContext()
434
+ context.sessionKey = 'html-escape-flow'
435
+
436
+ applyShellRunGuard({
437
+ path: context.appPath,
438
+ message: 'conda install "conda=<script>" -y',
439
+ }, context)
440
+
441
+ const notifyEvent = events.find((event) => event.type === 'notify')
442
+ assert.ok(notifyEvent)
443
+ assert.equal(notifyEvent.stream.html.includes('<script>'), false)
444
+ assert.match(notifyEvent.stream.html, /&lt;script&gt;/)
445
+ })
446
+
447
+ test('guard classifies structured shell messages with flags', () => {
448
+ const { context } = createContext()
449
+ const appTarget = {
450
+ path: context.appPath,
451
+ message: {
452
+ _: ['conda', 'install', 'python=3.12'],
453
+ p: './env',
454
+ y: true,
455
+ },
456
+ }
457
+ const baseTarget = {
458
+ conda: { path: '.env' },
459
+ path: context.appPath,
460
+ message: {
461
+ _: ['conda', 'remove', 'python'],
462
+ n: 'base',
463
+ },
464
+ }
465
+
466
+ assert.equal(applyShellRunGuard(appTarget, context).skipped.length, 0)
467
+ assert.equal(applyShellRunGuard(baseTarget, context).skipped.length, 1)
468
+ assert.equal(baseTarget.message, NOOP)
469
+ })
470
+
471
+ test('guard does not inspect unmarked shell messages', () => {
472
+ const { context } = createContext()
473
+ assertNoUnmarkedInspection({
474
+ path: context.appPath,
475
+ message: [
476
+ 'conda install conda=25.5.1 -y',
477
+ 'conda update --all',
478
+ ],
479
+ }, context)
480
+ })
481
+
482
+ test('Shell.activate guards only shell.run-marked messages before prepending activation commands', async () => {
483
+ CondaRuntimeGuard.resetNoticeSessionsForTest()
484
+ const { context, events, root } = createContext()
485
+ const shell = new Shell({
486
+ homedir: root,
487
+ bracketedPasteSupport: {},
488
+ path: (...parts) => path.join(root, ...parts),
489
+ bin: {
490
+ path: (...parts) => path.join(root, 'bin', ...parts),
491
+ activationCommands: () => [],
492
+ },
493
+ })
494
+ shell.platform = 'darwin'
495
+ shell.shell = 'bash'
496
+ shell.group = 'activate-test'
497
+ shell.ondata = (stream, type) => events.push({ stream, type })
498
+
499
+ const unmarked = await shell.activate({
500
+ id: 'persistent-start-test',
501
+ path: context.appPath,
502
+ persistent: true,
503
+ message: ['conda install conda=25.5.1 -y'],
504
+ })
505
+
506
+ assert.equal(unmarked.message.length, 2)
507
+ assert.match(unmarked.message[0], /conda activate base/)
508
+ assert.equal(unmarked.message[1], 'conda install conda=25.5.1 -y')
509
+ assert.equal(events.some((event) => event.type === 'notify'), false)
510
+
511
+ const params = await shell.activate(markShellRun({
512
+ id: 'persistent-start-test',
513
+ path: context.appPath,
514
+ persistent: true,
515
+ message: ['conda install conda=25.5.1 -y'],
516
+ }))
517
+
518
+ assert.equal(params.message.length, 2)
519
+ assert.match(params.message[0], /conda activate base/)
520
+ assert.equal(params.message[1], NOOP)
521
+ assert.equal(events.some((event) => event.type === 'notify'), true)
522
+ })
523
+
524
+ test('Shell.activate scopes visual notices to the concrete shell flow', async () => {
525
+ CondaRuntimeGuard.resetNoticeSessionsForTest()
526
+ const { context, events, root } = createContext()
527
+ const createShell = (id, startTime) => {
528
+ const shell = new Shell({
529
+ homedir: root,
530
+ bracketedPasteSupport: {},
531
+ path: (...parts) => path.join(root, ...parts),
532
+ bin: {
533
+ path: (...parts) => path.join(root, 'bin', ...parts),
534
+ activationCommands: () => [],
535
+ },
536
+ })
537
+ shell.platform = 'darwin'
538
+ shell.shell = 'bash'
539
+ shell.group = 'same-script-group'
540
+ shell.id = id
541
+ shell.start_time = startTime
542
+ shell.ondata = (stream, type) => events.push({ stream, type })
543
+ return shell
544
+ }
545
+
546
+ await createShell('flow-one', 1).activate(markShellRun({
547
+ id: 'flow-one',
548
+ path: context.appPath,
549
+ message: 'conda install conda=25.5.1 -y',
550
+ }))
551
+ await createShell('flow-two', 2).activate(markShellRun({
552
+ id: 'flow-two',
553
+ path: context.appPath,
554
+ message: 'conda install conda=25.5.1 -y',
555
+ }))
556
+
557
+ assert.equal(events.filter((event) => event.type === 'notify').length, 2)
558
+ })