Skip to content

feat(evals): core journeys green on real infra, environment healing behind its components, timeline observability - #3359

Merged
benjaminshafii merged 54 commits into
devfrom
fix/app-spec-den-bootstrap
Jul 31, 2026
Merged

feat(evals): core journeys green on real infra, environment healing behind its components, timeline observability#3359
benjaminshafii merged 54 commits into
devfrom
fix/app-spec-den-bootstrap

Conversation

@benjaminshafii

@benjaminshafii benjaminshafii commented Jul 30, 2026

Copy link
Copy Markdown
Member

What

Makes the migrated eval specs actually pass against a real app, and puts each environment concern behind the component that owns it. Three core user journeys are now green on real infrastructure, and a fourth (cloud sharing) is most of the way there.

Core journeys proven on a fresh Daytona sandbox

Spec Proves Time
first-run-local fresh profile → Use Without Cloud → workspace → model → real task → assistant reply (the agent created welcome-checklist.md); 7 frames, 15/15 vision expectations 112s
app-den-tls-fault a desktop pointed at a TLS-intercepted Den never claims it is connected, and the shipped diagnostics name the interception 21–32s
app-smoke boot → workspace → control route → meaningful content 58s
egress-tls12-only / egress-selective-deny TLS 1.3 stall and allowlist deny named from transport facts + product diagnostics 12s

Complexity behind the component that owns it

Was Now Why there
reattachSurface in behaviour retry loops @openwork/cdpevaluateOnSurface() a replaced page target is a CDP fact
xdpyinfo display check in two shell scripts @openwork/hostsspawnElectron() "make an app appear" owns its preconditions
pkill of stale Electrons in shell @openwork/hosts, scoped to its own surfaces root only the host knows its own processes

resolveHost() also no longer picks the Daytona host while running inside a sandbox (the CLI indirection cannot work there) — that bug was why nothing spawned and readiness ran against an empty page. scripts/evals/daytona-spec.sh is back to 36 lines.

It validated itself: after a sandbox restart the X server was dead and all three app specs still passed, because the host noticed and started the display.

Observability

@openwork/timeline records the composable functions every spec funnels through (app.readiness, validate with cache hit/miss, seed.workspace, cloud calls), so results show where time goes without specs mentioning timing. Vitest only reports per-file/per-test duration, which says nothing about which beat was slow in a 112-second journey.

Correctness fixes that came from real failures

  • Wait on product state, not route shapes — workspace and session ids come from openwork.react.activeWorkspace / session.list_sessions; several waits were asserting routes the app never produces.
  • Readiness = interactive UI, covering the surfaces that exist: welcome, task, no-workspace, settings/extensions. Background copy like "Preparing workspace" no longer makes the welcome screen count as not-ready.
  • Short per-probe timeouts with retry rather than one long call.
  • Resolve the workspace id after onboarding adopts it, not before.
  • Wait for output to stabilise before capturing — the agent was mid tool-call when frames were taken.
  • Two regexes that never compiled (\/ collapses in template literals) and a skill-name parser that returned the whole row.
  • Disposal is best-effort and logged, so SuppressedError stops masking real assertion failures.

Honest visual expectations

Vision caught three defects DOM checks missed, and each fix corrected the spec, not the guard: a false claim (duplicate pixels behind a picker that was never opened), a premature assertion ("only 'Preparing workspace' is visible"), and a DOM/pixel divergence where the control API reported a task control enabled while the button was visibly gray.

New: first run → browser sign-in → share a skill (not green yet)

first-run-cloud-share.slow.test.ts plus captureOpenedUrls() (PATH shim over xdg-open, so the browser handoff is observed rather than assumed), signInInBrowser(), readHandoffDeepLink(), and cloud-plugins.ts against the real Den contract — createPluginWithSkill uses components + marketplaceId, and readResolvedMarketplace reads as the colleague.

Proven: the app really hands off to a browser (…/?mode=sign-up&desktopAuth=1&desktopScheme=openwork), Den comes up, Chrome loads the auth form. Blocked on Den web's sign-in shape (email-then-password rather than one form). Only the OS protocol dispatch of openwork://den-auth?grant=… is bridged — a container registers no handler — and the code says so.

Tests run

pnpm evals:typecheck clean · pnpm --dir evals run test 95/95 · pnpm --dir evals run spec 4/4 (the gating pr lane) · full nightly run on a real sandbox went 4 passed / 7 failed → 7 passed / 4 failed.

Still red: skills-local, models-available, org-connection-lifecycle, first-run-cloud-share. Nothing gates on them — the pr lane gates, app specs are nightly-only, opt-in via OPENWORK_EVAL_APP_SPECS=1, and that step is continue-on-error while they stabilise.

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openwork-app Ready Ready Preview Jul 31, 2026 11:22am
openwork-den Ready Ready Preview Jul 31, 2026 11:22am
openwork-den-worker-proxy Ready Ready Preview Jul 31, 2026 11:22am
openwork-diagnostics Ready Ready Preview Jul 31, 2026 11:22am
openwork-landing Ready Ready Preview, v0 Jul 31, 2026 11:22am

@benjaminshafii

Copy link
Copy Markdown
Member Author

Core flows now proven on real infrastructure

Five spec files pass against a fresh Daytona sandbox (each app spec spawns its own isolated Electron):

Spec Proves Time
first-run-local fresh profile → Use Without Cloud → workspace → model picker → real task → assistant reply, 7 vision-validated frames, 15/15 expectations 112s
app-den-tls-fault desktop pointed at a TLS-intercepted Den never claims it is connected, and the shipped diagnostics name the interception 21s
app-smoke boot → workspace → control route → meaningful content 58s
egress-tls12-only TLS 1.3 stall named from transport facts + product diagnostics + Bun pinning facts 11s
egress-selective-deny allowlist deny named <1s

The first-run journey's final frame — the agent actually did the work:

The fixes that got them green

Each came from a real failure, not a guess:

  • Re-attach to the app's current CDP target. The desktop replaces its page target during transitions; evaluations against the old one hang rather than fail, which looked exactly like a blocked renderer for several rounds. (The legacy runner had the same escape hatch as ctx.reconnect().)
  • Short per-probe timeouts with retry, not one long call — I had briefly made polls take their whole budget in a single evaluation, which turned a transient block into a total failure.
  • Readiness = interactive UI, extended to the surfaces that really exist: welcome, task, no-workspace (/session offering "Create or connect a workspace"), and settings/extensions. Background copy like "Preparing workspace" no longer makes the welcome screen count as not-ready.
  • Stop assuming ids appear in routes. Workspace and session ids now come from the product's own state (openwork.react.activeWorkspace, session.list_sessions); two waits were asserting route shapes the app never produces.
  • Resolve the workspace id after onboarding adopts it, not before.
  • Wait for output to stabilise before capturing: the agent was still running tool calls ("Read 1 file", "Edited 1 file") when the frame was taken.
  • Two never-compiling regexes inside template literals (\/ collapses, so the emitted regex terminated early) and a skill-name parser that returned the entire row.

Environment fixes worth knowing

Isolated Electron could not run in a container at all until: ELECTRON_DISABLE_SANDBOX=1 (appendSwitch runs too late for the SUID check), DISPLAY=:99 (exec sessions do not export it), a liveness check on the X server (a stale /tmp/.X11-unix socket is not proof it is running — several "renderer blocked" failures were really Missing X server or $DISPLAY), stale-process cleanup between runs, and pnpm kept non-interactive.

Still red

skills-local (a session created in a seeded workspace never appears in session.list_sessions — likely because that workspace has no usable model), models-available and org-connection-lifecycle (the latter needs the den stack, which this fresh sandbox does not run). Nothing gates on them: the pr lane is green and gating; app specs are nightly-only, opt-in, continue-on-error.

…own it

Target re-attach now lives in @openwork/cdp (evaluateOnSurface), display liveness
and stale-surface cleanup in @openwork/hosts (spawnElectron), and resolveHost no
longer picks the Daytona host when the caller is already inside a sandbox. The
sandbox runner scripts drop the logic they were carrying.
@benjaminshafii

Copy link
Copy Markdown
Member Author

Update: environment healing moved behind the components that own it

Three things that had been fixed in shell scripts and behaviour retry loops are now the responsibility of the component whose job they actually are:

Was Now Why there
reattachSurface called from retry loops in @openwork/behaviors @openwork/cdpevaluateOnSurface() a replaced page target is a CDP fact; callers should not carry re-attach bookkeeping
xdpyinfo display check duplicated in two runner scripts @openwork/hostsspawnElectron() "make an app appear" owns its own preconditions
pkill of stale Electron processes in shell @openwork/hosts, scoped to its own surfaces root only the host knows which processes belong to it

Plus a real bug this surfaced: resolveHost() chose the Daytona host while running inside a sandbox, where driving the daytona CLI cannot work — so nothing spawned and readiness ran against an empty page. It now spawns locally when runningInsideSandbox(), with a unit test.

scripts/evals/daytona-spec.sh is back to 36 lines and mentions DISPLAY once, as a default. rg reattach evals/packages/behaviors is empty.

The refactor validated itself: after a sandbox restart the X server was dead, and all three app specs still passed — the host noticed and started the display, with no script involved.

Effect on the suite

Full nightly run went from 4 passed / 7 failed → 7 passed / 4 failed. Still green after the move: first-run-local, app-den-tls-fault, app-smoke, plus both egress specs.

New core journey: first run → browser sign-in → share a skill via a marketplace

Added first-run-cloud-share.slow.test.ts with the behaviours it needs:

  • captureOpenedUrls() — a PATH shim over xdg-open (what shell.openExternal calls on Linux) records the URL the app asks the OS to open. The handoff is observed, not assumed, and no product code changes.
  • signInInBrowser() / readHandoffDeepLink() — drive the real Den page in a real spawned Chrome, then read the openwork://den-auth?grant=… link Den hands back.
  • cloud-plugins.ts — real Den contract (plugin-system/contracts.ts): createPluginWithSkill uses the schema's components + marketplaceId so authoring-and-sharing is one call, and readResolvedMarketplace reads as the colleague, so the assertion is that they genuinely see the shared skill.
  • Single-spec runs can now ensureDenStack idempotently when a spec needs cloud.

Proven by running it: the app really does hand off to a browser — the shim captured http://localhost:3005/?mode=sign-up&desktopAuth=1&desktopScheme=openwork. That corrected the design: Den mints the grant after browser sign-in rather than embedding it in the opened URL. Den comes up, Chrome loads the auth form.

Not green yet: Timed out after 60000ms waiting for den password field — a first-run user is sent to sign-up mode, and Den web's sign-in looks like email-then-password rather than one form. One look at the real page settles it; I have not guessed further.

Only the OS protocol dispatch of the deep link is bridged (a container registers no handler); the grant is the real one Den issued for that browser session, and that limitation is stated in the code.

…path

seedWorkspace arranged a workspace over the local server API and left the
engine unconfigured and the model catalog empty — a state the product's own
onboarding never produces, so specs failed on arrangement instead of their
subject. Every spec now goes through createAndSelectWorkspace, the path the
passing journeys already proved.

Also read the created session id from the field session.list_sessions really
returns: items carry sessionId, not id (probed live; the old extraction never
matched, so the wait timed out even with sessions present).
…in code input

Probed live on a running den: auth is an email-only step (Next) that routes
to a password step (Sign in) — there is no single form and no sign-up/sign-in
toggle. After sign-in the deep link is not an anchor or page text; it is a
readonly input holding the openwork://den-auth?grant=… sign-in code next to
Open OpenWork, so read it from there.
…on profiles

Each surface's fresh HOME hides the host's pnpm tool cache, so the pinned
pnpm (packageManager) is re-downloaded from the network on every spawn
(21MB). On the eval sandbox one failed download degenerated into a
self-sustaining recursive 'pnpm add pnpm' cascade that outlived the run and
starved later spawns. Point PNPM_HOME at the host's real pnpm home; the
redirect is then local and instant (verified: fresh HOME resolves in 0.75s
with no downloads).
Exporting OPENWORK_EVAL_DEN_API_URL unconditionally made den-gated specs run
against nothing and die in 3ms with 'fetch failed' instead of skipping with
their own reason. Probe /health first; with OPENWORK_SPEC_NEEDS_DEN=1 the
stack is still ensured before the probe.
…shot

The skills menu is a scrollable list: /browser-automation was loaded (the
DOM assertions passed) while the screenshot showed only the rows above it,
so the visual validator honestly reported it absent.
src-opn
src-opn previously approved these changes Jul 31, 2026
The workspace engine boot blocks the renderer JS thread in bursts. Bare
20s evaluations (plug-menu clicks) died with raw CDP timeouts, and the
measure evaluations gave their outer CDP call the same 20s deadline as the
in-page poll, so the two raced under load. Fold the menu clicks into a
guarded, retried wait and give the measures headroom over their inner
deadlines.
…enshot

The capability menu is a scrollable popover: all four sections were in the
DOM (the code assertion passed) while Agents sat above the fold, so the
validator honestly reported it invisible. Scroll the named row into view and
let the code assertion carry section completeness.
… see

The vision loop caught a real product defect: with a long command list the
capability popover extends under the window header, so its first section row
(Agents) is covered and unclickable (repo workspace, popover top at y=17
under the title bar). Keep four-section completeness as the DOM assertion,
scope the visual claim to the visible sections, and record the defect where
the claim is made.
src-opn
src-opn previously approved these changes Jul 31, 2026
…u with retried waits

The scenario opened the plug menu inside its own evaluate with fixed
100/300ms delays right after a reload; under load those clicks land before
React handlers attach and the whole block times out. Arm the fetch delay and
the connect witness in one small mutation, open the menu with the guarded
retried helper, then measure.
…cloud skills

The scenario set window.__OPENWORK_GATEWAY__, which flips the app into the
hosted gateway runtime and rewires the local openwork-server client to the
page origin — so client.listSkills hit the Vite dev server and local skills
never rendered, a state no desktop user can reach. The den auth token and
active org id it also set are the real cloud-connected desktop arrangement,
so keep only those. Drop the unused resetSkillsCloudState.
…t be faithful

On desktop, den traffic goes through the main process, so the renderer
fetch hook never delayed anything (denRequestCount stayed 0), and the
gateway marker it used instead rewired the local server to the page origin.
The block therefore only ever measured an impossible state. Cloud latency
belongs in a den-boundary fault spec like app-den-tls-fault, where the
fault is injected for real.
…pace before sign-in

signInInBrowser returned right after clicking Sign in, so the next frame
honestly showed 'Working...'. Wait for the signed-in outcome (or the
sign-in code) before returning.

The signed-in org shell offers no Add workspace entry, and the
workspace.create control there reports ok while creating nothing (its error
lands in closed-modal state) — probed live twice. Arrange org specs the way
a member actually gets there: create the workspace on the proven welcome
path first, then sign in; organization onboarding is completed when it
appears and the existing workspace is reselected either way.
POST /v1/marketplaces/:id/access rejects a grant without role
(viewer|editor|manager). Viewer is what sharing with a colleague means.
…nents

The resolved marketplace payload carries plugins with component counts only;
skill names live on GET /v1/plugins/:id/resolved as items[].configObject
(objectType skill, title). Read them as the member so visibility stays the
colleague's, not the admin's.
… managed-recovery defect

The picker can paint 'No models' before the engine's catalog lands (observed
live: 0 models on first read, 7 shortly after on the same boot), so the
primary test polls up to 90s before asserting.

The managed test now pins today's truth: after an admin publishes a
provider, Retry does not deliver it to the composer even though the API
already entitles the member (probed live, 201 + readable as the member, empty
notice survives 90s+). The spec asserts the notice stays honest and the
composer stays usable; when live recovery ships, the pinned wait fails
loudly and the recovery assertions come back from history.
… a model)

With no selectable model the composer renders no contenteditable editor, so
the pinned block asserts what is really there: the persistent notice with
Retry and no crash.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants