pinokiod 8.0.44 → 8.0.46

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.
@@ -2,10 +2,12 @@ const fs = require('fs')
2
2
  const os = require('os')
3
3
  const path = require('path')
4
4
  const Environment = require('../../kernel/environment')
5
+ const { createCurrentSystemSnapshot } = require('./log_redaction')
5
6
 
6
7
  const DEFAULT_TAIL_LINES = 800
7
8
  const MAX_SECTION_CHARS = 120000
8
9
  const SENSITIVE_ENV_KEY = /(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|API[_-]?KEY|APIKEY|CREDENTIAL|COOKIE|SESSION|AUTH|PRIVATE[_-]?KEY)/i
10
+ const SENSITIVE_FIELD_KEY = /(?:^|[_-])(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|API[_-]?KEY|APIKEY|CREDENTIAL|COOKIE|SESSION|AUTH|AUTHORIZATION|PRIVATE[_-]?KEY)(?:$|[_-])/i
9
11
 
10
12
  const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
11
13
 
@@ -52,6 +54,13 @@ class AppLogReportService {
52
54
  }
53
55
  }
54
56
 
57
+ let systemSpec = this.buildSystemSpec()
58
+ if (redact) {
59
+ const redacted = this.redactStructuredValue(systemSpec, { appRoot, workspaceRoot })
60
+ systemSpec = redacted.value
61
+ this.mergeCounts(totals, redacted.counts)
62
+ }
63
+
55
64
  const metadata = {
56
65
  app_id: appId,
57
66
  title: status.title || appId,
@@ -62,7 +71,7 @@ class AppLogReportService {
62
71
  arch: os.arch(),
63
72
  node: process.version,
64
73
  tail_count: tailLines,
65
- system_spec: this.buildSystemSpec(),
74
+ system_spec: systemSpec,
66
75
  redaction_mode: redact ? 'server_deterministic' : 'none',
67
76
  latest_session: sessionIndex.latest_session,
68
77
  session: selectedSession,
@@ -305,6 +314,27 @@ class AppLogReportService {
305
314
  }
306
315
  }
307
316
 
317
+ redactStructuredValue(input, context = {}) {
318
+ const counts = {}
319
+ const json = JSON.stringify(input, (key, value) => {
320
+ const normalizedKey = String(key).replace(/([a-z0-9])([A-Z])/g, '$1_$2')
321
+ if (key && SENSITIVE_FIELD_KEY.test(normalizedKey) && value !== undefined && value !== null && value !== '') {
322
+ counts.env_secrets = (counts.env_secrets || 0) + 1
323
+ return '[REDACTED_SECRET]'
324
+ }
325
+ if (typeof value === 'string') {
326
+ const redacted = this.redactText(value, context)
327
+ this.mergeCounts(counts, redacted.counts)
328
+ return redacted.text
329
+ }
330
+ return value
331
+ })
332
+ return {
333
+ value: JSON.parse(json),
334
+ counts
335
+ }
336
+ }
337
+
308
338
  renderMarkdown(metadata, sections, redactions) {
309
339
  const lines = [
310
340
  '# Issue Report',
@@ -418,9 +448,11 @@ class AppLogReportService {
418
448
  buildSystemSpec() {
419
449
  const kernel = this.kernel || {}
420
450
  const info = kernel.sysinfo && typeof kernel.sysinfo === 'object' ? kernel.sysinfo : {}
421
- return this.compactObject({
451
+ const version = this.readPinokioVersion()
452
+ const snapshot = createCurrentSystemSnapshot(kernel, version)
453
+ const spec = this.compactObject({
422
454
  pinokio: {
423
- version: this.readPinokioVersion(),
455
+ version,
424
456
  node: process.version,
425
457
  platform: kernel.platform || process.platform,
426
458
  arch: kernel.arch || process.arch
@@ -438,6 +470,16 @@ class AppLogReportService {
438
470
  gpus: this.sanitizeGpus(info.gpus),
439
471
  graphics: this.sanitizeGraphics(info.graphics)
440
472
  })
473
+
474
+ return {
475
+ ...spec,
476
+ hardware: {
477
+ ...spec.hardware,
478
+ gpu_driver: snapshot.gpu_driver ?? null,
479
+ gpu_target: snapshot.gpu_target ?? null
480
+ },
481
+ diagnostics: snapshot
482
+ }
441
483
  }
442
484
 
443
485
  sanitizeOsInfo(value) {
@@ -95,6 +95,28 @@ function createFallbackStateSnapshot(kernel) {
95
95
  }
96
96
  }
97
97
 
98
+ function createCurrentSystemSnapshot(kernel, version) {
99
+ kernel = kernel || {}
100
+ return {
101
+ platform: kernel.platform,
102
+ arch: kernel.arch,
103
+ running: kernel.api && kernel.api.running,
104
+ home: kernel.homedir,
105
+ vars: kernel.vars,
106
+ memory: kernel.memory,
107
+ procs: kernel.procs,
108
+ gpu: kernel.gpu,
109
+ gpus: kernel.gpus,
110
+ gpu_model: kernel.gpu_model,
111
+ gpu_driver: kernel.gpu_driver,
112
+ gpu_target: kernel.gpu_target,
113
+ ram: kernel.ram,
114
+ vram: kernel.vram,
115
+ version,
116
+ ...kernel.sysinfo
117
+ }
118
+ }
119
+
98
120
  function createCurrentLogSnapshot(kernel, version) {
99
121
  const liveShells = kernel && kernel.shell && Array.isArray(kernel.shell.shells)
100
122
  ? kernel.shell.shells
@@ -118,21 +140,10 @@ function createCurrentLogSnapshot(kernel, version) {
118
140
  }
119
141
  }
120
142
 
121
- const info = {
122
- platform: kernel.platform,
123
- arch: kernel.arch,
124
- running: kernel.api.running,
125
- home: kernel.homedir,
126
- vars: kernel.vars,
127
- memory: kernel.memory,
128
- procs: kernel.procs,
129
- gpu: kernel.gpu,
130
- gpus: kernel.gpus,
131
- version,
132
- ...kernel.sysinfo
143
+ return {
144
+ info: createCurrentSystemSnapshot(kernel, version),
145
+ states
133
146
  }
134
-
135
- return { info, states }
136
147
  }
137
148
 
138
149
  async function writeCurrentLogSnapshot(kernel, version) {
@@ -418,6 +429,7 @@ module.exports = {
418
429
  LOG_REDACTION_FILE_MAX_BYTES,
419
430
  isTopLevelRedactableLogPath,
420
431
  normalizeTailLineCount,
432
+ createCurrentSystemSnapshot,
421
433
  createCurrentLogSnapshot,
422
434
  writeCurrentLogSnapshot,
423
435
  normalizeLogRedactionOverrides,
@@ -6,7 +6,7 @@ document.addEventListener("DOMContentLoaded", () => {
6
6
 
7
7
  const newWindowButton = document.querySelector("#new-window");
8
8
  const agent = document.body.getAttribute("data-agent");
9
- if (newWindowButton && !headerCollapseOnly) {
9
+ if (newWindowButton) {
10
10
  newWindowButton.addEventListener("click", (event) => {
11
11
  if (agent === "electron") {
12
12
  window.open("/", "_blank", "pinokio");
@@ -4536,7 +4536,6 @@ aside .qr {
4536
4536
  body.main-sidebar-page > header.navheader #inspector,
4537
4537
  body.main-sidebar-page > header.navheader #mobile-link-button,
4538
4538
  body.main-sidebar-page > header.navheader .urlbar,
4539
- body.main-sidebar-page > header.navheader #close-window,
4540
4539
  body.main-sidebar-page > header.navheader .runner,
4541
4540
  body.main-sidebar-page > header.navheader h1 > .path,
4542
4541
  body.main-sidebar-page > header.navheader h1 > .nav-button,
@@ -234,13 +234,13 @@ body[data-vault-mode="app"] .vault-overview {
234
234
  white-space: nowrap;
235
235
  }
236
236
  .vault-button:hover { background: var(--task-soft); }
237
- .vault-button.review-primary {
237
+ .vault-button.primary {
238
238
  border-color: var(--task-accent-contrast);
239
239
  background: var(--task-accent-contrast);
240
240
  color: #101828;
241
241
  font-weight: 650;
242
242
  }
243
- .vault-button.review-primary:hover {
243
+ .vault-button.primary:hover {
244
244
  border-color: color-mix(in srgb, var(--task-accent-contrast) 88%, var(--task-panel));
245
245
  background: color-mix(in srgb, var(--task-accent-contrast) 88%, var(--task-panel));
246
246
  color: #101828;
@@ -56,15 +56,13 @@ const COPY = {
56
56
  scan_queued: "Waiting to start scan",
57
57
  scan_analyzing: "Analyzing large files",
58
58
  scan_finishing: "Finishing scan",
59
- scan_estimate_help: "Overall progress combines file scanning and large-file analysis",
60
59
  scan_counting_help: "The first scan cannot know its total until this pass finishes",
61
- scan_count_estimate_help: "Estimated from the exact file total and timing of the last completed scan",
62
- scan_file_progress_help: "This pass uses the exact current file total; overall timing is estimated from the last scan",
63
- scan_hash_progress_help: "File discovery is complete; the remaining large-file count is exact",
60
+ scan_file_progress_help: "Based on the exact current file total",
61
+ scan_hash_progress_help: "Based on the exact large-file count and current-file bytes",
64
62
  scan_finishing_help: "File analysis is complete; deduplication records are being verified",
65
- about_percent: "About {percent} percent.",
66
63
  exact_percent: "{percent} percent.",
67
64
  scan_checked: "{done} of {total} large files checked",
65
+ scan_file_bytes: "{done} of {total}",
68
66
  scan_files_checked: "{done} of {total} files checked",
69
67
  scan_folders: "folders checked",
70
68
  scan_files: "files checked",
@@ -142,7 +140,6 @@ const COPY = {
142
140
  files: "files",
143
141
  location: "location",
144
142
  deduplicate: "Deduplicate",
145
- deduplicate_all: "Deduplicate all",
146
143
  deduplicating: "Deduplicating files",
147
144
  deduplicating_file: "Deduplicating file",
148
145
  deduplication_progress: "{done} of {total} files",
@@ -468,8 +465,8 @@ const bulkDeduplicationAction = (items) => {
468
465
  const candidates = bulkDeduplicationItems(items, selection)
469
466
  if (!candidates.length) return ""
470
467
  const context = state.sourceId || ""
471
- const label = `${COPY.deduplicate_all}: ${countLabel(candidates.length)}`
472
- return `<button class="vault-button" type="button" data-deduplicate-all="${selection}" data-deduplicate-context="${attr(context)}" aria-label="${attr(label)}" title="${attr(label)}">${esc(COPY.deduplicate_all)}</button>`
468
+ const label = `${COPY.deduplicate} ${countLabel(candidates.length)}`
469
+ return `<button class="vault-button primary" type="button" data-deduplicate-all="${selection}" data-deduplicate-context="${attr(context)}" aria-label="${attr(label)}" title="${attr(label)}">${esc(label)}</button>`
473
470
  }
474
471
  const batchAction = (source) => {
475
472
  if (!source || source.kind === "virtual" || source.kind === "pinokio") return ""
@@ -854,7 +851,7 @@ const renderOverview = () => {
854
851
  const opportunity = activeScan
855
852
  ? `<span class="vault-summary-state"><i class="fa-solid fa-circle-notch fa-spin"></i>${esc(COPY.scanning)}</span>`
856
853
  : pendingBytes
857
- ? `<span class="vault-summary-state attention"><i class="fa-regular fa-copy"></i><strong>${esc(COPY.more_can_be_saved.replace("{size}", fmt(pendingBytes)))}</strong></span><button class="vault-button review-primary" type="button" id="btn-review-metric">${esc(COPY.review_files)}</button>`
854
+ ? `<span class="vault-summary-state attention"><i class="fa-regular fa-copy"></i><strong>${esc(COPY.more_can_be_saved.replace("{size}", fmt(pendingBytes)))}</strong></span>${state.view === "duplicates" ? "" : `<button class="vault-button${state.view === "independent" ? "" : " primary"}" type="button" id="btn-review-metric">${esc(COPY.review_files)}</button>`}`
858
855
  : `<span class="vault-summary-state"><i class="fa-regular fa-circle-check"></i>${esc(COPY.nothing_more_to_save)}</span>`
859
856
  const freshness = last ? `${COPY.scanned} ${timeAgo(last.ts)}` : COPY.not_scanned
860
857
  metrics.innerHTML = `
@@ -904,50 +901,48 @@ const renderOverview = () => {
904
901
  const verifying = scanPhase === "verifying"
905
902
  const hashTotal = scan.hash_total || 0
906
903
  const hashDone = Math.min(Math.max(0, hashTotal - (scan.queued || 0)), hashTotal)
907
- const hashRatio = hashTotal ? Math.min(1, hashDone / hashTotal) : 1
904
+ const currentFileSize = Math.max(0, Number(scan.current_file_size) || 0)
905
+ const currentFileBytes = Math.min(currentFileSize, Math.max(0, Number(scan.current_file_bytes) || 0))
906
+ const currentFileRatio = currentFileSize ? currentFileBytes / currentFileSize : 0
907
+ const hashRatio = hashTotal ? Math.min(1, (hashDone + currentFileRatio) / hashTotal) : 1
908
908
  const scanSource = scan.scope_id ? sourceById(scan.scope_id) : null
909
909
  const scanProgressLabel = scanSource
910
910
  ? COPY.scan_location.replace("{location}", scanSource.label)
911
911
  : COPY.scan_progress
912
912
  const phase = queued ? COPY.scan_queued : counting ? COPY.scan_counting : walking ? scanProgressLabel : verifying ? COPY.scan_finishing : COPY.scan_analyzing
913
- const rawCountWeight = Number.isFinite(scan.estimated_count_weight) ? scan.estimated_count_weight : 0.4
914
- const countWeight = Math.max(0, Math.min(0.98, rawCountWeight))
915
- const rawWalkWeight = Number.isFinite(scan.estimated_walk_weight) ? scan.estimated_walk_weight : 0.5
916
- const walkWeight = Math.max(0, Math.min(0.98 - countWeight, rawWalkWeight))
917
- const analysisWeight = Math.max(0, 1 - countWeight - walkWeight)
918
- const countEstimate = Number.isFinite(scan.count_estimate_files) && scan.count_estimate_files > 0
919
- ? scan.count_estimate_files
920
- : null
921
- const countRatio = countEstimate === null ? null : Math.min(1, (scan.counted_files || 0) / countEstimate)
922
913
  const totalFiles = Number.isFinite(scan.total_files) ? scan.total_files : null
923
914
  const details = counting
924
915
  ? [`${scan.counted_dirs || 0} ${COPY.scan_folders_found}`, `${scan.counted_files || 0} ${COPY.scan_files_found}`]
925
916
  : [`${scan.dirs || 0} ${COPY.scan_folders}`, totalFiles === null
926
917
  ? `${scan.files || 0} ${COPY.scan_files}`
927
918
  : COPY.scan_files_checked.replace("{done}", scan.files || 0).replace("{total}", totalFiles), fmt(scan.bytes_total || 0)]
928
- if (scan.current_file) details.push(`${COPY.analyzing} ${scan.current_file}`)
919
+ if (scan.current_file) {
920
+ const fileProgress = currentFileSize
921
+ ? ` (${COPY.scan_file_bytes.replace("{done}", fmt(currentFileBytes)).replace("{total}", fmt(currentFileSize))})`
922
+ : ""
923
+ details.push(`${COPY.analyzing} ${scan.current_file}${fileProgress}`)
924
+ }
929
925
  if (walking && scan.queued > 1) details.push(`${scan.queued} ${COPY.waiting}`)
930
926
  if (!walking && hashTotal) {
931
927
  details.push(COPY.scan_checked.replace("{done}", hashDone).replace("{total}", hashTotal))
932
928
  }
933
929
  let percent = ""
934
930
  let progress
935
- if (queued || (counting && countRatio === null) || (walking && totalFiles === null)) {
936
- const ariaText = `${details.join(" · ")}. ${COPY.scan_counting_help}.`
931
+ if (queued || counting || verifying || (walking && totalFiles === null)) {
932
+ const progressHelp = verifying
933
+ ? COPY.scan_finishing_help
934
+ : counting ? COPY.scan_counting_help : phase
935
+ const ariaText = `${details.join(" · ")}. ${progressHelp}.`
937
936
  progress = { determinate: false, ariaText }
938
937
  } else {
939
- const progressRatio = counting
940
- ? countRatio * countWeight
941
- : verifying
942
- ? 0.99
943
- : walking
944
- ? countWeight + ((totalFiles ? Math.min(1, (scan.files || 0) / totalFiles) : 1) * walkWeight)
945
- : Math.min(0.98, countWeight + walkWeight + (analysisWeight * hashRatio))
938
+ const progressRatio = walking
939
+ ? (totalFiles ? Math.min(1, (scan.files || 0) / totalFiles) : 1)
940
+ : hashRatio
946
941
  const boundedProgress = Math.max(0, Math.min(1, progressRatio))
947
942
  const progressValue = Math.round(boundedProgress * 1000) / 10
948
- const progressHelp = counting ? COPY.scan_count_estimate_help : verifying ? COPY.scan_finishing_help : walking ? COPY.scan_file_progress_help : COPY.scan_estimate_help
949
- const progressText = COPY.about_percent.replace("{percent}", progressValue)
950
- percent = `~${progressValue}%`
943
+ const progressHelp = walking ? COPY.scan_file_progress_help : COPY.scan_hash_progress_help
944
+ const progressText = COPY.exact_percent.replace("{percent}", progressValue)
945
+ percent = `${progressValue}%`
951
946
  progress = { determinate: true, value: progressValue, ratio: boundedProgress, help: progressHelp, text: progressText }
952
947
  }
953
948
  if (!scanState.querySelector(".vault-progress-track")) {
@@ -994,7 +989,9 @@ const renderResult = () => {
994
989
  }
995
990
  const info = state.scanResult
996
991
  const duplicateLabel = countLabel(info.count, COPY.duplicate.toLowerCase(), COPY.duplicates.toLowerCase())
997
- const review = info.count ? `<button class="vault-button review-primary" type="button" id="btn-review-result">${esc(COPY.review)} ${duplicateLabel}<i class="fa-solid fa-chevron-right"></i></button>` : ""
992
+ const review = info.count && state.view !== "duplicates"
993
+ ? `<button class="vault-button${state.view === "independent" ? "" : " primary"}" type="button" id="btn-review-result">${esc(COPY.review)} ${duplicateLabel}<i class="fa-solid fa-chevron-right"></i></button>`
994
+ : ""
998
995
  if (info.incomplete) {
999
996
  result.className = `vault-result show incomplete${state.scanProblemsOpen ? " expanded" : ""}`
1000
997
  const unreadableLabel = `${info.inaccessible} ${info.inaccessible === 1 ? COPY.scan_unreadable_path : COPY.scan_unreadable_paths}`
@@ -23,6 +23,8 @@
23
23
  <%- include('partials/main_sidebar', { selected: 'vault' }) %>
24
24
  </main>
25
25
  <script src="/Socket.js"></script>
26
+ <script src="/common.js"></script>
27
+ <script src="/nav.js" data-header-collapse-only></script>
26
28
  <script src="/storage-size.js"></script>
27
29
  <script src="/vault.js"></script>
28
30
  </body>
@@ -0,0 +1,75 @@
1
+ const assert = require('node:assert/strict')
2
+ const test = require('node:test')
3
+
4
+ const generator = require('../script/update-amd-gfx-targets')
5
+
6
+ test('AMD generator joins LLVM aliases, Mesa families, and PCI IDs', () => {
7
+ const llvm = `
8
+ def : ProcessorModel<"gfx803", Model, FeatureISAVersion8_0_3.Features>;
9
+ def : ProcessorModel<"polaris10", Model, FeatureISAVersion8_0_3.Features>;
10
+ `
11
+ const families = `
12
+ const char *ac_get_llvm_processor_name(enum radeon_family family) {
13
+ switch (family) {
14
+ case CHIP_POLARIS10:
15
+ return "polaris10";
16
+ case CHIP_NAVI23:
17
+ return "gfx1032";
18
+ }
19
+ }
20
+ const char *ac_get_ip_type_string(void) {}
21
+ `
22
+
23
+ const processors = generator.parseLlvmProcessorTargets(llvm)
24
+ const familyTargets = generator.parseMesaFamilyTargets(families, processors)
25
+ const entries = generator.parseMesaPciTargets(
26
+ 'CHIPSET(0x67DF, POLARIS10)\n',
27
+ familyTargets
28
+ )
29
+ generator.parseLinuxPciTargets(
30
+ '{0x1002, 0x73FF, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CHIP_DIMGREY_CAVEFISH},',
31
+ familyTargets,
32
+ { DIMGREY_CAVEFISH: 'NAVI23' },
33
+ entries
34
+ )
35
+
36
+ assert.equal(familyTargets.POLARIS10, 'gfx803')
37
+ assert.equal(entries['1002:67df'].target, 'gfx803')
38
+ assert.equal(entries['1002:73ff'].target, 'gfx1032')
39
+ })
40
+
41
+ test('AMD generator derives former Mesa family aliases from pinned comments', () => {
42
+ assert.deepEqual(generator.parseMesaFamilyAliases(`
43
+ CHIP_NAVI21, /* formerly "Sienna Cichlid" */
44
+ CHIP_NAVI23, /* formerly "Dimgrey Cavefish" */
45
+ `), {
46
+ SIENNA_CICHLID: 'NAVI21',
47
+ DIMGREY_CAVEFISH: 'NAVI23'
48
+ })
49
+ })
50
+
51
+ test('AMD generator rejects conflicting PCI family targets', () => {
52
+ assert.throws(() => generator.parseLinuxPciTargets(
53
+ [
54
+ '{0x1002, 0x73FF, 0, 0, 0, 0, CHIP_NAVI21},',
55
+ '{0x1002, 0x73FF, 0, 0, 0, 0, CHIP_NAVI23},'
56
+ ].join('\n'),
57
+ { NAVI21: 'gfx1030', NAVI23: 'gfx1032' },
58
+ {}
59
+ ), /Conflicting targets/)
60
+ })
61
+
62
+ test('AMD generator keeps exact libdrm revisions without promoting model names', () => {
63
+ const pciTargets = {}
64
+ const revisions = generator.parseLibdrmRevisionTargets(
65
+ '744c, c8, AMD Radeon RX 7900 XTX\n',
66
+ { 'radeon rx 7900 xtx': 'gfx1100' },
67
+ pciTargets
68
+ )
69
+
70
+ assert.deepEqual(revisions['1002:744c:c8'], {
71
+ target: 'gfx1100',
72
+ model: 'AMD Radeon RX 7900 XTX'
73
+ })
74
+ assert.equal(pciTargets['1002:744c'], undefined)
75
+ })
@@ -6,17 +6,19 @@ const test = require('node:test')
6
6
  const system = require('systeminformation')
7
7
 
8
8
  const amd = require('../kernel/gpu/amd')
9
+ const amdPci = require('../kernel/gpu/amd_pci')
9
10
  const gfxTargets = require('../kernel/gpu/amd_gfx_targets.json')
11
+ const pciTargets = require('../kernel/gpu/amd_pci_targets.json')
10
12
  const packageJson = require('../package.json')
11
13
  const Sysinfo = require('../kernel/sysinfo')
12
14
 
13
15
  const root = path.join(__dirname, '..')
14
16
 
15
17
  test('AMD gfx generated data is checked in and self describing', () => {
16
- assert.equal(
17
- gfxTargets.source,
18
- 'https://raw.githubusercontent.com/ROCm/ROCm/develop/docs/reference/gpu-arch-specs.rst'
19
- )
18
+ assert.match(gfxTargets.source, new RegExp(gfxTargets.source_revision))
19
+ assert.doesNotMatch(gfxTargets.source, /\/develop\//)
20
+ assert.match(gfxTargets.source_revision, /^[0-9a-f]{40}$/)
21
+ assert.match(gfxTargets.source_sha256, /^[0-9a-f]{64}$/)
20
22
  assert.equal(gfxTargets.generated_by, 'script/update-amd-gfx-targets.js')
21
23
 
22
24
  assert.equal(gfxTargets.entries['radeon rx 6800 xt'], 'gfx1030')
@@ -24,8 +26,32 @@ test('AMD gfx generated data is checked in and self describing', () => {
24
26
  assert.equal(gfxTargets.entries.mi210, 'gfx90a')
25
27
  })
26
28
 
29
+ test('AMD PCI generated data is pinned, auditable, and covers mobile and RDNA1 GPUs', () => {
30
+ assert.equal(pciTargets.generated_by, 'script/update-amd-gfx-targets.js')
31
+ assert.ok(pciTargets.sources.length >= 6)
32
+ for (const source of pciTargets.sources) {
33
+ assert.match(source.revision, /^[0-9a-f]{40}$/)
34
+ assert.match(source.sha256, /^[0-9a-f]{64}$/)
35
+ assert.match(source.url, new RegExp(source.revision))
36
+ assert.doesNotMatch(source.url, /\/(?:main|master|develop)\//)
37
+ }
38
+
39
+ assert.equal(pciTargets.entries['1002:731f'].target, 'gfx1010')
40
+ assert.equal(pciTargets.entries['1002:7340'].target, 'gfx1012')
41
+ assert.equal(pciTargets.entries['1002:73ff'].target, 'gfx1032')
42
+ assert.equal(pciTargets.entries['1002:73ff'].family, 'NAVI23')
43
+ assert.equal(pciTargets.revision_entries['1002:73ff:c3'].model, 'AMD Radeon RX 6600M')
44
+ assert.equal(pciTargets.revision_entries['1002:73ff:c3'].target, 'gfx1032')
45
+ assert.equal(pciTargets.revision_entries['1002:744c:c8'].target, 'gfx1100')
46
+ assert.equal(
47
+ Object.values(pciTargets.entries).some((entry) => entry.family === 'LIBDRM_MODEL_CONSENSUS'),
48
+ false
49
+ )
50
+ })
51
+
27
52
  test('AMD gfx target refresh is an explicit maintainer command', () => {
28
53
  assert.equal(packageJson.scripts['update:amd-gfx-targets'], 'node script/update-amd-gfx-targets.js')
54
+ assert.equal(packageJson.scripts['check:amd-gfx-targets'], 'node script/update-amd-gfx-targets.js --check')
29
55
  assert.equal(Object.prototype.hasOwnProperty.call(packageJson.scripts, 'update:amd-rocm-targets'), false)
30
56
 
31
57
  for (const script of ['preinstall', 'install', 'postinstall', 'prestart', 'start', 'poststart']) {
@@ -51,6 +77,13 @@ test('AMD gfx resolver maps common product names to exact gfx targets', () => {
51
77
  }
52
78
  })
53
79
 
80
+ test('AMD model resolver does not hand-maintain product gaps covered by PCI identity', () => {
81
+ assert.equal(gfxTargets.entries['radeon rx 6600m'], undefined)
82
+ assert.equal(amd.resolve_rocm_gfx_target('AMD Radeon RX 6600'), 'gfx1032')
83
+ assert.equal(amd.resolve_rocm_gfx_target('AMD Radeon RX 6600M'), null)
84
+ assert.equal(amd.resolve_rocm_gfx_target('AMD Radeon(TM) RX 6600M'), null)
85
+ })
86
+
54
87
  test('AMD gfx resolver rejects unknown products and preserves raw gfx targets', () => {
55
88
  const unknownModels = [
56
89
  'AMD Radeon RX 480',
@@ -88,6 +121,9 @@ test('AMD gpu_target resolution uses CPU brand only for generic Radeon Graphics
88
121
  })
89
122
 
90
123
  test('Sysinfo exposes AMD gpu_target for resolved discrete GPUs', async (t) => {
124
+ t.mock.method(amdPci, 'resolve_gpu_target', async () => {
125
+ throw new Error('PCI fallback should not run for a resolved model')
126
+ })
91
127
  t.mock.method(system, 'graphics', async () => ({
92
128
  controllers: [
93
129
  {
@@ -109,7 +145,63 @@ test('Sysinfo exposes AMD gpu_target for resolved discrete GPUs', async (t) => {
109
145
  assert.equal(sys.info.gpu_target, 'gfx1030')
110
146
  })
111
147
 
148
+ test('Sysinfo exposes AMD gpu_target for a PCI-resolved mobile GPU', async (t) => {
149
+ t.mock.method(amdPci, 'resolve_gpu_target', async (controller) => {
150
+ assert.equal(controller.model, 'AMD Radeon RX 6600M')
151
+ return 'gfx1032'
152
+ })
153
+ t.mock.method(system, 'graphics', async () => ({
154
+ controllers: [
155
+ {
156
+ vendor: 'Advanced Micro Devices, Inc.',
157
+ model: 'AMD Radeon(TM) Graphics',
158
+ vram: 512
159
+ },
160
+ {
161
+ vendor: 'Advanced Micro Devices, Inc.',
162
+ model: 'AMD Radeon RX 6600M',
163
+ vram: 8192
164
+ }
165
+ ],
166
+ displays: []
167
+ }))
168
+
169
+ const sys = new Sysinfo()
170
+ sys.info = {}
171
+
172
+ await sys.gpus()
173
+
174
+ assert.equal(sys.info.gpu, 'amd')
175
+ assert.equal(sys.info.gpu_model, 'amd radeon rx 6600m')
176
+ assert.equal(sys.info.gpu_target, 'gfx1032')
177
+ })
178
+
179
+ test('Sysinfo uses exact AMD PCI identity after model lookup misses', async (t) => {
180
+ t.mock.method(amdPci, 'resolve_gpu_target', async (controller) => {
181
+ assert.equal(controller.model, 'AMD Radeon RX 5700 XT')
182
+ return 'gfx1010'
183
+ })
184
+ t.mock.method(system, 'graphics', async () => ({
185
+ controllers: [
186
+ {
187
+ vendor: 'AMD',
188
+ model: 'AMD Radeon RX 5700 XT',
189
+ vram: 8192
190
+ }
191
+ ],
192
+ displays: []
193
+ }))
194
+
195
+ const sys = new Sysinfo()
196
+ sys.info = {}
197
+
198
+ await sys.gpus()
199
+
200
+ assert.equal(sys.info.gpu_target, 'gfx1010')
201
+ })
202
+
112
203
  test('Sysinfo leaves unresolved AMD gpu_target null', async (t) => {
204
+ t.mock.method(amdPci, 'resolve_gpu_target', async () => null)
113
205
  t.mock.method(system, 'graphics', async () => ({
114
206
  controllers: [
115
207
  {
@@ -133,6 +225,9 @@ test('Sysinfo leaves unresolved AMD gpu_target null', async (t) => {
133
225
 
134
226
  test('Sysinfo exposes AMD gpu_target through generic APU CPU fallback', async (t) => {
135
227
  let cpuBrandCalls = 0
228
+ t.mock.method(amdPci, 'resolve_gpu_target', async () => {
229
+ throw new Error('PCI fallback should not run for a resolved APU')
230
+ })
136
231
  t.mock.method(system, 'graphics', async () => ({
137
232
  controllers: [
138
233
  {
@@ -159,11 +254,14 @@ test('Sysinfo exposes AMD gpu_target through generic APU CPU fallback', async (t
159
254
  assert.equal(cpuBrandCalls, 1)
160
255
  })
161
256
 
162
- test('AMD gfx runtime resolver stays offline and data-only', () => {
163
- const source = fs.readFileSync(path.join(root, 'kernel', 'gpu', 'amd.js'), 'utf8')
257
+ test('AMD model fallback stays offline and data-only', () => {
258
+ const modelSource = fs.readFileSync(path.join(root, 'kernel', 'gpu', 'amd.js'), 'utf8')
259
+ const pciSource = fs.readFileSync(path.join(root, 'kernel', 'gpu', 'amd_pci.js'), 'utf8')
164
260
 
165
- assert.doesNotMatch(source, /require\(["'](?:node:)?https?["']\)/)
166
- assert.doesNotMatch(source, /require\(["'](?:node:)?child_process["']\)/)
167
- assert.doesNotMatch(source, /\b(fetch|axios|rocminfo|hipInfo|ze_info)\b/)
168
- assert.doesNotMatch(source, /\b(kfd|topology)\b/i)
261
+ assert.doesNotMatch(modelSource, /require\(["'](?:node:)?https?["']\)/)
262
+ assert.doesNotMatch(modelSource, /require\(["'](?:node:)?child_process["']\)/)
263
+ assert.doesNotMatch(modelSource, /\b(fetch|axios|rocminfo|hipInfo|ze_info)\b/)
264
+ assert.doesNotMatch(modelSource, /\b(kfd|topology)\b/i)
265
+ assert.doesNotMatch(pciSource, /require\(["'](?:node:)?https?["']\)/)
266
+ assert.doesNotMatch(pciSource, /\b(fetch|axios|OpenCL|rocminfo|hipInfo)\b/i)
169
267
  })