pinokiod 8.0.45 → 8.0.47

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,
@@ -29,7 +29,10 @@ body.dark.vault-page {
29
29
  min-height: 0;
30
30
  overflow: hidden;
31
31
  }
32
- .vault-embed-main .vault-shell { width: 100%; }
32
+ .vault-embed-main .vault-shell {
33
+ width: 100%;
34
+ height: 100%;
35
+ }
33
36
  body.vault-page .task-container {
34
37
  display: flex;
35
38
  min-height: 0;
@@ -924,6 +927,8 @@ body.dark .vault-row-action .vault-text-button:hover:not(:disabled) {
924
927
  }
925
928
  @media (max-width: 820px) {
926
929
  body.vault-page .task-container { display: block; overflow-y: auto; }
930
+ .vault-embed-main { overflow-y: auto; }
931
+ .vault-embed-main .vault-shell { height: auto; }
927
932
  .vault-shell,
928
933
  .vault-shell .task-shell-body,
929
934
  .vault-body { height: auto; overflow: visible; }
@@ -42,6 +42,7 @@ const COPY = {
42
42
  scan_app: "Scan this app",
43
43
  scan_again: "Scan again",
44
44
  scanning: "Scanning…",
45
+ minimum_file_size: "Minimum file size to scan",
45
46
  scanning_elsewhere: "Another location is being scanned",
46
47
  scanning_elsewhere_hint: "This app can be scanned when the current scan finishes.",
47
48
  vault_options: "Save space options",
@@ -56,15 +57,13 @@ const COPY = {
56
57
  scan_queued: "Waiting to start scan",
57
58
  scan_analyzing: "Analyzing large files",
58
59
  scan_finishing: "Finishing scan",
59
- scan_estimate_help: "Overall progress combines file scanning and large-file analysis",
60
60
  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",
61
+ scan_file_progress_help: "Based on the exact current file total",
62
+ scan_hash_progress_help: "Based on the exact large-file count and current-file bytes",
64
63
  scan_finishing_help: "File analysis is complete; deduplication records are being verified",
65
- about_percent: "About {percent} percent.",
66
64
  exact_percent: "{percent} percent.",
67
65
  scan_checked: "{done} of {total} large files checked",
66
+ scan_file_bytes: "{done} of {total}",
68
67
  scan_files_checked: "{done} of {total} files checked",
69
68
  scan_folders: "folders checked",
70
69
  scan_files: "files checked",
@@ -170,7 +169,7 @@ const COPY = {
170
169
  no_activity_hint: "Scans and actions will be recorded here.",
171
170
  view_all: "View all files",
172
171
  show_all_locations: "Show all locations",
173
- tracked_note: "Only files 100 MB and larger appear here. Files keep their current locations.",
172
+ tracked_note: "Only files {size} and larger appear here. Files keep their current locations.",
174
173
  duplicate_note: "Only files waiting for review are shown.",
175
174
  reclaimable_note: "These copies are no longer used by any configured location. Deleting them frees disk space.",
176
175
  converted: "Deduplicated",
@@ -199,6 +198,11 @@ const statusUrl = (progress = false) => {
199
198
  return `/info/dedup${suffix ? `?${suffix}` : ""}`
200
199
  }
201
200
  const reviewedScanKey = `pinokio:vault:reviewed-scan:${SCOPE_ID || "global"}`
201
+ const candidateSizeKey = "pinokio:vault:candidate-size"
202
+ const candidateSizeBase = document.body.dataset.platform === "win32" ? 1024 : 1000
203
+ const candidateSizeOptions = [10, 50, 100, 500]
204
+ .map((value) => value * candidateSizeBase ** 2)
205
+ .concat(candidateSizeBase ** 3)
202
206
 
203
207
  const state = {
204
208
  data: null,
@@ -226,6 +230,10 @@ const esc = (value) => String(value == null ? "" : value).replace(/[&<>"']/g, (c
226
230
  }[char]))
227
231
  const attr = esc
228
232
  const fmt = window.PinokioFormatStorageSize
233
+ const candidateSize = () => {
234
+ const value = Number(el("vault-candidate-size").value)
235
+ return candidateSizeOptions.includes(value) ? value : candidateSizeOptions[2]
236
+ }
229
237
  const countLabel = (count, singular = COPY.file, plural = COPY.files) => `${count} ${count === 1 ? singular : plural}`
230
238
  const basename = (value) => String(value || "").split(/[\\/]/).filter(Boolean).pop() || ""
231
239
  const dirname = (value) => {
@@ -877,6 +885,7 @@ const renderOverview = () => {
877
885
  ? `<i class="fa-solid fa-circle-notch fa-spin"></i>${esc(COPY.scanning)}`
878
886
  : `<i class="fa-solid fa-rotate"></i>${esc(last ? COPY.scan_again : (IS_APP_MODE ? COPY.scan_app : COPY.scan))}`
879
887
  el("btn-scan").disabled = activeScan
888
+ el("vault-candidate-size").disabled = activeScan
880
889
  const optionsButton = el("btn-vault-options")
881
890
  const repairTitle = el("vault-repair-title")
882
891
  const repairDescription = el("vault-repair-description")
@@ -903,50 +912,48 @@ const renderOverview = () => {
903
912
  const verifying = scanPhase === "verifying"
904
913
  const hashTotal = scan.hash_total || 0
905
914
  const hashDone = Math.min(Math.max(0, hashTotal - (scan.queued || 0)), hashTotal)
906
- const hashRatio = hashTotal ? Math.min(1, hashDone / hashTotal) : 1
915
+ const currentFileSize = Math.max(0, Number(scan.current_file_size) || 0)
916
+ const currentFileBytes = Math.min(currentFileSize, Math.max(0, Number(scan.current_file_bytes) || 0))
917
+ const currentFileRatio = currentFileSize ? currentFileBytes / currentFileSize : 0
918
+ const hashRatio = hashTotal ? Math.min(1, (hashDone + currentFileRatio) / hashTotal) : 1
907
919
  const scanSource = scan.scope_id ? sourceById(scan.scope_id) : null
908
920
  const scanProgressLabel = scanSource
909
921
  ? COPY.scan_location.replace("{location}", scanSource.label)
910
922
  : COPY.scan_progress
911
923
  const phase = queued ? COPY.scan_queued : counting ? COPY.scan_counting : walking ? scanProgressLabel : verifying ? COPY.scan_finishing : COPY.scan_analyzing
912
- const rawCountWeight = Number.isFinite(scan.estimated_count_weight) ? scan.estimated_count_weight : 0.4
913
- const countWeight = Math.max(0, Math.min(0.98, rawCountWeight))
914
- const rawWalkWeight = Number.isFinite(scan.estimated_walk_weight) ? scan.estimated_walk_weight : 0.5
915
- const walkWeight = Math.max(0, Math.min(0.98 - countWeight, rawWalkWeight))
916
- const analysisWeight = Math.max(0, 1 - countWeight - walkWeight)
917
- const countEstimate = Number.isFinite(scan.count_estimate_files) && scan.count_estimate_files > 0
918
- ? scan.count_estimate_files
919
- : null
920
- const countRatio = countEstimate === null ? null : Math.min(1, (scan.counted_files || 0) / countEstimate)
921
924
  const totalFiles = Number.isFinite(scan.total_files) ? scan.total_files : null
922
925
  const details = counting
923
926
  ? [`${scan.counted_dirs || 0} ${COPY.scan_folders_found}`, `${scan.counted_files || 0} ${COPY.scan_files_found}`]
924
927
  : [`${scan.dirs || 0} ${COPY.scan_folders}`, totalFiles === null
925
928
  ? `${scan.files || 0} ${COPY.scan_files}`
926
929
  : COPY.scan_files_checked.replace("{done}", scan.files || 0).replace("{total}", totalFiles), fmt(scan.bytes_total || 0)]
927
- if (scan.current_file) details.push(`${COPY.analyzing} ${scan.current_file}`)
930
+ if (scan.current_file) {
931
+ const fileProgress = currentFileSize
932
+ ? ` (${COPY.scan_file_bytes.replace("{done}", fmt(currentFileBytes)).replace("{total}", fmt(currentFileSize))})`
933
+ : ""
934
+ details.push(`${COPY.analyzing} ${scan.current_file}${fileProgress}`)
935
+ }
928
936
  if (walking && scan.queued > 1) details.push(`${scan.queued} ${COPY.waiting}`)
929
937
  if (!walking && hashTotal) {
930
938
  details.push(COPY.scan_checked.replace("{done}", hashDone).replace("{total}", hashTotal))
931
939
  }
932
940
  let percent = ""
933
941
  let progress
934
- if (queued || (counting && countRatio === null) || (walking && totalFiles === null)) {
935
- const ariaText = `${details.join(" · ")}. ${COPY.scan_counting_help}.`
942
+ if (queued || counting || verifying || (walking && totalFiles === null)) {
943
+ const progressHelp = verifying
944
+ ? COPY.scan_finishing_help
945
+ : counting ? COPY.scan_counting_help : phase
946
+ const ariaText = `${details.join(" · ")}. ${progressHelp}.`
936
947
  progress = { determinate: false, ariaText }
937
948
  } else {
938
- const progressRatio = counting
939
- ? countRatio * countWeight
940
- : verifying
941
- ? 0.99
942
- : walking
943
- ? countWeight + ((totalFiles ? Math.min(1, (scan.files || 0) / totalFiles) : 1) * walkWeight)
944
- : Math.min(0.98, countWeight + walkWeight + (analysisWeight * hashRatio))
949
+ const progressRatio = walking
950
+ ? (totalFiles ? Math.min(1, (scan.files || 0) / totalFiles) : 1)
951
+ : hashRatio
945
952
  const boundedProgress = Math.max(0, Math.min(1, progressRatio))
946
953
  const progressValue = Math.round(boundedProgress * 1000) / 10
947
- const progressHelp = counting ? COPY.scan_count_estimate_help : verifying ? COPY.scan_finishing_help : walking ? COPY.scan_file_progress_help : COPY.scan_estimate_help
948
- const progressText = COPY.about_percent.replace("{percent}", progressValue)
949
- percent = `~${progressValue}%`
954
+ const progressHelp = walking ? COPY.scan_file_progress_help : COPY.scan_hash_progress_help
955
+ const progressText = COPY.exact_percent.replace("{percent}", progressValue)
956
+ percent = `${progressValue}%`
950
957
  progress = { determinate: true, value: progressValue, ratio: boundedProgress, help: progressHelp, text: progressText }
951
958
  }
952
959
  if (!scanState.querySelector(".vault-progress-track")) {
@@ -1118,7 +1125,7 @@ const render = () => {
1118
1125
  el("vault-explorer").style.display = "none"
1119
1126
  return
1120
1127
  }
1121
- el("vault-explorer").style.display = "grid"
1128
+ el("vault-explorer").style.display = ""
1122
1129
  const items = buildItems()
1123
1130
  renderViews(items)
1124
1131
  renderLocations(items)
@@ -1137,7 +1144,9 @@ const render = () => {
1137
1144
  } else {
1138
1145
  el("vault-pane-footer").textContent = state.view === "duplicates"
1139
1146
  ? COPY.duplicate_note
1140
- : state.view === "reclaimable" ? COPY.reclaimable_note : COPY.tracked_note
1147
+ : state.view === "reclaimable"
1148
+ ? COPY.reclaimable_note
1149
+ : COPY.tracked_note.replace("{size}", fmt(candidateSize()))
1141
1150
  }
1142
1151
  }
1143
1152
 
@@ -1472,7 +1481,11 @@ document.addEventListener("click", async (event) => {
1472
1481
  state.scanProblemsOpen = false
1473
1482
  state.feedback = null
1474
1483
  try {
1475
- const result = await post({ action: "scan", scope_id: SCOPE_ID })
1484
+ const result = await post({
1485
+ action: "scan",
1486
+ scope_id: SCOPE_ID,
1487
+ candidate_size: candidateSize()
1488
+ })
1476
1489
  if (result.error) throw new Error(result.error)
1477
1490
  await refresh()
1478
1491
  } catch (error) {
@@ -1588,12 +1601,29 @@ document.addEventListener("input", (event) => {
1588
1601
  renderActionProgress()
1589
1602
  })
1590
1603
  document.addEventListener("change", (event) => {
1604
+ if (event.target.id === "vault-candidate-size") {
1605
+ try { localStorage.setItem(candidateSizeKey, String(candidateSize())) } catch (error) {}
1606
+ render()
1607
+ return
1608
+ }
1591
1609
  if (event.target.id !== "vault-status-filter") return
1592
1610
  state.statusFilter = event.target.value
1593
1611
  render()
1594
1612
  })
1595
1613
 
1596
1614
  el("btn-scan").textContent = COPY.scan
1615
+ const candidateSizeSelect = el("vault-candidate-size")
1616
+ candidateSizeSelect.setAttribute("aria-label", COPY.minimum_file_size)
1617
+ candidateSizeSelect.innerHTML = candidateSizeOptions
1618
+ .map((size) => `<option value="${size}">${fmt(size)}+</option>`)
1619
+ .join("")
1620
+ candidateSizeSelect.value = String(candidateSizeOptions[2])
1621
+ try {
1622
+ const storedCandidateSize = Number(localStorage.getItem(candidateSizeKey))
1623
+ if (candidateSizeOptions.includes(storedCandidateSize)) {
1624
+ candidateSizeSelect.value = String(storedCandidateSize)
1625
+ }
1626
+ } catch (error) {}
1597
1627
  const addSourceButton = el("btn-add-source")
1598
1628
  if (addSourceButton) {
1599
1629
  addSourceButton.setAttribute("aria-label", COPY.add_external_folder)
@@ -4,6 +4,7 @@
4
4
  <section class='vault-overview'>
5
5
  <div class='vault-metrics' id='vault-metrics'></div>
6
6
  <div class='vault-overview-actions'>
7
+ <select class='vault-select' id='vault-candidate-size'></select>
7
8
  <button class='vault-button' id='btn-scan' type='button'></button>
8
9
  <% if (!appMode) { %>
9
10
  <details class='vault-advanced' id='vault-advanced'>
@@ -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
  })