wafer-cli 0.2.9__tar.gz → 0.2.10__tar.gz

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 (64) hide show
  1. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/PKG-INFO +1 -1
  2. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/README.md +41 -2
  3. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/pyproject.toml +2 -1
  4. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/tests/test_cli_coverage.py +125 -33
  5. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/tests/test_cli_parity_integration.py +1 -1
  6. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/tests/test_kernel_scope_cli.py +171 -18
  7. wafer_cli-0.2.10/tests/test_nsys_analyze.py +293 -0
  8. wafer_cli-0.2.10/tests/test_nsys_profile.py +157 -0
  9. wafer_cli-0.2.10/tests/test_output.py +263 -0
  10. wafer_cli-0.2.10/tests/test_skill_commands.py +231 -0
  11. wafer_cli-0.2.10/tests/test_targets_ops.py +206 -0
  12. wafer_cli-0.2.10/tests/test_wevin_cli.py +660 -0
  13. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/GUIDE.md +18 -7
  14. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/api_client.py +4 -0
  15. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/cli.py +1177 -278
  16. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/corpus.py +158 -32
  17. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/evaluate.py +75 -6
  18. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/kernel_scope.py +132 -31
  19. wafer_cli-0.2.10/wafer/nsys_analyze.py +1042 -0
  20. wafer_cli-0.2.10/wafer/nsys_profile.py +511 -0
  21. wafer_cli-0.2.10/wafer/output.py +241 -0
  22. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/skills/wafer-guide/SKILL.md +13 -0
  23. wafer_cli-0.2.10/wafer/ssh_keys.py +261 -0
  24. wafer_cli-0.2.10/wafer/targets_ops.py +718 -0
  25. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/wevin_cli.py +127 -18
  26. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/workspaces.py +232 -184
  27. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer_cli.egg-info/PKG-INFO +1 -1
  28. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer_cli.egg-info/SOURCES.txt +9 -1
  29. wafer_cli-0.2.9/tests/test_isa_cli.py +0 -212
  30. wafer_cli-0.2.9/tests/test_wevin_cli.py +0 -68
  31. wafer_cli-0.2.9/wafer/nsys_analyze.py +0 -212
  32. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/setup.cfg +0 -0
  33. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/tests/test_analytics.py +0 -0
  34. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/tests/test_billing.py +0 -0
  35. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/tests/test_config_integration.py +0 -0
  36. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/tests/test_file_operations_integration.py +0 -0
  37. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/tests/test_rocprof_compute_integration.py +0 -0
  38. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/tests/test_ssh_integration.py +0 -0
  39. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/tests/test_workflow_integration.py +0 -0
  40. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/__init__.py +0 -0
  41. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/analytics.py +0 -0
  42. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/auth.py +0 -0
  43. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/autotuner.py +0 -0
  44. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/billing.py +0 -0
  45. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/config.py +0 -0
  46. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/global_config.py +0 -0
  47. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/gpu_run.py +0 -0
  48. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/inference.py +0 -0
  49. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/ncu_analyze.py +0 -0
  50. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/problems.py +0 -0
  51. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/rocprof_compute.py +0 -0
  52. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/rocprof_sdk.py +0 -0
  53. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/rocprof_systems.py +0 -0
  54. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/target_lock.py +0 -0
  55. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/targets.py +0 -0
  56. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/templates/__init__.py +0 -0
  57. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/templates/ask_docs.py +0 -0
  58. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/templates/optimize_kernel.py +0 -0
  59. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/templates/trace_analyze.py +0 -0
  60. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer/tracelens.py +0 -0
  61. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer_cli.egg-info/dependency_links.txt +0 -0
  62. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer_cli.egg-info/entry_points.txt +0 -0
  63. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer_cli.egg-info/requires.txt +0 -0
  64. {wafer_cli-0.2.9 → wafer_cli-0.2.10}/wafer_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wafer-cli
3
- Version: 0.2.9
3
+ Version: 0.2.10
4
4
  Summary: CLI tool for running commands on remote GPUs and GPU kernel optimization agent
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: typer>=0.12.0
@@ -43,10 +43,16 @@ wafer remote-run --upload-dir ./my_code -- python3 train.py
43
43
 
44
44
  Create and manage persistent GPU environments.
45
45
 
46
+ **Available GPUs:**
47
+
48
+ - `MI300X` - AMD Instinct MI300X (192GB HBM3, ROCm)
49
+ - `B200` - NVIDIA Blackwell B200 (180GB HBM3e, CUDA) - default
50
+
46
51
  ```bash
47
52
  wafer workspaces list
48
- wafer workspaces create my-workspace
49
- wafer workspaces attach <workspace-id> # Get SSH credentials
53
+ wafer workspaces create my-workspace --gpu B200 --wait # NVIDIA B200
54
+ wafer workspaces create amd-dev --gpu MI300X # AMD MI300X
55
+ wafer workspaces ssh <workspace-id>
50
56
  wafer workspaces delete <workspace-id>
51
57
  ```
52
58
 
@@ -197,6 +203,39 @@ Now you can tab-complete:
197
203
 
198
204
  ---
199
205
 
206
+ ## AI Assistant Skills
207
+
208
+ Install the Wafer CLI skill to make wafer commands discoverable by your AI coding assistant:
209
+
210
+ ```bash
211
+ # Install for all supported tools (Claude Code, Codex CLI, Cursor)
212
+ wafer skill install
213
+
214
+ # Install for a specific tool
215
+ wafer skill install -t cursor # Cursor
216
+ wafer skill install -t claude # Claude Code
217
+ wafer skill install -t codex # Codex CLI
218
+
219
+ # Check installation status
220
+ wafer skill status
221
+
222
+ # Uninstall
223
+ wafer skill uninstall
224
+ ```
225
+
226
+ ### Installing from GitHub (Cursor)
227
+
228
+ You can also install the skill directly from GitHub in Cursor:
229
+
230
+ 1. Open Cursor Settings (Cmd+Shift+J / Ctrl+Shift+J)
231
+ 2. Navigate to **Rules** → **Add Rule** → **Remote Rule (Github)**
232
+ 3. Enter: `https://github.com/wafer-ai/skills`
233
+ 4. Cursor will automatically discover skills in `.cursor/skills/`
234
+
235
+ The skill provides comprehensive guidance for GPU kernel development, including documentation lookup, trace analysis, kernel evaluation, and optimization workflows.
236
+
237
+ ---
238
+
200
239
  ## Requirements
201
240
 
202
241
  - Python 3.10+
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wafer-cli"
3
- version = "0.2.9"
3
+ version = "0.2.10"
4
4
  description = "CLI tool for running commands on remote GPUs and GPU kernel optimization agent"
5
5
  requires-python = ">=3.11"
6
6
  dependencies = [
@@ -78,6 +78,7 @@ ignore = [
78
78
  [tool.ruff.lint.per-file-ignores]
79
79
  "tests/**/*.py" = ["ANN201"] # Don't require return type annotations in tests
80
80
  "wafer/evaluate.py" = ["PLR0915", "PLR1702", "E402"] # complex deployment flows - TODO: refactor
81
+ "wafer/output.py" = ["ANN401"] # Output collector uses **kwargs for flexible event data
81
82
 
82
83
  [tool.ruff.lint.pylint]
83
84
  max-args = 7 # Max function arguments (Tiger Style: few parameters)
@@ -61,7 +61,7 @@ def _cleanup_test_workspaces() -> None:
61
61
  return
62
62
  for ws in workspaces:
63
63
  if ws.get("name", "").startswith("test-"):
64
- runner.invoke(app, ["workspaces", "delete", ws["id"]])
64
+ runner.invoke(app, ["workspaces", "delete", ws["id"], "-y"])
65
65
 
66
66
 
67
67
  @pytest.fixture(scope="session", autouse=True)
@@ -213,7 +213,7 @@ class TestWorkspacesCommands:
213
213
  finally:
214
214
  # Delete
215
215
  if ws_id:
216
- result = runner.invoke(app, ["workspaces", "delete", ws_id])
216
+ result = runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
217
217
 
218
218
  def test_workspaces_create_pretty(self) -> None:
219
219
  """Create workspace with pretty output."""
@@ -228,53 +228,43 @@ class TestWorkspacesCommands:
228
228
  return
229
229
 
230
230
  # Extract ID from output
231
- # Output format: "Created workspace: <name> (<uuid>)"
231
+ # Output format: "Creating workspace: <name> (<uuid>)"
232
232
  match = re.search(r"\(([a-f0-9-]{36})\)", result.output)
233
233
  if match:
234
234
  ws_id = match.group(1)
235
+ assert "Creating workspace:" in result.output
235
236
  assert ws_id is not None, f"Could not extract workspace ID from: {result.output}"
236
237
 
237
238
  finally:
238
239
  if ws_id:
239
- runner.invoke(app, ["workspaces", "delete", ws_id])
240
+ runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
240
241
 
241
- def test_workspaces_attach_json(self) -> None:
242
- """Attach to workspace with JSON output."""
243
- ws_name = f"test-attach-{os.getpid()}"
242
+ def test_workspaces_create_wait_json(self) -> None:
243
+ """Create workspace with --wait JSON output."""
244
+ ws_name = f"test-create-wait-{os.getpid()}"
244
245
  ws_id = None
245
246
 
246
247
  try:
247
- # Create workspace first
248
- result = runner.invoke(app, ["workspaces", "create", ws_name, "--json"])
248
+ result = runner.invoke(
249
+ app,
250
+ ["workspaces", "create", ws_name, "--wait", "--json"],
251
+ )
249
252
  if result.exit_code != 0:
250
253
  if "auth" in result.output.lower() or "not authenticated" in result.output.lower():
251
254
  pytest.skip("Not authenticated")
255
+ assert "error" in result.output.lower() or "no gpu" in result.output.lower()
252
256
  return
253
257
 
254
258
  data = json.loads(result.stdout)
255
- ws_id = data.get("id")
259
+ ws_id = data.get("workspace_id")
256
260
  assert ws_id is not None
257
-
258
- # Attach with JSON (avoids file I/O side effects)
259
- result = runner.invoke(app, ["workspaces", "attach", ws_id, "--json"])
260
- if result.exit_code == 0:
261
- attach_data = json.loads(result.stdout)
262
- # Should have SSH connection info
263
- assert "ssh_host" in attach_data
264
- assert "ssh_port" in attach_data
265
- assert "ssh_user" in attach_data
266
- assert "private_key_pem" in attach_data
267
- else:
268
- # May fail with 503 if no GPU available - that's ok
269
- assert (
270
- "503" in result.output
271
- or "No GPU" in result.output
272
- or "error" in result.output.lower()
273
- )
261
+ assert "ssh_host" in data
262
+ assert "ssh_port" in data
263
+ assert "ssh_user" in data
274
264
 
275
265
  finally:
276
266
  if ws_id:
277
- runner.invoke(app, ["workspaces", "delete", ws_id])
267
+ runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
278
268
 
279
269
  def test_workspaces_show_not_found(self) -> None:
280
270
  """Show workspace with invalid ID returns 404."""
@@ -288,10 +278,10 @@ class TestWorkspacesCommands:
288
278
  or "auth" in result.output.lower()
289
279
  )
290
280
 
291
- def test_workspaces_attach_not_found(self) -> None:
292
- """Attach to workspace with invalid ID returns 404."""
281
+ def test_workspaces_ssh_not_found(self) -> None:
282
+ """SSH to workspace with invalid ID returns 404."""
293
283
  fake_id = "00000000-0000-0000-0000-000000000000"
294
- result = runner.invoke(app, ["workspaces", "attach", fake_id])
284
+ result = runner.invoke(app, ["workspaces", "ssh", fake_id])
295
285
  if result.exit_code != 0:
296
286
  # Should be 404 or auth error
297
287
  assert (
@@ -300,6 +290,15 @@ class TestWorkspacesCommands:
300
290
  or "auth" in result.output.lower()
301
291
  )
302
292
 
293
+ def test_workspaces_exec_option_order(self) -> None:
294
+ """Exec should reject options after workspace name."""
295
+ result = runner.invoke(
296
+ app,
297
+ ["workspaces", "exec", "dev", "--timeout", "10", "--", "echo", "hi"],
298
+ )
299
+ assert result.exit_code != 0
300
+ assert "options must come before the workspace name" in result.output.lower()
301
+
303
302
 
304
303
  class TestNsysAnalyzeCommand:
305
304
  """Test wafer nvidia nsys analyze command."""
@@ -333,7 +332,24 @@ class TestNsysAnalyzeCommand:
333
332
  result = runner.invoke(
334
333
  app, ["nvidia", "nsys", "analyze", str(real_nsys_file), "--remote", "--json"]
335
334
  )
336
- assert result.exit_code == 0
335
+ if result.exit_code != 0:
336
+ combined = result.output.lower()
337
+ if "auth" in combined or "login" in combined or "401" in combined or "403" in combined:
338
+ pytest.skip("Not authenticated")
339
+ if (
340
+ "no gpu" in combined
341
+ or "no targets" in combined
342
+ or "unavailable" in combined
343
+ or "timed out" in combined
344
+ or "timeout" in combined
345
+ or "connect" in combined
346
+ or "service" in combined
347
+ or "billing" in combined
348
+ or "spend limit" in combined
349
+ or "402" in combined
350
+ ):
351
+ pytest.skip(f"Remote nsys unavailable: {result.output.strip()}")
352
+ assert result.exit_code == 0
337
353
  data = json.loads(result.stdout)
338
354
  assert "kernels" in data or "timeline" in data or "summary" in data
339
355
 
@@ -344,7 +360,24 @@ class TestNsysAnalyzeCommand:
344
360
  pytest.skip("No real .nsys-rep fixture available")
345
361
 
346
362
  result = runner.invoke(app, ["nvidia", "nsys", "analyze", str(real_nsys_file), "--remote"])
347
- assert result.exit_code == 0
363
+ if result.exit_code != 0:
364
+ combined = result.output.lower()
365
+ if "auth" in combined or "login" in combined or "401" in combined or "403" in combined:
366
+ pytest.skip("Not authenticated")
367
+ if (
368
+ "no gpu" in combined
369
+ or "no targets" in combined
370
+ or "unavailable" in combined
371
+ or "timed out" in combined
372
+ or "timeout" in combined
373
+ or "connect" in combined
374
+ or "service" in combined
375
+ or "billing" in combined
376
+ or "spend limit" in combined
377
+ or "402" in combined
378
+ ):
379
+ pytest.skip(f"Remote nsys unavailable: {result.output.strip()}")
380
+ assert result.exit_code == 0
348
381
  assert "NSYS" in result.output or "Kernel" in result.output or "Profile" in result.output
349
382
 
350
383
  def test_nsys_analyze_local(self, real_nsys_file: Path | None) -> None:
@@ -627,3 +660,62 @@ class TestEvaluateCommand:
627
660
  or "error" in combined
628
661
  or result.exit_code in (0, 1)
629
662
  )
663
+
664
+
665
+ class TestWorkspacesExecFlagPassthrough:
666
+ """Test that flags after -- are passed through to the command, not intercepted."""
667
+
668
+ def test_exec_passes_dash_i_flag(self) -> None:
669
+ """The -i flag should pass through, not be intercepted as --image."""
670
+ result = runner.invoke(app, [
671
+ "workspaces", "exec", "test-ws", "--", "echo", "-i", "test"
672
+ ])
673
+ # Should NOT fail with "no such option" or similar parse error
674
+ assert "no such option" not in result.output.lower()
675
+ assert "unrecognized" not in result.output.lower()
676
+ # May fail with workspace not found - that's fine, parsing succeeded
677
+
678
+ def test_exec_passes_dash_v_flag(self) -> None:
679
+ """The -v flag should pass through, not be intercepted as --verbose."""
680
+ result = runner.invoke(app, [
681
+ "workspaces", "exec", "test-ws", "--", "python", "-v", "-c", "print(1)"
682
+ ])
683
+ assert "no such option" not in result.output.lower()
684
+
685
+ def test_exec_passes_double_dash_help(self) -> None:
686
+ """--help after -- should pass to command, not show exec help."""
687
+ result = runner.invoke(app, [
688
+ "workspaces", "exec", "test-ws", "--", "python", "--help"
689
+ ])
690
+ # Should NOT show the exec command's help text
691
+ assert "Execute a command in workspace" not in result.output
692
+
693
+ def test_exec_passes_multiple_flags(self) -> None:
694
+ """Multiple flags like -i -v -n should all pass through."""
695
+ result = runner.invoke(app, [
696
+ "workspaces", "exec", "test-ws", "--", "grep", "-i", "-v", "-n", "pattern"
697
+ ])
698
+ assert "no such option" not in result.output.lower()
699
+
700
+ def test_exec_no_command_shows_error(self) -> None:
701
+ """Missing command after -- should show helpful error."""
702
+ result = runner.invoke(app, [
703
+ "workspaces", "exec", "test-ws", "--"
704
+ ])
705
+ assert result.exit_code != 0
706
+ assert "no command" in result.output.lower() or "error" in result.output.lower()
707
+
708
+ def test_exec_preserves_flag_order(self) -> None:
709
+ """Flags should be preserved in order they were given."""
710
+ # This tests the shlex.join behavior
711
+ result = runner.invoke(app, [
712
+ "workspaces", "exec", "test-ws", "--", "cmd", "-a", "1", "-b", "2"
713
+ ])
714
+ assert "no such option" not in result.output.lower()
715
+
716
+ def test_exec_with_equals_syntax(self) -> None:
717
+ """Flags with --flag=value syntax should pass through."""
718
+ result = runner.invoke(app, [
719
+ "workspaces", "exec", "test-ws", "--", "cmd", "--output=/tmp/out"
720
+ ])
721
+ assert "no such option" not in result.output.lower()
@@ -77,7 +77,7 @@ class TestWorkspacesWorkflow:
77
77
  finally:
78
78
  # Cleanup
79
79
  if ws_id:
80
- run_cli("workspaces", "delete", ws_id, check=False)
80
+ run_cli("workspaces", "delete", ws_id, "-y", check=False)
81
81
 
82
82
 
83
83
  @requires_api
@@ -217,15 +217,15 @@ class TestTargetsCommandFunction:
217
217
 
218
218
 
219
219
  # ============================================================================
220
- # CLI Integration Tests
220
+ # CLI Integration Tests (using unified wafer amd isa command)
221
221
  # ============================================================================
222
222
 
223
- class TestKernelScopeCliCommands:
224
- """Tests for wafer amd kernel-scope CLI commands."""
223
+ class TestISAAnalyzerCliCommands:
224
+ """Tests for wafer amd isa CLI commands (unified ISA Analyzer)."""
225
225
 
226
226
  def test_analyze_command_help(self) -> None:
227
227
  """Should display help for analyze command."""
228
- result = runner.invoke(app, ["amd", "kernel-scope", "analyze", "--help"])
228
+ result = runner.invoke(app, ["amd", "isa", "analyze", "--help"])
229
229
 
230
230
  assert result.exit_code == 0
231
231
  output = strip_ansi(result.stdout)
@@ -237,7 +237,7 @@ class TestKernelScopeCliCommands:
237
237
  isa_file.write_text(SAMPLE_ISA)
238
238
 
239
239
  result = runner.invoke(app, [
240
- "amd", "kernel-scope", "analyze", str(isa_file)
240
+ "amd", "isa", "analyze", str(isa_file)
241
241
  ])
242
242
 
243
243
  assert result.exit_code == 0, f"Failed: {result.output}"
@@ -249,7 +249,7 @@ class TestKernelScopeCliCommands:
249
249
  isa_file.write_text(SAMPLE_ISA)
250
250
 
251
251
  result = runner.invoke(app, [
252
- "amd", "kernel-scope", "analyze", str(isa_file), "--json"
252
+ "amd", "isa", "analyze", str(isa_file), "--json"
253
253
  ])
254
254
 
255
255
  assert result.exit_code == 0, f"Failed: {result.output}"
@@ -262,7 +262,7 @@ class TestKernelScopeCliCommands:
262
262
  isa_file.write_text(SAMPLE_ISA)
263
263
 
264
264
  result = runner.invoke(app, [
265
- "amd", "kernel-scope", "analyze", str(isa_file), "--csv"
265
+ "amd", "isa", "analyze", str(isa_file), "--csv"
266
266
  ])
267
267
 
268
268
  assert result.exit_code == 0, f"Failed: {result.output}"
@@ -271,32 +271,32 @@ class TestKernelScopeCliCommands:
271
271
  def test_analyze_missing_file_via_cli(self, tmp_path: Path) -> None:
272
272
  """Should fail for missing file."""
273
273
  result = runner.invoke(app, [
274
- "amd", "kernel-scope", "analyze", str(tmp_path / "missing.s")
274
+ "amd", "isa", "analyze", str(tmp_path / "missing.s")
275
275
  ])
276
276
 
277
277
  assert result.exit_code != 0
278
278
 
279
279
  def test_metrics_via_cli(self) -> None:
280
280
  """Should list metrics via CLI."""
281
- result = runner.invoke(app, ["amd", "kernel-scope", "metrics"])
281
+ result = runner.invoke(app, ["amd", "isa", "metrics"])
282
282
 
283
283
  assert result.exit_code == 0
284
284
  assert "vgpr_count" in result.stdout
285
285
 
286
286
  def test_targets_via_cli(self) -> None:
287
287
  """Should list targets via CLI."""
288
- result = runner.invoke(app, ["amd", "kernel-scope", "targets"])
288
+ result = runner.invoke(app, ["amd", "isa", "targets"])
289
289
 
290
290
  assert result.exit_code == 0
291
291
  assert "gfx90a" in result.stdout or "gfx942" in result.stdout
292
292
 
293
293
 
294
- class TestKernelScopeCliHelp:
295
- """Tests for kernel-scope command help text."""
294
+ class TestISAAnalyzerCliHelp:
295
+ """Tests for ISA Analyzer command help text."""
296
296
 
297
- def test_kernel_scope_help(self) -> None:
298
- """Should display help for kernel-scope command group."""
299
- result = runner.invoke(app, ["amd", "kernel-scope", "--help"])
297
+ def test_isa_help(self) -> None:
298
+ """Should display help for isa command group."""
299
+ result = runner.invoke(app, ["amd", "isa", "--help"])
300
300
 
301
301
  assert result.exit_code == 0
302
302
  output = strip_ansi(result.stdout)
@@ -304,12 +304,12 @@ class TestKernelScopeCliHelp:
304
304
  assert "metrics" in output.lower()
305
305
  assert "targets" in output.lower()
306
306
 
307
- def test_amd_help_includes_kernel_scope(self) -> None:
308
- """AMD help should mention kernel-scope."""
307
+ def test_amd_help_includes_isa(self) -> None:
308
+ """AMD help should mention ISA analyzer."""
309
309
  result = runner.invoke(app, ["amd", "--help"])
310
310
 
311
311
  assert result.exit_code == 0
312
- assert "kernel-scope" in result.stdout
312
+ assert "isa" in result.stdout.lower()
313
313
 
314
314
 
315
315
  # ============================================================================
@@ -465,3 +465,156 @@ class TestEdgeCases:
465
465
  output = analyze_command(str(tmp_path), recursive=True)
466
466
 
467
467
  assert "1 files" in output or "test_kernel" in output
468
+
469
+
470
+ # ============================================================================
471
+ # Unified ISA Analyzer CLI Tests (supports .co, .s, .ll, .ttgir)
472
+ # ============================================================================
473
+
474
+
475
+ class TestUnifiedISAAnalyzerCLI:
476
+ """Tests for unified ISA analyzer CLI command."""
477
+
478
+ def test_cli_wafer_amd_isa_analyze_help(self) -> None:
479
+ """wafer amd isa analyze --help should show help."""
480
+ result = runner.invoke(app, ["amd", "isa", "analyze", "--help"])
481
+
482
+ assert result.exit_code == 0
483
+ assert "AMD GPU ISA" in result.output or "analyze" in result.output.lower()
484
+
485
+ def test_cli_wafer_amd_isa_metrics(self) -> None:
486
+ """wafer amd isa metrics should list available metrics."""
487
+ result = runner.invoke(app, ["amd", "isa", "metrics"])
488
+
489
+ assert result.exit_code == 0
490
+ output = strip_ansi(result.output)
491
+ assert "vgpr_count" in output
492
+ assert "spill_count" in output
493
+ assert "mfma_count" in output
494
+
495
+ def test_cli_wafer_amd_isa_targets(self) -> None:
496
+ """wafer amd isa targets should list supported GPU targets."""
497
+ result = runner.invoke(app, ["amd", "isa", "targets"])
498
+
499
+ assert result.exit_code == 0
500
+ output = strip_ansi(result.output)
501
+ assert "gfx90a" in output
502
+ assert "gfx942" in output
503
+
504
+ def test_cli_wafer_amd_isa_analyze_isa_file(self, tmp_path: Path) -> None:
505
+ """wafer amd isa analyze should analyze .s files."""
506
+ isa_file = tmp_path / "kernel.s"
507
+ isa_file.write_text(SAMPLE_ISA)
508
+
509
+ result = runner.invoke(app, ["amd", "isa", "analyze", str(isa_file)])
510
+
511
+ assert result.exit_code == 0
512
+ output = strip_ansi(result.output)
513
+ assert "test_kernel" in output
514
+ assert "gfx90a" in output
515
+
516
+ def test_cli_wafer_amd_isa_analyze_json_output(self, tmp_path: Path) -> None:
517
+ """wafer amd isa analyze --json should output JSON."""
518
+ isa_file = tmp_path / "kernel.s"
519
+ isa_file.write_text(SAMPLE_ISA)
520
+
521
+ result = runner.invoke(app, ["amd", "isa", "analyze", str(isa_file), "--json"])
522
+
523
+ assert result.exit_code == 0
524
+ data = json.loads(result.output)
525
+ assert data["success"] is True
526
+ assert data["file_type"] == "isa"
527
+ assert data["isa_analysis"]["kernel_name"] == "test_kernel"
528
+
529
+ def test_cli_wafer_amd_isa_analyze_csv_output(self, tmp_path: Path) -> None:
530
+ """wafer amd isa analyze --csv should output CSV."""
531
+ isa_file = tmp_path / "kernel.s"
532
+ isa_file.write_text(SAMPLE_ISA)
533
+
534
+ result = runner.invoke(app, ["amd", "isa", "analyze", str(isa_file), "--csv"])
535
+
536
+ assert result.exit_code == 0
537
+ lines = result.output.strip().split("\n")
538
+ assert len(lines) >= 2 # Header + data
539
+ assert "kernel_name" in lines[0]
540
+ assert "test_kernel" in lines[1]
541
+
542
+ def test_cli_wafer_amd_isa_analyze_co_uses_api(self, tmp_path: Path) -> None:
543
+ """wafer amd isa analyze on .co should attempt API call."""
544
+ co_file = tmp_path / "kernel.co"
545
+ co_file.write_bytes(b"fake code object")
546
+
547
+ result = runner.invoke(app, ["amd", "isa", "analyze", str(co_file)])
548
+
549
+ # Should exit with error (API call will fail in test environment)
550
+ # The error could be auth-related or API unavailable
551
+ assert result.exit_code != 0
552
+ output = result.output.lower()
553
+ assert "error" in output or "api" in output or "failed" in output
554
+
555
+ def test_cli_wafer_amd_isa_analyze_nonexistent_file(self) -> None:
556
+ """wafer amd isa analyze should error on nonexistent file."""
557
+ result = runner.invoke(app, ["amd", "isa", "analyze", "/nonexistent/path.s"])
558
+
559
+ assert result.exit_code != 0
560
+ assert "not found" in result.output.lower() or "error" in result.output.lower()
561
+
562
+ def test_cli_wafer_amd_isa_analyze_directory(self, tmp_path: Path) -> None:
563
+ """wafer amd isa analyze should analyze directories."""
564
+ (tmp_path / "kernel1.s").write_text(SAMPLE_ISA)
565
+ (tmp_path / "kernel2.s").write_text(SAMPLE_ISA_WITH_SPILLS)
566
+
567
+ result = runner.invoke(app, ["amd", "isa", "analyze", str(tmp_path)])
568
+
569
+ assert result.exit_code == 0
570
+ output = strip_ansi(result.output)
571
+ assert "2 files" in output
572
+ assert "Successful" in output or "successful" in output
573
+
574
+ def test_cli_wafer_amd_isa_analyze_with_filter(self, tmp_path: Path) -> None:
575
+ """wafer amd isa analyze --filter should filter results."""
576
+ (tmp_path / "no_spills.s").write_text(SAMPLE_ISA)
577
+ (tmp_path / "has_spills.s").write_text(SAMPLE_ISA_WITH_SPILLS)
578
+
579
+ result = runner.invoke(
580
+ app,
581
+ ["amd", "isa", "analyze", str(tmp_path), "--filter", "spills > 0"]
582
+ )
583
+
584
+ assert result.exit_code == 0
585
+ output = strip_ansi(result.output)
586
+ # Should only show the file with spills
587
+ assert "1 files" in output or "spilling_kernel" in output
588
+
589
+ def test_cli_wafer_amd_isa_analyze_output_to_file(self, tmp_path: Path) -> None:
590
+ """wafer amd isa analyze --output should write to file."""
591
+ isa_file = tmp_path / "kernel.s"
592
+ isa_file.write_text(SAMPLE_ISA)
593
+ output_file = tmp_path / "output.json"
594
+
595
+ result = runner.invoke(
596
+ app,
597
+ ["amd", "isa", "analyze", str(isa_file), "--json", "-o", str(output_file)]
598
+ )
599
+
600
+ assert result.exit_code == 0
601
+ assert output_file.exists()
602
+ data = json.loads(output_file.read_text())
603
+ assert data["success"] is True
604
+
605
+ def test_analyze_command_with_api_params(self, tmp_path: Path) -> None:
606
+ """analyze_command should accept API params for .co files."""
607
+ co_file = tmp_path / "kernel.co"
608
+ co_file.write_bytes(b"fake code object")
609
+
610
+ # Even with fake API params, should attempt to call API
611
+ # (will fail because .co file doesn't exist at API, but params are passed)
612
+ with pytest.raises(RuntimeError) as exc_info:
613
+ analyze_command(
614
+ str(co_file),
615
+ api_url="https://fake.api.wafer.dev",
616
+ auth_headers={"Authorization": "Bearer fake"}
617
+ )
618
+
619
+ # Should have tried to make API call (not just reject due to missing params)
620
+ assert "API error" in str(exc_info.value) or "Request failed" in str(exc_info.value)