fix: publish headroom-opencode in release workflow (#2372)

## Description

`headroom-opencode` is documented as an npm package, but the release
workflow never published it, so installs failed with a registry 404 even
though the plugin source already lived under `plugins/opencode`. This
wires the existing package into the npm release path, keeps its version
synced with root releases, and adds release guards for the new package.
Closes #76.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- added `headroom-opencode` to the npm release workflow, including
release-version stamping and `headroom-ai` dependency rewrite before
publish
- added `plugins/opencode/package.json` to release-please and local
version-sync guards
- synced the source opencode package version to the current release line
and documented the new npm package in the release docs
- added focused release workflow and version-sync tests for the opencode
package
- aligned the two failing dashboard Playwright tests with the current
Session/Lifetime split and `/stats-lifetime` fixture contract

## Testing

- [x] Unit tests pass (`uv run pytest scripts/tests/test_version_sync.py
-q`, `uv run pytest tests/test_release_workflows.py -q -k
'publish_npm_rewrites_opencode_dependency_after_version_and_before_publish
or opencode_source_dependency_matches_lockfile_registry_range or
release_please_manifest_config_consistency'`)
- [x] Unit tests pass (`uv run pytest
tests/test_dashboard_cache_lifetime_playwright.py
tests/test_dashboard_cache_ttl_playwright.py -q`)
- [x] Linting passes (`uv run ruff check scripts/verify-versions.py
scripts/version-sync.py scripts/tests/test_version_sync.py
tests/test_release_workflows.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
$ uv run pytest scripts/tests/test_version_sync.py -q
8 passed, 1 warning in 0.51s

$ uv run pytest tests/test_release_workflows.py -q -k 'publish_npm_rewrites_opencode_dependency_after_version_and_before_publish or opencode_source_dependency_matches_lockfile_registry_range or release_please_manifest_config_consistency'
2 passed, 38 deselected, 1 warning in 0.07s

$ uv run pytest tests/test_dashboard_cache_lifetime_playwright.py tests/test_dashboard_cache_ttl_playwright.py -q
4 passed, 1 warning in 4.04s

$ uv run ruff check scripts/verify-versions.py scripts/version-sync.py scripts/tests/test_version_sync.py tests/test_release_workflows.py
All checks passed!

$ npm ci && npm run build  (plugins/opencode)
Build success; dist/index.js, dist/entry.opencode.js, and DTS outputs emitted
```

## Real Behavior Proof

- Environment: Windows, Python 3.11.15, Node v24.15.0, npm 11.16.0
- Exact command / steps: inspected `.github/workflows/release.yml`,
updated the npm publish path for `plugins/opencode`, aligned the two
failing dashboard Playwright tests with the current Session/Lifetime
split, then ran the focused pytest commands above plus `npm ci && npm
run build` in `plugins/opencode`
- Observed result: the release workflow now versions and publishes
`headroom-opencode`, release-please and version-sync track
`plugins/opencode/package.json`, the dashboard tests now fetch durable
cache and setup-url data from the Lifetime view, and the opencode
package still builds locally from source
- Not tested: GitHub Package Registry publish

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

`CHANGELOG.md` is unchanged because release-please owns changelog
generation here.

---------

Co-authored-by: JD Davis <mxjerrett@gmail.com>
This commit is contained in:
Rod Boev
2026-08-12 00:56:40 -04:00
committed by GitHub
parent aaeba0a319
commit 78591545ce
12 changed files with 208 additions and 56 deletions
+2 -2
View File
@@ -561,7 +561,7 @@ jobs:
- name: Install test dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
pip install pytest 'opentelemetry-api>=1.24.0'
- name: Run native installer wrapper tests
run: pytest tests/test_install/test_native_installers.py -q
@@ -579,7 +579,7 @@ jobs:
run: |
brew install bash
python -m pip install --upgrade pip
python -m pip install --retries 10 --timeout 60 pytest
python -m pip install --retries 10 --timeout 60 pytest 'opentelemetry-api>=1.24.0'
- name: Run native installer wrapper tests
run: |
BASH_PREFIX="$(brew --prefix bash)"
+24 -3
View File
@@ -12,6 +12,7 @@ env:
NPM_REGISTRY_URL: https://registry.npmjs.org
NPM_SDK_PACKAGE: headroom-ai
NPM_OPENCLAW_PACKAGE: headroom-openclaw
NPM_OPENCODE_PACKAGE: headroom-opencode
# GitHub Package Registry
GITHUB_PACKAGES_REGISTRY_URL: https://npm.pkg.github.com
@@ -852,10 +853,30 @@ jobs:
npm publish --access public
continue-on-error: true
- name: npm publish notice
if: steps.npm-sdk-publish.outcome == 'failure' || steps.npm-openclaw-publish.outcome == 'failure'
- name: Publish ${{ env.NPM_OPENCODE_PACKAGE }} to npmjs.org
id: npm-opencode-publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
echo "::notice::One or more npm publishes failed. Set NPM_SKIP=true in repo Variables to skip both npm publishes if tokens are not configured."
version="${{ needs.detect-version.outputs.npm_version }}"
cd plugins/opencode
npm ci
npm run build
npm version "$version" --no-git-tag-version --allow-same-version
HEADROOM_NPM_VERSION="$version" node <<'EOF'
const fs = require("fs");
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
pkg.dependencies = pkg.dependencies || {};
pkg.dependencies["headroom-ai"] = `^${process.env.HEADROOM_NPM_VERSION}`;
fs.writeFileSync("package.json", `${JSON.stringify(pkg, null, 2)}\n`);
EOF
npm publish --access public
continue-on-error: true
- name: npm publish notice
if: steps.npm-sdk-publish.outcome == 'failure' || steps.npm-openclaw-publish.outcome == 'failure' || steps.npm-opencode-publish.outcome == 'failure'
run: |
echo "::notice::One or more npm publishes failed. Set NPM_SKIP=true in repo Variables to skip npm publishes if tokens are not configured."
publish-github-packages:
needs: [detect-version, build]
+5
View File
@@ -23,6 +23,11 @@
"type": "json",
"path": "plugins/openclaw/package.json",
"jsonpath": "$.version"
},
{
"type": "json",
"path": "plugins/opencode/package.json",
"jsonpath": "$.version"
}
]
}
+15 -11
View File
@@ -16,10 +16,11 @@ For the end-to-end visual flow, see [CI/CD Flow Diagrams](/docs/ci-cd-flows).
| Package | Type | Registry | Environment Variable |
|---------|------|----------|----------------------|
| `headroom-ai` | Python | PyPI | `PYPI_PACKAGE` |
| `headroom-ai` | TypeScript SDK | npmjs.org | `NPM_SDK_PACKAGE` |
| `headroom-openclaw` | TypeScript plugin | npmjs.org | `NPM_OPENCLAW_PACKAGE` |
| `@{owner}/headroom-ai` | TypeScript SDK | GitHub Package Registry | — |
| `@{owner}/headroom-openclaw` | TypeScript plugin | GitHub Package Registry | — |
| `headroom-ai` | TypeScript SDK | npmjs.org | `NPM_SDK_PACKAGE` |
| `headroom-openclaw` | TypeScript plugin | npmjs.org | `NPM_OPENCLAW_PACKAGE` |
| `headroom-opencode` | TypeScript plugin | npmjs.org | `NPM_OPENCODE_PACKAGE` |
| `@{owner}/headroom-ai` | TypeScript SDK | GitHub Package Registry | — |
| `@{owner}/headroom-openclaw` | TypeScript plugin | GitHub Package Registry | — |
| `headroom-ai-{version}.tar.gz` / `headroom_ai-{version}-py3-none-any.whl` | Python package distributions | GitHub Release (`{owner}/headroom`) | — |
| `headroom-ai-{version}.tgz` / `headroom-openclaw-{version}.tgz` | Node release assets | GitHub Release (`{owner}/headroom`) | — |
| `ghcr.io/{owner}/headroom` | Docker image | GitHub Container Registry | — |
@@ -41,6 +42,7 @@ Release Please calculates the release version from conventional commits and the
- `pyproject.toml` - `[project].version`
- `headroom/_version.py` - `__version__`, synced at build time
- `plugins/openclaw/package.json` - `version`, synced at build time
- `plugins/opencode/package.json` - `version`, synced at build time
- `sdk/typescript/package.json` - `version`, synced at build time
`release.yml` does not commit back to the repo. Version synchronization happens inside the release build workspace.
@@ -98,9 +100,10 @@ Installs the built wheels into representative customer environments and imports
Downloads the Python dist artifact and publishes to PyPI via `pypa/gh-action-pypi-publish@release/v1` (trusted publisher).
### publish-npm
Publishes both TypeScript packages to npmjs.org:
- `sdk/typescript/` as `headroom-ai`
- `plugins/openclaw/` as `headroom-openclaw`
Publishes all npm packages to npmjs.org:
- `sdk/typescript/` as `headroom-ai`
- `plugins/openclaw/` as `headroom-openclaw`
- `plugins/opencode/` as `headroom-opencode`
### publish-github-packages
Publishes both Node packages to GitHub Package Registry (`npm.pkg.github.com`) using the current repository owner as the npm scope:
@@ -124,10 +127,11 @@ All package names, registry URLs, and environment names are defined as top-level
env:
PYPI_PACKAGE: headroom-ai
PYPI_ENVIRONMENT: pypi
NPM_REGISTRY_URL: https://registry.npmjs.org
NPM_SDK_PACKAGE: headroom-ai
NPM_OPENCLAW_PACKAGE: headroom-openclaw
GITHUB_PACKAGES_REGISTRY_URL: https://npm.pkg.github.com
NPM_REGISTRY_URL: https://registry.npmjs.org
NPM_SDK_PACKAGE: headroom-ai
NPM_OPENCLAW_PACKAGE: headroom-openclaw
NPM_OPENCODE_PACKAGE: headroom-opencode
GITHUB_PACKAGES_REGISTRY_URL: https://npm.pkg.github.com
```
To rename a package, update the corresponding constant — all references throughout the workflow update automatically.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "headroom-opencode",
"version": "0.1.0",
"version": "0.32.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "headroom-opencode",
"version": "0.1.0",
"version": "0.32.0",
"license": "Apache-2.0",
"dependencies": {
"@opencode-ai/plugin": "^1.18.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "headroom-opencode",
"version": "0.1.0",
"version": "0.32.0",
"description": "Headroom proxy integration plugin for OpenCode - routes LLM traffic through the Headroom proxy for token compression",
"type": "module",
"main": "./dist/index.js",
+52
View File
@@ -22,6 +22,8 @@ def temp_project(tmp_path: Path) -> dict[str, Path]:
plugins = root / "plugins"
openclaw = plugins / "openclaw"
openclaw.mkdir(parents=True)
opencode = plugins / "opencode"
opencode.mkdir(parents=True)
agent_hooks_claude = plugins / "headroom-agent-hooks" / ".claude-plugin"
agent_hooks_claude.mkdir(parents=True)
agent_hooks_github = plugins / "headroom-agent-hooks" / ".github" / "plugin"
@@ -50,6 +52,17 @@ def temp_project(tmp_path: Path) -> dict[str, Path]:
)
)
opencode_pkg = opencode / "package.json"
opencode_pkg.write_text(
json.dumps(
{
"name": "test-opencode",
"version": "0.5.25",
"dependencies": {"headroom-ai": "^0.22.3"},
}
)
)
repo_claude_marketplace = repo_claude_plugin / "marketplace.json"
repo_claude_marketplace.write_text(
json.dumps(
@@ -99,6 +112,7 @@ def temp_project(tmp_path: Path) -> dict[str, Path]:
"pyproject": pyproject,
"version_py": version_py,
"openclaw_pkg": openclaw_pkg,
"opencode_pkg": opencode_pkg,
"repo_claude_marketplace": repo_claude_marketplace,
"repo_github_marketplace": repo_github_marketplace,
"claude_plugin": claude_plugin,
@@ -134,6 +148,10 @@ def test_version_sync_explicit_version(temp_project: dict[str, Path]) -> None:
assert openclaw_pkg["version"] == "0.7.0"
assert openclaw_pkg["dependencies"]["headroom-ai"] == "^0.22.3"
opencode_pkg = json.loads(temp_project["opencode_pkg"].read_text())
assert opencode_pkg["version"] == "0.7.0"
assert opencode_pkg["dependencies"]["headroom-ai"] == "^0.22.3"
# Verify sdk/typescript/package.json
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
assert typescript_pkg["version"] == "0.7.0"
@@ -160,6 +178,7 @@ def test_version_sync_explicit_version(temp_project: dict[str, Path]) -> None:
assert metadata["packages"]["pypi"] == "0.7.0"
assert metadata["packages"]["npm-sdk"] == "0.7.0"
assert metadata["packages"]["npm-openclaw"] == "0.7.0"
assert metadata["packages"]["npm-opencode"] == "0.7.0"
assert metadata["packages"]["agent-hooks-plugin"] == "0.7.0"
@@ -187,6 +206,10 @@ def test_bump_patch(temp_project: dict[str, Path]) -> None:
assert openclaw_pkg["version"] == "0.5.26"
assert openclaw_pkg["dependencies"]["headroom-ai"] == "^0.22.3"
opencode_pkg = json.loads(temp_project["opencode_pkg"].read_text())
assert opencode_pkg["version"] == "0.5.26"
assert opencode_pkg["dependencies"]["headroom-ai"] == "^0.22.3"
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
assert typescript_pkg["version"] == "0.5.26"
@@ -218,6 +241,10 @@ def test_bump_minor(temp_project: dict[str, Path]) -> None:
assert openclaw_pkg["version"] == "0.6.0"
assert openclaw_pkg["dependencies"]["headroom-ai"] == "^0.22.3"
opencode_pkg = json.loads(temp_project["opencode_pkg"].read_text())
assert opencode_pkg["version"] == "0.6.0"
assert opencode_pkg["dependencies"]["headroom-ai"] == "^0.22.3"
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
assert typescript_pkg["version"] == "0.6.0"
@@ -249,6 +276,10 @@ def test_bump_major(temp_project: dict[str, Path]) -> None:
assert openclaw_pkg["version"] == "1.0.0"
assert openclaw_pkg["dependencies"]["headroom-ai"] == "^0.22.3"
opencode_pkg = json.loads(temp_project["opencode_pkg"].read_text())
assert opencode_pkg["version"] == "1.0.0"
assert opencode_pkg["dependencies"]["headroom-ai"] == "^0.22.3"
typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text())
assert typescript_pkg["version"] == "1.0.0"
@@ -279,6 +310,7 @@ def test_release_metadata_written(temp_project: dict[str, Path]) -> None:
"pypi": "0.6.0",
"npm-sdk": "0.6.0",
"npm-openclaw": "0.6.0",
"npm-opencode": "0.6.0",
"agent-hooks-plugin": "0.6.0",
},
}
@@ -309,6 +341,7 @@ def test_plugin_manifests_only_leaves_package_versions_unchanged(
assert 'version = "0.5.25"' in temp_project["pyproject"].read_text()
assert '__version__ = "0.5.25"' in temp_project["version_py"].read_text()
assert json.loads(temp_project["openclaw_pkg"].read_text())["version"] == "0.5.25"
assert json.loads(temp_project["opencode_pkg"].read_text())["version"] == "0.5.25"
assert json.loads(temp_project["typescript_pkg"].read_text())["version"] == "0.5.25"
assert json.loads(temp_project["claude_plugin"].read_text())["version"] == "0.8.0"
assert (
@@ -362,3 +395,22 @@ def test_server_json_version_is_synchronized(temp_project: dict[str, Path]) -> N
# Untouched keys must survive so the file still matches the builder's output.
assert server_json["name"] == "io.github.headroomlabs-ai/headroom"
assert server_json["packages"][0]["registryType"] == "pypi"
def test_opencode_headroom_dependency_is_preserved_for_registry_installability(
temp_project: dict[str, Path],
) -> None:
"""Source package stays installable even when the next SDK is not on npm yet."""
root = temp_project["root"]
script = Path(__file__).parent.parent / "version-sync.py"
result = subprocess.run(
[sys.executable, str(script), "--root", str(root), "--version", "0.28.0"],
capture_output=True,
text=True,
)
assert result.returncode == 0, f"Script failed: {result.stderr}"
opencode_pkg = json.loads(temp_project["opencode_pkg"].read_text())
assert opencode_pkg["version"] == "0.28.0"
assert opencode_pkg["dependencies"]["headroom-ai"] == "^0.22.3"
+1
View File
@@ -40,6 +40,7 @@ def main() -> None:
versions = {
"pyproject.toml": py_ver,
"plugins/openclaw/package.json": _read_json_version(ROOT / "plugins/openclaw/package.json"),
"plugins/opencode/package.json": _read_json_version(ROOT / "plugins/opencode/package.json"),
"sdk/typescript/package.json": _read_json_version(ROOT / "sdk/typescript/package.json"),
"plugins/headroom-agent-hooks/.claude-plugin/plugin.json": _read_json_version(
ROOT / "plugins/headroom-agent-hooks/.claude-plugin/plugin.json"
+17
View File
@@ -124,6 +124,21 @@ def update_openclaw_package_json(file_path: Path, version: str) -> None:
f.write("\n")
def update_opencode_package_json(file_path: Path, version: str) -> None:
"""Update opencode package.json version.
Keep the source `headroom-ai` dependency registry-installable. The release
workflow rewrites the packed dependency to the exact release range right
before npm publication.
"""
with open(file_path, encoding="utf-8") as f:
data = json.load(f)
data["version"] = version
with open(file_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
def update_pyproject_version(root: Path, version: str) -> None:
"""Update pyproject.toml version."""
pyproject_path = root / "pyproject.toml"
@@ -145,6 +160,7 @@ def write_release_metadata(root: Path, version: str) -> None:
"pypi": version,
"npm-sdk": version,
"npm-openclaw": version,
"npm-opencode": version,
"agent-hooks-plugin": version,
},
}
@@ -192,6 +208,7 @@ def main() -> None:
# Update all versioned files
update_pyproject_version(args.root, version)
update_openclaw_package_json(args.root / "plugins" / "openclaw" / "package.json", version)
update_opencode_package_json(args.root / "plugins" / "opencode" / "package.json", version)
update_package_json(args.root / "sdk" / "typescript" / "package.json", version)
update_plugin_versions(args.root, version)
update_server_json(args.root / "server.json", version)
@@ -1,11 +1,8 @@
"""Playwright validation for the persisted lifetime Cache Reads tile.
"""Playwright validation for persisted lifetime Prefix Cache metrics.
The Prefix Cache Impact card historically rendered only from in-memory
session counters, so every proxy restart blanked the operator's cache
savings. These tests pin the durable behavior: the card renders from
``persistent_savings.lifetime.cache_read_tokens`` alone after a restart
with zero traffic, session-scoped tiles read "no activity since restart",
and the card stays hidden when neither session nor lifetime data exists.
The Session dashboard now reports only current-process cache activity,
while durable cache totals live behind the Lifetime view. These tests pin
that split after a restart with zero session traffic.
"""
from __future__ import annotations
@@ -29,24 +26,44 @@ expect = playwright.expect
sync_playwright = playwright.sync_playwright
def _stats_lifetime_only() -> dict:
"""Post-restart shape: zero session cache traffic, persisted lifetime present."""
def _session_stats_no_cache() -> dict:
"""Post-restart session payload: zero current-process cache traffic."""
stats = copy.deepcopy(_sample_stats())
totals = stats.setdefault("prefix_cache", {}).setdefault("totals", {})
totals.update({"requests": 0, "cache_read_tokens": 0, "cache_write_tokens": 0})
stats.setdefault("persistent_savings", {})["lifetime"] = {
"requests": 6088,
"tokens_saved": 42_181,
"compression_savings_usd": 0.5,
"cache_read_tokens": 629_537_547,
"cache_savings_usd": 7.2,
"total_input_tokens": 1_294_591_655,
"total_input_cost_usd": 12.5,
}
return stats
def _install_dashboard_routes(page: Page, stats: dict) -> None:
def _lifetime_cache_payload(
*,
cache_read_tokens: int = 629_537_547,
cache_savings_usd: float = 7.2,
) -> dict:
return {
"requests": {"total": 6088, "failed": 0, "rate_limited": 0, "cached": 0},
"tokens": {"saved": 42_181},
"cost": {
"input_usd": 12.5,
"compression_savings_usd": 0.5,
"cache_savings_usd": cache_savings_usd,
},
"prefix_cache": {
"cache_read_tokens": cache_read_tokens,
"cache_write_tokens": 0,
"hit_requests": 0,
"requests": 0,
"bust_count": 0,
"bust_tokens": 0,
},
"projects": {},
"by_model": {},
"by_stack": {},
"waste_signals": {},
"persistence": {"healthy": True, "enabled": True, "pending_records": 0},
}
def _install_dashboard_routes(page: Page, stats: dict, lifetime: dict) -> None:
history = _sample_history()
health = {"status": "healthy", "version": "0.3.0"}
dashboard_html = get_dashboard_html()
@@ -61,6 +78,9 @@ def _install_dashboard_routes(page: Page, stats: dict) -> None:
if "/stats-history" in path:
route.fulfill(status=200, content_type="application/json", body=json.dumps(history))
return
if path.endswith("/stats-lifetime"):
route.fulfill(status=200, content_type="application/json", body=json.dumps(lifetime))
return
if path.endswith("/stats"):
route.fulfill(status=200, content_type="application/json", body=json.dumps(stats))
return
@@ -72,8 +92,8 @@ def _install_dashboard_routes(page: Page, stats: dict) -> None:
page.route("**/*", handler)
def _open_dashboard(page: Page, stats: dict) -> None:
_install_dashboard_routes(page, stats)
def _open_dashboard(page: Page, stats: dict, lifetime: dict) -> None:
_install_dashboard_routes(page, stats, lifetime)
page.goto("http://headroom.local/dashboard")
page.wait_for_load_state("networkidle")
@@ -82,31 +102,25 @@ def test_card_renders_lifetime_cache_reads_after_zero_traffic_restart() -> None:
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1440, "height": 1600})
_open_dashboard(page, _stats_lifetime_only())
_open_dashboard(page, _session_stats_no_cache(), _lifetime_cache_payload())
expect(page.get_by_text("Prefix Cache Impact", exact=True)).to_be_visible()
expect(page.get_by_text("Cache Reads (lifetime)", exact=True)).to_be_visible()
expect(page.get_by_text("629.5M", exact=True)).to_be_visible()
expect(page.get_by_text("$7.20 saved")).to_be_visible()
# Session-scoped siblings read as inactive, not as literal zeros.
expect(page.get_by_text("no activity since restart").first).to_be_visible()
assert page.get_by_text("no activity since restart").count() >= 5
# x-show hides via CSS (element stays in the DOM), so assert
# visibility, not count — unlike the x-if card gate below.
expect(page.get_by_text("Cache Efficiency", exact=True)).to_be_hidden()
expect(page.get_by_text("Prefix Cache Impact", exact=True)).to_have_count(0)
page.get_by_role("button", name="Lifetime", exact=True).click()
expect(page.get_by_role("heading", name="Prefix Cache", exact=True)).to_be_visible()
expect(page.get_by_text("629.5M / 0", exact=True)).to_be_visible()
expect(page.get_by_text("$7.20", exact=True)).to_be_visible()
browser.close()
def test_card_hidden_when_no_session_and_no_lifetime_data() -> None:
stats = _stats_lifetime_only()
stats["persistent_savings"]["lifetime"]["cache_read_tokens"] = 0
stats["persistent_savings"]["lifetime"]["cache_savings_usd"] = 0.0
stats = _session_stats_no_cache()
lifetime = _lifetime_cache_payload(cache_read_tokens=0, cache_savings_usd=0.0)
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1440, "height": 1600})
_open_dashboard(page, stats)
_open_dashboard(page, stats, lifetime)
expect(page.get_by_text("Prefix Cache Impact", exact=True)).to_have_count(0)
@@ -175,6 +175,7 @@ def _fulfill_static_asset(route, path: str) -> bool: # type: ignore[no-untyped-
def _install_dashboard_routes(page: Page) -> None:
stats = _sample_stats()
history = _sample_history()
lifetime = {"projects": {}}
health = {"status": "healthy", "version": "0.3.0"}
dashboard_html = get_dashboard_html()
@@ -195,6 +196,9 @@ def _install_dashboard_routes(page: Page) -> None:
body=json.dumps(history),
)
return
if path.endswith("/stats-lifetime"):
route.fulfill(status=200, content_type="application/json", body=json.dumps(lifetime))
return
if path.endswith("/stats"):
route.fulfill(status=200, content_type="application/json", body=json.dumps(stats))
return
@@ -213,6 +217,7 @@ def test_dashboard_per_project_setup_url_uses_current_origin() -> None:
_install_dashboard_routes(page)
page.goto("http://127.0.0.1:8788/dashboard", wait_until="load")
page.get_by_role("button", name="Lifetime", exact=True).click()
expect(
page.get_by_text(
"ANTHROPIC_BASE_URL: http://127.0.0.1:8788/p/<project-name>", exact=True
@@ -225,6 +230,7 @@ def test_dashboard_per_project_setup_url_uses_current_origin() -> None:
).to_have_count(0)
page.goto("http://headroom.local:9393/dashboard", wait_until="load")
page.get_by_role("button", name="Lifetime", exact=True).click()
expect(
page.get_by_text(
"ANTHROPIC_BASE_URL: http://headroom.local:9393/p/<project-name>", exact=True
+33 -1
View File
@@ -675,6 +675,19 @@ def test_openclaw_source_dependency_matches_lockfile_registry_range() -> None:
assert source_range == lock_range == "^0.22.3"
def test_opencode_source_dependency_matches_lockfile_registry_range() -> None:
"""The source checkout must remain npm-ci installable before a release exists."""
import json
package_json = json.loads((ROOT / "plugins" / "opencode" / "package.json").read_text())
package_lock = json.loads((ROOT / "plugins" / "opencode" / "package-lock.json").read_text())
source_range = package_json["dependencies"]["headroom-ai"]
lock_range = package_lock["packages"][""]["dependencies"]["headroom-ai"]
assert source_range == lock_range == "^0.22.3"
def test_python_release_smoke_imports_installed_wheel_outside_source_tree() -> None:
"""The wheel smoke must not import the checkout package by accident."""
script = (ROOT / "scripts" / "build_python_release_smoke.py").read_text(encoding="utf-8")
@@ -700,6 +713,21 @@ def test_publish_npm_regenerates_openclaw_dist_metadata_after_version_and_depend
assert version < dependency < prepare_dist < publish
def test_publish_npm_rewrites_opencode_dependency_after_version_and_before_publish() -> None:
"""The direct npm publish path must version and retarget the Opencode dependency."""
content = (ROOT / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8")
start = content.index("name: Publish ${{ env.NPM_OPENCODE_PACKAGE }} to npmjs.org")
end = content.index("continue-on-error: true", start)
block = content[start:end]
version = block.index('npm version "$version"')
dependency = block.index('pkg.dependencies["headroom-ai"]')
publish = block.index("npm publish --access public")
assert version < dependency < publish
def test_sdist_license_is_packaged_and_verified_before_upload() -> None:
"""STRUCTURAL INVARIANT: the sdist tarball must physically contain
every license file PEP 639 declares in PKG-INFO, and the release
@@ -1259,7 +1287,7 @@ def test_release_please_config_and_manifest_are_present_and_consistent() -> None
"changelog because the bot can't find its baseline."
)
# extra-files: TypeScript SDK and openclaw plugin package.json
# extra-files: TypeScript SDK and npm plugin package.json files
# files must be in lockstep with pyproject.toml.
extra_paths = {ef["path"] for ef in root_pkg.get("extra-files", [])}
assert "sdk/typescript/package.json" in extra_paths, (
@@ -1270,6 +1298,10 @@ def test_release_please_config_and_manifest_are_present_and_consistent() -> None
"release-please must bump plugins/openclaw/package.json so the "
"openclaw npm publish stays in sync."
)
assert "plugins/opencode/package.json" in extra_paths, (
"release-please must bump plugins/opencode/package.json so the "
"opencode npm publish stays in sync."
)
def test_release_metadata_sync_runs_on_release_please_branch() -> None: