Skip to content

Add an end-to-end engine that drives the app against throwaway state - #362

Merged
juanmaguitar merged 1 commit into
trunkfrom
juanmaguitar/e2e-tests-baseline
Aug 19, 2026
Merged

Add an end-to-end engine that drives the app against throwaway state#362
juanmaguitar merged 1 commit into
trunkfrom
juanmaguitar/e2e-tests-baseline

Conversation

@juanmaguitar

@juanmaguitar juanmaguitar commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Why

Nothing in the suite has ever launched the app. The Git modules are covered against real repositories and the IPC handlers against a loaded main process, but the flows a contributor actually performs — linking a ticket, applying a patch, moving between branches — are verified only by a person following a How to test this section by hand.

That matters now because of #350. The git-native refactor moves ticket branches, applied patches, conflicts and trunk updates all at once, and a characterisation suite written afterwards can only record the refactor. Written first, it says which of the old guarantees were quietly dropped — which is the failure mode here, since nearly every regression in this area is silent: work parked and never restored, a patch quietly missing a file, a ticket that forgets its base.

This PR is the engine. The journeys themselves are #361.

What changes

Two suites, because there are two different apps under test and they answer different questions.

npm run test:e2e — the journeys. Drives the app built from the source tree and writes real state. Everything a journey needs before it can assert anything lives in one session helper, so no spec has to remember it:

  • It cannot reach your own sites. Every launch goes through the development-only data-directory redirect, and then reads back the path the app actually chose and refuses to continue if it is any other one. The env var is set last and is not overridable by the ambient environment.
  • Teardown always terminates. close() is raced against a timeout and the process killed after it — closing is known to hang on Windows in apps that keep child processes alive, which this one does constantly.
  • A failure leaves evidence. The trace is Playwright's; the screen and the state the app had persisted are attached here, because the interesting half of a failure in this app is on disk rather than on screen.
  • It can relaunch against the same profile. This is not a convenience. It is the only way to tell "the app persisted this" from "the app still had it in memory", and it is exactly what a change to the storage layer breaks without any other test noticing.

npm run test:e2e:packaged — the packaged smoke test. Launches an unsigned --dir build and asks only whether packaging worked. This is the test from the now-closed #70, rebuilt rather than rebased — see below.

Both run on macOS and Windows for every non-draft pull request, as two separate jobs so a five-second suite is not waiting behind a fifteen-minute one. Neither downloads a browser: the only thing launched is the Electron already in the tree, so there is no playwright install step anywhere.

Deliberately not in this PR: any journey. e2e/journeys/engine.spec.js asserts nothing about ticket branches or patches — it asserts that the four things every journey will depend on actually work, so that when a journey fails it is about the flow and not about the harness.

How to test this

Platforms: any for the journeys. The packaged half is macOS or Windows only, and the point of the CI matrix is that Windows is the half nobody can check locally.

Starting state: a clean checkout of this branch, npm ci done. Note the modification time of your real settings file before you start — on macOS, ~/Library/Application Support/electron-setup-wordpress-core/settings.json.

  1. npm run build:once && npm run test:e2e
    • Expected: 4 passed, in a few seconds. A real Electron window opens and closes four times.
  2. npm test
    • Expected: 1027 passed — the same count as on trunk. The unit runner only collects test/, so it must not pick up anything added here.
  3. npm run lint
    • Expected: clean. It is repo-wide.
  4. CSC_IDENTITY_AUTO_DISCOVERY=false npm run pack:dir && npm run test:e2e:packaged
    • Expected: 4 passed. Skip this if you would rather read the CI result — it takes several minutes.
  5. Break the guard on purpose: in e2e/helpers/app.cjs, change TOOLKIT_USER_DATA_DIR: this.userDataDir to TOOLKIT_USER_DATA_DIR: undefined, then npm run test:e2e.
    • Expected: all four tests fail, each saying the app is using your real profile and that it refuses to run. Put the line back.
  6. Break an assertion on purpose: in e2e/packaged/smoke.spec.js, delete any one entry from EXPECTED_API_KEYS, then npm run test:e2e:packaged.
    • Expected: the bridge test fails and names the missing key. Put it back.

What must not have happened:

  • Your real settings file must not have been touched. Compare its modification time with what you noted in the starting state. This is the whole reason step 5 exists: the guard is the only thing standing between a future change to the redirect hook and a suite that silently edits the site list you actually work on.
  • npm test must not have grown. If the count moved off 1027, the unit runner started collecting end-to-end specs, and npm test stops being something you run without thinking.
  • No Electron processes left running after any of the above. ps aux | grep -i electron on macOS, Task Manager on Windows.

Risks and limitations

  • Windows is unverified by hand. Everything above was run on macOS. Two of the four fixes in this branch are specifically about Windows behaviour I could not reproduce locally — path shapes and file handles — so the CI run on this PR is the first real evidence either way. That is the honest state of it.
  • CI time. The packaged job packages on both platforms for every non-draft PR: roughly ten to fifteen minutes per platform. The journeys job takes seconds. If the feedback loop gets annoying, the packaging job is what to trim, not the journeys.
  • The trunk update cannot be driven from a journey at all, on any platform. It reaches for a hardcoded clone URL rather than the checkout's own remote, so exercising it means cloning from the network — unacceptable in CI. Its two interesting cases stay in the integration suite, against a local server. Making the update read the remote is worth doing on its own merits and belongs in The checkout is a standard Git repository; the app is a porcelain over it #350; I left a note there.
  • The teardown's last-resort kill is a bare kill() rather than src/kill-tree.js. It only runs after close() has already had its timeout, and it is test code, but if orphaned Electron processes ever show up on the runners, that is where to look.

Related

Part of #359. Closes #360. Closes #67 — its three assertions ship here as the packaged project, on both platforms, documented in CONTRIBUTING.md, with Buildkite untouched. Replaces #70, which is closed. Unblocks #361, and #350 after it.


Design decisions and alternatives considered

@playwright/test rather than node --test with playwright-core. The first draft went the other way, to keep one runner in the repo and add no dependency. Requiring the suite to run in CI on two platforms for every pull request inverted that: retries, per-test timeouts, traces, the HTML report and the reporting artifacts are all things I would otherwise have written and then maintained, badly. Two runners is not a defect here — npm test stays the fast offline suite and npm run test:e2e is the one that launches an app, and they are meant to be told apart.

The dependency cost turned out to be smaller than assumed: @playwright/test at this version has no install script and downloads no browser. I checked the published tarball rather than trusting the recollection — an older version of Playwright did download browsers on install, which is why scripts/screenshots/capture.cjs depends on playwright-core directly and says so in its header.

A session object rather than a page fixture. A journey has to build its Git fixture before the app opens, because the site list has to name directories that already exist, and it needs the profile path to write that list. A fixture that launches the app for you takes that away. So the profile directory exists from construction and the spec decides when to start.

Rebuilt #70 rather than rebasing it. Two of its three assertions had gone stale in ways that would fail on first run: the list of bridged keys had roughly half the entries the app exposes today, and the native module it probes is no longer optional and no longer absent on Windows — it is a hard dependency of the bundled PHP runtime with a Windows-specific file-lock path — so its documented Windows exclusion had turned from a decision into a hole. Its structure, its comments and its reasoning are otherwise carried over almost verbatim; the closing comment on #70 lists what came across.

Not adding data-testid anywhere. The selectors use the text and roles already on screen, which also pins the visible copy as a contract. Where a name appears twice — the sidebar entry and the heading of the open site — roles tell them apart, and the assertion says which half of the app it is about.

Review outcome (required — see AGENTS.md)

2 [fix here] · 2 [follow-up] — both [fix here] fixed before this was pushed.

🔴 cross-platform · [fix here] — fixed. The launch guard compared path.resolve(a) !== path.resolve(b). That would have aborted every test on Windows, and possibly on macOS, for reasons unrelated to what it guards: macOS returns /var/folders/… from os.tmpdir() and /private/var/… from the app, since the first is a symlink to the second, and Windows returns short RUNNER~1 names in some environments and long ones in others, on a case-insensitive filesystem. Replaced with a samePath() that resolves through realpath and lowercases off POSIX.

🔴 cross-platform · [fix here] — fixed. The fake site directories were removed in an afterEach, which Playwright runs before fixture teardown — with the app still alive and holding handles on them. On Windows that is EPERM, and a failure that is not the test's. Cleanup moved onto the session, which removes them after close().

🟡 tests · [follow-up]. The teardown's last-resort kill() is not src/kill-tree.js. Noted in Risks above.

🔵 performance · [follow-up]. The packaging job's cost per pull request. Noted in Risks above.

No findings in architecture (nothing under src/ changes), security (the file-dialog stub is installed at runtime from the test, so nothing test-shaped ships in the app), or the new-dependency rule (devDependency, no install script — verified against the published tarball — no native compilation, so no allowScripts entry is needed and the zero-prerequisite promise is untouched).

One caveat. The standard asks for this pass to run in fresh context rather than in the session that wrote the code. It did not: the session that wrote this reviewed it. Worth a second pass by someone who has not already decided it is correct.

Implementation notes

Two things surfaced only by running this against the current tree, neither of which #70 could have seen:

process.mainModule.filename is not stable in Electron. The module-resolution assertions anchored a createRequire there. Partway through a run it reads back as the bare string 'electron', so the first module checked failed and the same test passed when run alone — a packaging test going red for a reason that has nothing to do with packaging. Now anchored at join(app.getAppPath(), 'package.json'), which is deterministic and is inside the asar either way.

Two bridged keys cannot be found by reading src/preload.js. signInToGithub and cancelGithubSignIn are spread in from an immediately invoked function that closes over a listener, so they exist only once the file has run. Any attempt to derive the expected-keys list by grepping the object literal misses them — which is the same class of gap the assertion itself exists to close, one level up. There is a comment in the file saying so, because the next person to regenerate that list will otherwise reintroduce the same two-key error.

The react-hooks/rules-of-hooks override for e2e/ is not a suppression of a real problem: Playwright's fixture API is async ({ deps }, use) => {}, and the rule reads a bare use(...) as React's use hook outside a component. There is no React in that directory; the app under test is a separate process.

Nothing in the suite has ever launched the app. The Git modules are covered
against real repositories and the IPC handlers against a loaded main process,
but the flows a contributor actually performs — linking a ticket, applying a
patch, moving between branches — are verified only by a person following the
"How to test this" section of a pull request.

This is the plumbing for closing that gap, ahead of the git-native refactor in
#350: the behaviour that refactor moves has to be pinned before it moves, or the
suite that results only records the refactor.

Three problems had to be solved before any flow can be asserted on, and none of
them are about the flow:

- The app must never touch the contributor's own site registry. Every launch
  goes through the development-only data-directory redirect, and the harness
  reads back the path the app actually chose and refuses to continue if it is
  any other one.
- Teardown must always terminate. Closing is known to hang on Windows in apps
  that keep child processes alive, which this one does constantly.
- A failure has to leave evidence, and in this app the interesting half of a
  failure is on disk rather than on screen — so a failing journey attaches the
  screen and the state the app had persisted.

Two suites, because there are two different apps under test. The journeys drive
the source tree and write real state; the packaged smoke test launches an
unsigned artifact and asks only whether packaging worked. Both run on macOS and
Windows for every non-draft pull request. Neither downloads a browser.

The packaged smoke test is the one from the closed #70, rebuilt rather than
rebased. Two of its assertions had gone stale in ways that mattered: the list of
bridged keys it compares against had roughly half the entries the app exposes
today, and the native module it probes is no longer optional and no longer
absent on Windows, so its deliberate Windows exclusion had become a hole.
Running it also turned up that two bridged keys are spread in from an
immediately invoked function, so no static reading of the preload file can find
them — which is the same class of gap that assertion exists to close.

Part of #359. Closes #360.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@juanmaguitar juanmaguitar added the e2e-tests End-to-end tests: the engine, the journeys, and the CI that runs them label Aug 18, 2026
@juanmaguitar
juanmaguitar merged commit 87ab727 into trunk Aug 19, 2026
7 checks passed
@juanmaguitar
juanmaguitar deleted the juanmaguitar/e2e-tests-baseline branch August 19, 2026 12:58
juanmaguitar added a commit that referenced this pull request Aug 19, 2026
**Stacked on #362.** Review that one first; this diff is only the three
files under `e2e/` that this PR adds.

## Why

#362 built an engine and proved it works. This is the first thing it was
built for: the ticket-branch flow, driven through the app rather than
through a module.

That flow is what a contributor spends a Contributor Day inside — link a
ticket, edit a file, realise it belongs to another ticket, come back
later — and it is the flow #350 moves wholesale. Today it is verified by
a person following a **How to test this** section. After this, it is
verified on macOS and Windows on every pull request.

It is also the flow whose failures are silent. Work that is not restored
produces no error; the app shows a clean tree and the contributor
discovers the loss when a reviewer asks where the change went.

## What changes

Four journeys, and one addition to the engine.

- **Linking a ticket** creates its branch, checks it out, and leaves the
gitignored substrate alone.
- **Unlinking parks the work**, and the next ticket starts from trunk
with none of the first one's edits.
- **Switching back restores the work byte for byte** — the edit *and*
the deletion. An edit that returns while a deletion does not is a
half-restored tree, which is worse than an obvious failure.
- **Deleting a ticket's work** removes that branch and only that branch,
after asking.

**Every assertion is marked `INVARIANT` or `CHARACTERISATION`,** and
that is the point of the file rather than decoration. An invariant must
hold under any model of how work is stored; a characterisation is true
because of how the app stores things today. During #350, a red invariant
is a bug and a red characterisation is a prompt to read it, decide
whether the new model is what you meant, and update it on purpose.
Without the distinction every failure looks the same, and the suite
becomes noise at exactly the moment it is supposed to be useful.

**Two things the app does turned out not to match how the flow reads
from the outside.** The tests follow the app:

- Linking a second ticket means unlinking the first. Once a ticket is
linked, its card shows that ticket's pull requests and attachments and
the ticket-number field is not on screen at all. Unlinking is not
throwing the ticket away — it parks the work on its branch and returns
the checkout to trunk — but it is a step, and a test that skipped it
would exercise a path no contributor can take.
- A ticket cannot be deleted while it is the linked one, because the
list of a site's tickets deliberately leaves out the current one. So the
`wasActive` branch in the delete handler — the one that returns the
checkout to trunk and clears the link — appears to be unreachable from
the interface. Left alone here; noting it because it is the kind of
thing worth knowing before #350 rewrites around it.

**The engine grows one thing: answering `window.confirm`.** Electron
implements the JavaScript dialogs natively and blocks the renderer on
them, so Playwright's `dialog` event never arrives — a test relying on
it clicks "Delete this ticket's work" and watches nothing happen.
Replacing `window.confirm` in the page works, and counting the calls
lets a test assert that the app *asked* before it deleted anything.

**Deliberately not in this PR:** applying and reverting patches, and
what survives a restart. Those are the next two in the stack.

## How to test this

Platforms: **any**. Windows is covered by CI, which is where the
interesting half is.

**Starting state:** this branch, `npm ci` done, `npm run build:once` run
once.

1. `npm run test:e2e`
- Expected: 8 passed in under ten seconds — the four from #362 and the
four here. Electron windows open and close as it goes.
2. Watch one of them rather than trusting the count: `npx playwright
test --project=journeys -g "switching back" --headed`
- Expected: the app opens, a ticket is linked, another, and the panel
switches back. Nothing is typed by a human.
3. Break an invariant on purpose. In
`e2e/journeys/ticket-branches.spec.js`, change `expect( read( site.dir,
'wp-login.php' ) ).toBe( MY_EDIT )` to any other string, then rerun.
- Expected: "switching back to a ticket restores its work byte for byte"
goes red on that line. Put it back.
4. Break the substrate on purpose. In `e2e/helpers/git-site.cjs`, delete
the line that writes `SUBSTRATE_CONTENT`, then rerun.
- Expected: every ticket-branch journey goes red. Put it back. This is
the assertion that stands in for "a switch did not silently wipe
`node_modules`".
5. `npm test` and `npm run lint`.
   - Expected: 1027 passed, and clean.

**What must not have happened:**

- **Your real settings file must not have been touched** — the guard
from #362 covers this, and it is worth confirming once more now that the
journeys write far more state than the engine test did.
- **No leftover directories.** Each journey builds a real repository
under the system temp directory and removes it after the app stops.
After a run, nothing matching `wpct-e2e-*` should remain.
- **No Electron processes still running.**

## Risks and limitations

- **Linking a ticket reaches the network, twice.** The app starts a
GitHub lookup for the ticket's pull requests, and — because the ticket
was linked by hand rather than restored on mount — it also auto-reads
the ticket's own facts from Trac (#292). Nothing here asserts on either,
and no journey waits for them, so an offline or rate-limited runner does
not fail these tests. But both calls happen, and on a machine where
Trac's human-check appears a window may open. An earlier version of this
section said Trac is only read on request; that is true on mount and
re-activation, and not true of a hand-linked ticket, which is what every
journey here does.
- **Selectors read the visible copy.** Renaming "Link ticket" or "Delete
this ticket's work" breaks these tests. That is deliberate — it makes
the copy a contract — but it is a maintenance cost, and worth saying out
loud rather than discovering in a rename.
- **Timing is waited for, never slept through.** Every step waits on
something the app renders or on the state of the repository on disk.
There is no `waitForTimeout` in the journeys, because a sleep tuned on a
laptop is a flake on a Windows runner.
- The row for a ticket is addressed by the ticket it names, not by
position: the list is ordered by how recently each ticket was used, so
`.first()` picks a different branch depending on how far the render has
got. That cost a wrong-branch deletion during development, which is why
it is called out in a comment in the file.

## Related

Part of #359 and #361. Stacked on #362, which closes #360.

---

<details>
<summary>Design decisions and alternatives considered</summary>

**A real repository per test, built from scratch.** These are Git
operations; a stub would only prove the app can talk to a stub. The
fixture mirrors the one the integration suite already uses one level
down — same trunk branch, same three tracked files, same gitignored
`node_modules` — so the two levels describe the same site and a failure
at one level can be read against the other.

**Nothing is shared between tests.** Each builds its own repository and
its own profile, so no journey can be affected by the order it runs in.
It costs about a second per test, which is the right trade at this size.

**Assertions read the repository, not the app.** Whether a branch exists
is asked of `isomorphic-git` against the directory on disk, not of the
app that just claimed to have made it. An assertion that asks the app to
confirm its own work is the "mocking the thing under test" shape from
the review standard, one level up.

**Marking assertions rather than splitting the files.** An alternative
was two files per flow, invariants in one and characterisations in the
other. Rejected because the reason a characterisation exists is the
invariant next to it — reading "the applied patch is a record in the
store" is only useful beside "the contributor's edit comes back".
Splitting them would put the two halves of one argument in different
files.

</details>

<details>
<summary>Review outcome (required — see AGENTS.md)</summary>

`0 [fix here] · 1 [follow-up]`.

**🔵 architecture · `[follow-up]` — not this PR's to fix.** The
`wasActive` branch of `branches:delete` in `src/main.js` returns the
checkout to trunk and clears the link when the deleted ticket is the
current one. The panel excludes the current ticket from the list it
offers delete controls for, so that branch appears unreachable from the
interface. Not touched here; worth confirming before #350 rewrites
around it.

No findings in architecture (nothing under `src/` changes), security,
performance, or cross-platform. The two cross-platform traps in this
diff — path comparison and deleting directories the app still holds open
— were fixed in #362 and are reused here rather than re-solved.

**Tests:** every invariant in this PR was verified by mutating it and
confirming the test goes red. Seven mutations, seven reds:

| Mutated assertion | Result |
| --- | --- |
| the store records the linked ticket | red |
| the second ticket is the one checked out | red |
| the edit comes back on switch | red |
| the deletion comes back on switch | red |
| delete leaves the other ticket alone | red |
| delete asked for confirmation | red |
| the substrate survives a switch | red |

**Same caveat as #362:** this pass ran in the session that wrote the
code, not in fresh context as the standard asks for.

</details>

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
juanmaguitar added a commit that referenced this pull request Aug 19, 2026
**Stacked on #368.** Review the ones below it first; this diff is one
new journey file, plus the seed repository it needed.

## Why

The other half of a contributor's day, and the half #350 changes most.
Someone else's work arrives as a patch file or a pull request, goes onto
the checkout, and has to come off again without taking anything of
theirs with it.

Today "applied" is a layer the app holds. After #350 it is a commit. So
what these journeys pin is the part that has to survive either model —
and the part whose failure is silent, because a patch that half-applied
or a revert that ate an unrelated edit produces no error at all.

## What changes

Three journeys.

- **A patch applies.** The file changes on disk, the gitignored
substrate and the untouched files are left alone, and the app records
what it applied.
- **A revert puts the checkout back**, byte for byte — and the
contributor's own work in a file the patch never touched survives both
directions.
- **A patch that does not fit is refused.** This is the one worth
having. The app warns *before* writing that the patch is about to land
on the contributor's own edits, while they can still stop; then it
refuses, says the checkout was not changed, names the file, and says how
much of the patch failed. Nothing is written and nothing is offered to
undo.

**Two things about the app the fixture had wrong**, both found by
running it rather than by reading:

- **Patches are rewritten onto core's modern layout.** A patch naming
`wp-login.php`, which is how Trac patches are written, is applied to
`src/wp-login.php` — see `src/patch-plan.cjs`, which exists because core
moved everything under `src/` and old patches predate it. A fixture with
its files at the repository root sends every patch somewhere the test
never looks, and the test then fails for a reason that is not a bug. The
seed repository now mirrors a real checkout.
- **Applying ends by rebuilding the site.** Without a `build` script the
chain fails *after* the patch is already on disk, so the journey would
be asserting on a half-finished flow. The seed repository has a
`package.json` whose build does nothing and exits.

**One assertion is deliberately narrow.** The conflict is asserted
against the alert, not the page. When an apply fails the preview stays
on screen and already names the file, so `getByText('src/wp-login.php')`
passes whether or not the app reported anything — green, and proving
nothing.

## How to test this

Platforms: **any**. Windows is covered by CI.

**Starting state:** this branch, `npm ci` and `npm run build:once` done.

1. `npm run test:e2e`
- Expected: **11 passed** — four from #362, four from #363, three here.
2. Watch the interesting one:
   ```
E2E_VIDEO=/tmp/e2e-video npx playwright test --project=journeys -g "does
not fit"
open
/tmp/e2e-video/a-patch-that-does-not-fit-is-refused-and-writes-nothing.webm
   ```
- Expected: the app opens, a ticket is linked, a patch file is chosen,
the preview lists `src/wp-login.php`, a warning appears about your own
edits, **Apply and rebuild** is clicked, and the refusal appears.
Nothing in the file changes.
3. Break the all-or-nothing invariant. In
`e2e/journeys/patch-apply.spec.js`, change `expect( read( site.dir,
LOGIN ) ).toBe( mine )` to any other string, then rerun.
- Expected: "a patch that does not fit is refused, and writes nothing"
goes red on that line. Put it back.
4. Break the path rewrite. In `e2e/helpers/git-site.cjs`, change `LOGIN`
from `src/wp-login.php` to `wp-login.php`, then rerun.
- Expected: the patch journeys go red — the patch is applied where the
fixture is not looking. This is the trap the fixture layout exists to
avoid. Put it back.
5. `npm test` and `npm run lint`.
   - Expected: 1027 passed, and clean.

**What must not have happened:**

- **The ticket-branch journeys must still pass.** This PR moves their
fixture's files under `src/`, so all four are re-exercised by step 1. If
only the patch journeys were run, that move would go unchecked.
- **No leftover directories.** Nothing matching `wpct-e2e-*` should
remain in the system temp directory after a run.
- **Your real settings file must not have been touched.**

## Risks and limitations

- **Linking a ticket reads Trac.** Correcting something I wrote on #363:
linking a ticket *by hand* auto-reads the ticket's own facts (#292), so
these journeys do reach the network at that point, and on a machine
where Trac's human-check appears a window may open. Nothing here asserts
on it and no journey waits for it, so an offline or blocked runner does
not fail these tests — but it happens, and #363 said it did not.
- **The patches are hand-written unified diffs**, one whole-line
replacement per file. That is deliberate — generating them with the same
code the app reads back would prove less than it looks — but it means
the shapes core actually produces (renames, binary files, empty
additions) are not exercised here. Those are covered a layer down, in
`test/patch-apply.integration.test.cjs`.
- **The rebuild is a no-op.** The seed repository's `build` script exits
immediately, so what these journeys prove about the rebuild is that the
chain reaches it and survives it, not that a real build works.
- **Applying a pull request is not covered**, only a patch file.
Fetching a PR puts GitHub in the path of a test about the checkout. The
app treats both identically once the patch text is in hand, which is the
point where these journeys join the flow.

## Related

Part of #359 and #361. Stacked on #368.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

e2e-tests End-to-end tests: the engine, the journeys, and the CI that runs them

Projects

None yet

Development

Successfully merging this pull request may close these issues.

e2e: an engine that drives the app against state it throws away e2e: packaged-app smoke test on macOS and Windows

1 participant