JerrettDavis Copilot commited on
Commit
e4d7c78
·
2 Parent(s): a74cd9468245cd

Merge upstream/main into feat/canonical-pipeline

Browse files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

.claude-plugin/marketplace.json CHANGED
@@ -5,14 +5,14 @@
5
  },
6
  "metadata": {
7
  "description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
8
- "version": "0.10.0"
9
  },
10
  "plugins": [
11
  {
12
  "name": "headroom",
13
  "source": "./plugins/headroom-agent-hooks",
14
  "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
15
- "version": "0.10.0",
16
  "author": {
17
  "name": "Headroom Contributors",
18
  "url": "https://github.com/chopratejas/headroom"
 
5
  },
6
  "metadata": {
7
  "description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
8
+ "version": "0.11.0"
9
  },
10
  "plugins": [
11
  {
12
  "name": "headroom",
13
  "source": "./plugins/headroom-agent-hooks",
14
  "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
15
+ "version": "0.11.0",
16
  "author": {
17
  "name": "Headroom Contributors",
18
  "url": "https://github.com/chopratejas/headroom"
.github/plugin/marketplace.json CHANGED
@@ -5,14 +5,14 @@
5
  },
6
  "metadata": {
7
  "description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
8
- "version": "0.10.0"
9
  },
10
  "plugins": [
11
  {
12
  "name": "headroom",
13
  "source": "./plugins/headroom-agent-hooks",
14
  "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
15
- "version": "0.10.0",
16
  "author": {
17
  "name": "Headroom Contributors",
18
  "url": "https://github.com/chopratejas/headroom"
 
5
  },
6
  "metadata": {
7
  "description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
8
+ "version": "0.11.0"
9
  },
10
  "plugins": [
11
  {
12
  "name": "headroom",
13
  "source": "./plugins/headroom-agent-hooks",
14
  "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
15
+ "version": "0.11.0",
16
  "author": {
17
  "name": "Headroom Contributors",
18
  "url": "https://github.com/chopratejas/headroom"
CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
 
8
  ## [Unreleased]
9
 
 
 
 
 
 
 
 
 
10
  ### Added
11
  - **Telemetry stack & install-mode identity fields** — anonymous beacon now
12
  reports `headroom_stack` (how Headroom is invoked: `proxy`, `wrap_claude`,
@@ -39,6 +47,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
39
  hourly/daily/weekly/monthly rollups. Responses now include a
40
  `history_summary` block describing stored versus returned points.
41
 
 
 
 
 
 
 
 
 
 
 
 
42
  ## [0.5.22] - 2026-04-11
43
 
44
  ### Added
 
7
 
8
  ## [Unreleased]
9
 
10
+ ### Fixed
11
+ - **`headroom learn` no longer clobbers prior recommendations on re-run** —
12
+ the marker block in `CLAUDE.md` / `MEMORY.md` is now merged with the
13
+ prior block instead of wholesale-replaced. Sections re-surfaced by the
14
+ new run win; sections not re-surfaced are carried forward so learnings
15
+ accumulate across runs instead of disappearing. To fully rebuild the
16
+ block, delete it manually and re-run. (#231)
17
+
18
  ### Added
19
  - **Telemetry stack & install-mode identity fields** — anonymous beacon now
20
  reports `headroom_stack` (how Headroom is invoked: `proxy`, `wrap_claude`,
 
47
  hourly/daily/weekly/monthly rollups. Responses now include a
48
  `history_summary` block describing stored versus returned points.
49
 
50
+ ### Fixed
51
+ - **Streaming Anthropic requests are now visible to `/stats.recent_requests`
52
+ and `/transformations/feed`** — `_finalize_stream_response` did not call
53
+ `self.logger.log(...)`, so the entire streaming Anthropic code path (the
54
+ one Claude Code uses) silently bypassed the request logger. Only the
55
+ non-streaming Anthropic path and the Bedrock streaming path were logged.
56
+ As a consequence, `--log-messages` had no observable effect on the live
57
+ transformations feed for typical traffic. The streaming finalizer now
58
+ emits the same `RequestLog` shape the other paths do, including
59
+ `request_messages` when `log_full_messages` is enabled.
60
+
61
  ## [0.5.22] - 2026-04-11
62
 
63
  ### Added
codecov.yml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ codecov:
2
+ require_ci_to_pass: true
3
+
4
+ coverage:
5
+ status:
6
+ project:
7
+ default:
8
+ target: auto
9
+ patch:
10
+ default:
11
+ target: auto
12
+
13
+ ignore:
14
+ - "tests/**"
15
+ - "scripts/tests/**"
16
+ - ".github/**"
17
+ - ".claude-plugin/**"
18
+ - "plugins/headroom-agent-hooks/.claude-plugin/**"
19
+ - "plugins/headroom-agent-hooks/.github/**"
headroom/cli/wrap.py CHANGED
The diff for this file is too large to render. See raw diff
 
headroom/copilot_auth.py CHANGED
@@ -3,10 +3,13 @@
3
  from __future__ import annotations
4
 
5
  import asyncio
 
6
  import json
7
  import logging
8
  import os
 
9
  import time
 
10
  from dataclasses import dataclass
11
  from datetime import datetime
12
  from pathlib import Path
@@ -67,6 +70,11 @@ def _token_exchange_url() -> str:
67
  return os.environ.get("GITHUB_COPILOT_TOKEN_EXCHANGE_URL", DEFAULT_TOKEN_EXCHANGE_URL).strip()
68
 
69
 
 
 
 
 
 
70
  def _resolve_token_file_paths() -> list[Path]:
71
  override = os.environ.get("GITHUB_COPILOT_TOKEN_FILE", "").strip()
72
  if override:
@@ -83,6 +91,108 @@ def _resolve_token_file_paths() -> list[Path]:
83
  return paths
84
 
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  def _parse_expiry(value: Any) -> float | None:
87
  if value in (None, ""):
88
  return None
@@ -157,6 +267,14 @@ def read_cached_oauth_token() -> str | None:
157
  if token:
158
  return token
159
 
 
 
 
 
 
 
 
 
160
  host = _github_host()
161
  for path in _resolve_token_file_paths():
162
  try:
@@ -203,6 +321,16 @@ def is_copilot_api_url(url: str | None) -> bool:
203
  return "githubcopilot.com" in host
204
 
205
 
 
 
 
 
 
 
 
 
 
 
206
  class CopilotTokenProvider:
207
  """Resolve and cache short-lived Copilot API tokens."""
208
 
@@ -233,6 +361,16 @@ class CopilotTokenProvider:
233
  if not oauth_token:
234
  raise RuntimeError("No GitHub Copilot OAuth token is available.")
235
 
 
 
 
 
 
 
 
 
 
 
236
  exchanged = await self._exchange_token(oauth_token)
237
  self._cached = exchanged
238
  return exchanged
 
3
  from __future__ import annotations
4
 
5
  import asyncio
6
+ import ctypes
7
  import json
8
  import logging
9
  import os
10
+ import subprocess
11
  import time
12
+ from ctypes import wintypes
13
  from dataclasses import dataclass
14
  from datetime import datetime
15
  from pathlib import Path
 
70
  return os.environ.get("GITHUB_COPILOT_TOKEN_EXCHANGE_URL", DEFAULT_TOKEN_EXCHANGE_URL).strip()
71
 
72
 
73
+ def _should_exchange_oauth_token() -> bool:
74
+ raw = os.environ.get("GITHUB_COPILOT_USE_TOKEN_EXCHANGE", "").strip().lower()
75
+ return raw in {"1", "true", "yes", "on"}
76
+
77
+
78
  def _resolve_token_file_paths() -> list[Path]:
79
  override = os.environ.get("GITHUB_COPILOT_TOKEN_FILE", "").strip()
80
  if override:
 
91
  return paths
92
 
93
 
94
+ def _read_gh_cli_oauth_token() -> str | None:
95
+ gh_bin = os.environ.get("GH_PATH", "").strip() or "gh"
96
+ command = [gh_bin, "auth", "token"]
97
+ host = _github_host()
98
+ if host and host != DEFAULT_GITHUB_HOST:
99
+ command.extend(["--hostname", host])
100
+
101
+ try:
102
+ result = subprocess.run(
103
+ command,
104
+ capture_output=True,
105
+ text=True,
106
+ encoding="utf-8",
107
+ errors="replace",
108
+ check=False,
109
+ )
110
+ except OSError as exc:
111
+ logger.debug("Unable to invoke GitHub CLI for Copilot auth discovery: %s", exc)
112
+ return None
113
+
114
+ if result.returncode != 0:
115
+ logger.debug("GitHub CLI auth token lookup failed with exit code %s", result.returncode)
116
+ return None
117
+
118
+ token = result.stdout.strip()
119
+ return token or None
120
+
121
+
122
+ def _read_windows_copilot_cli_oauth_token() -> str | None:
123
+ if os.name != "nt":
124
+ return None
125
+
126
+ class FILETIME(ctypes.Structure):
127
+ _fields_ = [
128
+ ("dwLowDateTime", wintypes.DWORD),
129
+ ("dwHighDateTime", wintypes.DWORD),
130
+ ]
131
+
132
+ class CREDENTIAL(ctypes.Structure):
133
+ _fields_ = [
134
+ ("Flags", wintypes.DWORD),
135
+ ("Type", wintypes.DWORD),
136
+ ("TargetName", wintypes.LPWSTR),
137
+ ("Comment", wintypes.LPWSTR),
138
+ ("LastWritten", FILETIME),
139
+ ("CredentialBlobSize", wintypes.DWORD),
140
+ ("CredentialBlob", ctypes.POINTER(ctypes.c_ubyte)),
141
+ ("Persist", wintypes.DWORD),
142
+ ("AttributeCount", wintypes.DWORD),
143
+ ("Attributes", wintypes.LPVOID),
144
+ ("TargetAlias", wintypes.LPWSTR),
145
+ ("UserName", wintypes.LPWSTR),
146
+ ]
147
+
148
+ cred_ptr = ctypes.POINTER(CREDENTIAL)
149
+ credentials = ctypes.POINTER(cred_ptr)()
150
+ count = wintypes.DWORD()
151
+ win_dll = getattr(ctypes, "WinDLL", None)
152
+ if win_dll is None:
153
+ return None
154
+
155
+ advapi32 = win_dll("Advapi32.dll")
156
+ advapi32.CredEnumerateW.argtypes = [
157
+ wintypes.LPCWSTR,
158
+ wintypes.DWORD,
159
+ ctypes.POINTER(wintypes.DWORD),
160
+ ctypes.POINTER(ctypes.POINTER(cred_ptr)),
161
+ ]
162
+ advapi32.CredEnumerateW.restype = wintypes.BOOL
163
+ advapi32.CredFree.argtypes = [wintypes.LPVOID]
164
+
165
+ try:
166
+ if not advapi32.CredEnumerateW(None, 0, ctypes.byref(count), ctypes.byref(credentials)):
167
+ return None
168
+ except OSError as exc:
169
+ logger.debug("Unable to enumerate Windows credentials for Copilot auth discovery: %s", exc)
170
+ return None
171
+
172
+ host = _github_host().lower()
173
+ service_prefixes = [f"copilot-cli/{host}:"]
174
+ if "://" not in host:
175
+ service_prefixes.append(f"copilot-cli/https://{host}:")
176
+
177
+ try:
178
+ for idx in range(count.value):
179
+ credential = credentials[idx].contents
180
+ target = (credential.TargetName or "").strip().lower()
181
+ if not any(target.startswith(prefix) for prefix in service_prefixes):
182
+ continue
183
+ if credential.CredentialBlobSize <= 0 or not credential.CredentialBlob:
184
+ continue
185
+ blob = ctypes.string_at(credential.CredentialBlob, credential.CredentialBlobSize)
186
+ token = blob.decode("utf-8", errors="replace").strip()
187
+ if token:
188
+ return token
189
+ finally:
190
+ if credentials:
191
+ advapi32.CredFree(credentials)
192
+
193
+ return None
194
+
195
+
196
  def _parse_expiry(value: Any) -> float | None:
197
  if value in (None, ""):
198
  return None
 
267
  if token:
268
  return token
269
 
270
+ windows_copilot_token = _read_windows_copilot_cli_oauth_token()
271
+ if windows_copilot_token:
272
+ return windows_copilot_token
273
+
274
+ gh_token = _read_gh_cli_oauth_token()
275
+ if gh_token:
276
+ return gh_token
277
+
278
  host = _github_host()
279
  for path in _resolve_token_file_paths():
280
  try:
 
321
  return "githubcopilot.com" in host
322
 
323
 
324
+ def build_copilot_upstream_url(base_url: str, path: str) -> str:
325
+ """Build an upstream URL, normalizing GitHub Copilot's non-/v1 path layout."""
326
+
327
+ normalized_base = base_url.rstrip("/")
328
+ normalized_path = path if path.startswith("/") else f"/{path}"
329
+ if is_copilot_api_url(normalized_base) and normalized_path.startswith("/v1/"):
330
+ normalized_path = normalized_path[3:]
331
+ return f"{normalized_base}{normalized_path}"
332
+
333
+
334
  class CopilotTokenProvider:
335
  """Resolve and cache short-lived Copilot API tokens."""
336
 
 
361
  if not oauth_token:
362
  raise RuntimeError("No GitHub Copilot OAuth token is available.")
363
 
364
+ if not _should_exchange_oauth_token():
365
+ direct_token = CopilotAPIToken(
366
+ token=oauth_token,
367
+ expires_at=time.time() + 3600,
368
+ api_url=os.environ.get("GITHUB_COPILOT_API_URL", DEFAULT_API_URL).strip()
369
+ or DEFAULT_API_URL,
370
+ )
371
+ self._cached = direct_token
372
+ return direct_token
373
+
374
  exchanged = await self._exchange_token(oauth_token)
375
  self._cached = exchanged
376
  return exchanged
headroom/dashboard/templates/dashboard.html CHANGED
@@ -55,12 +55,12 @@
55
  <div class="inline-flex rounded-lg border border-border bg-surface p-1">
56
  <button class="px-3 py-1.5 text-sm rounded-md transition-colors"
57
  :class="viewMode === 'session' ? 'bg-accent text-black' : 'text-gray-400 hover:text-gray-200'"
58
- @click="viewMode = 'session'">
59
  Session
60
  </button>
61
  <button class="px-3 py-1.5 text-sm rounded-md transition-colors"
62
  :class="viewMode === 'history' ? 'bg-accent text-black' : 'text-gray-400 hover:text-gray-200'"
63
- @click="viewMode = 'history'">
64
  Historical
65
  </button>
66
  </div>
@@ -89,7 +89,7 @@
89
  </div>
90
  <button x-show="log_full_messages" id="feed-toggle"
91
  class="px-3 py-1.5 text-sm rounded-md border border-border bg-surface text-gray-300 hover:text-white transition-colors"
92
- @click="feedOpen = !feedOpen"
93
  :class="feedOpen ? 'bg-accent text-black' : ''">
94
  Live Feed
95
  </button>
@@ -1256,6 +1256,11 @@
1256
  savingsHistory: [],
1257
  expandedRows: {},
1258
  pollInterval: null,
 
 
 
 
 
1259
  feedOpen: false,
1260
  transformations: [],
1261
  feedScrolled_: false,
@@ -1267,31 +1272,55 @@
1267
 
1268
  async init() {
1269
  await this.fetchStats();
1270
- await this.fetchTransformations();
1271
 
1272
  this.pollInterval = setInterval(() => {
1273
- this.fetchStats();
1274
- this.fetchTransformations();
1275
- }, 3000);
1276
 
1277
  // Keyboard shortcuts
1278
  document.addEventListener('keydown', (e) => {
1279
  if (e.key === 'r' || e.key === 'R') {
1280
- this.fetchStats();
1281
  }
1282
  });
1283
  },
1284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1285
  async fetchStats() {
1286
  try {
1287
- const [statsRes, historyRes, healthRes] = await Promise.all([
1288
- fetch('/stats'),
1289
- fetch('/stats-history'),
1290
  fetch('/health')
1291
  ]);
1292
 
1293
  this.stats = await statsRes.json();
1294
- this.historyStats = await historyRes.json();
1295
  const health = await healthRes.json();
1296
  this.healthy = health.status === 'healthy';
1297
  this.version = health.version || '0.3.0';
@@ -1312,6 +1341,18 @@
1312
  }
1313
  },
1314
 
 
 
 
 
 
 
 
 
 
 
 
 
1315
  async fetchTransformations() {
1316
  try {
1317
  const prevLen = this.transformations.length;
@@ -1324,6 +1365,7 @@
1324
  }
1325
  this.transformations = data.transformations || [];
1326
  this.log_full_messages = data.log_full_messages ?? this.log_full_messages;
 
1327
  this.renderTransformations();
1328
  }
1329
  } catch (e) {
 
55
  <div class="inline-flex rounded-lg border border-border bg-surface p-1">
56
  <button class="px-3 py-1.5 text-sm rounded-md transition-colors"
57
  :class="viewMode === 'session' ? 'bg-accent text-black' : 'text-gray-400 hover:text-gray-200'"
58
+ @click="setViewMode('session')">
59
  Session
60
  </button>
61
  <button class="px-3 py-1.5 text-sm rounded-md transition-colors"
62
  :class="viewMode === 'history' ? 'bg-accent text-black' : 'text-gray-400 hover:text-gray-200'"
63
+ @click="setViewMode('history')">
64
  Historical
65
  </button>
66
  </div>
 
89
  </div>
90
  <button x-show="log_full_messages" id="feed-toggle"
91
  class="px-3 py-1.5 text-sm rounded-md border border-border bg-surface text-gray-300 hover:text-white transition-colors"
92
+ @click="toggleFeed()"
93
  :class="feedOpen ? 'bg-accent text-black' : ''">
94
  Live Feed
95
  </button>
 
1256
  savingsHistory: [],
1257
  expandedRows: {},
1258
  pollInterval: null,
1259
+ statsPollMs: 5000,
1260
+ historyPollMs: 30000,
1261
+ feedPollMs: 5000,
1262
+ lastHistoryFetchMs: 0,
1263
+ lastFeedFetchMs: 0,
1264
  feedOpen: false,
1265
  transformations: [],
1266
  feedScrolled_: false,
 
1272
 
1273
  async init() {
1274
  await this.fetchStats();
 
1275
 
1276
  this.pollInterval = setInterval(() => {
1277
+ this.pollDashboard();
1278
+ }, this.statsPollMs);
 
1279
 
1280
  // Keyboard shortcuts
1281
  document.addEventListener('keydown', (e) => {
1282
  if (e.key === 'r' || e.key === 'R') {
1283
+ this.pollDashboard(true);
1284
  }
1285
  });
1286
  },
1287
 
1288
+ async pollDashboard(force = false) {
1289
+ if (!force && document.hidden) return;
1290
+
1291
+ await this.fetchStats();
1292
+
1293
+ const now = Date.now();
1294
+ if (this.viewMode === 'history' && (force || now - this.lastHistoryFetchMs >= this.historyPollMs)) {
1295
+ await this.fetchHistoryStats();
1296
+ }
1297
+ if (this.feedOpen && (force || now - this.lastFeedFetchMs >= this.feedPollMs)) {
1298
+ await this.fetchTransformations();
1299
+ }
1300
+ },
1301
+
1302
+ async setViewMode(mode) {
1303
+ this.viewMode = mode;
1304
+ if (mode === 'history') {
1305
+ await this.fetchHistoryStats();
1306
+ }
1307
+ },
1308
+
1309
+ async toggleFeed() {
1310
+ this.feedOpen = !this.feedOpen;
1311
+ if (this.feedOpen) {
1312
+ await this.fetchTransformations();
1313
+ }
1314
+ },
1315
+
1316
  async fetchStats() {
1317
  try {
1318
+ const [statsRes, healthRes] = await Promise.all([
1319
+ fetch('/stats?cached=1'),
 
1320
  fetch('/health')
1321
  ]);
1322
 
1323
  this.stats = await statsRes.json();
 
1324
  const health = await healthRes.json();
1325
  this.healthy = health.status === 'healthy';
1326
  this.version = health.version || '0.3.0';
 
1341
  }
1342
  },
1343
 
1344
+ async fetchHistoryStats() {
1345
+ try {
1346
+ const response = await fetch('/stats-history');
1347
+ if (response.ok) {
1348
+ this.historyStats = await response.json();
1349
+ this.lastHistoryFetchMs = Date.now();
1350
+ }
1351
+ } catch (e) {
1352
+ console.error('Failed to fetch history stats:', e);
1353
+ }
1354
+ },
1355
+
1356
  async fetchTransformations() {
1357
  try {
1358
  const prevLen = this.transformations.length;
 
1365
  }
1366
  this.transformations = data.transformations || [];
1367
  this.log_full_messages = data.log_full_messages ?? this.log_full_messages;
1368
+ this.lastFeedFetchMs = Date.now();
1369
  this.renderTransformations();
1370
  }
1371
  } catch (e) {
headroom/learn/writer.py CHANGED
@@ -87,8 +87,73 @@ def _build_section(recommendations: list[Recommendation]) -> str:
87
  return "\n".join(lines)
88
 
89
 
90
- def _merge_into_file(file_path: Path, section: str) -> str:
91
- """Merge the section into an existing file, replacing any prior section."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  if file_path.exists():
93
  existing = file_path.read_text()
94
  if _MARKER_START in existing:
@@ -119,8 +184,7 @@ class ClaudeCodeWriter(ContextWriter):
119
 
120
  if context_recs:
121
  claude_md_path = self._resolve_context_path(project)
122
- section_content = _build_section(context_recs)
123
- full_content = _merge_into_file(claude_md_path, section_content)
124
  result.add(claude_md_path, full_content)
125
  if not dry_run:
126
  claude_md_path.parent.mkdir(parents=True, exist_ok=True)
@@ -128,8 +192,7 @@ class ClaudeCodeWriter(ContextWriter):
128
 
129
  if memory_recs:
130
  memory_path = self._resolve_memory_path(project)
131
- section_content = _build_section(memory_recs)
132
- full_content = _merge_into_file(memory_path, section_content)
133
  result.add(memory_path, full_content)
134
  if not dry_run:
135
  memory_path.parent.mkdir(parents=True, exist_ok=True)
@@ -174,8 +237,7 @@ class CodexWriter(ContextWriter):
174
 
175
  if context_recs:
176
  agents_md = project.context_file or (project.project_path / "AGENTS.md")
177
- section_content = _build_section(context_recs)
178
- full_content = _merge_into_file(agents_md, section_content)
179
  result.add(agents_md, full_content)
180
  if not dry_run:
181
  agents_md.parent.mkdir(parents=True, exist_ok=True)
@@ -183,8 +245,7 @@ class CodexWriter(ContextWriter):
183
 
184
  if memory_recs:
185
  instructions_md = project.memory_file or (project.data_path.parent / "instructions.md")
186
- section_content = _build_section(memory_recs)
187
- full_content = _merge_into_file(instructions_md, section_content)
188
  result.add(instructions_md, full_content)
189
  if not dry_run:
190
  instructions_md.parent.mkdir(parents=True, exist_ok=True)
@@ -214,8 +275,7 @@ class GeminiWriter(ContextWriter):
214
  return result
215
 
216
  gemini_md = project.context_file or (project.project_path / "GEMINI.md")
217
- section_content = _build_section(recommendations)
218
- full_content = _merge_into_file(gemini_md, section_content)
219
  result.add(gemini_md, full_content)
220
  if not dry_run:
221
  gemini_md.parent.mkdir(parents=True, exist_ok=True)
 
87
  return "\n".join(lines)
88
 
89
 
90
+ # Matches the "*~N tokens/session saved*" annotation emitted by _build_section.
91
+ _TOKENS_ANNOTATION_PATTERN = re.compile(r"\*~([\d,]+) tokens/session saved\*\n?")
92
+
93
+
94
+ def _parse_prior_recommendations(existing: str) -> list[Recommendation]:
95
+ """Parse recommendations out of a prior marker block.
96
+
97
+ Returns [] if no marker block is present or it contains no sections.
98
+ The returned Recommendation objects are round-trip compatible with
99
+ _build_section — target is set to a placeholder since the marker block
100
+ itself doesn't record it (blocks are always per-file and per-target).
101
+ """
102
+ match = _MARKER_PATTERN.search(existing)
103
+ if not match:
104
+ return []
105
+ inner = match.group(0)[len(_MARKER_START) : -len(_MARKER_END)]
106
+
107
+ recs: list[Recommendation] = []
108
+ for part in re.split(r"\n### ", "\n" + inner)[1:]:
109
+ heading_line, _, body = part.partition("\n")
110
+ heading = heading_line.strip()
111
+ if not heading:
112
+ continue
113
+
114
+ tokens_saved = 0
115
+ tokens_match = _TOKENS_ANNOTATION_PATTERN.match(body)
116
+ if tokens_match:
117
+ tokens_saved = int(tokens_match.group(1).replace(",", ""))
118
+ body = body[tokens_match.end() :]
119
+
120
+ recs.append(
121
+ Recommendation(
122
+ target=RecommendationTarget.CONTEXT_FILE,
123
+ section=heading,
124
+ content=body.rstrip(),
125
+ estimated_tokens_saved=tokens_saved,
126
+ )
127
+ )
128
+ return recs
129
+
130
+
131
+ def _merge_recommendations(
132
+ file_path: Path,
133
+ new_recommendations: list[Recommendation],
134
+ ) -> list[Recommendation]:
135
+ """Union new recommendations with prior ones whose section is not re-surfaced.
136
+
137
+ Sections produced by the current run take precedence over same-named
138
+ prior sections — the latest analysis is authoritative. Prior sections
139
+ whose headings do not reappear in the new run are carried forward so
140
+ a re-run doesn't silently drop accumulated learnings. To fully rebuild
141
+ the block, delete it manually and re-run.
142
+ """
143
+ if not file_path.exists():
144
+ return new_recommendations
145
+ prior = _parse_prior_recommendations(file_path.read_text())
146
+ if not prior:
147
+ return new_recommendations
148
+ new_sections = {r.section for r in new_recommendations}
149
+ carried = [p for p in prior if p.section not in new_sections]
150
+ return list(new_recommendations) + carried
151
+
152
+
153
+ def _merge_into_file(file_path: Path, new_recommendations: list[Recommendation]) -> str:
154
+ """Merge new recommendations with any existing marker block and rebuild the file."""
155
+ merged = _merge_recommendations(file_path, new_recommendations)
156
+ section = _build_section(merged)
157
  if file_path.exists():
158
  existing = file_path.read_text()
159
  if _MARKER_START in existing:
 
184
 
185
  if context_recs:
186
  claude_md_path = self._resolve_context_path(project)
187
+ full_content = _merge_into_file(claude_md_path, context_recs)
 
188
  result.add(claude_md_path, full_content)
189
  if not dry_run:
190
  claude_md_path.parent.mkdir(parents=True, exist_ok=True)
 
192
 
193
  if memory_recs:
194
  memory_path = self._resolve_memory_path(project)
195
+ full_content = _merge_into_file(memory_path, memory_recs)
 
196
  result.add(memory_path, full_content)
197
  if not dry_run:
198
  memory_path.parent.mkdir(parents=True, exist_ok=True)
 
237
 
238
  if context_recs:
239
  agents_md = project.context_file or (project.project_path / "AGENTS.md")
240
+ full_content = _merge_into_file(agents_md, context_recs)
 
241
  result.add(agents_md, full_content)
242
  if not dry_run:
243
  agents_md.parent.mkdir(parents=True, exist_ok=True)
 
245
 
246
  if memory_recs:
247
  instructions_md = project.memory_file or (project.data_path.parent / "instructions.md")
248
+ full_content = _merge_into_file(instructions_md, memory_recs)
 
249
  result.add(instructions_md, full_content)
250
  if not dry_run:
251
  instructions_md.parent.mkdir(parents=True, exist_ok=True)
 
275
  return result
276
 
277
  gemini_md = project.context_file or (project.project_path / "GEMINI.md")
278
+ full_content = _merge_into_file(gemini_md, recommendations)
 
279
  result.add(gemini_md, full_content)
280
  if not dry_run:
281
  gemini_md.parent.mkdir(parents=True, exist_ok=True)
headroom/proxy/handlers/openai.py CHANGED
The diff for this file is too large to render. See raw diff
 
headroom/proxy/handlers/streaming.py CHANGED
@@ -451,6 +451,7 @@ class StreamingMixin:
451
  optimization_latency: float,
452
  stream_state: dict[str, Any],
453
  start_time: float,
 
454
  pipeline_timing: dict[str, float] | None = None,
455
  prefix_tracker: Any | None = None,
456
  original_messages: list[dict] | None = None,
@@ -547,6 +548,38 @@ class StreamingMixin:
547
  uncached_input_tokens=uncached_input_tokens,
548
  )
549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  async def _stream_response(
551
  self,
552
  url: str,
@@ -702,6 +735,7 @@ class StreamingMixin:
702
  optimization_latency=optimization_latency,
703
  stream_state=stream_state,
704
  start_time=start_time,
 
705
  pipeline_timing=pipeline_timing,
706
  prefix_tracker=prefix_tracker,
707
  original_messages=original_messages,
@@ -875,6 +909,7 @@ class StreamingMixin:
875
  optimization_latency=optimization_latency,
876
  stream_state=stream_state,
877
  start_time=start_time,
 
878
  pipeline_timing=pipeline_timing,
879
  prefix_tracker=prefix_tracker,
880
  original_messages=original_messages,
 
451
  optimization_latency: float,
452
  stream_state: dict[str, Any],
453
  start_time: float,
454
+ tags: dict[str, str] | None = None,
455
  pipeline_timing: dict[str, float] | None = None,
456
  prefix_tracker: Any | None = None,
457
  original_messages: list[dict] | None = None,
 
548
  uncached_input_tokens=uncached_input_tokens,
549
  )
550
 
551
+ # Log the request to the in-memory request logger so it shows up in
552
+ # /stats `recent_requests` and `/transformations/feed`. Without this
553
+ # the streaming Anthropic path (which is what Claude Code uses) is
554
+ # invisible to both surfaces — only Bedrock streaming and the
555
+ # non-streaming Anthropic path were logged previously.
556
+ if getattr(self, "logger", None) is not None:
557
+ from headroom.proxy.models import RequestLog
558
+
559
+ self.logger.log(
560
+ RequestLog(
561
+ request_id=request_id,
562
+ timestamp=datetime.now().isoformat(),
563
+ provider=provider,
564
+ model=model,
565
+ input_tokens_original=original_tokens,
566
+ input_tokens_optimized=optimized_tokens,
567
+ output_tokens=output_tokens,
568
+ tokens_saved=tokens_saved,
569
+ savings_percent=(tokens_saved / original_tokens * 100)
570
+ if original_tokens > 0
571
+ else 0,
572
+ optimization_latency_ms=optimization_latency,
573
+ total_latency_ms=total_latency,
574
+ tags=tags or {},
575
+ cache_hit=False,
576
+ transforms_applied=transforms_applied,
577
+ request_messages=body.get("messages")
578
+ if getattr(self.config, "log_full_messages", False)
579
+ else None,
580
+ )
581
+ )
582
+
583
  async def _stream_response(
584
  self,
585
  url: str,
 
735
  optimization_latency=optimization_latency,
736
  stream_state=stream_state,
737
  start_time=start_time,
738
+ tags=tags,
739
  pipeline_timing=pipeline_timing,
740
  prefix_tracker=prefix_tracker,
741
  original_messages=original_messages,
 
909
  optimization_latency=optimization_latency,
910
  stream_state=stream_state,
911
  start_time=start_time,
912
+ tags=tags,
913
  pipeline_timing=pipeline_timing,
914
  prefix_tracker=prefix_tracker,
915
  original_messages=original_messages,
headroom/proxy/helpers.py CHANGED
@@ -11,8 +11,10 @@ from __future__ import annotations
11
  import json
12
  import logging
13
  import random
 
 
14
  from pathlib import Path
15
- from typing import TYPE_CHECKING, Any
16
 
17
  from headroom import paths as _paths
18
 
@@ -21,6 +23,14 @@ if TYPE_CHECKING:
21
 
22
  logger = logging.getLogger("headroom.proxy")
23
 
 
 
 
 
 
 
 
 
24
  # Maximum request body size (100MB - increased to support image-heavy requests)
25
  MAX_REQUEST_BODY_SIZE = 100 * 1024 * 1024
26
 
@@ -114,11 +124,18 @@ def _get_rtk_stats() -> dict[str, Any] | None:
114
  """Get rtk (Rust Token Killer) savings stats if rtk is installed.
115
 
116
  Reads from rtk's tracking database via `rtk gain --format json`.
117
- Returns None if rtk is not installed.
 
118
  """
119
  import shutil
120
  import subprocess as _sp
121
 
 
 
 
 
 
 
122
  rtk_bin = shutil.which("rtk")
123
  if not rtk_bin:
124
  # Check headroom-managed install. Preserve the historical Unix-name
@@ -128,7 +145,16 @@ def _get_rtk_stats() -> dict[str, Any] | None:
128
  if rtk_managed.exists():
129
  rtk_bin = str(rtk_managed)
130
  else:
131
- return None
 
 
 
 
 
 
 
 
 
132
 
133
  try:
134
  result = _sp.run(
@@ -140,21 +166,36 @@ def _get_rtk_stats() -> dict[str, Any] | None:
140
  if result.returncode == 0 and result.stdout.strip():
141
  data = json.loads(result.stdout)
142
  summary = data.get("summary", {})
143
- return {
144
  "installed": True,
145
  "total_commands": summary.get("total_commands", 0),
146
  "tokens_saved": summary.get("total_saved", 0),
147
  "avg_savings_pct": summary.get("avg_savings_pct", 0.0),
148
  }
 
 
 
 
 
 
 
149
  except Exception:
150
- pass
151
-
152
- return {
153
- "installed": True,
154
- "total_commands": 0,
155
- "tokens_saved": 0,
156
- "avg_savings_pct": 0.0,
157
- }
 
 
 
 
 
 
 
 
158
 
159
 
160
  def is_anthropic_auth(headers: dict[str, str]) -> bool:
 
11
  import json
12
  import logging
13
  import random
14
+ import threading
15
+ import time
16
  from pathlib import Path
17
+ from typing import TYPE_CHECKING, Any, cast
18
 
19
  from headroom import paths as _paths
20
 
 
23
 
24
  logger = logging.getLogger("headroom.proxy")
25
 
26
+ RTK_STATS_CACHE_TTL_SECONDS = 5.0
27
+ _rtk_stats_cache_lock = threading.Lock()
28
+ _rtk_stats_cache: dict[str, Any] = {
29
+ "expires_at": 0.0,
30
+ "has_value": False,
31
+ "value": None,
32
+ }
33
+
34
  # Maximum request body size (100MB - increased to support image-heavy requests)
35
  MAX_REQUEST_BODY_SIZE = 100 * 1024 * 1024
36
 
 
124
  """Get rtk (Rust Token Killer) savings stats if rtk is installed.
125
 
126
  Reads from rtk's tracking database via `rtk gain --format json`.
127
+ Results are memoized briefly so dashboard polling does not spawn a new
128
+ subprocess on every refresh.
129
  """
130
  import shutil
131
  import subprocess as _sp
132
 
133
+ now = time.monotonic()
134
+ with _rtk_stats_cache_lock:
135
+ if _rtk_stats_cache["has_value"] and now < float(_rtk_stats_cache["expires_at"]):
136
+ return cast(dict[str, Any] | None, _rtk_stats_cache["value"])
137
+
138
+ payload: dict[str, Any] | None
139
  rtk_bin = shutil.which("rtk")
140
  if not rtk_bin:
141
  # Check headroom-managed install. Preserve the historical Unix-name
 
145
  if rtk_managed.exists():
146
  rtk_bin = str(rtk_managed)
147
  else:
148
+ payload = None
149
+ with _rtk_stats_cache_lock:
150
+ _rtk_stats_cache.update(
151
+ {
152
+ "expires_at": time.monotonic() + RTK_STATS_CACHE_TTL_SECONDS,
153
+ "has_value": True,
154
+ "value": payload,
155
+ }
156
+ )
157
+ return payload
158
 
159
  try:
160
  result = _sp.run(
 
166
  if result.returncode == 0 and result.stdout.strip():
167
  data = json.loads(result.stdout)
168
  summary = data.get("summary", {})
169
+ payload = {
170
  "installed": True,
171
  "total_commands": summary.get("total_commands", 0),
172
  "tokens_saved": summary.get("total_saved", 0),
173
  "avg_savings_pct": summary.get("avg_savings_pct", 0.0),
174
  }
175
+ else:
176
+ payload = {
177
+ "installed": True,
178
+ "total_commands": 0,
179
+ "tokens_saved": 0,
180
+ "avg_savings_pct": 0.0,
181
+ }
182
  except Exception:
183
+ payload = {
184
+ "installed": True,
185
+ "total_commands": 0,
186
+ "tokens_saved": 0,
187
+ "avg_savings_pct": 0.0,
188
+ }
189
+
190
+ with _rtk_stats_cache_lock:
191
+ _rtk_stats_cache.update(
192
+ {
193
+ "expires_at": time.monotonic() + RTK_STATS_CACHE_TTL_SECONDS,
194
+ "has_value": True,
195
+ "value": payload,
196
+ }
197
+ )
198
+ return payload
199
 
200
 
201
  def is_anthropic_auth(headers: dict[str, str]) -> bool:
headroom/proxy/server.py CHANGED
@@ -32,7 +32,7 @@ import sys
32
  import time
33
  from datetime import datetime, timezone
34
  from pathlib import Path
35
- from typing import TYPE_CHECKING, Any, Literal
36
 
37
  if TYPE_CHECKING:
38
  from ..backends.base import Backend
@@ -1403,9 +1403,12 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
1403
  """Serve the Headroom dashboard UI."""
1404
  return get_dashboard_html()
1405
 
1406
- @app.get("/stats")
1407
- async def stats():
1408
- """Get comprehensive proxy statistics.
 
 
 
1409
 
1410
  This is the main stats endpoint - it aggregates data from all subsystems:
1411
  - Request metrics (total, cached, failed, by model/provider)
@@ -1634,6 +1637,44 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
1634
  **get_quota_registry().get_all_stats(),
1635
  }
1636
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1637
  @app.get("/stats-history")
1638
  async def stats_history(
1639
  format: Literal["json", "csv"] = "json",
 
32
  import time
33
  from datetime import datetime, timezone
34
  from pathlib import Path
35
+ from typing import TYPE_CHECKING, Any, Literal, cast
36
 
37
  if TYPE_CHECKING:
38
  from ..backends.base import Backend
 
1403
  """Serve the Headroom dashboard UI."""
1404
  return get_dashboard_html()
1405
 
1406
+ DASHBOARD_STATS_CACHE_TTL_SECONDS = 5.0
1407
+ _stats_snapshot_lock = asyncio.Lock()
1408
+ _stats_snapshot: dict[str, Any] = {"expires_at": 0.0, "value": None}
1409
+
1410
+ async def _build_stats_payload() -> dict[str, Any]:
1411
+ """Build the full `/stats` response payload.
1412
 
1413
  This is the main stats endpoint - it aggregates data from all subsystems:
1414
  - Request metrics (total, cached, failed, by model/provider)
 
1637
  **get_quota_registry().get_all_stats(),
1638
  }
1639
 
1640
+ async def _get_cached_stats_payload() -> dict[str, Any]:
1641
+ """Return a short-TTL cached `/stats` snapshot for dashboard polling."""
1642
+ now = time.monotonic()
1643
+ cached_payload = cast(dict[str, Any] | None, _stats_snapshot.get("value"))
1644
+ if cached_payload is not None and now < float(_stats_snapshot["expires_at"]):
1645
+ return cached_payload
1646
+
1647
+ async with _stats_snapshot_lock:
1648
+ now = time.monotonic()
1649
+ cached_payload = cast(dict[str, Any] | None, _stats_snapshot.get("value"))
1650
+ if cached_payload is not None and now < float(_stats_snapshot["expires_at"]):
1651
+ return cached_payload
1652
+
1653
+ payload = await _build_stats_payload()
1654
+ _stats_snapshot["value"] = payload
1655
+ _stats_snapshot["expires_at"] = time.monotonic() + DASHBOARD_STATS_CACHE_TTL_SECONDS
1656
+ return payload
1657
+
1658
+ @app.get("/stats")
1659
+ async def stats(cached: bool = False):
1660
+ """Get comprehensive proxy statistics.
1661
+
1662
+ This is the main stats endpoint - it aggregates data from all subsystems:
1663
+ - Request metrics (total, cached, failed, by model/provider)
1664
+ - Token usage and savings
1665
+ - Cost tracking
1666
+ - Canonical persisted display_session metrics for downstream dashboards
1667
+ - Compression (CCR) statistics
1668
+ - Telemetry/TOIN (data flywheel) statistics
1669
+ - Cache and rate limiter stats
1670
+
1671
+ Use ``?cached=1`` for the dashboard fast path. That returns a short-TTL
1672
+ snapshot to avoid rebuilding the full payload on every UI poll.
1673
+ """
1674
+ if cached:
1675
+ return await _get_cached_stats_payload()
1676
+ return await _build_stats_payload()
1677
+
1678
  @app.get("/stats-history")
1679
  async def stats_history(
1680
  format: Literal["json", "csv"] = "json",
plugins/headroom-agent-hooks/.claude-plugin/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "headroom",
3
- "version": "0.10.0",
4
  "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
5
  "author": {
6
  "name": "Headroom Contributors",
 
1
  {
2
  "name": "headroom",
3
+ "version": "0.11.0",
4
  "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
5
  "author": {
6
  "name": "Headroom Contributors",
plugins/headroom-agent-hooks/.github/plugin/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "headroom",
3
- "version": "0.10.0",
4
  "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
5
  "author": {
6
  "name": "Headroom Contributors",
 
1
  {
2
  "name": "headroom",
3
+ "version": "0.11.0",
4
  "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
5
  "author": {
6
  "name": "Headroom Contributors",
tests/test_cli/test_wrap_copilot.py CHANGED
@@ -1,212 +1,335 @@
1
- """Tests for `headroom wrap copilot` command."""
2
-
3
- from __future__ import annotations
4
-
5
- import sys
6
- import types
7
- from pathlib import Path
8
- from unittest.mock import patch
9
-
10
- import pytest
11
- from click.testing import CliRunner
12
-
13
- if "fastapi" not in sys.modules:
14
- fastapi_mod = types.ModuleType("fastapi")
15
- fastapi_mod.FastAPI = type("FastAPI", (), {})
16
- fastapi_mod.Request = type("Request", (), {})
17
- fastapi_mod.WebSocket = type("WebSocket", (), {})
18
- sys.modules["fastapi"] = fastapi_mod
19
-
20
- if "fastapi.responses" not in sys.modules:
21
- responses_mod = types.ModuleType("fastapi.responses")
22
- responses_mod.Response = type("Response", (), {})
23
- sys.modules["fastapi.responses"] = responses_mod
24
-
25
- from headroom.cli import wrap as wrap_cli
26
- from headroom.cli.main import main
27
-
28
-
29
- @pytest.fixture
30
- def runner() -> CliRunner:
31
- return CliRunner()
32
-
33
-
34
- def test_wrap_copilot_auto_anthropic_injects_instructions(
35
- runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
36
- ) -> None:
37
- monkeypatch.chdir(tmp_path)
38
- monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test-dummy")
39
- captured: dict[str, object] = {}
40
-
41
- def fake_launch_tool(**kwargs): # noqa: ANN003
42
- captured.update(kwargs)
43
-
44
- with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
45
- with patch("headroom.cli.wrap._ensure_rtk_binary", return_value=Path("/tmp/rtk")):
46
- with patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool):
47
- result = runner.invoke(
48
- main,
49
- ["wrap", "copilot", "--", "--model", "claude-sonnet-4-20250514"],
50
- )
51
-
52
- assert result.exit_code == 0, result.output
53
- instructions = tmp_path / ".github" / "copilot-instructions.md"
54
- assert instructions.exists()
55
- content = instructions.read_text()
56
- assert wrap_cli._RTK_MARKER in content
57
- assert "RTK (Rust Token Killer)" in content
58
-
59
- env = captured["env"]
60
- assert isinstance(env, dict)
61
- assert env["COPILOT_PROVIDER_TYPE"] == "anthropic"
62
- assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787"
63
- assert "COPILOT_PROVIDER_WIRE_API" not in env
64
- assert captured["agent_type"] == "copilot"
65
- assert captured["tool_label"] == "COPILOT"
66
- assert captured["args"] == ("--model", "claude-sonnet-4-20250514")
67
-
68
-
69
- def test_wrap_copilot_openai_backend_sets_completions_env(
70
- runner: CliRunner, monkeypatch: pytest.MonkeyPatch
71
- ) -> None:
72
- monkeypatch.setenv("OPENAI_API_KEY", "sk-test-dummy")
73
- captured: dict[str, object] = {}
74
-
75
- def fake_launch_tool(**kwargs): # noqa: ANN003
76
- captured.update(kwargs)
77
-
78
- with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
79
- with patch("headroom.cli.wrap._check_proxy", return_value=False):
80
- with patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool):
81
- result = runner.invoke(
82
- main,
83
- [
84
- "wrap",
85
- "copilot",
86
- "--no-rtk",
87
- "--backend",
88
- "anyllm",
89
- "--anyllm-provider",
90
- "groq",
91
- "--region",
92
- "us-central1",
93
- "--",
94
- "--model",
95
- "gpt-4o",
96
- ],
97
- )
98
-
99
- assert result.exit_code == 0, result.output
100
-
101
- env = captured["env"]
102
- assert isinstance(env, dict)
103
- assert env["COPILOT_PROVIDER_TYPE"] == "openai"
104
- assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"
105
- assert env["COPILOT_PROVIDER_WIRE_API"] == "completions"
106
- assert captured["backend"] == "anyllm"
107
- assert captured["anyllm_provider"] == "groq"
108
- assert captured["region"] == "us-central1"
109
- assert captured["args"] == ("--model", "gpt-4o")
110
-
111
-
112
- def test_wrap_copilot_auto_detects_running_proxy_backend(
113
- runner: CliRunner, monkeypatch: pytest.MonkeyPatch
114
- ) -> None:
115
- monkeypatch.setenv("OPENAI_API_KEY", "sk-test-dummy")
116
- captured: dict[str, object] = {}
117
-
118
- def fake_launch_tool(**kwargs): # noqa: ANN003
119
- captured.update(kwargs)
120
-
121
- with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
122
- with patch("headroom.cli.wrap._check_proxy", return_value=True):
123
- with patch("headroom.cli.wrap._detect_running_proxy_backend", return_value="anyllm"):
124
- with patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool):
125
- result = runner.invoke(
126
- main,
127
- ["wrap", "copilot", "--no-rtk", "--", "--model", "gpt-4o"],
128
- )
129
-
130
- assert result.exit_code == 0, result.output
131
- env = captured["env"]
132
- assert isinstance(env, dict)
133
- assert env["COPILOT_PROVIDER_TYPE"] == "openai"
134
- assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"
135
- assert env["COPILOT_PROVIDER_WIRE_API"] == "completions"
136
-
137
-
138
- def test_wrap_copilot_rejects_wire_api_for_anthropic_provider(runner: CliRunner) -> None:
139
- with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
140
- result = runner.invoke(
141
- main,
142
- [
143
- "wrap",
144
- "copilot",
145
- "--wire-api",
146
- "responses",
147
- "--",
148
- "--model",
149
- "claude-sonnet-4-20250514",
150
- ],
151
- )
152
-
153
- assert result.exit_code != 0
154
- assert "--wire-api is only valid" in result.output
155
-
156
-
157
- def test_wrap_copilot_rejects_responses_for_translated_backends(runner: CliRunner) -> None:
158
- with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
159
- with patch("headroom.cli.wrap._check_proxy", return_value=False):
160
- result = runner.invoke(
161
- main,
162
- [
163
- "wrap",
164
- "copilot",
165
- "--backend",
166
- "anyllm",
167
- "--wire-api",
168
- "responses",
169
- "--",
170
- "--model",
171
- "gpt-4o",
172
- ],
173
- )
174
-
175
- assert result.exit_code != 0
176
- assert "not supported with translated backends" in result.output
177
-
178
-
179
- def test_wrap_copilot_clears_stale_wire_api_in_anthropic_mode(
180
- runner: CliRunner, monkeypatch: pytest.MonkeyPatch
181
- ) -> None:
182
- monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test-dummy")
183
- captured: dict[str, object] = {}
184
-
185
- def fake_launch_tool(**kwargs): # noqa: ANN003
186
- captured.update(kwargs)
187
-
188
- with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
189
- with patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool):
190
- result = runner.invoke(
191
- main,
192
- ["wrap", "copilot", "--no-rtk", "--", "--model", "claude-sonnet-4-20250514"],
193
- env={
194
- "COPILOT_PROVIDER_WIRE_API": "responses",
195
- "ANTHROPIC_API_KEY": "sk-test-dummy",
196
- },
197
- )
198
-
199
- assert result.exit_code == 0, result.output
200
- env = captured["env"]
201
- assert isinstance(env, dict)
202
- assert env["COPILOT_PROVIDER_TYPE"] == "anthropic"
203
- assert "COPILOT_PROVIDER_WIRE_API" not in env
204
-
205
-
206
- def test_wrap_copilot_fails_when_binary_missing(runner: CliRunner) -> None:
207
- with patch("headroom.cli.wrap.shutil.which", return_value=None):
208
- result = runner.invoke(main, ["wrap", "copilot", "--", "--model", "gpt-4o"])
209
-
210
- assert result.exit_code == 1
211
- assert "'copilot' not found in PATH" in result.output
212
- assert "Install GitHub Copilot CLI" in result.output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for `headroom wrap copilot` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+ import types
8
+ from pathlib import Path
9
+ from unittest.mock import patch
10
+
11
+ import click
12
+ import pytest
13
+ from click.testing import CliRunner
14
+
15
+ from headroom.copilot_auth import DEFAULT_API_URL
16
+
17
+
18
+ @pytest.fixture
19
+ def runner() -> CliRunner:
20
+ return CliRunner()
21
+
22
+
23
+ @pytest.fixture
24
+ def wrap_modules(monkeypatch: pytest.MonkeyPatch) -> tuple[types.ModuleType, click.Group]:
25
+ headroom_pkg = sys.modules.get("headroom")
26
+ saved_headroom_cli_attr = (
27
+ headroom_pkg.cli if headroom_pkg is not None and hasattr(headroom_pkg, "cli") else None
28
+ )
29
+ saved_modules = {
30
+ name: sys.modules.get(name)
31
+ for name in ("headroom.cli", "headroom.cli.main", "headroom.cli.wrap")
32
+ }
33
+
34
+ fake_main_module = types.ModuleType("headroom.cli.main")
35
+ fake_main_module.main = click.Group()
36
+ sys.modules["headroom.cli.main"] = fake_main_module
37
+ sys.modules.pop("headroom.cli", None)
38
+ sys.modules.pop("headroom.cli.wrap", None)
39
+
40
+ wrap_cli = importlib.import_module("headroom.cli.wrap")
41
+ monkeypatch.setattr(wrap_cli, "_check_proxy", lambda _port: False)
42
+
43
+ try:
44
+ yield wrap_cli, fake_main_module.main
45
+ finally:
46
+ for name in ("headroom.cli.wrap", "headroom.cli.main", "headroom.cli"):
47
+ sys.modules.pop(name, None)
48
+ for name, module in saved_modules.items():
49
+ if module is not None:
50
+ sys.modules[name] = module
51
+ if saved_modules["headroom.cli"] is not None:
52
+ cli_pkg = saved_modules["headroom.cli"]
53
+ if saved_modules["headroom.cli.main"] is not None:
54
+ cli_pkg.main = saved_modules["headroom.cli.main"]
55
+ if saved_modules["headroom.cli.wrap"] is not None:
56
+ cli_pkg.wrap = saved_modules["headroom.cli.wrap"]
57
+ if headroom_pkg is not None:
58
+ if saved_headroom_cli_attr is None:
59
+ if hasattr(headroom_pkg, "cli"):
60
+ delattr(headroom_pkg, "cli")
61
+ else:
62
+ headroom_pkg.cli = saved_headroom_cli_attr
63
+
64
+
65
+ def test_wrap_copilot_auto_anthropic_injects_instructions(
66
+ runner: CliRunner,
67
+ wrap_modules: tuple[types.ModuleType, click.Group],
68
+ tmp_path: Path,
69
+ monkeypatch: pytest.MonkeyPatch,
70
+ ) -> None:
71
+ wrap_cli, main = wrap_modules
72
+ monkeypatch.chdir(tmp_path)
73
+ monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test-dummy")
74
+ captured: dict[str, object] = {}
75
+
76
+ def fake_launch_tool(**kwargs): # noqa: ANN003
77
+ captured.update(kwargs)
78
+
79
+ with (
80
+ patch("headroom.cli.wrap.shutil.which", return_value="copilot"),
81
+ patch("headroom.cli.wrap.has_oauth_auth", return_value=False),
82
+ patch("headroom.cli.wrap._ensure_rtk_binary", return_value=Path("/tmp/rtk")),
83
+ patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool),
84
+ ):
85
+ result = runner.invoke(
86
+ main,
87
+ ["wrap", "copilot", "--", "--model", "claude-sonnet-4-20250514"],
88
+ )
89
+
90
+ assert result.exit_code == 0, result.output
91
+ instructions = tmp_path / ".github" / "copilot-instructions.md"
92
+ assert instructions.exists()
93
+ content = instructions.read_text()
94
+ assert wrap_cli._RTK_MARKER in content
95
+ assert "RTK (Rust Token Killer)" in content
96
+
97
+ env = captured["env"]
98
+ assert isinstance(env, dict)
99
+ assert env["COPILOT_PROVIDER_TYPE"] == "anthropic"
100
+ assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787"
101
+ assert "COPILOT_PROVIDER_WIRE_API" not in env
102
+ assert captured["agent_type"] == "copilot"
103
+ assert captured["tool_label"] == "COPILOT"
104
+ assert captured["args"] == ("--model", "claude-sonnet-4-20250514")
105
+
106
+
107
+ def test_wrap_copilot_openai_backend_sets_completions_env(
108
+ runner: CliRunner,
109
+ wrap_modules: tuple[types.ModuleType, click.Group],
110
+ monkeypatch: pytest.MonkeyPatch,
111
+ ) -> None:
112
+ _wrap_cli, main = wrap_modules
113
+ monkeypatch.setenv("OPENAI_API_KEY", "sk-test-dummy")
114
+ captured: dict[str, object] = {}
115
+
116
+ def fake_launch_tool(**kwargs): # noqa: ANN003
117
+ captured.update(kwargs)
118
+
119
+ with (
120
+ patch("headroom.cli.wrap.shutil.which", return_value="copilot"),
121
+ patch("headroom.cli.wrap.has_oauth_auth", return_value=False),
122
+ patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool),
123
+ ):
124
+ result = runner.invoke(
125
+ main,
126
+ [
127
+ "wrap",
128
+ "copilot",
129
+ "--no-rtk",
130
+ "--backend",
131
+ "anyllm",
132
+ "--anyllm-provider",
133
+ "groq",
134
+ "--region",
135
+ "us-central1",
136
+ "--",
137
+ "--model",
138
+ "gpt-4o",
139
+ ],
140
+ )
141
+
142
+ assert result.exit_code == 0, result.output
143
+
144
+ env = captured["env"]
145
+ assert isinstance(env, dict)
146
+ assert env["COPILOT_PROVIDER_TYPE"] == "openai"
147
+ assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"
148
+ assert env["COPILOT_PROVIDER_WIRE_API"] == "completions"
149
+ assert captured["backend"] == "anyllm"
150
+ assert captured["anyllm_provider"] == "groq"
151
+ assert captured["region"] == "us-central1"
152
+ assert captured["args"] == ("--model", "gpt-4o")
153
+
154
+
155
+ def test_wrap_copilot_auto_detects_running_proxy_backend(
156
+ runner: CliRunner,
157
+ wrap_modules: tuple[types.ModuleType, click.Group],
158
+ monkeypatch: pytest.MonkeyPatch,
159
+ ) -> None:
160
+ _wrap_cli, main = wrap_modules
161
+ monkeypatch.setenv("OPENAI_API_KEY", "sk-test-dummy")
162
+ captured: dict[str, object] = {}
163
+
164
+ def fake_launch_tool(**kwargs): # noqa: ANN003
165
+ captured.update(kwargs)
166
+
167
+ with (
168
+ patch("headroom.cli.wrap.shutil.which", return_value="copilot"),
169
+ patch("headroom.cli.wrap.has_oauth_auth", return_value=False),
170
+ patch("headroom.cli.wrap._check_proxy", return_value=True),
171
+ patch("headroom.cli.wrap._detect_running_proxy_backend", return_value="anyllm"),
172
+ patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool),
173
+ ):
174
+ result = runner.invoke(
175
+ main,
176
+ ["wrap", "copilot", "--no-rtk", "--", "--model", "gpt-4o"],
177
+ )
178
+
179
+ assert result.exit_code == 0, result.output
180
+ env = captured["env"]
181
+ assert isinstance(env, dict)
182
+ assert env["COPILOT_PROVIDER_TYPE"] == "openai"
183
+ assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"
184
+ assert env["COPILOT_PROVIDER_WIRE_API"] == "completions"
185
+
186
+
187
+ def test_wrap_copilot_prefers_existing_oauth_session(
188
+ runner: CliRunner,
189
+ wrap_modules: tuple[types.ModuleType, click.Group],
190
+ monkeypatch: pytest.MonkeyPatch,
191
+ ) -> None:
192
+ _wrap_cli, main = wrap_modules
193
+ monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test-dummy")
194
+ captured: dict[str, object] = {}
195
+
196
+ def fake_launch_tool(**kwargs): # noqa: ANN003
197
+ captured.update(kwargs)
198
+
199
+ with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
200
+ with patch("headroom.cli.wrap.resolve_client_bearer_token", return_value="gho-existing"):
201
+ with patch("headroom.cli.wrap.has_oauth_auth", return_value=True):
202
+ with patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool):
203
+ result = runner.invoke(
204
+ main,
205
+ ["wrap", "copilot", "--no-rtk", "--", "--model", "claude-sonnet-4.6"],
206
+ )
207
+
208
+ assert result.exit_code == 0, result.output
209
+ env = captured["env"]
210
+ assert isinstance(env, dict)
211
+ assert env["COPILOT_PROVIDER_TYPE"] == "openai"
212
+ assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"
213
+ assert env["COPILOT_PROVIDER_WIRE_API"] == "completions"
214
+ assert env["COPILOT_PROVIDER_BEARER_TOKEN"] == "gho-existing"
215
+ assert "COPILOT_PROVIDER_API_KEY" not in env
216
+ assert captured["openai_api_url"] == DEFAULT_API_URL
217
+
218
+
219
+ def test_wrap_copilot_translated_backend_still_requires_byok(
220
+ runner: CliRunner,
221
+ wrap_modules: tuple[types.ModuleType, click.Group],
222
+ ) -> None:
223
+ _wrap_cli, main = wrap_modules
224
+ with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
225
+ with patch("headroom.cli.wrap.has_oauth_auth", return_value=True):
226
+ result = runner.invoke(
227
+ main,
228
+ [
229
+ "wrap",
230
+ "copilot",
231
+ "--no-rtk",
232
+ "--backend",
233
+ "anyllm",
234
+ "--",
235
+ "--model",
236
+ "gpt-4o",
237
+ ],
238
+ )
239
+
240
+ assert result.exit_code == 1
241
+ assert "Copilot BYOK mode requires a provider API key" in result.output
242
+
243
+
244
+ def test_wrap_copilot_rejects_wire_api_for_anthropic_provider(
245
+ runner: CliRunner,
246
+ wrap_modules: tuple[types.ModuleType, click.Group],
247
+ ) -> None:
248
+ _wrap_cli, main = wrap_modules
249
+ with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
250
+ result = runner.invoke(
251
+ main,
252
+ [
253
+ "wrap",
254
+ "copilot",
255
+ "--wire-api",
256
+ "responses",
257
+ "--",
258
+ "--model",
259
+ "claude-sonnet-4-20250514",
260
+ ],
261
+ )
262
+
263
+ assert result.exit_code != 0
264
+ assert "--wire-api is only valid" in result.output
265
+
266
+
267
+ def test_wrap_copilot_rejects_responses_for_translated_backends(
268
+ runner: CliRunner,
269
+ wrap_modules: tuple[types.ModuleType, click.Group],
270
+ ) -> None:
271
+ _wrap_cli, main = wrap_modules
272
+ with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
273
+ result = runner.invoke(
274
+ main,
275
+ [
276
+ "wrap",
277
+ "copilot",
278
+ "--backend",
279
+ "anyllm",
280
+ "--wire-api",
281
+ "responses",
282
+ "--",
283
+ "--model",
284
+ "gpt-4o",
285
+ ],
286
+ )
287
+
288
+ assert result.exit_code != 0
289
+ assert "not supported with translated backends" in result.output
290
+
291
+
292
+ def test_wrap_copilot_clears_stale_wire_api_in_anthropic_mode(
293
+ runner: CliRunner,
294
+ wrap_modules: tuple[types.ModuleType, click.Group],
295
+ monkeypatch: pytest.MonkeyPatch,
296
+ ) -> None:
297
+ _wrap_cli, main = wrap_modules
298
+ monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test-dummy")
299
+ captured: dict[str, object] = {}
300
+
301
+ def fake_launch_tool(**kwargs): # noqa: ANN003
302
+ captured.update(kwargs)
303
+
304
+ with (
305
+ patch("headroom.cli.wrap.shutil.which", return_value="copilot"),
306
+ patch("headroom.cli.wrap.has_oauth_auth", return_value=False),
307
+ patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool),
308
+ ):
309
+ result = runner.invoke(
310
+ main,
311
+ ["wrap", "copilot", "--no-rtk", "--", "--model", "claude-sonnet-4-20250514"],
312
+ env={
313
+ "COPILOT_PROVIDER_WIRE_API": "responses",
314
+ "ANTHROPIC_API_KEY": "sk-test-dummy",
315
+ },
316
+ )
317
+
318
+ assert result.exit_code == 0, result.output
319
+ env = captured["env"]
320
+ assert isinstance(env, dict)
321
+ assert env["COPILOT_PROVIDER_TYPE"] == "anthropic"
322
+ assert "COPILOT_PROVIDER_WIRE_API" not in env
323
+
324
+
325
+ def test_wrap_copilot_fails_when_binary_missing(
326
+ runner: CliRunner,
327
+ wrap_modules: tuple[types.ModuleType, click.Group],
328
+ ) -> None:
329
+ _wrap_cli, main = wrap_modules
330
+ with patch("headroom.cli.wrap.shutil.which", return_value=None):
331
+ result = runner.invoke(main, ["wrap", "copilot", "--", "--model", "gpt-4o"])
332
+
333
+ assert result.exit_code == 1
334
+ assert "'copilot' not found in PATH" in result.output
335
+ assert "Install GitHub Copilot CLI" in result.output
tests/test_cli/test_wrap_persistent.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
 
3
  import click
4
 
5
- from headroom.cli.wrap import _ensure_proxy, _find_persistent_manifest, _recover_persistent_proxy
6
 
7
 
8
  class _Manifest:
@@ -15,8 +15,8 @@ class _Manifest:
15
  def test_ensure_proxy_recovers_matching_persistent_deployment(monkeypatch) -> None:
16
  calls: list[str] = []
17
 
18
- monkeypatch.setattr("headroom.cli.wrap._check_proxy", lambda port: False)
19
- monkeypatch.setattr("headroom.cli.wrap._find_persistent_manifest", lambda port: _Manifest())
20
  monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
21
  monkeypatch.setattr(
22
  "headroom.install.supervisors.start_supervisor",
@@ -26,13 +26,14 @@ def test_ensure_proxy_recovers_matching_persistent_deployment(monkeypatch) -> No
26
  "headroom.install.runtime.wait_ready", lambda manifest, timeout_seconds=45: True
27
  )
28
  monkeypatch.setattr(
29
- "headroom.cli.wrap._start_proxy",
 
30
  lambda *args, **kwargs: (_ for _ in ()).throw(
31
  AssertionError("ephemeral proxy should not start")
32
  ),
33
  )
34
 
35
- result = _ensure_proxy(8787, False)
36
 
37
  assert result is None
38
  assert calls == ["start:default"]
@@ -41,8 +42,8 @@ def test_ensure_proxy_recovers_matching_persistent_deployment(monkeypatch) -> No
41
  def test_ensure_proxy_recovers_persistent_deployment_when_socket_is_bound(monkeypatch) -> None:
42
  calls: list[str] = []
43
 
44
- monkeypatch.setattr("headroom.cli.wrap._check_proxy", lambda port: True)
45
- monkeypatch.setattr("headroom.cli.wrap._find_persistent_manifest", lambda port: _Manifest())
46
  monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
47
  monkeypatch.setattr(
48
  "headroom.install.supervisors.start_supervisor",
@@ -52,26 +53,41 @@ def test_ensure_proxy_recovers_persistent_deployment_when_socket_is_bound(monkey
52
  "headroom.install.runtime.wait_ready", lambda manifest, timeout_seconds=45: True
53
  )
54
 
55
- result = _ensure_proxy(8787, False)
56
 
57
  assert result is None
58
  assert calls == ["start:default"]
59
 
60
 
61
  def test_ensure_proxy_rejects_unhealthy_persistent_deployment(monkeypatch) -> None:
62
- monkeypatch.setattr("headroom.cli.wrap._check_proxy", lambda port: True)
63
- monkeypatch.setattr("headroom.cli.wrap._find_persistent_manifest", lambda port: _Manifest())
64
  monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
65
- monkeypatch.setattr("headroom.cli.wrap._recover_persistent_proxy", lambda port: False)
66
 
67
  try:
68
- _ensure_proxy(8787, False)
69
  except click.ClickException as exc:
70
  assert "is not healthy" in str(exc)
71
  else:
72
  raise AssertionError("expected unhealthy persistent deployment to raise")
73
 
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  def test_find_persistent_manifest_prefers_default_profile(monkeypatch) -> None:
76
  class DefaultManifest:
77
  profile = "default"
@@ -86,23 +102,23 @@ def test_find_persistent_manifest_prefers_default_profile(monkeypatch) -> None:
86
  lambda: [OtherManifest(), DefaultManifest()],
87
  )
88
 
89
- manifest = _find_persistent_manifest(8787)
90
 
91
  assert manifest.profile == "default"
92
 
93
 
94
  def test_recover_persistent_proxy_reuses_healthy_deployment(monkeypatch) -> None:
95
- monkeypatch.setattr("headroom.cli.wrap._find_persistent_manifest", lambda port: _Manifest())
96
  monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: True)
97
 
98
- assert _recover_persistent_proxy(8787) is True
99
 
100
 
101
  def test_recover_persistent_proxy_warns_for_task_deployment(monkeypatch) -> None:
102
  class TaskManifest(_Manifest):
103
  supervisor_kind = "task"
104
 
105
- monkeypatch.setattr("headroom.cli.wrap._find_persistent_manifest", lambda port: TaskManifest())
106
  monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
107
 
108
- assert _recover_persistent_proxy(8787) is False
 
2
 
3
  import click
4
 
5
+ import headroom.cli.wrap as wrap_cli
6
 
7
 
8
  class _Manifest:
 
15
  def test_ensure_proxy_recovers_matching_persistent_deployment(monkeypatch) -> None:
16
  calls: list[str] = []
17
 
18
+ monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: False)
19
+ monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
20
  monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
21
  monkeypatch.setattr(
22
  "headroom.install.supervisors.start_supervisor",
 
26
  "headroom.install.runtime.wait_ready", lambda manifest, timeout_seconds=45: True
27
  )
28
  monkeypatch.setattr(
29
+ wrap_cli,
30
+ "_start_proxy",
31
  lambda *args, **kwargs: (_ for _ in ()).throw(
32
  AssertionError("ephemeral proxy should not start")
33
  ),
34
  )
35
 
36
+ result = wrap_cli._ensure_proxy(8787, False)
37
 
38
  assert result is None
39
  assert calls == ["start:default"]
 
42
  def test_ensure_proxy_recovers_persistent_deployment_when_socket_is_bound(monkeypatch) -> None:
43
  calls: list[str] = []
44
 
45
+ monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
46
+ monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
47
  monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
48
  monkeypatch.setattr(
49
  "headroom.install.supervisors.start_supervisor",
 
53
  "headroom.install.runtime.wait_ready", lambda manifest, timeout_seconds=45: True
54
  )
55
 
56
+ result = wrap_cli._ensure_proxy(8787, False)
57
 
58
  assert result is None
59
  assert calls == ["start:default"]
60
 
61
 
62
  def test_ensure_proxy_rejects_unhealthy_persistent_deployment(monkeypatch) -> None:
63
+ monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
64
+ monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
65
  monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
66
+ monkeypatch.setattr(wrap_cli, "_recover_persistent_proxy", lambda port: False)
67
 
68
  try:
69
+ wrap_cli._ensure_proxy(8787, False)
70
  except click.ClickException as exc:
71
  assert "is not healthy" in str(exc)
72
  else:
73
  raise AssertionError("expected unhealthy persistent deployment to raise")
74
 
75
 
76
+ def test_ensure_proxy_falls_back_when_persistent_manifest_is_stale(monkeypatch) -> None:
77
+ calls: list[str] = []
78
+
79
+ monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: False)
80
+ monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
81
+ monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
82
+ monkeypatch.setattr(wrap_cli, "_recover_persistent_proxy", lambda port: False)
83
+ monkeypatch.setattr(wrap_cli, "_start_proxy", lambda *args, **kwargs: calls.append("start"))
84
+
85
+ result = wrap_cli._ensure_proxy(8787, False)
86
+
87
+ assert result is None
88
+ assert calls == ["start"]
89
+
90
+
91
  def test_find_persistent_manifest_prefers_default_profile(monkeypatch) -> None:
92
  class DefaultManifest:
93
  profile = "default"
 
102
  lambda: [OtherManifest(), DefaultManifest()],
103
  )
104
 
105
+ manifest = wrap_cli._find_persistent_manifest(8787)
106
 
107
  assert manifest.profile == "default"
108
 
109
 
110
  def test_recover_persistent_proxy_reuses_healthy_deployment(monkeypatch) -> None:
111
+ monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
112
  monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: True)
113
 
114
+ assert wrap_cli._recover_persistent_proxy(8787) is True
115
 
116
 
117
  def test_recover_persistent_proxy_warns_for_task_deployment(monkeypatch) -> None:
118
  class TaskManifest(_Manifest):
119
  supervisor_kind = "task"
120
 
121
+ monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: TaskManifest())
122
  monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
123
 
124
+ assert wrap_cli._recover_persistent_proxy(8787) is False
tests/test_copilot_auth.py CHANGED
@@ -1,8 +1,11 @@
1
  from __future__ import annotations
2
 
 
3
  import json
4
  import time
5
  from pathlib import Path
 
 
6
 
7
  import pytest
8
 
@@ -14,6 +17,68 @@ def test_read_cached_oauth_token_prefers_env(monkeypatch: pytest.MonkeyPatch) ->
14
  assert copilot_auth.read_cached_oauth_token() == "gho-env"
15
 
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  def test_read_cached_oauth_token_reads_hosts_file(
18
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
19
  ) -> None:
@@ -31,6 +96,8 @@ def test_read_cached_oauth_token_reads_hosts_file(
31
  )
32
  monkeypatch.delenv("GITHUB_COPILOT_TOKEN", raising=False)
33
  monkeypatch.setenv("GITHUB_COPILOT_TOKEN_FILE", str(hosts))
 
 
34
 
35
  assert copilot_auth.read_cached_oauth_token() == "gho-file"
36
 
@@ -44,10 +111,68 @@ def test_read_cached_oauth_token_skips_expired_entries(
44
  encoding="utf-8",
45
  )
46
  monkeypatch.setenv("GITHUB_COPILOT_TOKEN_FILE", str(hosts))
 
 
47
 
48
  assert copilot_auth.read_cached_oauth_token() is None
49
 
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  def test_resolve_client_bearer_token_prefers_api_token(monkeypatch: pytest.MonkeyPatch) -> None:
52
  monkeypatch.setenv("GITHUB_COPILOT_API_TOKEN", "copilot-api")
53
  monkeypatch.setenv("GITHUB_COPILOT_TOKEN", "gho-oauth")
@@ -55,16 +180,36 @@ def test_resolve_client_bearer_token_prefers_api_token(monkeypatch: pytest.Monke
55
  assert copilot_auth.resolve_client_bearer_token() == "copilot-api"
56
 
57
 
 
 
 
 
 
 
58
  def test_is_copilot_api_url_matches_expected_hosts() -> None:
59
  assert copilot_auth.is_copilot_api_url("https://api.githubcopilot.com/v1/chat/completions")
60
  assert copilot_auth.is_copilot_api_url("wss://api.githubcopilot.com/v1/responses")
61
  assert not copilot_auth.is_copilot_api_url("https://api.openai.com/v1/chat/completions")
62
 
63
 
64
- @pytest.mark.asyncio
65
- async def test_apply_copilot_api_auth_replaces_authorization(
66
- monkeypatch: pytest.MonkeyPatch,
67
- ) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  async def fake_get_api_token() -> copilot_auth.CopilotAPIToken:
69
  return copilot_auth.CopilotAPIToken(
70
  token="copilot-session",
@@ -78,18 +223,48 @@ async def test_apply_copilot_api_auth_replaces_authorization(
78
  fake_get_api_token,
79
  )
80
 
81
- headers = await copilot_auth.apply_copilot_api_auth(
82
- {"authorization": "Bearer downstream-token"},
83
- url="https://api.githubcopilot.com/v1/chat/completions",
 
 
84
  )
85
 
86
  assert headers["Authorization"] == "Bearer copilot-session"
87
  assert "authorization" not in headers
88
 
89
 
90
- @pytest.mark.asyncio
91
- async def test_token_provider_exchanges_and_caches(monkeypatch: pytest.MonkeyPatch) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  monkeypatch.setenv("GITHUB_COPILOT_TOKEN", "gho-oauth")
 
93
 
94
  provider = copilot_auth.CopilotTokenProvider()
95
  calls = {"count": 0}
@@ -106,9 +281,84 @@ async def test_token_provider_exchanges_and_caches(monkeypatch: pytest.MonkeyPat
106
 
107
  monkeypatch.setattr(provider, "_exchange_token_sync", staticmethod(fake_exchange))
108
 
109
- first = await provider.get_api_token()
110
- second = await provider.get_api_token()
111
 
112
  assert first.token == "copilot-api"
113
  assert second.token == "copilot-api"
114
  assert calls["count"] == 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ import asyncio
4
  import json
5
  import time
6
  from pathlib import Path
7
+ from types import SimpleNamespace
8
+ from urllib import error as urllib_error
9
 
10
  import pytest
11
 
 
17
  assert copilot_auth.read_cached_oauth_token() == "gho-env"
18
 
19
 
20
+ def test_should_exchange_oauth_token_supports_truthy_values(
21
+ monkeypatch: pytest.MonkeyPatch,
22
+ ) -> None:
23
+ for raw in ("1", "true", "YES", "On"):
24
+ monkeypatch.setenv("GITHUB_COPILOT_USE_TOKEN_EXCHANGE", raw)
25
+ assert copilot_auth._should_exchange_oauth_token() is True
26
+
27
+ monkeypatch.setenv("GITHUB_COPILOT_USE_TOKEN_EXCHANGE", "off")
28
+ assert copilot_auth._should_exchange_oauth_token() is False
29
+
30
+
31
+ def test_resolve_token_file_paths_prefers_override(monkeypatch: pytest.MonkeyPatch) -> None:
32
+ monkeypatch.setenv("GITHUB_COPILOT_TOKEN_FILE", "~/custom-token.json")
33
+
34
+ paths = copilot_auth._resolve_token_file_paths()
35
+
36
+ assert paths == [Path("~/custom-token.json").expanduser()]
37
+
38
+
39
+ def test_resolve_token_file_paths_includes_localappdata_and_config(
40
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
41
+ ) -> None:
42
+ monkeypatch.delenv("GITHUB_COPILOT_TOKEN_FILE", raising=False)
43
+ monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "local"))
44
+ monkeypatch.setattr(copilot_auth.Path, "home", staticmethod(lambda: tmp_path / "home"))
45
+
46
+ paths = copilot_auth._resolve_token_file_paths()
47
+
48
+ assert paths == [
49
+ tmp_path / "local" / "github-copilot" / "apps.json",
50
+ tmp_path / "local" / "github-copilot" / "hosts.json",
51
+ tmp_path / "home" / ".config" / "github-copilot" / "apps.json",
52
+ tmp_path / "home" / ".config" / "github-copilot" / "hosts.json",
53
+ ]
54
+
55
+
56
+ def test_read_cached_oauth_token_falls_back_to_gh_cli(monkeypatch: pytest.MonkeyPatch) -> None:
57
+ monkeypatch.delenv("GITHUB_COPILOT_GITHUB_TOKEN", raising=False)
58
+ monkeypatch.delenv("GITHUB_COPILOT_TOKEN", raising=False)
59
+ monkeypatch.delenv("GITHUB_TOKEN", raising=False)
60
+ monkeypatch.delenv("COPILOT_GITHUB_TOKEN", raising=False)
61
+ monkeypatch.setattr(copilot_auth, "_read_windows_copilot_cli_oauth_token", lambda: None)
62
+ monkeypatch.setattr(copilot_auth, "_read_gh_cli_oauth_token", lambda: "gho-gh-cli")
63
+
64
+ assert copilot_auth.read_cached_oauth_token() == "gho-gh-cli"
65
+
66
+
67
+ def test_read_cached_oauth_token_prefers_copilot_cli_windows_token(
68
+ monkeypatch: pytest.MonkeyPatch,
69
+ ) -> None:
70
+ monkeypatch.delenv("GITHUB_COPILOT_GITHUB_TOKEN", raising=False)
71
+ monkeypatch.delenv("GITHUB_COPILOT_TOKEN", raising=False)
72
+ monkeypatch.delenv("GITHUB_TOKEN", raising=False)
73
+ monkeypatch.delenv("COPILOT_GITHUB_TOKEN", raising=False)
74
+ monkeypatch.setattr(
75
+ copilot_auth, "_read_windows_copilot_cli_oauth_token", lambda: "gho-copilot"
76
+ )
77
+ monkeypatch.setattr(copilot_auth, "_read_gh_cli_oauth_token", lambda: "gho-gh-cli")
78
+
79
+ assert copilot_auth.read_cached_oauth_token() == "gho-copilot"
80
+
81
+
82
  def test_read_cached_oauth_token_reads_hosts_file(
83
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
84
  ) -> None:
 
96
  )
97
  monkeypatch.delenv("GITHUB_COPILOT_TOKEN", raising=False)
98
  monkeypatch.setenv("GITHUB_COPILOT_TOKEN_FILE", str(hosts))
99
+ monkeypatch.setattr(copilot_auth, "_read_windows_copilot_cli_oauth_token", lambda: None)
100
+ monkeypatch.setattr(copilot_auth, "_read_gh_cli_oauth_token", lambda: None)
101
 
102
  assert copilot_auth.read_cached_oauth_token() == "gho-file"
103
 
 
111
  encoding="utf-8",
112
  )
113
  monkeypatch.setenv("GITHUB_COPILOT_TOKEN_FILE", str(hosts))
114
+ monkeypatch.setattr(copilot_auth, "_read_windows_copilot_cli_oauth_token", lambda: None)
115
+ monkeypatch.setattr(copilot_auth, "_read_gh_cli_oauth_token", lambda: None)
116
 
117
  assert copilot_auth.read_cached_oauth_token() is None
118
 
119
 
120
+ def test_read_gh_cli_oauth_token_uses_hostname(monkeypatch: pytest.MonkeyPatch) -> None:
121
+ calls: list[list[str]] = []
122
+
123
+ class CompletedProcess:
124
+ def __init__(self) -> None:
125
+ self.returncode = 0
126
+ self.stdout = "gho-gh-cli\n"
127
+
128
+ def fake_run(*args: object, **kwargs: object) -> CompletedProcess:
129
+ calls.append(list(args[0]))
130
+ assert kwargs["capture_output"] is True
131
+ assert kwargs["check"] is False
132
+ return CompletedProcess()
133
+
134
+ monkeypatch.setenv("GITHUB_COPILOT_HOST", "example.ghe.com")
135
+ monkeypatch.setattr(copilot_auth.subprocess, "run", fake_run)
136
+
137
+ assert copilot_auth._read_gh_cli_oauth_token() == "gho-gh-cli"
138
+ assert calls == [["gh", "auth", "token", "--hostname", "example.ghe.com"]]
139
+
140
+
141
+ def test_read_gh_cli_oauth_token_returns_none_when_invocation_fails(
142
+ monkeypatch: pytest.MonkeyPatch,
143
+ ) -> None:
144
+ def fake_run(*args: object, **kwargs: object) -> None: # noqa: ANN002, ANN003
145
+ raise OSError("gh missing")
146
+
147
+ monkeypatch.setattr(copilot_auth.subprocess, "run", fake_run)
148
+
149
+ assert copilot_auth._read_gh_cli_oauth_token() is None
150
+
151
+
152
+ def test_read_gh_cli_oauth_token_returns_none_for_nonzero_exit(
153
+ monkeypatch: pytest.MonkeyPatch,
154
+ ) -> None:
155
+ monkeypatch.setattr(
156
+ copilot_auth.subprocess,
157
+ "run",
158
+ lambda *args, **kwargs: SimpleNamespace(returncode=1, stdout="ignored"),
159
+ )
160
+
161
+ assert copilot_auth._read_gh_cli_oauth_token() is None
162
+
163
+
164
+ def test_read_gh_cli_oauth_token_returns_none_for_blank_stdout(
165
+ monkeypatch: pytest.MonkeyPatch,
166
+ ) -> None:
167
+ monkeypatch.setattr(
168
+ copilot_auth.subprocess,
169
+ "run",
170
+ lambda *args, **kwargs: SimpleNamespace(returncode=0, stdout=" \n"),
171
+ )
172
+
173
+ assert copilot_auth._read_gh_cli_oauth_token() is None
174
+
175
+
176
  def test_resolve_client_bearer_token_prefers_api_token(monkeypatch: pytest.MonkeyPatch) -> None:
177
  monkeypatch.setenv("GITHUB_COPILOT_API_TOKEN", "copilot-api")
178
  monkeypatch.setenv("GITHUB_COPILOT_TOKEN", "gho-oauth")
 
180
  assert copilot_auth.resolve_client_bearer_token() == "copilot-api"
181
 
182
 
183
+ def test_has_oauth_auth_false_when_no_tokens(monkeypatch: pytest.MonkeyPatch) -> None:
184
+ monkeypatch.setattr(copilot_auth, "resolve_client_bearer_token", lambda: None)
185
+
186
+ assert copilot_auth.has_oauth_auth() is False
187
+
188
+
189
  def test_is_copilot_api_url_matches_expected_hosts() -> None:
190
  assert copilot_auth.is_copilot_api_url("https://api.githubcopilot.com/v1/chat/completions")
191
  assert copilot_auth.is_copilot_api_url("wss://api.githubcopilot.com/v1/responses")
192
  assert not copilot_auth.is_copilot_api_url("https://api.openai.com/v1/chat/completions")
193
 
194
 
195
+ def test_build_copilot_upstream_url_strips_v1_only_for_copilot_hosts() -> None:
196
+ assert (
197
+ copilot_auth.build_copilot_upstream_url(
198
+ "https://api.githubcopilot.com",
199
+ "/v1/chat/completions",
200
+ )
201
+ == "https://api.githubcopilot.com/chat/completions"
202
+ )
203
+ assert (
204
+ copilot_auth.build_copilot_upstream_url(
205
+ "https://api.openai.com",
206
+ "/v1/chat/completions",
207
+ )
208
+ == "https://api.openai.com/v1/chat/completions"
209
+ )
210
+
211
+
212
+ def test_apply_copilot_api_auth_replaces_authorization(monkeypatch: pytest.MonkeyPatch) -> None:
213
  async def fake_get_api_token() -> copilot_auth.CopilotAPIToken:
214
  return copilot_auth.CopilotAPIToken(
215
  token="copilot-session",
 
223
  fake_get_api_token,
224
  )
225
 
226
+ headers = asyncio.run(
227
+ copilot_auth.apply_copilot_api_auth(
228
+ {"authorization": "Bearer downstream-token"},
229
+ url="https://api.githubcopilot.com/v1/chat/completions",
230
+ )
231
  )
232
 
233
  assert headers["Authorization"] == "Bearer copilot-session"
234
  assert "authorization" not in headers
235
 
236
 
237
+ def test_token_provider_reuses_oauth_token_without_exchange(
238
+ monkeypatch: pytest.MonkeyPatch,
239
+ ) -> None:
240
+ monkeypatch.setenv("GITHUB_COPILOT_TOKEN", "gho-oauth")
241
+
242
+ provider = copilot_auth.CopilotTokenProvider()
243
+ calls = {"count": 0}
244
+
245
+ def fake_exchange(headers: dict[str, str]) -> dict[str, object]:
246
+ calls["count"] += 1
247
+ return {
248
+ "token": "copilot-api",
249
+ "expires_at": int(time.time()) + 3600,
250
+ "refresh_in": 1200,
251
+ "endpoints": {"api": "https://api.githubcopilot.com"},
252
+ "sku": "copilot_individual",
253
+ }
254
+
255
+ monkeypatch.setattr(provider, "_exchange_token_sync", staticmethod(fake_exchange))
256
+
257
+ first = asyncio.run(provider.get_api_token())
258
+ second = asyncio.run(provider.get_api_token())
259
+
260
+ assert first.token == "gho-oauth"
261
+ assert second.token == "gho-oauth"
262
+ assert calls["count"] == 0
263
+
264
+
265
+ def test_token_provider_can_exchange_when_enabled(monkeypatch: pytest.MonkeyPatch) -> None:
266
  monkeypatch.setenv("GITHUB_COPILOT_TOKEN", "gho-oauth")
267
+ monkeypatch.setenv("GITHUB_COPILOT_USE_TOKEN_EXCHANGE", "true")
268
 
269
  provider = copilot_auth.CopilotTokenProvider()
270
  calls = {"count": 0}
 
281
 
282
  monkeypatch.setattr(provider, "_exchange_token_sync", staticmethod(fake_exchange))
283
 
284
+ first = asyncio.run(provider.get_api_token())
285
+ second = asyncio.run(provider.get_api_token())
286
 
287
  assert first.token == "copilot-api"
288
  assert second.token == "copilot-api"
289
  assert calls["count"] == 1
290
+
291
+
292
+ def test_token_provider_prefers_explicit_api_token(monkeypatch: pytest.MonkeyPatch) -> None:
293
+ monkeypatch.setenv("GITHUB_COPILOT_API_TOKEN", "copilot-api")
294
+ monkeypatch.setenv("GITHUB_COPILOT_API_URL", "https://api.githubcopilot.com")
295
+
296
+ token = asyncio.run(copilot_auth.CopilotTokenProvider().get_api_token())
297
+
298
+ assert token.token == "copilot-api"
299
+ assert token.api_url == "https://api.githubcopilot.com"
300
+
301
+
302
+ def test_token_provider_raises_without_oauth_token(monkeypatch: pytest.MonkeyPatch) -> None:
303
+ monkeypatch.delenv("GITHUB_COPILOT_API_TOKEN", raising=False)
304
+ monkeypatch.setattr(copilot_auth, "read_cached_oauth_token", lambda: None)
305
+
306
+ with pytest.raises(RuntimeError, match="No GitHub Copilot OAuth token"):
307
+ asyncio.run(copilot_auth.CopilotTokenProvider().get_api_token())
308
+
309
+
310
+ def test_exchange_token_raises_when_exchange_returns_empty_token(
311
+ monkeypatch: pytest.MonkeyPatch,
312
+ ) -> None:
313
+ provider = copilot_auth.CopilotTokenProvider()
314
+ monkeypatch.setattr(
315
+ provider,
316
+ "_exchange_token_sync",
317
+ staticmethod(lambda headers: {"token": "", "expires_at": int(time.time()) + 1}),
318
+ )
319
+
320
+ with pytest.raises(RuntimeError, match="empty token"):
321
+ asyncio.run(provider._exchange_token("gho-oauth"))
322
+
323
+
324
+ def test_exchange_token_sync_raises_for_http_error(monkeypatch: pytest.MonkeyPatch) -> None:
325
+ class DummyResponse:
326
+ def read(self) -> bytes:
327
+ return b'{"message":"Not Found"}'
328
+
329
+ def close(self) -> None:
330
+ return None
331
+
332
+ def fake_urlopen(request, timeout: float): # noqa: ANN001, ANN202
333
+ raise urllib_error.HTTPError(
334
+ url=request.full_url,
335
+ code=404,
336
+ msg="Not Found",
337
+ hdrs=None,
338
+ fp=DummyResponse(),
339
+ )
340
+
341
+ monkeypatch.setattr(copilot_auth.urllib_request, "urlopen", fake_urlopen)
342
+
343
+ with pytest.raises(RuntimeError, match="HTTP 404"):
344
+ copilot_auth.CopilotTokenProvider._exchange_token_sync({"Authorization": "token test"})
345
+
346
+
347
+ def test_apply_copilot_api_auth_returns_original_headers_for_non_copilot_url() -> None:
348
+ headers = asyncio.run(
349
+ copilot_auth.apply_copilot_api_auth(
350
+ {"authorization": "Bearer downstream-token"},
351
+ url="https://api.openai.com/v1/chat/completions",
352
+ )
353
+ )
354
+
355
+ assert headers == {"authorization": "Bearer downstream-token"}
356
+
357
+
358
+ def test_read_windows_copilot_cli_oauth_token_returns_none_without_windll(
359
+ monkeypatch: pytest.MonkeyPatch,
360
+ ) -> None:
361
+ monkeypatch.setattr(copilot_auth.os, "name", "nt")
362
+ monkeypatch.delattr(copilot_auth.ctypes, "WinDLL", raising=False)
363
+
364
+ assert copilot_auth._read_windows_copilot_cli_oauth_token() is None
tests/test_learn/test_writer.py CHANGED
@@ -3,7 +3,12 @@
3
  from pathlib import Path
4
 
5
  from headroom.learn.models import ProjectInfo, Recommendation, RecommendationTarget
6
- from headroom.learn.writer import _MARKER_END, _MARKER_START, ClaudeCodeWriter
 
 
 
 
 
7
 
8
 
9
  def _project(tmp_path: Path) -> ProjectInfo:
@@ -80,26 +85,114 @@ class TestClaudeCodeWriter:
80
  assert "Existing instructions here" in content
81
  assert "Use uv" in content
82
 
83
- def test_replaces_existing_headroom_section(self, tmp_path):
 
84
  proj = _project(tmp_path)
85
  claude_md = proj.project_path / "CLAUDE.md"
86
- old_section = (
87
- f"# My Project\n\n{_MARKER_START}\n## Old Patterns\nold stuff\n{_MARKER_END}\n"
 
 
 
 
 
 
 
 
88
  )
89
- claude_md.write_text(old_section)
90
 
91
  writer = ClaudeCodeWriter()
92
- recs = [_rec(RecommendationTarget.CONTEXT_FILE, "Environment", "- New stuff")]
93
  writer.write(recs, proj, dry_run=False)
94
 
95
  content = claude_md.read_text()
96
- assert "old stuff" not in content
97
- assert "New stuff" in content
98
  assert "My Project" in content
99
- # Should have exactly one marker pair
 
 
 
 
 
 
 
 
 
100
  assert content.count(_MARKER_START) == 1
101
  assert content.count(_MARKER_END) == 1
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  def test_appends_to_existing_memory_md(self, tmp_path):
104
  proj = _project(tmp_path)
105
  memory_md = proj.data_path / "memory" / "MEMORY.md"
@@ -113,3 +206,36 @@ class TestClaudeCodeWriter:
113
  assert "Existing Memory" in content
114
  assert "Some facts" in content
115
  assert "New pattern" in content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from pathlib import Path
4
 
5
  from headroom.learn.models import ProjectInfo, Recommendation, RecommendationTarget
6
+ from headroom.learn.writer import (
7
+ _MARKER_END,
8
+ _MARKER_START,
9
+ ClaudeCodeWriter,
10
+ _parse_prior_recommendations,
11
+ )
12
 
13
 
14
  def _project(tmp_path: Path) -> ProjectInfo:
 
85
  assert "Existing instructions here" in content
86
  assert "Use uv" in content
87
 
88
+ def test_carries_forward_prior_sections_not_resurfaced(self, tmp_path):
89
+ """Re-running learn must not drop prior sections that the new run didn't re-surface."""
90
  proj = _project(tmp_path)
91
  claude_md = proj.project_path / "CLAUDE.md"
92
+ prior_block = (
93
+ f"# My Project\n\n{_MARKER_START}\n"
94
+ "## Headroom Learned Patterns\n"
95
+ "*Auto-generated by `headroom learn` on 2026-01-01 — do not edit manually*\n\n"
96
+ "### Large Files\n"
97
+ "*~15,000 tokens/session saved*\n"
98
+ "- src/App.tsx is huge\n\n"
99
+ "### Build Commands\n"
100
+ "- cargo check from src-tauri/\n\n"
101
+ f"{_MARKER_END}\n"
102
  )
103
+ claude_md.write_text(prior_block)
104
 
105
  writer = ClaudeCodeWriter()
106
+ recs = [_rec(RecommendationTarget.CONTEXT_FILE, "Environment", "- Use uv")]
107
  writer.write(recs, proj, dry_run=False)
108
 
109
  content = claude_md.read_text()
 
 
110
  assert "My Project" in content
111
+ # New section present
112
+ assert "Use uv" in content
113
+ # Prior sections preserved (neither heading re-surfaced by the new run)
114
+ assert "### Large Files" in content
115
+ assert "src/App.tsx is huge" in content
116
+ assert "### Build Commands" in content
117
+ assert "cargo check from src-tauri/" in content
118
+ # Tokens annotation round-tripped
119
+ assert "*~15,000 tokens/session saved*" in content
120
+ # Still exactly one marker pair
121
  assert content.count(_MARKER_START) == 1
122
  assert content.count(_MARKER_END) == 1
123
 
124
+ def test_new_run_overrides_same_named_prior_section(self, tmp_path):
125
+ """When a section appears in both prior and new, the new run wins."""
126
+ proj = _project(tmp_path)
127
+ claude_md = proj.project_path / "CLAUDE.md"
128
+ prior_block = (
129
+ f"{_MARKER_START}\n"
130
+ "## Headroom Learned Patterns\n"
131
+ "*Auto-generated by `headroom learn` on 2026-01-01 — do not edit manually*\n\n"
132
+ "### Environment\n"
133
+ "- old stale environment note\n\n"
134
+ f"{_MARKER_END}\n"
135
+ )
136
+ claude_md.write_text(prior_block)
137
+
138
+ writer = ClaudeCodeWriter()
139
+ recs = [_rec(RecommendationTarget.CONTEXT_FILE, "Environment", "- fresh environment note")]
140
+ writer.write(recs, proj, dry_run=False)
141
+
142
+ content = claude_md.read_text()
143
+ assert "fresh environment note" in content
144
+ assert "old stale environment note" not in content
145
+ # Only one Environment section in the final block
146
+ assert content.count("### Environment") == 1
147
+
148
+ def test_memory_md_carry_forward(self, tmp_path):
149
+ """Carry-forward also works for MEMORY.md."""
150
+ proj = _project(tmp_path)
151
+ memory_md = proj.data_path / "memory" / "MEMORY.md"
152
+ memory_md.write_text(
153
+ f"{_MARKER_START}\n"
154
+ "## Headroom Learned Patterns\n"
155
+ "*Auto-generated by `headroom learn` on 2026-01-01 — do not edit manually*\n\n"
156
+ "### User Workflow Preferences\n"
157
+ "- User rejects sleep-based polling\n\n"
158
+ f"{_MARKER_END}\n"
159
+ )
160
+
161
+ writer = ClaudeCodeWriter()
162
+ recs = [
163
+ _rec(RecommendationTarget.MEMORY_FILE, "Related Codebases", "- web app at ~/Code/web")
164
+ ]
165
+ writer.write(recs, proj, dry_run=False)
166
+
167
+ content = memory_md.read_text()
168
+ assert "User rejects sleep-based polling" in content
169
+ assert "web app at ~/Code/web" in content
170
+
171
+ def test_section_without_tokens_annotation_round_trips(self, tmp_path):
172
+ """Prior sections emitted without a tokens annotation must still carry forward cleanly."""
173
+ proj = _project(tmp_path)
174
+ claude_md = proj.project_path / "CLAUDE.md"
175
+ claude_md.write_text(
176
+ f"{_MARKER_START}\n"
177
+ "## Headroom Learned Patterns\n"
178
+ "*Auto-generated by `headroom learn` on 2026-01-01 — do not edit manually*\n\n"
179
+ "### Misc\n"
180
+ "- one-liner pattern\n\n"
181
+ f"{_MARKER_END}\n"
182
+ )
183
+
184
+ writer = ClaudeCodeWriter()
185
+ recs = [_rec(RecommendationTarget.CONTEXT_FILE, "Other", "- new one")]
186
+ writer.write(recs, proj, dry_run=False)
187
+
188
+ content = claude_md.read_text()
189
+ assert "### Misc" in content
190
+ assert "one-liner pattern" in content
191
+ # No spurious tokens annotation injected for a prior that didn't have one
192
+ misc_idx = content.index("### Misc")
193
+ after_misc = content[misc_idx : misc_idx + 200]
194
+ assert "tokens/session saved" not in after_misc
195
+
196
  def test_appends_to_existing_memory_md(self, tmp_path):
197
  proj = _project(tmp_path)
198
  memory_md = proj.data_path / "memory" / "MEMORY.md"
 
206
  assert "Existing Memory" in content
207
  assert "Some facts" in content
208
  assert "New pattern" in content
209
+
210
+
211
+ class TestParsePriorRecommendations:
212
+ """Direct coverage for _parse_prior_recommendations edge cases."""
213
+
214
+ def test_no_marker_block_returns_empty(self):
215
+ """A file without any marker block yields no prior recommendations."""
216
+ assert _parse_prior_recommendations("# Project\n\nJust a regular README.\n") == []
217
+
218
+ def test_empty_marker_block_yields_no_recs(self):
219
+ """A marker block with nothing between the markers yields no recs."""
220
+ content = f"prefix\n{_MARKER_START}\n{_MARKER_END}\nsuffix\n"
221
+ assert _parse_prior_recommendations(content) == []
222
+
223
+ def test_marker_block_with_empty_heading_is_skipped(self):
224
+ """A stray `### ` (empty heading) inside the block is skipped, not raised."""
225
+ # Leading `### ` with no heading text, followed by a real section.
226
+ content = (
227
+ f"{_MARKER_START}\n"
228
+ "## Headroom Learned Patterns\n"
229
+ "### \n"
230
+ "some orphan content\n"
231
+ "\n"
232
+ "### Real Section\n"
233
+ "- real bullet\n"
234
+ "\n"
235
+ f"{_MARKER_END}\n"
236
+ )
237
+ recs = _parse_prior_recommendations(content)
238
+ # Only the real section is parsed; the empty-heading entry is dropped.
239
+ assert len(recs) == 1
240
+ assert recs[0].section == "Real Section"
241
+ assert "real bullet" in recs[0].content
tests/test_proxy_copilot_auth_hooks.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations
2
 
 
3
  import importlib.util
4
  import sys
5
  import types
@@ -9,6 +10,27 @@ from types import SimpleNamespace
9
  import pytest
10
 
11
  ROOT = Path(__file__).resolve().parents[1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
 
14
  def _load_handler_module(monkeypatch: pytest.MonkeyPatch, module_name: str, relative_path: str):
@@ -54,8 +76,7 @@ def _load_handler_module(monkeypatch: pytest.MonkeyPatch, module_name: str, rela
54
  return module
55
 
56
 
57
- @pytest.mark.asyncio
58
- async def test_openai_passthrough_applies_copilot_auth(monkeypatch: pytest.MonkeyPatch) -> None:
59
  openai_mod = _load_handler_module(
60
  monkeypatch,
61
  "tests.headroom_proxy_handlers_openai",
@@ -100,20 +121,21 @@ async def test_openai_passthrough_applies_copilot_auth(monkeypatch: pytest.Monke
100
  request.body = body
101
 
102
  handler = Dummy()
103
- response = await handler.handle_passthrough(
104
- request,
105
- "https://api.githubcopilot.com",
106
- "models",
107
- "openai",
 
 
108
  )
109
 
110
- assert seen["url"] == "https://api.githubcopilot.com/v1/models"
111
  assert seen["request_kwargs"]["headers"] == {"Authorization": "Bearer upstream-token"}
112
  assert response.status_code == 200
113
 
114
 
115
- @pytest.mark.asyncio
116
- async def test_streaming_response_applies_copilot_auth(monkeypatch: pytest.MonkeyPatch) -> None:
117
  streaming_mod = _load_handler_module(
118
  monkeypatch,
119
  "tests.headroom_proxy_handlers_streaming",
@@ -155,19 +177,21 @@ async def test_streaming_response_applies_copilot_auth(monkeypatch: pytest.Monke
155
  return SimpleNamespace(headers={}, status_code=200)
156
 
157
  handler = Dummy()
158
- response = await handler._stream_response(
159
- url="https://api.githubcopilot.com/v1/responses",
160
- headers={"authorization": "Bearer downstream"},
161
- body={"model": "gpt-4o"},
162
- provider="openai",
163
- model="gpt-4o",
164
- request_id="req-test",
165
- original_tokens=0,
166
- optimized_tokens=0,
167
- tokens_saved=0,
168
- transforms_applied=[],
169
- tags={},
170
- optimization_latency=0.0,
 
 
171
  )
172
 
173
  assert seen["url"] == "https://api.githubcopilot.com/v1/responses"
 
1
  from __future__ import annotations
2
 
3
+ import asyncio
4
  import importlib.util
5
  import sys
6
  import types
 
10
  import pytest
11
 
12
  ROOT = Path(__file__).resolve().parents[1]
13
+ _ISOLATED_MODULE_NAMES = (
14
+ "headroom.proxy",
15
+ "headroom.proxy.handlers",
16
+ "httpx",
17
+ "fastapi.responses",
18
+ "tests.headroom_proxy_handlers_openai",
19
+ "tests.headroom_proxy_handlers_streaming",
20
+ )
21
+
22
+
23
+ @pytest.fixture(autouse=True)
24
+ def restore_isolated_modules() -> None:
25
+ saved_modules = {name: sys.modules.get(name) for name in _ISOLATED_MODULE_NAMES}
26
+ try:
27
+ yield
28
+ finally:
29
+ for name in _ISOLATED_MODULE_NAMES:
30
+ sys.modules.pop(name, None)
31
+ for name, module in saved_modules.items():
32
+ if module is not None:
33
+ sys.modules[name] = module
34
 
35
 
36
  def _load_handler_module(monkeypatch: pytest.MonkeyPatch, module_name: str, relative_path: str):
 
76
  return module
77
 
78
 
79
+ def test_openai_passthrough_applies_copilot_auth(monkeypatch: pytest.MonkeyPatch) -> None:
 
80
  openai_mod = _load_handler_module(
81
  monkeypatch,
82
  "tests.headroom_proxy_handlers_openai",
 
121
  request.body = body
122
 
123
  handler = Dummy()
124
+ response = asyncio.run(
125
+ handler.handle_passthrough(
126
+ request,
127
+ "https://api.githubcopilot.com",
128
+ "models",
129
+ "openai",
130
+ )
131
  )
132
 
133
+ assert seen["url"] == "https://api.githubcopilot.com/models"
134
  assert seen["request_kwargs"]["headers"] == {"Authorization": "Bearer upstream-token"}
135
  assert response.status_code == 200
136
 
137
 
138
+ def test_streaming_response_applies_copilot_auth(monkeypatch: pytest.MonkeyPatch) -> None:
 
139
  streaming_mod = _load_handler_module(
140
  monkeypatch,
141
  "tests.headroom_proxy_handlers_streaming",
 
177
  return SimpleNamespace(headers={}, status_code=200)
178
 
179
  handler = Dummy()
180
+ response = asyncio.run(
181
+ handler._stream_response(
182
+ url="https://api.githubcopilot.com/v1/responses",
183
+ headers={"authorization": "Bearer downstream"},
184
+ body={"model": "gpt-4o"},
185
+ provider="openai",
186
+ model="gpt-4o",
187
+ request_id="req-test",
188
+ original_tokens=0,
189
+ optimized_tokens=0,
190
+ tokens_saved=0,
191
+ transforms_applied=[],
192
+ tags={},
193
+ optimization_latency=0.0,
194
+ )
195
  )
196
 
197
  assert seen["url"] == "https://api.githubcopilot.com/v1/responses"
tests/test_proxy_dashboard_stats_cache.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ from types import SimpleNamespace
7
+
8
+ import pytest
9
+
10
+ from headroom.dashboard import get_dashboard_html
11
+ from headroom.proxy import helpers as proxy_helpers
12
+
13
+
14
+ class _StatsStub:
15
+ def __init__(self, calls: dict[str, int], key: str, payload: dict):
16
+ self._calls = calls
17
+ self._key = key
18
+ self._payload = payload
19
+
20
+ def get_stats(self) -> dict:
21
+ self._calls[self._key] += 1
22
+ return dict(self._payload)
23
+
24
+
25
+ class _ToinStub:
26
+ def get_stats(self) -> dict:
27
+ return {"patterns": 0}
28
+
29
+
30
+ @pytest.fixture(autouse=True)
31
+ def _reset_rtk_stats_cache() -> None:
32
+ proxy_helpers._rtk_stats_cache.update({"expires_at": 0.0, "has_value": False, "value": None})
33
+
34
+
35
+ def test_get_rtk_stats_memoizes_subprocess_calls(monkeypatch: pytest.MonkeyPatch) -> None:
36
+ now = {"value": 100.0}
37
+ calls = {"run": 0}
38
+
39
+ def _fake_run(*args, **kwargs):
40
+ calls["run"] += 1
41
+ return SimpleNamespace(
42
+ returncode=0,
43
+ stdout=json.dumps({"summary": {"total_commands": 7, "total_saved": 1234}}),
44
+ )
45
+
46
+ monkeypatch.setattr(proxy_helpers.time, "monotonic", lambda: now["value"])
47
+ monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/rtk")
48
+ monkeypatch.setattr(subprocess, "run", _fake_run)
49
+
50
+ first = proxy_helpers._get_rtk_stats()
51
+ second = proxy_helpers._get_rtk_stats()
52
+
53
+ assert first == second
54
+ assert first == {
55
+ "installed": True,
56
+ "total_commands": 7,
57
+ "tokens_saved": 1234,
58
+ "avg_savings_pct": 0.0,
59
+ }
60
+ assert calls["run"] == 1
61
+
62
+ now["value"] += proxy_helpers.RTK_STATS_CACHE_TTL_SECONDS + 0.1
63
+ third = proxy_helpers._get_rtk_stats()
64
+
65
+ assert third == first
66
+ assert calls["run"] == 2
67
+
68
+
69
+ def test_stats_cached_query_reuses_short_ttl_snapshot(monkeypatch: pytest.MonkeyPatch) -> None:
70
+ pytest.importorskip("fastapi")
71
+ from fastapi.testclient import TestClient
72
+
73
+ import headroom.proxy.server as server
74
+ from headroom.proxy.server import ProxyConfig, create_app
75
+
76
+ calls = {"store": 0, "telemetry": 0, "feedback": 0, "rtk": 0}
77
+ now = {"value": 100.0}
78
+
79
+ monkeypatch.setattr(server.time, "monotonic", lambda: now["value"])
80
+ monkeypatch.setattr(
81
+ server,
82
+ "get_compression_store",
83
+ lambda: _StatsStub(calls, "store", {"entry_count": 1, "max_entries": 100}),
84
+ )
85
+ monkeypatch.setattr(
86
+ server,
87
+ "get_telemetry_collector",
88
+ lambda: _StatsStub(calls, "telemetry", {"enabled": True}),
89
+ )
90
+ monkeypatch.setattr(
91
+ server,
92
+ "get_compression_feedback",
93
+ lambda: _StatsStub(calls, "feedback", {}),
94
+ )
95
+
96
+ def _fake_rtk_stats() -> dict[str, int | bool | float]:
97
+ calls["rtk"] += 1
98
+ return {
99
+ "installed": True,
100
+ "total_commands": 1,
101
+ "tokens_saved": 5,
102
+ "avg_savings_pct": 10.0,
103
+ }
104
+
105
+ monkeypatch.setattr(server, "_get_rtk_stats", _fake_rtk_stats)
106
+ monkeypatch.setattr(server, "get_toin", lambda: _ToinStub())
107
+
108
+ app = create_app(
109
+ ProxyConfig(
110
+ optimize=False,
111
+ cache_enabled=False,
112
+ rate_limit_enabled=False,
113
+ cost_tracking_enabled=False,
114
+ log_requests=False,
115
+ ccr_inject_tool=False,
116
+ ccr_handle_responses=False,
117
+ ccr_context_tracking=False,
118
+ )
119
+ )
120
+
121
+ with TestClient(app) as client:
122
+ first = client.get("/stats?cached=1")
123
+ second = client.get("/stats?cached=1")
124
+ now["value"] += 5.1
125
+ third = client.get("/stats?cached=1")
126
+ uncached = client.get("/stats")
127
+
128
+ assert first.status_code == 200
129
+ assert second.status_code == 200
130
+ assert third.status_code == 200
131
+ assert uncached.status_code == 200
132
+
133
+ assert calls == {"store": 3, "telemetry": 3, "feedback": 3, "rtk": 3}
134
+ assert first.json()["cli_filtering"]["tokens_saved"] == 5
135
+
136
+
137
+ def test_dashboard_uses_cached_stats_and_lazy_history_feed_polling() -> None:
138
+ html = get_dashboard_html()
139
+
140
+ assert "fetch('/stats?cached=1')" in html
141
+ assert "@click=\"setViewMode('history')\"" in html
142
+ assert '@click="toggleFeed()"' in html
143
+ assert "this.viewMode === 'history'" in html
144
+ assert "this.feedOpen" in html
tests/test_proxy_streaming_request_logger.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests that the Anthropic streaming finalizer logs requests for the feed.
2
+
3
+ Without this, the streaming Anthropic path (which is what Claude Code uses)
4
+ silently bypassed the request logger, leaving `/stats.recent_requests` and
5
+ `/transformations/feed` permanently empty even when `--log-messages` was set.
6
+ The non-streaming Anthropic path and the Bedrock streaming path were the
7
+ only ones that called `self.logger.log(...)`.
8
+ """
9
+
10
+ from unittest.mock import AsyncMock, MagicMock
11
+
12
+ import httpx
13
+ import pytest
14
+
15
+ from headroom.proxy.request_logger import RequestLogger
16
+ from headroom.proxy.server import HeadroomProxy
17
+
18
+
19
+ def _build_proxy_with_real_logger(*, log_full_messages: bool) -> HeadroomProxy:
20
+ """Build a HeadroomProxy with mocks for everything except the request logger,
21
+ so we can assert what actually gets recorded."""
22
+ proxy = object.__new__(HeadroomProxy)
23
+ proxy.http_client = MagicMock(spec=httpx.AsyncClient)
24
+ proxy.metrics = MagicMock()
25
+ proxy.metrics.record_request = AsyncMock(return_value=None)
26
+ proxy.cost_tracker = MagicMock()
27
+ proxy.cost_tracker.record_tokens.return_value = None
28
+ proxy.memory_manager = None
29
+ proxy.memory_handler = None
30
+ proxy._config = MagicMock()
31
+ proxy._config.log_full_messages = log_full_messages
32
+ proxy._config.ccr_inject_tool = False
33
+ proxy.config = proxy._config
34
+ proxy.logger = RequestLogger(log_file=None, log_full_messages=log_full_messages)
35
+ return proxy
36
+
37
+
38
+ def _stream_state(output_tokens: int = 42) -> dict:
39
+ return {
40
+ "output_tokens": output_tokens,
41
+ "total_bytes": 200,
42
+ "ttfb_ms": 35.0,
43
+ "input_tokens": 1000,
44
+ "cache_read_input_tokens": 0,
45
+ "cache_creation_input_tokens": 0,
46
+ "cache_creation_ephemeral_5m_input_tokens": 0,
47
+ "cache_creation_ephemeral_1h_input_tokens": 0,
48
+ "sse_buffer": "",
49
+ }
50
+
51
+
52
+ @pytest.mark.asyncio
53
+ async def test_finalize_stream_response_logs_request_for_feed():
54
+ proxy = _build_proxy_with_real_logger(log_full_messages=False)
55
+
56
+ await proxy._finalize_stream_response(
57
+ body={"messages": [{"role": "user", "content": "hi"}]},
58
+ provider="anthropic",
59
+ model="claude-sonnet-4-6",
60
+ request_id="req-stream-1",
61
+ original_tokens=1000,
62
+ optimized_tokens=600,
63
+ tokens_saved=400,
64
+ transforms_applied=["smart_crusher"],
65
+ optimization_latency=12.0,
66
+ stream_state=_stream_state(),
67
+ start_time=0.0,
68
+ tags={"stack": "wrap_claude"},
69
+ )
70
+
71
+ entries = proxy.logger.get_recent(10)
72
+ assert len(entries) == 1, "streaming finalizer must log exactly one entry per request"
73
+ entry = entries[0]
74
+ assert entry["request_id"] == "req-stream-1"
75
+ assert entry["provider"] == "anthropic"
76
+ assert entry["model"] == "claude-sonnet-4-6"
77
+ assert entry["input_tokens_original"] == 1000
78
+ assert entry["input_tokens_optimized"] == 600
79
+ assert entry["tokens_saved"] == 400
80
+ assert entry["savings_percent"] == pytest.approx(40.0)
81
+ assert entry["transforms_applied"] == ["smart_crusher"]
82
+ assert entry["tags"] == {"stack": "wrap_claude"}
83
+ assert entry["cache_hit"] is False
84
+
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_finalize_stream_response_includes_messages_when_log_full_messages_enabled():
88
+ proxy = _build_proxy_with_real_logger(log_full_messages=True)
89
+ body = {"messages": [{"role": "user", "content": "hello"}]}
90
+
91
+ await proxy._finalize_stream_response(
92
+ body=body,
93
+ provider="anthropic",
94
+ model="claude-sonnet-4-6",
95
+ request_id="req-stream-2",
96
+ original_tokens=10,
97
+ optimized_tokens=8,
98
+ tokens_saved=2,
99
+ transforms_applied=[],
100
+ optimization_latency=1.0,
101
+ stream_state=_stream_state(output_tokens=5),
102
+ start_time=0.0,
103
+ )
104
+
105
+ entries = proxy.logger.get_recent_with_messages(10)
106
+ assert len(entries) == 1
107
+ assert entries[0]["request_messages"] == body["messages"]
108
+
109
+
110
+ @pytest.mark.asyncio
111
+ async def test_finalize_stream_response_omits_messages_when_log_full_messages_disabled():
112
+ proxy = _build_proxy_with_real_logger(log_full_messages=False)
113
+
114
+ await proxy._finalize_stream_response(
115
+ body={"messages": [{"role": "user", "content": "hello"}]},
116
+ provider="anthropic",
117
+ model="claude-sonnet-4-6",
118
+ request_id="req-stream-3",
119
+ original_tokens=10,
120
+ optimized_tokens=8,
121
+ tokens_saved=2,
122
+ transforms_applied=[],
123
+ optimization_latency=1.0,
124
+ stream_state=_stream_state(output_tokens=5),
125
+ start_time=0.0,
126
+ )
127
+
128
+ entries = proxy.logger.get_recent_with_messages(10)
129
+ assert len(entries) == 1
130
+ assert entries[0]["request_messages"] is None
131
+
132
+
133
+ @pytest.mark.asyncio
134
+ async def test_finalize_stream_response_handles_zero_original_tokens():
135
+ proxy = _build_proxy_with_real_logger(log_full_messages=False)
136
+
137
+ await proxy._finalize_stream_response(
138
+ body={"messages": []},
139
+ provider="anthropic",
140
+ model="claude-sonnet-4-6",
141
+ request_id="req-stream-4",
142
+ original_tokens=0,
143
+ optimized_tokens=0,
144
+ tokens_saved=0,
145
+ transforms_applied=[],
146
+ optimization_latency=0.0,
147
+ stream_state=_stream_state(output_tokens=0),
148
+ start_time=0.0,
149
+ )
150
+
151
+ entries = proxy.logger.get_recent(10)
152
+ assert len(entries) == 1
153
+ assert entries[0]["savings_percent"] == 0
154
+
155
+
156
+ @pytest.mark.asyncio
157
+ async def test_finalize_stream_response_no_op_when_logger_disabled():
158
+ proxy = _build_proxy_with_real_logger(log_full_messages=False)
159
+ proxy.logger = None # `--no-log-requests` would put us here
160
+
161
+ # Should not raise.
162
+ await proxy._finalize_stream_response(
163
+ body={"messages": []},
164
+ provider="anthropic",
165
+ model="claude-sonnet-4-6",
166
+ request_id="req-stream-5",
167
+ original_tokens=10,
168
+ optimized_tokens=8,
169
+ tokens_saved=2,
170
+ transforms_applied=[],
171
+ optimization_latency=1.0,
172
+ stream_state=_stream_state(),
173
+ start_time=0.0,
174
+ )