pinokiod 7.3.5 → 7.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/kernel/api/index.js +3 -2
  2. package/kernel/api/script/index.js +1 -0
  3. package/kernel/resource_usage/gpu.js +1078 -270
  4. package/kernel/resource_usage/index.js +9 -4
  5. package/package.json +2 -1
  6. package/server/index.js +14 -2
  7. package/server/public/nav.js +1 -1
  8. package/server/public/style.css +298 -191
  9. package/server/public/task-launcher.css +16 -20
  10. package/server/public/universal-launcher.css +0 -113
  11. package/server/public/universal-launcher.js +1 -1
  12. package/server/views/app.ejs +592 -298
  13. package/server/views/autolaunch.ejs +1 -1
  14. package/server/views/checkpoints.ejs +2 -6
  15. package/server/views/connect.ejs +1 -1
  16. package/server/views/explore.ejs +2 -1
  17. package/server/views/index.ejs +89 -60
  18. package/server/views/install.ejs +5 -7
  19. package/server/views/invalid_content.ejs +1 -1
  20. package/server/views/layout.ejs +8 -2
  21. package/server/views/logs.ejs +5 -27
  22. package/server/views/net.ejs +1 -1
  23. package/server/views/network.ejs +1 -1
  24. package/server/views/partials/fs_status.ejs +0 -8
  25. package/server/views/partials/main_sidebar.ejs +108 -44
  26. package/server/views/plugin_detail.ejs +1 -1
  27. package/server/views/plugins.ejs +1 -28
  28. package/server/views/screenshots.ejs +1 -1
  29. package/server/views/settings.ejs +2 -1
  30. package/server/views/setup.ejs +15 -1
  31. package/server/views/skills.ejs +1 -1
  32. package/server/views/task_builder.ejs +1 -1
  33. package/server/views/task_install.ejs +1 -1
  34. package/server/views/task_launch.ejs +1 -1
  35. package/server/views/task_list.ejs +1 -1
  36. package/server/views/tools.ejs +1 -1
  37. package/test/resource-usage-gpu.test.js +320 -70
  38. package/test/script-api.test.js +90 -0
@@ -1,10 +1,18 @@
1
1
  const assert = require("node:assert/strict")
2
+ const fs = require("node:fs")
3
+ const os = require("node:os")
4
+ const path = require("node:path")
2
5
  const test = require("node:test")
3
6
 
4
7
  const {
5
8
  GpuSampler,
6
- parseNvidiaCsv,
7
- parseWindowsGpuProcessMemoryCsv
9
+ NvmlGpuMemoryClient,
10
+ WindowsPdhGpuMemoryClient,
11
+ collectLinuxDrmFdinfoProcesses,
12
+ decodeWindowsMultiSz,
13
+ extractPidFromWindowsGpuInstance,
14
+ isDedicatedDrmMemoryRegion,
15
+ parseLinuxDrmFdinfo
8
16
  } = require("../kernel/resource_usage/gpu")
9
17
 
10
18
  const MIB = 1024 * 1024
@@ -16,111 +24,353 @@ function gpuProcess(pid, bytes) {
16
24
  }
17
25
  }
18
26
 
19
- test("parseNvidiaCsv treats nounits memory as MiB and sums duplicate PIDs", () => {
20
- const processes = parseNvidiaCsv([
21
- "1234, 256",
22
- "1234, 128",
23
- "5678, N/A"
27
+ test("extractPidFromWindowsGpuInstance handles full PDH counter paths", () => {
28
+ assert.equal(extractPidFromWindowsGpuInstance("app_pid_3456_phys_0"), 3456)
29
+ assert.equal(extractPidFromWindowsGpuInstance("\\\\HOST\\GPU Process Memory(pid_1234_luid_0x00000000_phys_0)\\Dedicated Usage"), 1234)
30
+ assert.equal(extractPidFromWindowsGpuInstance("\\\\HOST\\GPU Process Memory(_total)\\Dedicated Usage"), null)
31
+ })
32
+
33
+ test("decodeWindowsMultiSz decodes double-null UTF-16 string lists", () => {
34
+ const text = "one\u0000two\u0000\u0000"
35
+ const buffer = Buffer.from(text, "utf16le")
36
+
37
+ assert.deepEqual(decodeWindowsMultiSz(buffer, text.length), ["one", "two"])
38
+ })
39
+
40
+ test("Windows PDH binds formatted counter value with exported symbol name", () => {
41
+ const signatures = []
42
+ const fakeKoffi = {
43
+ struct: (name, fields) => ({ name, fields }),
44
+ load: (library) => {
45
+ assert.equal(library, "pdh.dll")
46
+ return {
47
+ func: (signature) => {
48
+ signatures.push(signature)
49
+ return () => 0
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ const client = new WindowsPdhGpuMemoryClient({ koffi: fakeKoffi })
56
+ client.init()
57
+
58
+ assert(signatures.some((signature) => signature.includes("PdhGetFormattedCounterValue(")))
59
+ assert(!signatures.some((signature) => signature.includes("PdhGetFormattedCounterValueW(")))
60
+ })
61
+
62
+ test("parseLinuxDrmFdinfo counts dedicated DRM memory regions only", () => {
63
+ const amdgpu = parseLinuxDrmFdinfo([
64
+ "drm-driver:\tamdgpu",
65
+ "drm-pdev:\t0000:03:00.0",
66
+ "drm-client-id:\t17",
67
+ "drm-memory-vram:\t4 MiB",
68
+ "drm-memory-gtt:\t128 MiB"
69
+ ].join("\n"))
70
+
71
+ const intel = parseLinuxDrmFdinfo([
72
+ "drm-driver:\ti915",
73
+ "drm-pdev:\t0000:00:02.0",
74
+ "drm-client-id:\t9",
75
+ "drm-resident-local0:\t8 MiB",
76
+ "drm-resident-system:\t64 MiB"
24
77
  ].join("\n"))
25
78
 
26
- assert.equal(processes.get(1234).usedGpuMemoryBytes, 384 * MIB)
27
- assert.equal(processes.has(5678), false)
79
+ assert.equal(amdgpu.dedicatedBytes, 4 * MIB)
80
+ assert.equal(intel.dedicatedBytes, 8 * MIB)
81
+ assert.equal(isDedicatedDrmMemoryRegion("vram"), true)
82
+ assert.equal(isDedicatedDrmMemoryRegion("local0"), true)
83
+ assert.equal(isDedicatedDrmMemoryRegion("gtt"), false)
84
+ assert.equal(isDedicatedDrmMemoryRegion("system"), false)
28
85
  })
29
86
 
30
- test("parseWindowsGpuProcessMemoryCsv extracts dedicated GPU bytes from process counter instances", () => {
31
- const processes = parseWindowsGpuProcessMemoryCsv([
32
- "\"(PDH-CSV 4.0)\",\"\\\\HOST\\GPU Process Memory(pid_1234_luid_0x00000000_0x00011111_phys_0)\\Dedicated Usage\",\"\\\\HOST\\GPU Process Memory(pid_1234_luid_0x00000000_0x00011111_phys_1)\\Dedicated Usage\",\"\\\\HOST\\GPU Process Memory(_total)\\Dedicated Usage\"",
33
- "\"06/18/2026 12:00:00.000\",\"268435456.000000\",\"134217728.000000\",\"1047527424.000000\"",
34
- "The command completed successfully."
35
- ].join("\r\n"))
87
+ test("collectLinuxDrmFdinfoProcesses deduplicates DRM client fds", async () => {
88
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pinokio-drm-fdinfo-"))
89
+ try {
90
+ await fs.promises.mkdir(path.join(root, "1234", "fdinfo"), { recursive: true })
91
+ await fs.promises.mkdir(path.join(root, "5678", "fdinfo"), { recursive: true })
92
+ await fs.promises.writeFile(path.join(root, "1234", "fdinfo", "3"), [
93
+ "drm-driver:\tamdgpu",
94
+ "drm-pdev:\t0000:03:00.0",
95
+ "drm-client-id:\t17",
96
+ "drm-resident-vram:\t12 MiB"
97
+ ].join("\n"))
98
+ await fs.promises.writeFile(path.join(root, "1234", "fdinfo", "4"), [
99
+ "drm-driver:\tamdgpu",
100
+ "drm-pdev:\t0000:03:00.0",
101
+ "drm-client-id:\t17",
102
+ "drm-resident-vram:\t11 MiB"
103
+ ].join("\n"))
104
+ await fs.promises.writeFile(path.join(root, "1234", "fdinfo", "5"), [
105
+ "drm-driver:\tamdgpu",
106
+ "drm-pdev:\t0000:03:00.0",
107
+ "drm-client-id:\t18",
108
+ "drm-resident-vram:\t5 MiB"
109
+ ].join("\n"))
110
+ await fs.promises.writeFile(path.join(root, "5678", "fdinfo", "9"), [
111
+ "drm-driver:\tamdgpu",
112
+ "drm-pdev:\t0000:03:00.0",
113
+ "drm-client-id:\t22",
114
+ "drm-resident-gtt:\t64 MiB"
115
+ ].join("\n"))
36
116
 
37
- assert.equal(processes.get(1234).usedGpuMemoryBytes, 384 * MIB)
38
- assert.equal(processes.has(999), false)
117
+ const processes = await collectLinuxDrmFdinfoProcesses([1234, 5678], { procRoot: root })
118
+
119
+ assert.equal(processes.get(1234).usedGpuMemoryBytes, 17 * MIB)
120
+ assert.equal(processes.has(5678), false)
121
+ } finally {
122
+ await fs.promises.rm(root, { recursive: true, force: true })
123
+ }
39
124
  })
40
125
 
41
- test("GpuSampler uses Windows GPU counters before nvidia-smi on Windows", async () => {
42
- const sampler = new GpuSampler({ platform: "win32" })
43
- let nvidiaCalls = 0
44
- sampler.collectWindowsGpuProcessMemory = async () => {
45
- return {
46
- provider: "windows-gpu-process-memory",
47
- processes: new Map([
126
+ test("GpuSampler uses Windows PDH only on Windows", async () => {
127
+ const sampler = new GpuSampler({
128
+ platform: "win32",
129
+ windowsPdhClient: {
130
+ collect: () => new Map([
48
131
  [1234, gpuProcess(1234, 500 * MIB)]
49
- ]),
50
- error: null
132
+ ])
51
133
  }
134
+ })
135
+
136
+ const snapshot = await sampler.collect(new Set([1234]))
137
+
138
+ assert.equal(snapshot.available, true)
139
+ assert.deepEqual(snapshot.providers, ["windows-pdh"])
140
+ assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 500 * MIB)
141
+ })
142
+
143
+ test("GpuSampler reports Windows VRAM unavailable when PDH fails", async () => {
144
+ const originalWarn = console.warn
145
+ console.warn = () => {}
146
+
147
+ try {
148
+ const sampler = new GpuSampler({
149
+ platform: "win32",
150
+ windowsPdhClient: {
151
+ collect: () => {
152
+ throw new Error("pdh unavailable")
153
+ }
154
+ }
155
+ })
156
+
157
+ const snapshot = await sampler.collect(new Set([1234]))
158
+
159
+ assert.equal(snapshot.available, false)
160
+ assert.deepEqual(snapshot.providers, ["windows-pdh"])
161
+ assert.equal(snapshot.processes.size, 0)
162
+ assert.equal(snapshot.errors[0].provider, "windows-pdh")
163
+ } finally {
164
+ console.warn = originalWarn
52
165
  }
53
- sampler.collectNvidia = async () => {
54
- nvidiaCalls += 1
55
- return {
56
- provider: "nvidia-smi",
57
- processes: new Map([
58
- [1234, gpuProcess(1234, 500 * MIB)]
59
- ]),
60
- error: null
61
- }
166
+ })
167
+
168
+ test("GpuSampler logs provider failures once per backoff window", async () => {
169
+ const originalWarn = console.warn
170
+ const warnings = []
171
+ console.warn = (...args) => {
172
+ warnings.push(args)
62
173
  }
63
- sampler.collectAmd = async () => null
64
174
 
65
- const snapshot = await sampler.collect()
175
+ try {
176
+ const sampler = new GpuSampler({
177
+ platform: "win32",
178
+ windowsPdhClient: {
179
+ collect: () => {
180
+ const error = new Error("pdh unavailable")
181
+ error.code = "PDH_TEST"
182
+ throw error
183
+ }
184
+ }
185
+ })
66
186
 
67
- assert.equal(nvidiaCalls, 0)
68
- assert.deepEqual(snapshot.providers, ["windows-gpu-process-memory"])
69
- assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 500 * MIB)
187
+ await sampler.collect(new Set([1234]))
188
+ await sampler.collect(new Set([1234]))
189
+
190
+ assert.equal(warnings.length, 1)
191
+ assert.equal(warnings[0][0], "[resource-usage:gpu] provider failed")
192
+ assert.deepEqual(warnings[0][1], {
193
+ provider: "windows-pdh",
194
+ platform: "win32",
195
+ pid_count: 1,
196
+ error: "pdh unavailable",
197
+ code: "PDH_TEST"
198
+ })
199
+ } finally {
200
+ console.warn = originalWarn
201
+ }
70
202
  })
71
203
 
72
- test("GpuSampler does not query nvidia-smi on Windows when OS counters fail", async () => {
73
- const sampler = new GpuSampler({ platform: "win32" })
74
- let nvidiaCalls = 0
75
- sampler.collectWindowsGpuProcessMemory = async () => ({
76
- provider: "windows-gpu-process-memory",
77
- processes: new Map(),
78
- error: "counter unavailable"
204
+ test("GpuSampler uses Linux DRM fdinfo before native library providers", async () => {
205
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pinokio-drm-sampler-"))
206
+ try {
207
+ await fs.promises.mkdir(path.join(root, "1234", "fdinfo"), { recursive: true })
208
+ await fs.promises.writeFile(path.join(root, "1234", "fdinfo", "3"), [
209
+ "drm-driver:\ti915",
210
+ "drm-pdev:\t0000:00:02.0",
211
+ "drm-client-id:\t4",
212
+ "drm-resident-local0:\t32 MiB"
213
+ ].join("\n"))
214
+
215
+ const sampler = new GpuSampler({ platform: "linux", procRoot: root })
216
+ let nvmlCalls = 0
217
+ let amdCalls = 0
218
+ let rocmCalls = 0
219
+ sampler.collectNvml = async () => {
220
+ nvmlCalls += 1
221
+ return null
222
+ }
223
+ sampler.collectAmdSmi = async () => {
224
+ amdCalls += 1
225
+ return null
226
+ }
227
+ sampler.collectRocmSmi = async () => {
228
+ rocmCalls += 1
229
+ return null
230
+ }
231
+
232
+ const snapshot = await sampler.collect(new Set([1234]))
233
+
234
+ assert.equal(nvmlCalls, 0)
235
+ assert.equal(amdCalls, 0)
236
+ assert.equal(rocmCalls, 0)
237
+ assert.deepEqual(snapshot.providers, ["linux-drm-fdinfo"])
238
+ assert.equal(snapshot.available, true)
239
+ assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 32 * MIB)
240
+ } finally {
241
+ await fs.promises.rm(root, { recursive: true, force: true })
242
+ }
243
+ })
244
+
245
+ test("GpuSampler uses NVML when fdinfo does not cover target PID", async () => {
246
+ const sampler = new GpuSampler({
247
+ platform: "linux",
248
+ nvmlClient: {
249
+ collect: () => new Map([
250
+ [1234, gpuProcess(1234, 700 * MIB)]
251
+ ])
252
+ }
79
253
  })
80
- sampler.collectNvidia = async () => {
81
- nvidiaCalls += 1
82
- return {
83
- provider: "nvidia-smi",
84
- processes: new Map([
85
- [1234, gpuProcess(1234, 300 * MIB)]
86
- ]),
87
- error: null
254
+ sampler.collectLinuxDrmFdinfo = async () => null
255
+ let amdCalls = 0
256
+ sampler.collectAmdSmi = async () => {
257
+ amdCalls += 1
258
+ return null
259
+ }
260
+
261
+ const snapshot = await sampler.collect(new Set([1234]))
262
+
263
+ assert.equal(amdCalls, 0)
264
+ assert.deepEqual(snapshot.providers, ["linux-nvml"])
265
+ assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 700 * MIB)
266
+ })
267
+
268
+ test("NvmlGpuMemoryClient sums the same PID across devices", () => {
269
+ const client = new NvmlGpuMemoryClient({ koffi: { sizeof: () => 1 } })
270
+ const compute = { name: "compute" }
271
+ const graphics = { name: "graphics" }
272
+ client.init = () => {}
273
+ client.getDeviceHandles = () => ["gpu0", "gpu1"]
274
+ client.functions = { compute, graphics, mps: null }
275
+ client.collectProcessList = (device, entry) => {
276
+ const samples = {
277
+ "gpu0:compute": [{ pid: 1234, usedGpuMemory: 300 * MIB }],
278
+ "gpu0:graphics": [{ pid: 1234, usedGpuMemory: 250 * MIB }],
279
+ "gpu1:compute": [{ pid: 1234, usedGpuMemory: 400 * MIB }],
280
+ "gpu1:graphics": [{ pid: 1234, usedGpuMemory: 100 * MIB }]
88
281
  }
282
+ return samples[`${device}:${entry && entry.name}`] || []
89
283
  }
90
- sampler.collectAmd = async () => null
91
284
 
92
- const snapshot = await sampler.collect()
285
+ const processes = client.collect([1234])
93
286
 
94
- assert.equal(nvidiaCalls, 0)
95
- assert.equal(snapshot.available, false)
96
- assert.deepEqual(snapshot.providers, ["windows-gpu-process-memory"])
97
- assert.equal(snapshot.processes.has(1234), false)
287
+ assert.equal(processes.get(1234).usedGpuMemoryBytes, 700 * MIB)
288
+ })
289
+
290
+
291
+ test("GpuSampler uses AMD SMI after fdinfo and NVML miss", async () => {
292
+ const sampler = new GpuSampler({
293
+ platform: "linux",
294
+ amdSmiClient: {
295
+ collect: () => new Map([
296
+ [2222, gpuProcess(2222, 300 * MIB)]
297
+ ])
298
+ }
299
+ })
300
+ sampler.collectLinuxDrmFdinfo = async () => null
301
+ sampler.collectNvml = async () => null
302
+ let rocmCalls = 0
303
+ sampler.collectRocmSmi = async () => {
304
+ rocmCalls += 1
305
+ return null
306
+ }
307
+
308
+ const snapshot = await sampler.collect(new Set([2222]))
309
+
310
+ assert.equal(rocmCalls, 0)
311
+ assert.deepEqual(snapshot.providers, ["linux-amdsmi"])
312
+ assert.equal(snapshot.processes.get(2222).usedGpuMemoryBytes, 300 * MIB)
313
+ })
314
+
315
+ test("GpuSampler uses ROCm SMI after AMD SMI misses", async () => {
316
+ const sampler = new GpuSampler({
317
+ platform: "linux",
318
+ rocmSmiClient: {
319
+ collect: () => new Map([
320
+ [3333, gpuProcess(3333, 200 * MIB)]
321
+ ])
322
+ }
323
+ })
324
+ sampler.collectLinuxDrmFdinfo = async () => null
325
+ sampler.collectNvml = async () => null
326
+ sampler.collectAmdSmi = async () => null
327
+
328
+ const snapshot = await sampler.collect(new Set([3333]))
329
+
330
+ assert.deepEqual(snapshot.providers, ["linux-rocm-smi"])
331
+ assert.equal(snapshot.processes.get(3333).usedGpuMemoryBytes, 200 * MIB)
98
332
  })
99
333
 
100
334
  test("GpuSampler merges overlapping provider samples by PID without double-counting", async () => {
101
335
  const sampler = new GpuSampler({ platform: "linux" })
102
- sampler.collectNvidia = async () => ({
103
- provider: "nvidia-smi",
336
+ sampler.collectLinuxDrmFdinfo = async () => ({
337
+ provider: "linux-drm-fdinfo",
104
338
  processes: new Map([
105
- [1234, gpuProcess(1234, 300 * MIB)],
106
- [3333, gpuProcess(3333, 200 * MIB)]
339
+ [1234, gpuProcess(1234, 300 * MIB)]
107
340
  ]),
108
341
  error: null
109
342
  })
110
- sampler.collectAmd = async () => ({
111
- provider: "amd-smi",
343
+ sampler.collectNvml = async () => ({
344
+ provider: "linux-nvml",
112
345
  processes: new Map([
113
346
  [1234, gpuProcess(1234, 500 * MIB)],
114
- [2222, gpuProcess(2222, 100 * MIB)]
347
+ [3333, gpuProcess(3333, 200 * MIB)]
115
348
  ]),
116
349
  error: null
117
350
  })
351
+ sampler.collectAmdSmi = async () => null
352
+ sampler.collectRocmSmi = async () => null
118
353
 
119
354
  const snapshot = await sampler.collect()
120
355
 
121
356
  assert.equal(snapshot.available, true)
122
- assert.deepEqual(snapshot.providers, ["nvidia-smi", "amd-smi"])
357
+ assert.deepEqual(snapshot.providers, ["linux-drm-fdinfo", "linux-nvml"])
123
358
  assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 500 * MIB)
124
- assert.equal(snapshot.processes.get(2222).usedGpuMemoryBytes, 100 * MIB)
125
359
  assert.equal(snapshot.processes.get(3333).usedGpuMemoryBytes, 200 * MIB)
126
360
  })
361
+
362
+ test("GpuSampler does not collect VRAM providers on macOS", async () => {
363
+ const sampler = new GpuSampler({ platform: "darwin" })
364
+ sampler.collectNvml = async () => {
365
+ throw new Error("NVML should not be queried on macOS")
366
+ }
367
+ sampler.collectAmdSmi = async () => {
368
+ throw new Error("AMD SMI should not be queried on macOS")
369
+ }
370
+
371
+ const snapshot = await sampler.collect()
372
+
373
+ assert.equal(snapshot.available, false)
374
+ assert.deepEqual(snapshot.providers, [])
375
+ assert.equal(snapshot.processes.size, 0)
376
+ })
@@ -1,7 +1,10 @@
1
1
  const assert = require('node:assert/strict')
2
+ const fs = require('node:fs')
3
+ const os = require('node:os')
2
4
  const path = require('node:path')
3
5
  const test = require('node:test')
4
6
 
7
+ const Api = require('../kernel/api')
5
8
  const Script = require('../kernel/api/script')
6
9
 
7
10
  test('script.restart stops explicit target and schedules start with explicit params', async () => {
@@ -95,3 +98,90 @@ test('script.restart self restart preserves session id and parent args', async (
95
98
  params: { prompt: 'again' }
96
99
  })
97
100
  })
101
+
102
+ test('script.run refreshes git mapping before resolving remote dependency uri', async () => {
103
+ const script = new Script()
104
+ const calls = []
105
+ let initialized = false
106
+ const remoteUri = 'https://github.com/example/dependency.pinokio.git/start.js'
107
+ const resolvedPath = '/pinokio/api/github_com_example_dependency_pinokio_git/start.js'
108
+ const kernel = {
109
+ path: (...parts) => path.join('/pinokio', ...parts),
110
+ bin: {
111
+ sh: async () => {
112
+ throw new Error('should not clone when init maps existing dependency')
113
+ }
114
+ },
115
+ api: {
116
+ userdir: '/pinokio/api',
117
+ running: {},
118
+ init: async () => {
119
+ calls.push('init')
120
+ initialized = true
121
+ },
122
+ filePath: (uri) => {
123
+ calls.push('filePath')
124
+ assert.equal(initialized, true)
125
+ assert.equal(uri, remoteUri)
126
+ return resolvedPath
127
+ },
128
+ resolveGitURI: (uri) => {
129
+ calls.push('resolveGitURI')
130
+ assert.equal(initialized, true)
131
+ assert.equal(uri, remoteUri)
132
+ return resolvedPath
133
+ },
134
+ getGitURI: () => 'https://github.com/example/dependency.pinokio.git',
135
+ process: (request, done) => {
136
+ calls.push('process')
137
+ assert.equal(request.uri, remoteUri)
138
+ done({ input: { ok: true } })
139
+ }
140
+ }
141
+ }
142
+
143
+ const result = await script.run({
144
+ params: {
145
+ uri: remoteUri,
146
+ params: { prompt: 'go' }
147
+ }
148
+ }, () => {}, kernel)
149
+
150
+ assert.deepEqual(result, { ok: true })
151
+ assert.deepEqual(calls, ['init', 'filePath', 'resolveGitURI', 'init', 'process'])
152
+ })
153
+
154
+ test('api.linkGit keeps previous git mapping until refresh completes', async () => {
155
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pinokio-linkgit-'))
156
+ const userdir = path.join(root, 'api')
157
+ const repo = path.join(userdir, 'demo')
158
+ fs.mkdirSync(path.join(repo, '.git'), { recursive: true })
159
+ fs.writeFileSync(path.join(repo, '.git', 'config'), [
160
+ '[remote "origin"]',
161
+ '\turl = https://github.com/example/demo.git',
162
+ '\tfetch = +refs/heads/*:refs/remotes/origin/*',
163
+ ''
164
+ ].join('\n'))
165
+
166
+ try {
167
+ const api = new Api({
168
+ homedir: root,
169
+ path: (...parts) => path.join(root, ...parts)
170
+ })
171
+ api.userdir = userdir
172
+ api.gitPath = {
173
+ 'https://github.com/example/old.git': path.join(userdir, 'old')
174
+ }
175
+
176
+ const refresh = api.linkGit()
177
+ assert.deepEqual(api.gitPath, {
178
+ 'https://github.com/example/old.git': path.join(userdir, 'old')
179
+ })
180
+ await refresh
181
+ assert.deepEqual(api.gitPath, {
182
+ 'https://github.com/example/demo.git': repo
183
+ })
184
+ } finally {
185
+ fs.rmSync(root, { recursive: true, force: true })
186
+ }
187
+ })