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:
@@ -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)"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
"type": "json",
|
||||
"path": "plugins/openclaw/package.json",
|
||||
"jsonpath": "$.version"
|
||||
},
|
||||
{
|
||||
"type": "json",
|
||||
"path": "plugins/opencode/package.json",
|
||||
"jsonpath": "$.version"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Generated
+2
-2
@@ -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,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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user