diff --git a/CHANGELOG.md b/CHANGELOG.md index 25130095..0873a0b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ This project follows [Semantic Versioning](https://semver.org/). +## 0.8.7 - 2026-08-22 + +### Changed + +- Menu-bar data refreshes automatically every 30 minutes, while manual refresh and opening the + dashboard trigger a fresh bounded query instead of only repainting cached values. +- Codex quota collection now follows the official app-server rate-limit query path, with bounded + fallback and explicit stale-data handling when the upstream service cannot be reached. +- GPT, Codex, and Cursor capacity displays now preserve live changes and avoid duplicate balance + rows or misleading low estimates caused by mixed-age snapshots. + +### Security + +- Dependency checks now pin the build environment to `pip 26.2` and retain the existing release + artifact audit boundary. + +### Fixed + +- Cross-platform tests no longer invoke POSIX-only process-group behavior or hard-code `/bin/sh` + on Windows. +- Long Provider refreshes no longer cross the old three-minute desktop deadline and get reported + as failed shortly before the background refresh succeeds. +- The 30-minute menu-bar schedule now waits one interval before its first background refresh and + defers provider credential initialization until refresh is due, so app launch does not compete + with an immediate user or Capacity-page refresh. +- Refresh failures keep the last known-good quota visible with a clear freshness state instead of + silently presenting it as current data. + ## 0.8.6 - 2026-08-16 ### Added diff --git a/README.en.md b/README.en.md index 6fccef7c..ec4d82a9 100644 --- a/README.en.md +++ b/README.en.md @@ -1,15 +1,15 @@ - - + +
# UsageHub ### The All-in-One AI Usage & Provider Manager — Menu-bar Summary · Capacity · API Spend -[![Version](https://img.shields.io/github/v/release/tttboy123/openusage-bar?include_prereleases&color=0A84FF&label=version)](https://github.com/tttboy123/openusage-bar/releases) -[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-lightgrey.svg)](https://github.com/tttboy123/openusage-bar/releases) +[![Version](https://img.shields.io/github/v/release/tttboy123/usagehub?include_prereleases&color=0A84FF&label=version)](https://github.com/tttboy123/usagehub/releases) +[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-lightgrey.svg)](https://github.com/tttboy123/usagehub/releases) [![Built with](https://img.shields.io/badge/built%20with-Electron%20%2B%20SwiftUI-blue.svg)](https://www.electronjs.org/) -[![Downloads](https://img.shields.io/github/downloads/tttboy123/openusage-bar/total)](https://github.com/tttboy123/openusage-bar/releases/latest) +[![Downloads](https://img.shields.io/github/downloads/tttboy123/usagehub/total)](https://github.com/tttboy123/usagehub/releases/latest) [![License](https://img.shields.io/badge/License-Apache--2.0-111111?style=flat-square)](LICENSE) [中文](README.md) | English | [Changelog](CHANGELOG.md) | [Install guide](docs/release-quick-start.md) | [Provider support](docs/provider-support.md) | [Local API](docs/api/local-api-v1.md) @@ -140,8 +140,8 @@ corrupt existing configs. Why is the menu-bar icon missing? The tray icon only shows while the app is running — open UsageHub (or let it auto-start at login). -Versions before 0.8.6 had a tray-icon path bug that made it invisible; 0.8.6 ships a bundled brand -template icon. +Versions before 0.8.6 had a tray-icon path bug that made it invisible; 0.8.6 fixed it with a bundled +brand template icon. @@ -190,13 +190,13 @@ Remove quarantine for this app only; do not disable Gatekeeper system-wide. ### macOS -Grab the latest `UsageHub-0.8.6-mac-arm64.dmg` from [Releases](https://github.com/tttboy123/openusage-bar/releases), +Grab the latest `UsageHub-0.8.7-mac-arm64.dmg` from [Releases](https://github.com/tttboy123/usagehub/releases), open it, and drag **UsageHub** into **Applications**. First launch registers the login item and bundled collector; the menu-bar icon immediately shows today's usage summary. -Native SwiftUI build: [OpenUsage-Bar-v0.8.6-macos-arm64.dmg](https://github.com/tttboy123/openusage-bar/releases/download/v0.8.6/OpenUsage-Bar-v0.8.6-macos-arm64.dmg) (available after candidate publication). +Native SwiftUI build: [OpenUsage-Bar-v0.8.7-macos-arm64.dmg](https://github.com/tttboy123/usagehub/releases/download/v0.8.7/OpenUsage-Bar-v0.8.7-macos-arm64.dmg) (available after candidate publication). -> 0.8.6 is a candidate pre-release without Developer ID notarization; Windows/Linux installers +> 0.8.7 is a candidate pre-release without Developer ID notarization; Windows/Linux installers > ship with the cross-platform release. ## Development diff --git a/README.md b/README.md index 5104622d..5d31c3a7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - - + +
# UsageHub @@ -139,7 +139,7 @@ flowchart LR 为什么菜单栏看不到图标? 菜单栏图标只在 App 运行时显示。请确认 App 已打开(或已加入登录项自动启动)。0.8.6 之前 -存在托盘图标路径硬编码导致的不可见问题,已在此版本修复:图标现在使用随包携带的品牌模板图标。 +存在托盘图标路径硬编码导致的不可见问题,已在 0.8.6 修复:图标现在使用随包携带的品牌模板图标。 @@ -188,12 +188,12 @@ xattr -dr com.apple.quarantine "/Applications/UsageHub.app" ### macOS 从 [Releases](https://github.com/tttboy123/usagehub/releases) 下载最新 -`UsageHub-0.8.6-mac-arm64.dmg`,双击打开后把 **UsageHub** 拖入 **Applications**。 +`UsageHub-0.8.7-mac-arm64.dmg`,双击打开后把 **UsageHub** 拖入 **Applications**。 首次打开自动注册登录项与后台采集器,菜单栏图标随即显示今日用量简况。 -原生 SwiftUI 版本:[OpenUsage-Bar-v0.8.6-macos-arm64.dmg](https://github.com/tttboy123/usagehub/releases/download/v0.8.6/OpenUsage-Bar-v0.8.6-macos-arm64.dmg)(候选发布后可用)。 +原生 SwiftUI 版本:[OpenUsage-Bar-v0.8.7-macos-arm64.dmg](https://github.com/tttboy123/usagehub/releases/download/v0.8.7/OpenUsage-Bar-v0.8.7-macos-arm64.dmg)(候选发布后可用)。 -> 0.8.6 为候选预发布,未做 Developer ID 公证;Windows/Linux 安装包随跨平台发布提供。 +> 0.8.7 为候选预发布,未做 Developer ID 公证;Windows/Linux 安装包随跨平台发布提供。 ## Development diff --git a/ROADMAP.md b/ROADMAP.md index 3e094b49..2d350fe5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,7 +1,7 @@ # OpenUsage Bar Roadmap - - + + OpenUsage Bar is a local-first AI usage product with two compatible layers: @@ -22,10 +22,10 @@ contract, packaging, and canary gates below pass. The existing external Canary remains **0 / 5 qualified Apple Silicon Macs** with its 30-day clock **not started**. -The repository's single candidate truth is now `0.8.6` build `28`, channel +The repository's single candidate truth is now `0.8.7` build `29`, channel `rc`, across machine state, bundle metadata, Python, Web, Desktop, release guides, and verification markers. That is candidate preparation, not evidence -that `v0.8.6` has been published or that its Canary clock has started. +that `v0.8.7` has been published or that its Canary clock has started. ## Compatibility invariants diff --git a/desktop/collector_runtime.js b/desktop/collector_runtime.js index f65dc942..ace3626d 100644 --- a/desktop/collector_runtime.js +++ b/desktop/collector_runtime.js @@ -5,7 +5,7 @@ const childProcess = require("child_process"); const path = require("path"); const MAX_RUNTIME_PATH_LENGTH = 4096; -const MAX_COLLECTOR_COMMAND_TIMEOUT_MS = 180_000; +const MAX_COLLECTOR_COMMAND_TIMEOUT_MS = 300_000; const COLLECTOR_INTERVAL_SECONDS = "1800"; const DELETE_STATE_CONFIRMATION = "DELETE-LOCAL-USAGEHUB-STATE"; const VALID_LIFECYCLE_PLAN = Symbol("validCollectorLifecyclePlan"); @@ -300,7 +300,7 @@ function runCollectorCommand( async function waitForActiveRefresh({ readStatus, wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)), - attempts = 90, + attempts = 150, intervalMs = 2_000, } = {}) { if ( @@ -308,7 +308,7 @@ async function waitForActiveRefresh({ typeof wait !== "function" || !Number.isInteger(attempts) || attempts < 1 || - attempts > 90 || + attempts > 150 || !Number.isInteger(intervalMs) || intervalMs < 100 || intervalMs > 2_000 diff --git a/desktop/main.js b/desktop/main.js index b9020438..88d3b720 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -335,7 +335,7 @@ async function refreshUsageData() { const ok = await runCollectorCommand( command, ["__refresh-current", "--ledger", ledger], - { timeoutMs: 180_000 }, + { timeoutMs: 300_000 }, ); if (ok) await refreshTraySnapshot().catch(() => {}); return { diff --git a/desktop/package-lock.json b/desktop/package-lock.json index eb95ce66..ef9f1766 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "usagehub-desktop", - "version": "0.8.6", + "version": "0.8.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "usagehub-desktop", - "version": "0.8.6", + "version": "0.8.7", "license": "Apache-2.0", "devDependencies": { "electron": "^43.2.0", diff --git a/desktop/package.json b/desktop/package.json index 221c5c67..2db44247 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "usagehub-desktop", - "version": "0.8.6", + "version": "0.8.7", "description": "UsageHub cross-platform desktop client (Electron) wrapping the local web dashboard", "main": "main.js", "scripts": { @@ -16,11 +16,11 @@ "build": { "appId": "com.lune.openusagebar", "productName": "UsageHub", - "buildVersion": "28", - "buildNumber": "28", + "buildVersion": "29", + "buildNumber": "29", "extraMetadata": { - "buildVersion": "28", - "buildNumber": "28" + "buildVersion": "29", + "buildNumber": "29" }, "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", "files": [ @@ -115,7 +115,7 @@ "category": "Utility", "desktop": { "entry": { - "X-AppImage-Version": "28" + "X-AppImage-Version": "29" } }, "extraResources": [ diff --git a/desktop/product_version_truth.js b/desktop/product_version_truth.js index 591c8c40..aafbcfcf 100644 --- a/desktop/product_version_truth.js +++ b/desktop/product_version_truth.js @@ -5,8 +5,8 @@ const productVersionTruth = Object.freeze({ displayName: "UsageHub", legacyDisplayName: "OpenUsage Bar", - candidateVersion: "0.8.6", - candidateBuild: "28", + candidateVersion: "0.8.7", + candidateBuild: "29", channel: "rc", releaseStage: "candidate", publicationStatus: "not_published", diff --git a/desktop/tests/build_identity_ui.test.mjs b/desktop/tests/build_identity_ui.test.mjs index b9091a4d..c28a465f 100644 --- a/desktop/tests/build_identity_ui.test.mjs +++ b/desktop/tests/build_identity_ui.test.mjs @@ -10,8 +10,8 @@ const require = createRequire(import.meta.url); const BASE_IDENTITY = Object.freeze({ displayName: "UsageHub", legacyDisplayName: "OpenUsage Bar", - candidateVersion: "0.8.6", - candidateBuild: "28", + candidateVersion: "0.8.7", + candidateBuild: "29", channel: "rc", releaseStage: "candidate", publicationStatus: "not_published", diff --git a/desktop/tests/collector_lifecycle.test.mjs b/desktop/tests/collector_lifecycle.test.mjs index b214b650..f1ab5809 100644 --- a/desktop/tests/collector_lifecycle.test.mjs +++ b/desktop/tests/collector_lifecycle.test.mjs @@ -103,6 +103,32 @@ test("desktop reuses an active scheduled refresh instead of launching a competit assert.equal(waits, 2); }); +test("desktop can wait through a scheduled refresh beyond the old three-minute ceiling", async () => { + const { waitForActiveRefresh } = require("../collector_runtime.js"); + let reads = 0; + let waits = 0; + + const result = await waitForActiveRefresh({ + readStatus: async () => { + reads += 1; + return reads <= 101 + ? { phase: "running", state: "attention", succeeded: null } + : { phase: "idle", state: "ok", succeeded: true }; + }, + wait: async () => { + waits += 1; + }, + attempts: 105, + intervalMs: 2_000, + }); + + assert.deepEqual(result, { + waited: true, + status: { phase: "idle", state: "ok", succeeded: true }, + }); + assert.equal(waits, 101); +}); + test("Linux invokes the packaged managed-service command and waits for readiness", { skip: process.platform === "win32", }, async (context) => { diff --git a/desktop/tests/product_version_truth.test.mjs b/desktop/tests/product_version_truth.test.mjs index e953e709..2fb9e949 100644 --- a/desktop/tests/product_version_truth.test.mjs +++ b/desktop/tests/product_version_truth.test.mjs @@ -9,8 +9,8 @@ const { formatCandidateVersion, productVersionTruth } = productVersionModule; const expectedIdentity = { displayName: "UsageHub", legacyDisplayName: "OpenUsage Bar", - candidateVersion: "0.8.6", - candidateBuild: "28", + candidateVersion: "0.8.7", + candidateBuild: "29", channel: "rc", releaseStage: "candidate", publicationStatus: "not_published", @@ -36,7 +36,7 @@ test("freezes the projection and module exports", () => { }); test("formats version and channel without embedding final UI copy", () => { - assert.equal(formatCandidateVersion(productVersionTruth), "0.8.6 RC"); + assert.equal(formatCandidateVersion(productVersionTruth), "0.8.7 RC"); assert.equal( formatCandidateVersion({ candidateVersion: "1.2.3", channel: "beta" }), "1.2.3 BETA", diff --git a/docs/agent-development-workflow.md b/docs/agent-development-workflow.md index d27dd822..09a30860 100644 --- a/docs/agent-development-workflow.md +++ b/docs/agent-development-workflow.md @@ -121,7 +121,7 @@ clean source tree on an idle reference machine. `docs/schemas/product-version-truth-v1.schema.json`; every role runs `scripts/verify_product_version_truth.py` after changing a bound surface. - Candidate identity and published identity are independent facts. The current - candidate is UsageHub 0.8.6 build 28 RC; the published stable baseline remains + candidate is UsageHub 0.8.7 build 29 RC; the published stable baseline remains v0.7.1. A candidate may never use the `stable` channel. - Product versions are not API versions. Local API `1.0`, Gateway `gateway.openusage/v1`, and runtime capability diff --git a/docs/artifact-build-identity.md b/docs/artifact-build-identity.md index 1b42c549..5c5da6bb 100644 --- a/docs/artifact-build-identity.md +++ b/docs/artifact-build-identity.md @@ -30,8 +30,8 @@ For an extracted or mounted package, `release_artifact_audit.py` performs a three-way binding among the packaged bytes, the canonical source, and product truth. It also validates native metadata: -- macOS: `UsageHub`, version `0.8.6`, build `28` in `Info.plist`; -- Windows: `UsageHub`, product version `0.8.6.28`, and a file version whose +- macOS: `UsageHub`, version `0.8.7`, build `29` in `Info.plist`; +- Windows: `UsageHub`, product version `0.8.7.29`, and a file version whose normalized build is `28` in PE `VersionInfo`; - Linux: version/build fields in `app.asar`; a final AppImage additionally has exactly one desktop entry with `Name=UsageHub` and diff --git a/docs/canary.md b/docs/canary.md index f35e89ec..6944127f 100644 --- a/docs/canary.md +++ b/docs/canary.md @@ -76,7 +76,7 @@ maintainer explicitly activates the timed cohort, the clock state is `not_started`. The repository is preparing the -[v0.8.6 RC candidate](https://github.com/tttboy123/openusage-bar/releases/tag/v0.8.6). +[v0.8.7 RC candidate](https://github.com/tttboy123/openusage-bar/releases/tag/v0.8.7). That link becomes an intake surface only after the immutable candidate is published; preparing metadata does not activate or qualify the cohort. The last published baseline remains v0.7.1. @@ -111,18 +111,18 @@ Record pass/fail and UTC date for each event: files extracted from it. Use the single candidate version below only after the candidate assets have been published and checksummed. - The current candidate version is `0.8.6`: + The current candidate version is `0.8.7`: ```bash - gh attestation verify OpenUsage-Bar-v0.8.6-macos-arm64.zip \ + gh attestation verify OpenUsage-Bar-v0.8.7-macos-arm64.zip \ --repo tttboy123/openusage-bar \ --signer-workflow \ tttboy123/openusage-bar/.github/workflows/release.yml \ - --source-ref refs/tags/v0.8.6 \ + --source-ref refs/tags/v0.8.7 \ --deny-self-hosted-runners - shasum -a 256 -c OpenUsage-Bar-v0.8.6-macos-arm64.zip.sha256 - unzip OpenUsage-Bar-v0.8.6-macos-arm64.zip - cd OpenUsage-Bar-v0.8.6-macos-arm64 - scripts/verify_canary_candidate.py --assets-dir .. --version 0.8.6 + shasum -a 256 -c OpenUsage-Bar-v0.8.7-macos-arm64.zip.sha256 + unzip OpenUsage-Bar-v0.8.7-macos-arm64.zip + cd OpenUsage-Bar-v0.8.7-macos-arm64 + scripts/verify_canary_candidate.py --assets-dir .. --version 0.8.7 ``` The packaged verifier requires the expected version and exactly one release diff --git a/docs/distribution-trust-posture.md b/docs/distribution-trust-posture.md index 12b62006..764a01fb 100644 --- a/docs/distribution-trust-posture.md +++ b/docs/distribution-trust-posture.md @@ -87,13 +87,13 @@ Inspection and immediate binding verification use: python scripts/distribution_trust_posture.py inspect \ --platform linux \ --package-root dist-desktop/linux-unpacked \ - --artifact dist-desktop/UsageHub-0.8.6-linux-x86_64.AppImage \ + --artifact dist-desktop/UsageHub-0.8.7-linux-x86_64.AppImage \ --output dist-evidence/usagehub-distribution-trust-linux-x64.json python scripts/distribution_trust_posture.py verify \ --report dist-evidence/usagehub-distribution-trust-linux-x64.json \ --platform linux \ - --artifact dist-desktop/UsageHub-0.8.6-linux-x86_64.AppImage + --artifact dist-desktop/UsageHub-0.8.7-linux-x86_64.AppImage ``` The CLI intentionally has no option for supplying signing status, diff --git a/docs/product-version-truth.md b/docs/product-version-truth.md index 0ff1d472..101bf14a 100644 --- a/docs/product-version-truth.md +++ b/docs/product-version-truth.md @@ -7,7 +7,7 @@ work. It does not bump a version or make a release claim. The current repository has two deliberately separate truths: - published stable baseline: `v0.7.1`; -- development candidate: `UsageHub 0.8.6 RC (build 28)`, not published and +- development candidate: `UsageHub 0.8.7 RC (build 29)`, not published and not release eligible. `releaseStage=candidate` describes the candidate's lifecycle. The independent diff --git a/docs/release-handoff.md b/docs/release-handoff.md index ba5d0764..dac63c82 100644 --- a/docs/release-handoff.md +++ b/docs/release-handoff.md @@ -55,7 +55,7 @@ assembly is not reported as success. python scripts/release_handoff.py assemble \ --bundle-dir dist-handoff/mac-arm64 \ --collector dist-collector/openusage-collector \ - --artifact dist-desktop/UsageHub-0.8.6-mac-arm64.dmg \ + --artifact dist-desktop/UsageHub-0.8.7-mac-arm64.dmg \ --evidence dist-evidence/usagehub-native-evidence-mac-arm64.json \ --trust-posture-report \ dist-evidence/usagehub-distribution-trust-mac-arm64.json diff --git a/docs/release-quick-start.md b/docs/release-quick-start.md index b91aafd6..1a77111d 100644 --- a/docs/release-quick-start.md +++ b/docs/release-quick-start.md @@ -1,13 +1,13 @@ # UsageHub 安装指南 / Install guide -UsageHub 0.8.6 RC 候选版支持 Apple Silicon Mac 和 macOS 15 或更高版本。当前 +UsageHub 0.8.7 RC 候选版支持 Apple Silicon Mac 和 macOS 15 或更高版本。当前 发布形态为桌面客户端(Electron 封装本地 Web 仪表盘),原生 SwiftUI 菜单栏 版本同步维护。 ## 图形化安装(推荐,桌面客户端) -1. 从 [v0.8.6 发布页](https://github.com/tttboy123/openusage-bar/releases/tag/v0.8.6) - 下载 `UsageHub-0.8.6-mac-arm64.dmg`。 +1. 从 [v0.8.7 发布页](https://github.com/tttboy123/usagehub/releases/tag/v0.8.7) + 下载 `UsageHub-0.8.7-mac-arm64.dmg`。 2. 双击 DMG,将 **UsageHub** 拖入 **Applications**。 3. 在访达“应用程序”中打开。App 会自动注册登录项和内置采集器,菜单栏图标 随即显示今日用量简况。 @@ -20,7 +20,7 @@ UsageHub 0.8.6 RC 候选版支持 Apple Silicon Mac 和 macOS 15 或更高版本 ## Install (English) -After candidate publication, download the v0.8.6 DMG, open it, drag +After candidate publication, download the v0.8.7 DMG, open it, drag **UsageHub** to **Applications**, then open it from Finder. The app registers its login item and bundled collector on first launch and shows today's usage summary in the menu bar. If macOS says the app is damaged, verify the download @@ -33,19 +33,19 @@ Login Items** if macOS requests background approval. 将 DMG 和 `.dmg.sha256` 放在同一目录后执行: ```bash -shasum -a 256 -c UsageHub-0.8.6-mac-arm64.dmg.sha256 +shasum -a 256 -c UsageHub-0.8.7-mac-arm64.dmg.sha256 ``` ## 原生 SwiftUI 版本(可选) 原生菜单栏版本仍以 `OpenUsage Bar.app` 分发,产物名为 -`OpenUsage-Bar-v0.8.6-macos-arm64.dmg`(候选发布后可用)。其 ZIP 中附带 +`OpenUsage-Bar-v0.8.7-macos-arm64.dmg`(候选发布后可用)。其 ZIP 中附带 事务式安装、回滚和卸载工具: ```bash -shasum -a 256 -c OpenUsage-Bar-v0.8.6-macos-arm64.zip.sha256 -unzip OpenUsage-Bar-v0.8.6-macos-arm64.zip -cd OpenUsage-Bar-v0.8.6-macos-arm64 +shasum -a 256 -c OpenUsage-Bar-v0.8.7-macos-arm64.zip.sha256 +unzip OpenUsage-Bar-v0.8.7-macos-arm64.zip +cd OpenUsage-Bar-v0.8.7-macos-arm64 scripts/install_app.sh ``` @@ -73,7 +73,7 @@ readiness inventory: python scripts/release_readiness.py --output /tmp/openusage-release-readiness.json ``` -The current `0.8.6` candidate intentionally reports +The current `0.8.7` candidate intentionally reports `release_readiness_blocked`: local product/version and build-identity checks pass, but `releaseEligible=false` and the required external evidence is still missing. The report is path-free and fail-closed, but it is not a release @@ -148,15 +148,15 @@ GitHub CLI. Then the packaged candidate verifier checks the manifest, SBOM, checksums, every release asset and all attestations: ```bash -gh attestation verify OpenUsage-Bar-v0.8.6-macos-arm64.zip \ +gh attestation verify OpenUsage-Bar-v0.8.7-macos-arm64.zip \ --repo tttboy123/openusage-bar \ --signer-workflow tttboy123/openusage-bar/.github/workflows/release.yml \ - --source-ref refs/tags/v0.8.6 \ + --source-ref refs/tags/v0.8.7 \ --deny-self-hosted-runners -shasum -a 256 -c OpenUsage-Bar-v0.8.6-macos-arm64.zip.sha256 -unzip OpenUsage-Bar-v0.8.6-macos-arm64.zip -cd OpenUsage-Bar-v0.8.6-macos-arm64 -scripts/verify_canary_candidate.py --assets-dir .. --version 0.8.6 +shasum -a 256 -c OpenUsage-Bar-v0.8.7-macos-arm64.zip.sha256 +unzip OpenUsage-Bar-v0.8.7-macos-arm64.zip +cd OpenUsage-Bar-v0.8.7-macos-arm64 +scripts/verify_canary_candidate.py --assets-dir .. --version 0.8.7 ``` After installing the verified candidate, a tester may explicitly create a diff --git a/openusage_bar/bundle_config.py b/openusage_bar/bundle_config.py index c98f5030..f7cb9275 100644 --- a/openusage_bar/bundle_config.py +++ b/openusage_bar/bundle_config.py @@ -5,8 +5,8 @@ APP_BUNDLE_PATH = "/Applications/OpenUsage Bar.app" BUNDLE_ID = "com.lune.openusagebar.settings" LAUNCH_AGENT_LABEL = "com.lune.openusagebar.collector" -APP_VERSION = "0.8.6" -BUILD_VERSION = "28" +APP_VERSION = "0.8.7" +BUILD_VERSION = "29" def info_plist() -> dict[str, object]: diff --git a/openusage_bar/codex_app_server.py b/openusage_bar/codex_app_server.py index 72f9839c..2a0bd71b 100644 --- a/openusage_bar/codex_app_server.py +++ b/openusage_bar/codex_app_server.py @@ -157,7 +157,7 @@ def run_codex_app_server_helper( "id": 1, "method": "initialize", "params": { - "clientInfo": {"name": "usagehub", "version": "0.8.6"} + "clientInfo": {"name": "usagehub", "version": "0.8.7"} }, }, ) diff --git a/openusage_bar/collector_cli.py b/openusage_bar/collector_cli.py index 294f145d..a993f26b 100644 --- a/openusage_bar/collector_cli.py +++ b/openusage_bar/collector_cli.py @@ -67,6 +67,34 @@ def refresh(self) -> None: return None +class LazyRefresher: + """Build credential-owning adapters only when a refresh is requested.""" + + def __init__(self, factory: Callable[[], Any]) -> None: + if not callable(factory): + raise ValueError("refresh factory unavailable") + self._factory = factory + self._delegate: Any | None = None + self._lock = threading.Lock() + + def _resolved(self) -> Any: + delegate = self._delegate + if delegate is not None: + return delegate + with self._lock: + delegate = self._delegate + if delegate is None: + try: + delegate = self._factory() + except Exception: + delegate = UnavailableRefresher() + self._delegate = delegate + return delegate + + def refresh(self, *args: Any, **kwargs: Any) -> Any: + return self._resolved().refresh(*args, **kwargs) + + def build_default_refresher( store: ActivityStore, *, @@ -892,6 +920,8 @@ def _run_daemon( refresh_coordinator: Any | None = None, ) -> int: while not stop_event.is_set(): + if waiter(interval): + break if catalog_monitor is not None: try: catalog_monitor.maybe_run() @@ -904,8 +934,6 @@ def _run_daemon( refresh_coordinator.run_blocking() except Exception: stderr.write("refresh unavailable; retained last-good ledger data\n") - if waiter(interval): - break return 0 @@ -1700,10 +1728,7 @@ def main( catalog_monitor = OpenUsageCatalogMonitor(active_store, clock=clock) if refresher is None: factory = refresher_factory or build_default_refresher - try: - refresher = factory(active_store) - except Exception: - refresher = UnavailableRefresher() + refresher = LazyRefresher(lambda: factory(active_store)) active_stop = stop_event or threading.Event() if stop_event is None and threading.current_thread() is threading.main_thread(): def stop(*_: object) -> None: diff --git a/openusage_bar/resources/artifact-build-identity.v1.json b/openusage_bar/resources/artifact-build-identity.v1.json index f5213b14..4f60b50d 100644 --- a/openusage_bar/resources/artifact-build-identity.v1.json +++ b/openusage_bar/resources/artifact-build-identity.v1.json @@ -2,8 +2,8 @@ "canaryClock": "not_started", "canaryQualifiedMachines": 0, "canaryTargetMachines": 5, - "candidateBuild": "28", - "candidateVersion": "0.8.6", + "candidateBuild": "29", + "candidateVersion": "0.8.7", "channel": "rc", "displayName": "UsageHub", "publicationStatus": "not_published", diff --git a/openusage_bar/resources/product-version-truth.v1.json b/openusage_bar/resources/product-version-truth.v1.json index 693fb664..baaa2b8b 100644 --- a/openusage_bar/resources/product-version-truth.v1.json +++ b/openusage_bar/resources/product-version-truth.v1.json @@ -10,8 +10,8 @@ "bundleIdentifier": "com.lune.openusagebar" }, "candidate": { - "version": "0.8.6", - "build": "28", + "version": "0.8.7", + "build": "29", "channel": "rc", "releaseStage": "candidate", "publicationStatus": "not_published", diff --git a/openusage_bar/resources/release-state.v1.json b/openusage_bar/resources/release-state.v1.json index ac3b1dba..3f891e62 100644 --- a/openusage_bar/resources/release-state.v1.json +++ b/openusage_bar/resources/release-state.v1.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, - "currentVersion": "0.8.6", - "buildVersion": "28", + "currentVersion": "0.8.7", + "buildVersion": "29", "channel": "rc", "apiVersion": "1.0", "canary": { diff --git a/scripts/canary_linux_observer_topology.py b/scripts/canary_linux_observer_topology.py index 6f9d65a9..478b72fb 100644 --- a/scripts/canary_linux_observer_topology.py +++ b/scripts/canary_linux_observer_topology.py @@ -413,7 +413,7 @@ def _observe_runtime(*, remaining_timeout) -> LinuxObserverRuntimeFact: expected_collector, "daemon", "--interval", - "300", + "1800", "--api-transport", "unix", "--api-socket", @@ -423,7 +423,7 @@ def _observe_runtime(*, remaining_timeout) -> LinuxObserverRuntimeFact: from openusage_bar.platform_services import systemd_unit expected_unit = systemd_unit( - interval=300, + interval=1800, api_socket=expected_socket, command=expected_collector, ).encode("utf-8") diff --git a/setup.py b/setup.py index 77ff6e93..51d5c1ea 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ common = { "name": "openusage-bar", - "version": "0.8.6", + "version": "0.8.7", "description": ( "UsageHub (formerly OpenUsage Bar): local-first AI usage ledger " "and observation component for schedulers and native clients." diff --git a/swift_app/Resources/OpenUsageActivity-Info.plist b/swift_app/Resources/OpenUsageActivity-Info.plist index 5a2f9088..0b06f595 100644 --- a/swift_app/Resources/OpenUsageActivity-Info.plist +++ b/swift_app/Resources/OpenUsageActivity-Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.8.6 + 0.8.7 CFBundleVersion - 28 + 29 LSMinimumSystemVersion 15.0 NSHighResolutionCapable diff --git a/swift_app/Resources/OpenUsageBar-Info.plist b/swift_app/Resources/OpenUsageBar-Info.plist index 9e488d1f..8fb28dc6 100644 --- a/swift_app/Resources/OpenUsageBar-Info.plist +++ b/swift_app/Resources/OpenUsageBar-Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.8.6 + 0.8.7 CFBundleVersion - 28 + 29 LSMinimumSystemVersion 15.0 LSUIElement diff --git a/swift_app/Resources/OpenUsageProviderSettings-Info.plist b/swift_app/Resources/OpenUsageProviderSettings-Info.plist index 716bf269..e2613138 100644 --- a/swift_app/Resources/OpenUsageProviderSettings-Info.plist +++ b/swift_app/Resources/OpenUsageProviderSettings-Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.8.6 + 0.8.7 CFBundleVersion - 28 + 29 LSMinimumSystemVersion 15.0 NSHighResolutionCapable diff --git a/swift_app/Resources/product-build-identity.v1.json b/swift_app/Resources/product-build-identity.v1.json index f5213b14..4f60b50d 100644 --- a/swift_app/Resources/product-build-identity.v1.json +++ b/swift_app/Resources/product-build-identity.v1.json @@ -2,8 +2,8 @@ "canaryClock": "not_started", "canaryQualifiedMachines": 0, "canaryTargetMachines": 5, - "candidateBuild": "28", - "candidateVersion": "0.8.6", + "candidateBuild": "29", + "candidateVersion": "0.8.7", "channel": "rc", "displayName": "UsageHub", "publicationStatus": "not_published", diff --git a/swift_app/Sources/UsageCore/ProductVersionTruth.swift b/swift_app/Sources/UsageCore/ProductVersionTruth.swift index be085b18..a670df50 100644 --- a/swift_app/Sources/UsageCore/ProductVersionTruth.swift +++ b/swift_app/Sources/UsageCore/ProductVersionTruth.swift @@ -16,8 +16,8 @@ public struct ProductVersionTruth: Equatable, Sendable { public static let current = ProductVersionTruth( displayName: "UsageHub", legacyDisplayName: "OpenUsage Bar", - candidateVersion: "0.8.6", - candidateBuild: "28", + candidateVersion: "0.8.7", + candidateBuild: "29", channel: "rc", releaseStage: "candidate", publicationStatus: "not_published", diff --git a/swift_app/Tests/OpenUsageActivityTests/ProductBuildIdentityTests.swift b/swift_app/Tests/OpenUsageActivityTests/ProductBuildIdentityTests.swift index df2f28ff..aabd1171 100644 --- a/swift_app/Tests/OpenUsageActivityTests/ProductBuildIdentityTests.swift +++ b/swift_app/Tests/OpenUsageActivityTests/ProductBuildIdentityTests.swift @@ -15,7 +15,7 @@ struct ProductBuildIdentityTests { stage: stage, publication: publication, eligible: eligible )) - #expect(copy.versionAndBuild == "0.8.6 RC (build 28)") + #expect(copy.versionAndBuild == "0.8.7 RC (build 29)") #expect(copy.lifecycle == expected) #expect(copy.published == "Published stable: v0.7.1") #expect(copy.accessibilityLabel.contains(expected)) @@ -95,8 +95,8 @@ struct ProductBuildIdentityTests { ProductVersionTruth( displayName: "UsageHub", legacyDisplayName: "OpenUsage Bar", - candidateVersion: "0.8.6", - candidateBuild: "28", + candidateVersion: "0.8.7", + candidateBuild: "29", channel: "rc", releaseStage: stage, publicationStatus: publication, diff --git a/swift_app/Tests/UsageCoreTests/ProductVersionTruthTests.swift b/swift_app/Tests/UsageCoreTests/ProductVersionTruthTests.swift index c7fe52ff..913f123a 100644 --- a/swift_app/Tests/UsageCoreTests/ProductVersionTruthTests.swift +++ b/swift_app/Tests/UsageCoreTests/ProductVersionTruthTests.swift @@ -9,8 +9,8 @@ struct ProductVersionTruthTests { #expect(identity.displayName == "UsageHub") #expect(identity.legacyDisplayName == "OpenUsage Bar") - #expect(identity.candidateVersion == "0.8.6") - #expect(identity.candidateBuild == "28") + #expect(identity.candidateVersion == "0.8.7") + #expect(identity.candidateBuild == "29") #expect(identity.channel == "rc") #expect(identity.releaseStage == "candidate") #expect(identity.publicationStatus == "not_published") @@ -24,7 +24,7 @@ struct ProductVersionTruthTests { @Test("Formats version and channel without embedding final UI copy") func formatsCandidateVersion() { - #expect(ProductVersionTruth.current.formattedCandidateVersion == "0.8.6 RC") + #expect(ProductVersionTruth.current.formattedCandidateVersion == "0.8.7 RC") #expect( ProductVersionTruth.formatCandidateVersion(version: "1.2.3", channel: "beta") == "1.2.3 BETA" diff --git a/tests/test_aggregator.py b/tests/test_aggregator.py index 37fa8823..f72824ad 100644 --- a/tests/test_aggregator.py +++ b/tests/test_aggregator.py @@ -1,4 +1,5 @@ import json +import os import subprocess import sys import tempfile @@ -137,6 +138,10 @@ def fetch_current(self): self.assertTrue(result.cards[0].stale) self.assertEqual(result.cards[0].status, ProviderStatus.STALE) + @unittest.skipUnless( + os.name == "posix" and hasattr(os, "fork"), + "POSIX process-group test", + ) def test_real_overflow_is_bounded_and_process_group_is_reaped(self): with tempfile.TemporaryDirectory() as directory: helper = Path(directory) / "security-helper" @@ -173,6 +178,10 @@ def test_real_overflow_is_bounded_and_process_group_is_reaped(self): time.sleep(0.05) self.assertEqual(state, "") + @unittest.skipUnless( + os.name == "posix" and hasattr(os, "fork"), + "POSIX process-group test", + ) def test_real_silent_timeout_is_killed_and_reaped(self): with tempfile.TemporaryDirectory() as directory: helper = Path(directory) / "security-helper" diff --git a/tests/test_artifact_build_identity.py b/tests/test_artifact_build_identity.py index fbcf0a7c..76fe6d44 100644 --- a/tests/test_artifact_build_identity.py +++ b/tests/test_artifact_build_identity.py @@ -108,11 +108,11 @@ def test_web_and_swift_package_the_exact_canonical_bytes_at_fixed_paths(self): self.assertIn("artifact-build-identity.v1.json", build_script) self.assertIn("Contents/Resources/product-build-identity.v1.json", build_script) - def test_electron_maps_identity_to_resources_and_native_build_28(self): + def test_electron_maps_identity_to_resources_and_native_build_29(self): package = json.loads((ROOT / "desktop/package.json").read_text(encoding="utf-8")) build = package["build"] - self.assertEqual(build["buildVersion"], "28") + self.assertEqual(build["buildVersion"], "29") mappings = build["extraResources"] self.assertIn( { diff --git a/tests/test_bundle_config.py b/tests/test_bundle_config.py index ca50e759..b4d97455 100644 --- a/tests/test_bundle_config.py +++ b/tests/test_bundle_config.py @@ -43,7 +43,7 @@ def test_info_plist_defines_stable_agent_application(self): def test_all_three_bundles_share_the_canonical_version(self): expected = (APP_VERSION, BUILD_VERSION) - self.assertEqual(expected, ("0.8.6", "28")) + self.assertEqual(expected, ("0.8.7", "29")) self.assertEqual( (info_plist()["CFBundleShortVersionString"], info_plist()["CFBundleVersion"]), expected, diff --git a/tests/test_canary_linux_observer_topology.py b/tests/test_canary_linux_observer_topology.py index 9137f16f..8f0fcd7e 100644 --- a/tests/test_canary_linux_observer_topology.py +++ b/tests/test_canary_linux_observer_topology.py @@ -596,7 +596,7 @@ def _runtime_state(): str(executable), "daemon", "--interval", - "300", + "1800", "--api-transport", "unix", "--api-socket", @@ -607,7 +607,7 @@ def _runtime_state(): from openusage_bar.platform_services import systemd_unit unit_bytes = systemd_unit( - interval=300, + interval=1800, api_socket=str(socket_path), command=str(executable), ).encode("utf-8") diff --git a/tests/test_codex_subscription.py b/tests/test_codex_subscription.py index e0f1a235..a2a0fca4 100644 --- a/tests/test_codex_subscription.py +++ b/tests/test_codex_subscription.py @@ -110,8 +110,8 @@ def test_app_server_outer_bounded_process_receives_allowlisted_environment(self) from openusage_bar.codex_app_server import read_codex_app_server_rate_limits read_codex_app_server_rate_limits( - codex_command=("/bin/sh",), - helper_command=("/bin/sh",), + codex_command=(sys.executable,), + helper_command=(sys.executable,), environment={ "HOME": "/tmp/home", "PATH": "/usr/bin", diff --git a/tests/test_collector_cli.py b/tests/test_collector_cli.py index bf0255fc..e1b3ee69 100644 --- a/tests/test_collector_cli.py +++ b/tests/test_collector_cli.py @@ -2041,7 +2041,7 @@ def close(): self.assertEqual(code, 0) self.assertTrue(closed.is_set()) - def test_daemon_immediate_repeat_clean_stop_and_non_overlap(self): + def test_daemon_waits_before_each_repeat_clean_stop_and_non_overlap(self): refresher = FakeRefresher() class CatalogMonitor: calls = 0 @@ -2057,7 +2057,7 @@ def wait(seconds): nonlocal waits self.assertEqual(seconds, 60) waits += 1 - if waits == 2: + if waits == 3: stop.set() return stop.is_set() @@ -2072,6 +2072,81 @@ def wait(seconds): self.assertEqual(refresher.max_active, 1) self.assertEqual(catalog_monitor.calls, 2) + def test_daemon_stop_during_initial_interval_performs_no_refresh(self): + refresher = FakeRefresher() + stop = threading.Event() + + def wait(seconds): + self.assertEqual(seconds, 60) + stop.set() + return True + + with tempfile.TemporaryDirectory() as directory: + code, out, err = self.run_cli( + ["daemon", "--interval", "60", "--api-socket", str(Path(directory) / "api.sock")], + refresher=refresher, + stop_event=stop, + waiter=wait, + ) + + self.assertEqual((code, out, err), (0, "", "")) + self.assertEqual(refresher.calls, 0) + + def test_daemon_defers_refresher_factory_until_after_initial_interval(self): + stop = threading.Event() + factory_calls = [] + built = FakeRefresher() + + def factory(_store): + factory_calls.append(True) + return built + + def stop_before_interval(_seconds): + stop.set() + return True + + with tempfile.TemporaryDirectory() as directory: + code, out, err = self.run_cli( + ["daemon", "--interval", "60", "--api-socket", str(Path(directory) / "api.sock")], + refresher_factory=factory, + stop_event=stop, + waiter=stop_before_interval, + ) + + self.assertEqual((code, out, err), (0, "", "")) + self.assertEqual(factory_calls, []) + self.assertEqual(built.calls, 0) + + def test_daemon_builds_refresher_once_for_due_repeats(self): + stop = threading.Event() + factory_calls = [] + built = FakeRefresher() + waits = 0 + + def factory(_store): + factory_calls.append(True) + return built + + def wait(_seconds): + nonlocal waits + waits += 1 + if waits == 3: + stop.set() + return True + return False + + with tempfile.TemporaryDirectory() as directory: + code, out, err = self.run_cli( + ["daemon", "--interval", "60", "--api-socket", str(Path(directory) / "api.sock")], + refresher_factory=factory, + stop_event=stop, + waiter=wait, + ) + + self.assertEqual((code, out, err), (0, "", "")) + self.assertEqual(factory_calls, [True]) + self.assertEqual(built.calls, 2) + def test_daemon_rejects_zero_bool_and_too_small_interval(self): for interval in ("0", "1", "true"): code, out, err = self.run_cli(["daemon", "--interval", interval], refresher=FakeRefresher()) @@ -2096,6 +2171,16 @@ def refresh_current(self): stop = threading.Event() with tempfile.TemporaryDirectory() as directory: + waits = 0 + + def wait(_seconds): + nonlocal waits + waits += 1 + if waits == 2: + stop.set() + return True + return False + code, out, err = self.run_cli( [ "daemon", @@ -2106,7 +2191,7 @@ def refresh_current(self): ], refresher=refresher, stop_event=stop, - waiter=lambda _seconds: True, + waiter=wait, ) self.assertEqual((code, out, err), (0, "", "")) diff --git a/tests/test_desktop_packaging_contract.py b/tests/test_desktop_packaging_contract.py index 9a00b9f6..8518a535 100644 --- a/tests/test_desktop_packaging_contract.py +++ b/tests/test_desktop_packaging_contract.py @@ -639,7 +639,7 @@ def test_ci_wires_architecture_and_deterministic_artifact_paths(self): (DESKTOP / "package.json").read_text(encoding="utf-8") ) forbidden_version_literals = tuple(dict.fromkeys(( - "0.8.6", + "0.8.7", str(package["version"]), ))) observed = { @@ -756,7 +756,7 @@ def test_identity_source_gate_rejects_desktop_build_configuration_drift(self): self.assertEqual( baseline.stdout, "artifact_build_identity_ok product=UsageHub " - "candidate=0.8.6 build=28\n", + "candidate=0.8.7 build=29\n", ) cases = ( "buildVersion", @@ -774,11 +774,11 @@ def test_identity_source_gate_rejects_desktop_build_configuration_drift(self): package = json.loads(package_path.read_text(encoding="utf-8")) build = package["build"] if case == "buildVersion": - build["buildVersion"] = "29" + build["buildVersion"] = "30" elif case == "buildNumber": - build["buildNumber"] = "29" + build["buildNumber"] = "30" elif case == "extraMetadata": - build["extraMetadata"]["buildNumber"] = "29" + build["extraMetadata"]["buildNumber"] = "30" elif case == "extraResources": build["extraResources"] = [ value diff --git a/tests/test_native_ci_evidence.py b/tests/test_native_ci_evidence.py index a21c313f..0f13bdec 100644 --- a/tests/test_native_ci_evidence.py +++ b/tests/test_native_ci_evidence.py @@ -31,15 +31,15 @@ BUILD_IDENTITY = ROOT / "openusage_bar/resources/artifact-build-identity.v1.json" PRODUCT_TRUTH = ROOT / "openusage_bar/resources/product-version-truth.v1.json" EXPECTED_BUILD_IDENTITY_SHA256 = ( - "73aafbd28ea8cc553a5dc14c3610c8455964c39eaaafe66dc468d6b37ee16c56" + "7261f8782257a2051bc378150d884002e29b8b31bde99cdadb351e5e39986147" ) EXPECTED_BUILD_IDENTITY_SIZE = 424 EXPECTED_IDENTITY = { "canaryClock": "not_started", "canaryQualifiedMachines": 0, "canaryTargetMachines": 5, - "candidateBuild": "28", - "candidateVersion": "0.8.6", + "candidateBuild": "29", + "candidateVersion": "0.8.7", "channel": "rc", "displayName": "UsageHub", "publicationStatus": "not_published", @@ -82,17 +82,17 @@ } TARGET_FILES = { - ("mac", "x64"): ("openusage-collector", "UsageHub-0.8.6-mac-x64.dmg"), - ("mac", "arm64"): ("openusage-collector", "UsageHub-0.8.6-mac-arm64.dmg"), - ("win", "x64"): ("openusage-collector.exe", "UsageHub-0.8.6-win-x64.exe"), - ("win", "arm64"): ("openusage-collector.exe", "UsageHub-0.8.6-win-arm64.exe"), + ("mac", "x64"): ("openusage-collector", "UsageHub-0.8.7-mac-x64.dmg"), + ("mac", "arm64"): ("openusage-collector", "UsageHub-0.8.7-mac-arm64.dmg"), + ("win", "x64"): ("openusage-collector.exe", "UsageHub-0.8.7-win-x64.exe"), + ("win", "arm64"): ("openusage-collector.exe", "UsageHub-0.8.7-win-arm64.exe"), ("linux", "x64"): ( "openusage-collector", - "UsageHub-0.8.6-linux-x86_64.AppImage", + "UsageHub-0.8.7-linux-x86_64.AppImage", ), ("linux", "arm64"): ( "openusage-collector", - "UsageHub-0.8.6-linux-arm64.AppImage", + "UsageHub-0.8.7-linux-arm64.AppImage", ), } @@ -370,7 +370,7 @@ def _generate_command( "--product-truth", str(product_truth), "--version", - "0.8.6", + "0.8.7", "--source-sha", "a" * 40, "--source-event", @@ -515,7 +515,7 @@ def test_payload_rejects_lstat_to_read_same_path_replacement(self): root = Path(directory) collector = root / "openusage-collector" replacement = root / "private-replacement-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "usagehub-native-evidence-mac-x64.json" invalid_collector = b"invalid-original-collector" collector.write_bytes(invalid_collector) @@ -916,7 +916,7 @@ def test_generate_rejects_hostile_or_crossed_native_lifecycle_evidence(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector.exe" - artifact = root / "UsageHub-0.8.6-win-x64.exe" + artifact = root / "UsageHub-0.8.7-win-x64.exe" source_evidence = root / "observer-source.json" lifecycle_evidence = root / "native-lifecycle.json" _write_native_row_files( @@ -1090,7 +1090,7 @@ def test_macos_keeps_the_n_minus_one_shape_and_rejects_source_evidence(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "native-mac.json" source_evidence = root / "observer-source.json" _write_native_row_files( @@ -1185,7 +1185,7 @@ def test_generate_rejects_noncanonical_private_and_platform_swapped_source_evide with self.subTest(case=name), tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector.exe" - artifact = root / "UsageHub-0.8.6-win-x64.exe" + artifact = root / "UsageHub-0.8.7-win-x64.exe" evidence = root / "native-win.json" source_evidence = root / "observer-source.json" _write_native_row_files( @@ -1294,7 +1294,7 @@ def test_verify_rejects_forged_private_swapped_or_noncanonical_embedded_source_e with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector.exe" - artifact = root / "UsageHub-0.8.6-win-x64.exe" + artifact = root / "UsageHub-0.8.7-win-x64.exe" evidence = root / "native-win.json" source_evidence = root / "observer-source.json" _write_native_row_files( @@ -1394,7 +1394,7 @@ def test_generate_writes_canonical_path_free_evidence_and_verify_accepts_it(self with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "usagehub-native-evidence-mac-x64.json" _write_macos_x64_collector(collector) _write_dmg(artifact) @@ -1475,7 +1475,7 @@ def test_generate_rejects_noncanonical_build_identity_bytes(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "usagehub-native-evidence-mac-x64.json" tampered_identity = root / "artifact-build-identity.v1.json" _write_macos_x64_collector(collector) @@ -1508,7 +1508,7 @@ def test_verify_rejects_build_identity_digest_size_and_identity_tamper(self): with self.subTest(case=case), tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "usagehub-native-evidence-mac-x64.json" _write_macos_x64_collector(collector) _write_dmg(artifact) @@ -1560,7 +1560,7 @@ def test_verify_rejects_wrong_json_types_without_a_traceback_or_path_leak(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "usagehub-native-evidence-mac-x64.json" _write_macos_x64_collector(collector) _write_dmg(artifact) @@ -1607,7 +1607,7 @@ def test_verify_rejects_a_forged_distribution_release_claim(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "usagehub-native-evidence-mac-x64.json" _write_macos_x64_collector(collector) _write_dmg(artifact) @@ -1654,7 +1654,7 @@ def test_verify_rejects_float_schema_version_even_when_it_compares_equal(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "usagehub-native-evidence-mac-x64.json" _write_macos_x64_collector(collector) _write_dmg(artifact) @@ -1699,7 +1699,7 @@ def test_generate_rejects_pe32_collector_when_contract_requires_pe32_plus(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector.exe" - artifact = root / "UsageHub-0.8.6-win-x64.exe" + artifact = root / "UsageHub-0.8.7-win-x64.exe" evidence = root / "usagehub-native-evidence-win-x64.json" source_evidence = root / "observer-source.json" _write_pe(collector, arch="x64", pe_plus=False) @@ -1802,7 +1802,7 @@ def test_verify_rejects_final_container_hash_drift(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "evidence.json" _write_macho(collector, arch="x64") _write_dmg(artifact) @@ -1846,7 +1846,7 @@ def test_generate_rejects_symlinked_collector_without_echoing_target(self): root = Path(directory) real_collector = root / "missing-private-user-collector" collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "evidence.json" collector.symlink_to(real_collector) _write_dmg(artifact) @@ -1875,7 +1875,7 @@ def test_verify_rejects_a_valid_but_swapped_trust_posture_report(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "evidence.json" _write_macho(collector, arch="x64") _write_dmg(artifact) @@ -1927,7 +1927,7 @@ def test_verify_rejects_duplicate_json_keys(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "evidence.json" _write_macho(collector, arch="x64") _write_dmg(artifact) @@ -1955,7 +1955,7 @@ def test_verify_rejects_deep_json_without_a_traceback(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) collector = root / "openusage-collector" - artifact = root / "UsageHub-0.8.6-mac-x64.dmg" + artifact = root / "UsageHub-0.8.7-mac-x64.dmg" evidence = root / "evidence.json" _write_macho(collector, arch="x64") _write_dmg(artifact) diff --git a/tests/test_native_lifecycle_evidence.py b/tests/test_native_lifecycle_evidence.py index 2815aee2..8ad791c4 100644 --- a/tests/test_native_lifecycle_evidence.py +++ b/tests/test_native_lifecycle_evidence.py @@ -26,7 +26,7 @@ def windows_x64_record() -> dict[str, object]: "serviceManager": "task_scheduler", }, "artifact": { - "name": "UsageHub-0.8.6-win-x64.exe", + "name": "UsageHub-0.8.7-win-x64.exe", "sha256": "b" * 64, "sizeBytes": 128, }, diff --git a/tests/test_native_lifecycle_runner.py b/tests/test_native_lifecycle_runner.py index 43a89075..d990a667 100644 --- a/tests/test_native_lifecycle_runner.py +++ b/tests/test_native_lifecycle_runner.py @@ -672,7 +672,7 @@ def __init__( ) self.root = root - self.artifact = root / "UsageHub-0.8.6-win-x64.exe" + self.artifact = root / "UsageHub-0.8.7-win-x64.exe" self.artifact.write_bytes(b"audited final NSIS container") self.artifact_sha256 = hashlib.sha256(self.artifact.read_bytes()).hexdigest() self.run_directory = root / "native-run" @@ -986,7 +986,7 @@ def dependency_context() -> DependencyContext: artifact = ( Path(tempfile.gettempdir()).resolve() - / "UsageHub-0.8.6-linux-x86_64.AppImage" + / "UsageHub-0.8.7-linux-x86_64.AppImage" ) with patch( "scripts.native_lifecycle_evidence.native_lifecycle_dependencies_for_host", @@ -1117,7 +1117,7 @@ def test_linux_host_copy_file_creates_and_observes_one_private_execution_copy( "5d228523ef8526d2b117df416820dcc217474675aff07b8ed553a766b8377088" ) with tempfile.TemporaryDirectory() as directory: - source = Path(directory) / "UsageHub-0.8.6-linux-x86_64.AppImage" + source = Path(directory) / "UsageHub-0.8.7-linux-x86_64.AppImage" source.write_bytes(source_bytes) source.chmod(0o644) source_before = source.lstat() @@ -1230,7 +1230,7 @@ def test_linux_host_start_and_stop_own_one_bound_execution_process_group( xauthority.write_bytes(b"private X authority") xauthority.chmod(0o600) authority = LifecycleStatePaths(platform="linux", home=home) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited process lease artifact") events: list[object] = [] service_state, local_state, command = _linux_positive_listener_facts(home) @@ -1459,7 +1459,7 @@ def test_linux_host_start_reaps_a_spawned_group_when_identity_binding_fails( xauthority.write_bytes(b"private X authority") xauthority.chmod(0o600) authority = LifecycleStatePaths(platform="linux", home=home) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited failed process lease artifact") events: list[object] = [] reserved_attempts = [0] @@ -1595,7 +1595,7 @@ def test_linux_host_context_reaps_the_private_lease_after_public_binding_drift( xauthority.write_bytes(b"private X authority") xauthority.chmod(0o600) authority = LifecycleStatePaths(platform="linux", home=home) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited drift process lease artifact") events: list[object] = [] @@ -1915,7 +1915,7 @@ def test_linux_host_run_process_allows_only_one_proven_preserve_uninstall( state_root.mkdir(parents=True) runtime_parent = home / ".local" / "share" / "usagehub" runtime_parent.mkdir(parents=True) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact_bytes = b"audited preserve-uninstall artifact" artifact.write_bytes(artifact_bytes) artifact_sha256 = hashlib.sha256(artifact_bytes).hexdigest() @@ -2421,7 +2421,7 @@ def test_linux_host_preserve_uninstall_rejects_a_rebound_public_run_root( home = root / "authoritative-home" home.mkdir() authority = LifecycleStatePaths(platform="linux", home=home) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited run-root binding artifact") context = None run_directory: Path | None = None @@ -2563,7 +2563,7 @@ def test_linux_host_completed_preserve_transaction_releases_private_run_root( ) (home / ".local" / "share" / "usagehub").mkdir(parents=True) authority = LifecycleStatePaths(platform="linux", home=home) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited completed preserve artifact") service_absent = LinuxCollectorServiceAbsenceState( unit_missing=True, @@ -2690,7 +2690,7 @@ def test_linux_host_delete_data_runs_only_for_the_second_bound_execution_generat (state_root / "activity.sqlite3").write_bytes(b"PRIVATE_LEDGER") (config_root / "settings.json").write_bytes(b"PRIVATE_CONFIG") authority = LifecycleStatePaths(platform="linux", home=home) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited delete-data artifact") service_absent = LinuxCollectorServiceAbsenceState( unit_missing=True, @@ -2860,7 +2860,7 @@ def _assert_linux_delete_data_rejects_recreated_root( config_parent.mkdir(parents=True) runtime_parent.mkdir(parents=True) authority = authority_type(platform="linux", home=home) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited delete-data race artifact") service_absent = service_absence_type( unit_missing=True, @@ -3134,7 +3134,7 @@ def test_linux_host_preserve_completion_reproves_socket_absence_after_service_sa state_root.mkdir(parents=True) (home / ".local" / "share" / "usagehub").mkdir(parents=True) authority = LifecycleStatePaths(platform="linux", home=home) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited preserve race artifact") socket_path = state_root / "openusage.sock" socket_bytes = b"PRIVATE_CONCURRENT_SOCKET_ENTRY" @@ -3261,7 +3261,7 @@ def test_linux_host_preserve_completion_reproves_runtime_absence_after_service_s runtime_root = runtime_parent / "runtime" runtime_marker = runtime_root / "PRIVATE_CONCURRENT_RUNTIME" authority = LifecycleStatePaths(platform="linux", home=home) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited preserve runtime race artifact") service_absent = LinuxCollectorServiceAbsenceState( unit_missing=True, @@ -3387,7 +3387,7 @@ def test_linux_host_set_file_mode_promotes_only_the_owned_execution_copy_to_0700 "f4f1c1610a5d3b905e72c19e6f6ae2bb3b9fbf323d3a9619680726cd4ac7ca51" ) with tempfile.TemporaryDirectory() as directory: - source = Path(directory) / "UsageHub-0.8.6-linux-x86_64.AppImage" + source = Path(directory) / "UsageHub-0.8.7-linux-x86_64.AppImage" source.write_bytes(source_bytes) source.chmod(0o644) source_before = source.lstat() @@ -3544,7 +3544,7 @@ def test_linux_host_copy_file_creates_one_sentinel_after_completed_execution_cop ) with tempfile.TemporaryDirectory() as directory: root = Path(directory) - source = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + source = root / "UsageHub-0.8.7-linux-x86_64.AppImage" source.write_bytes(source_bytes) source.chmod(0o644) source_before = source.lstat() @@ -3772,7 +3772,7 @@ def signature(path: Path) -> tuple[int, ...]: with tempfile.TemporaryDirectory() as directory: root = Path(directory) - source = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + source = root / "UsageHub-0.8.7-linux-x86_64.AppImage" source.write_bytes(source_bytes) source.chmod(0o644) source_signature = signature(source) @@ -4055,7 +4055,7 @@ def test_linux_host_copy_rejects_a_source_inside_the_owned_run_directory( run_directory = dependencies.make_run_directory("linux", "x64") nested = run_directory / "nested-source" nested.mkdir(mode=0o700) - source = nested / "UsageHub-0.8.6-linux-x86_64.AppImage" + source = nested / "UsageHub-0.8.7-linux-x86_64.AppImage" source.write_bytes(b"private nested source") source.chmod(0o600) destination = run_directory / source.name @@ -4110,7 +4110,7 @@ def test_linux_host_copy_failure_cleans_its_partial_file_and_run_directory( real_write = os.write with tempfile.TemporaryDirectory() as directory: - source = Path(directory) / "UsageHub-0.8.6-linux-x86_64.AppImage" + source = Path(directory) / "UsageHub-0.8.7-linux-x86_64.AppImage" source_bytes = b"audited source remains unchanged" source.write_bytes(source_bytes) source.chmod(0o644) @@ -4208,7 +4208,7 @@ def test_linux_host_inspect_rejects_a_public_entry_replaced_during_hashing( real_rmdir = os.rmdir with tempfile.TemporaryDirectory() as directory: root = Path(directory) - source = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + source = root / "UsageHub-0.8.7-linux-x86_64.AppImage" source_bytes = b"audited execution copy" source.write_bytes(source_bytes) source.chmod(0o644) @@ -4321,7 +4321,7 @@ def test_linux_host_inspect_rejects_same_inode_mutation_before_final_path_stat( real_stat = os.stat with tempfile.TemporaryDirectory() as directory: root = Path(directory) - source = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + source = root / "UsageHub-0.8.7-linux-x86_64.AppImage" source_bytes = b"trusted inspect payload" mutated_bytes = b"hostile inspect payload" self.assertEqual(len(mutated_bytes), len(source_bytes)) @@ -4628,7 +4628,7 @@ def test_linux_host_execution_copy_quarantine_restore_does_not_clobber_new_publi real_stat = os.stat with tempfile.TemporaryDirectory() as directory: root = Path(directory) - source = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + source = root / "UsageHub-0.8.7-linux-x86_64.AppImage" owned_bytes = b"owned execution copy" source.write_bytes(owned_bytes) source.chmod(0o644) @@ -4799,7 +4799,7 @@ def test_generate_cannot_accept_a_caller_executor_or_write_real_evidence( with tempfile.TemporaryDirectory() as directory: root = Path(directory) - artifact = root / "UsageHub-0.8.6-win-x64.exe" + artifact = root / "UsageHub-0.8.7-win-x64.exe" artifact.write_bytes(b"final NSIS bytes") output = root / "native-lifecycle.json" @@ -4833,7 +4833,7 @@ def test_linux_native_executor_runs_product_managed_lifecycle_from_audited_copy( with tempfile.TemporaryDirectory() as directory: root = Path(directory) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited final AppImage container") artifact_sha256 = hashlib.sha256(artifact.read_bytes()).hexdigest() run_directory = root / "native-run" @@ -5454,7 +5454,7 @@ def test_windows_native_executor_uses_only_external_low_level_facts(self) -> Non with tempfile.TemporaryDirectory() as directory: root = Path(directory) - artifact = root / "UsageHub-0.8.6-win-x64.exe" + artifact = root / "UsageHub-0.8.7-win-x64.exe" artifact.write_bytes(b"audited final NSIS container") artifact_sha256 = hashlib.sha256(artifact.read_bytes()).hexdigest() run_directory = root / "native-run" @@ -6168,7 +6168,7 @@ def test_validator_rejects_boolean_counters_and_unknown_fields(self) -> None: "serviceManager": "task_scheduler", }, "artifact": { - "name": "UsageHub-0.8.6-win-x64.exe", + "name": "UsageHub-0.8.7-win-x64.exe", "sha256": "b" * 64, "sizeBytes": 128, }, @@ -8336,7 +8336,7 @@ def test_linux_host_context_preserves_the_private_run_directory_when_started_pro home = root / "authoritative-home" (home / ".local" / "share" / "usagehub").mkdir(parents=True) authority = LifecycleStatePaths(platform="linux", home=home) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact_bytes = b"audited host-context artifact" artifact.write_bytes(artifact_bytes) artifact_sha256 = hashlib.sha256(artifact_bytes).hexdigest() @@ -8516,7 +8516,7 @@ def test_linux_host_context_abandons_every_held_descriptor_without_mutating_an_u with tempfile.TemporaryDirectory() as directory: root = Path(directory) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited abandon descriptor artifact") real_open = lifecycle_evidence.os.open real_close = lifecycle_evidence.os.close @@ -8757,7 +8757,7 @@ def test_linux_host_runtime_install_token_is_revoked_by_remaining_callbacks( root = Path(directory) home = root / "authoritative-home" (home / ".local" / "share" / "usagehub").mkdir(parents=True) - source = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + source = root / "UsageHub-0.8.7-linux-x86_64.AppImage" source.write_bytes(b"audited callback token source") authority = LifecycleStatePaths(platform="linux", home=home) @@ -11692,7 +11692,7 @@ def test_default_generate_fails_closed_without_a_real_platform_backend(self) -> with tempfile.TemporaryDirectory() as directory: root = Path(directory) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"final AppImage bytes") output = root / "private-machine-name.json" with self.assertRaisesRegex( @@ -11724,7 +11724,7 @@ def test_linux_default_host_factory_owns_failed_baseline_cleanup(self) -> None: home = root / "authoritative-home" home.mkdir() authority = LifecycleStatePaths(platform="linux", home=home) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"audited default-host artifact") output = root / "native-lifecycle-report.json" cleanup_paths: list[Path] = [] @@ -11814,7 +11814,7 @@ def test_validator_rejects_a_failed_lifecycle_check(self) -> None: "serviceManager": "task_scheduler", }, "artifact": { - "name": "UsageHub-0.8.6-win-x64.exe", + "name": "UsageHub-0.8.7-win-x64.exe", "sha256": "b" * 64, "sizeBytes": 128, }, @@ -11833,7 +11833,7 @@ def test_verify_rehashes_container_binds_target_and_requires_canonical_report(se with tempfile.TemporaryDirectory() as directory: root = Path(directory) - artifact = root / "UsageHub-0.8.6-linux-x86_64.AppImage" + artifact = root / "UsageHub-0.8.7-linux-x86_64.AppImage" artifact.write_bytes(b"final AppImage bytes") output = root / "native-lifecycle.json" expected = { @@ -11922,7 +11922,7 @@ def execute( outcome = executor.execute( platform="linux", arch="x64", - artifact=Path("/tmp/UsageHub-0.8.6-linux-x86_64.AppImage"), + artifact=Path("/tmp/UsageHub-0.8.7-linux-x86_64.AppImage"), artifact_sha256="b" * 64, ) @@ -11934,7 +11934,7 @@ def execute( "linux", "x64", Path( - "/tmp/UsageHub-0.8.6-linux-x86_64.AppImage" + "/tmp/UsageHub-0.8.7-linux-x86_64.AppImage" ), "b" * 64, ) diff --git a/tests/test_product_version_truth.py b/tests/test_product_version_truth.py index 745814dd..386f49fe 100644 --- a/tests/test_product_version_truth.py +++ b/tests/test_product_version_truth.py @@ -109,6 +109,20 @@ def _repository_fixture() -> Iterator[Path]: stderr=subprocess.DEVNULL, check=True, ) + subprocess.run( + ["git", "config", "maintenance.auto", "false"], + cwd=root, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + ) + subprocess.run( + ["git", "config", "gc.auto", "0"], + cwd=root, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + ) subprocess.run( ["git", "add", "."], cwd=root, @@ -213,7 +227,7 @@ def test_committed_contract_is_closed_and_separates_version_domains(self): self.assertEqual( payload["candidate"], { - "build": "28", + "build": "29", "canary": { "clock": "not_started", "qualifiedMachines": 0, @@ -224,7 +238,7 @@ def test_committed_contract_is_closed_and_separates_version_domains(self): "publicationStatus": "not_published", "releaseEligible": False, "releaseStage": "candidate", - "version": "0.8.6", + "version": "0.8.7", }, ) self.assertEqual(payload["publishedBaseline"]["version"], "0.7.1") @@ -250,8 +264,8 @@ def test_actual_repository_verifies_with_safe_candidate_summary(self): self.assertEqual(result.stderr, "") self.assertEqual( result.stdout, - "product_version_truth_ok product=UsageHub candidate=0.8.6 " - "build=28 channel=rc published=v0.7.1\n", + "product_version_truth_ok product=UsageHub candidate=0.8.7 " + "build=29 channel=rc published=v0.7.1\n", ) def test_current_candidate_cannot_cross_the_release_publication_gate(self): @@ -279,7 +293,7 @@ def test_copied_repository_verifies_without_platform_dependencies(self): result = _run_verifier(root) self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("candidate=0.8.6", result.stdout) + self.assertIn("candidate=0.8.7", result.stdout) class ProductVersionTruthClosedContractTests(ProductVersionTruthTestCase): @@ -451,7 +465,7 @@ def receipt(self, root: Path) -> dict[str, object]: "repository": "tttboy123/openusage-bar", "schemaVersion": "github-release-receipt/v1", "sourceSha": source_sha, - "tag": "v0.8.6", + "tag": "v0.8.7", } def test_only_the_three_release_state_rows_are_valid(self): @@ -470,7 +484,7 @@ def test_only_the_three_release_state_rows_are_valid(self): with self.subTest(stage=stage), _repository_fixture() as root: receipt = self.receipt(root) if receipt_kind == "receipt" else None if receipt is not None: - subprocess.run(["git", "tag", "v0.8.6"], cwd=root, check=True) + subprocess.run(["git", "tag", "v0.8.7"], cwd=root, check=True) self.transition( root, stage=stage, @@ -505,7 +519,7 @@ def test_partial_or_crossed_release_state_rows_are_rejected(self): ), _repository_fixture() as root: receipt = self.receipt(root) if receipt_kind == "receipt" else None if receipt is not None: - subprocess.run(["git", "tag", "v0.8.6"], cwd=root, check=True) + subprocess.run(["git", "tag", "v0.8.7"], cwd=root, check=True) self.transition( root, stage=stage, @@ -535,7 +549,7 @@ def add_private_actor(receipt: dict[str, object]) -> None: for label, mutate in mutations: with self.subTest(mutation=label), _repository_fixture() as root: receipt = self.receipt(root) - subprocess.run(["git", "tag", "v0.8.6"], cwd=root, check=True) + subprocess.run(["git", "tag", "v0.8.7"], cwd=root, check=True) mutate(receipt) self.transition( root, @@ -548,8 +562,8 @@ def add_private_actor(receipt: dict[str, object]) -> None: def test_tag_and_local_artifact_cannot_impersonate_publication(self): with _repository_fixture() as root: - subprocess.run(["git", "tag", "v0.8.6"], cwd=root, check=True) - artifact = root / "dist/OpenUsage-Bar-v0.8.6-macos-arm64.dmg" + subprocess.run(["git", "tag", "v0.8.7"], cwd=root, check=True) + artifact = root / "dist/OpenUsage-Bar-v0.8.7-macos-arm64.dmg" artifact.parent.mkdir() artifact.write_bytes(b"local candidate bytes, not a publication receipt\n") @@ -557,7 +571,7 @@ def test_tag_and_local_artifact_cannot_impersonate_publication(self): self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("published=v0.7.1", result.stdout) - self.assertNotIn("published=v0.8.6", result.stdout) + self.assertNotIn("published=v0.8.7", result.stdout) def test_publication_receipt_never_enters_renderer_safe_projections(self): for relative in self.PROJECTION_PATHS: @@ -609,7 +623,7 @@ def test_setup_package_and_lock_identity_are_bound(self): ( "setup-version", Path("setup.py"), - '"version": "0.8.6"', + '"version": "0.8.7"', '"version": "0.8.5"', ), ) @@ -669,7 +683,7 @@ def test_client_build_identity_projections_are_bound(self): ( "desktop-projection", Path("desktop/product_version_truth.js"), - 'candidateVersion: "0.8.6"', + 'candidateVersion: "0.8.7"', 'candidateVersion: "0.8.5"', ), ( @@ -682,7 +696,7 @@ def test_client_build_identity_projections_are_bound(self): "swift-projection", Path("swift_app/Sources/UsageCore/ProductVersionTruth.swift"), 'publishedBaselineTag: "v0.7.1"', - 'publishedBaselineTag: "v0.8.6"', + 'publishedBaselineTag: "v0.8.7"', ), ) ) @@ -767,10 +781,10 @@ def test_readme_and_roadmap_identity_markers_are_bound(self): ( relative.name, relative, - "candidate=0.8.6 build=28 channel=rc stage=candidate " + "candidate=0.8.7 build=29 channel=rc stage=candidate " "publication=not_published published=v0.7.1", - "candidate=0.8.6 build=28 channel=rc stage=published " - "publication=published published=v0.8.6", + "candidate=0.8.7 build=29 channel=rc stage=published " + "publication=published published=v0.8.7", ) for relative in ( Path("README.md"), diff --git a/tests/test_release_artifact_audit.py b/tests/test_release_artifact_audit.py index 13c53975..97241de5 100644 --- a/tests/test_release_artifact_audit.py +++ b/tests/test_release_artifact_audit.py @@ -29,10 +29,10 @@ ) PACKAGED_IDENTITY_NAME = "product-build-identity.v1.json" PACKAGE_METADATA = { - "buildNumber": "28", - "buildVersion": "28", + "buildNumber": "29", + "buildVersion": "29", "name": "usagehub-desktop", - "version": "0.8.6", + "version": "0.8.7", } @@ -97,9 +97,9 @@ def _write_asar(path: Path, *, extra_payload: bytes = b"") -> None: def _windows_versioned_executable() -> bytes: image = bytearray(native_collector_bytes("win32", b"usagehub-app")) for key, value in ( - ("FileVersion", "28"), + ("FileVersion", "29"), ("ProductName", "UsageHub"), - ("ProductVersion", "0.8.6.28"), + ("ProductVersion", "0.8.7.29"), ): while len(image) % 4: image.append(0) @@ -149,8 +149,8 @@ def write_desktop_package( { "CFBundleDisplayName": "UsageHub", "CFBundleName": "UsageHub", - "CFBundleShortVersionString": "0.8.6", - "CFBundleVersion": "28", + "CFBundleShortVersionString": "0.8.7", + "CFBundleVersion": "29", } ) ) @@ -162,7 +162,7 @@ def write_desktop_package( desktop.write_text( "[Desktop Entry]\n" "Name=UsageHub\n" - "X-AppImage-Version=28\n", + "X-AppImage-Version=29\n", encoding="utf-8", ) return collector @@ -736,7 +736,7 @@ def test_desktop_package_binds_canonical_identity_digest_and_size(self): observed["identity"]["candidateVersion"], observed["identity"]["candidateBuild"], ), - ("UsageHub", "0.8.6", "28"), + ("UsageHub", "0.8.7", "29"), ) def test_desktop_package_rejects_identity_and_native_metadata_tamper(self): @@ -760,7 +760,7 @@ def test_desktop_package_rejects_identity_and_native_metadata_tamper(self): elif case == "mac": info = root / "Contents/Info.plist" payload = plistlib.loads(info.read_bytes()) - payload["CFBundleVersion"] = "29" + payload["CFBundleVersion"] = "30" info.write_bytes(plistlib.dumps(payload)) expected_reason = "version_mismatch" elif case == "windows": @@ -777,7 +777,7 @@ def test_desktop_package_rejects_identity_and_native_metadata_tamper(self): desktop.write_text( "[Desktop Entry]\n" "Name=UsageHub\n" - "X-AppImage-Version=29\n", + "X-AppImage-Version=30\n", encoding="utf-8", ) expected_reason = "version_mismatch" diff --git a/tests/test_release_handoff.py b/tests/test_release_handoff.py index 7c141a3e..e82ad05f 100644 --- a/tests/test_release_handoff.py +++ b/tests/test_release_handoff.py @@ -320,7 +320,7 @@ def test_assemble_is_deterministic_path_free_and_fully_offline_verifiable(self): ) self.assertEqual( payload["product"], - {"name": "UsageHub", "version": "0.8.6"}, + {"name": "UsageHub", "version": "0.8.7"}, ) self.assertEqual( payload["buildIdentity"], @@ -514,7 +514,7 @@ def test_verify_rejects_build_identity_drift_from_native_evidence(self): bundle, payload = _assemble_fixture(root) manifest = bundle / "usagehub-release-handoff-mac-x64.json" identity = payload["buildIdentity"]["identity"] - identity["candidateBuild"] = "29" + identity["candidateBuild"] = "30" encoded_identity = ( json.dumps( identity, diff --git a/tests/test_release_readiness.py b/tests/test_release_readiness.py index 350c9edc..b7a7ba76 100644 --- a/tests/test_release_readiness.py +++ b/tests/test_release_readiness.py @@ -76,8 +76,8 @@ def test_current_candidate_is_machine_readable_blocked_without_path_leak(self): self.assertEqual( report["product"], { - "candidateBuild": "28", - "candidateVersion": "0.8.6", + "candidateBuild": "29", + "candidateVersion": "0.8.7", "channel": "rc", "name": "UsageHub", "publishedBaseline": "v0.7.1", @@ -134,7 +134,7 @@ def test_self_reported_evidence_never_promotes_ready_candidate_to_authoritative_ "version": "0.8.7", }, "product": {"displayName": "UsageHub"}, - "publishedBaseline": {"tag": "v0.8.6"}, + "publishedBaseline": {"tag": "v0.8.7"}, } with ( diff --git a/web/package-lock.json b/web/package-lock.json index 8ba123dd..d3157fbc 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "usagehub-web", - "version": "0.8.6", + "version": "0.8.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "usagehub-web", - "version": "0.8.6", + "version": "0.8.7", "license": "Apache-2.0", "dependencies": { "@phosphor-icons/react": "^2.1.7", diff --git a/web/package.json b/web/package.json index 6eb96ae3..235f98d5 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "usagehub-web", - "version": "0.8.6", + "version": "0.8.7", "private": true, "type": "module", "scripts": { diff --git a/web/public/product-build-identity.v1.json b/web/public/product-build-identity.v1.json index f5213b14..4f60b50d 100644 --- a/web/public/product-build-identity.v1.json +++ b/web/public/product-build-identity.v1.json @@ -2,8 +2,8 @@ "canaryClock": "not_started", "canaryQualifiedMachines": 0, "canaryTargetMachines": 5, - "candidateBuild": "28", - "candidateVersion": "0.8.6", + "candidateBuild": "29", + "candidateVersion": "0.8.7", "channel": "rc", "displayName": "UsageHub", "publicationStatus": "not_published", diff --git a/web/src/api.ts b/web/src/api.ts index 57c73798..34de4dbb 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -421,7 +421,7 @@ export interface RefreshStatus { succeeded?: boolean | null; } -const HOST_REFRESH_DEADLINE_MS = 185_000; +const HOST_REFRESH_DEADLINE_MS = 305_000; const REFRESH_STATES = new Set(["ok", "attention", "error", "disabled", "unknown"]); function normalizeHostRefreshResult(value: unknown): RefreshStatus | null { diff --git a/web/src/productVersionTruth.ts b/web/src/productVersionTruth.ts index 340f4810..088eceaa 100644 --- a/web/src/productVersionTruth.ts +++ b/web/src/productVersionTruth.ts @@ -19,8 +19,8 @@ export type ProductVersionTruth = Readonly<{ export const productVersionTruth: ProductVersionTruth = Object.freeze({ displayName: "UsageHub", legacyDisplayName: "OpenUsage Bar", - candidateVersion: "0.8.6", - candidateBuild: "28", + candidateVersion: "0.8.7", + candidateBuild: "29", channel: "rc", releaseStage: "candidate", publicationStatus: "not_published", diff --git a/web/tests/capacity-ui.test.mjs b/web/tests/capacity-ui.test.mjs index 4c7b5a60..7c9a9f89 100644 --- a/web/tests/capacity-ui.test.mjs +++ b/web/tests/capacity-ui.test.mjs @@ -8,6 +8,11 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const capacity = fs.readFileSync(path.join(root, "src/pages/CapacityPage.tsx"), "utf8"); const app = fs.readFileSync(path.join(root, "src/App.tsx"), "utf8"); const api = fs.readFileSync(path.join(root, "src/api.ts"), "utf8"); +const desktopMain = fs.readFileSync(path.join(root, "../desktop/main.js"), "utf8"); +const collectorRuntime = fs.readFileSync( + path.join(root, "../desktop/collector_runtime.js"), + "utf8", +); const i18n = fs.readFileSync(path.join(root, "src/i18n.ts"), "utf8"); const css = fs.readFileSync(path.join(root, "src/styles/app.css"), "utf8"); @@ -31,3 +36,11 @@ test("entering Capacity uses the trusted host refresh and Local API stays read-o assert.match(app, /aria-busy=\{refreshing\}/u); assert.match(app, /role="status" aria-live="polite"/u); }); + +test("the user refresh deadline covers the bounded five-minute collector window", () => { + assert.match(collectorRuntime, /MAX_COLLECTOR_COMMAND_TIMEOUT_MS = 300_000/u); + assert.match(collectorRuntime, /attempts = 150/u); + assert.match(collectorRuntime, /attempts > 150/u); + assert.match(desktopMain, /timeoutMs: 300_000/u); + assert.match(api, /HOST_REFRESH_DEADLINE_MS = 305_000/u); +}); diff --git a/web/tests/product-build-identity-ui.test.mjs b/web/tests/product-build-identity-ui.test.mjs index 415f955e..77c224b4 100644 --- a/web/tests/product-build-identity-ui.test.mjs +++ b/web/tests/product-build-identity-ui.test.mjs @@ -11,8 +11,8 @@ const cssSource = await readFile(new URL("../src/styles/app.css", import.meta.ur const BASE_IDENTITY = Object.freeze({ displayName: "UsageHub", legacyDisplayName: "OpenUsage Bar", - candidateVersion: "0.8.6", - candidateBuild: "28", + candidateVersion: "0.8.7", + candidateBuild: "29", channel: "rc", releaseStage: "candidate", publicationStatus: "not_published", diff --git a/web/tests/product-version-truth.test.mjs b/web/tests/product-version-truth.test.mjs index 485e4833..34c33c9c 100644 --- a/web/tests/product-version-truth.test.mjs +++ b/web/tests/product-version-truth.test.mjs @@ -9,8 +9,8 @@ import { const expectedIdentity = { displayName: "UsageHub", legacyDisplayName: "OpenUsage Bar", - candidateVersion: "0.8.6", - candidateBuild: "28", + candidateVersion: "0.8.7", + candidateBuild: "29", channel: "rc", releaseStage: "candidate", publicationStatus: "not_published", @@ -35,7 +35,7 @@ test("freezes the renderer projection", () => { }); test("formats version and channel without embedding final UI copy", () => { - assert.equal(formatCandidateVersion(productVersionTruth), "0.8.6 RC"); + assert.equal(formatCandidateVersion(productVersionTruth), "0.8.7 RC"); assert.equal( formatCandidateVersion({ candidateVersion: "1.2.3", channel: "beta" }), "1.2.3 BETA",