diff --git a/.agents/capture/README.md b/.agents/capture/README.md index 0699d1472..7a2d7e123 100644 --- a/.agents/capture/README.md +++ b/.agents/capture/README.md @@ -23,8 +23,9 @@ eval "$(pnpm exec obsidian-e2e capture launch --vault /tmp/qa-capture/Notes --pr amp orb service start quickadd-capture \ --command "$PWD/node_modules/.bin/obsidian-e2e capture launch --vault /tmp/qa-capture/Notes" # `obsidian` is the CLI bridge from .agents/setup (~/.local/bin/obsidian). -HOME=$OBSIDIAN_E2E_CAPTURE_HOME obsidian vault=Notes plugins:restrict off -HOME=$OBSIDIAN_E2E_CAPTURE_HOME obsidian vault=Notes quickadd:list # lists "Add to inbox" +# On Linux the CLI socket follows XDG_RUNTIME_DIR, which the capture app sets to its HOME. +HOME=$OBSIDIAN_E2E_CAPTURE_HOME XDG_RUNTIME_DIR=$OBSIDIAN_E2E_CAPTURE_HOME obsidian vault=Notes plugins:restrict off +HOME=$OBSIDIAN_E2E_CAPTURE_HOME XDG_RUNTIME_DIR=$OBSIDIAN_E2E_CAPTURE_HOME obsidian vault=Notes quickadd:list # lists "Add to inbox" # After the restrict-off reload (any reload drops the injected capture CSS): pnpm exec obsidian-e2e capture prepare --width 1280 --height 800 --scale 2 \ @@ -32,7 +33,7 @@ pnpm exec obsidian-e2e capture prepare --width 1280 --height 800 --scale 2 \ ``` After `pnpm run build`, reload the plugin in the capture instance with -`HOME=$OBSIDIAN_E2E_CAPTURE_HOME obsidian vault=Notes plugin:reload id=quickadd`, +`HOME=$OBSIDIAN_E2E_CAPTURE_HOME XDG_RUNTIME_DIR=$OBSIDIAN_E2E_CAPTURE_HOME obsidian vault=Notes plugin:reload id=quickadd`, then re-run `pnpm exec obsidian-e2e capture prepare` (any app reload drops the injected capture CSS). ## Recipes diff --git a/.agents/obsidian-open b/.agents/obsidian-open deleted file mode 100755 index 48f570e1a..000000000 --- a/.agents/obsidian-open +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# obsidian-e2e 0.10 launches isolated instances through macOS's `open` command. -# Setup patches this worktree's installed runner to call this repository-local -# compatibility shim instead. Accept only the exact argv emitted by that runner. -readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" -readonly REPO_ROOT="$(dirname -- "$SCRIPT_DIR")" -readonly PROFILE_ROOT="/tmp/quickadd-obsidian-e2e" - -if (($# != 9)) || [[ "$1" != "-n" || "$2" != "-g" || "$3" != "-a" || \ - "$4" != "Obsidian" || "$5" != "--env" || "$6" != HOME=* || \ - "$7" != "--args" || "$9" != "--password-store=basic" ]]; then - echo "Unsupported Obsidian launcher argument shape." >&2 - exit 2 -fi - -raw_home="${6#HOME=}" -if [[ -z "$raw_home" || ! -d "$raw_home" ]]; then - echo "Obsidian HOME must be an existing directory." >&2 - exit 2 -fi -canonical_home="$(realpath -e -- "$raw_home")" -canonical_profile_root="$(realpath -e -- "$PROFILE_ROOT")" -instance_dir="$(dirname -- "$canonical_home")" - -# Requiring the supplied spelling to equal its canonical path rejects `..`, -# symlink aliases, and symlink escapes. The parent checks require exactly one -# instance directory between the profile root and its `home` child. -if [[ "$raw_home" != "$canonical_home" || "$(basename -- "$canonical_home")" != "home" || \ - "$(dirname -- "$instance_dir")" != "$canonical_profile_root" || \ - "$(basename -- "$instance_dir")" == "." || \ - "$(stat -c %u -- "$instance_dir")" != "$(id -u)" || \ - "$(stat -c %u -- "$canonical_home")" != "$(id -u)" || \ - ! -f "$instance_dir/obsidian-e2e-instance.json" || \ - "$(stat -c %u -- "$instance_dir/obsidian-e2e-instance.json")" != "$(id -u)" ]]; then - echo "Refusing non-canonical or escaped QuickAdd instance HOME: $raw_home" >&2 - exit 2 -fi - -expected_user_data="--user-data-dir=$canonical_home/Library/Application Support/obsidian" -if [[ "$8" != "$expected_user_data" ]]; then - echo "Unsupported Obsidian user-data directory." >&2 - exit 2 -fi - -export HOME="$canonical_home" -app_args=("$8" "$9") - -# Tests exercise the complete argument and path validation without launching a -# GUI. The production runner never sets this variable. -if [[ "${QUICKADD_OBSIDIAN_OPEN_VALIDATE_ONLY:-}" == "1" ]]; then - exit 0 -fi - -[[ -x /opt/Obsidian/obsidian ]] || { - echo "Missing executable Obsidian app: /opt/Obsidian/obsidian" >&2 - exit 1 -} -[[ -r /opt/Obsidian/resources/obsidian.asar ]] || { - echo "Missing readable Obsidian app archive: /opt/Obsidian/resources/obsidian.asar" >&2 - exit 1 -} -command -v xvfb-run >/dev/null 2>&1 || { - echo "xvfb-run is unavailable; rerun $REPO_ROOT/.agents/setup." >&2 - exit 1 -} - -log_dir="$HOME/.cache/obsidian-e2e" -mkdir -p "$log_dir" - -# Match macOS `open` semantics: return after starting the detached app. The -# runner owns teardown and identifies this process by its --user-data-dir. -( - trap '' HUP - exec xvfb-run -a /opt/Obsidian/obsidian --no-sandbox "${app_args[@]}" \ - >>"$log_dir/obsidian.log" 2>&1 0) { - const argument = args.shift(); - if (argument === "--check") { - checkOnly = true; - } else if (argument === "--dist-dir" && args[0]) { - distDir = path.resolve(args.shift()); - } else { - throw new Error(`Unsupported or incomplete argument: ${argument}`); - } -} -const shimPath = path.join(agentsDir, "obsidian-open"); - -const launchOriginal = 'resolveExec(deps)("/usr/bin/open", ['; -const launchPatched = `resolveExec(deps)(${JSON.stringify(shimPath)}, [`; -const staleLaunchPattern = /resolveExec\(deps\)\("[^"\n]+\/\.agents\/obsidian-open", \[/g; -const asarOriginal = - 'return [path.join("/Applications", leaf), path.join(os.userInfo().homedir, "Applications", leaf)];'; -const asarPatched = 'return ["/opt/Obsidian/resources/obsidian.asar"];'; - -const countLiteral = (source, literal) => source.split(literal).length - 1; -const files = []; - -// Read and plan every candidate before writing anything. Each semantic target -// must occur exactly once across the bundle and both targets must share one -// output file, allowing the update to be one atomic rename. -for (const name of (await fs.readdir(distDir)).sort()) { - if (!name.endsWith(".mjs")) continue; - const filePath = path.join(distDir, name); - const source = await fs.readFile(filePath, "utf8"); - const launchOriginalCount = countLiteral(source, launchOriginal); - const launchPatchedCount = countLiteral(source, launchPatched); - const staleLaunchMatches = [...source.matchAll(staleLaunchPattern)].map((match) => match[0]); - const staleOnlyCount = staleLaunchMatches.filter((match) => match !== launchPatched).length; - const asarOriginalCount = countLiteral(source, asarOriginal); - const asarPatchedCount = countLiteral(source, asarPatched); - files.push({ - filePath, - source, - launchOriginalCount, - launchPatchedCount, - staleOnlyCount, - asarOriginalCount, - asarPatchedCount, - }); -} - -const totals = files.reduce( - (total, file) => ({ - launchOriginal: total.launchOriginal + file.launchOriginalCount, - launchPatched: total.launchPatched + file.launchPatchedCount, - launchStale: total.launchStale + file.staleOnlyCount, - asarOriginal: total.asarOriginal + file.asarOriginalCount, - asarPatched: total.asarPatched + file.asarPatchedCount, - }), - { launchOriginal: 0, launchPatched: 0, launchStale: 0, asarOriginal: 0, asarPatched: 0 }, -); -const launchTotal = totals.launchOriginal + totals.launchPatched + totals.launchStale; -const asarTotal = totals.asarOriginal + totals.asarPatched; -if (launchTotal !== 1 || asarTotal !== 1) { - throw new Error( - `Expected exactly one launch and asar target; found launch=${JSON.stringify(totals)}, asarTotal=${asarTotal}`, - ); -} -if (checkOnly && (totals.launchPatched !== 1 || totals.asarPatched !== 1)) { - throw new Error("obsidian-e2e Linux bridge is not fully patched"); -} - -const targetFiles = files.filter( - (file) => - file.launchOriginalCount + file.launchPatchedCount + file.staleOnlyCount > 0 || - file.asarOriginalCount + file.asarPatchedCount > 0, -); -if (targetFiles.length !== 1) { - throw new Error(`Expected both patch targets in one file; found ${targetFiles.length} files`); -} - -const target = targetFiles[0]; -let output = target.source; -if (!checkOnly) { - output = output.replace(launchOriginal, launchPatched).replace(staleLaunchPattern, launchPatched); - output = output.replace(asarOriginal, asarPatched); -} -if (countLiteral(output, launchPatched) !== 1 || countLiteral(output, asarPatched) !== 1) { - throw new Error("Planned obsidian-e2e output did not contain exactly one patched target each"); -} - -if (!checkOnly && output !== target.source) { - const temporary = `${target.filePath}.quickadd-${process.pid}.tmp`; - try { - const stat = await fs.stat(target.filePath); - await fs.writeFile(temporary, output, { mode: stat.mode }); - await fs.rename(temporary, target.filePath); - } catch (error) { - await fs.rm(temporary, { force: true }); - throw error; - } -} - -console.log( - checkOnly - ? "obsidian-e2e Linux bridge is valid" - : `obsidian-e2e Linux bridge is valid (${output === target.source ? 0 : 1} file changed)`, -); diff --git a/.agents/resume b/.agents/resume index 1f087f755..2001a7bee 100755 --- a/.agents/resume +++ b/.agents/resume @@ -46,14 +46,9 @@ done echo "Invalid Obsidian CLI bridge; rerun .agents/setup." >&2 exit 1 } -[[ -x "$SCRIPT_DIR/obsidian-open" && -r /opt/Obsidian/resources/obsidian.asar ]] || { - echo "Invalid Obsidian app bridge; rerun .agents/setup." >&2 - exit 1 -} [[ -z "$(dpkg -V obsidian 2>/dev/null || true)" ]] || { echo "Obsidian package integrity check failed; rerun .agents/setup." >&2 exit 1 } -node "$SCRIPT_DIR/patch-obsidian-e2e-linux.mjs" --check >/dev/null echo "QuickAdd E2E prerequisites are available; Obsidian starts on demand." diff --git a/.agents/run-e2e b/.agents/run-e2e index 98b7ef9e0..1474bac50 100755 --- a/.agents/run-e2e +++ b/.agents/run-e2e @@ -21,13 +21,6 @@ export COREPACK_HOME="${COREPACK_HOME:-$ORIGINAL_HOME/.cache/quickadd-corepack}" cd "$REPO_ROOT" -# `pnpm install` rewrites obsidian-e2e's dist, which drops the orb's Linux -# launcher patch that .agents/setup applied. Reapply it (idempotent) so a -# dependency bump mid-session doesn't stop the suite from starting. -if [[ -e /opt/Obsidian/resources/obsidian.asar ]]; then - node "$SCRIPT_DIR/patch-obsidian-e2e-linux.mjs" >/dev/null -fi - # The runner loads whatever main.js is on disk. Build it from the checked-out # source so a stale bundle can't pass or fail a run for code it doesn't hold. pnpm run --silent build >/dev/null diff --git a/.agents/setup b/.agents/setup index 296e89394..486f6ce39 100755 --- a/.agents/setup +++ b/.agents/setup @@ -163,34 +163,8 @@ ln -sfn /opt/Obsidian/obsidian-cli "$HOME/.local/bin/obsidian" exit 1 } -# Undo global bridges installed by the first iteration of this setup, but only -# when they still point at the exact paths QuickAdd managed. -managed_legacy_open=false -if [[ "$(readlink -f /usr/bin/open 2>/dev/null || true)" == "/usr/local/lib/quickadd-obsidian-open" ]]; then - managed_legacy_open=true - sudo rm -f /usr/bin/open - sudo update-alternatives --auto open >/dev/null -fi -if [[ "$managed_legacy_open" == true ]]; then - sudo rm -f /usr/local/lib/quickadd-obsidian-open -fi -legacy_asar=/Applications/Obsidian.app/Contents/Resources/obsidian.asar -if [[ -L "$legacy_asar" ]] && \ - [[ "$(readlink -f "$legacy_asar")" == "/opt/Obsidian/resources/obsidian.asar" ]]; then - sudo rm -f "$legacy_asar" - sudo rmdir --ignore-fail-on-non-empty \ - /Applications/Obsidian.app/Contents/Resources \ - /Applications/Obsidian.app/Contents /Applications/Obsidian.app /Applications -fi -[[ "$(readlink -f /usr/bin/open 2>/dev/null || true)" != "/usr/local/lib/quickadd-obsidian-open" ]] || { - echo "Failed to restore the system open command." >&2 - exit 1 -} - echo "[setup] Project dependencies, build, and isolated vault" pnpm --dir "$REPO_ROOT" install --frozen-lockfile -node "$SCRIPT_DIR/patch-obsidian-e2e-linux.mjs" -node "$SCRIPT_DIR/patch-obsidian-e2e-linux.mjs" --check pnpm --dir "$REPO_ROOT" run build pnpm --dir "$REPO_ROOT" run --silent provision:e2e-vault diff --git a/AGENTS.md b/AGENTS.md index b56fe324c..aac76fc64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,16 +44,16 @@ Automation or scripted work should surface disruptive operations in the PR descr ## Obsidian Runtime Workflow Agents with the `verify-in-obsidian` skill get the generic workflow there: -vault-mode choice, the runner script quartet, the `--print-env` HOME remap, -instance teardown, and the dev-tools loop. This section is the QuickAdd-specific +vault-mode choice, the runner script quartet, `--print-env`, instance +teardown, and the dev-tools loop. This section is the QuickAdd-specific brief that a skill-less agent still needs. -- Plugin id `quickadd`. Reload with `obsidian vault= plugin:reload - id=quickadd`; the runner's ready probe is `quickadd:list`. Run a choice with - `pnpm run obsidian:e2e -- quickadd:run choice=` - note that `ok:true` - means the run finished, not that it did what you wanted. Drive anything else - through `pnpm run obsidian:e2e -- eval code=`; top-level `await` - is not allowed there, so wrap async work in an IIFE the eval can await. +- Plugin id `quickadd`; the runner's ready probe is `quickadd:list`. Run a + choice with `pnpm run obsidian:e2e -- quickadd:run choice=` - note that + `ok:true` means the run finished, not that it did what you wanted. Drive + anything else through `pnpm run obsidian:e2e -- eval code=`; + top-level `await` is not allowed there, so wrap async work in an IIFE the + eval can await. - The four runner scripts - `provision:e2e-vault`, `start:e2e-obsidian`, `stop:e2e-obsidian`, `obsidian:e2e` - run the shared `obsidian-e2e` bin, configured by `obsidian-e2e.config.mjs` at the repo root (plugin id, the @@ -91,7 +91,6 @@ pnpm run obsidian:e2e -- dev:errors # point the Vitest tests/e2e suite at the isolated instance: eval "$(pnpm run --silent start:e2e-obsidian -- --print-env)" -export HOME="$OBSIDIAN_E2E_OBSIDIAN_HOME" # re-point the CLI socket pnpm run test:e2e pnpm run stop:e2e-obsidian # stop this worktree's instance on wrap-up diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fcb601eac..5b8f1dfd8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,7 +80,6 @@ Obsidian instance, so the tests never touch your own vaults: ```bash eval "$(pnpm run --silent start:e2e-obsidian -- --print-env)" -export HOME="$OBSIDIAN_E2E_OBSIDIAN_HOME" pnpm run test:e2e pnpm run stop:e2e-obsidian ``` diff --git a/package.json b/package.json index 81d707748..c0be0242d 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "jsdom": "30", "moment": "2.31.0", "obsidian": "1.13.1", - "obsidian-e2e": "^0.12.1", + "obsidian-e2e": "^0.13.0", "svelte": "^5", "svelte-check": "^4", "svelte-eslint-parser": "^1.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 420ed0724..8fd70ba7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,8 +74,8 @@ importers: specifier: 1.13.1 version: 1.13.1(@codemirror/state@6.5.0)(@codemirror/view@6.38.6) obsidian-e2e: - specifier: ^0.12.1 - version: 0.12.1(vite-plus@0.1.24(@types/node@26.6.1)(@vitest/coverage-v8@5.0.2)(esbuild@0.28.2)(jiti@2.6.1)(jsdom@30.1.0)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.1)(esbuild@0.28.2)(jiti@2.6.1)(yaml@2.9.1))(yaml@2.9.1))(vitest@5.0.2) + specifier: ^0.13.0 + version: 0.13.0(vite-plus@0.1.24(@types/node@26.6.1)(@vitest/coverage-v8@5.0.2)(esbuild@0.28.2)(jiti@2.6.1)(jsdom@30.1.0)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.1)(esbuild@0.28.2)(jiti@2.6.1)(yaml@2.9.1))(yaml@2.9.1))(vitest@5.0.2) svelte: specifier: ^5 version: 5.57.0(@typescript-eslint/types@8.70.0) @@ -1667,8 +1667,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - obsidian-e2e@0.12.1: - resolution: {integrity: sha512-S3bpb9T2OZJipbIYAP1BV1xX0CYioRDN4JgJAwTF/Xi5k31tapG/8xIpr5p7C4z0XYFCafeec5tgeTqGTXrz+Q==} + obsidian-e2e@0.13.0: + resolution: {integrity: sha512-gUEFGFwDnH+s11c5qeRTphh/l4SxpiIMXc4tuBT4gG3I+5Aym2Bj0SwOFOjVf/aWvC7hUa/XjptiszKBbDp/Gw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3404,7 +3404,7 @@ snapshots: natural-compare@1.4.0: {} - obsidian-e2e@0.12.1(vite-plus@0.1.24(@types/node@26.6.1)(@vitest/coverage-v8@5.0.2)(esbuild@0.28.2)(jiti@2.6.1)(jsdom@30.1.0)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.1)(esbuild@0.28.2)(jiti@2.6.1)(yaml@2.9.1))(yaml@2.9.1))(vitest@5.0.2): + obsidian-e2e@0.13.0(vite-plus@0.1.24(@types/node@26.6.1)(@vitest/coverage-v8@5.0.2)(esbuild@0.28.2)(jiti@2.6.1)(jsdom@30.1.0)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.1)(esbuild@0.28.2)(jiti@2.6.1)(yaml@2.9.1))(yaml@2.9.1))(vitest@5.0.2): dependencies: vite-plus: 0.1.24(@types/node@26.6.1)(@vitest/coverage-v8@5.0.2)(esbuild@0.28.2)(jiti@2.6.1)(jsdom@30.1.0)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.1)(esbuild@0.28.2)(jiti@2.6.1)(yaml@2.9.1))(yaml@2.9.1) yaml: 2.9.1 diff --git a/tests/orb-setup.test.ts b/tests/orb-setup.test.ts index c1086b3ed..4d3ec108f 100644 --- a/tests/orb-setup.test.ts +++ b/tests/orb-setup.test.ts @@ -1,4 +1,4 @@ -import { execFileSync, spawnSync } from "node:child_process"; +import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -7,8 +7,6 @@ import { afterEach, describe, expect, it } from "vitest"; const repoRoot = path.resolve(import.meta.dirname, ".."); const runE2E = path.join(repoRoot, ".agents", "run-e2e"); -const openShim = path.join(repoRoot, ".agents", "obsidian-open"); -const patcher = path.join(repoRoot, ".agents", "patch-obsidian-e2e-linux.mjs"); const profileRoot = "/tmp/quickadd-obsidian-e2e"; const runningOnLinux = process.platform === "linux"; const temporaryPaths: string[] = []; @@ -227,124 +225,3 @@ exit 0 } }); }); - -describe.skipIf(!runningOnLinux)("Linux Obsidian launcher validation", () => { - function validArgs(validHome: string): string[] { - return [ - "-n", - "-g", - "-a", - "Obsidian", - "--env", - `HOME=${validHome}`, - "--args", - `--user-data-dir=${validHome}/Library/Application Support/obsidian`, - "--password-store=basic", - ]; - } - - function validate(args: string[]): ReturnType { - return spawnSync(openShim, args, { - env: { ...process.env, QUICKADD_OBSIDIAN_OPEN_VALIDATE_ONLY: "1" }, - encoding: "utf8", - }); - } - - it("preserves a pre-existing runner-owned profile root", () => { - const testCreatedRoot = !fs.existsSync(profileRoot); - if (testCreatedRoot) { - fs.mkdirSync(profileRoot, { mode: 0o700 }); - fs.chmodSync(profileRoot, 0o700); - } - const before = profileRootState(false); - const instanceName = `orb-root-preservation-${process.pid}-${Date.now()}`; - fs.mkdirSync(path.join(profileRoot, instanceName, "home"), { recursive: true }); - - try { - cleanProfileInstance(instanceName, before); - expect(profileRootState(false)).toEqual(before); - } finally { - fs.rmSync(path.join(profileRoot, instanceName), { recursive: true, force: true }); - if (testCreatedRoot) { - const finalState = profileRootState(false); - expect(finalState).toEqual(before); - expect(finalState.entries).toEqual([]); - expect(finalState.mode).toBe(0o700); - fs.rmdirSync(profileRoot); - expect(fs.existsSync(profileRoot)).toBe(false); - } - } - }); - - it("accepts only the runner's exact canonical argv", () => { - const initialRoot = prepareProfileRoot(); - const instanceDir = fs.mkdtempSync(path.join(profileRoot, "orb-script-test-")); - const instanceName = path.basename(instanceDir); - const validHome = path.join(instanceDir, "home"); - const args = validArgs(validHome); - const escapedHome = temporaryDirectory("quickadd-obsidian-escaped-home-"); - const linkName = `orb-script-link-${process.pid}-${Date.now()}`; - const linkPath = path.join(profileRoot, linkName); - fs.mkdirSync(validHome, { recursive: true, mode: 0o700 }); - fs.writeFileSync(path.join(instanceDir, "obsidian-e2e-instance.json"), "{}", { - mode: 0o600, - flag: "wx", - }); - try { - expect(validate(args).status).toBe(0); - const adversarial = [ - args.slice(1), - args.filter((argument) => argument !== "-g"), - args.filter((_, index) => index !== 4 && index !== 5), - [args[1], args[0], ...args.slice(2)], - [...args, "--extra"], - args.filter((argument) => argument !== "--password-store=basic"), - [...args.slice(0, 7), args[8], args[7]], - args.map((argument) => - argument === `HOME=${validHome}` ? `HOME=${validHome}/../home` : argument, - ), - ]; - fs.symlinkSync(escapedHome, linkPath); - adversarial.push( - args.map((argument) => - argument.includes(validHome) - ? argument.replace(validHome, linkPath) - : argument, - ), - ); - for (const args of adversarial) expect(validate(args).status).not.toBe(0); - } finally { - fs.rmSync(linkPath, { force: true }); - cleanProfileInstance(instanceName, initialRoot); - } - }); -}); - -describe("obsidian-e2e Linux patcher", () => { - const originalLaunch = 'resolveExec(deps)("/usr/bin/open", ['; - const originalAsar = - 'return [path.join("/Applications", leaf), path.join(os.userInfo().homedir, "Applications", leaf)];'; - - it("rejects duplicate targets without modifying the bundle", () => { - const dist = temporaryDirectory("quickadd-patcher-"); - const bundle = path.join(dist, "runner.mjs"); - const source = `${originalLaunch}\n${originalLaunch}\n${originalAsar}\n`; - fs.writeFileSync(bundle, source); - const result = spawnSync(process.execPath, [patcher, "--dist-dir", dist], { encoding: "utf8" }); - expect(result.status).not.toBe(0); - expect(fs.readFileSync(bundle, "utf8")).toBe(source); - expect(fs.readdirSync(dist)).toEqual(["runner.mjs"]); - }); - - it("atomically patches and then validates one expected bundle", () => { - const dist = temporaryDirectory("quickadd-patcher-"); - const bundle = path.join(dist, "runner.mjs"); - fs.writeFileSync(bundle, `${originalLaunch}\n${originalAsar}\n`); - execFileSync(process.execPath, [patcher, "--dist-dir", dist]); - execFileSync(process.execPath, [patcher, "--check", "--dist-dir", dist]); - const patchedBundle = fs.readFileSync(bundle, "utf8"); - expect(patchedBundle).toMatch(/\.agents[\\/]+obsidian-open/); - expect(patchedBundle).toContain("/opt/Obsidian/resources/obsidian.asar"); - expect(fs.readdirSync(dist)).toEqual(["runner.mjs"]); - }); -});