feat(drivers): Tauri driver adapter and a runnable sample, run on Windows and Ubuntu (Task 2.2, part 3b) - #11
Andreas-Froyland wants to merge 3 commits into
Conversation
…sumer (Task 2.2, part 3b, in progress) - packages/qa/src/drivers/tauri.ts: TauriApp starts tauri-driver as an owned process, opens a WebdriverIO session on the installed app, restarts and closes it (waiting for the app to exit), and owns the native driver and the app (found by parent pid and exact executable path) so the runner reaps them. Configuration mistakes are reported before anything starts; a tauri-driver that dies is reported at once with its exit code. - packages/qa/src/drivers/os.ts: processes by exact executable, children of a process, port checks. - examples/tauri-smoke/qa: project.json, the lifecycle (NSIS into the test root on Windows with the installer's outside effects undone; the .deb unpacked into the test root on Linux), and the Stage 0 persistence scenario. - docs/guides/run-the-sample.md: the setup guide for a fresh Windows or Ubuntu machine. - webdriverio 9.31.9 (the Stage 0 version), kept out of the package's main entry point. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…al sample runs - Evidence: one passing run per platform as the CLI wrote it (Windows 11 desktop; a fresh Ubuntu 24.04 WSL2 machine set up by following the guide), with the versions and hashes behind each. - tool-layout: the Stage 0 harness is promoted; the mapping says what the tool checks and what it does not (driver version match is a setup step, the installed binary was hash-checked by hand); the webdriverio dependency and its npm audit advisory (extract-zip via @puppeteer/browsers, a path the adapter never takes) are recorded. - README and local-runs: the adapter and runnable sample exist, with links to the guide and evidence. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
18 issues found across 25 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="examples/tauri-smoke/qa/lifecycle.ts">
<violation number="1" location="examples/tauri-smoke/qa/lifecycle.ts:39">
P2: This code claims `installDir` as owned without checking that it was absent. Refuse an existing install directory before recording ownership; otherwise untracked files in a designated root can be overwritten and recursively deleted by cleanup.</violation>
<violation number="2" location="examples/tauri-smoke/qa/lifecycle.ts:40">
P1: The Windows installer creates artifacts outside the runner ledger, but this lifecycle has no durable cleanup record for them. If the Node process dies after the installer finishes, `reset` removes only `smoke-app`, leaves the registry and shortcuts behind, and the next run refuses the existing `UNINSTALL_KEY`; persist cleanup metadata for these side effects or make reset invoke the lifecycle-specific cleanup before clearing the dirty state.</violation>
</file>
<file name="examples/tauri-smoke/qa/app.ts">
<violation number="1" location="examples/tauri-smoke/qa/app.ts:26">
P2: An empty `XDG_DATA_HOME` is treated as configured even though the XDG fallback should apply; filtering it out makes the cache directory become `dataDirs()[0]`. Reset then leaves the real app data behind and `settingFile()` checks the wrong file. Use truthy fallback semantics for the XDG variables.</violation>
<violation number="2" location="examples/tauri-smoke/qa/app.ts:75">
P3: On Linux without `/usr/bin/WebKitWebDriver`, this error tells the user to configure a Windows Edge driver. Report the Linux-specific `WebKitWebDriver`/`webkit2gtk-driver` fix instead.</violation>
</file>
<file name="packages/qa/src/drivers/os.ts">
<violation number="1" location="packages/qa/src/drivers/os.ts:28">
P1: Process discovery fails open on Linux: any `/proc` access error becomes “no processes,” bypassing the already-running refusal and ownership tracking. Propagate enumeration errors and only ignore an individual PID that disappeared between the directory scan and the read.</violation>
<violation number="2" location="packages/qa/src/drivers/os.ts:55">
P2: The `timeoutMs` deadline is not enforced while `portInUse` awaits `connect`. A stalled loopback connection can remain pending beyond the deadline; add a socket timeout and destroy it on timeout.</violation>
</file>
<file name="packages/qa/src/drivers/tauri.ts">
<violation number="1" location="packages/qa/src/drivers/tauri.ts:75">
P2: Driver-death handling leaves `waitForPort` running after the exit race wins, so a failed start still keeps timers and socket probes alive for up to the 60-second timeout. Make the port wait abortable and cancel it when tauri-driver exits.</violation>
<violation number="2" location="packages/qa/src/drivers/tauri.ts:121">
P1: `#open()` records the application only after `remote()` resolves. If the launch phase times out or WebDriver rejects after launching the app, `#own` is never reached or is refused, so startup fails without a cleanup handle and the app can remain running outside the ledger. Make session startup transactional and retain cleanup ownership for any app created before rethrowing.</violation>
<violation number="3" location="packages/qa/src/drivers/tauri.ts:121">
P1: The post-session scan owns every process with the application path, including an instance started by someone else after preflight. Track the process launched by the native-driver chain or retain and exclude baseline identities before recording cleanup ownership.</violation>
<violation number="4" location="packages/qa/src/drivers/tauri.ts:128">
P1: An unidentifiable live process is silently omitted from the ownership ledger, so cleanup has nothing to reap. Retry and verify that the PID exited, or fail the start as infrastructure error instead of skipping it.</violation>
</file>
<file name="examples/tauri-smoke/scripts/write-candidate.mjs">
<violation number="1" location="examples/tauri-smoke/scripts/write-candidate.mjs:14">
P3: This fallback treats every non-Windows host as Linux. Reject unsupported platforms explicitly instead of labeling a macOS or other host's bundle as a Linux candidate.</violation>
<violation number="2" location="examples/tauri-smoke/scripts/write-candidate.mjs:23">
P2: The optional id is written verbatim, so values such as `stage 0` produce a manifest that `release-qa run` rejects as malformed. Validate the id before writing and fail with the accepted grammar.</violation>
</file>
<file name="packages/qa/test/drivers/tauri.test.ts">
<violation number="1" location="packages/qa/test/drivers/tauri.test.ts:14">
P3: `began` is captured before `await arrange(...)`, so the measured `ms` includes arrange's `acquireTestRoot` warm-up — the PowerShell identity lock that the fixture's own comment says "can take seconds on a busy machine" and was deliberately moved inside arrange to stay "outside any deadline". On a slow or loaded Windows machine, that noise counts against the `ms < 15_000` assertion and can make these tests flake. Start the clock after arrange so the bound measures only `executeScenario`.</violation>
<violation number="2" location="packages/qa/test/drivers/tauri.test.ts:35">
P3: This test is titled "reported with its exit code", but the assertion only matches /tauri-driver exited/. If the adapter stopped including the exit code in its message (`tauri-driver exited (${code ?? signal})...` in src/drivers/tauri.ts), this test would still pass, so the behavior the test name promises is not pinned. Assert the code explicitly.</violation>
</file>
<file name="examples/tauri-smoke/qa/persistence.spec.ts">
<violation number="1" location="examples/tauri-smoke/qa/persistence.spec.ts:29">
P2: A failed Save that never creates `setting.txt` throws `ENOENT` before `assert.equal`, so the runner reports infrastructure-error instead of candidate failure. Assert file existence first.</violation>
<violation number="2" location="examples/tauri-smoke/qa/persistence.spec.ts:39">
P2: After the final restart, the cleared state is verified only through the readout, and `#saved-value` starts empty in the static HTML (`examples/tauri-smoke/web/index.html`). `restart()` returns as soon as the WebDriver session is up, so `shows(ctx, '')` — which polls immediately at 200 ms — can pass during the window before the app's first `load_value`/`refresh` settles the DOM, even if a misbehaving candidate re-created `setting.txt` at startup. Every other state transition checks disk (`readFileSync`/`existsSync`); add the same disk assertion here so a startup re-write cannot slip through as a false pass.</violation>
</file>
<file name="examples/tauri-smoke/qa/evidence/linux/candidate.json">
<violation number="1" location="examples/tauri-smoke/qa/evidence/linux/candidate.json:8">
P2: This committed candidate manifest references a `.deb` that is not present, so `release-qa run --candidate` fails before it can reproduce the recorded Linux run. Include the artifact with the manifest, or store this as explicitly non-runnable metadata instead of a candidate manifest.</violation>
</file>
<file name="docs/guides/run-the-sample.md">
<violation number="1" location="docs/guides/run-the-sample.md:29">
P2: The Windows setup fails for the per-user WebView2 installation this section documents: it only queries HKLM, so `$v` is unavailable and the driver download URL cannot be formed. Query the HKCU location as a fallback before downloading.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| } | ||
| const dir = installDir(ctx); | ||
| await ctx.own({ kind: 'path', path: dir, label: 'installed app' }); | ||
| if (windows) await runToEnd(ctx, 'installer', ctx.artifact.path, ['/S', `/D=${dir}`]); |
There was a problem hiding this comment.
P1: The Windows installer creates artifacts outside the runner ledger, but this lifecycle has no durable cleanup record for them. If the Node process dies after the installer finishes, reset removes only smoke-app, leaves the registry and shortcuts behind, and the next run refuses the existing UNINSTALL_KEY; persist cleanup metadata for these side effects or make reset invoke the lifecycle-specific cleanup before clearing the dirty state.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/tauri-smoke/qa/lifecycle.ts, line 40:
<comment>The Windows installer creates artifacts outside the runner ledger, but this lifecycle has no durable cleanup record for them. If the Node process dies after the installer finishes, `reset` removes only `smoke-app`, leaves the registry and shortcuts behind, and the next run refuses the existing `UNINSTALL_KEY`; persist cleanup metadata for these side effects or make reset invoke the lifecycle-specific cleanup before clearing the dirty state.</comment>
<file context>
@@ -0,0 +1,103 @@
+ }
+ const dir = installDir(ctx);
+ await ctx.own({ kind: 'path', path: dir, label: 'installed app' });
+ if (windows) await runToEnd(ctx, 'installer', ctx.artifact.path, ['/S', `/D=${dir}`]);
+ else await runToEnd(ctx, 'unpack', 'dpkg-deb', ['-x', ctx.artifact.path, dir]);
+ if (!existsSync(executable(ctx))) throw new Error(`the installer finished but ${executable(ctx)} does not exist`);
</file context>
| connectionRetryTimeout: this.#options.startTimeoutMs, | ||
| capabilities: { 'tauri:options': { application: this.#options.application } } as WebdriverIO.Capabilities, | ||
| }); | ||
| await this.#own(await processesRunning(this.#options.application), 'application'); |
There was a problem hiding this comment.
P1: #open() records the application only after remote() resolves. If the launch phase times out or WebDriver rejects after launching the app, #own is never reached or is refused, so startup fails without a cleanup handle and the app can remain running outside the ledger. Make session startup transactional and retain cleanup ownership for any app created before rethrowing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/qa/src/drivers/tauri.ts, line 121:
<comment>`#open()` records the application only after `remote()` resolves. If the launch phase times out or WebDriver rejects after launching the app, `#own` is never reached or is refused, so startup fails without a cleanup handle and the app can remain running outside the ledger. Make session startup transactional and retain cleanup ownership for any app created before rethrowing.</comment>
<file context>
@@ -0,0 +1,131 @@
+ connectionRetryTimeout: this.#options.startTimeoutMs,
+ capabilities: { 'tauri:options': { application: this.#options.application } } as WebdriverIO.Capabilities,
+ });
+ await this.#own(await processesRunning(this.#options.application), 'application');
+ }
+
</file context>
| return pidsIn(await output('powershell', ['-NoProfile', '-NonInteractive', '-Command', script])); | ||
| } | ||
| const pids: number[] = []; | ||
| for (const entry of await readdir('/proc').catch(() => [] as string[])) { |
There was a problem hiding this comment.
P1: Process discovery fails open on Linux: any /proc access error becomes “no processes,” bypassing the already-running refusal and ownership tracking. Propagate enumeration errors and only ignore an individual PID that disappeared between the directory scan and the read.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/qa/src/drivers/os.ts, line 28:
<comment>Process discovery fails open on Linux: any `/proc` access error becomes “no processes,” bypassing the already-running refusal and ownership tracking. Propagate enumeration errors and only ignore an individual PID that disappeared between the directory scan and the read.</comment>
<file context>
@@ -0,0 +1,69 @@
+ return pidsIn(await output('powershell', ['-NoProfile', '-NonInteractive', '-Command', script]));
+ }
+ const pids: number[] = [];
+ for (const entry of await readdir('/proc').catch(() => [] as string[])) {
+ if (!/^\d+$/.test(entry)) continue;
+ const exe = await readlink(`/proc/${entry}/exe`).catch(() => undefined);
</file context>
| connectionRetryTimeout: this.#options.startTimeoutMs, | ||
| capabilities: { 'tauri:options': { application: this.#options.application } } as WebdriverIO.Capabilities, | ||
| }); | ||
| await this.#own(await processesRunning(this.#options.application), 'application'); |
There was a problem hiding this comment.
P1: The post-session scan owns every process with the application path, including an instance started by someone else after preflight. Track the process launched by the native-driver chain or retain and exclude baseline identities before recording cleanup ownership.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/qa/src/drivers/tauri.ts, line 121:
<comment>The post-session scan owns every process with the application path, including an instance started by someone else after preflight. Track the process launched by the native-driver chain or retain and exclude baseline identities before recording cleanup ownership.</comment>
<file context>
@@ -0,0 +1,131 @@
+ connectionRetryTimeout: this.#options.startTimeoutMs,
+ capabilities: { 'tauri:options': { application: this.#options.application } } as WebdriverIO.Capabilities,
+ });
+ await this.#own(await processesRunning(this.#options.application), 'application');
+ }
+
</file context>
| for (const pid of pids) { | ||
| const identity = await processIdentity(pid); | ||
| // A process that already exited needs no owning; one that is alive but unidentifiable cannot be owned safely. | ||
| if (identity !== undefined) await this.#ctx.own({ kind: 'process', pid, identity, label }); |
There was a problem hiding this comment.
P1: An unidentifiable live process is silently omitted from the ownership ledger, so cleanup has nothing to reap. Retry and verify that the PID exited, or fail the start as infrastructure error instead of skipping it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/qa/src/drivers/tauri.ts, line 128:
<comment>An unidentifiable live process is silently omitted from the ownership ledger, so cleanup has nothing to reap. Retry and verify that the PID exited, or fail the start as infrastructure error instead of skipping it.</comment>
<file context>
@@ -0,0 +1,131 @@
+ for (const pid of pids) {
+ const identity = await processIdentity(pid);
+ // A process that already exited needs no owning; one that is alive but unidentifiable cannot be owned safely.
+ if (identity !== undefined) await this.#ctx.own({ kind: 'process', pid, identity, label });
+ }
+ }
</file context>
| else driver.once('exit', fail); | ||
| }); | ||
| exited.catch(() => undefined); // it also exits normally later, when the run's cleanup stops it | ||
| await Promise.race([waitForPort(port, app.#options.startTimeoutMs), exited]); |
There was a problem hiding this comment.
P2: Driver-death handling leaves waitForPort running after the exit race wins, so a failed start still keeps timers and socket probes alive for up to the 60-second timeout. Make the port wait abortable and cancel it when tauri-driver exits.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/qa/src/drivers/tauri.ts, line 75:
<comment>Driver-death handling leaves `waitForPort` running after the exit race wins, so a failed start still keeps timers and socket probes alive for up to the 60-second timeout. Make the port wait abortable and cancel it when tauri-driver exits.</comment>
<file context>
@@ -0,0 +1,131 @@
+ else driver.once('exit', fail);
+ });
+ exited.catch(() => undefined); // it also exits normally later, when the run's cleanup stops it
+ await Promise.race([waitForPort(port, app.#options.startTimeoutMs), exited]);
+ // The native driver is tauri-driver's child, not the run's; own it so it cannot outlive the run.
+ await app.#own(await childrenOf(driver.pid as number), 'native driver');
</file context>
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const bundle = join(dirname(fileURLToPath(import.meta.url)), '..', 'src-tauri', 'target', 'release', 'bundle'); | ||
| const [profile, kind, extension] = process.platform === 'win32' ? ['windows', 'nsis', '.exe'] : ['linux', 'deb', '.deb']; |
There was a problem hiding this comment.
P3: This fallback treats every non-Windows host as Linux. Reject unsupported platforms explicitly instead of labeling a macOS or other host's bundle as a Linux candidate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/tauri-smoke/scripts/write-candidate.mjs, line 14:
<comment>This fallback treats every non-Windows host as Linux. Reject unsupported platforms explicitly instead of labeling a macOS or other host's bundle as a Linux candidate.</comment>
<file context>
@@ -0,0 +1,26 @@
+import { fileURLToPath } from 'node:url';
+
+const bundle = join(dirname(fileURLToPath(import.meta.url)), '..', 'src-tauri', 'target', 'release', 'bundle');
+const [profile, kind, extension] = process.platform === 'win32' ? ['windows', 'nsis', '.exe'] : ['linux', 'deb', '.deb'];
+
+const files = readdirSync(join(bundle, kind)).filter((name) => name.endsWith(extension));
</file context>
| afterEach(cleanUpProcessesAndRoots); | ||
|
|
||
| async function launchWith(options: Parameters<typeof TauriApp.start>[1]) { | ||
| const began = Date.now(); |
There was a problem hiding this comment.
P3: began is captured before await arrange(...), so the measured ms includes arrange's acquireTestRoot warm-up — the PowerShell identity lock that the fixture's own comment says "can take seconds on a busy machine" and was deliberately moved inside arrange to stay "outside any deadline". On a slow or loaded Windows machine, that noise counts against the ms < 15_000 assertion and can make these tests flake. Start the clock after arrange so the bound measures only executeScenario.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/qa/test/drivers/tauri.test.ts, line 14:
<comment>`began` is captured before `await arrange(...)`, so the measured `ms` includes arrange's `acquireTestRoot` warm-up — the PowerShell identity lock that the fixture's own comment says "can take seconds on a busy machine" and was deliberately moved inside arrange to stay "outside any deadline". On a slow or loaded Windows machine, that noise counts against the `ms < 15_000` assertion and can make these tests flake. Start the clock after arrange so the bound measures only `executeScenario`.</comment>
<file context>
@@ -0,0 +1,56 @@
+afterEach(cleanUpProcessesAndRoots);
+
+async function launchWith(options: Parameters<typeof TauriApp.start>[1]) {
+ const began = Date.now();
+ const { context } = await arrange({
+ lifecycle: lifecycleOf([], { launch: async (ctx) => { await TauriApp.start(ctx, options); } }),
</file context>
| const began = Date.now(); | |
| const { context } = await arrange({ | |
| lifecycle: lifecycleOf([], { launch: async (ctx) => { await TauriApp.start(ctx, options); } }), | |
| timeouts: { phaseMs: 30_000, stepsMs: 2000, cleanupMs: 20_000 }, | |
| }); | |
| const began = Date.now(); |
| // Node rejects tauri-driver's arguments and exits at once: a driver that cannot start. | ||
| const { result, ms } = await launchWith({ application: `${process.execPath}.not-running`, nativeDriver: process.execPath, tauriDriver: process.execPath, startTimeoutMs: 20_000 }); | ||
| expect(result).toMatchObject({ outcome: 'interrupted', reason: 'infrastructure-error' }); | ||
| expect(result.detail).toMatch(/tauri-driver exited/); |
There was a problem hiding this comment.
P3: This test is titled "reported with its exit code", but the assertion only matches /tauri-driver exited/. If the adapter stopped including the exit code in its message (tauri-driver exited (${code ?? signal})... in src/drivers/tauri.ts), this test would still pass, so the behavior the test name promises is not pinned. Assert the code explicitly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/qa/test/drivers/tauri.test.ts, line 35:
<comment>This test is titled "reported with its exit code", but the assertion only matches /tauri-driver exited/. If the adapter stopped including the exit code in its message (`tauri-driver exited (${code ?? signal})...` in src/drivers/tauri.ts), this test would still pass, so the behavior the test name promises is not pinned. Assert the code explicitly.</comment>
<file context>
@@ -0,0 +1,56 @@
+ // Node rejects tauri-driver's arguments and exits at once: a driver that cannot start.
+ const { result, ms } = await launchWith({ application: `${process.execPath}.not-running`, nativeDriver: process.execPath, tauriDriver: process.execPath, startTimeoutMs: 20_000 });
+ expect(result).toMatchObject({ outcome: 'interrupted', reason: 'infrastructure-error' });
+ expect(result.detail).toMatch(/tauri-driver exited/);
+ expect(ms).toBeLessThan(15_000);
+ });
</file context>
| expect(result.detail).toMatch(/tauri-driver exited/); | |
| expect(result.detail).toMatch(/tauri-driver exited \(\d+\) before it listened on port/); |
| const configured = process.env.RELEASE_QA_NATIVE_DRIVER; | ||
| if (configured !== undefined && configured !== '') return configured; | ||
| if (!windows && existsSync('/usr/bin/WebKitWebDriver')) return '/usr/bin/WebKitWebDriver'; | ||
| throw new Error('set RELEASE_QA_NATIVE_DRIVER to the msedgedriver.exe matching the installed WebView2 runtime (see the setup guide)'); |
There was a problem hiding this comment.
P3: On Linux without /usr/bin/WebKitWebDriver, this error tells the user to configure a Windows Edge driver. Report the Linux-specific WebKitWebDriver/webkit2gtk-driver fix instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/tauri-smoke/qa/app.ts, line 75:
<comment>On Linux without `/usr/bin/WebKitWebDriver`, this error tells the user to configure a Windows Edge driver. Report the Linux-specific `WebKitWebDriver`/`webkit2gtk-driver` fix instead.</comment>
<file context>
@@ -0,0 +1,84 @@
+ const configured = process.env.RELEASE_QA_NATIVE_DRIVER;
+ if (configured !== undefined && configured !== '') return configured;
+ if (!windows && existsSync('/usr/bin/WebKitWebDriver')) return '/usr/bin/WebKitWebDriver';
+ throw new Error('set RELEASE_QA_NATIVE_DRIVER to the msedgedriver.exe matching the installed WebView2 runtime (see the setup guide)');
+}
+
</file context>
| throw new Error('set RELEASE_QA_NATIVE_DRIVER to the msedgedriver.exe matching the installed WebView2 runtime (see the setup guide)'); | |
| throw new Error(windows ? 'set RELEASE_QA_NATIVE_DRIVER to the msedgedriver.exe matching the installed WebView2 runtime (see the setup guide)' : 'install webkit2gtk-driver or set RELEASE_QA_NATIVE_DRIVER to WebKitWebDriver (see the setup guide)'); |
Summary
Task 2.2 of the plan, part 3b: the Tauri driver adapter and a runnable sample consumer, run for real on Windows and Ubuntu/Xvfb through
release-qa run. With #8–#10 this meets Task 2.2's exit criterion. A maintainer or agent can run the sample on either OS without the dashboard, and a fresh Ubuntu machine was taken to a passing run by following the written guide.The adapter (
packages/qa/src/drivers/tauri.ts)TauriApp.start(ctx, { application, nativeDriver })runs the chain Stage 0 proved: WebdriverIOremote(), thentauri-driver2.0.6, then Edge WebDriver orWebKitWebDriver, then the installed app. It offersbrowser,restart()andclose(), andclose()waits until the app has really exited.tauri-driveris started throughctx.spawn, so the run owns it. The native driver (found by parent pid) and the app (found by exact executable path, never by name) are recorded withctx.own, so the runner reaps them even if a hook doesn't.tauri-driverthat dies is reported at once with its exit code. It used to wait out the full 60 s timeout. The cause was a real race: spawning waits to record the process, so a driver that dies instantly has already emittedexit.The sample consumer (
examples/tauri-smoke/qa/)project.json, the lifecycle, and the Stage 0 persistence scenario. The scenario saves a value, reads it independently from disk, restarts and sees it, clears it, restarts and sees it gone. It waits with the runner'sctx.waitFor, so a value that never shows isfailed, not an infrastructure error.Windows. The NSIS installer runs silently into the test root. I measured what a per-user install creates outside its directory, by installing into a throwaway directory and diffing. It adds an uninstall entry, Start Menu and Desktop shortcuts, and
HKCU\Software\frogbyte\Release QA Smoke, which the uninstaller leaves behind. The lifecycle handles all of this:_?=), removes the leftover key, and removes the vendor key only if it's empty. The rule for "empty" is based on measuredreg queryoutput: an empty key prints nothing, and a key with a subkey prints one line.Linux. The
.debis unpacked into the test root (dpkg-deb -x). There's no root and nothing system-wide. I checked the unpacked binary is byte for byte whatapt-get installputs in/usr/bin, and it's also Stage 0's recorded installed binary.Real runs (evidence)
failed, exit 1, cleanup ok; missing native driver:interrupted, exit 3 (now in 2 s, with the real cause), cleanup okdoctoron Linux reportsvirtual (Xvfb serving :99). On WSL2, WSLg setsWAYLAND_DISPLAY, which would make GTK draw through WSLg instead of Xvfb. The guide says to unset it.Decisions to review
tool-layout.mdrecords that "packaged-app GUI runs need a designated machine and are not in CI", and hosted Windows runners are Windows Server rather than the Windows 11 desktop baseline. So I followed that decision and did it as in Stage 0: this laptop, plus a throwaway WSL2 distro. Say if you'd rather add a CI job.npm auditadvisory.webdriverio9.31.9 (Stage 0's version, pinned exactly) brings one advisory:extract-zip≤2.0.1, a symlink path traversal when extracting an archive. It's reached through@puppeteer/browsers, which WebdriverIO uses to download browsers. The adapter never takes that path: it connects to a runningtauri-driver, and drivers are fetched and hash-checked separately. No patchedextract-zipexists, and npm's only fix is WebdriverIO below 8.15. This is recorded intool-layout.md.../../../packages/qa/src/...) because nothing is published. Itsqa/files are type-checked as part of the package's typecheck.Testing
Unit tests for the OS helpers (processes by exact executable, children, ports) and for the adapter's pre-session refusals, driven with Node standing in for a driver that can't start.
Mutation check: 9 rules, all killed, files restored.
The real behaviour is proven by the runs above, not by the unit suite.
Clean
npm ci,npm run typecheck(now including the sample'sqa/files),npm test: 648 passed, 3 skipped. No leaked processes, temp directories or sample install traces.CI on
ubuntu-24.04andwindows-2025Not verified
evidence: []: the scenario takes no screenshots yet. The DOM checks and the independent disk read prove state, as in Stage 0..debis unpacked, so the package manager's steps and desktop integration aren't exercised.🤖 Generated with Claude Code
Summary by cubic
Adds a Tauri driver adapter and a runnable sample consumer so the packaged sample app now passes
release-qa runon an interactive Windows desktop and Ubuntu with Xvfb, meeting Task 2.2's exit criterion. Includes a setup guide and recorded evidence of real runs on both.tauri-driverand opens a WebdriverIO session on the installed app, and owns the native driver and app processes so the runner reaps them.tauri-driverthat dies immediately with its exit code.ctx.waitForso a missing value is afailedscenario rather than an infrastructure error..debso no root access is needed.webdriverio9.31.9 exactly pinned, kept out of the main entry point; the relatednpm auditadvisory (extract-zippath traversal via@puppeteer/browsers) is documented and never reached by the adapter.Written for commit 414e2da. Summary will update on new commits.