wafer-cli 0.2.49__tar.gz → 0.2.51__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.
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/PKG-INFO +1 -1
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/pyproject.toml +2 -2
- wafer_cli-0.2.51/tests/test_auth.py +311 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_billing.py +7 -6
- wafer_cli-0.2.51/tests/test_distributed_traces_cli.py +597 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_kernel_scope_cli.py +7 -1
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/agent_defaults.py +186 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/auth.py +55 -20
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/baseline.py +16 -17
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/billing.py +3 -15
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/cli.py +1273 -305
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/cli_instructions.py +1 -4
- wafer_cli-0.2.51/wafer/corpora/amd/amd_instinct_gpu_specs.md +252 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna2/01-architecture-overview.md +65 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna2/02-matrix-instructions.md +85 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna2/README.md +21 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/01-introduction.md +87 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/02-program-organization.md +149 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/03-kernel-state.md +326 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/04-program-flow-control.md +216 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/05-scalar-alu.md +263 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/06-vector-alu.md +277 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/07-matrix-instructions.md +346 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/08-scalar-memory.md +145 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/09-vector-memory.md +247 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/10-flat-memory.md +227 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/11-data-share.md +237 -0
- wafer_cli-0.2.51/wafer/corpora/amd/cdna3-isa/README.md +49 -0
- wafer_cli-0.2.51/wafer/corpora/amd/composable-kernel/01-ck-overview.md +217 -0
- wafer_cli-0.2.51/wafer/corpora/amd/hip/01-hip-programming-model.md +143 -0
- wafer_cli-0.2.51/wafer/corpora/amd/hip/02-hip-memory-management.md +183 -0
- wafer_cli-0.2.51/wafer/corpora/amd/hip/03-hip-synchronization.md +211 -0
- wafer_cli-0.2.51/wafer/corpora/amd/hip/04-hip-intrinsics.md +254 -0
- wafer_cli-0.2.51/wafer/corpora/amd/rocm-profiling/01-rocprofiler-overview.md +174 -0
- wafer_cli-0.2.51/wafer/corpora/common/flash-attention/01-flash-attention-overview.md +185 -0
- wafer_cli-0.2.51/wafer/corpora/common/vllm/01-vllm-overview.md +208 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/blackwell/01-architecture-overview.md +133 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/cuda-guide/01-cuda-programming-model.md +133 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/cuda-guide/02-cuda-memory-management.md +202 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/cuda-guide/03-cuda-best-practices.md +201 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/cuda-guide/04-cuda-streams-events.md +255 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/cutlass/01-cutlass-overview.md +165 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/hopper/01-overview.md +113 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/hopper/02-streaming-multiprocessor.md +143 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/hopper/03-tensor-cores.md +158 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/hopper/04-memory-hierarchy.md +219 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/hopper/05-synchronization.md +242 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/hopper/README.md +40 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/nsight/01-nsight-compute-overview.md +167 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/nsight/02-nsight-systems.md +187 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/ptx-isa/01-ptx-overview.md +169 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/ptx-isa/02-ptx-tensor-operations.md +179 -0
- wafer_cli-0.2.51/wafer/corpora/nvidia/triton/01-triton-overview.md +203 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/corpus.py +100 -5
- wafer_cli-0.2.51/wafer/distributed_traces.py +562 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/evaluate.py +2476 -5
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/kernel_scope.py +7 -8
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/nsys_analyze.py +55 -29
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/nsys_profile.py +5 -9
- wafer_cli-0.2.51/wafer/skills/wafer-guide/SKILL.md +319 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/targets.py +13 -1
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/targets_cli.py +4 -4
- wafer_cli-0.2.51/wafer/templates/optimize_flashinfer.py +197 -0
- wafer_cli-0.2.51/wafer/trace_compare.py +344 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/wevin_cli.py +144 -23
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/workspaces.py +150 -65
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer_cli.egg-info/PKG-INFO +1 -1
- wafer_cli-0.2.51/wafer_cli.egg-info/SOURCES.txt +115 -0
- wafer_cli-0.2.49/tests/test_auth.py +0 -193
- wafer_cli-0.2.49/wafer/skills/wafer-guide/SKILL.md +0 -129
- wafer_cli-0.2.49/wafer/trace_compare.py +0 -183
- wafer_cli-0.2.49/wafer_cli.egg-info/SOURCES.txt +0 -71
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/README.md +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/setup.cfg +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_analytics.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_cli_coverage.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_cli_parity_integration.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_config_integration.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_file_operations_integration.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_nsys_analyze.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_nsys_profile.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_output.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_rocprof_compute_integration.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_skill_commands.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_ssh_integration.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_targets_ops.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_wevin_cli.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/tests/test_workflow_integration.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/GUIDE.md +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/__init__.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/analytics.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/api_client.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/autotuner.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/config.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/global_config.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/gpu_run.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/inference.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/ncu_analyze.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/output.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/problems.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/rocprof_compute.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/rocprof_sdk.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/rocprof_systems.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/specs_cli.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/ssh_keys.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/target_lock.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/targets_ops.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/templates/__init__.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/templates/aiter_optimize.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/templates/ask_docs.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/templates/audit.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/templates/optimize_kernel.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/templates/optimize_kernelbench.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/templates/optimize_vllm.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/templates/trace_analyze.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/tests/test_eval_cli_parity.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer/tracelens.py +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer_cli.egg-info/dependency_links.txt +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer_cli.egg-info/entry_points.txt +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer_cli.egg-info/requires.txt +0 -0
- {wafer_cli-0.2.49 → wafer_cli-0.2.51}/wafer_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "wafer-cli"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.51"
|
|
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
|
|
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 "
|
|
149
|
-
assert "upgrade"
|
|
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
|
|
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"
|
|
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."""
|