webgpu-inspector-cli 0.1.0__py3-none-any.whl

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,439 @@
1
+ /**
2
+ * collector.js - Injected into the page to collect WebGPU Inspector messages.
3
+ *
4
+ * Listens for __WebGPUInspector CustomEvents and accumulates GPU object state,
5
+ * validation errors, frame timing, and capture results. Exposes a query API
6
+ * on window.__wgi that the Python bridge calls via page.evaluate().
7
+ */
8
+ (function () {
9
+ "use strict";
10
+
11
+ if (window.__wgi) return; // Already injected
12
+
13
+ // --- Action constants (mirrors src/utils/actions.js) ---
14
+ const Actions = {
15
+ AddObject: "webgpu_inspect_add_object",
16
+ DeleteObject: "webgpu_inspect_delete_object",
17
+ DeleteObjects: "webgpu_inspect_delete_objects",
18
+ ObjectSetLabel: "webgpu_inspect_object_set_label",
19
+ ResolveAsyncObject: "webgpu_inspect_resolve_async_object",
20
+ ValidationError: "webgpu_inspect_validation_error",
21
+ MemoryLeakWarning: "webgpu_inspect_memory_leak_warning",
22
+ DeltaTime: "webgpu_inspect_delta_time",
23
+ CaptureFrameResults: "webgpu_inspect_capture_frame_results",
24
+ CaptureFrameCommands: "webgpu_inspect_capture_frame_commands",
25
+ CaptureTextureData: "webgpu_inspect_capture_texture_data",
26
+ CaptureBufferData: "webgpu_inspect_capture_buffer_data",
27
+ CaptureTextureFrames: "webgpu_inspect_capture_texture_frames",
28
+ Recording: "webgpu_record_recording",
29
+ };
30
+
31
+ const PanelActions = {
32
+ RequestTexture: "webgpu_inspect_request_texture",
33
+ CompileShader: "webgpu_inspect_compile_shader",
34
+ RevertShader: "webgpu_inspect_revert_shader",
35
+ Capture: "webgpu_inspector_capture",
36
+ InitializeInspector: "webgpu_initialize_inspector",
37
+ };
38
+
39
+ // --- State ---
40
+ const objects = new Map(); // id -> { id, type, descriptor, label, stacktrace, parent, pending }
41
+ const errors = []; // [{ id, objectId, message, stacktrace, timestamp }]
42
+ const memoryLeaks = [];
43
+ let errorCount = 0;
44
+ let deltaFrameTime = -1;
45
+ let totalTextureMemory = 0;
46
+ let totalBufferMemory = 0;
47
+
48
+ // Capture state
49
+ let captureStatus = "idle"; // "idle" | "pending" | "complete"
50
+ let capturedFrameResults = null;
51
+ let capturedCommands = null;
52
+ let capturedTextures = new Map(); // id -> { chunks: [], totalChunks, assembled }
53
+ let capturedBuffers = new Map(); // id -> { data }
54
+
55
+ // --- Texture memory calculation ---
56
+ const FORMAT_SIZES = {
57
+ "r8unorm": 1, "r8snorm": 1, "r8uint": 1, "r8sint": 1,
58
+ "r16uint": 2, "r16sint": 2, "r16float": 2,
59
+ "rg8unorm": 2, "rg8snorm": 2, "rg8uint": 2, "rg8sint": 2,
60
+ "r32uint": 4, "r32sint": 4, "r32float": 4,
61
+ "rg16uint": 4, "rg16sint": 4, "rg16float": 4,
62
+ "rgba8unorm": 4, "rgba8unorm-srgb": 4, "rgba8snorm": 4,
63
+ "rgba8uint": 4, "rgba8sint": 4,
64
+ "bgra8unorm": 4, "bgra8unorm-srgb": 4,
65
+ "rgb10a2uint": 4, "rgb10a2unorm": 4, "rg11b10ufloat": 4,
66
+ "rg32uint": 8, "rg32sint": 8, "rg32float": 8,
67
+ "rgba16uint": 8, "rgba16sint": 8, "rgba16float": 8,
68
+ "rgba32uint": 16, "rgba32sint": 16, "rgba32float": 16,
69
+ "depth16unorm": 2, "depth24plus": 4, "depth24plus-stencil8": 4,
70
+ "depth32float": 4, "depth32float-stencil8": 5,
71
+ "stencil8": 1,
72
+ };
73
+
74
+ function getTextureGpuSize(descriptor) {
75
+ if (!descriptor) return 0;
76
+ const format = descriptor.format;
77
+ const bpp = FORMAT_SIZES[format];
78
+ if (bpp === undefined) return 0;
79
+ const w = descriptor.size?.[0] ?? descriptor.size?.width ?? 1;
80
+ const h = descriptor.size?.[1] ?? descriptor.size?.height ?? 1;
81
+ const d = descriptor.size?.[2] ?? descriptor.size?.depthOrArrayLayers ?? 1;
82
+ const mips = descriptor.mipLevelCount ?? 1;
83
+ let total = 0;
84
+ for (let m = 0; m < mips; m++) {
85
+ const mw = Math.max(1, w >> m);
86
+ const mh = Math.max(1, h >> m);
87
+ total += mw * mh * d * bpp;
88
+ }
89
+ return total;
90
+ }
91
+
92
+ // --- Message handler ---
93
+ function handleMessage(detail) {
94
+ if (!detail || !detail.__webgpuInspector || !detail.__webgpuInspectorPage) {
95
+ return;
96
+ }
97
+ const action = detail.action;
98
+
99
+ switch (action) {
100
+ case Actions.AddObject: {
101
+ let descriptor = null;
102
+ try {
103
+ descriptor = detail.descriptor ? JSON.parse(detail.descriptor) : null;
104
+ } catch (e) {
105
+ descriptor = null;
106
+ }
107
+ const obj = {
108
+ id: detail.id,
109
+ type: detail.type,
110
+ descriptor: descriptor,
111
+ label: descriptor?.label || null,
112
+ stacktrace: detail.stacktrace || "",
113
+ parent: detail.parent ?? null,
114
+ pending: !!detail.pending,
115
+ };
116
+
117
+ // Track memory
118
+ if (detail.type === "Buffer") {
119
+ obj.size = descriptor?.size ?? 0;
120
+ totalBufferMemory += obj.size;
121
+ } else if (detail.type === "Texture") {
122
+ // If texture already exists (reconfigured), update it
123
+ const prev = objects.get(detail.id);
124
+ if (prev && prev.type === "Texture") {
125
+ totalTextureMemory -= prev.gpuSize || 0;
126
+ }
127
+ obj.gpuSize = getTextureGpuSize(descriptor);
128
+ totalTextureMemory += obj.gpuSize;
129
+ } else if (detail.type === "ShaderModule") {
130
+ obj.code = descriptor?.code || null;
131
+ obj.size = descriptor?.code?.length ?? 0;
132
+ }
133
+
134
+ objects.set(detail.id, obj);
135
+ break;
136
+ }
137
+
138
+ case Actions.DeleteObject: {
139
+ const obj = objects.get(detail.id);
140
+ if (obj) {
141
+ if (obj.type === "Buffer") totalBufferMemory -= obj.size || 0;
142
+ if (obj.type === "Texture") totalTextureMemory -= obj.gpuSize || 0;
143
+ objects.delete(detail.id);
144
+ }
145
+ break;
146
+ }
147
+
148
+ case Actions.DeleteObjects: {
149
+ const ids = detail.idList || [];
150
+ for (const id of ids) {
151
+ const obj = objects.get(id);
152
+ if (obj) {
153
+ if (obj.type === "Buffer") totalBufferMemory -= obj.size || 0;
154
+ if (obj.type === "Texture") totalTextureMemory -= obj.gpuSize || 0;
155
+ objects.delete(id);
156
+ }
157
+ }
158
+ break;
159
+ }
160
+
161
+ case Actions.ObjectSetLabel: {
162
+ const obj = objects.get(detail.id);
163
+ if (obj) obj.label = detail.label;
164
+ break;
165
+ }
166
+
167
+ case Actions.ResolveAsyncObject: {
168
+ const obj = objects.get(detail.id);
169
+ if (obj) obj.pending = false;
170
+ break;
171
+ }
172
+
173
+ case Actions.ValidationError: {
174
+ errorCount++;
175
+ errors.push({
176
+ id: errorCount,
177
+ objectId: detail.id ?? 0,
178
+ message: detail.message,
179
+ stacktrace: detail.stacktrace || "",
180
+ timestamp: Date.now(),
181
+ });
182
+ break;
183
+ }
184
+
185
+ case Actions.MemoryLeakWarning: {
186
+ memoryLeaks.push({
187
+ id: detail.id,
188
+ type: detail.type,
189
+ message: detail.message,
190
+ timestamp: Date.now(),
191
+ });
192
+ break;
193
+ }
194
+
195
+ case Actions.DeltaTime: {
196
+ deltaFrameTime = detail.deltaTime;
197
+ break;
198
+ }
199
+
200
+ case Actions.CaptureFrameResults: {
201
+ capturedFrameResults = {
202
+ frame: detail.frame,
203
+ count: detail.count,
204
+ batches: detail.batches,
205
+ };
206
+ captureStatus = "complete";
207
+ break;
208
+ }
209
+
210
+ case Actions.CaptureFrameCommands: {
211
+ capturedCommands = detail;
212
+ break;
213
+ }
214
+
215
+ case Actions.CaptureTextureData: {
216
+ const texId = detail.id;
217
+ if (!capturedTextures.has(texId)) {
218
+ capturedTextures.set(texId, { chunks: [], totalChunks: 0, complete: false });
219
+ }
220
+ const entry = capturedTextures.get(texId);
221
+ if (detail.index !== undefined) {
222
+ entry.chunks[detail.index] = detail.data;
223
+ entry.totalChunks = detail.count || entry.totalChunks;
224
+ // Check if all chunks received
225
+ const received = entry.chunks.filter(c => c !== undefined).length;
226
+ if (received >= entry.totalChunks && entry.totalChunks > 0) {
227
+ entry.complete = true;
228
+ }
229
+ } else {
230
+ // Single chunk
231
+ entry.chunks = [detail.data];
232
+ entry.totalChunks = 1;
233
+ entry.complete = true;
234
+ }
235
+ break;
236
+ }
237
+
238
+ case Actions.CaptureBufferData: {
239
+ const bufId = detail.id;
240
+ capturedBuffers.set(bufId, {
241
+ data: detail.data,
242
+ offset: detail.offset || 0,
243
+ size: detail.size || 0,
244
+ });
245
+ break;
246
+ }
247
+ }
248
+ }
249
+
250
+ // --- Listen for inspector messages ---
251
+ window.addEventListener("__WebGPUInspector", (event) => {
252
+ handleMessage(event.detail);
253
+ });
254
+
255
+ // --- Query API ---
256
+ window.__wgi = {
257
+ getObjects(type) {
258
+ const result = [];
259
+ for (const obj of objects.values()) {
260
+ if (!type || obj.type === type) {
261
+ result.push({
262
+ id: obj.id,
263
+ type: obj.type,
264
+ label: obj.label,
265
+ descriptor: obj.descriptor,
266
+ stacktrace: obj.stacktrace,
267
+ parent: obj.parent,
268
+ pending: obj.pending,
269
+ size: obj.size,
270
+ gpuSize: obj.gpuSize,
271
+ });
272
+ }
273
+ }
274
+ return result;
275
+ },
276
+
277
+ getObject(id) {
278
+ const obj = objects.get(id);
279
+ if (!obj) return null;
280
+ return {
281
+ id: obj.id,
282
+ type: obj.type,
283
+ label: obj.label,
284
+ descriptor: obj.descriptor,
285
+ stacktrace: obj.stacktrace,
286
+ parent: obj.parent,
287
+ pending: obj.pending,
288
+ size: obj.size,
289
+ gpuSize: obj.gpuSize,
290
+ code: obj.code,
291
+ };
292
+ },
293
+
294
+ getObjectCount() {
295
+ return objects.size;
296
+ },
297
+
298
+ getErrors() {
299
+ return errors.slice();
300
+ },
301
+
302
+ getErrorCount() {
303
+ return errors.length;
304
+ },
305
+
306
+ clearErrors() {
307
+ errors.length = 0;
308
+ errorCount = 0;
309
+ },
310
+
311
+ getFrameRate() {
312
+ if (deltaFrameTime <= 0) return { fps: 0, deltaTime: -1 };
313
+ return {
314
+ fps: Math.round(1000 / deltaFrameTime),
315
+ deltaTime: deltaFrameTime,
316
+ };
317
+ },
318
+
319
+ getMemoryUsage() {
320
+ return {
321
+ totalTextureMemory: totalTextureMemory,
322
+ totalBufferMemory: totalBufferMemory,
323
+ totalMemory: totalTextureMemory + totalBufferMemory,
324
+ };
325
+ },
326
+
327
+ getSummary() {
328
+ const typeCounts = {};
329
+ for (const obj of objects.values()) {
330
+ typeCounts[obj.type] = (typeCounts[obj.type] || 0) + 1;
331
+ }
332
+ const fr = deltaFrameTime > 0 ? Math.round(1000 / deltaFrameTime) : 0;
333
+ return {
334
+ objectCount: objects.size,
335
+ typeCounts: typeCounts,
336
+ errorCount: errors.length,
337
+ fps: fr,
338
+ deltaTime: deltaFrameTime,
339
+ totalTextureMemory: totalTextureMemory,
340
+ totalBufferMemory: totalBufferMemory,
341
+ totalMemory: totalTextureMemory + totalBufferMemory,
342
+ };
343
+ },
344
+
345
+ // --- Capture ---
346
+
347
+ requestCapture(options) {
348
+ captureStatus = "pending";
349
+ capturedFrameResults = null;
350
+ capturedCommands = null;
351
+ capturedTextures = new Map();
352
+ capturedBuffers = new Map();
353
+
354
+ const data = options || {};
355
+ window.dispatchEvent(new CustomEvent("__WebGPUInspector", {
356
+ detail: {
357
+ __webgpuInspector: true,
358
+ action: "webgpu_inspector_capture",
359
+ data: JSON.stringify(data),
360
+ }
361
+ }));
362
+ return true;
363
+ },
364
+
365
+ getCaptureStatus() {
366
+ return captureStatus;
367
+ },
368
+
369
+ getCapturedFrameResults() {
370
+ return capturedFrameResults;
371
+ },
372
+
373
+ getCapturedCommands() {
374
+ return capturedCommands;
375
+ },
376
+
377
+ requestTexture(id, mipLevel) {
378
+ window.dispatchEvent(new CustomEvent("__WebGPUInspector", {
379
+ detail: {
380
+ __webgpuInspector: true,
381
+ action: "webgpu_inspect_request_texture",
382
+ id: id,
383
+ mipLevel: mipLevel || 0,
384
+ }
385
+ }));
386
+ return true;
387
+ },
388
+
389
+ getTextureData(id) {
390
+ const entry = capturedTextures.get(id);
391
+ if (!entry) return null;
392
+ return {
393
+ complete: entry.complete,
394
+ totalChunks: entry.totalChunks,
395
+ receivedChunks: entry.chunks.filter(c => c !== undefined).length,
396
+ data: entry.complete ? entry.chunks.join("") : null,
397
+ };
398
+ },
399
+
400
+ getBufferData(id) {
401
+ return capturedBuffers.get(id) || null;
402
+ },
403
+
404
+ // --- Shaders ---
405
+
406
+ getShaderCode(id) {
407
+ const obj = objects.get(id);
408
+ if (!obj || obj.type !== "ShaderModule") return null;
409
+ return obj.code || (obj.descriptor?.code ?? null);
410
+ },
411
+
412
+ compileShader(id, code) {
413
+ window.dispatchEvent(new CustomEvent("__WebGPUInspector", {
414
+ detail: {
415
+ __webgpuInspector: true,
416
+ action: "webgpu_inspect_compile_shader",
417
+ id: id,
418
+ code: code,
419
+ }
420
+ }));
421
+ return true;
422
+ },
423
+
424
+ revertShader(id) {
425
+ window.dispatchEvent(new CustomEvent("__WebGPUInspector", {
426
+ detail: {
427
+ __webgpuInspector: true,
428
+ action: "webgpu_inspect_revert_shader",
429
+ id: id,
430
+ }
431
+ }));
432
+ return true;
433
+ },
434
+
435
+ getMemoryLeaks() {
436
+ return memoryLeaks.slice();
437
+ },
438
+ };
439
+ })();
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: "webgpu-inspector-cli"
3
+ description: "Debug WebGPU applications from the command line - inspect GPU objects, view shaders, capture frames, check validation errors"
4
+ ---
5
+
6
+ # webgpu-inspector-cli
7
+
8
+ CLI tool for debugging WebGPU applications. Launches a browser, injects the WebGPU Inspector, and provides commands to inspect GPU state.
9
+
10
+ ## Prerequisites
11
+
12
+ ```bash
13
+ git clone --recurse-submodules https://github.com/scasekar/webgpu-inspector-cli
14
+ cd webgpu-inspector-cli/agent-harness && pip install -e .
15
+ python -m playwright install chromium
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```bash
21
+ # 1. Launch browser with your WebGPU app
22
+ webgpu-inspector-cli browser launch --url https://your-app.com
23
+
24
+ # 2. Check for problems
25
+ webgpu-inspector-cli --json errors list
26
+ webgpu-inspector-cli --json status summary
27
+
28
+ # 3. Inspect GPU objects
29
+ webgpu-inspector-cli --json objects list
30
+ webgpu-inspector-cli --json objects inspect --id 8
31
+
32
+ # 4. View shader code
33
+ webgpu-inspector-cli shaders view --id 8
34
+
35
+ # 5. Capture a frame
36
+ webgpu-inspector-cli --json capture frame
37
+
38
+ # 6. Clean up
39
+ webgpu-inspector-cli browser close
40
+ ```
41
+
42
+ ## Command Groups
43
+
44
+ ### browser - Session lifecycle
45
+ - `browser launch --url URL [--headless] [--gpu-backend BACKEND]`
46
+ - `browser close`
47
+ - `browser navigate --url URL`
48
+ - `browser screenshot -o PATH`
49
+ - `browser status`
50
+
51
+ ### objects - GPU object inspection
52
+ - `objects list [--type TYPE]` - List all GPU objects (Adapter, Device, Buffer, Texture, ShaderModule, RenderPipeline, etc.)
53
+ - `objects inspect --id ID` - Full details including descriptor and creation stacktrace
54
+ - `objects search --label PATTERN` - Find objects by label
55
+ - `objects memory` - GPU memory breakdown
56
+
57
+ ### capture - Frame capture
58
+ - `capture frame [--timeout N]` - Capture next frame's GPU commands
59
+ - `capture commands [--pass-index N]` - List commands from captured frame
60
+ - `capture texture --id ID [-o PATH]` - Read texture pixels, optionally save as PNG
61
+ - `capture buffer --id ID [--format hex|float32|uint32]` - Read buffer data
62
+
63
+ ### shaders - Shader inspection
64
+ - `shaders list` - List all shader modules
65
+ - `shaders view --id ID` - View WGSL source code
66
+ - `shaders compile --id ID --file PATH` - Hot-replace shader code
67
+ - `shaders revert --id ID` - Revert to original
68
+
69
+ ### errors - Validation errors
70
+ - `errors list` - All validation errors with stacktraces
71
+ - `errors watch [--timeout N]` - Stream errors in real-time
72
+ - `errors clear` - Reset error history
73
+
74
+ ### status - Runtime monitoring
75
+ - `status summary` - Object counts, memory, FPS, error count
76
+ - `status fps` - Current frame rate
77
+ - `status memory` - Memory breakdown
78
+
79
+ ## Agent Guidance
80
+
81
+ - Always use `--json` flag for machine-readable output
82
+ - Launch browser first with `browser launch --url URL`
83
+ - Close browser when done with `browser close`
84
+ - For debugging workflow: check `errors list` first, then `objects list` to understand GPU state
85
+ - Shader IDs are integers - get them from `shaders list` before using `shaders view`
86
+ - Frame capture is async - `capture frame` polls until complete (default 30s timeout)
87
+ - Texture data can be saved as PNG with `capture texture --id ID -o output.png`
File without changes
@@ -0,0 +1,151 @@
1
+ """Unit tests for WebGPU Inspector CLI core modules."""
2
+
3
+ import os
4
+ import sys
5
+ import json
6
+ import pytest
7
+ from pathlib import Path
8
+
9
+
10
+ # --- Session Tests ---
11
+
12
+ class TestSession:
13
+ def setup_method(self):
14
+ from webgpu_inspector_cli.core.session import Session
15
+ self.session = Session()
16
+
17
+ def test_push_pop_shader_edit(self):
18
+ self.session.push_shader_edit(1, "original code")
19
+ result = self.session.pop_shader_edit(1)
20
+ assert result == "original code"
21
+
22
+ def test_pop_empty_returns_none(self):
23
+ result = self.session.pop_shader_edit(999)
24
+ assert result is None
25
+
26
+ def test_clear_shader_edits(self):
27
+ self.session.push_shader_edit(1, "v1")
28
+ self.session.push_shader_edit(1, "v2")
29
+ self.session.clear_shader_edits(1)
30
+ assert not self.session.has_shader_edits(1)
31
+
32
+ def test_has_shader_edits(self):
33
+ assert not self.session.has_shader_edits(1)
34
+ self.session.push_shader_edit(1, "code")
35
+ assert self.session.has_shader_edits(1)
36
+
37
+ def test_multiple_edits_stack(self):
38
+ """Edits should pop in LIFO order."""
39
+ self.session.push_shader_edit(1, "v1")
40
+ self.session.push_shader_edit(1, "v2")
41
+ self.session.push_shader_edit(1, "v3")
42
+ assert self.session.pop_shader_edit(1) == "v3"
43
+ assert self.session.pop_shader_edit(1) == "v2"
44
+ assert self.session.pop_shader_edit(1) == "v1"
45
+ assert self.session.pop_shader_edit(1) is None
46
+
47
+ def test_separate_shader_histories(self):
48
+ """Different shader IDs should have independent histories."""
49
+ self.session.push_shader_edit(1, "shader1_v1")
50
+ self.session.push_shader_edit(2, "shader2_v1")
51
+ assert self.session.pop_shader_edit(1) == "shader1_v1"
52
+ assert self.session.pop_shader_edit(2) == "shader2_v1"
53
+
54
+
55
+ # --- Bridge Path Resolution Tests ---
56
+
57
+ class TestBridgePaths:
58
+ def test_find_inspector_js(self):
59
+ from webgpu_inspector_cli.core.bridge import _find_inspector_js
60
+ path = _find_inspector_js()
61
+ assert path.exists()
62
+ assert path.name == "webgpu_inspector_loader.js"
63
+
64
+ def test_find_collector_js(self):
65
+ from webgpu_inspector_cli.core.bridge import _find_collector_js
66
+ path = _find_collector_js()
67
+ assert path.exists()
68
+ assert path.name == "collector.js"
69
+
70
+ def test_bridge_not_connected(self):
71
+ from webgpu_inspector_cli.core.bridge import Bridge
72
+ bridge = Bridge()
73
+ assert not bridge.is_connected
74
+ with pytest.raises(RuntimeError, match="No browser session"):
75
+ bridge.query("getSummary")
76
+
77
+ def test_bridge_require_not_connected(self):
78
+ from webgpu_inspector_cli.core.bridge import require_bridge, Bridge
79
+ import webgpu_inspector_cli.core.bridge as bridge_mod
80
+ # Reset global singleton
81
+ bridge_mod._bridge = Bridge()
82
+ with pytest.raises(RuntimeError, match="No active browser session"):
83
+ require_bridge()
84
+ bridge_mod._bridge = None
85
+
86
+
87
+ # --- Format Helpers Tests ---
88
+
89
+ class TestFormatHelpers:
90
+ def test_format_bytes_zero(self):
91
+ from webgpu_inspector_cli.commands.objects import _format_bytes
92
+ assert _format_bytes(0) == "0 B"
93
+
94
+ def test_format_bytes_small(self):
95
+ from webgpu_inspector_cli.commands.objects import _format_bytes
96
+ assert _format_bytes(512) == "512 B"
97
+
98
+ def test_format_bytes_kb(self):
99
+ from webgpu_inspector_cli.commands.objects import _format_bytes
100
+ result = _format_bytes(2048)
101
+ assert "KB" in result
102
+
103
+ def test_format_bytes_mb(self):
104
+ from webgpu_inspector_cli.commands.objects import _format_bytes
105
+ result = _format_bytes(4608000)
106
+ assert "MB" in result
107
+
108
+ def test_format_bytes_none(self):
109
+ from webgpu_inspector_cli.commands.objects import _format_bytes
110
+ assert _format_bytes(None) == "0 B"
111
+
112
+
113
+ # --- Collector JS Content Tests ---
114
+
115
+ class TestCollectorJS:
116
+ def test_collector_js_exists_and_valid(self):
117
+ from webgpu_inspector_cli.core.bridge import _find_collector_js
118
+ path = _find_collector_js()
119
+ content = path.read_text()
120
+ # Check it defines window.__wgi
121
+ assert "window.__wgi" in content
122
+ # Check it handles key actions
123
+ assert "AddObject" in content
124
+ assert "DeleteObject" in content
125
+ assert "ValidationError" in content
126
+ assert "DeltaTime" in content
127
+ assert "CaptureFrameResults" in content
128
+
129
+ def test_collector_has_query_api(self):
130
+ from webgpu_inspector_cli.core.bridge import _find_collector_js
131
+ content = _find_collector_js().read_text()
132
+ # Check all query functions exist
133
+ for fn in ["getObjects", "getObject", "getErrors", "getFrameRate",
134
+ "getMemoryUsage", "getSummary", "requestCapture",
135
+ "getCaptureStatus", "getShaderCode", "compileShader",
136
+ "revertShader"]:
137
+ assert fn in content, f"Missing query function: {fn}"
138
+
139
+
140
+ # --- CLI Entry Point Tests ---
141
+
142
+ class TestCLIStructure:
143
+ def test_cli_import(self):
144
+ from webgpu_inspector_cli.webgpu_inspector_cli import cli
145
+ assert cli is not None
146
+
147
+ def test_cli_has_commands(self):
148
+ from webgpu_inspector_cli.webgpu_inspector_cli import cli
149
+ command_names = set(cli.commands.keys())
150
+ expected = {"browser", "objects", "capture", "shaders", "errors", "status", "repl"}
151
+ assert expected.issubset(command_names)