Skip to content

feat(evals): make surface placement explicit, and own browser disposal - #3387

Merged
benjaminshafii merged 56 commits into
devfrom
feat/eval-surface-placement
Jul 31, 2026
Merged

feat(evals): make surface placement explicit, and own browser disposal#3387
benjaminshafii merged 56 commits into
devfrom
feat/eval-surface-placement

Conversation

@benjaminshafii

Copy link
Copy Markdown
Member

Why

resolveHost() reads one ambient env var (OPENWORK_EVAL_DAYTONA_SANDBOX), so a whole process got one host: everything local, or everything in one sandbox. desktop() called it internally and DesktopOptions had no host field, so a spec could not express "app here, browser there", or two desktops in two sandboxes. Topology was inherited from the environment rather than declared — the same shape of problem as env-var-gated skips.

What changed

  • desktop({ host }) and a new chrome({ host }) accept a placement.
  • localHost() / daytonaSandbox(id) name one, keeping REPO_ROOT wiring inside the package. daytonaSandbox() throws from inside a sandbox, where the CLI indirection has nothing to target.
  • resolveHost() default behaviour is unchanged, so every existing spec is untouched.
await using sandboxA = daytonaSandbox("openwork-test-a");
await using app     = await desktop({ host: sandboxA });
await using browser = await chrome({ host: daytonaSandbox("openwork-test-b") });

Bug fixed along the way

attachSurface only closes the CDP socket — it never kills the process. first-run-cloud-share hand-rolled spawnChrome + attachSurface, so host.disposeSurface was never reached and a Chrome process survived every run. chrome() owns disposal: stop the client, then dispose the surface.

Proof (Daytona sandbox, real den)

Spec Result Exercises
first-run-cloud-share PASS x3 — 47.0s / 35.7s / 41.0s the new chrome() seam
app-smoke PASS x2 — 51.8s / 51.7s desktop() default path, unchanged

Process check before and after all runs, using ps -eo comm= (self-match-proof, unlike pgrep -f):

BASELINE:          none
AFTER-ALL-RUNS:    none

tsc -p evals clean; pnpm --dir evals test 95 pass / 0 fail.

Limits, stated

  • A split topology is not proven end-to-end. It typechecks and the seam is exercised, but putting the app and browser in different sandboxes needs two sandboxes and a laptop-side vitest run (OPENWORK_EVAL_DAYTONA_SANDBOX mode). Not done here.
  • No capability model yet. Host.kind is still a bare string; requires([...]) and honest skip reasons are the follow-up this unblocks.
  • No fraimz.html: the spec lane writes evals/results/rolls/<stamp>-<name>/index.html. Frames for these runs are validated there. Worth reconciling with AGENTS.md separately.

Unrelated finding

org-connection-lifecycle fails on this sandbox: DOM waits pass (NEEDS YOUR SIGN-IN, OAuth required present in body.innerText) but the vision check reports the Extensions panel blank. Not touched by this PR and not in #3359's green list either, so it may never have passed. Needs its own investigation — either a reveal/scroll issue before the frame, or a real rendering defect.

…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.
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.
…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.
resolveHost() reads one ambient env var, so a whole process got one host:
everything local, or everything in one sandbox. A spec could not say 'app
here, browser there' or 'two desktops in two sandboxes' — the topology was
inherited from the environment instead of declared.

- desktop({ host }) and the new chrome({ host }) take a placement.
- localHost() / daytonaSandbox(id) name one, with REPO_ROOT wiring kept
  inside the package. daytonaSandbox() fails loudly from inside a sandbox,
  where the CLI indirection has nothing to target.
- resolveHost() default behaviour is unchanged, so existing specs are
  untouched.

chrome() also fixes a real leak: attachSurface only closes the CDP socket, so
the hand-rolled spawnChrome + attachSurface in first-run-cloud-share left a
browser process running after every run. Disposing now stops the client and
then disposes the surface, which is what kills it.
@vercel

vercel Bot commented Jul 31, 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 1:40pm
openwork-den Ready Ready Preview Jul 31, 2026 1:40pm
openwork-den-worker-proxy Ready Ready Preview Jul 31, 2026 1:40pm
openwork-diagnostics Ready Ready Preview Jul 31, 2026 1:40pm
openwork-landing Ready Ready Preview, v0 Jul 31, 2026 1:40pm

@src-opn
src-opn self-requested a review July 31, 2026 13:30
src-opn
src-opn previously approved these changes Jul 31, 2026
@benjaminshafii
benjaminshafii enabled auto-merge (squash) July 31, 2026 13:36
…ement

# Conflicts:
#	evals/specs/first-run-cloud-share.slow.test.ts
@benjaminshafii

Copy link
Copy Markdown
Member Author

Conflict resolved + re-verified after merging dev

The conflict was squash-merge divergence: this branch carried all 50 commits from fix/app-spec-den-bootstrap while dev has them as one squash (915c43d18). Only c830ab530 is actually new here.

Merged origin/dev in (d6431becc). Exactly one file conflicted — first-run-cloud-share.slow.test.ts, add/add, because the merge base predates the file. The two differing hunks were precisely this PR's intentional change vs dev's pre-change version, so taking this branch's side loses nothing.

Re-verified on the merged tree, which pulls in dev's app changes that plausibly touch these paths (Revert "wait for signed-in Den startup before Run Task" #3371, gateway checkpoint ladder #3376, provider logos in the connect flow #3375), plus a dependabot lockfile bump requiring a fresh pnpm install:

Spec Result Exercises
app-smoke PASS — 52.3s desktop() default host path
first-run-cloud-share PASS — 41.9s the new chrome() seam

Process check after both runs (ps -eo comm=): none. tsc -p evals clean, pnpm --dir evals test 95 pass / 0 fail on the merged tree.

@src-opn
src-opn self-requested a review July 31, 2026 13:45
@benjaminshafii
benjaminshafii merged commit 0e84c10 into dev Jul 31, 2026
15 checks passed
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.

2 participants