Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ name: Desktop validation

on:
pull_request:
paths: ['desktop/**', 'src/powercontext/**', 'pyproject.toml', 'uv.lock', 'openapi/powercontext.yaml', 'tests/fixtures/transport_loopback_vectors.json', 'website/assets/**', '.github/workflows/desktop.yml']
paths: ['desktop/**', 'src/powercontext/**', 'pyproject.toml', 'uv.lock', 'openapi/powercontext.yaml', 'tests/fixtures/transport_loopback_vectors.json', 'tests/test_desktop_fixture_qualification.py', 'website/assets/**', '.github/workflows/desktop.yml']
push:
branches: [master, codex/desktop-preview]
paths: ['desktop/**', 'src/powercontext/**', 'pyproject.toml', 'uv.lock', 'openapi/powercontext.yaml', 'tests/fixtures/transport_loopback_vectors.json', 'website/assets/**', '.github/workflows/desktop.yml']
paths: ['desktop/**', 'src/powercontext/**', 'pyproject.toml', 'uv.lock', 'openapi/powercontext.yaml', 'tests/fixtures/transport_loopback_vectors.json', 'tests/test_desktop_fixture_qualification.py', 'website/assets/**', '.github/workflows/desktop.yml']
workflow_dispatch:
inputs:
installer_run:
Expand Down Expand Up @@ -71,16 +71,22 @@ jobs:
- name: Build Server wheel for isolated acceptance
working-directory: .
run: uv build --wheel --out-dir desktop/.artifacts/server-wheel
- name: Qualify the disposable checkout and built-wheel fixture
working-directory: .
run: uv run --no-sync python desktop/tests/fixture_qualification.py --server-commit $env:GITHUB_SHA
- name: Check installed UI acceptance harness
working-directory: .
run: |
uv run --no-sync ruff check --no-fix desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py
uv run --no-sync ruff check --no-fix desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py desktop/tests/fixture_qualification.py
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
uv run --no-sync ruff format --check desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py
uv run --no-sync ruff format --check desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py desktop/tests/fixture_qualification.py
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
uv run --no-sync ty check desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py
uv run --no-sync ty check desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py desktop/tests/fixture_qualification.py
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
uv run --no-sync ty check --python-platform linux desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py
uv run --no-sync ty check --python-platform linux desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py desktop/tests/fixture_qualification.py
- name: Check fixture qualification regressions
working-directory: .
run: uv run --no-sync python -m pytest tests/test_desktop_fixture_qualification.py
- name: Build native API acceptance client
run: cargo build --locked --manifest-path src-tauri/Cargo.toml --example server_probe --example diagnostic_cli_probe
- name: Test native adapter with real SQLite Server
Expand All @@ -89,8 +95,8 @@ jobs:
- name: Test native diagnostics with isolated real CLI
working-directory: .
run: uv run --no-sync python desktop/tests/real_cli.py
- name: Build unsigned internal Windows installer
run: pnpm desktop:build
- name: Build fixture-qualified unsigned internal Windows installer
run: pnpm tauri build --bundles nsis --ci --features ci-fixtures -- --locked
- name: Prepare matching WebView2 driver
run: ./scripts/prepare-webdriver.ps1
- name: Smoke test installed package
Expand All @@ -104,6 +110,7 @@ jobs:
path: |
desktop/src-tauri/target/release/bundle/nsis/*-setup.exe
desktop/.artifacts/windows-smoke.json
desktop/.artifacts/ci-compatibility.json
desktop/.artifacts/real-server.json
desktop/.artifacts/real-cli.json
desktop/.artifacts/installed-ui.json
Expand Down Expand Up @@ -142,6 +149,8 @@ jobs:
uv sync --locked --python 3.12
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
uv build --wheel --out-dir desktop/.artifacts/server-wheel
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
uv run --no-sync python desktop/tests/fixture_qualification.py --server-commit $env:GITHUB_SHA
- name: Retrieve and verify exact installer
env:
GH_TOKEN: ${{ github.token }}
Expand Down
13 changes: 13 additions & 0 deletions desktop/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ Selecting a compatibility profile does not attest the remote binary identity. Ex

This qualification used macOS 26.2 ARM64, Rust 1.95.0 and Python 3.12.7. An external system Python launcher was used for native process creation because this environment can terminate native subprocesses before their first request. The wheel, native client, fixtures and business assertions were preserved. The real CLI's fixed version, service-status and integration commands were also exercised with an isolated home and PATH; their actual outputs passed the native projections. These checks establish the stated SQLite compatibility and CLI response handling. Windows registered-executable checks, credentials, process containment and installed-package acceptance require their actual Windows CI checks. No remote legacy deployment was tested in this qualification.

## CI fixture 的独立资格

CI fixture 使用独立资格记录,不会修改历史生产资格。原生 HTTP 测试显式注入 synthetic profile;真实 Server 和安装验收使用 `fixture_qualification.py` 从当前合同、Server 提交和构建 wheel 生成的 `.artifacts/ci-compatibility.json`,读取时重新核对合同、操作集合和 wheel 哈希。

只有显式启用 `ci-fixtures` 的测试安装包会包含这个 profile。普通构建仍只使用历史资格记录,并保留合同不匹配时的 fail-closed 行为。CI fixture 和其安装包不能作为生产发布资格证据。

```sh
uv build --wheel --out-dir desktop/.artifacts/server-wheel
DESKTOP_FIXTURE_COMMIT=$(git rev-parse HEAD)
uv run python desktop/tests/fixture_qualification.py --server-commit "$DESKTOP_FIXTURE_COMMIT"
pnpm --dir desktop tauri build --bundles nsis --ci --features ci-fixtures -- --locked
```

## Remaining release qualification

- Clean standard-user Windows 11, including absent/present WebView2 and bootstrap recovery.
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ name = "powercontext_desktop"

[features]
custom-protocol = ["tauri/custom-protocol"]
ci-fixtures = []

[build-dependencies]
tauri-build = { version = "=2.6.0", features = [] }
Expand Down
9 changes: 9 additions & 0 deletions desktop/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@
*/

fn main() {
if std::env::var_os("CARGO_FEATURE_CI_FIXTURES").is_some() {
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../.artifacts/ci-compatibility.json");
println!("cargo:rerun-if-changed={}", manifest.display());
let output = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
std::fs::copy(manifest, output.join("ci-compatibility.json")).expect(
"ci-fixtures requires explicit built-wheel qualification; run fixture_qualification.py",
);
}
// Integration-test executables also need ComCtl32 v6 for Tauri's menu imports.
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
println!("cargo:rustc-link-arg-tests=/MANIFEST:EMBED");
Expand Down
24 changes: 5 additions & 19 deletions desktop/src-tauri/examples/server_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use powercontext_desktop::{
connections::{
profiles::ProfileRepository,
session::{ConnectionManager, WriteStatus},
session::{CompatibilityProfile, ConnectionManager, WriteStatus},
},
credentials::{Secret, WindowsVault},
error::SafeError,
Expand All @@ -27,6 +27,7 @@ use powercontext_desktop::{
use serde::Deserialize;
#[derive(Deserialize)]
struct Fixture {
compatibility_profile: CompatibilityProfile,
response_loss_path: Option<String>,
identity_change_path: Option<String>,
endpoint: String,
Expand Down Expand Up @@ -115,31 +116,16 @@ async fn main() {
assert_eq!(exact.text, text);
// Exercise the same native context owner used by product IPC, not only bare HTTP adapters.
let temporary = tempfile::tempdir().unwrap();
let manager = ConnectionManager::new(
let manager = ConnectionManager::with_compatibility(
ProfileRepository::open(
temporary.path().join("profiles.json"),
std::sync::Arc::new(WindowsVault),
)
.unwrap(),
vec![fixture.compatibility_profile.clone()],
);
let raw: serde_json::Value = serde_json::from_slice(&std::fs::read(&args[1]).unwrap()).unwrap();
let contract: serde_json::Value =
serde_json::from_str(include_str!("../src/transport/operations.json")).unwrap();
let contract_sha256 = contract["contractSha256"].as_str().unwrap();
let state = manager.state().unwrap();
let mut qualified = state
.compatibility_profiles
.iter()
.filter(|profile| profile.contract_sha256 == contract_sha256);
let compatibility = qualified
.next()
.expect("qualified fixture contract")
.id
.clone();
assert!(
qualified.next().is_none(),
"ambiguous fixture qualification"
);
let compatibility = &fixture.compatibility_profile.id;
let credential = raw["token"]
.as_str()
.map(|value| serde_json::json!({"secret":value,"storage":"session_only"}));
Expand Down
27 changes: 24 additions & 3 deletions desktop/src-tauri/src/connections/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,31 @@ pub struct ConnectionManager {
}
impl ConnectionManager {
pub fn new(profiles: ProfileRepository) -> Self {
let (changed, _) = watch::channel(0);
// No configuration becomes qualified merely because a health probe returned 200.
let compatibility = serde_json::from_str(include_str!("compatibility.json"))
.expect("bundled compatibility manifest");
let compatibility: Vec<CompatibilityProfile> =
serde_json::from_str(include_str!("compatibility.json"))
.expect("bundled compatibility manifest");
#[cfg(feature = "ci-fixtures")]
let compatibility = {
let mut compatibility = compatibility;
compatibility.push(
serde_json::from_str(include_str!(concat!(
env!("OUT_DIR"),
"/ci-compatibility.json"
)))
.expect("explicit CI fixture qualification"),
);
compatibility
};
Self::with_compatibility(profiles, compatibility)
}
/// Supply explicit native qualification evidence, including synthetic test fixtures.
/// This constructor is never exposed through IPC or renderer configuration.
pub fn with_compatibility(
profiles: ProfileRepository,
compatibility: Vec<CompatibilityProfile>,
) -> Self {
let (changed, _) = watch::channel(0);
Self {
inner: Mutex::new(Inner {
profiles,
Expand Down
37 changes: 37 additions & 0 deletions desktop/src-tauri/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2026 OceanBase.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use powercontext_desktop::connections::{
profiles::ProfileRepository,
session::{CompatibilityProfile, ConnectionManager},
};
use sha2::{Digest, Sha256};

pub const PROFILE_ID: &str = "synthetic-native-fixture";

pub fn connection_manager(profiles: ProfileRepository) -> ConnectionManager {
let contract: serde_json::Value =
serde_json::from_str(include_str!("../../src/transport/operations.json")).unwrap();
let profile = CompatibilityProfile {
id: PROFILE_ID.into(),
server_commit: "synthetic-http-fixture-not-a-server-release".into(),
contract_sha256: contract["contractSha256"].as_str().unwrap().into(),
artifact_sha256: format!("{:x}", Sha256::digest(include_bytes!("mod.rs"))),
operations: contract["operations"].as_object().unwrap().keys().cloned().collect(),
evidence: "Synthetic HTTP fixtures in tests/memory.rs and tests/session.rs; not release qualification".into(),
};
ConnectionManager::with_compatibility(profiles, vec![profile])
}
22 changes: 4 additions & 18 deletions desktop/src-tauri/tests/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

mod common;

use powercontext_desktop::{
connections::{
profiles::ProfileRepository,
Expand Down Expand Up @@ -147,26 +149,10 @@ async fn setup() -> Fixture {
}
});
let dir = tempfile::tempdir().unwrap();
let manager = Arc::new(ConnectionManager::new(
let manager = Arc::new(common::connection_manager(
ProfileRepository::open(dir.path().join("profiles.json"), Arc::new(WindowsVault)).unwrap(),
));
let contract: serde_json::Value =
serde_json::from_str(include_str!("../src/transport/operations.json")).unwrap();
let contract_sha256 = contract["contractSha256"].as_str().unwrap();
let state = manager.state().unwrap();
let mut qualified = state
.compatibility_profiles
.iter()
.filter(|profile| profile.contract_sha256 == contract_sha256);
let compatibility = qualified
.next()
.expect("qualified fixture contract")
.id
.clone();
assert!(
qualified.next().is_none(),
"ambiguous fixture qualification"
);
let compatibility = common::PROFILE_ID;
let mut ids = vec![];
for name in ["A", "B"] {
let state = manager.save_profile(serde_json::from_value(serde_json::json!({"id":null,"revision":null,"name":name,"endpoint":endpoint,"authentication":"unauthenticated_loopback","caPem":null,"compatibility":compatibility,"keepCredential":false,"credential":null})).unwrap()).unwrap();
Expand Down
73 changes: 55 additions & 18 deletions desktop/src-tauri/tests/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

mod common;

use powercontext_desktop::{
connections::{profiles::ProfileRepository, session::ConnectionManager},
credentials::WindowsVault,
Expand Down Expand Up @@ -108,23 +110,7 @@ async fn fixture() -> Fixture {
}
}
fn add(manager: &ConnectionManager, name: &str, endpoint: &str) -> String {
let contract: serde_json::Value =
serde_json::from_str(include_str!("../src/transport/operations.json")).unwrap();
let contract_sha256 = contract["contractSha256"].as_str().unwrap();
let state = manager.state().unwrap();
let mut qualified = state
.compatibility_profiles
.iter()
.filter(|profile| profile.contract_sha256 == contract_sha256);
let compatibility = qualified
.next()
.expect("qualified fixture contract")
.id
.clone();
assert!(
qualified.next().is_none(),
"ambiguous fixture qualification"
);
let compatibility = common::PROFILE_ID;
let state = manager.save_profile(serde_json::from_value(serde_json::json!({
"id":null,"revision":null,"name":name,"endpoint":endpoint,
"authentication":"unauthenticated_loopback","caPem":null,"compatibility":compatibility,
Expand All @@ -139,10 +125,61 @@ fn add(manager: &ConnectionManager, name: &str, endpoint: &str) -> String {
.clone()
}
fn manager(dir: &tempfile::TempDir) -> Arc<ConnectionManager> {
Arc::new(ConnectionManager::new(
Arc::new(common::connection_manager(
ProfileRepository::open(dir.path().join("profiles.json"), Arc::new(WindowsVault)).unwrap(),
))
}
#[tokio::test]
async fn successful_probes_do_not_qualify_a_synthetic_profile_in_the_product_manager() {
let server = fixture().await;
let dir = tempfile::tempdir().unwrap();
let manager = ConnectionManager::new(
ProfileRepository::open(dir.path().join("profiles.json"), Arc::new(WindowsVault)).unwrap(),
);
let id = add(&manager, "Unqualified", &server.endpoint);
let state = manager.check(&id, true).await.unwrap();
let active = state.active.unwrap();
assert!(!active.report.compatibility_verified);
assert!(active.report.supported_operations.is_empty());
assert_eq!(
manager
.scopes(active.generation, "", None)
.await
.err()
.unwrap()
.code,
SafeError::CompatibilityUnverified
);
}

#[tokio::test]
async fn explicit_fixture_evidence_with_a_different_contract_remains_unverified() {
let server = fixture().await;
let dir = tempfile::tempdir().unwrap();
let qualified = common::connection_manager(
ProfileRepository::open(dir.path().join("qualified.json"), Arc::new(WindowsVault)).unwrap(),
);
let mut evidence = qualified.state().unwrap().compatibility_profiles[0].clone();
evidence.contract_sha256 = "0".repeat(64);
let manager = ConnectionManager::with_compatibility(
ProfileRepository::open(dir.path().join("profiles.json"), Arc::new(WindowsVault)).unwrap(),
vec![evidence],
);
let id = add(&manager, "Mismatched", &server.endpoint);
let state = manager.check(&id, true).await.unwrap();
let active = state.active.unwrap();
assert!(!active.report.compatibility_verified);
assert_eq!(
manager
.scopes(active.generation, "", None)
.await
.err()
.unwrap()
.code,
SafeError::CompatibilityUnverified
);
}

#[tokio::test]
async fn checking_another_profile_does_not_activate_it_and_capability_denial_is_independent() {
let server = fixture().await;
Expand Down
Loading
Loading