From 0daa089ab6a09442d118014008040add2bc5c473 Mon Sep 17 00:00:00 2001 From: John Jeong Date: Mon, 24 Aug 2026 21:35:42 +0900 Subject: [PATCH] Launch QA app bundles through LaunchServices Preserve QA launch environment while keeping macOS permission checks bound to the app bundle identity. --- .agents/skills/qa-critical-ux/SKILL.md | 5 +- .../scripts/launch-native-dev-qa.sh | 23 ++++++ .../scripts/launch-native-dev-qa.test.mjs | 74 +++++++++++++++++++ .../scripts/run-native-dev-qa.sh | 6 +- .github/workflows/ci.yaml | 1 + 5 files changed, 106 insertions(+), 3 deletions(-) create mode 100755 .agents/skills/qa-critical-ux/scripts/launch-native-dev-qa.sh create mode 100644 .agents/skills/qa-critical-ux/scripts/launch-native-dev-qa.test.mjs diff --git a/.agents/skills/qa-critical-ux/SKILL.md b/.agents/skills/qa-critical-ux/SKILL.md index e565af9542..8d1645948b 100644 --- a/.agents/skills/qa-critical-ux/SKILL.md +++ b/.agents/skills/qa-critical-ux/SKILL.md @@ -47,7 +47,10 @@ configuration (`VITE_APP_URL`, `VITE_API_URL`, `VITE_SUPABASE_URL`, `VITE_SUPABASE_ANON_KEY` must match production; local URLs cannot pass), uses its own persistent Cargo cache under `~/Library/Caches/anarlog` (never the repo `target` directory), and launches with `AUDIO_SYNC_PROBE=1` and -`LISTENER_DEBUG=1`. Reuse an already-current bundle with `--launch-only`. +`LISTENER_DEBUG=1`. It launches the `.app` through LaunchServices so macOS +evaluates process-scoped permissions against the bundle identity. Do not run +the executable under `Contents/MacOS` directly. Reuse an already-current +bundle with `--launch-only`. Pin release-gate builds to the intended candidate commit: diff --git a/.agents/skills/qa-critical-ux/scripts/launch-native-dev-qa.sh b/.agents/skills/qa-critical-ux/scripts/launch-native-dev-qa.sh new file mode 100755 index 0000000000..34bb3a6559 --- /dev/null +++ b/.agents/skills/qa-critical-ux/scripts/launch-native-dev-qa.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -euo pipefail + +[[ $# -eq 1 ]] || { + echo "Usage: $0 " >&2 + exit 2 +} + +qa_bundle_dir="$1" +qa_open_executable="${ANARLOG_QA_OPEN_EXECUTABLE:-/usr/bin/open}" +qa_open_args=( + -W + --env AUDIO_SYNC_PROBE=1 + --env LISTENER_DEBUG=1 + --env NO_AEC= +) + +if [[ -n "${ONBOARDING+x}" ]]; then + qa_open_args+=(--env "ONBOARDING=$ONBOARDING") +fi + +exec "$qa_open_executable" "${qa_open_args[@]}" "$qa_bundle_dir" diff --git a/.agents/skills/qa-critical-ux/scripts/launch-native-dev-qa.test.mjs b/.agents/skills/qa-critical-ux/scripts/launch-native-dev-qa.test.mjs new file mode 100644 index 0000000000..f2beb40572 --- /dev/null +++ b/.agents/skills/qa-critical-ux/scripts/launch-native-dev-qa.test.mjs @@ -0,0 +1,74 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { chmod, mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const launcher = fileURLToPath( + new URL("./launch-native-dev-qa.sh", import.meta.url), +); + +async function fixture() { + const directory = await mkdtemp(path.join(tmpdir(), "anarlog-native-qa-")); + const appBundle = path.join(directory, "Anarlog Dev.app"); + const fakeOpen = path.join(directory, "open"); + + await mkdir(appBundle); + await writeFile(fakeOpen, "#!/usr/bin/env bash\nprintf '%s\\n' \"$@\"\n"); + await chmod(fakeOpen, 0o755); + + return { + appBundle, + directory, + launch(environment = {}) { + const childEnvironment = { + ...process.env, + ANARLOG_QA_OPEN_EXECUTABLE: fakeOpen, + ...environment, + }; + if (!("ONBOARDING" in environment)) { + delete childEnvironment.ONBOARDING; + } + + return spawnSync(launcher, [appBundle], { + encoding: "utf8", + env: childEnvironment, + }); + }, + }; +} + +test("launches the app bundle through LaunchServices with QA probes", async (t) => { + const harness = await fixture(); + t.after(() => rm(harness.directory, { force: true, recursive: true })); + + const result = harness.launch(); + + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(result.stdout.trim().split("\n"), [ + "-W", + "--env", + "AUDIO_SYNC_PROBE=1", + "--env", + "LISTENER_DEBUG=1", + "--env", + "NO_AEC=", + harness.appBundle, + ]); +}); + +test("forwards the onboarding reset through LaunchServices", async (t) => { + const harness = await fixture(); + t.after(() => rm(harness.directory, { force: true, recursive: true })); + + const result = harness.launch({ ONBOARDING: "1" }); + + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(result.stdout.trim().split("\n").slice(-3), [ + "--env", + "ONBOARDING=1", + harness.appBundle, + ]); +}); diff --git a/.agents/skills/qa-critical-ux/scripts/run-native-dev-qa.sh b/.agents/skills/qa-critical-ux/scripts/run-native-dev-qa.sh index 327f4255e8..1241116dd1 100755 --- a/.agents/skills/qa-critical-ux/scripts/run-native-dev-qa.sh +++ b/.agents/skills/qa-critical-ux/scripts/run-native-dev-qa.sh @@ -14,6 +14,7 @@ qa_lock_dir="$qa_target_dir/.anarlog-native-dev-qa-lock" qa_frontend_dist="$qa_repo_root/apps/desktop/dist" qa_config_validator="$qa_repo_root/.agents/skills/qa-critical-ux/scripts/validate-native-dev-qa-config.mjs" qa_gitbutler_candidate_resolver="$qa_repo_root/.agents/skills/qa-critical-ux/scripts/resolve-gitbutler-candidate.mjs" +qa_launcher="$qa_repo_root/.agents/skills/qa-critical-ux/scripts/launch-native-dev-qa.sh" qa_release_workflow="$qa_repo_root/.github/workflows/desktop_cd.yaml" qa_expected_supabase_url="https://ijoptyyjrfqwaqhyxkxj.supabase.co" qa_xcode_developer_dir="/Applications/Xcode.app/Contents/Developer" @@ -490,6 +491,8 @@ validate_manifest() { fail "Compiled frontend validator not found: $qa_config_validator" [[ -f "$qa_gitbutler_candidate_resolver" ]] || fail "GitButler candidate resolver not found: $qa_gitbutler_candidate_resolver" +[[ -x "$qa_launcher" ]] || + fail "Native Dev QA launcher not found: $qa_launcher" qa_node_executable="$(command -v node)" || fail "Node.js is required for native Dev QA configuration validation." qa_git_executable="$(command -v git)" || @@ -681,5 +684,4 @@ cleanup trap - EXIT INT TERM cd "$qa_repo_root" -exec env -u NO_AEC AUDIO_SYNC_PROBE=1 LISTENER_DEBUG=1 \ - "$qa_bundle_executable" +exec "$qa_launcher" "$qa_bundle_dir" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e7a4fe058d..60a66cab33 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,6 +10,7 @@ jobs: python3 .github/scripts/check_license_boundary.py - run: >- node --test + .agents/skills/qa-critical-ux/scripts/launch-native-dev-qa.test.mjs scripts/desktop-release-provenance.test.mjs scripts/publish-anarlog-skill.test.mjs scripts/repack-linux-appimage.test.mjs