wafer-cli 0.2.50__tar.gz → 0.2.52__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 (121) hide show
  1. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/PKG-INFO +1 -1
  2. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/pyproject.toml +2 -2
  3. wafer_cli-0.2.52/tests/test_auth.py +311 -0
  4. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_billing.py +7 -6
  5. wafer_cli-0.2.52/tests/test_distributed_traces_cli.py +597 -0
  6. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_kernel_scope_cli.py +7 -1
  7. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/agent_defaults.py +186 -0
  8. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/auth.py +55 -20
  9. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/baseline.py +16 -17
  10. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/billing.py +3 -15
  11. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/cli.py +1286 -308
  12. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/cli_instructions.py +15 -5
  13. wafer_cli-0.2.52/wafer/corpora/amd/amd_instinct_gpu_specs.md +252 -0
  14. wafer_cli-0.2.52/wafer/corpora/amd/cdna2/01-architecture-overview.md +65 -0
  15. wafer_cli-0.2.52/wafer/corpora/amd/cdna2/02-matrix-instructions.md +85 -0
  16. wafer_cli-0.2.52/wafer/corpora/amd/cdna2/README.md +21 -0
  17. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/01-introduction.md +87 -0
  18. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/02-program-organization.md +149 -0
  19. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/03-kernel-state.md +326 -0
  20. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/04-program-flow-control.md +216 -0
  21. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/05-scalar-alu.md +263 -0
  22. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/06-vector-alu.md +277 -0
  23. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/07-matrix-instructions.md +346 -0
  24. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/08-scalar-memory.md +145 -0
  25. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/09-vector-memory.md +247 -0
  26. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/10-flat-memory.md +227 -0
  27. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/11-data-share.md +237 -0
  28. wafer_cli-0.2.52/wafer/corpora/amd/cdna3-isa/README.md +49 -0
  29. wafer_cli-0.2.52/wafer/corpora/amd/composable-kernel/01-ck-overview.md +217 -0
  30. wafer_cli-0.2.52/wafer/corpora/amd/hip/01-hip-programming-model.md +143 -0
  31. wafer_cli-0.2.52/wafer/corpora/amd/hip/02-hip-memory-management.md +183 -0
  32. wafer_cli-0.2.52/wafer/corpora/amd/hip/03-hip-synchronization.md +211 -0
  33. wafer_cli-0.2.52/wafer/corpora/amd/hip/04-hip-intrinsics.md +254 -0
  34. wafer_cli-0.2.52/wafer/corpora/amd/rocm-profiling/01-rocprofiler-overview.md +174 -0
  35. wafer_cli-0.2.52/wafer/corpora/common/flash-attention/01-flash-attention-overview.md +185 -0
  36. wafer_cli-0.2.52/wafer/corpora/common/vllm/01-vllm-overview.md +208 -0
  37. wafer_cli-0.2.52/wafer/corpora/nvidia/blackwell/01-architecture-overview.md +133 -0
  38. wafer_cli-0.2.52/wafer/corpora/nvidia/cuda-guide/01-cuda-programming-model.md +133 -0
  39. wafer_cli-0.2.52/wafer/corpora/nvidia/cuda-guide/02-cuda-memory-management.md +202 -0
  40. wafer_cli-0.2.52/wafer/corpora/nvidia/cuda-guide/03-cuda-best-practices.md +201 -0
  41. wafer_cli-0.2.52/wafer/corpora/nvidia/cuda-guide/04-cuda-streams-events.md +255 -0
  42. wafer_cli-0.2.52/wafer/corpora/nvidia/cutlass/01-cutlass-overview.md +165 -0
  43. wafer_cli-0.2.52/wafer/corpora/nvidia/hopper/01-overview.md +113 -0
  44. wafer_cli-0.2.52/wafer/corpora/nvidia/hopper/02-streaming-multiprocessor.md +143 -0
  45. wafer_cli-0.2.52/wafer/corpora/nvidia/hopper/03-tensor-cores.md +158 -0
  46. wafer_cli-0.2.52/wafer/corpora/nvidia/hopper/04-memory-hierarchy.md +219 -0
  47. wafer_cli-0.2.52/wafer/corpora/nvidia/hopper/05-synchronization.md +242 -0
  48. wafer_cli-0.2.52/wafer/corpora/nvidia/hopper/README.md +40 -0
  49. wafer_cli-0.2.52/wafer/corpora/nvidia/nsight/01-nsight-compute-overview.md +167 -0
  50. wafer_cli-0.2.52/wafer/corpora/nvidia/nsight/02-nsight-systems.md +187 -0
  51. wafer_cli-0.2.52/wafer/corpora/nvidia/ptx-isa/01-ptx-overview.md +169 -0
  52. wafer_cli-0.2.52/wafer/corpora/nvidia/ptx-isa/02-ptx-tensor-operations.md +179 -0
  53. wafer_cli-0.2.52/wafer/corpora/nvidia/triton/01-triton-overview.md +203 -0
  54. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/corpus.py +100 -5
  55. wafer_cli-0.2.52/wafer/distributed_traces.py +562 -0
  56. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/evaluate.py +2477 -6
  57. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/kernel_scope.py +7 -8
  58. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/nsys_analyze.py +55 -29
  59. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/nsys_profile.py +5 -9
  60. wafer_cli-0.2.52/wafer/skills/wafer-guide/SKILL.md +319 -0
  61. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/targets.py +14 -2
  62. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/targets_cli.py +4 -4
  63. wafer_cli-0.2.52/wafer/templates/optimize_flashinfer.py +197 -0
  64. wafer_cli-0.2.52/wafer/trace_compare.py +344 -0
  65. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/wevin_cli.py +144 -23
  66. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/workspaces.py +150 -65
  67. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer_cli.egg-info/PKG-INFO +1 -1
  68. wafer_cli-0.2.52/wafer_cli.egg-info/SOURCES.txt +115 -0
  69. wafer_cli-0.2.50/tests/test_auth.py +0 -193
  70. wafer_cli-0.2.50/wafer/skills/wafer-guide/SKILL.md +0 -129
  71. wafer_cli-0.2.50/wafer/trace_compare.py +0 -183
  72. wafer_cli-0.2.50/wafer_cli.egg-info/SOURCES.txt +0 -71
  73. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/README.md +0 -0
  74. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/setup.cfg +0 -0
  75. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_analytics.py +0 -0
  76. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_cli_coverage.py +0 -0
  77. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_cli_parity_integration.py +0 -0
  78. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_config_integration.py +0 -0
  79. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_file_operations_integration.py +0 -0
  80. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_nsys_analyze.py +0 -0
  81. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_nsys_profile.py +0 -0
  82. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_output.py +0 -0
  83. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_rocprof_compute_integration.py +0 -0
  84. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_skill_commands.py +0 -0
  85. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_ssh_integration.py +0 -0
  86. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_targets_ops.py +0 -0
  87. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_wevin_cli.py +0 -0
  88. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/tests/test_workflow_integration.py +0 -0
  89. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/GUIDE.md +0 -0
  90. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/__init__.py +0 -0
  91. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/analytics.py +0 -0
  92. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/api_client.py +0 -0
  93. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/autotuner.py +0 -0
  94. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/config.py +0 -0
  95. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/global_config.py +0 -0
  96. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/gpu_run.py +0 -0
  97. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/inference.py +0 -0
  98. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/ncu_analyze.py +0 -0
  99. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/output.py +0 -0
  100. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/problems.py +0 -0
  101. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/rocprof_compute.py +0 -0
  102. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/rocprof_sdk.py +0 -0
  103. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/rocprof_systems.py +0 -0
  104. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/specs_cli.py +0 -0
  105. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/ssh_keys.py +0 -0
  106. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/target_lock.py +0 -0
  107. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/targets_ops.py +0 -0
  108. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/templates/__init__.py +0 -0
  109. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/templates/aiter_optimize.py +0 -0
  110. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/templates/ask_docs.py +0 -0
  111. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/templates/audit.py +0 -0
  112. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/templates/optimize_kernel.py +0 -0
  113. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/templates/optimize_kernelbench.py +0 -0
  114. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/templates/optimize_vllm.py +0 -0
  115. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/templates/trace_analyze.py +0 -0
  116. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/tests/test_eval_cli_parity.py +0 -0
  117. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer/tracelens.py +0 -0
  118. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer_cli.egg-info/dependency_links.txt +0 -0
  119. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer_cli.egg-info/entry_points.txt +0 -0
  120. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/wafer_cli.egg-info/requires.txt +0 -0
  121. {wafer_cli-0.2.50 → wafer_cli-0.2.52}/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.50
3
+ Version: 0.2.52
4
4
  Summary: CLI for running GPU workloads, managing remote workspaces, and evaluating/optimizing kernels
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wafer-cli"
3
- version = "0.2.50"
3
+ version = "0.2.52"
4
4
  description = "CLI for running GPU workloads, managing remote workspaces, and evaluating/optimizing kernels"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -37,7 +37,7 @@ where = ["."]
37
37
  include = ["wafer*"]
38
38
 
39
39
  [tool.setuptools.package-data]
40
- wafer = ["GUIDE.md", "skills/*/SKILL.md"]
40
+ wafer = ["GUIDE.md", "skills/*/SKILL.md", "corpora/**/*.md"]
41
41
 
42
42
  [tool.ruff]
43
43
  line-length = 100
@@ -0,0 +1,311 @@
1
+ """Tests for authentication and credential management.
2
+
3
+ Tests the auth module's local JWT exp check, token refresh, and
4
+ network error handling.
5
+
6
+ Run with: PYTHONPATH=. uv run pytest tests/test_auth.py -v
7
+ """
8
+
9
+ import base64
10
+ import json
11
+ import time
12
+ from unittest.mock import MagicMock, patch
13
+
14
+ import httpx
15
+ import pytest
16
+
17
+
18
+ def _make_jwt(exp: float, extra: dict | None = None) -> str:
19
+ """Build a fake JWT with the given exp claim.
20
+
21
+ The token has no valid signature — we only need the payload section
22
+ for _token_expired() to decode.
23
+ """
24
+ header = base64.urlsafe_b64encode(json.dumps({"alg": "HS256"}).encode()).rstrip(b"=")
25
+ payload_dict = {"exp": exp, "sub": "user_123"}
26
+ if extra:
27
+ payload_dict.update(extra)
28
+ payload = base64.urlsafe_b64encode(json.dumps(payload_dict).encode()).rstrip(b"=")
29
+ signature = base64.urlsafe_b64encode(b"fakesig").rstrip(b"=")
30
+ return f"{header.decode()}.{payload.decode()}.{signature.decode()}"
31
+
32
+
33
+ # =============================================================================
34
+ # Unit Tests for _token_expired (pure function, no mocks needed)
35
+ # =============================================================================
36
+
37
+
38
+ class TestTokenExpired:
39
+ """Test local JWT exp claim checking."""
40
+
41
+ def test_valid_token_not_expired(self) -> None:
42
+ """Token with exp far in the future is not expired."""
43
+ from wafer.auth import _token_expired
44
+
45
+ token = _make_jwt(exp=time.time() + 3600)
46
+ assert _token_expired(token) is False
47
+
48
+ def test_expired_token(self) -> None:
49
+ """Token with exp in the past is expired."""
50
+ from wafer.auth import _token_expired
51
+
52
+ token = _make_jwt(exp=time.time() - 60)
53
+ assert _token_expired(token) is True
54
+
55
+ def test_token_within_margin_is_expired(self) -> None:
56
+ """Token expiring within the margin (30s) is treated as expired."""
57
+ from wafer.auth import _EXPIRY_MARGIN_SECONDS, _token_expired
58
+
59
+ token = _make_jwt(exp=time.time() + _EXPIRY_MARGIN_SECONDS - 1)
60
+ assert _token_expired(token) is True
61
+
62
+ def test_token_just_outside_margin_is_valid(self) -> None:
63
+ """Token expiring just beyond the margin is still valid."""
64
+ from wafer.auth import _EXPIRY_MARGIN_SECONDS, _token_expired
65
+
66
+ token = _make_jwt(exp=time.time() + _EXPIRY_MARGIN_SECONDS + 10)
67
+ assert _token_expired(token) is False
68
+
69
+ def test_garbage_token_is_expired(self) -> None:
70
+ """Unparseable token is treated as expired (fail closed)."""
71
+ from wafer.auth import _token_expired
72
+
73
+ assert _token_expired("not-a-jwt") is True
74
+
75
+ def test_missing_exp_claim_is_expired(self) -> None:
76
+ """Token without exp claim is treated as expired."""
77
+ from wafer.auth import _token_expired
78
+
79
+ header = base64.urlsafe_b64encode(json.dumps({"alg": "HS256"}).encode()).rstrip(b"=")
80
+ payload = base64.urlsafe_b64encode(json.dumps({"sub": "user_123"}).encode()).rstrip(b"=")
81
+ sig = base64.urlsafe_b64encode(b"fakesig").rstrip(b"=")
82
+ token = f"{header.decode()}.{payload.decode()}.{sig.decode()}"
83
+ assert _token_expired(token) is True
84
+
85
+ def test_empty_string_is_expired(self) -> None:
86
+ """Empty string is treated as expired."""
87
+ from wafer.auth import _token_expired
88
+
89
+ assert _token_expired("") is True
90
+
91
+
92
+ # =============================================================================
93
+ # Unit Tests for get_valid_token
94
+ # =============================================================================
95
+
96
+
97
+ class TestGetValidToken:
98
+ """Test that get_valid_token uses local exp check and handles refresh."""
99
+
100
+ def test_returns_token_when_not_expired(self) -> None:
101
+ """get_valid_token returns token directly when exp is in the future."""
102
+ from wafer.auth import Credentials, get_valid_token
103
+
104
+ valid_token = _make_jwt(exp=time.time() + 3600)
105
+ mock_creds = Credentials(
106
+ access_token=valid_token,
107
+ refresh_token="test_refresh",
108
+ email="test@example.com",
109
+ )
110
+
111
+ with patch("wafer.auth.load_credentials", return_value=mock_creds):
112
+ result = get_valid_token()
113
+
114
+ assert result == valid_token
115
+
116
+ def test_returns_none_when_no_credentials(self) -> None:
117
+ """get_valid_token returns None when no credentials are stored."""
118
+ from wafer.auth import get_valid_token
119
+
120
+ with patch("wafer.auth.load_credentials", return_value=None):
121
+ result = get_valid_token()
122
+
123
+ assert result is None
124
+
125
+ def test_refreshes_expired_token(self) -> None:
126
+ """get_valid_token refreshes and returns new token when expired."""
127
+ from wafer.auth import Credentials, get_valid_token
128
+
129
+ expired_token = _make_jwt(exp=time.time() - 60)
130
+ new_token = _make_jwt(exp=time.time() + 3600)
131
+ mock_creds = Credentials(
132
+ access_token=expired_token,
133
+ refresh_token="test_refresh",
134
+ email="test@example.com",
135
+ )
136
+
137
+ with patch("wafer.auth.load_credentials", return_value=mock_creds):
138
+ with patch("wafer.auth.refresh_access_token", return_value=(new_token, "new_refresh")):
139
+ with patch("wafer.auth.save_credentials") as mock_save:
140
+ result = get_valid_token()
141
+
142
+ assert result == new_token
143
+ mock_save.assert_called_once_with(new_token, "new_refresh", "test@example.com")
144
+
145
+ def test_returns_none_when_expired_and_no_refresh_token(self) -> None:
146
+ """get_valid_token returns None when expired with no refresh token."""
147
+ from wafer.auth import Credentials, get_valid_token
148
+
149
+ expired_token = _make_jwt(exp=time.time() - 60)
150
+ mock_creds = Credentials(
151
+ access_token=expired_token,
152
+ refresh_token=None,
153
+ email="test@example.com",
154
+ )
155
+
156
+ with patch("wafer.auth.load_credentials", return_value=mock_creds):
157
+ result = get_valid_token()
158
+
159
+ assert result is None
160
+
161
+ def test_returns_none_when_refresh_http_error(self) -> None:
162
+ """get_valid_token returns None when refresh gets HTTP error."""
163
+ from wafer.auth import Credentials, get_valid_token
164
+
165
+ expired_token = _make_jwt(exp=time.time() - 60)
166
+ mock_creds = Credentials(
167
+ access_token=expired_token,
168
+ refresh_token="test_refresh",
169
+ email="test@example.com",
170
+ )
171
+
172
+ mock_response = MagicMock()
173
+ mock_response.status_code = 401
174
+ http_error = httpx.HTTPStatusError("401", request=MagicMock(), response=mock_response)
175
+
176
+ with patch("wafer.auth.load_credentials", return_value=mock_creds):
177
+ with patch("wafer.auth.refresh_access_token", side_effect=http_error):
178
+ result = get_valid_token()
179
+
180
+ assert result is None
181
+
182
+ def test_returns_none_when_refresh_network_error(self) -> None:
183
+ """get_valid_token returns None when refresh has network error."""
184
+ from wafer.auth import Credentials, get_valid_token
185
+
186
+ expired_token = _make_jwt(exp=time.time() - 60)
187
+ mock_creds = Credentials(
188
+ access_token=expired_token,
189
+ refresh_token="test_refresh",
190
+ email="test@example.com",
191
+ )
192
+
193
+ with patch("wafer.auth.load_credentials", return_value=mock_creds):
194
+ with patch("wafer.auth.refresh_access_token", side_effect=httpx.ReadTimeout("timeout")):
195
+ result = get_valid_token()
196
+
197
+ assert result is None
198
+
199
+ def test_no_network_call_when_token_valid(self) -> None:
200
+ """get_valid_token makes NO network calls when token is not expired.
201
+
202
+ This is the core fix: the old code called verify_token() (network)
203
+ on every invocation. The new code only checks the JWT exp claim locally.
204
+ """
205
+ from wafer.auth import Credentials, get_valid_token
206
+
207
+ valid_token = _make_jwt(exp=time.time() + 3600)
208
+ mock_creds = Credentials(
209
+ access_token=valid_token,
210
+ refresh_token="test_refresh",
211
+ email="test@example.com",
212
+ )
213
+
214
+ with patch("wafer.auth.load_credentials", return_value=mock_creds):
215
+ with patch("wafer.auth.verify_token") as mock_verify:
216
+ with patch("wafer.auth.refresh_access_token") as mock_refresh:
217
+ result = get_valid_token()
218
+
219
+ assert result == valid_token
220
+ mock_verify.assert_not_called()
221
+ mock_refresh.assert_not_called()
222
+
223
+
224
+ # =============================================================================
225
+ # Unit Tests for get_auth_headers
226
+ # =============================================================================
227
+
228
+
229
+ class TestGetAuthHeaders:
230
+ """Test get_auth_headers returns correct headers based on token validity."""
231
+
232
+ def test_returns_bearer_token_when_valid(self) -> None:
233
+ """get_auth_headers returns Bearer token when token is not expired."""
234
+ from wafer.auth import Credentials, get_auth_headers
235
+
236
+ valid_token = _make_jwt(exp=time.time() + 3600)
237
+ mock_creds = Credentials(
238
+ access_token=valid_token,
239
+ refresh_token="test_refresh",
240
+ email="test@example.com",
241
+ )
242
+
243
+ with patch("wafer.auth.load_credentials", return_value=mock_creds):
244
+ result = get_auth_headers()
245
+
246
+ assert result == {"Authorization": f"Bearer {valid_token}"}
247
+
248
+ def test_returns_empty_dict_when_expired_and_refresh_fails(self) -> None:
249
+ """get_auth_headers returns {} when token expired and refresh fails."""
250
+ from wafer.auth import Credentials, get_auth_headers
251
+
252
+ expired_token = _make_jwt(exp=time.time() - 60)
253
+ mock_creds = Credentials(
254
+ access_token=expired_token,
255
+ refresh_token="test_refresh",
256
+ email="test@example.com",
257
+ )
258
+
259
+ with patch("wafer.auth.load_credentials", return_value=mock_creds):
260
+ with patch("wafer.auth.refresh_access_token", side_effect=httpx.ReadTimeout("timeout")):
261
+ result = get_auth_headers()
262
+
263
+ assert result == {}
264
+
265
+ def test_returns_empty_dict_when_no_credentials(self) -> None:
266
+ """get_auth_headers returns {} when not logged in."""
267
+ from wafer.auth import get_auth_headers
268
+
269
+ with patch("wafer.auth.load_credentials", return_value=None):
270
+ result = get_auth_headers()
271
+
272
+ assert result == {}
273
+
274
+
275
+ # =============================================================================
276
+ # Unit Tests for verify_token (still exists for explicit verification)
277
+ # =============================================================================
278
+
279
+
280
+ class TestVerifyTokenRaisesOnNetworkError:
281
+ """Document that verify_token raises network errors (caller must handle)."""
282
+
283
+ def test_verify_token_raises_read_timeout(self) -> None:
284
+ """verify_token raises ReadTimeout when API times out."""
285
+ from wafer.auth import verify_token
286
+
287
+ with patch("wafer.auth.get_api_url", return_value="https://api.example.com"):
288
+ with patch("httpx.Client") as mock_client_class:
289
+ mock_client = MagicMock()
290
+ mock_client.__enter__ = MagicMock(return_value=mock_client)
291
+ mock_client.__exit__ = MagicMock(return_value=False)
292
+ mock_client.post.side_effect = httpx.ReadTimeout("timeout")
293
+ mock_client_class.return_value = mock_client
294
+
295
+ with pytest.raises(httpx.ReadTimeout):
296
+ verify_token("test_token")
297
+
298
+ def test_verify_token_raises_connect_error(self) -> None:
299
+ """verify_token raises ConnectError when API is unreachable."""
300
+ from wafer.auth import verify_token
301
+
302
+ with patch("wafer.auth.get_api_url", return_value="https://api.example.com"):
303
+ with patch("httpx.Client") as mock_client_class:
304
+ mock_client = MagicMock()
305
+ mock_client.__enter__ = MagicMock(return_value=mock_client)
306
+ mock_client.__exit__ = MagicMock(return_value=False)
307
+ mock_client.post.side_effect = httpx.ConnectError("refused")
308
+ mock_client_class.return_value = mock_client
309
+
310
+ with pytest.raises(httpx.ConnectError):
311
+ verify_token("test_token")
@@ -128,8 +128,8 @@ class TestFormatUsageText:
128
128
  assert "$20.00" in text # topup
129
129
  assert "active" in text.lower()
130
130
 
131
- def test_start_tier_shows_upgrade_prompt(self) -> None:
132
- """Start tier should suggest upgrade."""
131
+ def test_start_tier_does_not_show_upgrade_prompt(self) -> None:
132
+ """Start tier should not suggest upgrade."""
133
133
  from wafer.billing import format_usage_text
134
134
 
135
135
  usage = {
@@ -145,8 +145,8 @@ class TestFormatUsageText:
145
145
  }
146
146
  text = format_usage_text(usage)
147
147
 
148
- assert "Start" in text
149
- assert "upgrade" in text.lower() or "portal" in text.lower()
148
+ assert "Hacker" in text
149
+ assert "upgrade" not in text.lower()
150
150
 
151
151
  def test_enterprise_tier_shows_unlimited(self) -> None:
152
152
  """Enterprise tier should show unlimited credits."""
@@ -394,7 +394,7 @@ class TestBillingTopupCommand:
394
394
  assert "500" in result.output # Should mention maximum
395
395
 
396
396
  def test_start_tier_blocked(self) -> None:
397
- """Start tier users should be blocked with upgrade message."""
397
+ """Start tier users should see a non-upgrade error message."""
398
398
  with patch("wafer.billing.get_auth_headers") as mock_auth:
399
399
  mock_auth.return_value = {"Authorization": "Bearer test"}
400
400
 
@@ -413,7 +413,8 @@ class TestBillingTopupCommand:
413
413
  result = runner.invoke(app, ["billing", "topup"])
414
414
 
415
415
  assert result.exit_code != 0
416
- assert "upgrade" in result.output.lower() or "portal" in result.output.lower()
416
+ assert "upgrade" not in result.output.lower()
417
+ assert "topup" in result.output.lower() or "top-up" in result.output.lower()
417
418
 
418
419
  def test_no_browser_flag(self) -> None:
419
420
  """--no-browser should print URL instead of opening browser."""