test(desktop): cover bridge, backup and package contracts

This commit is contained in:
abraxas914 committed 2026-08-23 18:15:52 +08:00
1 parent 6cc27a78c8
commit f526f4ad5b
25 files changed
+7665

No files matched your search

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 --artifact <portable.tar.gz> --expected-commit <sha> [--work-dir <dir>]" >&2
}
artifact=""
expected_commit=""
work_dir=""
keep_work_dir=0
while [[ $# -gt 0 ]]; do
case "$1" in
--artifact) artifact="$2"; shift 2 ;;
--expected-commit) expected_commit="$2"; shift 2 ;;
--work-dir) work_dir="$2"; shift 2 ;;
--keep-work-dir) keep_work_dir=1; shift ;;
*) usage; exit 2 ;;
esac
done
[[ -n "$artifact" && -n "$expected_commit" ]] || { usage; exit 2; }
artifact="$(realpath "$artifact")"
[[ -f "$artifact" ]] || { echo "Artifact not found: $artifact" >&2; exit 1; }
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
driver="$script_dir/../package/real_package_journey.py"
if [[ -z "$work_dir" ]]; then
work_dir="$(mktemp -d -t ga-linux-package-e2e.XXXXXX)"
else
mkdir -p "$work_dir"
work_dir="$(realpath "$work_dir")"
fi
extract_dir="$work_dir/extracted"
report_dir="$work_dir/report"
driver_work="$work_dir/driver-work"
mkdir -p "$extract_dir" "$report_dir" "$driver_work"
cleanup() {
if [[ $keep_work_dir -eq 0 ]]; then
echo "Evidence kept at $report_dir; extracted package kept until the report is collected."
else
echo "Work directory kept at $work_dir"
fi
}
trap cleanup EXIT
tar -xzf "$artifact" -C "$extract_dir"
package_root="$extract_dir/GenericAgent-Desktop-Linux-Portable"
if [[ ! -d "$package_root" ]]; then
mapfile -t roots < <(find "$extract_dir" -mindepth 1 -maxdepth 1 -type d)
[[ ${#roots[@]} -eq 1 ]] || { echo "Could not identify the portable package root" >&2; exit 1; }
package_root="${roots[0]}"
fi
chmod +x "$package_root/GenericAgent.AppImage"
sidecar="$(dirname "$artifact")/SHA256SUMS-linux.txt"
[[ -f "$sidecar" ]] || { echo "Linux SHA-256 sidecar not found: $sidecar" >&2; exit 1; }
expected_hash="$(grep -Eo '[0-9a-fA-F]{64}' "$sidecar" | head -n 1 | tr '[:upper:]' '[:lower:]')"
actual_hash="$(sha256sum "$artifact" | awk '{print $1}')"
[[ "$actual_hash" == "$expected_hash" ]] || { echo "Artifact SHA-256 mismatch" >&2; exit 1; }
python3 "$driver" \
--platform linux \
--artifact "$artifact" \
--expected-commit "$expected_commit" \
--package-root "$package_root" \
--application-relative "GenericAgent.AppImage" \
--runtime-relative "runtime" \
--relocated-root "$work_dir/relocated/含 空格/GenericAgent 包" \
--report-dir "$report_dir" \
--work-dir "$driver_work" \
--allow-user-settings-mutation
+22
View File
@@ -0,0 +1,22 @@
# Ubuntu 24.04 release-package journey
Run this on a real Ubuntu 24.04 x64 desktop in a dedicated test account. FUSE and a screenshot
tool (`gnome-screenshot`, `scrot`, or ImageMagick `import`) must be available.
```bash
frontends/desktop/e2e/linux/Invoke-LinuxUserJourney.sh \
--artifact /path/GenericAgent-Desktop-Linux-Portable.tar.gz \
--expected-commit <candidate-sha> \
--keep-work-dir
```
The wrapper verifies/extracts the tar, preserves the AppImage launch path, and runs the shared
production journey. The journey covers package shape, embedded Python, first launch, warm
restart, package-owned bridge plus external `GA_ROOT`, deterministic chat, upload, memory import,
foreign-port protection, recovery after release, relocation into a path containing spaces and
Chinese characters, stale-override fallback, optional P2P degradation, screenshots, and cleanup.
After automation passes, edit `report/real-package-report.json` and mark the Linux manual items
`pass`: executable bit and desktop launcher, window dragging/close behavior, retry button after
port release, native directory picker, and visual/loading checks. P2 evidence is incomplete while
any manual item remains `pending`.
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 --artifact <desktop.dmg> --expected-commit <sha> [--work-dir <dir>]" >&2
}
artifact=""
expected_commit=""
work_dir=""
keep_work_dir=0
while [[ $# -gt 0 ]]; do
case "$1" in
--artifact) artifact="$2"; shift 2 ;;
--expected-commit) expected_commit="$2"; shift 2 ;;
--work-dir) work_dir="$2"; shift 2 ;;
--keep-work-dir) keep_work_dir=1; shift ;;
*) usage; exit 2 ;;
esac
done
[[ -n "$artifact" && -n "$expected_commit" ]] || { usage; exit 2; }
artifact="$(cd "$(dirname "$artifact")" && pwd)/$(basename "$artifact")"
[[ -f "$artifact" ]] || { echo "Artifact not found: $artifact" >&2; exit 1; }
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
driver="$script_dir/../package/real_package_journey.py"
if [[ -z "$work_dir" ]]; then
work_dir="$(mktemp -d -t ga-macos-package-e2e.XXXXXX)"
else
mkdir -p "$work_dir"
work_dir="$(cd "$work_dir" && pwd)"
fi
report_dir="$work_dir/report"
driver_work="$work_dir/driver-work"
mount_point="$work_dir/dmg"
install_app="/Applications/GenericAgent P2 E2E $$.app"
mounted=0
mkdir -p "$report_dir" "$driver_work" "$mount_point"
cleanup() {
if [[ $mounted -eq 1 ]]; then
hdiutil detach "$mount_point" -quiet || true
fi
if [[ -d "$install_app" ]]; then
rm -rf -- "$install_app"
fi
if [[ $keep_work_dir -eq 1 ]]; then
echo "Work directory kept at $work_dir"
else
echo "Evidence kept at $report_dir"
fi
}
trap cleanup EXIT
[[ ! -e "$install_app" ]] || { echo "Refusing to replace existing app: $install_app" >&2; exit 1; }
sidecar="$artifact.sha256"
[[ -f "$sidecar" ]] || { echo "macOS SHA-256 sidecar not found: $sidecar" >&2; exit 1; }
expected_hash="$(grep -Eo '[0-9a-fA-F]{64}' "$sidecar" | head -n 1 | tr '[:upper:]' '[:lower:]')"
actual_hash="$(shasum -a 256 "$artifact" | awk '{print $1}')"
[[ "$actual_hash" == "$expected_hash" ]] || { echo "Artifact SHA-256 mismatch" >&2; exit 1; }
hdiutil attach "$artifact" -mountpoint "$mount_point" -nobrowse -readonly -quiet
mounted=1
source_app="$(find "$mount_point" -mindepth 1 -maxdepth 1 -type d -name '*.app' | head -n 1)"
[[ -n "$source_app" ]] || { echo "DMG does not contain an application" >&2; exit 1; }
ditto "$source_app" "$install_app"
hdiutil detach "$mount_point" -quiet
mounted=0
binary="$(find "$install_app/Contents/MacOS" -mindepth 1 -maxdepth 1 -type f -perm -111 | head -n 1)"
[[ -n "$binary" ]] || { echo "Installed application has no executable" >&2; exit 1; }
binary_relative="${binary#"$install_app"/}"
python3 "$driver" \
--platform macos \
--artifact "$artifact" \
--expected-commit "$expected_commit" \
--package-root "$install_app" \
--application-relative "$binary_relative" \
--runtime-relative "Contents/Resources/runtime" \
--relocated-root "$work_dir/relocated/含 空格/GenericAgent 已移动.app" \
--report-dir "$report_dir" \
--work-dir "$driver_work" \
--allow-user-settings-mutation \
--allow-external-package
+27
View File
@@ -0,0 +1,27 @@
# macOS 15 DMG release-package journey
Run this on a real macOS 15 machine in a dedicated test account. The wrapper mounts the DMG,
copies its app into `/Applications` under a collision-safe E2E name, launches the production
binary, and later moves that exact app to a path containing spaces and Chinese characters.
```bash
frontends/desktop/e2e/macos/Invoke-macOSUserJourney.sh \
--artifact /path/GenericAgent-Desktop-macOS-aarch64.dmg \
--expected-commit <candidate-sha> \
--keep-work-dir
```
The release workflow builds this artifact on the explicit macOS 15 arm64 runner and names it
`aarch64`; it is not an Intel/universal binary. The app is ad-hoc signed only, not Developer ID
signed or notarized.
In addition to the shared chat/data/port/relocation checks, this journey hard-fails if the DMG
does not contain the build-time `.prepared` marker or if any file inside the `.app` changes from
first launch through restart and relocation. It verifies the package bridge remains inside the
app while `GA_ROOT` points at an external core, then verifies a deleted override falls back to the
stable writable runtime at `~/Library/Application Support/GenericAgent/runtime/app`.
After automation, mark the manual report items `pass`: Gatekeeper/open-anyway, traffic lights,
focus, retry button after port release, native directory picker, and visual/loading checks. The
wrapper restores the settings file exactly and removes its temporary `/Applications` copy; run it
only in a dedicated account because the normal Application Support runtime may be created.
+28
View File
@@ -0,0 +1,28 @@
# P2 release-package evidence gate
`real_package_journey.py` is the common L5 runner. Platform wrappers install or extract one
artifact produced from a candidate commit and invoke this runner against the production binary.
It temporarily uses the real per-user settings path, restores it byte-for-byte, and therefore
must run in a dedicated Windows/Linux/macOS test account.
Every platform report records the candidate commit, artifact SHA-256, OS/architecture, bootstrap
snapshots, bridge identity/PIDs, package paths before and after relocation, screenshots, redacted
fake-model transcript, data results, cleanup status, and the short manual checklist. Automated
success does not imply P2 completion: testers must change every manual checklist value to `pass`.
After all three reports and the Windows native retry report are complete, combine them:
```bash
python3 frontends/desktop/e2e/package/verify_candidate_evidence.py \
--expected-commit <candidate-sha> \
--windows <windows-production-contract-report.json> \
--linux <linux-real-package-report.json> \
--macos <macos-real-package-report.json> \
--windows-native-report <windows-e2e-report.json> \
--output <candidate-evidence-manifest.json>
```
The verifier fails on a commit mismatch, missing artifact digest, failed automated scenario,
incomplete bootstrap evidence, missing macOS app immutability proof, unfinished manual checklist,
or unclean final process/port state. The manifest is evidence for the candidate SHA; it is not
intended to be committed to the repository.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""Combine three real-package reports and enforce the P2 candidate evidence gate."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
COMMON_CHECKS = (
"packageShape",
"deterministicChat",
"uploadUnderExternalRoot",
"isolatedConductorOwnership",
"memoryImport",
"warmRestart",
"foreignListenerSurvived",
"portRecovery",
"relocation",
"staleOverrideFallback",
"optionalP2PDoesNotBlockReady",
"settingsRestored",
"finalPortFree",
"finalConductorPortFree",
"defaultConductorPortPreserved",
"ownedProcessStops",
"finalOwnedProcessesExited",
)
SUCCESSFUL_APPLICATION_SCENARIOS = {
"first-launch",
"warm-restart",
"after-port-release",
"relocated",
"stale-override",
}
def load(path: str) -> dict[str, Any]:
target = Path(path).expanduser().resolve()
try:
value = json.loads(target.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"cannot read {target}: {error}") from error
if not isinstance(value, dict):
raise ValueError(f"report is not an object: {target}")
value["_path"] = str(target)
return value
def assert_report(name: str, report: dict[str, Any], expected_commit: str) -> list[str]:
failures: list[str] = []
actual_commit = str(report.get("expectedCommit", "")).lower()
if actual_commit != expected_commit.lower():
failures.append(f"{name}: commit {actual_commit!r} != {expected_commit!r}")
if report.get("releaseVersion") != "0.2.0":
failures.append(f"{name}: release version is not 0.2.0")
if report.get("success") is not True:
failures.append(f"{name}: automated journey did not pass")
if not str(report.get("artifact", {}).get("sha256", "")):
failures.append(f"{name}: artifact SHA-256 is missing")
checks = report.get("checks", {})
for check in COMMON_CHECKS:
if not checks.get(check):
failures.append(f"{name}: required check {check} did not pass")
environment = report.get("environment")
isolated_port = (
environment.get("isolatedConductorPort") if isinstance(environment, dict) else None
)
if (
not isinstance(isolated_port, int)
or isinstance(isolated_port, bool)
or not 1 <= isolated_port <= 65535
or isolated_port in {8900, 14168}
):
failures.append(f"{name}: isolated conductor port is missing or invalid")
ownership = checks.get("isolatedConductorOwnership")
if not isinstance(ownership, dict) or set(ownership) != SUCCESSFUL_APPLICATION_SCENARIOS:
failures.append(f"{name}: conductor ownership does not cover every successful launch")
elif isinstance(isolated_port, int):
for scenario, state in ownership.items():
pid = state.get("pid") if isinstance(state, dict) else None
if (
not isinstance(state, dict)
or state.get("status") != "running"
or state.get("running") is not True
or state.get("owned") is not True
or state.get("external") is not False
or state.get("port") != isolated_port
or not isinstance(pid, int)
or isinstance(pid, bool)
or pid <= 0
):
failures.append(f"{name}: invalid conductor ownership evidence for {scenario}")
stops = checks.get("ownedProcessStops")
if not isinstance(stops, dict) or set(stops) != SUCCESSFUL_APPLICATION_SCENARIOS:
failures.append(f"{name}: owned process stops do not cover every successful launch")
else:
for scenario, pids in stops.items():
expected_conductor_pid = (
ownership.get(scenario, {}).get("pid") if isinstance(ownership, dict) else None
)
if (
not isinstance(pids, dict)
or set(pids) != {"app", "bridge", "conductor"}
or any(
not isinstance(pid, int) or isinstance(pid, bool) or pid <= 0
for pid in pids.values()
)
or len(set(pids.values())) != 3
or pids.get("conductor") != expected_conductor_pid
):
failures.append(f"{name}: invalid owned process stop evidence for {scenario}")
if name == "macos" and checks.get("macAppImmutable") is not True:
failures.append("macos: signed .app immutability did not pass")
required_bootstrap = SUCCESSFUL_APPLICATION_SCENARIOS | {"foreign-port"}
missing_bootstrap = sorted(required_bootstrap - set(report.get("bootstrap", {})))
if missing_bootstrap:
failures.append(f"{name}: missing bootstrap evidence {missing_bootstrap}")
pending = [key for key, value in report.get("manualChecklist", {}).items() if value != "pass"]
if pending:
failures.append(f"{name}: manual checklist is incomplete: {pending}")
if len(report.get("screenshots", [])) < 2 and name != "windows":
failures.append(f"{name}: fewer than two screenshots were recorded")
return failures
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--expected-commit", required=True)
parser.add_argument("--windows", required=True)
parser.add_argument("--linux", required=True)
parser.add_argument("--macos", required=True)
parser.add_argument("--windows-native-report", required=True)
parser.add_argument("--output", required=True)
args = parser.parse_args()
reports = {
"windows": load(args.windows),
"linux": load(args.linux),
"macos": load(args.macos),
}
failures: list[str] = []
for name, report in reports.items():
failures.extend(assert_report(name, report, args.expected_commit))
windows_native = load(args.windows_native_report)
if windows_native.get("success") is not True:
failures.append("windows native wrapper did not pass")
if windows_native.get("checks", {}).get("portConflictRecovery") is not True:
failures.append("windows native retry path did not pass")
if windows_native.get("checks", {}).get("settingsRestored") is not True:
failures.append("windows native wrapper did not restore the original settings file")
native_pending = [
key for key, value in windows_native.get("manualChecklist", {}).items() if value != "pass"
]
if native_pending:
failures.append(f"windows native manual checklist is incomplete: {native_pending}")
manifest = {
"schemaVersion": 1,
"candidateCommit": args.expected_commit,
"releaseVersion": "0.2.0",
"platforms": {
name: {
"report": report["_path"],
"artifactSha256": report.get("artifact", {}).get("sha256"),
"environment": report.get("environment"),
"success": report.get("success"),
}
for name, report in reports.items()
},
"windowsNativeReport": windows_native["_path"],
"gate": "pass" if not failures else "fail",
"failures": failures,
}
output = Path(args.output).expanduser().resolve()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
if failures:
for failure in failures:
print(f"FAIL: {failure}", file=sys.stderr)
return 1
print(output)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+8
View File
@@ -0,0 +1,8 @@
pytest
aiohttp>=3.9
requests>=2.28
psutil
fastapi
uvicorn
websockets
pydantic
@@ -0,0 +1,662 @@
[CmdletBinding()]
param(
[string]$Repo = "abraxas914/GenericAgent",
[Parameter(Mandatory = $true)]
[string]$RunId,
[string]$ExpectedCommit = "",
[string]$PackageZip = "",
[string]$WorkDir = "",
[ValidateSet("Full", "Smoke", "FailureOnly")]
[string]$Mode = "Full",
[switch]$KeepWorkDir
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version 2.0
if (-not $WorkDir) {
$WorkDir = Join-Path $env:TEMP ("ga-desktop-e2e\run-" + $RunId)
}
$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$WorkDir = [System.IO.Path]::GetFullPath($WorkDir)
$DownloadDir = Join-Path $WorkDir "download"
$ExtractDir = Join-Path $WorkDir "app"
$ReportDir = Join-Path $WorkDir "report"
$ScreensDir = Join-Path $ReportDir "screenshots"
$ReportPath = Join-Path $ReportDir "e2e-report.json"
$RunSucceeded = $false
$PortWasFreeAtStart = $false
$UserHome = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)
$SettingsPath = Join-Path $UserHome ".ga_desktop_settings.json"
$SettingsExisted = Test-Path -LiteralPath $SettingsPath
[byte[]]$SettingsBackupBytes = @()
$SettingsAttributes = $null
if ($SettingsExisted) {
$SettingsBackupBytes = [System.IO.File]::ReadAllBytes($SettingsPath)
$SettingsAttributes = [System.IO.File]::GetAttributes($SettingsPath)
}
$Report = [ordered]@{
repo = $Repo
runId = $RunId
expectedCommit = $ExpectedCommit
mode = $Mode
package = [ordered]@{}
environment = [ordered]@{}
checks = [ordered]@{}
screenshots = @()
manualChecklist = [ordered]@{
windowsTitlebarVisible = "manual"
exactlyThreeWindowButtons = "manual"
titlebarBlankAreaDrags = "manual"
buttonAreaDoesNotDrag = "manual"
minimizeWorks = "manual"
maximizeRestoreWorks = "manual"
closeHidesToTray = "manual"
sidebarNavHasNoBlankRow = "manual"
nativeDirectoryPicker = "manual"
shortcutSelfHealsAfterMove = "manual"
loadingFallbackAndMainRender = "manual"
}
failures = @()
startedAt = (Get-Date).ToUniversalTime().ToString("o")
}
function Write-Step([string]$Message) {
Write-Host ""
Write-Host "==> $Message"
}
function Add-Failure([string]$Message) {
$script:Report.failures += $Message
}
function Save-Report {
$script:Report.completedAt = (Get-Date).ToUniversalTime().ToString("o")
$json = $script:Report | ConvertTo-Json -Depth 12
[System.IO.Directory]::CreateDirectory($ReportDir) | Out-Null
[System.IO.File]::WriteAllText($ReportPath, $json, [System.Text.UTF8Encoding]::new($false))
}
function Fail([string]$Message) {
Add-Failure $Message
Save-Report
throw $Message
}
function Ensure-Dir([string]$Path) {
[System.IO.Directory]::CreateDirectory($Path) | Out-Null
}
function Test-Command([string]$Name) {
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
return $null -ne $cmd
}
function Normalize-Commit([string]$Commit) {
return ($Commit -replace "\s", "").ToLowerInvariant()
}
function Test-CommitMatches([string]$Actual, [string]$Expected) {
if (-not $Expected) { return $true }
$actualNorm = Normalize-Commit $Actual
$expectedNorm = Normalize-Commit $Expected
return $actualNorm.StartsWith($expectedNorm) -or $expectedNorm.StartsWith($actualNorm)
}
function Get-RunMetadata {
if (-not (Test-Command "gh")) {
Fail "GitHub CLI 'gh' is required when -PackageZip is not enough to skip run metadata."
}
$json = & gh run view $RunId --repo $Repo --json headSha,status,conclusion,event,name 2>&1
if ($LASTEXITCODE -ne 0) {
Fail "gh run view failed: $json"
}
$meta = ($json | Out-String) | ConvertFrom-Json
$script:Report.run = $meta
if ($ExpectedCommit -and -not (Test-CommitMatches $meta.headSha $ExpectedCommit)) {
Fail "Run headSha $($meta.headSha) does not match expected commit $ExpectedCommit"
}
return $meta
}
function Resolve-PackageZip {
Ensure-Dir $DownloadDir
if ($PackageZip) {
$zipPath = [System.IO.Path]::GetFullPath($PackageZip)
if (-not (Test-Path -LiteralPath $zipPath)) {
Fail "Package zip not found: $zipPath"
}
return $zipPath
}
if (-not (Test-Command "gh")) {
Fail "GitHub CLI 'gh' is required to download artifacts."
}
Write-Step "Download GitHub Actions artifact"
$downloadOutput = & gh run download $RunId --repo $Repo --dir $DownloadDir 2>&1
if ($LASTEXITCODE -ne 0) {
Fail "gh run download failed: $downloadOutput"
}
$zips = @(Get-ChildItem -LiteralPath $DownloadDir -Recurse -Filter "GenericAgent-Desktop-Windows-Portable.zip")
if ($zips.Count -ne 1) {
Fail "Expected exactly one GenericAgent-Desktop-Windows-Portable.zip under $DownloadDir, found $($zips.Count)"
}
return $zips[0].FullName
}
function Test-Sha256([string]$ZipPath) {
Write-Step "Verify SHA-256"
$hash = (Get-FileHash -LiteralPath $ZipPath -Algorithm SHA256).Hash.ToLowerInvariant()
$shaFile = $ZipPath + ".sha256"
if (-not (Test-Path -LiteralPath $shaFile)) {
$matches = @(Get-ChildItem -LiteralPath (Split-Path -Parent $ZipPath) -Recurse -File | Where-Object {
$_.Name -like "*.sha256" -or $_.Name -like "SHA256SUMS*.txt"
})
if ($matches.Count -eq 1) {
$shaFile = $matches[0].FullName
}
}
if (Test-Path -LiteralPath $shaFile) {
$expectedText = [System.IO.File]::ReadAllText($shaFile)
$expectedHash = ([regex]::Match($expectedText, "[A-Fa-f0-9]{64}")).Value.ToLowerInvariant()
if (-not $expectedHash) {
Fail "Could not parse SHA-256 from $shaFile"
}
if ($hash -ne $expectedHash) {
Fail "SHA-256 mismatch. actual=$hash expected=$expectedHash"
}
$script:Report.package.sha256File = $shaFile
$script:Report.package.expectedSha256 = $expectedHash
} else {
Fail "SHA-256 sidecar not found next to $ZipPath"
}
$script:Report.package.sha256 = $hash
}
function Expand-Package([string]$ZipPath) {
Write-Step "Extract package"
if (Test-Path -LiteralPath $ExtractDir) {
Remove-Item -LiteralPath $ExtractDir -Recurse -Force
}
Ensure-Dir $ExtractDir
Expand-Archive -LiteralPath $ZipPath -DestinationPath $ExtractDir -Force
$packageRoot = Join-Path $ExtractDir "GenericAgent-Desktop-Windows-Portable"
if (-not (Test-Path -LiteralPath $packageRoot)) {
$dirs = @(Get-ChildItem -LiteralPath $ExtractDir -Directory)
if ($dirs.Count -eq 1) {
$packageRoot = $dirs[0].FullName
}
}
if (-not (Test-Path -LiteralPath $packageRoot)) {
Fail "Could not find extracted package root under $ExtractDir"
}
return [System.IO.Path]::GetFullPath($packageRoot)
}
function Assert-PackageShape([string]$PackageRoot) {
Write-Step "Validate package structure"
$required = @(
"GenericAgent.exe",
"runtime\install_windows.ps1",
"runtime\wheels",
"runtime\python",
"runtime\app\frontends\desktop_bridge.py"
)
foreach ($rel in $required) {
$path = Join-Path $PackageRoot $rel
if (-not (Test-Path -LiteralPath $path)) {
Fail "Package is missing required path: $rel"
}
}
$script:Report.package.root = $PackageRoot
$script:Report.package.exe = Join-Path $PackageRoot "GenericAgent.exe"
$script:Report.package.form = "windows-portable-zip"
}
function Get-WindowsEnvironment {
$os = Get-CimInstance Win32_OperatingSystem
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$arch = if ($env:PROCESSOR_ARCHITECTURE) { $env:PROCESSOR_ARCHITECTURE } else { [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() }
return [ordered]@{
caption = $os.Caption
version = $os.Version
buildNumber = $os.BuildNumber
architecture = $arch
cpu = $cpu.Name
userInteractive = [Environment]::UserInteractive
webView2Installed = Test-WebView2Installed
}
}
function Test-WebView2Installed {
$keys = @(
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
"HKLM:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
"HKCU:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
)
foreach ($key in $keys) {
if (Test-Path $key) { return $true }
}
return $false
}
function Get-PortState {
try {
$connections = @(Get-NetTCPConnection -LocalAddress "127.0.0.1" -LocalPort 14168 -ErrorAction SilentlyContinue)
return @($connections | Select-Object LocalAddress, LocalPort, State, OwningProcess)
} catch {
return @()
}
}
function Get-GaProcesses {
return @(Get-Process | Where-Object {
$_.ProcessName -like "GenericAgent*" -or $_.ProcessName -like "ga-desktop*" -or $_.ProcessName -like "python*"
} | Select-Object ProcessName, Id, Path)
}
function Stop-ProcessTreeSafe([int]$ProcessId) {
try {
Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
} catch {}
}
function Capture-Screenshot([string]$Name) {
Ensure-Dir $ScreensDir
$path = Join-Path $ScreensDir $Name
try {
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
$bitmap = [System.Drawing.Bitmap]::new($bounds.Width, $bounds.Height)
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
$bitmap.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
$graphics.Dispose()
$bitmap.Dispose()
$script:Report.screenshots += $path
return $path
} catch {
Add-Failure "Screenshot failed for ${Name}: $($_.Exception.Message)"
return $null
}
}
function Wait-ForBootstrapPhase(
[string]$ReportDirectory,
[string]$Phase,
[int]$TimeoutSeconds,
[int]$AfterSequence = 0
) {
$latest = Join-Path $ReportDirectory "bootstrap-latest.json"
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
if (Test-Path -LiteralPath $latest) {
try {
$snapshot = [System.IO.File]::ReadAllText($latest) | ConvertFrom-Json
if ([int]$snapshot.seq -le $AfterSequence) {
Start-Sleep -Milliseconds 500
continue
}
if ($snapshot.phase -eq $Phase) {
return $snapshot
}
if ($Phase -ne "failed" -and $snapshot.phase -eq "failed") {
return $snapshot
}
} catch {}
}
Start-Sleep -Milliseconds 500
}
return $null
}
function Wait-ForBootstrapAnyPhase([string]$ReportDirectory, [string[]]$Phases, [int]$TimeoutSeconds) {
$latest = Join-Path $ReportDirectory "bootstrap-latest.json"
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
if (Test-Path -LiteralPath $latest) {
try {
$snapshot = [System.IO.File]::ReadAllText($latest) | ConvertFrom-Json
if ($Phases -contains [string]$snapshot.phase) {
return $snapshot
}
} catch {}
}
Start-Sleep -Milliseconds 500
}
return $null
}
function Wait-ForBridgeIdentity([string]$ExpectedRoot, [int]$TimeoutSeconds) {
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
try {
$identity = Invoke-RestMethod -Uri "http://127.0.0.1:14168/services/identity" -TimeoutSec 2
if ($identity.ga_root) {
$reported = [System.IO.Path]::GetFullPath([string]$identity.ga_root).TrimEnd('\')
$expected = [System.IO.Path]::GetFullPath($ExpectedRoot).TrimEnd('\')
if ($reported.Equals($expected, [System.StringComparison]::OrdinalIgnoreCase)) {
return $identity
}
}
} catch {}
Start-Sleep -Milliseconds 700
}
return $null
}
function Start-GenericAgent([string]$PackageRoot, [string]$Scenario) {
$exe = Join-Path $PackageRoot "GenericAgent.exe"
$scenarioReportDir = Join-Path $ReportDir $Scenario
Ensure-Dir $scenarioReportDir
$env:GA_DESKTOP_E2E_REPORT_DIR = $scenarioReportDir
try {
return Start-Process -FilePath $exe -WorkingDirectory $PackageRoot -PassThru
} finally {
Remove-Item Env:\GA_DESKTOP_E2E_REPORT_DIR -ErrorAction SilentlyContinue
}
}
function Stop-GenericAgent {
foreach ($proc in @(Get-Process GenericAgent -ErrorAction SilentlyContinue)) {
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
}
foreach ($proc in @(Get-Process ga-desktop -ErrorAction SilentlyContinue)) {
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
}
}
function Stop-BridgeOnPort {
foreach ($conn in @(Get-PortState)) {
if ($conn.OwningProcess) {
try {
$proc = Get-Process -Id $conn.OwningProcess -ErrorAction Stop
if ($proc.ProcessName -like "python*" -or $proc.ProcessName -like "GenericAgent*" -or $proc.ProcessName -like "ga-desktop*") {
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
}
} catch {}
}
}
}
function Invoke-FreshLaunch([string]$PackageRoot) {
Write-Step "Fresh launch and first-run prepare"
$marker = Join-Path $PackageRoot "runtime\.prepared"
if (Test-Path -LiteralPath $marker) {
Fail "Fresh package already has runtime\\.prepared before launch"
}
$proc = Start-GenericAgent $PackageRoot "fresh"
$script:Report.checks.freshProcessId = $proc.Id
Start-Sleep -Seconds 2
Capture-Screenshot "loading-first.png" | Out-Null
$prepSnapshot = Wait-ForBootstrapAnyPhase (Join-Path $ReportDir "fresh") @("preparing", "ready", "failed") 30
if ($prepSnapshot -and $prepSnapshot.phase -eq "preparing") {
Capture-Screenshot "preparing.png" | Out-Null
} else {
$prepPhase = if ($prepSnapshot) { [string]$prepSnapshot.phase } else { "unavailable" }
$script:Report.checks.preparingScreenshot = "not captured; phase was $prepPhase"
}
$snapshot = Wait-ForBootstrapPhase (Join-Path $ReportDir "fresh") "ready" 240
if (-not $snapshot -or $snapshot.phase -ne "ready") {
Capture-Screenshot "fresh-failed.png" | Out-Null
Fail "Fresh launch did not reach bootstrap ready"
}
if (-not (Test-Path -LiteralPath $marker)) {
Fail "Fresh launch reached ready but runtime\\.prepared was not created"
}
$identity = Wait-ForBridgeIdentity (Join-Path $PackageRoot "runtime\app") 30
if (-not $identity) {
Fail "Bridge identity did not match extracted runtime app"
}
Capture-Screenshot "main-ready.png" | Out-Null
$script:Report.checks.freshReady = $true
$script:Report.checks.bridgeIdentity = $identity
}
function Start-ForeignPortListener {
Write-Step "Start unknown foreign listener on 127.0.0.1:14168"
$ready = Join-Path $WorkDir "foreign-listener.ready"
Remove-Item -LiteralPath $ready -Force -ErrorAction SilentlyContinue
$fixture = Join-Path $ScriptRoot "fixtures\foreign-port-listener.ps1"
$proc = Start-Process -FilePath "powershell.exe" -ArgumentList @(
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", $fixture,
"-Port", "14168",
"-ReadyFile", $ready
) -WindowStyle Hidden -PassThru
$deadline = (Get-Date).AddSeconds(20)
while ((Get-Date) -lt $deadline) {
if ((Test-Path -LiteralPath $ready) -and (Get-PortState).Count -gt 0) {
return $proc
}
Start-Sleep -Milliseconds 250
}
Stop-ProcessTreeSafe $proc.Id
Fail "Foreign listener did not bind 127.0.0.1:14168"
}
function Invoke-SetupRetry(
[System.Diagnostics.Process]$AppProcess,
[int]$TimeoutSeconds,
[int]$AfterSequence
) {
Add-Type -AssemblyName System.Windows.Forms
$sig = @"
using System;
using System.Runtime.InteropServices;
public static class Win32Focus {
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
}
"@
Add-Type -TypeDefinition $sig -ErrorAction SilentlyContinue
try {
$AppProcess.Refresh()
if ($AppProcess.MainWindowHandle -ne [IntPtr]::Zero) {
[Win32Focus]::ShowWindow($AppProcess.MainWindowHandle, 9) | Out-Null
[Win32Focus]::SetForegroundWindow($AppProcess.MainWindowHandle) | Out-Null
Start-Sleep -Milliseconds 500
[System.Windows.Forms.SendKeys]::SendWait("{TAB}{TAB}{ENTER}")
}
} catch {
Add-Failure "Automatic setup retry keypress failed: $($_.Exception.Message)"
}
$snapshot = Wait-ForBootstrapPhase `
(Join-Path $ReportDir "port-conflict") "ready" $TimeoutSeconds $AfterSequence
if ($snapshot -and $snapshot.phase -eq "ready") {
$script:Report.checks.setupRetry = "automatic"
return $true
}
Write-Host ""
Write-Host "Manual action required: click 'Retry startup' in the setup window."
$snapshot = Wait-ForBootstrapPhase `
(Join-Path $ReportDir "port-conflict") "ready" 180 $AfterSequence
if ($snapshot -and $snapshot.phase -eq "ready") {
$script:Report.checks.setupRetry = "manual"
return $true
}
return $false
}
function Invoke-PortConflictScenario([string]$PackageRoot) {
Write-Step "Port conflict recovery scenario"
Stop-GenericAgent
Stop-BridgeOnPort
Start-Sleep -Seconds 2
$listener = Start-ForeignPortListener
$script:Report.checks.foreignListenerPid = $listener.Id
$appProc = Start-GenericAgent $PackageRoot "port-conflict"
Start-Sleep -Seconds 2
$failed = Wait-ForBootstrapPhase (Join-Path $ReportDir "port-conflict") "failed" 80
if (-not $failed -or $failed.phase -ne "failed") {
Stop-ProcessTreeSafe $listener.Id
Fail "Port conflict scenario did not reach failed phase"
}
if ($failed.failure.code -ne "port_conflict") {
Stop-ProcessTreeSafe $listener.Id
Fail "Expected port_conflict, got $($failed.failure.code)"
}
try {
Get-Process -Id $listener.Id -ErrorAction Stop | Out-Null
$script:Report.checks.foreignListenerSurvived = $true
} catch {
Fail "Foreign listener was not alive after port_conflict"
}
Capture-Screenshot "setup-failure.png" | Out-Null
Stop-ProcessTreeSafe $listener.Id
Start-Sleep -Seconds 2
if (-not (Invoke-SetupRetry $appProc 40 ([int]$failed.seq))) {
Fail "Setup retry did not reach ready after releasing port"
}
Capture-Screenshot "setup-retry-ready.png" | Out-Null
$script:Report.checks.portConflictRecovery = $true
}
function Invoke-ProductionContractJourney(
[string]$PackageRoot,
[string]$ZipPath
) {
Write-Step "Run production package chat, data, relocation, and fallback journey"
if (-not $ExpectedCommit) {
Fail "Full mode requires -ExpectedCommit for the production build identity assertion"
}
Stop-GenericAgent
Stop-BridgeOnPort
Start-Sleep -Seconds 2
# The package itself must be movable while the journey is running. Running the driver
# with runtime\python\python.exe would keep that executable open and make Windows reject
# the relocation, so copy the already-prepared embedded interpreter outside the package.
$driverPythonRoot = Join-Path $WorkDir "production-contract-python"
if (Test-Path -LiteralPath $driverPythonRoot) {
Remove-Item -LiteralPath $driverPythonRoot -Recurse -Force
}
Copy-Item -LiteralPath (Join-Path $PackageRoot "runtime\python") `
-Destination $driverPythonRoot -Recurse -Force
$python = Join-Path $driverPythonRoot "python.exe"
if (-not (Test-Path -LiteralPath $python)) {
Fail "Could not create an out-of-package Python for the relocation journey"
}
$driver = [System.IO.Path]::GetFullPath((Join-Path $ScriptRoot "..\package\real_package_journey.py"))
$contractReport = Join-Path $ReportDir "production-contract"
$contractWork = Join-Path $WorkDir "production-contract-work"
$relocated = Join-Path $ExtractDir "relocated\含 空格\GenericAgent 包"
& $python $driver `
--platform windows `
--artifact $ZipPath `
--expected-commit $ExpectedCommit `
--package-root $PackageRoot `
--application-relative "GenericAgent.exe" `
--runtime-relative "runtime" `
--relocated-root $relocated `
--report-dir $contractReport `
--work-dir $contractWork `
--allow-user-settings-mutation `
--screenshots-optional
if ($LASTEXITCODE -ne 0) {
Fail "Production package contract journey failed; see $contractReport"
}
$script:Report.checks.productionContract = [System.IO.File]::ReadAllText(
(Join-Path $contractReport "real-package-report.json")
) | ConvertFrom-Json
}
try {
Ensure-Dir $WorkDir
Ensure-Dir $ReportDir
Ensure-Dir $ScreensDir
$Report.environment = Get-WindowsEnvironment
if (-not $PackageZip) {
Write-Step "Read GitHub Actions run metadata"
Get-RunMetadata | Out-Null
} elseif ($ExpectedCommit) {
$Report.run = [ordered]@{ headSha = $ExpectedCommit; source = "local-package" }
}
$zip = Resolve-PackageZip
$Report.package.zip = $zip
Test-Sha256 $zip
$packageRoot = Expand-Package $zip
Assert-PackageShape $packageRoot
$Report.environment.initialPortState = @(Get-PortState)
$Report.environment.initialProcesses = @(Get-GaProcesses)
if ($Report.environment.initialPortState.Count -gt 0) {
Fail "127.0.0.1:14168 was already occupied before the package journey"
}
$script:PortWasFreeAtStart = $true
if ($Mode -eq "Smoke" -or $Mode -eq "Full") {
Invoke-FreshLaunch $packageRoot
}
if ($Mode -eq "FailureOnly" -or $Mode -eq "Full") {
Invoke-PortConflictScenario $packageRoot
}
if ($Mode -eq "Full") {
Invoke-ProductionContractJourney $packageRoot $zip
}
Stop-GenericAgent
Stop-BridgeOnPort
Start-Sleep -Seconds 2
$Report.environment.finalPortState = @(Get-PortState)
$Report.environment.finalProcesses = @(Get-GaProcesses)
if ($Report.environment.finalPortState.Count -gt 0) {
Fail "Package journey left a listener on 127.0.0.1:14168"
}
$Report.success = $true
$script:RunSucceeded = $true
Save-Report
Write-Host ""
Write-Host "E2E report: $ReportPath"
} catch {
$Report.success = $false
Add-Failure $_.Exception.Message
$Report.environment.finalPortState = @(Get-PortState)
$Report.environment.finalProcesses = @(Get-GaProcesses)
Save-Report
Write-Error $_.Exception.Message
exit 1
} finally {
if ($script:PortWasFreeAtStart) {
Stop-GenericAgent
Stop-BridgeOnPort
}
if ($SettingsExisted) {
[System.IO.File]::WriteAllBytes($SettingsPath, $SettingsBackupBytes)
if ($null -ne $SettingsAttributes) {
[System.IO.File]::SetAttributes($SettingsPath, $SettingsAttributes)
}
} else {
Remove-Item -LiteralPath $SettingsPath -Force -ErrorAction SilentlyContinue
}
$script:Report.checks.settingsRestored = if ($SettingsExisted) {
[Convert]::ToBase64String([System.IO.File]::ReadAllBytes($SettingsPath)) -eq `
[Convert]::ToBase64String($SettingsBackupBytes)
} else {
-not (Test-Path -LiteralPath $SettingsPath)
}
Save-Report
if ($script:RunSucceeded -and -not $KeepWorkDir) {
Remove-Item -LiteralPath $ExtractDir -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $DownloadDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "Report kept at: $ReportDir"
} else {
Write-Host "WorkDir kept for diagnostics: $WorkDir"
}
}
+62
View File
@@ -0,0 +1,62 @@
# Windows Desktop E2E
This folder contains the Windows validation harness for the portable desktop package. It simulates the user path:
1. Download a GitHub Actions artifact, or use a local zip.
2. Verify commit, SHA-256, and required package files.
3. Extract to a clean directory.
4. Launch `GenericAgent.exe` directly.
5. Wait for first-run prepare, bridge identity, and bootstrap `ready`.
6. In `Full` mode, inject an unknown process on port `14168`, verify `port_conflict`, release it, and retry from setup.
7. Run the shared production-package contract with the embedded Python: package-owned bridge plus
external `GA_ROOT`, fake-model chat, upload, memory import, warm restart, a second foreign-port
assertion, relocation into a path with spaces and Chinese characters, stale override fallback,
optional P2P degradation, and process/settings cleanup.
## Full Run
```powershell
.\frontends\desktop\e2e\windows\Invoke-WindowsUserJourney.ps1 `
-Repo abraxas914/GenericAgent `
-RunId 29071095889 `
-ExpectedCommit 696ddfc `
-Mode Full
```
Use `-PackageZip C:\path\GenericAgent-Desktop-Windows-Portable.zip` when the artifact has expired or has already been downloaded.
## Modes
- `Smoke`: package verification, extraction, first launch, prepare marker, bridge identity, bootstrap ready.
- `FailureOnly`: assumes the package can be extracted and focuses on the unknown port conflict and setup retry path.
- `Full`: runs `Smoke`, the native retry failure path, and the complete production package journey.
It requires `-ExpectedCommit` so the bridge build identity is tied to the candidate SHA.
## Manual Checks
The script collects screenshots and writes these checklist items to the report for the tester to mark externally:
- Loading, prepare, setup, and main windows always show the Windows titlebar.
- Right side has exactly minimize, maximize, and close.
- Blank titlebar area drags the window; button area does not.
- Minimize works.
- Maximize and restore work.
- Close hides to tray instead of exiting.
- Sidebar nav sits directly below the custom titlebar with no blank row.
- The native directory picker opens and returns a real directory.
- The shortcut self-heals after the portable folder is moved.
- Loading, fallback, and main React pages render through the Tauri resource protocol.
## Report
Reports are written under `<WorkDir>\report`:
- `e2e-report.json`
- `bootstrap-events.jsonl`
- `bootstrap-latest.json`
- screenshots such as `loading-first.png`, `main-ready.png`, and `setup-failure.png`
- `production-contract/real-package-report.json`, including artifact SHA-256, OS/architecture,
bootstrap phases, bridge identities, PIDs/ports, before/after paths, deterministic data results,
and cleanup state
Failures exit non-zero and keep diagnostics unless `-KeepWorkDir` is omitted and cleanup succeeds.
@@ -0,0 +1,35 @@
param(
[int]$Port = 14168,
[string]$ReadyFile = ""
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version 2.0
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), $Port)
$listener.Start()
if ($ReadyFile) {
$dir = Split-Path -Parent $ReadyFile
if ($dir) {
[System.IO.Directory]::CreateDirectory($dir) | Out-Null
}
[System.IO.File]::WriteAllText($ReadyFile, "ready", [System.Text.Encoding]::ASCII)
}
try {
while ($true) {
$client = $listener.AcceptTcpClient()
try {
$stream = $client.GetStream()
$body = "foreign listener"
$response = "HTTP/1.1 404 Not Found`r`nContent-Type: text/plain`r`nContent-Length: $($body.Length)`r`nConnection: close`r`n`r`n$body"
$bytes = [System.Text.Encoding]::ASCII.GetBytes($response)
$stream.Write($bytes, 0, $bytes.Length)
} finally {
$client.Close()
}
}
} finally {
$listener.Stop()
}
@@ -0,0 +1,385 @@
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { access, readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const desktopDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const repoRoot = path.resolve(desktopDir, '..', '..');
const distDir = path.join(desktopDir, 'dist');
const expectedVersion = '0.2.0';
const expectedSourceCommit = '21f12f6dfe57ada8572bb21fe6604caac230b1cf';
const expectedNoticeSha256 = '2acf865e87e59090121369aac0575467067fdd7999923a70d785a46ceae3330f';
const noticeAttributeRules = [
'frontends/desktop/public/THIRD_PARTY_NOTICES.txt text eol=lf',
'frontends/desktop/dist/THIRD_PARTY_NOTICES.txt text eol=lf',
];
const excludedManifestFiles = new Set(['README.md', 'build-provenance.json']);
const removedLegacyAssets = [
'styles.css',
'i18n.js',
'phosphor-icons.js',
'vendor/marked.min.js',
'assets/fonts/fonts.css',
'assets/fonts/README.md',
'assets/fonts/azonix-wordmark.woff2',
'assets/fonts/jetbrains-mono-latin.woff2',
'assets/fonts/lexend-latin.woff2',
'assets/fonts/noto-sans-latin.woff2',
];
const semiPackages = [
'@douyinfe/semi-animation',
'@douyinfe/semi-animation-react',
'@douyinfe/semi-animation-styled',
'@douyinfe/semi-foundation',
'@douyinfe/semi-icons',
'@douyinfe/semi-illustrations',
'@douyinfe/semi-json-viewer-core',
'@douyinfe/semi-theme-default',
'@douyinfe/semi-ui',
];
function fail(message) {
throw new Error(message);
}
async function exists(file) {
try {
await access(file);
return true;
} catch {
return false;
}
}
async function readJson(file) {
return JSON.parse(await readFile(file, 'utf8'));
}
async function walk(dir) {
const entries = await readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) files.push(...await walk(fullPath));
if (entry.isFile()) files.push(fullPath);
}
return files;
}
function sha256(contents) {
return createHash('sha256').update(contents).digest('hex');
}
function relativeToDist(file) {
return path.relative(distDir, file).split(path.sep).join('/');
}
async function assertExists(relativePath) {
if (!await exists(path.join(distDir, relativePath))) {
fail(`missing compiled asset: ${relativePath}`);
}
}
function workflowJob(workflow, name) {
const match = new RegExp(`^ ${name}:\\s*$`, 'm').exec(workflow);
if (!match) return '';
const remainder = workflow.slice(match.index + match[0].length);
const nextJob = /^ [a-zA-Z0-9_-]+:\s*$/m.exec(remainder);
return nextJob ? remainder.slice(0, nextJob.index) : remainder;
}
async function verifyHtmlReferences(relativePath) {
const html = await readFile(path.join(distDir, relativePath), 'utf8');
const references = [...html.matchAll(/\b(?:src|href)=["']([^"']+)["']/g)].map((match) => match[1]);
for (const reference of references) {
if (/^(?:[a-z]+:|#|\/\/)/i.test(reference)) continue;
const cleanReference = reference.split(/[?#]/, 1)[0].replace(/^\//, '');
if (cleanReference) await assertExists(cleanReference);
}
}
function verifyTrackedSourceBoundary() {
const tracked = execFileSync('git', ['ls-files'], { cwd: repoRoot, encoding: 'utf8' })
.split('\n')
.filter(Boolean);
const forbidden = tracked.filter((file) => (
file.startsWith('frontends/desktop/src/')
|| file.startsWith('frontends/desktop/public/')
|| (file.startsWith('frontends/desktop/e2e/')
&& file !== 'frontends/desktop/e2e/requirements.txt'
&& !file.startsWith('frontends/desktop/e2e/package/')
&& !file.startsWith('frontends/desktop/e2e/linux/')
&& !file.startsWith('frontends/desktop/e2e/macos/')
&& !file.startsWith('frontends/desktop/e2e/windows/'))
|| /^frontends\/desktop\/(?:index|loading|setup)\.html$/.test(file)
|| /^frontends\/desktop\/(?:vite\.config\.[^/]+|tsconfig(?:\.[^/]+)?\.json|package-lock\.json)$/.test(file)
));
if (forbidden.length > 0) {
fail(`tracked React development files crossed the compiled-only boundary: ${forbidden.join(', ')}`);
}
}
async function verifyNoticeAttributes() {
const attributes = await readFile(path.join(repoRoot, '.gitattributes'), 'utf8');
const lines = attributes.split(/\r?\n/);
for (const rule of noticeAttributeRules) {
const noticePath = rule.split(/\s+/, 1)[0];
const matchingRules = lines.filter((line) => line.trim().split(/\s+/, 1)[0] === noticePath);
if (matchingRules.length !== 1 || matchingRules[0] !== rule) {
fail(`third-party notice must have one exact LF attribute rule: ${rule}`);
}
}
}
async function verifyVersionsRendererAndEntry() {
const packageJson = await readJson(path.join(desktopDir, 'package.json'));
const tauriConfig = await readJson(path.join(desktopDir, 'src-tauri', 'tauri.conf.json'));
const tauriE2eConfig = await readJson(path.join(desktopDir, 'src-tauri', 'tauri.e2e.conf.json'));
const cargoToml = await readFile(path.join(desktopDir, 'src-tauri', 'Cargo.toml'), 'utf8');
const cargoLock = await readFile(path.join(desktopDir, 'src-tauri', 'Cargo.lock'), 'utf8');
const desktopShell = await readFile(path.join(desktopDir, 'src-tauri', 'src', 'lib.rs'), 'utf8');
if (packageJson.version !== expectedVersion) fail(`package.json version must be ${expectedVersion}`);
if (tauriConfig.version !== expectedVersion) fail(`tauri.conf.json version must be ${expectedVersion}`);
if (tauriConfig.build?.frontendDist !== '../dist') fail('Tauri frontendDist must be ../dist');
for (const sourceBuildKey of ['beforeBuildCommand', 'beforeDevCommand', 'devUrl']) {
if (sourceBuildKey in tauriConfig.build) fail(`compiled-only Tauri config must not require ${sourceBuildKey}`);
}
if (!/^\[package\][\s\S]*?^name = "ga-desktop"$[\s\S]*?^version = "0\.2\.0"$/m.test(cargoToml)) {
fail(`Cargo.toml ga-desktop version must be ${expectedVersion}`);
}
if (!/^\[\[package\]\][\s\S]*?^name = "ga-desktop"$\nversion = "0\.2\.0"$/m.test(cargoLock)) {
fail(`Cargo.lock ga-desktop version must be ${expectedVersion}`);
}
const productionSecurity = tauriConfig.app?.security;
if (!productionSecurity?.csp || typeof productionSecurity.csp !== 'object') {
fail('production renderer must use an explicit CSP');
}
if (String(productionSecurity.csp['script-src']).includes('unsafe-eval')) {
fail('production CSP must not enable unsafe-eval');
}
if (!String(productionSecurity.csp['connect-src']).includes('127.0.0.1:14168')) {
fail('production CSP must retain the Desktop loopback bridge');
}
if (JSON.stringify(productionSecurity).includes('wdio:')) {
fail('production Tauri security config must not grant WebDriver permissions');
}
if (!JSON.stringify(tauriE2eConfig).includes('wdio:default')) {
fail('E2E-only Tauri config must retain its isolated WebDriver capability');
}
if (!desktopShell.includes('fn main_ui_url_from_current')) {
fail('desktop shell must resolve index.html from the active Tauri asset origin');
}
if (/tauri::Url::parse\("http:\/\/127\.0\.0\.1:14168\/?"\)/.test(desktopShell)) {
fail('desktop shell must not navigate the renderer to the legacy bridge root');
}
}
async function verifyReleaseContract() {
const workflow = await readFile(
path.join(repoRoot, '.github', 'workflows', 'desktop-release-package.yml'),
'utf8',
);
const buildJobs = ['build-windows', 'build-linux', 'build-macos'];
const tauriCommands = {
'build-windows': 'npm run tauri build -- --bundles nsis',
'build-linux': 'npm run tauri build -- --bundles appimage',
'build-macos': 'npm run tauri build -- --bundles app',
};
const jobNames = [...workflow.slice(workflow.search(/^jobs:\s*$/m)).matchAll(/^ ([a-zA-Z0-9_-]+):\s*$/gm)]
.map((match) => match[1]);
if (JSON.stringify(jobNames) !== JSON.stringify([...buildJobs, 'publish-release'])) {
fail('release workflow must contain exactly three builders and one publisher');
}
if (!/^permissions:\n contents: read\s*$/m.test(workflow)) {
fail('release workflow must default to contents: read');
}
if ((workflow.match(/^ contents: write\s*$/gm) ?? []).length !== 1) {
fail('release workflow must grant contents: write exactly once');
}
if ((workflow.match(/^ run: npm run test:dist$/gm) ?? []).length !== 3) {
fail('all three builders must run the compiled renderer byte contract');
}
if (/\bnpm ci\b|\bnpm run build\b|\b(?:npx|npm exec)\s+vite\b/.test(workflow)) {
fail('compiled-only release workflow must not require the React source toolchain');
}
for (const job of buildJobs) {
const body = workflowJob(workflow, job);
if (!body) fail(`missing release builder: ${job}`);
if (!/^ permissions:\n contents: read\s*$/m.test(body) || body.includes('contents: write')) {
fail(`${job} must have read-only repository permission`);
}
if (!/uses: actions\/checkout@[0-9a-f]{40}[^\n]*\n with:\n persist-credentials: false/.test(body)) {
fail(`${job} checkout must use a full SHA and disable persisted credentials`);
}
if (body.includes('secrets.GITHUB_TOKEN') || /\bgh release\b/.test(body)) {
fail(`${job} must not receive a release token or publish`);
}
if (!body.includes('npm install --package-lock=false')
|| !body.includes("if(v!=='2.11.4')")
|| !body.includes('test ! -e package-lock.json')
|| body.includes('npm ci')) {
fail(`${job} must install only the submitted minimal Tauri package manifest`);
}
const adjacentGate = new RegExp(
' - name: Verify tracked compiled renderer bytes\\n'
+ ' working-directory: frontends/desktop\\n'
+ ' run: npm run test:dist\\n\\n'
+ ' - name: [^\\n]+\\n'
+ ' working-directory: frontends/desktop\\n'
+ ` run: ${tauriCommands[job].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
);
if (!adjacentGate.test(body)) {
fail(`${job} must validate notice bytes immediately before Tauri embeds dist`);
}
if (!(body.includes("--exclude='./frontends/desktop/dist'")
|| body.includes("--exclude='frontends/desktop/dist'"))) {
fail(`${job} must not duplicate Tauri-embedded dist inside runtime/app`);
}
if (!(body.includes('test ! -e "$RUNTIME/app/frontends/desktop/dist"')
|| body.includes('test ! -e "$RUNTIME_SRC/app/frontends/desktop/dist"'))) {
fail(`${job} must verify dist is not duplicated inside runtime/app`);
}
}
const windows = workflowJob(workflow, 'build-windows');
if (!windows.includes('cygpath -u "$RUNNER_TEMP"')
|| !windows.includes('PBS_ARCHIVE="${RUNNER_TEMP_POSIX}/pbs-windows-x86_64.tar.gz"')) {
fail('Windows packaging must convert RUNNER_TEMP with cygpath before POSIX tools use it');
}
const macos = workflowJob(workflow, 'build-macos');
if (!macos.includes('runs-on: macos-15')
|| !macos.includes('test "$(uname -m)" = arm64')
|| !workflow.includes('MACOS_PACKAGING_PYTHON_VERSION: "3.12.10"')
|| !workflow.includes('PBS_PYTHON_VERSION: "3.12.14"')) {
fail('macOS packaging must use the pinned arm64 host and separate Python inputs');
}
const linux = workflowJob(workflow, 'build-linux');
if (!linux.includes('runs-on: ubuntu-22.04')
|| !linux.includes('prefix-key: "v1-rust-release-ubuntu-22.04-glibc-2.35"')
|| linux.includes('prefix-key: "v0-rust"')
|| !linux.includes('cache-targets: "false"')
|| !linux.includes('cache-bin: "false"')) {
fail('Linux packaging must isolate its Ubuntu 22.04/glibc 2.35 Rust cache ABI');
}
const publisher = workflowJob(workflow, 'publish-release');
if (!publisher.includes('needs: [build-windows, build-linux, build-macos]')
|| !publisher.includes("github.event_name == 'push'")
|| !publisher.includes('refs/tags/desktop-portable-')
|| !/^ permissions:\n contents: write\s*$/m.test(publisher)) {
fail('publisher must be the sole tag-only writer after all three builders');
}
const publisherRun = publisher.slice(publisher.indexOf(' run: |'));
if (!publisher.includes('TAG_NAME: ${{ github.ref_name }}')
|| publisherRun.includes('${{ github.ref_name }}')
|| !publisherRun.includes('^desktop-portable-[A-Za-z0-9._-]+$')) {
fail('publisher must pass the tag through env and validate it before shell use');
}
if ((publisher.match(/actions\/download-artifact@/g) ?? []).length !== 3
|| (publisher.match(/\bgh release create\b/g) ?? []).length !== 1
|| publisher.includes('gh release upload')
|| !publisher.includes('--draft')
|| !publisher.includes('gh release edit "$TAG_NAME" --draft=false --prerelease')) {
fail('publisher must aggregate three artifacts as a verified draft, then expose one prerelease');
}
for (const file of [
'GenericAgent-Desktop-Windows-Portable.zip',
'SHA256SUMS-windows.txt',
'GenericAgent-Desktop-Linux-Portable.tar.gz',
'SHA256SUMS-linux.txt',
'GenericAgent-Desktop-macOS-aarch64.dmg',
'GenericAgent-Desktop-macOS-aarch64.dmg.sha256',
]) {
if (!publisher.includes(file)) fail(`publisher does not require release asset: ${file}`);
}
}
async function verifyGeneratedManifest(files) {
const provenance = await readJson(path.join(distDir, 'build-provenance.json'));
const generatedFiles = files
.map((file) => ({ file, relativePath: relativeToDist(file) }))
.filter(({ relativePath }) => !excludedManifestFiles.has(relativePath))
.sort((left, right) => left.relativePath.localeCompare(right.relativePath, 'en'));
const manifestLines = [];
for (const { file, relativePath } of generatedFiles) {
manifestLines.push(`${sha256(await readFile(file))} ${relativePath}\n`);
}
const manifestDigest = sha256(manifestLines.join(''));
if (provenance.version !== expectedVersion) fail(`provenance version must be ${expectedVersion}`);
if (provenance.sourceRepository !== 'https://github.com/abraxas914/GenericAgent') {
fail('provenance source repository is incorrect');
}
if (provenance.sourceCommit !== expectedSourceCommit) fail('provenance source commit is incorrect');
if (provenance.generatedAssetCount !== generatedFiles.length) {
fail(`provenance asset count mismatch: expected ${provenance.generatedAssetCount}, found ${generatedFiles.length}`);
}
if (provenance.generatedManifestSha256 !== manifestDigest) {
fail(`provenance manifest mismatch: expected ${provenance.generatedManifestSha256}, found ${manifestDigest}`);
}
}
async function main() {
const requiredFiles = [
'index.html',
'loading.html',
'setup.html',
'fallback.html',
'assets/ga-logo.svg',
'THIRD_PARTY_NOTICES.txt',
'README.md',
'build-provenance.json',
];
await Promise.all(requiredFiles.map(assertExists));
for (const deadAsset of removedLegacyAssets) {
if (await exists(path.join(distDir, deadAsset))) {
fail(`dead legacy React asset remains in compiled output: ${deadAsset}`);
}
}
verifyTrackedSourceBoundary();
await verifyNoticeAttributes();
const files = await walk(distDir);
const forbiddenSource = files.map(relativeToDist).filter((file) => /(?:\.map|\.tsx?|\.jsx)$/i.test(file));
if (forbiddenSource.length > 0) fail(`compiled distribution contains source files: ${forbiddenSource.join(', ')}`);
const searchableFiles = files.filter((file) => /\.(?:html|js|css)$/i.test(file));
for (const file of searchableFiles) {
const contents = await readFile(file, 'utf8');
if (/(?:webdriverio|__GA_E2E__|wdio:|sourceMappingURL|webpack:\/\/|\/Users\/)/i.test(contents)) {
fail(`compiled distribution leaks source/E2E material: ${relativeToDist(file)}`);
}
}
const notice = await readFile(path.join(distDir, 'THIRD_PARTY_NOTICES.txt'));
if (sha256(notice) !== expectedNoticeSha256) fail('Semi Design third-party notice hash is incorrect');
const readme = await readFile(path.join(distDir, 'README.md'), 'utf8');
for (const value of [
expectedSourceCommit,
...semiPackages,
'https://github.com/DouyinFE/semi-design',
'https://semi.design',
'Copyright (c) 2021 DouyinFE',
'License: MIT',
'THIRD_PARTY_NOTICES.txt',
'5ddf03bb152666637bdfcfa44f1fac3cff5a66b6',
'2fb55d944e4444b08cb9ad76c13aef7a5788186b',
'3e7ca6a2b20eefdb3ee335dc0d520c3b6d9d57f8',
'not a claim that later builds are bit-for-bit reproducible',
]) {
if (!readme.includes(value)) fail(`compiled README is missing required provenance/license text: ${value}`);
}
await Promise.all(['index.html', 'loading.html', 'setup.html'].map(verifyHtmlReferences));
await verifyVersionsRendererAndEntry();
await verifyReleaseContract();
await verifyGeneratedManifest(files);
console.log('Compiled React Desktop 2.0 distribution contracts passed.');
}
await main();
+117
View File
@@ -0,0 +1,117 @@
"""Security and one-shot semantics for the bridge E2E control plane."""
from __future__ import annotations
import ast
import os
import threading
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent.parent
SOURCE = (ROOT / "frontends" / "desktop_bridge.py").read_text(encoding="utf-8")
TREE = ast.parse(SOURCE)
def _function_node(name: str) -> ast.FunctionDef | ast.AsyncFunctionDef:
return next(
node
for node in TREE.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name
)
def _load_helpers():
wanted = {
"_E2E_NEXT_TURN",
"_E2E_CONTROL_LOCK",
"_e2e_control_token",
"_set_e2e_next_turn",
"_consume_e2e_next_turn",
}
nodes = []
for node in TREE.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in wanted:
nodes.append(node)
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
names = {target.id for target in targets if isinstance(target, ast.Name)}
if names & wanted:
nodes.append(node)
namespace = {"os": os, "threading": threading}
exec(compile(ast.Module(body=nodes, type_ignores=[]), "desktop_bridge.py", "exec"), namespace)
return namespace
def test_control_requires_explicit_e2e_flag_and_random_token(monkeypatch):
helpers = _load_helpers()
monkeypatch.delenv("GA_E2E", raising=False)
monkeypatch.setenv("GA_E2E_CONTROL_TOKEN", "secret")
assert helpers["_e2e_control_token"]() is None
monkeypatch.setenv("GA_E2E", "1")
monkeypatch.delenv("GA_E2E_CONTROL_TOKEN", raising=False)
assert helpers["_e2e_control_token"]() is None
monkeypatch.setenv("GA_E2E_CONTROL_TOKEN", "secret")
assert helpers["_e2e_control_token"]() == "secret"
def test_next_turn_override_is_validated_and_consumed_once():
helpers = _load_helpers()
with pytest.raises(ValueError, match="empty"):
helpers["_set_e2e_next_turn"]("crash")
helpers["_set_e2e_next_turn"]("empty")
assert helpers["_consume_e2e_next_turn"]() == "empty"
assert helpers["_consume_e2e_next_turn"]() is None
def test_e2e_route_is_not_registered_without_the_control_gate():
create_app = _function_node("create_app")
registrations = [
node
for node in ast.walk(create_app)
if isinstance(node, ast.Call)
and any(
isinstance(arg, ast.Constant) and arg.value == "/__e2e__/next-turn"
for arg in node.args
)
]
assert len(registrations) == 1
registration = registrations[0]
gate = next(
node
for node in ast.walk(create_app)
if isinstance(node, ast.If) and registration in list(ast.walk(node))
)
assert isinstance(gate.test, ast.Compare)
assert isinstance(gate.test.left, ast.Call)
assert isinstance(gate.test.left.func, ast.Name)
assert gate.test.left.func.id == "_e2e_control_token"
def test_e2e_handler_requires_loopback_and_secret_header():
handler = _function_node("e2e_next_turn_handler")
called_names = {
node.func.id
for node in ast.walk(handler)
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
}
constants = {
node.value
for node in ast.walk(handler)
if isinstance(node, ast.Constant) and isinstance(node.value, str)
}
assert "_e2e_control_token" in called_names
assert "_is_local_peer" in called_names
assert "X-GA-E2E-Token" in constants
assert any(
isinstance(node, ast.Attribute)
and isinstance(node.value, ast.Name)
and node.value.id == "web"
and node.attr == "HTTPNotFound"
for node in ast.walk(handler)
)
+173
View File
@@ -0,0 +1,173 @@
"""Unit tests for model hot-switching within a session.
Tests the contract: set_session_model changes llm_no without destroying session
state. Idle agents switch immediately; running turns defer the new client.
Run: pytest frontends/tests/test_bridge_model_switch.py -v
"""
class FakeAgent:
"""Minimal mock of an agent object with next_llm capability."""
def __init__(self):
self.current_llm = 0
self.next_llm_calls = []
def next_llm(self, llm_no: int):
self.next_llm_calls.append(llm_no)
self.current_llm = llm_no
class FakeSession:
"""Minimal mock of desktop_bridge.Session for model switch tests."""
def __init__(self, sid="test-session", llm_no=0, messages=None, agent=None):
self.id = sid
self.llm_no = llm_no
self.messages = messages or [
{"id": 1, "role": "user", "content": "Hello"},
{"id": 2, "role": "assistant", "content": "Hi there"},
{"id": 3, "role": "user", "content": "Switch model now"},
]
self.agent = agent
self.status = "idle"
self.updated_at = 0.0
def simulate_set_session_model(sess: FakeSession, llm_no: int) -> dict:
"""Replicate set_session_model logic from desktop_bridge.py:1105-1118."""
import time
sess.llm_no = int(llm_no)
if sess.status != "running" and sess.agent is not None and hasattr(sess.agent, "next_llm"):
try:
sess.agent.next_llm(int(llm_no))
except Exception:
pass
sess.updated_at = time.time()
return {"ok": True, "sessionId": sess.id, "llmNo": sess.llm_no}
class TestModelHotSwitch:
"""Core contract: model switch preserves session state."""
def test_llm_no_updated(self):
sess = FakeSession(llm_no=0)
result = simulate_set_session_model(sess, 2)
assert sess.llm_no == 2
assert result["llmNo"] == 2
assert result["ok"] is True
def test_messages_preserved(self):
original_messages = [
{"id": 1, "role": "user", "content": "Hello"},
{"id": 2, "role": "assistant", "content": "World"},
]
sess = FakeSession(messages=list(original_messages))
simulate_set_session_model(sess, 3)
assert sess.messages == original_messages
assert len(sess.messages) == 2
def test_agent_next_llm_called(self):
agent = FakeAgent()
sess = FakeSession(agent=agent)
simulate_set_session_model(sess, 5)
assert agent.next_llm_calls == [5]
assert agent.current_llm == 5
def test_agent_none_no_error(self):
sess = FakeSession(agent=None)
result = simulate_set_session_model(sess, 1)
assert result["ok"] is True
assert sess.llm_no == 1
def test_agent_without_next_llm_no_error(self):
class LegacyAgent:
pass
sess = FakeSession(agent=LegacyAgent())
result = simulate_set_session_model(sess, 4)
assert result["ok"] is True
assert sess.llm_no == 4
def test_updated_at_changed(self):
sess = FakeSession()
sess.updated_at = 0.0
simulate_set_session_model(sess, 1)
assert sess.updated_at > 0.0
def test_status_not_changed(self):
sess = FakeSession()
sess.status = "idle"
simulate_set_session_model(sess, 2)
assert sess.status == "idle"
def test_switch_during_running_preserves_status(self):
agent = FakeAgent()
sess = FakeSession(agent=agent)
sess.status = "running"
simulate_set_session_model(sess, 3)
assert sess.status == "running"
assert sess.llm_no == 3
assert agent.next_llm_calls == []
def test_multiple_switches(self):
agent = FakeAgent()
sess = FakeSession(agent=agent, llm_no=0)
simulate_set_session_model(sess, 1)
simulate_set_session_model(sess, 2)
simulate_set_session_model(sess, 0)
assert sess.llm_no == 0
assert agent.next_llm_calls == [1, 2, 0]
def test_switch_back_to_same_model(self):
agent = FakeAgent()
sess = FakeSession(agent=agent, llm_no=3)
simulate_set_session_model(sess, 3)
assert sess.llm_no == 3
assert agent.next_llm_calls == [3]
class TestModelSwitchFrontendContract:
"""Frontend side: sendPrompt uses current selectedModelNo."""
def test_llm_no_passed_to_send_prompt(self):
"""sendPrompt reads selectedModelNo at call time, not at session creation."""
selectedModelNo = 0
calls = []
def mock_send_prompt(sid, prompt, llm_no, files=None, images=None):
calls.append({"sid": sid, "llm_no": llm_no})
# First message with model 0
mock_send_prompt("s1", "hello", selectedModelNo)
# User switches model
selectedModelNo = 2
# Second message with model 2
mock_send_prompt("s1", "world", selectedModelNo)
assert calls[0]["llm_no"] == 0
assert calls[1]["llm_no"] == 2
# Same session ID — context preserved
assert calls[0]["sid"] == calls[1]["sid"]
def test_model_change_does_not_create_new_session(self):
"""Switching model mid-session does not call createSession."""
session_creates = []
def mock_create():
session_creates.append(1)
return "new-session"
# Simulate: active session exists, user switches model
active_session_id = "existing-session"
# selectModel only updates selectedModelNo + calls legacy bridge
# It does NOT create a new session
assert len(session_creates) == 0
def test_live_model_cleared_on_switch(self):
"""selectModel sets liveModel to null (re-fetched from bridge on next poll)."""
state = {"selectedModelNo": 0, "liveModel": {"isMixin": False, "current": "gpt-4"}}
# Simulate selectModel logic
state["selectedModelNo"] = 2
state["liveModel"] = None # cleared
assert state["liveModel"] is None
assert state["selectedModelNo"] == 2
@@ -0,0 +1,194 @@
"""Origin enforcement for the local Desktop bridge transport."""
from __future__ import annotations
import asyncio
from pathlib import Path
import pytest
from aiohttp import WSServerHandshakeError, web
from aiohttp.test_utils import TestClient, TestServer
from frontends.tests.test_bridge_sessions import _mod as bridge
PROTECTED_PATHS = (
"/status",
"/memory/export",
"/memory/import/inspect",
"/upload",
"/upload/raw",
)
async def _with_client(callback, tmp_path: Path):
side_effect = tmp_path / "request-reached-handler"
async def protected_handler(_request):
side_effect.write_text("reached", encoding="utf-8")
return web.Response(text="protected-secret")
async def http_error(_request):
raise web.HTTPBadRequest(text="bad request")
async def internal_error(_request):
raise RuntimeError("secret failure detail")
app = web.Application(middlewares=[bridge.cors_middleware])
for path in PROTECTED_PATHS:
app.router.add_route("*", path, protected_handler)
app.router.add_get("/ws", bridge.ws_handler)
app.router.add_get("/http-error", http_error)
app.router.add_get("/internal-error", internal_error)
client = TestClient(TestServer(app))
await client.start_server()
try:
await callback(client, side_effect)
finally:
await client.close()
def test_evil_origins_are_rejected_before_sensitive_handlers(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setenv("BRIDGE_PORT", "14168")
monkeypatch.delenv("GA_E2E", raising=False)
monkeypatch.delenv("VITE_PORT", raising=False)
evil_origins = (
"null",
"https://localhost:5173",
"http://localhost:5174",
"http://127.0.0.1:5173",
"http://127.0.0.1:14169",
"http://localhost:5173.evil.example",
"http://tauri.localhost.evil.example",
"tauri://evil",
)
async def scenario(client: TestClient, side_effect: Path):
for origin in evil_origins:
for path in PROTECTED_PATHS:
for method in ("GET", "POST", "OPTIONS"):
response = await client.request(method, path, headers={"Origin": origin})
assert response.status == 403, (origin, method, path, await response.text())
assert response.headers.get("Access-Control-Allow-Origin") is None
assert "protected-secret" not in await response.text()
assert not side_effect.exists()
asyncio.run(_with_client(scenario, tmp_path))
def test_allowed_origins_are_reflected_exactly_without_credentials(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setenv("BRIDGE_PORT", "15168")
monkeypatch.setenv("GA_E2E", "1")
monkeypatch.setenv("VITE_PORT", "5273")
allowed = (
"tauri://localhost",
"http://tauri.localhost",
"http://localhost:5173",
"http://127.0.0.1:15168",
"http://localhost:15168",
"http://[::1]:15168",
"http://127.0.0.1:5273",
)
async def scenario(client: TestClient, side_effect: Path):
for origin in allowed:
response = await client.get("/status", headers={"Origin": origin})
assert response.status == 200
assert response.headers["Access-Control-Allow-Origin"] == origin
assert "origin" in response.headers.get("Vary", "").lower()
assert "Access-Control-Allow-Credentials" not in response.headers
assert response.headers["Access-Control-Allow-Origin"] != "*"
preflight = await client.options("/memory/export", headers={"Origin": origin})
assert preflight.status == 204
assert preflight.headers["Access-Control-Allow-Origin"] == origin
assert side_effect.exists()
asyncio.run(_with_client(scenario, tmp_path))
def test_no_origin_cli_is_allowed_but_cross_site_navigation_is_rejected(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.delenv("GA_E2E", raising=False)
async def scenario(client: TestClient, side_effect: Path):
response = await client.get("/status")
assert response.status == 200
assert "Access-Control-Allow-Origin" not in response.headers
assert await response.text() == "protected-secret"
side_effect.unlink()
rejected = await client.get(
"/status", headers={"Sec-Fetch-Site": "cross-site"}
)
assert rejected.status == 403
assert not side_effect.exists()
asyncio.run(_with_client(scenario, tmp_path))
def test_e2e_origin_requires_explicit_mode_and_a_strict_valid_port(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
async def scenario(client: TestClient, side_effect: Path):
origin = "http://127.0.0.1:5273"
response = await client.get("/status", headers={"Origin": origin})
assert response.status == 403
assert not side_effect.exists()
monkeypatch.setenv("GA_E2E", "1")
for invalid in ("", "0", "65536", "52x73", "5273/path"):
monkeypatch.setenv("VITE_PORT", invalid)
asyncio.run(_with_client(scenario, tmp_path))
monkeypatch.setenv("VITE_PORT", "5273")
monkeypatch.delenv("GA_E2E")
asyncio.run(_with_client(scenario, tmp_path))
def test_allowed_origin_headers_cover_http_errors_and_internal_errors(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
origin = "tauri://localhost"
async def scenario(client: TestClient, _side_effect: Path):
bad = await client.get("/http-error", headers={"Origin": origin})
assert bad.status == 400
assert bad.headers["Access-Control-Allow-Origin"] == origin
failed = await client.get("/internal-error", headers={"Origin": origin})
assert failed.status == 500
assert failed.headers["Access-Control-Allow-Origin"] == origin
assert "secret failure detail" not in await failed.text()
asyncio.run(_with_client(scenario, tmp_path))
def test_websocket_evil_origin_fails_before_prepare(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.delenv("GA_E2E", raising=False)
async def scenario(client: TestClient, side_effect: Path):
with pytest.raises(WSServerHandshakeError) as raised:
await client.ws_connect("/ws", headers={"Origin": "http://evil.example"})
assert raised.value.status == 403
assert not side_effect.exists()
asyncio.run(_with_client(scenario, tmp_path))
def test_bridge_source_has_no_wildcard_cors_and_checks_ws_before_prepare():
source = Path(bridge.__file__).read_text(encoding="utf-8")
assert 'Access-Control-Allow-Origin": "*"' not in source
assert "cors_headers" not in source
ws_start = source.index("async def ws_handler")
ws_end = source.index(
"# ---------------------------------------------------------------------------\n# Transport layer",
ws_start,
)
ws_source = source[ws_start:ws_end]
assert ws_source.index("_request_origin_error(request)") < ws_source.index("WebSocketResponse")
assert ws_source.index("_request_origin_error(request)") < ws_source.index("await ws.prepare(request)")
File diff suppressed because it is too large. Load diff
+261
View File
@@ -0,0 +1,261 @@
"""Unit tests for desktop_bridge.py submit_prompt path-prepend logic and add_message.
Tests the core file-attachment contract: files_meta → agent_prompt path prepend.
Run: pytest frontends/tests/test_bridge_submit.py -v
"""
import json
class TestPathPrepend:
"""Test the agent_prompt construction logic from submit_prompt."""
@staticmethod
def _build_agent_prompt(prompt: str, files_meta: list | None) -> str:
"""Replicate the path prepend logic from desktop_bridge.py submit_prompt."""
agent_prompt = prompt
if files_meta:
paths = [f["path"] for f in files_meta if f.get("path")]
if paths:
agent_prompt = " ".join(paths) + "\n" + prompt
return agent_prompt
def test_single_file_prepend(self):
prompt = "Analyze this file"
files = [{"name": "data.csv", "path": "/tmp/uploads/data.csv", "size": 1024}]
result = self._build_agent_prompt(prompt, files)
assert result == "/tmp/uploads/data.csv\nAnalyze this file"
def test_multiple_files_prepend(self):
prompt = "Compare these"
files = [
{"name": "a.py", "path": "/tmp/a.py", "size": 100},
{"name": "b.py", "path": "/tmp/b.py", "size": 200},
{"name": "c.py", "path": "/tmp/c.py", "size": 300},
]
result = self._build_agent_prompt(prompt, files)
assert result == "/tmp/a.py /tmp/b.py /tmp/c.py\nCompare these"
def test_no_files_meta_unchanged(self):
prompt = "Just a question"
assert self._build_agent_prompt(prompt, None) == prompt
assert self._build_agent_prompt(prompt, []) == prompt
def test_files_without_path_skipped(self):
prompt = "Check files"
files = [
{"name": "good.txt", "path": "/tmp/good.txt"},
{"name": "bad.txt"}, # no path
{"name": "empty.txt", "path": ""}, # empty path
]
result = self._build_agent_prompt(prompt, files)
assert result == "/tmp/good.txt\nCheck files"
def test_path_with_spaces(self):
prompt = "Read it"
files = [{"name": "my report.txt", "path": "/tmp/desktop_uploads/my report.txt"}]
result = self._build_agent_prompt(prompt, files)
assert "/tmp/desktop_uploads/my report.txt" in result
def test_path_with_cjk(self):
prompt = "分析这个"
files = [{"name": "报告.csv", "path": "/tmp/desktop_uploads/报告.csv"}]
result = self._build_agent_prompt(prompt, files)
assert result.startswith("/tmp/desktop_uploads/报告.csv\n")
def test_empty_prompt_with_files(self):
prompt = ""
files = [{"name": "f.txt", "path": "/tmp/f.txt"}]
result = self._build_agent_prompt(prompt, files)
assert result == "/tmp/f.txt\n"
class TestAddMessageExtra:
"""Test that add_message stores extra fields correctly."""
@staticmethod
def _simulate_add_message(role: str, content: str, **extra) -> dict:
"""Replicate add_message logic from AgentManager."""
msg = {
"id": 1,
"role": role,
"content": content,
"ts": 1000.0,
}
msg.update(extra)
return msg
def test_files_stored_in_message(self):
files_meta = [{"name": "x.py", "path": "/tmp/x.py", "size": 512}]
msg = self._simulate_add_message("user", "hello", files=files_meta)
assert msg["files"] == files_meta
def test_images_stored_in_message(self):
image_metas = [{"name": "pic.png", "path": "/tmp/pic.png"}]
msg = self._simulate_add_message("user", "see this", images=image_metas)
assert msg["images"] == image_metas
def test_display_stored_separately(self):
msg = self._simulate_add_message("user", "full prompt with paths", display="clean display text")
assert msg["display"] == "clean display text"
assert msg["content"] == "full prompt with paths"
def test_no_extra_fields_minimal(self):
msg = self._simulate_add_message("assistant", "response")
assert "files" not in msg
assert "images" not in msg
assert "display" not in msg
def test_combined_files_and_images(self):
msg = self._simulate_add_message(
"user", "both",
files=[{"name": "f.txt", "path": "/tmp/f.txt"}],
images=[{"name": "i.png", "path": "/tmp/i.png"}],
)
assert len(msg["files"]) == 1
assert len(msg["images"]) == 1
class TestImagePathExtraction:
"""Test image_paths extraction from image_metas (used for _patch_chat_for_images)."""
def test_extracts_paths(self):
image_metas = [
{"name": "a.png", "path": "/tmp/a.png"},
{"name": "b.jpg", "path": "/tmp/b.jpg"},
]
image_paths = [m["path"] for m in (image_metas or []) if m.get("path")]
assert image_paths == ["/tmp/a.png", "/tmp/b.jpg"]
def test_none_metas(self):
image_metas = None
image_paths = [m["path"] for m in (image_metas or []) if m.get("path")]
assert image_paths == []
def test_skips_entries_without_path(self):
image_metas = [
{"name": "a.png", "path": "/tmp/a.png"},
{"name": "broken.png"},
]
image_paths = [m["path"] for m in (image_metas or []) if m.get("path")]
assert image_paths == ["/tmp/a.png"]
class TestSessionFiltering:
"""Test the tui_ prefix filter for conductor sessions."""
def test_filters_tui_prefix(self):
sessions = [
{"id": "abc123", "title": "Chat"},
{"id": "tui_worker_1", "title": "Worker"},
{"id": "def456", "title": "Another"},
{"id": "tui_conductor_main", "title": "Main"},
]
filtered = [s for s in sessions if not s["id"].startswith("tui_")]
assert len(filtered) == 2
assert all(not s["id"].startswith("tui_") for s in filtered)
def test_no_tui_sessions_unchanged(self):
sessions = [{"id": "a"}, {"id": "b"}]
filtered = [s for s in sessions if not s["id"].startswith("tui_")]
assert len(filtered) == 2
def test_all_tui_sessions_empty_result(self):
sessions = [{"id": "tui_x"}, {"id": "tui_y"}]
filtered = [s for s in sessions if not s["id"].startswith("tui_")]
assert len(filtered) == 0
class TestRunAgentTurnScope:
"""Guard tests: run_agent_turn must NOT reference variables from submit_prompt's scope.
These tests parse the actual bridge source to catch NameError regressions
(e.g. referencing 'sid' inside run_agent_turn which runs in a different thread).
"""
@staticmethod
def _get_bridge_source() -> str:
from pathlib import Path
candidates = [
Path(__file__).parent.parent / "desktop_bridge.py",
Path(__file__).parent.parent.parent / "frontends" / "desktop_bridge.py",
]
for p in candidates:
if p.exists():
return p.read_text(encoding="utf-8")
raise FileNotFoundError("desktop_bridge.py not found")
@staticmethod
def _extract_method_body(source: str, method_name: str) -> str:
"""Extract a method with Python's parser so multiline signatures remain supported."""
import ast
tree = ast.parse(source)
method = next(
(
node
for node in ast.walk(tree)
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == method_name
),
None,
)
return ast.get_source_segment(source, method) if method is not None else ""
def test_run_agent_turn_does_not_reference_bare_sid(self):
"""run_agent_turn must use sess.id, never bare 'sid' (which is out of scope)."""
import re
source = self._get_bridge_source()
body = self._extract_method_body(source, "run_agent_turn")
assert body, "Could not extract run_agent_turn body"
# Find bare 'sid' references that are NOT part of 'sess.id' or string literals
bare_sid_refs = re.findall(r'(?<!\.)(?<!\w)sid(?!\w)(?!.*#.*sid)', body)
# Filter out string contents (inside quotes)
real_refs = []
for match in re.finditer(r'(?<!\.)(?<!\w)sid(?!\w)', body):
pos = match.start()
# Check we're not inside a string
before = body[:pos]
single_q = before.count("'") % 2
double_q = before.count('"') % 2
if not single_q and not double_q:
real_refs.append(match.group())
assert len(real_refs) == 0, (
f"run_agent_turn references bare 'sid' {len(real_refs)} time(s). "
f"Use 'sess.id' instead — 'sid' is only defined in submit_prompt's scope."
)
def test_patch_chat_for_images_exists(self):
"""_patch_chat_for_images must exist in bridge — it's the image injection path."""
source = self._get_bridge_source()
assert "_patch_chat_for_images" in source, (
"_patch_chat_for_images method missing from desktop_bridge.py — "
"image uploads will silently fail (agent won't see images)"
)
def test_submit_prompt_separates_agent_prompt_from_stored_message(self):
"""submit_prompt must store clean prompt in message but pass paths to agent."""
source = self._get_bridge_source()
body = self._extract_method_body(source, "submit_prompt")
assert body, "Could not extract submit_prompt body"
# Must have agent_prompt as a separate variable
assert "agent_prompt" in body, (
"submit_prompt must use 'agent_prompt' (with paths) separate from 'prompt' "
"(clean, stored in message). Otherwise UI shows raw paths to user."
)
# add_message must use the clean prompt, not agent_prompt
import re
add_msg_call = re.search(r'add_message\(.*?,\s*"user",\s*(\w+)', body)
assert add_msg_call, "add_message call not found in submit_prompt"
arg = add_msg_call.group(1)
assert arg == "prompt", (
f"add_message stores '{arg}' but should store 'prompt' (clean text). "
f"agent_prompt (with file paths) goes only to run_agent_turn."
)
def test_image_paths_passed_to_run_agent_turn(self):
"""submit_prompt must extract image_paths and pass to run_agent_turn thread."""
source = self._get_bridge_source()
body = self._extract_method_body(source, "submit_prompt")
assert "image_paths" in body, (
"submit_prompt must extract image_paths from image_metas and pass to "
"run_agent_turn. Without this, _patch_chat_for_images receives None."
)
+153
View File
@@ -0,0 +1,153 @@
"""Unit tests for desktop_bridge.py upload/path-safety logic.
Follows the existing bridge testing strategy: replicate pure upload/path logic
without importing the whole desktop_bridge module (which starts services).
Run: pytest frontends/tests/test_bridge_uploads.py -v
"""
from __future__ import annotations
import base64
import re
from pathlib import Path
# Mirrors desktop_bridge._safe_session_dir
def _safe_session_dir(sid: str | None) -> str:
s = re.sub(r"[^A-Za-z0-9_-]", "", str(sid or ""))
return s or "_misc"
# Mirrors desktop_bridge._session_upload_dir
def _session_upload_dir(upload_root: Path, sid: str) -> Path:
return upload_root / _safe_session_dir(sid)
# Mirrors the upload_handler naming/path rules.
def _build_upload_path(upload_root: Path, sid: str, name: str, token: str) -> Path:
safe_name = (name or "file").strip().replace("/", "_").replace("\\", "_") or "file"
return _session_upload_dir(upload_root, sid) / f"{token}__{safe_name}"
# Mirrors upload_handler decode logic.
def _decode_upload_data(data_url: str) -> bytes:
if "," in data_url:
b64 = data_url.split(",", 1)[1]
else:
b64 = data_url
return base64.b64decode(b64)
# Mirrors upload_delete_handler path whitelist.
def _delete_allowed(upload_root: Path, raw_path: str) -> bool:
target = Path(raw_path).resolve()
root = upload_root.resolve()
return root in target.parents
# Mirrors upload_raw_handler path whitelist.
def _raw_allowed(upload_root: Path, raw_path: str) -> bool:
target = Path(raw_path).resolve()
try:
target.relative_to(upload_root.resolve())
return True
except (ValueError, OSError):
return False
# Mirrors upload_raw_handler original filename recovery.
def _original_upload_name(path: Path) -> str:
return path.name.split("__", 1)[-1]
class TestSafeSessionDir:
def test_keeps_safe_ascii_chars(self):
assert _safe_session_dir("abc-DEF_123") == "abc-DEF_123"
def test_strips_path_traversal_and_spaces(self):
assert _safe_session_dir(" ../../team notes ") == "teamnotes"
def test_empty_or_none_falls_back_to_misc(self):
assert _safe_session_dir("") == "_misc"
assert _safe_session_dir(None) == "_misc"
def test_non_ascii_only_sid_falls_back_to_misc(self):
assert _safe_session_dir("报告/会议") == "_misc"
class TestSessionUploadDir:
def test_scopes_uploads_under_sanitized_sid(self, tmp_path: Path):
root = tmp_path / "desktop_uploads"
actual = _session_upload_dir(root, "../../tui_worker")
assert actual == root / "tui_worker"
def test_files_bucket_stays_isolated(self, tmp_path: Path):
root = tmp_path / "desktop_uploads"
actual = _session_upload_dir(root, "_files")
assert actual == root / "_files"
class TestUploadPathConstruction:
def test_replaces_slashes_in_uploaded_name(self, tmp_path: Path):
root = tmp_path / "desktop_uploads"
path = _build_upload_path(root, "session-1", "dir/sub\\name.txt", "abc123def456")
assert path == root / "session-1" / "abc123def456__dir_sub_name.txt"
def test_uses_file_when_name_empty(self, tmp_path: Path):
root = tmp_path / "desktop_uploads"
path = _build_upload_path(root, "session-1", " ", "abc123def456")
assert path.name == "abc123def456__file"
class TestUploadDecode:
def test_decodes_data_url_payload(self):
payload = "hello,world".encode("utf-8")
data_url = "data:text/plain;base64," + base64.b64encode(payload).decode("ascii")
assert _decode_upload_data(data_url) == payload
def test_decodes_raw_base64_payload(self):
payload = b"abc123"
raw = base64.b64encode(payload).decode("ascii")
assert _decode_upload_data(raw) == payload
class TestUploadPathSafety:
def test_delete_allows_file_inside_upload_root(self, tmp_path: Path):
root = tmp_path / "desktop_uploads"
target = root / "session-1" / "abc__note.txt"
target.parent.mkdir(parents=True)
target.write_text("ok", encoding="utf-8")
assert _delete_allowed(root, str(target)) is True
def test_delete_rejects_file_outside_upload_root(self, tmp_path: Path):
root = tmp_path / "desktop_uploads"
outside = tmp_path / "outside.txt"
outside.write_text("nope", encoding="utf-8")
assert _delete_allowed(root, str(outside)) is False
def test_raw_allows_file_inside_upload_root(self, tmp_path: Path):
root = tmp_path / "desktop_uploads"
target = root / "session-1" / "abc__report.csv"
target.parent.mkdir(parents=True)
target.write_text("x,y", encoding="utf-8")
assert _raw_allowed(root, str(target)) is True
def test_raw_rejects_path_traversal_outside_upload_root(self, tmp_path: Path):
root = tmp_path / "desktop_uploads"
outside = tmp_path / "secret.txt"
outside.write_text("top secret", encoding="utf-8")
assert _raw_allowed(root, str(outside)) is False
class TestOriginalUploadName:
def test_strips_uuid_prefix_from_uploaded_file(self):
path = Path("/tmp/desktop_uploads/session-1/abc123def456__my report.txt")
assert _original_upload_name(path) == "my report.txt"
def test_preserves_cjk_filename(self):
path = Path("/tmp/desktop_uploads/session-1/abc123def456__报告.csv")
assert _original_upload_name(path) == "报告.csv"
def test_filename_without_prefix_returns_name_as_is(self):
path = Path("/tmp/desktop_uploads/session-1/plain.txt")
assert _original_upload_name(path) == "plain.txt"
+420
View File
@@ -0,0 +1,420 @@
"""Unit tests for desktop_bridge.py utility functions.
Tests pure functions that don't require agent/session infrastructure.
Run: pytest frontends/tests/test_bridge_utils.py -v
"""
import ast
import json
import os
import sys
import re
from pathlib import Path
from types import SimpleNamespace
# Add project root so we can import bridge helpers
ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(ROOT / "frontends"))
from data_backup import materialize_import_source, merge_data_files
# Import the functions under test (module-level helpers)
import importlib.util
spec = importlib.util.spec_from_file_location("desktop_bridge", ROOT / "frontends" / "desktop_bridge.py")
# We can't import the whole module (it starts aiohttp etc.) so we extract functions manually
_bridge_source = (ROOT / "frontends" / "desktop_bridge.py").read_text(encoding="utf-8")
def _load_named_helpers(names, namespace):
tree = ast.parse(_bridge_source)
nodes = [
node
for node in tree.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in names
]
exec(compile(ast.Module(body=nodes, type_ignores=[]), "desktop_bridge.py", "exec"), namespace)
return namespace
class TestGaRootBoundary:
def test_external_core_override_needs_only_agentmain(self, tmp_path, monkeypatch):
external = tmp_path / "external core"
external.mkdir()
(external / "agentmain.py").write_text("# core\n", encoding="utf-8")
monkeypatch.setenv("GA_ROOT", str(external))
monkeypatch.setattr(sys, "argv", ["desktop_bridge.py"])
helpers = _load_named_helpers(
{"_ga_root_override"},
{"os": os, "sys": sys, "Path": Path, "Optional": __import__("typing").Optional},
)
assert helpers["_ga_root_override"]() == external.resolve()
def test_invalid_override_falls_back_instead_of_becoming_app_dir(self, tmp_path, monkeypatch):
missing = tmp_path / "deleted-core"
monkeypatch.setenv("GA_ROOT", str(missing))
monkeypatch.setattr(sys, "argv", ["desktop_bridge.py"])
helpers = _load_named_helpers(
{"_ga_root_override"},
{"os": os, "sys": sys, "Path": Path, "Optional": __import__("typing").Optional},
)
assert helpers["_ga_root_override"]() is None
class TestMemoryImport:
def test_source_memory_wins_while_current_responses_remain_add_only(self, tmp_path):
source = tmp_path / "source"
target = tmp_path / "target"
(source / "memory").mkdir(parents=True)
(source / "memory" / "same.md").write_text("new", encoding="utf-8")
(source / "memory" / "added.md").write_text("added", encoding="utf-8")
(source / "temp" / "model_responses").mkdir(parents=True)
(source / "temp" / "model_responses" / "same.json").write_text("new", encoding="utf-8")
(source / "temp" / "model_responses" / "added.json").write_text("added", encoding="utf-8")
(target / "memory").mkdir(parents=True)
(target / "memory" / "same.md").write_text("old", encoding="utf-8")
(target / "temp" / "model_responses").mkdir(parents=True)
(target / "temp" / "model_responses" / "same.json").write_text("old", encoding="utf-8")
result = merge_data_files(str(source), str(target))
assert result["memoryCopied"] == 2
assert result["memorySkipped"] == 0
assert result["responsesCopied"] == 1
assert result["responsesSkipped"] == 1
assert (target / "memory" / "same.md").read_text(encoding="utf-8") == "new"
assert (target / "memory" / "added.md").read_text(encoding="utf-8") == "added"
assert (target / "temp" / "model_responses" / "same.json").read_text(encoding="utf-8") == "old"
assert (Path(result["backupDir"]) / "memory" / "same.md").read_text(encoding="utf-8") == "old"
def test_bridge_adopts_only_sessions_committed_by_transaction(self, tmp_path):
source = tmp_path / "source"
target = tmp_path / "target"
(source / "memory").mkdir(parents=True)
(source / "memory" / "imported.md").write_text("new", encoding="utf-8")
source_sessions = source / "temp" / "desktop_sessions"
source_sessions.mkdir(parents=True)
source_sessions.joinpath("sess-imported.json").write_text(
json.dumps({"id": "sess-imported", "title": "Imported", "messages": []}),
encoding="utf-8",
)
target.mkdir()
class Lock:
def __enter__(self):
return self
def __exit__(self, *_args):
return False
class Manager:
ga_root = str(target)
lock = Lock()
sessions = {}
@staticmethod
def begin_maintenance(kind, running_extras_fn):
assert kind == "import"
assert running_extras_fn() == []
return "test-token"
@staticmethod
def end_maintenance(token):
assert token == "test-token"
@staticmethod
def _session_from_item(item):
return SimpleNamespace(id=item["id"], title=item.get("title", ""))
helpers = _load_named_helpers(
{"_import_data_source"},
{
"manager": Manager(),
"services": SimpleNamespace(running_managed_ids=lambda: []),
"materialize_import_source": materialize_import_source,
"merge_data_files": merge_data_files,
},
)
result = helpers["_import_data_source"](str(source))
manager = helpers["manager"]
assert result["sessionsAdded"] == 1
assert "_preparedSessions" not in result
assert manager.sessions["sess-imported"].title == "Imported"
persisted = target / "temp" / "desktop_sessions" / "sess-imported.json"
assert json.loads(persisted.read_text(encoding="utf-8"))["id"] == "sess-imported"
def _load_empty_turn_helpers():
tree = ast.parse(_bridge_source)
wanted = {"_EMPTY_TURN_MICROCOPY", "_get_ui_lang", "empty_turn_fallback"}
nodes = []
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in wanted:
nodes.append(node)
elif isinstance(node, ast.Assign):
names = {target.id for target in node.targets if isinstance(target, ast.Name)}
if names & wanted:
nodes.append(node)
namespace = {"Path": Path, "json": json}
exec(compile(ast.Module(body=nodes, type_ignores=[]), "desktop_bridge.py", "exec"), namespace)
return namespace
class TestEmptyTurnFallback:
def test_uses_english_microcopy(self, tmp_path, monkeypatch):
(tmp_path / ".ga_desktop_settings.json").write_text(
'{"ui":{"lang":"en"},"lang":"zh"}', encoding="utf-8"
)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
helpers = _load_empty_turn_helpers()
assert helpers["empty_turn_fallback"]() == (
'⚠️ This turn ended without a visible response. You can send "continue" to retry.'
)
def test_supports_legacy_top_level_language(self, tmp_path, monkeypatch):
(tmp_path / ".ga_desktop_settings.json").write_text('{"lang":"en"}', encoding="utf-8")
monkeypatch.setattr(Path, "home", lambda: tmp_path)
helpers = _load_empty_turn_helpers()
assert helpers["empty_turn_fallback"]() == (
'⚠️ This turn ended without a visible response. You can send "continue" to retry.'
)
def test_defaults_to_chinese_for_missing_or_invalid_settings(self, tmp_path, monkeypatch):
monkeypatch.setattr(Path, "home", lambda: tmp_path)
helpers = _load_empty_turn_helpers()
expected = '⚠️ 这一轮结束了,但没有产出可见回复。你可以发送"继续"重试。'
assert helpers["empty_turn_fallback"]() == expected
(tmp_path / ".ga_desktop_settings.json").write_text("not-json", encoding="utf-8")
assert helpers["empty_turn_fallback"]() == expected
def test_defaults_to_chinese_for_non_string_language(self, tmp_path, monkeypatch):
(tmp_path / ".ga_desktop_settings.json").write_text(
'{"ui":{"lang":[]},"lang":[]}', encoding="utf-8"
)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
helpers = _load_empty_turn_helpers()
assert helpers["empty_turn_fallback"]() == (
'⚠️ 这一轮结束了,但没有产出可见回复。你可以发送"继续"重试。'
)
# === strip_final_info_marker ===
_FINAL_INFO_RE = re.compile(r'\n*`{5}\n*\[Info\] Final response to user\.\n*`{5}\s*$')
def strip_final_info_marker(text):
return _FINAL_INFO_RE.sub('', str(text or ''))
class TestStripFinalInfoMarker:
def test_removes_marker_at_end(self):
text = "Hello world\n`````\n[Info] Final response to user.\n`````"
assert strip_final_info_marker(text) == "Hello world"
def test_no_marker_unchanged(self):
text = "Just normal text"
assert strip_final_info_marker(text) == "Just normal text"
def test_empty_string(self):
assert strip_final_info_marker("") == ""
def test_none_becomes_empty(self):
# str(None or '') → '' since `or` short-circuits
assert strip_final_info_marker(None) == ""
def test_marker_only_in_middle_not_removed(self):
text = "Before\n`````\n[Info] Final response to user.\n`````\nAfter"
assert strip_final_info_marker(text) == text
# === normalize_final_turn_segs ===
def normalize_final_turn_segs(full, outputs):
if not outputs or not isinstance(outputs, (list, tuple)):
return None
segs = [strip_final_info_marker(s) for s in outputs]
full_text = strip_final_info_marker(full)
if not segs:
return None
joined = "".join(segs)
if full_text.strip() == joined.strip():
return segs
if joined and full_text.startswith(joined):
suffix = full_text[len(joined):]
if suffix.strip():
segs[-1] = segs[-1] + suffix
return segs
return None
class TestNormalizeFinalTurnSegs:
def test_exact_match(self):
segs = normalize_final_turn_segs("AB", ["A", "B"])
assert segs == ["A", "B"]
def test_suffix_appended_to_last_seg(self):
segs = normalize_final_turn_segs("ABC_extra", ["A", "BC"])
assert segs is not None
assert segs[-1] == "BC_extra"
def test_no_match_returns_none(self):
segs = normalize_final_turn_segs("XYZ", ["A", "B"])
assert segs is None
def test_none_outputs_returns_none(self):
assert normalize_final_turn_segs("text", None) is None
def test_empty_outputs_returns_none(self):
assert normalize_final_turn_segs("text", []) is None
def test_string_outputs_returns_none(self):
assert normalize_final_turn_segs("text", "not a list") is None
def test_whitespace_match(self):
segs = normalize_final_turn_segs("A B ", ["A B "])
assert segs == ["A B "]
# === _extract_first_timestamp ===
def _extract_first_timestamp(content):
m = re.search(r'^=== Prompt === (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', content, re.MULTILINE)
if m:
try:
from datetime import datetime
return datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S").timestamp()
except Exception:
pass
return 0.0
class TestExtractFirstTimestamp:
def test_extracts_valid_timestamp(self):
content = "=== Prompt === 2024-06-15 14:30:00\nSome prompt text"
ts = _extract_first_timestamp(content)
assert ts > 0
from datetime import datetime
expected = datetime(2024, 6, 15, 14, 30, 0).timestamp()
assert ts == expected
def test_no_match_returns_zero(self):
assert _extract_first_timestamp("No timestamp here") == 0.0
def test_empty_string(self):
assert _extract_first_timestamp("") == 0.0
def test_multiline_finds_first(self):
content = "Some header\n=== Prompt === 2024-01-01 00:00:00\n=== Response === 2024-01-01 00:01:00"
ts = _extract_first_timestamp(content)
from datetime import datetime
assert ts == datetime(2024, 1, 1, 0, 0, 0).timestamp()
# === _next_native_var ===
def _next_native_var(text, protocol):
proto = str(protocol or "").strip().lower()
if proto == "claude":
prefix = "native_claude_config"
elif proto in ("oai", "openai"):
prefix = "native_oai_config"
else:
raise ValueError("protocol is required: choose 'oai' or 'claude'")
nums = [0]
if re.search(rf"^{prefix}\s*=", text, re.M):
nums.append(0)
nums.extend(int(m.group(1)) for m in re.finditer(rf"^{prefix}(\d+)\s*=", text, re.M))
n = max(nums) + 1
return prefix if n == 1 and not re.search(rf"^{prefix}\s*=", text, re.M) else f"{prefix}{n}"
class TestNextNativeVar:
def test_first_oai_config(self):
assert _next_native_var("", "oai") == "native_oai_config"
def test_first_claude_config(self):
assert _next_native_var("", "claude") == "native_claude_config"
def test_increments_when_one_exists(self):
text = "native_oai_config = {'key': 'xxx'}"
result = _next_native_var(text, "oai")
# When base var exists: nums=[0,0], max=0, n=1, but base already exists → prefix1
assert result == "native_oai_config1"
def test_increments_past_existing_numbered(self):
text = "native_claude_config = {}\nnative_claude_config2 = {}\nnative_claude_config3 = {}"
result = _next_native_var(text, "claude")
assert result == "native_claude_config4"
def test_raises_on_invalid_protocol(self):
import pytest
with pytest.raises(ValueError, match="protocol is required"):
_next_native_var("", "gemini")
def test_openai_alias(self):
assert _next_native_var("", "openai") == "native_oai_config"
# === _format_py_dict ===
def _format_py_dict(d):
lines = [f" '{k}': {json.dumps(v, ensure_ascii=False)}," if isinstance(v, str) else f" '{k}': {v}," for k, v in d.items()]
return "{\n" + "\n".join(lines) + "\n}"
class TestFormatPyDict:
def test_simple_dict(self):
result = _format_py_dict({"key": "sk-xxx", "model": "gpt-4"})
assert "'key': \"sk-xxx\"" in result
assert "'model': \"gpt-4\"" in result
assert result.startswith("{")
assert result.endswith("}")
def test_non_string_values(self):
result = _format_py_dict({"timeout": 30, "stream": True})
assert "'timeout': 30," in result
assert "'stream': True," in result
def test_empty_dict(self):
result = _format_py_dict({})
assert result == "{\n\n}"
def test_chinese_values_preserved(self):
result = _format_py_dict({"name": "模型一"})
assert "模型一" in result
# === _load_plan_baseline ===
def _load_plan_baseline(item, msgs):
base = int(item.get("plan_scan_baseline", 0) or 0)
if base >= len(msgs):
return 0
return max(0, base)
class TestLoadPlanBaseline:
def test_valid_baseline(self):
assert _load_plan_baseline({"plan_scan_baseline": 5}, list(range(10))) == 5
def test_baseline_exceeds_messages_returns_zero(self):
assert _load_plan_baseline({"plan_scan_baseline": 20}, list(range(5))) == 0
def test_missing_key_returns_zero(self):
assert _load_plan_baseline({}, list(range(10))) == 0
def test_none_value_returns_zero(self):
assert _load_plan_baseline({"plan_scan_baseline": None}, list(range(10))) == 0
def test_negative_clamped_to_zero(self):
assert _load_plan_baseline({"plan_scan_baseline": -5}, list(range(10))) == 0
@@ -0,0 +1,411 @@
"""Runtime model-routing tests for the isolated Conductor process."""
from __future__ import annotations
import importlib.util
import argparse
import os
import queue
import sys
import threading
import types
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent.parent
CONDUCTOR_PATH = ROOT / "frontends" / "conductor.py"
class StubGenericAgent:
pass
agentmain_stub = types.ModuleType("agentmain")
agentmain_stub.GenericAgent = StubGenericAgent
previous_agentmain = sys.modules.get("agentmain")
sys.modules["agentmain"] = agentmain_stub
spec = importlib.util.spec_from_file_location("conductor_model_under_test", CONDUCTOR_PATH)
conductor = importlib.util.module_from_spec(spec)
assert spec.loader is not None
_argv = sys.argv
try:
sys.argv = [str(CONDUCTOR_PATH), "--no-browser"]
spec.loader.exec_module(conductor)
finally:
sys.argv = _argv
if previous_agentmain is None:
del sys.modules["agentmain"]
else:
sys.modules["agentmain"] = previous_agentmain
class Backend:
def __init__(self, name: str):
self.name = name
self.model = name
class Client:
def __init__(self, name: str | None):
if name is not None:
self.backend = Backend(name)
class FakeAgent:
def __init__(self, names: list[str | None], failing: set[int] | None = None):
self.llmclients = [Client(name) for name in names]
self.llm_no = 0
self.llmclient = self.llmclients[0]
self.next_llm_calls: list[int] = []
self.reloads = 0
self.failing = failing or set()
def load_llm_sessions(self):
self.reloads += 1
def next_llm(self, no: int):
# Deliberately mirrors GenericAgent's modulo behavior. The Conductor
# resolver must validate before calling this method.
self.next_llm_calls.append(no)
if no in self.failing:
raise RuntimeError("broken client")
self.llm_no = no % len(self.llmclients)
self.llmclient = self.llmclients[self.llm_no]
def get_llm_name(self, client=None, model=False):
client = client or self.llmclient
return client.backend.model if model else client.backend.name
def test_out_of_range_config_falls_back_without_modulo(monkeypatch):
agent = FakeAgent(["zero", "one", "two", "three"])
monkeypatch.setattr(
conductor,
"_settings_doc",
lambda: {"conductor": {"llmNo": 99}, "ui": {"llmNo": 1}},
)
state = conductor._apply_desktop_model(agent)
assert agent.next_llm_calls == [1]
assert state["effective"] == 1
assert state["fallbackReason"] == "invalid_configured"
def test_conductor_default_port_override_is_e2e_scoped(monkeypatch):
monkeypatch.delenv(conductor.E2E_REPORT_DIR_ENV, raising=False)
monkeypatch.setenv(conductor.E2E_CONDUCTOR_PORT_ENV, "29890")
assert conductor._default_conductor_port() == 8900
monkeypatch.setenv(conductor.E2E_REPORT_DIR_ENV, "/tmp/package-evidence")
assert conductor._default_conductor_port() == 29890
@pytest.mark.parametrize("value", ["1", "8900", "65535"])
def test_conductor_port_parser_accepts_full_valid_range(value):
assert conductor._parse_conductor_port(value) == int(value)
@pytest.mark.parametrize("value", ["", "0", "65536", "2.5", "-1", "1234"])
def test_conductor_port_parser_rejects_invalid_values(value):
with pytest.raises(argparse.ArgumentTypeError, match="between 1 and 65535"):
conductor._parse_conductor_port(value)
def test_unusable_configured_client_falls_back_to_ui_default(monkeypatch):
agent = FakeAgent(["zero", "one", None, "three"])
monkeypatch.setattr(
conductor,
"_settings_doc",
lambda: {"conductor": {"llmNo": 2}, "ui": {"llmNo": 1}},
)
state = conductor._apply_desktop_model(agent)
assert agent.next_llm_calls == [1]
assert state["effective"] == 1
assert state["fallbackReason"] == "configured_unavailable"
def test_configured_activation_failure_falls_back_to_ui_default(monkeypatch):
agent = FakeAgent(["zero", "one", "two"], failing={2})
monkeypatch.setattr(
conductor,
"_settings_doc",
lambda: {"conductor": {"llmNo": 2}, "ui": {"llmNo": 1}},
)
state = conductor._apply_desktop_model(agent)
assert agent.next_llm_calls == [2, 1]
assert state["effective"] == 1
assert state["fallbackReason"] == "configured_unavailable"
def test_missing_config_uses_first_usable_when_ui_is_invalid(monkeypatch):
agent = FakeAgent([None, None, "two", "three"])
monkeypatch.setattr(conductor, "_settings_doc", lambda: {"ui": {"llmNo": 99}})
state = conductor._apply_desktop_model(agent)
assert agent.next_llm_calls == [2]
assert state["effective"] == 2
assert state["fallbackReason"] == "first_available"
def test_explicit_numeric_worker_model_rejects_out_of_range():
agent = FakeAgent(["zero", "one"])
with pytest.raises(ValueError, match="out of range"):
conductor._select_llm(agent, 99)
assert agent.next_llm_calls == []
def test_runtime_model_snapshot_is_broadcast_with_running_state(monkeypatch):
instance = conductor.Conductor()
payloads: list[dict] = []
monkeypatch.setattr(conductor, "schedule_broadcast", payloads.append)
state = {
"configured": 2,
"effective": 1,
"fallbackReason": "configured_unavailable",
"current": "model-one",
}
instance._publish_model_state(state, running=True)
assert instance.model_snapshot() == {**state, "running": True}
assert payloads == [{"type": "model", "model": {**state, "running": True}}]
def test_long_lived_conductor_refreshes_model_between_tasks(monkeypatch):
"""A saved binding waits for task cleanup, then affects the next task."""
class EndOfInbox(Exception):
pass
second_boundary_entered = threading.Event()
class BoundaryInbox:
def __init__(self):
self.events = [
{"type": "user_message", "id": "first"},
{"type": "user_message", "id": "second"},
]
def get(self):
if not self.events:
raise EndOfInbox
if len(self.events) == 1:
second_boundary_entered.set()
return self.events.pop(0)
def task_done(self):
pass
def empty(self):
# Keep each event as a separate task boundary for this regression.
return True
selected = {"llmNo": 1}
timeline: list[tuple[str, int]] = []
allow_first_cleanup = threading.Event()
first_cleanup_done = threading.Event()
class TaskAgent(FakeAgent):
def __init__(self):
super().__init__(["zero", "one", "two"])
self.inc_out = False
self.task_models: list[int] = []
self.task_queue: queue.Queue = queue.Queue()
self.cleanup_threads: list[threading.Thread] = []
def next_llm(self, no: int):
super().next_llm(no)
timeline.append(("select", self.llm_no))
def put_task(self, _prompt: str, source: str):
assert source == "conductor"
self.task_models.append(self.llm_no)
timeline.append(("submit", self.llm_no))
if len(self.task_models) == 1:
# Simulate save_config while task one is already submitted. The
# display queue reports done before the agent runner persists its
# final state and calls task_done().
selected["llmNo"] = 2
assert self.llm_no == 1
self.task_queue.put("first")
def finish_first_task():
assert allow_first_cleanup.wait(timeout=2)
assert self.task_queue.get_nowait() == "first"
self.task_queue.task_done()
first_cleanup_done.set()
cleanup = threading.Thread(target=finish_first_task)
cleanup.start()
self.cleanup_threads.append(cleanup)
else:
self.task_queue.put("second")
assert self.task_queue.get_nowait() == "second"
self.task_queue.task_done()
output: queue.Queue = queue.Queue()
output.put({"done": True, "turn": len(self.task_models)})
return output
instance = conductor.Conductor()
instance.agent = TaskAgent()
instance.inbox = BoundaryInbox()
payloads: list[dict] = []
monkeypatch.setattr(
conductor,
"_settings_doc",
lambda: {"conductor": {"llmNo": selected["llmNo"]}},
)
monkeypatch.setattr(conductor, "schedule_broadcast", payloads.append)
monkeypatch.setattr(conductor, "start_agent_runner", lambda *_args: None)
monkeypatch.setattr(conductor.time, "sleep", lambda _seconds: None)
monkeypatch.setattr(instance, "_build_prompt", lambda events: events[0]["id"])
run_errors: list[BaseException] = []
def run_loop():
try:
instance._run()
except EndOfInbox:
pass
except BaseException as error: # surfaced below in the test thread
run_errors.append(error)
runner = threading.Thread(target=run_loop)
runner.start()
assert second_boundary_entered.wait(timeout=2)
# The display `done` marker has already arrived, but queue cleanup is still
# blocked. Model two must not be applied or submitted yet.
assert instance.agent.task_models == [1]
assert timeline == [("select", 1), ("submit", 1)]
allow_first_cleanup.set()
assert first_cleanup_done.wait(timeout=2)
runner.join(timeout=2)
for cleanup in instance.agent.cleanup_threads:
cleanup.join(timeout=2)
assert not runner.is_alive()
assert run_errors == []
assert instance.agent.task_models == [1, 2]
assert timeline == [
("select", 1),
("submit", 1),
("select", 2),
("submit", 2),
]
assert instance.model_snapshot() == {
"configured": 2,
"effective": 2,
"fallbackReason": None,
"current": "two",
"running": False,
}
assert [item["model"]["effective"] for item in payloads] == [1, 1, 2, 2]
assert [item["model"]["running"] for item in payloads] == [True, False, True, False]
def test_conductor_defers_events_without_models_and_recovers(monkeypatch):
class EndOfInbox(Exception):
pass
ready = {"value": False}
class BoundaryInbox:
def __init__(self):
self.events = [
{"type": "user_message", "id": "deferred"},
{"type": "user_message", "id": "recovery"},
]
def get(self):
if not self.events:
raise EndOfInbox
if len(self.events) == 1:
ready["value"] = True
return self.events.pop(0)
def task_done(self):
pass
def empty(self):
return True
class RecoveringAgent:
def __init__(self):
self.inc_out = False
self.llmclients: list[Client] = []
self.llm_no = 0
self.task_queue: queue.Queue = queue.Queue()
self.submissions: list[str] = []
def load_llm_sessions(self):
if ready["value"] and not self.llmclients:
self.llmclients = [Client("recovered")]
def next_llm(self, no: int):
self.llm_no = no
self.llmclient = self.llmclients[no]
def get_llm_name(self, client=None, model=False):
client = client or self.llmclient
return client.backend.model if model else client.backend.name
def put_task(self, prompt: str, source: str):
assert source == "conductor"
self.submissions.append(prompt)
output: queue.Queue = queue.Queue()
output.put({"done": True, "turn": 1})
return output
instance = conductor.Conductor()
instance.agent = RecoveringAgent()
instance.inbox = BoundaryInbox()
payloads: list[dict] = []
monkeypatch.setattr(conductor, "_settings_doc", lambda: {})
monkeypatch.setattr(conductor, "schedule_broadcast", payloads.append)
monkeypatch.setattr(conductor, "start_agent_runner", lambda *_args: None)
monkeypatch.setattr(conductor.time, "sleep", lambda _seconds: None)
monkeypatch.setattr(instance, "_build_prompt", lambda events: ",".join(e["id"] for e in events))
with pytest.raises(EndOfInbox):
instance._run()
assert instance.agent.submissions == ["deferred,recovery"]
model_payloads = [item["model"] for item in payloads if item["type"] == "model"]
assert model_payloads[0]["effective"] is None
assert model_payloads[0]["fallbackReason"] == "no_models"
assert model_payloads[0]["running"] is False
assert [model["running"] for model in model_payloads[1:]] == [True, False]
assert any(
item["item"]["text"].startswith("Conductor paused: no usable model")
for item in payloads
if item["type"] == "log"
)
def test_parallel_worker_model_selection_keeps_agents_isolated():
from concurrent.futures import ThreadPoolExecutor
agents = [FakeAgent(["zero", "one", "two", "three"]) for _ in range(10)]
requested = [i % 4 for i in range(10)]
with ThreadPoolExecutor(max_workers=10) as executor:
selected = list(executor.map(
lambda pair: conductor._select_llm(pair[0], pair[1]),
zip(agents, requested),
))
assert selected == [True] * 10
assert [agent.llm_no for agent in agents] == requested
assert [agent.next_llm_calls for agent in agents] == [[no] for no in requested]
+444
View File
@@ -0,0 +1,444 @@
"""Regression tests for the per-call token ledger."""
from __future__ import annotations
import importlib.util
import json
import sys
import threading
import time
import types
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent.parent
_SPEC = importlib.util.spec_from_file_location(
"desktop_cost_tracker_under_test", ROOT / "frontends" / "cost_tracker.py"
)
assert _SPEC and _SPEC.loader
cost_tracker = importlib.util.module_from_spec(_SPEC)
sys.modules[_SPEC.name] = cost_tracker
_SPEC.loader.exec_module(cost_tracker)
def _close_ledger() -> None:
fd = cost_tracker._ledger_fd
if fd is not None:
fd.close()
cost_tracker._ledger_fd = None
cost_tracker._ledger_path = None
def setup_function() -> None:
_close_ledger()
def teardown_function() -> None:
_close_ledger()
def test_uninitialized_append_returns_before_clock_json_and_lock(monkeypatch):
class ForbiddenLock:
def __enter__(self):
raise AssertionError("uninitialized TUI path acquired the ledger lock")
def __exit__(self, *_args):
return False
monkeypatch.setattr(
cost_tracker.json,
"dumps",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
AssertionError("uninitialized TUI path serialized JSON")
),
)
monkeypatch.setattr(
cost_tracker.time,
"time",
lambda: (_ for _ in ()).throw(
AssertionError("uninitialized TUI path read the clock")
),
)
monkeypatch.setattr(cost_tracker, "_ledger_lock", ForbiddenLock())
cost_tracker._append_ledger("tui-main", 1, 2, 3, 4)
def test_public_tracker_and_context_interfaces_keep_tui_semantics(monkeypatch):
cost_tracker._trackers.clear()
tracker = cost_tracker.get("tui-main")
assert tracker is cost_tracker.get("tui-main")
tracker.requests = 2
tracker.input = 10
tracker.output = 4
tracker.cache_create = 3
tracker.cache_read = 5
tracker.started_at = 100.0
monkeypatch.setattr(cost_tracker.time, "time", lambda: 106.5)
assert tracker.total_input_side() == 18
assert tracker.total_tokens() == 22
assert tracker.cache_hit_rate() == pytest.approx(5 / 18 * 100)
assert tracker.elapsed_seconds() == 6.5
snapshot = cost_tracker.all_trackers()
assert snapshot == {"tui-main": tracker}
snapshot.clear()
assert cost_tracker.all_trackers() == {"tui-main": tracker}
backend = types.SimpleNamespace(
context_win="200",
history=[{"role": "user", "content": "你好"}],
)
assert cost_tracker.context_window_chars(backend) == 600
assert cost_tracker.current_input_chars(backend) == len(
json.dumps(backend.history[0], ensure_ascii=False)
)
assert cost_tracker.context_window_chars(types.SimpleNamespace(context_win="bad")) == 0
def test_install_remains_idempotent_without_desktop_ledger(monkeypatch):
original_calls = []
printed = []
fake_llmcore = types.ModuleType("llmcore")
fake_llmcore._record_usage = lambda value, mode: original_calls.append((value, mode))
monkeypatch.setitem(sys.modules, "llmcore", fake_llmcore)
monkeypatch.setattr(cost_tracker, "_INSTALLED", False)
monkeypatch.setattr(cost_tracker, "_trackers", {})
monkeypatch.setattr("builtins.print", lambda *args, **_kwargs: printed.append(args))
cost_tracker.install()
wrapped_record = fake_llmcore._record_usage
wrapped_print = fake_llmcore.print
cost_tracker.install()
assert fake_llmcore._record_usage is wrapped_record
assert fake_llmcore.print is wrapped_print
fake_llmcore._record_usage(
{"prompt_tokens": 7, "prompt_tokens_details": {"cached_tokens": 2}},
"chat_completions",
)
fake_llmcore.print("[Output] tokens=3")
tracker = cost_tracker.get(threading.current_thread().name)
assert (tracker.requests, tracker.input, tracker.output, tracker.cache_read) == (1, 5, 3, 2)
assert original_calls and printed == [("[Output] tokens=3",)]
assert cost_tracker._ledger_path is None
def test_append_persists_and_aggregates_per_session(tmp_path):
cost_tracker.init_ledger(str(tmp_path))
cost_tracker._append_ledger("GA-sess-1", 10, 0, 2, 3)
cost_tracker._append_ledger("GA-sess-1", 0, 7, 0, 0)
cost_tracker._append_ledger("GA-sess-2", 4, 5, 0, 1)
entries = cost_tracker.read_ledger()
assert len(entries) == 3
aggregate = cost_tracker.aggregate_ledger()
by_id = {entry["sessionId"]: entry for entry in aggregate["history"]}
assert by_id["sess-1"]["input"] == 10
assert by_id["sess-1"]["output"] == 7
assert by_id["sess-1"]["cacheCreate"] == 2
assert by_id["sess-1"]["cacheRead"] == 3
assert aggregate["snap"]["GA-sess-2"] == {
"input": 4,
"output": 5,
"cacheCreate": 0,
"cacheRead": 1,
}
def test_concurrent_appends_are_serialized(tmp_path, monkeypatch):
cost_tracker.init_ledger(str(tmp_path))
real_fd = cost_tracker._ledger_fd
class DetectConcurrentWrites:
def __init__(self):
self.active = 0
self.overlapped = False
def write(self, value):
self.active += 1
if self.active > 1:
self.overlapped = True
time.sleep(0.01)
result = real_fd.write(value)
self.active -= 1
return result
def flush(self):
real_fd.flush()
def close(self):
real_fd.close()
detector = DetectConcurrentWrites()
monkeypatch.setattr(cost_tracker, "_ledger_fd", detector)
threads = [
threading.Thread(target=cost_tracker._append_ledger, args=(f"GA-{i}", 1, 0, 0, 0))
for i in range(12)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert detector.overlapped is False
assert len(cost_tracker.read_ledger()) == 12
def test_threshold_compaction_runs_during_process_lifetime(tmp_path, monkeypatch):
monkeypatch.setattr(cost_tracker, "_COMPACT_THRESHOLD", 1)
cost_tracker.init_ledger(str(tmp_path))
cost_tracker._append_ledger("GA-sess-1", 10, 7, 2, 3)
entries = cost_tracker.read_ledger()
assert len(entries) == 1
assert entries[0]["_compacted"] is True
def test_compaction_preserves_last_activity_timestamp(tmp_path):
cost_tracker.init_ledger(str(tmp_path))
ledger_path = tmp_path / "temp" / "token_ledger.jsonl"
first_ts = 1_700_000_000.0
second_ts = 1_700_000_100.0
cost_tracker._ledger_fd.close()
cost_tracker._ledger_fd = None
ledger_path.write_text(
"\n".join(
[
json.dumps({"t": first_ts, "k": "GA-sess-1", "i": 10, "o": 0, "cc": 0, "cr": 0}),
json.dumps({"t": second_ts, "k": "GA-sess-1", "i": 0, "o": 7, "cc": 0, "cr": 0}),
]
)
+ "\n",
encoding="utf-8",
)
cost_tracker._ledger_fd = ledger_path.open("a", encoding="utf-8")
cost_tracker._compact_ledger()
history = cost_tracker.aggregate_ledger()["history"]
assert history[0]["ts"] == second_ts
def test_reinitializing_closes_the_previous_descriptor(tmp_path):
cost_tracker.init_ledger(str(tmp_path / "one"))
previous_fd = cost_tracker._ledger_fd
cost_tracker.init_ledger(str(tmp_path / "two"))
assert previous_fd.closed is True
def test_init_migrates_legacy_snapshot_when_ledger_is_empty(tmp_path):
temp_dir = tmp_path / "temp"
temp_dir.mkdir()
(temp_dir / "desktop_token_history.json").write_text(
json.dumps(
{
"history": [
{
"sessionId": "sess-legacy",
"title": "Legacy session",
"input": 3,
"output": 4,
"cacheCreate": 5,
"cacheRead": 6,
"model": "legacy-model",
"ts": 1_700_000_000.0,
}
],
"snap": {
"GA-sess-legacy": {
"input": 30,
"output": 40,
"cacheCreate": 50,
"cacheRead": 60,
}
},
}
),
encoding="utf-8",
)
cost_tracker.init_ledger(str(tmp_path))
aggregate = cost_tracker.aggregate_ledger()
assert aggregate["snap"]["GA-sess-legacy"] == {
"input": 30,
"output": 40,
"cacheCreate": 50,
"cacheRead": 60,
}
assert aggregate["history"][0]["title"] == "Legacy session"
assert aggregate["history"][0]["model"] == "legacy-model"
assert aggregate["history"][0]["ts"] == 1_700_000_000.0
def test_init_does_not_migrate_legacy_snapshot_into_nonempty_ledger(tmp_path):
temp_dir = tmp_path / "temp"
temp_dir.mkdir()
(temp_dir / "token_ledger.jsonl").write_text(
json.dumps({"t": 2.0, "k": "GA-current", "i": 1, "o": 0, "cc": 0, "cr": 0}) + "\n",
encoding="utf-8",
)
(temp_dir / "desktop_token_history.json").write_text(
json.dumps({"snap": {"GA-legacy": {"input": 99, "output": 0, "cacheCreate": 0, "cacheRead": 0}}}),
encoding="utf-8",
)
cost_tracker.init_ledger(str(tmp_path))
assert set(cost_tracker.aggregate_ledger()["snap"]) == {"GA-current"}
@pytest.mark.parametrize(
("api_mode", "usage", "expected"),
[
(
"messages",
{
"input_tokens": 10,
"output_tokens": 1,
"cache_creation_input_tokens": 2,
"cache_read_input_tokens": 3,
},
{"input": 10, "output": 7, "cacheCreate": 2, "cacheRead": 3},
),
(
"chat_completions",
{"prompt_tokens": 13, "prompt_tokens_details": {"cached_tokens": 3}},
{"input": 10, "output": 7, "cacheCreate": 0, "cacheRead": 3},
),
(
"responses",
{"input_tokens": 13, "input_tokens_details": {"cached_tokens": 3}},
{"input": 10, "output": 7, "cacheCreate": 0, "cacheRead": 3},
),
],
)
def test_install_hooks_append_each_api_mode(tmp_path, monkeypatch, api_mode, usage, expected):
original_calls = []
fake_llmcore = types.ModuleType("llmcore")
fake_llmcore._record_usage = lambda value, mode: original_calls.append((value, mode))
monkeypatch.setitem(sys.modules, "llmcore", fake_llmcore)
monkeypatch.setattr(cost_tracker, "_INSTALLED", False)
cost_tracker._trackers.clear()
cost_tracker.init_ledger(str(tmp_path))
cost_tracker.install()
fake_llmcore._record_usage(usage, api_mode)
fake_llmcore.print("[Output] tokens=7")
snapshot = next(iter(cost_tracker.aggregate_ledger()["snap"].values()))
assert snapshot == expected
assert original_calls == [(usage, api_mode)]
def test_concurrent_appends_remain_exact_across_compactions(tmp_path, monkeypatch):
monkeypatch.setattr(cost_tracker, "_COMPACT_THRESHOLD", 1_000)
cost_tracker.init_ledger(str(tmp_path))
per_thread = 20
def append_session(key: str) -> None:
for _ in range(per_thread):
cost_tracker._append_ledger(key, 1, 2, 3, 4)
threads = [
threading.Thread(target=append_session, args=(key,))
for key in (f"GA-sess-{i}" for i in range(8))
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
aggregate = cost_tracker.aggregate_ledger()
assert len(aggregate["snap"]) == 8
for snapshot in aggregate["snap"].values():
assert snapshot == {
"input": per_thread,
"output": per_thread * 2,
"cacheCreate": per_thread * 3,
"cacheRead": per_thread * 4,
}
def test_corrupt_and_truncated_lines_are_skipped(tmp_path):
cost_tracker.init_ledger(str(tmp_path))
cost_tracker._append_ledger("GA-valid", 3, 4, 5, 6)
ledger_path = tmp_path / "temp" / "token_ledger.jsonl"
cost_tracker._ledger_fd.write("not-json\n{\"truncated\":\n")
cost_tracker._ledger_fd.flush()
assert cost_tracker.aggregate_ledger()["snap"] == {
"GA-valid": {"input": 3, "output": 4, "cacheCreate": 5, "cacheRead": 6}
}
def test_compaction_uses_atomic_replace_and_keeps_descriptor_writable(tmp_path, monkeypatch):
cost_tracker.init_ledger(str(tmp_path))
cost_tracker._append_ledger("GA-valid", 3, 4, 0, 0)
real_replace = cost_tracker.os.replace
replacements = []
def observe_replace(source, target):
replacements.append((Path(source), Path(target)))
assert Path(source).name == "token_ledger.jsonl.tmp"
assert Path(target).name == "token_ledger.jsonl"
real_replace(source, target)
monkeypatch.setattr(cost_tracker.os, "replace", observe_replace)
cost_tracker._compact_ledger()
cost_tracker._append_ledger("GA-valid", 0, 2, 0, 0)
assert len(replacements) == 1
assert cost_tracker.aggregate_ledger()["snap"]["GA-valid"]["output"] == 6
def test_real_10mb_threshold_compacts_on_init(tmp_path):
temp_dir = tmp_path / "temp"
temp_dir.mkdir()
ledger_path = temp_dir / "token_ledger.jsonl"
valid = json.dumps({"t": 1, "k": "GA-valid", "i": 3, "o": 4, "cc": 0, "cr": 0}) + "\n"
with ledger_path.open("wb") as ledger:
ledger.write(valid.encode("utf-8"))
ledger.truncate(cost_tracker._COMPACT_THRESHOLD)
cost_tracker.init_ledger(str(tmp_path))
assert ledger_path.stat().st_size < 1_000
assert cost_tracker.aggregate_ledger()["snap"]["GA-valid"] == {
"input": 3,
"output": 4,
"cacheCreate": 0,
"cacheRead": 0,
}
def test_repeated_init_migrates_legacy_snapshot_only_once(tmp_path):
temp_dir = tmp_path / "temp"
temp_dir.mkdir()
(temp_dir / "desktop_token_history.json").write_text(
json.dumps({
"snap": {
"GA-legacy": {"input": 7, "output": 8, "cacheCreate": 9, "cacheRead": 10}
}
}),
encoding="utf-8",
)
cost_tracker.init_ledger(str(tmp_path))
cost_tracker.init_ledger(str(tmp_path))
assert cost_tracker.aggregate_ledger()["snap"]["GA-legacy"] == {
"input": 7,
"output": 8,
"cacheCreate": 9,
"cacheRead": 10,
}
assert len(cost_tracker.read_ledger()) == 1
+674
View File
@@ -0,0 +1,674 @@
from __future__ import annotations
import json
import stat
import zipfile
from pathlib import Path
import pytest
import frontends.data_backup as data_backup
from frontends.data_backup import (
BACKUP_FORMAT_VERSION,
BACKUP_SCHEMA,
BackupFormatError,
export_data_backup,
inspect_import_source,
materialize_import_source,
merge_data_files,
)
def _seed_data(root: Path) -> None:
(root / "memory" / "nested").mkdir(parents=True)
(root / "memory" / "notes.md").write_text("memory", encoding="utf-8")
(root / "memory" / "nested" / "facts.json").write_text("{}", encoding="utf-8")
(root / "temp" / "model_responses").mkdir(parents=True)
(root / "temp" / "model_responses" / "response.json").write_text("{}", encoding="utf-8")
(root / "temp" / "desktop_sessions").mkdir(parents=True)
(root / "temp" / "desktop_sessions" / "sess-one.json").write_text(
json.dumps({"id": "sess-one", "title": "One", "messages": []}),
encoding="utf-8",
)
(root / "mykey.py").write_text("secret", encoding="utf-8")
(root / "agentmain.py").write_text("code", encoding="utf-8")
(root / "logs").mkdir()
(root / "logs" / "bridge.log").write_text("private log", encoding="utf-8")
class TestDataBackupExport:
def test_exports_only_allowed_data_and_private_manifest(self, tmp_path: Path):
source = tmp_path / "source"
source.mkdir()
_seed_data(source)
destination = tmp_path / "GenericAgent-data-backup.zip"
result = export_data_backup(str(source), str(destination), "localRepository")
assert result["content"] == {"memory": 2, "responses": 1, "sessions": 1}
with zipfile.ZipFile(destination) as archive:
names = set(archive.namelist())
assert names == {
"manifest.json",
"memory/notes.md",
"memory/nested/facts.json",
"temp/model_responses/response.json",
"temp/desktop_sessions/sess-one.json",
}
manifest = json.loads(archive.read("manifest.json"))
assert manifest["schema"] == BACKUP_SCHEMA
assert manifest["formatVersion"] == BACKUP_FORMAT_VERSION
assert manifest["sourceMode"] == "localRepository"
assert str(source) not in json.dumps(manifest)
assert "mykey.py" not in names
assert "agentmain.py" not in names
assert "logs/bridge.log" not in names
def test_rejects_symlinks_instead_of_reading_through_them(self, tmp_path: Path):
source = tmp_path / "source"
(source / "memory").mkdir(parents=True)
secret = tmp_path / "outside-secret.txt"
secret.write_text("secret", encoding="utf-8")
(source / "memory" / "linked.txt").symlink_to(secret)
destination = tmp_path / "backup.zip"
with pytest.raises(ValueError, match="link or reparse point"):
export_data_backup(str(source), str(destination), "included")
assert not destination.exists()
def test_rejects_destination_inside_exported_data_before_replacing_it(
self, tmp_path: Path
):
source = tmp_path / "source"
(source / "memory").mkdir(parents=True)
destination = source / "memory" / "nested-backup.zip"
destination.write_bytes(b"old-destination")
with pytest.raises(ValueError, match="protected application data"):
export_data_backup(str(source), str(destination), "included")
assert destination.read_bytes() == b"old-destination"
def test_rejects_http_readable_destination_before_creating_it(self, tmp_path: Path):
source = tmp_path / "source"
(source / "memory").mkdir(parents=True)
(source / "memory" / "one.md").write_text("one", encoding="utf-8")
upload_root = source / "temp" / "desktop_uploads"
upload_root.mkdir(parents=True)
destination = upload_root / "exfil.zip"
with pytest.raises(ValueError, match="protected application data"):
export_data_backup(
str(source), str(destination), "included", forbidden_roots=(upload_root,)
)
assert not destination.exists()
def test_export_enforces_the_same_entry_limit_as_import(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
source = tmp_path / "source"
(source / "memory").mkdir(parents=True)
(source / "memory" / "one.md").write_text("one", encoding="utf-8")
destination = tmp_path / "backup.zip"
destination.write_bytes(b"old")
monkeypatch.setattr(data_backup, "MAX_ARCHIVE_ENTRIES", 1)
with pytest.raises(BackupFormatError, match="too many files"):
export_data_backup(str(source), str(destination), "included")
assert destination.read_bytes() == b"old"
def test_export_enforces_the_same_size_limit_as_import(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
source = tmp_path / "source"
(source / "memory").mkdir(parents=True)
(source / "memory" / "one.md").write_text("one", encoding="utf-8")
destination = tmp_path / "backup.zip"
destination.write_bytes(b"old")
monkeypatch.setattr(data_backup, "MAX_ARCHIVE_UNCOMPRESSED_BYTES", 1)
with pytest.raises(BackupFormatError, match="too large"):
export_data_backup(str(source), str(destination), "included")
assert destination.read_bytes() == b"old"
def test_export_revalidates_written_zip_before_replacing_destination(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
source = tmp_path / "source"
(source / "memory").mkdir(parents=True)
data_file = source / "memory" / "one.md"
data_file.write_text("one", encoding="utf-8")
destination = tmp_path / "backup.zip"
destination.write_bytes(b"old")
monkeypatch.setattr(data_backup, "MAX_ARCHIVE_UNCOMPRESSED_BYTES", 10_000)
original_write = zipfile.ZipFile.write
def grow_then_write(archive, filename, arcname=None, *args, **kwargs):
path = Path(filename)
if path == data_file:
path.write_bytes(b"x" * 20_000)
return original_write(archive, filename, arcname, *args, **kwargs)
monkeypatch.setattr(zipfile.ZipFile, "write", grow_then_write)
with pytest.raises(BackupFormatError, match="too large"):
export_data_backup(str(source), str(destination), "included")
assert destination.read_bytes() == b"old"
assert not list(tmp_path.glob(".backup.*.tmp"))
class TestDataBackupInspection:
def test_inspects_generated_backup(self, tmp_path: Path):
source = tmp_path / "source"
source.mkdir()
_seed_data(source)
destination = tmp_path / "backup.zip"
export_data_backup(str(source), str(destination), "included")
result = inspect_import_source(str(destination))
assert result["sourceType"] == "backupZip"
assert result["sourceMode"] == "included"
assert result["content"]["sessions"] == 1
def test_rejects_traversal_before_extraction(self, tmp_path: Path):
destination = tmp_path / "traversal.zip"
manifest = {
"schema": BACKUP_SCHEMA,
"formatVersion": BACKUP_FORMAT_VERSION,
"exportedAt": "2026-08-22T00:00:00Z",
"sourceMode": "included",
"content": {"memory": 0, "responses": 0, "sessions": 0},
}
with zipfile.ZipFile(destination, "w") as archive:
archive.writestr("manifest.json", json.dumps(manifest))
archive.writestr("../outside.txt", "escape")
with pytest.raises(BackupFormatError, match="invalid backup entry path"):
inspect_import_source(str(destination))
def test_rejects_symlink_entries(self, tmp_path: Path):
destination = tmp_path / "link.zip"
manifest = {
"schema": BACKUP_SCHEMA,
"formatVersion": BACKUP_FORMAT_VERSION,
"exportedAt": "2026-08-22T00:00:00Z",
"sourceMode": "included",
"content": {"memory": 1, "responses": 0, "sessions": 0},
}
link = zipfile.ZipInfo("memory/link")
link.create_system = 3
link.external_attr = (stat.S_IFLNK | 0o777) << 16
with zipfile.ZipFile(destination, "w") as archive:
archive.writestr("manifest.json", json.dumps(manifest))
archive.writestr(link, "../../secret")
with pytest.raises(BackupFormatError, match="contains links"):
inspect_import_source(str(destination))
def test_rejects_manifest_count_mismatch(self, tmp_path: Path):
destination = tmp_path / "mismatch.zip"
manifest = {
"schema": BACKUP_SCHEMA,
"formatVersion": BACKUP_FORMAT_VERSION,
"exportedAt": "2026-08-22T00:00:00Z",
"sourceMode": "included",
"content": {"memory": 2, "responses": 0, "sessions": 0},
}
with zipfile.ZipFile(destination, "w") as archive:
archive.writestr("manifest.json", json.dumps(manifest))
archive.writestr("memory/one.md", "one")
with pytest.raises(BackupFormatError, match="summary"):
inspect_import_source(str(destination))
def test_rejects_duplicate_paths_even_when_case_differs(self, tmp_path: Path):
destination = tmp_path / "duplicates.zip"
manifest = {
"schema": BACKUP_SCHEMA,
"formatVersion": BACKUP_FORMAT_VERSION,
"exportedAt": "2026-08-22T00:00:00Z",
"sourceMode": "included",
"content": {"memory": 2, "responses": 0, "sessions": 0},
}
with zipfile.ZipFile(destination, "w") as archive:
archive.writestr("manifest.json", json.dumps(manifest))
archive.writestr("memory/Note.md", "one")
archive.writestr("memory/note.md", "two")
with pytest.raises(BackupFormatError, match="duplicate"):
inspect_import_source(str(destination))
def test_accepts_session_only_legacy_folder(self, tmp_path: Path):
source = tmp_path / "legacy"
(source / "temp").mkdir(parents=True)
(source / "temp" / "desktop_sessions.json").write_text(
json.dumps([{"id": "sess-only", "messages": [], "msg_seq": 0}]),
encoding="utf-8",
)
result = inspect_import_source(str(source))
assert result["sourceType"] == "legacyFolder"
assert result["content"] == {"memory": 0, "responses": 0, "sessions": 1}
def test_rejects_empty_legacy_folder_and_empty_backup(self, tmp_path: Path):
source = tmp_path / "legacy"
(source / "temp").mkdir(parents=True)
(source / "temp" / "desktop_sessions.json").write_text("[]", encoding="utf-8")
with pytest.raises(BackupFormatError, match="no importable data"):
inspect_import_source(str(source))
backup = tmp_path / "empty.zip"
manifest = {
"schema": BACKUP_SCHEMA,
"formatVersion": BACKUP_FORMAT_VERSION,
"exportedAt": "2026-08-22T00:00:00Z",
"sourceMode": "included",
"content": {"memory": 0, "responses": 0, "sessions": 0},
}
with zipfile.ZipFile(backup, "w") as archive:
archive.writestr("manifest.json", json.dumps(manifest))
with pytest.raises(BackupFormatError, match="no importable data"):
inspect_import_source(str(backup))
def test_accepts_generated_sessions_only_zip(self, tmp_path: Path):
source = tmp_path / "source"
sessions = source / "temp" / "desktop_sessions"
sessions.mkdir(parents=True)
(sessions / "sess-only.json").write_text(
json.dumps({"id": "sess-only", "messages": [], "msg_seq": 0}),
encoding="utf-8",
)
backup = tmp_path / "sessions-only.zip"
export_data_backup(str(source), str(backup), "included")
inspection = inspect_import_source(str(backup))
assert inspection["content"] == {"memory": 0, "responses": 0, "sessions": 1}
class TestDataBackupImport:
def test_sessions_only_backup_round_trips(self, tmp_path: Path):
source = tmp_path / "source"
sessions = source / "temp" / "desktop_sessions"
sessions.mkdir(parents=True)
(sessions / "sess-only.json").write_text(
json.dumps({"id": "sess-only", "messages": [], "msg_seq": 0}),
encoding="utf-8",
)
backup = tmp_path / "sessions-only.zip"
export_data_backup(str(source), str(backup), "included")
target = tmp_path / "target"
target.mkdir()
with materialize_import_source(str(backup)) as extracted:
result = merge_data_files(str(extracted), str(target))
assert result["sessionsAdded"] == 1
assert result["memoryCopied"] == 0
assert (target / "temp" / "desktop_sessions" / "sess-only.json").is_file()
def test_materializes_backup_and_applies_the_full_merge_contract(self, tmp_path: Path):
source = tmp_path / "source"
source.mkdir()
_seed_data(source)
backup = tmp_path / "backup.zip"
export_data_backup(str(source), str(backup), "included")
target = tmp_path / "target"
(target / "memory").mkdir(parents=True)
(target / "memory" / "notes.md").write_text("current", encoding="utf-8")
with materialize_import_source(str(backup)) as extracted:
result = merge_data_files(str(extracted), str(target))
assert (extracted / "temp" / "desktop_sessions" / "sess-one.json").is_file()
assert result.keys() >= {
"ok",
"memoryCopied",
"responsesCopied",
"responsesSkipped",
"sessionsAdded",
"sessionsSkipped",
"sessionsFileFound",
"backupDir",
}
assert result["memoryCopied"] == 2
assert result["memorySkipped"] == 0
assert result["responsesCopied"] == 1
assert result["sessionsAdded"] == 1
assert result["sessionsSkipped"] == 0
assert result["sessionsFileFound"] is True
assert (target / "memory" / "notes.md").read_text(encoding="utf-8") == "memory"
assert (target / "memory" / "nested" / "facts.json").is_file()
assert (target / "temp" / "desktop_sessions" / "sess-one.json").is_file()
backup_dir = Path(result["backupDir"])
assert backup_dir.is_dir()
assert (backup_dir / "memory" / "notes.md").read_text(encoding="utf-8") == "current"
def test_memory_is_source_wins_responses_are_add_only(self, tmp_path: Path):
source = tmp_path / "source"
target = tmp_path / "target"
(source / "memory").mkdir(parents=True)
(source / "memory" / "same.md").write_text("new", encoding="utf-8")
(source / "memory" / "added.md").write_text("added", encoding="utf-8")
(source / "temp" / "model_responses").mkdir(parents=True)
(source / "temp" / "model_responses" / "same.json").write_text("new", encoding="utf-8")
(source / "temp" / "model_responses" / "added.json").write_text("added", encoding="utf-8")
(target / "memory").mkdir(parents=True)
(target / "memory" / "same.md").write_text("old", encoding="utf-8")
(target / "temp" / "model_responses").mkdir(parents=True)
(target / "temp" / "model_responses" / "same.json").write_text("old", encoding="utf-8")
result = merge_data_files(str(source), str(target))
assert result["memoryCopied"] == 2
assert result["responsesCopied"] == 1
assert result["responsesSkipped"] == 1
assert (target / "memory" / "same.md").read_text(encoding="utf-8") == "new"
assert (target / "memory" / "added.md").read_text(encoding="utf-8") == "added"
assert (target / "temp" / "model_responses" / "same.json").read_text(encoding="utf-8") == "old"
assert (target / "temp" / "model_responses" / "added.json").read_text(encoding="utf-8") == "added"
assert (Path(result["backupDir"]) / "memory" / "same.md").read_text(encoding="utf-8") == "old"
def test_sessions_dedupe_by_desktop_id_across_new_and_legacy_stores(self, tmp_path: Path):
source = tmp_path / "source"
target = tmp_path / "target"
(source / "memory").mkdir(parents=True)
source_sessions = source / "temp" / "desktop_sessions"
source_sessions.mkdir(parents=True)
(source_sessions / "existing.json").write_text(
json.dumps({"id": "sess-existing", "messages": []}), encoding="utf-8"
)
(source_sessions / "new.json").write_text(
json.dumps({"id": "sess-new", "messages": []}), encoding="utf-8"
)
(source_sessions / "tui.json").write_text(
json.dumps({"id": "tui_worker", "messages": []}), encoding="utf-8"
)
(source_sessions / "corrupt.json").write_text("{bad", encoding="utf-8")
(source / "temp" / "desktop_sessions.json").write_text(
json.dumps([
{"id": "sess-new", "messages": []},
{"id": "sess-legacy", "messages": []},
{"id": "../../escape", "messages": []},
]),
encoding="utf-8",
)
target_sessions = target / "temp" / "desktop_sessions"
target_sessions.mkdir(parents=True)
(target_sessions / "different-name.json").write_text(
json.dumps({"id": "sess-existing", "messages": []}), encoding="utf-8"
)
result = merge_data_files(
str(source), str(target), existing_session_ids={"sess-in-memory"}
)
assert result["sessionsAdded"] == 2
assert result["sessionsSkipped"] == 5
assert result["sessionsFileFound"] is True
assert (target_sessions / "sess-new.json").is_file()
assert (target_sessions / "sess-legacy.json").is_file()
assert not (tmp_path / "escape.json").exists()
def test_invalid_session_schema_is_skipped_and_valid_record_is_canonical(
self, tmp_path: Path
):
source = tmp_path / "source"
sessions = source / "temp" / "desktop_sessions"
sessions.mkdir(parents=True)
records = {
"int-id.json": {"id": 7, "messages": [], "msg_seq": 0},
"nan-time.json": {
"id": "sess-nan",
"messages": [],
"msg_seq": 0,
"updated_at": float("nan"),
},
"bad-seq.json": {"id": "sess-seq", "messages": [], "msg_seq": -1},
"bad-messages.json": {
"id": "sess-msg",
"messages": ["not-an-object"],
"msg_seq": 0,
},
"valid.json": {
"id": "sess-valid",
"title": "Valid",
"messages": [{"id": 1, "role": "user", "content": "hi"}],
"msg_seq": 1,
"created_at": 10,
"updated_at": 11.5,
"llm_history": [{"role": "user", "content": "hi"}],
"unknown": "drop-me",
},
}
for name, record in records.items():
(sessions / name).write_text(json.dumps(record), encoding="utf-8")
target = tmp_path / "target"
target.mkdir()
result = merge_data_files(str(source), str(target))
assert result["sessionsAdded"] == 1
assert result["sessionsSkipped"] == 4
document = json.loads(
(target / "temp" / "desktop_sessions" / "sess-valid.json").read_text(
encoding="utf-8"
)
)
assert document["id"] == "sess-valid"
assert document["updated_at"] == 11.5
assert document["messages"][0]["role"] == "user"
assert "unknown" not in document
def test_mocked_windows_reparse_source_is_rejected_before_read(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
source = tmp_path / "source"
memory = source / "memory"
memory.mkdir(parents=True)
(memory / "secret.md").write_text("secret", encoding="utf-8")
destination = tmp_path / "backup.zip"
real_check = data_backup._is_link_or_reparse
monkeypatch.setattr(
data_backup,
"_is_link_or_reparse",
lambda path: Path(path) == memory or real_check(Path(path)),
)
with pytest.raises(ValueError, match="safe directory"):
export_data_backup(str(source), str(destination), "included")
assert not destination.exists()
def test_mocked_windows_reparse_target_ancestor_is_rejected_before_write(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
source = tmp_path / "source"
target = tmp_path / "target"
(source / "memory").mkdir(parents=True)
(source / "memory" / "new.md").write_text("new", encoding="utf-8")
target_memory = target / "memory"
target_memory.mkdir(parents=True)
(target_memory / "old.md").write_text("old", encoding="utf-8")
real_check = data_backup._is_link_or_reparse
monkeypatch.setattr(
data_backup,
"_is_link_or_reparse",
lambda path: Path(path) == target_memory or real_check(Path(path)),
)
with pytest.raises(ValueError, match="safe directory"):
merge_data_files(str(source), str(target))
assert (target_memory / "old.md").read_text(encoding="utf-8") == "old"
assert not (target_memory / "new.md").exists()
def test_empty_target_needs_no_backup_and_reports_missing_sessions(self, tmp_path: Path):
source = tmp_path / "source"
target = tmp_path / "target"
(source / "memory").mkdir(parents=True)
(source / "memory" / "one.md").write_text("one", encoding="utf-8")
target.mkdir()
result = merge_data_files(str(source), str(target))
assert result["backupDir"] == ""
assert result["sessionsAdded"] == 0
assert result["sessionsSkipped"] == 0
assert result["sessionsFileFound"] is False
def test_backup_failure_never_writes_the_destination(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
source = tmp_path / "source"
target = tmp_path / "target"
(source / "memory").mkdir(parents=True)
(source / "memory" / "same.md").write_text("new", encoding="utf-8")
(source / "temp" / "model_responses").mkdir(parents=True)
(source / "temp" / "model_responses" / "added.json").write_text("new", encoding="utf-8")
(target / "memory").mkdir(parents=True)
(target / "memory" / "same.md").write_text("old", encoding="utf-8")
def fail_backup(*_args, **_kwargs):
raise OSError("backup disk full")
monkeypatch.setattr(data_backup, "_create_memory_backup", fail_backup)
with pytest.raises(OSError, match="backup disk full"):
merge_data_files(str(source), str(target))
assert (target / "memory" / "same.md").read_text(encoding="utf-8") == "old"
assert not (target / "temp" / "model_responses" / "added.json").exists()
def test_staging_copy_failure_cleans_up_without_partial_writes(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
source = tmp_path / "source"
target = tmp_path / "target"
responses = source / "temp" / "model_responses"
responses.mkdir(parents=True)
(responses / "a-ok.json").write_text("ok", encoding="utf-8")
(responses / "z-fail.json").write_text("fail", encoding="utf-8")
target.mkdir()
real_copy2 = data_backup.shutil.copy2
def fail_second_copy(source_path, destination_path, *args, **kwargs):
if Path(source_path).name == "z-fail.json":
raise OSError("copy failed")
return real_copy2(source_path, destination_path, *args, **kwargs)
monkeypatch.setattr(data_backup.shutil, "copy2", fail_second_copy)
with pytest.raises(OSError, match="copy failed"):
merge_data_files(str(source), str(target))
assert not (target / "temp" / "model_responses" / "a-ok.json").exists()
assert not list(target.glob(".genericagent-memory-import-*"))
def test_activation_partial_failure_rolls_memory_and_added_files_back(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
source = tmp_path / "source"
target = tmp_path / "target"
(source / "memory").mkdir(parents=True)
(source / "memory" / "same.md").write_text("new", encoding="utf-8")
(source / "temp" / "model_responses").mkdir(parents=True)
(source / "temp" / "model_responses" / "fail.json").write_text("new", encoding="utf-8")
(target / "memory").mkdir(parents=True)
(target / "memory" / "same.md").write_text("old", encoding="utf-8")
failing_target = target / "temp" / "model_responses" / "fail.json"
real_install = data_backup._install_file_add_only
def fail_response_activation(source_path, destination_path):
if Path(destination_path) == failing_target:
raise OSError("activation failed")
return real_install(source_path, destination_path)
monkeypatch.setattr(
data_backup, "_install_file_add_only", fail_response_activation
)
with pytest.raises(OSError, match="activation failed"):
merge_data_files(str(source), str(target))
assert (target / "memory" / "same.md").read_text(encoding="utf-8") == "old"
assert not failing_target.exists()
backups = list((target / "temp").glob("memory_import_backup_*"))
assert len(backups) == 1
assert (backups[0] / "memory" / "same.md").read_text(encoding="utf-8") == "old"
def test_add_only_install_falls_back_on_filesystems_without_hard_links(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
source = tmp_path / "source"
target = tmp_path / "target"
responses = source / "temp" / "model_responses"
responses.mkdir(parents=True)
(responses / "portable.json").write_text("portable", encoding="utf-8")
target.mkdir()
def unsupported_link(*_args, **_kwargs):
raise OSError(data_backup.errno.EOPNOTSUPP, "hard links unsupported")
monkeypatch.setattr(data_backup.os, "link", unsupported_link)
result = merge_data_files(str(source), str(target))
assert result["responsesCopied"] == 1
assert (
target / "temp" / "model_responses" / "portable.json"
).read_text(encoding="utf-8") == "portable"
def test_concurrent_response_creation_is_never_overwritten(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
source = tmp_path / "source"
target = tmp_path / "target"
(source / "memory").mkdir(parents=True)
(source / "memory" / "same.md").write_text("new", encoding="utf-8")
responses = source / "temp" / "model_responses"
responses.mkdir(parents=True)
(responses / "race.json").write_text("import", encoding="utf-8")
(target / "memory").mkdir(parents=True)
(target / "memory" / "same.md").write_text("old", encoding="utf-8")
racing_target = target / "temp" / "model_responses" / "race.json"
real_link = data_backup.os.link
def collide_then_link(source_path, destination_path, **kwargs):
destination = Path(destination_path)
if destination == racing_target:
destination.write_text("concurrent", encoding="utf-8")
return real_link(source_path, destination_path, **kwargs)
monkeypatch.setattr(data_backup.os, "link", collide_then_link)
with pytest.raises(FileExistsError):
merge_data_files(str(source), str(target))
assert racing_target.read_text(encoding="utf-8") == "concurrent"
assert (target / "memory" / "same.md").read_text(encoding="utf-8") == "old"
def test_memory_backup_refuses_symlinked_temp_directory(self, tmp_path: Path):
source = tmp_path / "source"
target = tmp_path / "target"
outside = tmp_path / "outside"
(source / "memory").mkdir(parents=True)
(source / "memory" / "same.md").write_text("new", encoding="utf-8")
(target / "memory").mkdir(parents=True)
(target / "memory" / "same.md").write_text("old", encoding="utf-8")
outside.mkdir()
(target / "temp").symlink_to(outside, target_is_directory=True)
with pytest.raises(ValueError, match="backup destination"):
merge_data_files(str(source), str(target))
assert (target / "memory" / "same.md").read_text(encoding="utf-8") == "old"
assert list(outside.iterdir()) == []
+144
View File
@@ -0,0 +1,144 @@
"""Contracts for the package-owned bridge/conductor plus external GA_ROOT architecture."""
from __future__ import annotations
import ast
import json
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
BRIDGE_SOURCE = (ROOT / "frontends" / "desktop_bridge.py").read_text(encoding="utf-8")
CONDUCTOR_SOURCE = (ROOT / "frontends" / "conductor.py").read_text(encoding="utf-8")
HUB_SOURCE = (ROOT / "frontends" / "hub.py").read_text(encoding="utf-8")
PROBE = ROOT / "frontends" / "ga_contract_probe.py"
def _load_function(source: str, name: str, namespace: dict):
tree = ast.parse(source)
node = next(item for item in tree.body if isinstance(item, ast.FunctionDef) and item.name == name)
exec(compile(ast.Module(body=[node], type_ignores=[]), "contract.py", "exec"), namespace)
return namespace[name]
def test_conductor_prefers_a_valid_external_ga_root(tmp_path, monkeypatch):
external = tmp_path / "external"
external.mkdir()
(external / "agentmain.py").write_text("# core\n", encoding="utf-8")
monkeypatch.setenv("GA_ROOT", str(external))
resolve = _load_function(CONDUCTOR_SOURCE, "_resolve_ga_root", {"os": os})
assert resolve() == str(external.resolve())
def test_bridge_discovers_package_conductor_but_external_scheduler(tmp_path):
external = tmp_path / "external"
(external / "reflect").mkdir(parents=True)
(external / "reflect" / "scheduler.py").write_text("# scheduler\n", encoding="utf-8")
bundle_frontends = tmp_path / "bundle" / "frontends"
bundle_frontends.mkdir(parents=True)
bundled_conductor = bundle_frontends / "conductor.py"
bundled_conductor.write_text("# conductor\n", encoding="utf-8")
discover = _load_function(
BRIDGE_SOURCE,
"discover_extra_services",
{
"Path": Path,
"List": list,
"APP_DIR": bundle_frontends,
"sys": sys,
"_configured_conductor_port": lambda: 29890,
},
)
catalog = {item["id"]: item for item in discover(external)}
assert catalog["frontends/conductor.py"]["cmd"][1] == str(bundled_conductor)
assert catalog["frontends/conductor.py"]["cmd"][-2:] == ["--port", "29890"]
assert catalog["frontends/conductor.py"]["port"] == 29890
assert catalog["reflect/scheduler.py"]["cmd"][-1] == "reflect/scheduler.py"
def test_service_spawn_injects_effective_ga_root():
tree = ast.parse(BRIDGE_SOURCE)
service_manager = next(
item for item in tree.body if isinstance(item, ast.ClassDef) and item.name == "ServiceManager"
)
start_service = next(
item for item in service_manager.body
if isinstance(item, ast.FunctionDef) and item.name == "start_service"
)
string_constants = {
node.value for node in ast.walk(start_service)
if isinstance(node, ast.Constant) and isinstance(node.value, str)
}
assert "GA_ROOT" in string_constants
def test_missing_optional_p2p_dependencies_cannot_abort_hub_setup():
tree = ast.parse(HUB_SOURCE)
p2p_import = next(
node
for node in ast.walk(tree)
if isinstance(node, ast.ImportFrom) and node.module == "hub_p2p"
)
guard = next(
node
for node in ast.walk(tree)
if isinstance(node, ast.Try) and p2p_import in set(ast.walk(node))
)
assert any(
handler.type is None
or (isinstance(handler.type, ast.Name) and handler.type.id == "Exception")
for handler in guard.handlers
)
def _run_probe(core: Path) -> tuple[subprocess.CompletedProcess[str], dict]:
completed = subprocess.run(
[sys.executable, str(PROBE), str(core)],
check=False,
capture_output=True,
text=True,
)
return completed, json.loads(completed.stdout.strip().splitlines()[-1])
def test_compatibility_probe_rejects_a_core_missing_generic_agent(tmp_path):
(tmp_path / "agentmain.py").write_text("# no GenericAgent\n", encoding="utf-8")
(tmp_path / "llmcore.py").write_text(
"def reload_mykeys(): pass\ndef _record_usage(usage, api_mode): pass\n",
encoding="utf-8",
)
completed, verdict = _run_probe(tmp_path)
assert completed.returncode == 1
assert verdict == {"ok": False, "missing": ["agentmain.GenericAgent"], "error": ""}
def test_compatibility_probe_accepts_the_desktop_core_contract(tmp_path):
(tmp_path / "agentmain.py").write_text(
"""class GenericAgent:
def run(self): pass
def put_task(self, prompt, images=None, source=None): pass
def next_llm(self): pass
def load_llm_sessions(self): pass
def get_llm_name(self, model=None): pass
def abort(self): pass
""",
encoding="utf-8",
)
(tmp_path / "llmcore.py").write_text(
"def reload_mykeys(): pass\ndef _record_usage(usage, api_mode): pass\n",
encoding="utf-8",
)
completed, verdict = _run_probe(tmp_path)
assert completed.returncode == 0
assert verdict == {"ok": True, "missing": [], "error": ""}
+556
View File
@@ -0,0 +1,556 @@
import importlib.util
import io
import json
import plistlib
import urllib.error
import urllib.request
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[2]
def load_module(name: str, relative: str):
path = ROOT / relative
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
journey = load_module(
"real_package_journey",
"frontends/desktop/e2e/package/real_package_journey.py",
)
evidence = load_module(
"verify_candidate_evidence",
"frontends/desktop/e2e/package/verify_candidate_evidence.py",
)
def complete_report(platform: str = "linux"):
checks = {name: True for name in evidence.COMMON_CHECKS}
isolated_port = 29890
ownership = {
scenario: owned_conductor(port=isolated_port, pid=44120 + index)
for index, scenario in enumerate(sorted(evidence.SUCCESSFUL_APPLICATION_SCENARIOS))
}
checks["isolatedConductorOwnership"] = ownership
checks["ownedProcessStops"] = {
scenario: {
"app": 45120 + index * 3,
"bridge": 45121 + index * 3,
"conductor": ownership[scenario]["pid"],
}
for index, scenario in enumerate(sorted(evidence.SUCCESSFUL_APPLICATION_SCENARIOS))
}
checks["portRecovery"] = "release-then-production-restart"
if platform == "macos":
checks["macAppImmutable"] = True
bootstrap = {
name: {"phase": "failed" if name == "foreign-port" else "ready"}
for name in (
"first-launch",
"warm-restart",
"foreign-port",
"after-port-release",
"relocated",
"stale-override",
)
}
return {
"expectedCommit": "abc1234",
"releaseVersion": "0.2.0",
"artifact": {"sha256": "f" * 64},
"environment": {"isolatedConductorPort": isolated_port},
"success": True,
"checks": checks,
"bootstrap": bootstrap,
"manualChecklist": {"nativeVisuals": "pass"},
"screenshots": ["ready.png", "foreign.png"],
}
def test_candidate_report_contract_accepts_complete_platform_evidence():
assert evidence.assert_report("linux", complete_report(), "abc1234") == []
assert evidence.assert_report("macos", complete_report("macos"), "abc1234") == []
def test_candidate_report_contract_rejects_incomplete_manual_and_commit_evidence():
report = complete_report()
report["expectedCommit"] = "different"
report["manualChecklist"]["nativeVisuals"] = "pending"
failures = evidence.assert_report("linux", report, "abc1234")
assert any("commit" in failure for failure in failures)
assert any("manual checklist" in failure for failure in failures)
def test_candidate_report_contract_rejects_partial_or_unowned_conductor_evidence():
report = complete_report()
report["checks"]["isolatedConductorOwnership"].pop("relocated")
report["checks"]["ownedProcessStops"]["warm-restart"].pop("conductor")
failures = evidence.assert_report("linux", report, "abc1234")
assert any("ownership does not cover every successful launch" in item for item in failures)
assert any("invalid owned process stop evidence for warm-restart" in item for item in failures)
def test_stdlib_fake_model_emits_sse_and_redacts_auth_in_transcript():
fake = journey.FakeOpenAI()
fake.start()
try:
request = urllib.request.Request(
fake.base_url + "/v1/chat/completions",
data=json.dumps({"model": "e2e-model"}).encode(),
headers={"Authorization": "Bearer secret", "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=5) as response:
body = response.read().decode()
assert "Harness reply" in body
assert "[DONE]" in body
assert fake.transcript == [
{
"path": "/v1/chat/completions",
"model": "e2e-model",
"authorization": "[redacted]",
"at": fake.transcript[0]["at"],
}
]
finally:
fake.stop()
def write_info_plist(root: Path, **overrides):
contents = root / "Contents"
contents.mkdir(parents=True, exist_ok=True)
values = {
"CFBundleShortVersionString": "0.2.0",
"CFBundleVersion": "0.2.0",
**overrides,
}
with (contents / "Info.plist").open("wb") as stream:
plistlib.dump(values, stream)
def test_macos_package_version_comes_from_both_native_bundle_keys(tmp_path):
write_info_plist(tmp_path)
assert journey.read_macos_bundle_versions(tmp_path) == ("0.2.0", "0.2.0")
@pytest.mark.parametrize("key", ["CFBundleShortVersionString", "CFBundleVersion"])
@pytest.mark.parametrize("value", ["0.2.1", 200])
def test_macos_package_version_rejects_wrong_or_non_string_keys(tmp_path, key, value):
write_info_plist(tmp_path, **{key: value})
with pytest.raises(journey.JourneyFailure, match=key):
journey.read_macos_bundle_versions(tmp_path)
@pytest.mark.parametrize("key", ["CFBundleShortVersionString", "CFBundleVersion"])
def test_macos_package_version_rejects_missing_keys(tmp_path, key):
write_info_plist(tmp_path)
path = tmp_path / "Contents" / "Info.plist"
with path.open("rb") as stream:
values = plistlib.load(stream)
del values[key]
with path.open("wb") as stream:
plistlib.dump(values, stream)
with pytest.raises(journey.JourneyFailure, match=key):
journey.read_macos_bundle_versions(tmp_path)
@pytest.mark.parametrize("payload", [b"not a plist", b"", plistlib.dumps([])])
def test_macos_package_version_rejects_invalid_or_empty_plist(tmp_path, payload):
contents = tmp_path / "Contents"
contents.mkdir()
(contents / "Info.plist").write_bytes(payload)
with pytest.raises(journey.JourneyFailure, match="missing or invalid"):
journey.read_macos_bundle_versions(tmp_path)
def test_macos_package_version_rejects_missing_plist(tmp_path):
with pytest.raises(journey.JourneyFailure, match="missing or invalid"):
journey.read_macos_bundle_versions(tmp_path)
def test_package_shape_rejects_excluded_source_package_json(tmp_path):
package_root = tmp_path / "GenericAgent.app"
runtime_root = package_root / "Contents" / "Resources" / "runtime"
application = package_root / "Contents" / "MacOS" / "GenericAgent"
for path in [
application,
runtime_root / "app" / "agentmain.py",
runtime_root / "app" / "frontends" / "desktop_bridge.py",
runtime_root / "app" / "frontends" / "desktop" / "static" / "index.html",
runtime_root / "python" / "bin" / "python3",
runtime_root / ".prepared",
runtime_root / "app" / "frontends" / "desktop" / "package.json",
]:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("{}", encoding="utf-8")
write_info_plist(package_root)
candidate = object.__new__(journey.Journey)
candidate.args = type("Args", (), {"platform": "macos"})()
candidate.package_root = package_root
candidate.runtime_root = runtime_root
candidate.application = application
candidate.report = {"checks": {}}
with pytest.raises(journey.JourneyFailure, match="excluded Desktop source metadata"):
candidate.check_package_shape()
(runtime_root / "app" / "frontends" / "desktop" / "package.json").unlink()
candidate.check_package_shape()
assert candidate.report["checks"] == {
"packagedVersion": "0.2.0",
"packagedBundleVersion": "0.2.0",
"packageShape": True,
}
def chat_snapshot(*, status="idle", unfinished=False):
return {
"status": status,
"hasUnfinishedWork": unfinished,
"messages": [{"role": "assistant", "content": "Harness reply"}],
"lastError": "model failed" if status == "error" else "",
}
def test_package_chat_waits_for_the_live_turn_thread_after_reply_and_idle_status():
assert journey.completed_assistant_reply(chat_snapshot(status="running", unfinished=True)) is None
assert journey.completed_assistant_reply(chat_snapshot(unfinished=True)) is None
assert journey.completed_assistant_reply(chat_snapshot()) == "Harness reply"
missing_barrier = chat_snapshot()
missing_barrier.pop("hasUnfinishedWork")
assert journey.completed_assistant_reply(missing_barrier) is None
@pytest.mark.parametrize("status", ["error", "cancelled"])
def test_package_chat_fails_immediately_on_terminal_session_state(status):
with pytest.raises(journey.JourneyFailure, match=f"terminal state {status}"):
journey.completed_assistant_reply(chat_snapshot(status=status))
def valid_import_conflict():
return {
"ok": False,
"error": "managed Desktop services are running",
"code": "maintenance_conflict",
"runningSessions": [],
"runningExtras": ["frontends/conductor.py", "reflect/scheduler.py"],
}
def test_package_import_requires_the_exact_running_extras_conflict():
assert journey.validate_import_maintenance_conflict(409, valid_import_conflict()) == [
"frontends/conductor.py",
"reflect/scheduler.py",
]
def test_package_request_reads_the_expected_http_error_json(monkeypatch):
payload = {**valid_import_conflict(), "runningExtras": ["frontends/conductor.py"]}
error = urllib.error.HTTPError(
"http://127.0.0.1:14168/memory/import",
409,
"Conflict",
{},
io.BytesIO(json.dumps(payload).encode("utf-8")),
)
def reject(*_args, **_kwargs):
raise error
monkeypatch.setattr(urllib.request, "urlopen", reject)
status, response = journey.request_json_with_status(
"POST",
"/memory/import",
{"sourceDir": "/source"},
)
assert status == 409
assert response == payload
@pytest.mark.parametrize(
("status", "payload"),
[
(200, valid_import_conflict()),
(409, {**valid_import_conflict(), "ok": True}),
(409, {**valid_import_conflict(), "code": "other_conflict"}),
(409, {**valid_import_conflict(), "runningSessions": ["sess-live"]}),
(409, {**valid_import_conflict(), "runningExtras": []}),
(409, {**valid_import_conflict(), "runningExtras": ["reflect/scheduler.py"]}),
(409, {**valid_import_conflict(), "runningExtras": ["frontends/conductor.py"] * 2}),
],
)
def test_package_import_rejects_inexact_maintenance_conflicts(status, payload):
with pytest.raises(journey.JourneyFailure):
journey.validate_import_maintenance_conflict(status, payload)
def stopped_panel(*services):
return {"services": list(services)}
def owned_conductor(port=29890, pid=44123):
return {
"id": "frontends/conductor.py",
"status": "running",
"running": True,
"owned": True,
"external": False,
"port": port,
"pid": pid,
}
def test_package_journey_requires_exact_owned_isolated_conductor_state():
state = owned_conductor()
payload = stopped_panel(state)
assert journey.verified_owned_conductor(payload, 29890, True) is state
assert journey.verified_owned_conductor(payload, 29890, False) is None
mutations = [
{**state, "status": "error"},
{**state, "running": False},
{**state, "owned": False},
{**state, "external": True},
{**state, "port": 8900},
{**state, "pid": None},
]
for mutation in mutations:
assert journey.verified_owned_conductor(stopped_panel(mutation), 29890, True) is None
assert journey.verified_owned_conductor(stopped_panel(state, state), 29890, True) is None
def test_package_start_injects_only_the_isolated_e2e_conductor_port(tmp_path, monkeypatch):
candidate = object.__new__(journey.Journey)
candidate.process = None
candidate.package_root = tmp_path
candidate.application = tmp_path / "GenericAgent"
candidate.report_dir = tmp_path / "reports"
candidate.conductor_port = 29890
candidate.process_log = None
candidate.report = {"pids": []}
captured = {}
class FakeProcess:
pid = 44120
def fake_popen(command, **kwargs):
captured["command"] = command
captured.update(kwargs)
return FakeProcess()
monkeypatch.setattr(journey, "loopback_port_is_free", lambda port: port == 29890)
monkeypatch.setattr(journey.subprocess, "Popen", fake_popen)
candidate.start_application("first-launch")
candidate.process_log.close()
assert captured["env"]["GA_DESKTOP_E2E_CONDUCTOR_PORT"] == "29890"
assert captured["env"]["GA_DESKTOP_E2E_REPORT_DIR"].endswith(
"bootstrap/first-launch"
)
assert candidate.report["pids"] == [{"scenario": "first-launch", "app": 44120}]
def test_isolated_conductor_allocator_rejects_active_and_default_product_ports(monkeypatch):
candidates = iter([24170, 14168, 8900, 29890])
class FakeReservation:
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def bind(self, _address):
return None
def getsockname(self):
return ("127.0.0.1", next(candidates))
monkeypatch.setattr(journey, "BRIDGE_PORT", 24170)
monkeypatch.setattr(journey.socket, "socket", lambda *_args, **_kwargs: FakeReservation())
assert journey.allocate_isolated_conductor_port() == 29890
def test_every_successful_ready_snapshot_records_its_owned_conductor_pid(
tmp_path, monkeypatch
):
candidate = object.__new__(journey.Journey)
candidate.args = SimpleNamespace(start_timeout=1, expected_commit="abc1234")
candidate.report_dir = tmp_path / "report"
candidate.external_root = tmp_path / "external"
candidate.runtime_root = tmp_path / "package" / "runtime"
candidate.conductor_port = 29890
candidate.report = {
"bootstrap": {},
"identities": {},
"pids": [{"scenario": "warm-restart", "app": 44120}],
"checks": {},
}
expected_app_dir = candidate.runtime_root / "app" / "frontends"
latest = candidate.scenario_dir("warm-restart") / "bootstrap-latest.json"
journey.write_json(latest, {"phase": "ready"})
def fake_request(_method, path, body=None, timeout=5.0):
del body, timeout
if path == "/services/identity":
return {
"ga_root": str(candidate.external_root),
"app_dir": str(expected_app_dir),
"pid": 44121,
"build_id": "desktop-abc1234",
}
if path == "/services/panel":
return stopped_panel(owned_conductor(pid=44122))
raise AssertionError(path)
monkeypatch.setattr(journey, "request_json", fake_request)
monkeypatch.setattr(
journey,
"loopback_port_is_free",
lambda port: False if port == 29890 else True,
)
candidate.wait_ready("warm-restart", candidate.external_root)
assert candidate.report["pids"][-1]["bridge"] == 44121
assert candidate.report["pids"][-1]["conductor"] == 44122
assert candidate.report["checks"]["isolatedConductorOwnership"] == {
"warm-restart": owned_conductor(pid=44122)
}
def test_package_import_waits_until_every_reported_extra_is_offline():
ids = ["frontends/conductor.py", "reflect/scheduler.py"]
complete = stopped_panel(
{"id": "frontends/conductor.py", "running": False, "status": "offline"},
{"id": "reflect/scheduler.py", "running": False, "status": "offline"},
)
assert journey.verified_stopped_extras_panel(complete, ids) is complete
assert journey.verified_stopped_extras_panel(
stopped_panel({"id": ids[0], "running": False, "status": "offline"}),
ids,
) is None
assert journey.verified_stopped_extras_panel(
stopped_panel(
{"id": ids[0], "running": False, "status": "offline"},
{"id": ids[1], "running": True, "status": "running"},
),
ids,
) is None
assert journey.verified_stopped_extras_panel(
stopped_panel(
{"id": ids[0], "running": False, "status": "offline"},
{"id": ids[1], "running": False, "status": "error"},
),
ids,
) is None
def test_real_package_memory_import_stops_reported_extras_before_one_successful_import(
tmp_path, monkeypatch
):
candidate = object.__new__(journey.Journey)
candidate.external_root = tmp_path / "external"
candidate.work_dir = tmp_path / "work"
candidate.fake = SimpleNamespace(transcript=[{"model": "e2e-model"}])
candidate.report = {"checks": {}}
sid = "sess-package-test"
session_file = candidate.external_root / "temp" / "desktop_sessions" / f"{sid}.json"
imported_sid = "sess-p2-package-imported"
stopped = False
calls = []
def fake_request(method, path, body=None, timeout=5.0):
nonlocal stopped
calls.append((method, path))
if path == "/session/new":
session_file.parent.mkdir(parents=True, exist_ok=True)
session_file.write_text('{"id":"sess-package-test"}\n', encoding="utf-8")
return {"sessionId": sid}
if path == "/upload":
upload = candidate.external_root / "temp" / "desktop_uploads" / "p2.png"
upload.parent.mkdir(parents=True, exist_ok=True)
upload.write_bytes(b"png")
return {"ok": True, "path": str(upload)}
if path.endswith("/prompt"):
return {"ok": True}
if path.endswith("/messages?limit=20"):
return chat_snapshot()
if path == "/services/stop-extras":
stopped = True
return {"ok": True}
if path == "/services/panel":
assert stopped is True
return stopped_panel(
{"id": "frontends/conductor.py", "running": False, "status": "offline"}
)
if path == "/memory/import":
assert stopped is True
source = Path(body["sourceDir"])
memory = candidate.external_root / "memory" / "p2-package-memory.txt"
responses = candidate.external_root / "temp" / "model_responses"
backup = candidate.external_root / "temp" / "memory_import_backup_test"
(backup / "memory").mkdir(parents=True)
(backup / "memory" / memory.name).write_bytes(memory.read_bytes())
memory.write_bytes((source / "memory" / memory.name).read_bytes())
(responses / "new.json").write_bytes(
(source / "temp" / "model_responses" / "new.json").read_bytes()
)
imported = candidate.external_root / "temp" / "desktop_sessions" / f"{imported_sid}.json"
imported.write_bytes(
(source / "temp" / "desktop_sessions" / imported.name).read_bytes()
)
return {
"ok": True,
"memoryCopied": 1,
"responsesCopied": 1,
"responsesSkipped": 1,
"sessionsAdded": 1,
"backupDir": str(backup),
}
raise AssertionError(f"unexpected request: {method} {path}")
def fake_conflict(method, path, body=None, timeout=5.0):
calls.append((method, f"{path}:expected-conflict"))
memory = candidate.external_root / "memory" / "p2-package-memory.txt"
responses = candidate.external_root / "temp" / "model_responses"
assert memory.read_text(encoding="utf-8") == "before\n"
assert (responses / "shared.json").read_text(encoding="utf-8") == "destination\n"
assert not (responses / "new.json").exists()
assert session_file.read_text(encoding="utf-8") == '{"id":"sess-package-test"}\n'
return 409, {**valid_import_conflict(), "runningExtras": ["frontends/conductor.py"]}
monkeypatch.setattr(journey, "request_json", fake_request)
monkeypatch.setattr(journey, "request_json_with_status", fake_conflict)
candidate.run_chat_upload_memory()
assert calls == [
("POST", "/session/new"),
("POST", "/upload"),
("POST", f"/session/{sid}/prompt"),
("GET", f"/session/{sid}/messages?limit=20"),
("POST", "/memory/import:expected-conflict"),
("POST", "/services/stop-extras"),
("GET", "/services/panel"),
("POST", "/memory/import"),
]
gate = candidate.report["checks"]["memoryImportMaintenanceGate"]
assert gate["runningSessions"] == []
assert gate["runningExtras"] == ["frontends/conductor.py"]
assert candidate.report["checks"]["memoryImport"]["ok"] is True
@@ -0,0 +1,125 @@
"""Security contracts for the packaged Tauri shell."""
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
TAURI_ROOT = ROOT / "frontends" / "desktop" / "src-tauri"
def _json(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def test_production_csp_is_local_and_keeps_required_tauri_ipc_and_services():
config = _json(TAURI_ROOT / "tauri.conf.json")
security = config["app"]["security"]
csp = security["csp"]
assert isinstance(csp, dict)
assert csp["object-src"] == "'none'"
assert csp["base-uri"] == "'self'"
assert csp["form-action"] == "'none'"
assert csp["frame-src"] == "'none'"
assert csp["frame-ancestors"] == "'none'"
assert "'unsafe-inline'" not in csp["script-src"]
assert "'unsafe-eval'" not in csp["script-src"]
connect_sources = set(csp["connect-src"].split())
assert {
"'self'",
"ipc:",
"http://ipc.localhost",
"http://127.0.0.1:14168",
"ws://127.0.0.1:14168",
"http://127.0.0.1:8900",
"ws://127.0.0.1:8900",
} <= connect_sources
assert not any("*" in source for source in connect_sources)
assert not any(source.startswith("https://") for source in connect_sources)
def test_global_tauri_remains_only_because_compiled_compatibility_page_uses_it():
config = _json(TAURI_ROOT / "tauri.conf.json")
fallback = (ROOT / "frontends" / "desktop" / "dist" / "fallback.html").read_text(
encoding="utf-8"
)
assert config["app"]["withGlobalTauri"] is True
assert "window.__TAURI__" in fallback
def test_main_and_setup_capabilities_are_window_scoped_without_remote_ipc_access():
config = _json(TAURI_ROOT / "tauri.conf.json")
main = _json(TAURI_ROOT / "capabilities" / "default.json")
setup = _json(TAURI_ROOT / "capabilities" / "setup.json")
assert config["app"]["security"]["capabilities"] == ["default", "setup"]
assert main["windows"] == ["main"]
assert setup["windows"] == ["setup"]
assert "remote" not in main
assert "remote" not in setup
main_permissions = set(main["permissions"])
setup_permissions = set(setup["permissions"])
assert "core:default" not in main_permissions
assert "core:window:default" not in main_permissions
assert "core:webview:default" not in main_permissions
assert "opener:default" not in main_permissions
assert {
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-close",
"core:window:allow-start-dragging",
"opener:allow-open-url",
"opener:allow-default-urls",
} <= main_permissions
assert {
"allow-start-bridge-with-config",
"allow-retry-bootstrap",
"allow-get-bootstrap-snapshot",
"allow-get-config",
"allow-discover-python-for-project",
"allow-pick-directory",
"allow-pick-python-interpreter",
} <= setup_permissions
assert not setup_permissions.intersection(
{
"allow-export-mykey",
"allow-pick-data-export-path",
"allow-reveal-in-file-manager",
"allow-set-ga-source",
"allow-clear-ga-source",
"opener:allow-open-url",
}
)
def test_e2e_config_only_adds_test_driver_and_dynamic_loopback_connectivity():
e2e = _json(TAURI_ROOT / "tauri.e2e.conf.json")
security = e2e["app"]["security"]
capabilities = security["capabilities"]
assert capabilities[:2] == ["default", "setup"]
capability = capabilities[2]
assert capability["identifier"] == "e2e"
assert capability["permissions"] == ["wdio:default"]
assert set(security["csp"]) == {"connect-src"}
connect_sources = set(security["csp"]["connect-src"].split())
assert {
"'self'",
"ipc:",
"http://ipc.localhost",
"http://127.0.0.1:*",
"ws://127.0.0.1:*",
} <= connect_sources
assert {source for source in connect_sources if "*" in source} == {
"http://127.0.0.1:*",
"ws://127.0.0.1:*",
}
assert not any(source.startswith("https://") for source in connect_sources)