Repository navigation
Launch skill: attach the repo's automation library to a running instance - #332487
Joaquín Ruales (jruales) wants to merge 62 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for attaching VS Code’s smoke-test automation page objects to an existing Code OSS instance over CDP.
Changes:
- Adds the
attach()automation helper. - Documents automation-library usage and raw Playwright interoperability.
- Updates the launch skill to recommend repository page objects.
Show a summary per file
| File | Description |
|---|---|
.agents/skills/launch/SKILL.md |
Introduces the automation-library workflow. |
.agents/skills/launch/scripts/attach.ts |
Implements CDP attachment and session creation. |
.agents/skills/launch/automation-library.md |
Documents setup, APIs, and usage guidance. |
Review details
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Balanced
|
Copilot review |
There was a problem hiding this comment.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
.agents/skills/launch/scripts/attach.ts:57
- These aliases make every object returned by
attach()anany, so callers lose type checking and completion for the exact page-object API this wrapper is meant to expose. Runtimerequiredoes not prevent using type-onlyimport(...)aliases: Node erases them while the editor retains thePage,Browser,BrowserContext,Code, andWorkbenchcontracts.
type PlaywrightPage = any;
type PlaywrightBrowser = any;
type PlaywrightContext = any;
type AutomationCode = any;
type AutomationWorkbench = any;
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
|
Copilot review |
Re-reviewed |
Agent user-study resultsSeven subagent runs (models:
Changes driven by the studies
Note on CIThe |
f6dce94 to
d6fcd48
Compare
|
Update on the Root cause. #331833 ( That explains why the failure looked arbitrary rather than universal: PRs landing on a fresh cache pass, PRs landing on a stale cache fail. Rebasing onto latest #332489 fixes exactly this by calling Evidence this PR is not implicated:
No action needed here; this should go green once #332489 merges and the cache is rebuilt. |
b9328af to
8eb7c26
Compare
There was a problem hiding this comment.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
.agents/skills/launch/SKILL.md:158
- The PR description says
SKILL.mdis 85 lines shorter, but these hunks make it 94 lines longer (+46 lines in the UI-driving hunk and +48 in cleanup). Please update that scope/size claim so it matches the submitted diff.
## Drive the UI
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
…d tests
Three findings, all reproduced first:
editor.* settings are LANGUAGE_OVERRIDABLE, so normalizing only the root
key does not guarantee the effective value. Verified live: a profile with
"[typescript]": { "editor.editContext": false } rendered a TypeScript
editor as {nec:0, ta:1} - a textarea - while the page objects wait on
.native-edit-context. The scanner now reports occurrences at any depth and
rewrites nested overrides too; the same profile now renders {nec:1, ta:0}.
Neither launcher passed --sync=off. Sync enablement lives in
StorageScope.APPLICATION, which the clone preserves, so a source profile
with sync on could upload these automation-only values to the user's real
settings. Both launchers now force it off.
The JSONC rewriter had no checked-in coverage. Added a node --test suite
of 30 valid and 5 malformed fixtures covering comments, duplicate and
effective keys, nested overrides, escaped strings, exponents, trailing
commas and malformed roots. Each result is re-parsed to assert both keys
resolve to true and that no override at any depth contradicts them;
malformed input must exit non-zero and leave the file byte-identical.
Mutation-checked: disabling nested rewriting fails 6 of the 35.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…follow symlinks
Four findings, each reproduced first:
R20's nested rewrite was too broad. VS Code only treats direct children of
a top-level [language] key as overrides (OVERRIDE_PROPERTY_REGEX in
ConfigurationModelParser.toOverrides), so "some.extension": { ... } is
ordinary data owned by another consumer - and it was being rewritten.
Nested rewrites are now limited to direct properties of a top-level
override block; verified that [typescript] is normalized while
some.extension is left untouched.
The structural check ran only when a key had to be appended, so a
malformed file that already contained both keys took the rewrite path
twice and was written back out. It now runs once before any rewrite.
findRootObject counted depth without tracking delimiter types, so
parsed as a balanced root object and was rewritten. It now matches {} and
[] and rejects a mismatch.
rsync -a preserves symlinks, so a settings.json linked to a dotfiles
checkout was written through to the user's real file - reproduced, the
real file was edited. The link is now materialized as a regular file
first, keeping the throwaway profile actually throwaway.
Tests grow to 43 (from 35) covering all four, and each new guard is
mutation-checked: removing any one of them fails at least one test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…overrides Six findings, each reproduced first: Forwarded args were appended after --sync=off, and VS Code keeps the last value for string options, so `-- --sync=on` defeated the guarantee. Both launchers now append the enforced value last. Override keys were compared in their raw source spelling. VS Code parses "\u005btypescript\u005d" as [typescript], so that override was not recognized and stayed false. Keys are now decoded before comparison. findRootObject took the first opening brace without checking the prefix, so leading junk was rewritten and reported as success. Everything before the root object (BOM aside) must now be trivia. A dangling symlink took the ENOENT path with the link still in place, so the write created its target outside the throwaway profile. A dangling link is now replaced too. editor.editContext is also valid at workspace and folder scope, and those models merge after user configuration, so the profile rewrite cannot guarantee the effective value. Reproduced: a workspace setting it to false made a page object time out after 20s on .native-edit-context. attach() now detects the textarea/native-edit-context mismatch during preflight and explains where to look, and the healthy path is unaffected. The 'missing file' fixture wrote an empty file, so the ENOENT branch was never exercised. The helper now distinguishes an absent file from an empty one, and both are covered. Tests grow to 49; removing any new guard fails at least one of them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… check Three findings, both reproduced before changing anything. The launchers normalized only the default profile. The clone preserves userDataProfiles and profileAssociations in application state, and resolveProfileForBrowserWindow hands an associated workspace its named profile, which reads User/profiles/<id>/settings.json. Reproduced by seeding a profile associated with the launched workspace: the cloned settings kept editor.editContext false and all three editors rendered a textarea. Both launchers now normalize every existing profile settings file. Files that do not exist are left absent on purpose - a profile inheriting settings via useDefaultFlags points back at the default resource, so creating one would invent an override. The preflight treated any textarea-backed editor as evidence of a disabled setting, but inlineEditsSideBySideView and longDistancePreviewEditor pass editContext: false themselves for speed, so a visible inline-edit preview would have failed a healthy window. Those opt-outs are always a subset while a disabling setting applies to every editor, so the check now requires all of them to be textarea-backed. The fixed run above shows why this matters: one editor of three used native-edit-context and attach() correctly succeeded. Tests grow to 51; dropping the profile discovery fails the new one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…restore
Two findings, both reproduced first.
Validation checked delimiter balance but not syntax, so balanced-yet-invalid
input passed. Reproduced: `{ "a": 1 "b": 2 }` was rewritten and reported
success, exactly the fail-closed contract this script claims to keep. The
file is now parsed the way VS Code parses it (strip comments, then tolerate
trailing commas, per src/vs/base/common/jsonc.ts) before anything is written,
with fixtures for a missing comma, a missing colon, an invalid escape and an
unquoted key. A leading BOM is still accepted, since JSON.parse rejects it
but VS Code does not.
The editContext preflight ran right after the smoke-test driver appeared,
which is before the workbench restores, so it usually inspected zero editors
and proved nothing. It now runs after whenWorkbenchRestored().
That move exposed the check itself being wrong: requiring every editor to be
textarea-backed never fired, because a restored window had four .monaco-editor
elements of which only two own an input. The signal that actually separates
the two cases is whether anything still uses EditContext - the editors that
opt out for speed are always a subset, while a disabling setting leaves none.
Verified live both ways: a workspace setting it to false now fails with the
actionable message, and the same workspace without it attaches with two of
four editors on native-edit-context.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… preservation Six findings. Profile discovery was written twice in shell and a third time inside the test, so the test could stay green while either launcher regressed - and it needed bash, which a Windows user running the documented `node --test` command does not have. Discovery now lives in the shared script behind `--user-data-dir`, both launchers call that, and the test drives the same entry point. The symlink guard only inspected the final settings.json component, but `rsync -a` preserves linked directories too: a symlinked `User` or `User/profiles/<id>` arrived here as an ordinary file path that still resolved outside the throwaway profile. Reproduced, and writing there edited the real file. Linked ancestors are now rejected. The walk is bounded by the profile root because on macOS /tmp is itself a symlink, which an unbounded walk would reject on every run. The fixture loop asserted only the two injected keys, so an implementation that discarded the file and wrote just those keys would have passed - the one thing a data-preserving merge must never do. Fixtures now also assert that every unrelated value and every real comment survives. The two symlink tests are skipped on Windows, where creating file symlinks needs elevation, matching what the repository's watcher tests do. `logsPath` defaulted to a POSIX path even though attach() is documented for launch.ps1; it now derives from the platform temp dir. Tests grow to 55; the new directory guard and the discovery are both mutation-checked. Verified live: launch plus a chat round-trip still works. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…iscovery Three findings. The cleanup guard in SKILL.md checked RUN_DIR with a shell glob, but a glob matches `/` too, so `/tmp/code-oss-dev-x/../../etc` passed the pattern and would have reached `rm -rf`. The path is canonicalized first and only a resolved `code-oss-dev-*` basename is accepted; the traversal above now resolves to /private/etc and is rejected. The editContext preflight required *no* editor in the window to use EditContext, which only caught a window-wide disable. The setting is LANGUAGE_OVERRIDABLE and valid per folder, so a `[typescript]` override can leave one file's editor textarea-backed while Chat stays native-backed - and that file's page object is exactly the one that hangs. The known opt-outs (the inline-edit previews, which pass editContext: false for speed) all live under an inline-edits view, so they are excluded by ancestor and any other textarea-backed editor is reported. Verified live both ways: the folder override now fails with the actionable message, and the same workspace without it attaches. Profile discovery used existsSync, which follows links, so a *dangling* settings symlink looked absent and was skipped as an inheriting profile - leaving the link in place for VS Code to write through later. Reproduced: one file discovered instead of two, link untouched. lstatSync is used instead, and the materialization already in place handles the rest. SKILL.md said attach() detects workspace overrides up front, which overstates a check that can only inspect editors the window actually restored. It now says what the check does and what to look at if a helper still times out. Tests grow to 56; the new discovery case is mutation-checked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eanup Four findings. A bare carriage return ends a line comment for VS Code's scanner, but the comment mask only reset on a newline, so a valid CR-only settings.json had everything after its first `//` blanked and was rejected as malformed. Reproduced, fixed, and covered by a fixture. The test's own JSONC parser had the same bug. The symlink materialization treated every read failure as a dangling link, so an unreadable target (EACCES, EIO, a directory) meant the link was unlinked, replaced with an empty file, and rewritten with only the automation keys - silently discarding that profile's settings. Only ENOENT proves a link is dangling; anything else now propagates to the fail-closed path. Verified: a directory target aborts with exit 1 and is left alone. The preservation assertion stripped the automation keys recursively from both sides, which also hid corruption of a *deeply* nested occurrence - the very thing it was added to catch. Stripping is now limited to direct children of a recognized top-level override block. Mutation-checked: widening the normalizer to rewrite any nested occurrence now fails three tests, where before it failed none. Cleanup was documented only as Bash, using jq, pgrep, sed and xargs, in a skill that otherwise supports launch.ps1 and states Windows needs neither Bash nor jq - so Windows users could not run the survivor check that section exists to enforce. Added a PowerShell equivalent with the same run-dir canonicalization, verified to accept a real run directory and reject both a traversal and a non-run path. Two bugs surfaced writing it: `$pid` is read-only in PowerShell, so the existing `$pid = $info.pid` line silently left the shell's own PID in place, and `?.` requires PowerShell 7 while this skill supports 5.1. Both fixed. Tests grow to 58. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cipe Three findings, two of them bugs in the PowerShell recipe added last round. Discovery treated an absent named-profile settings.json as "this profile inherits". It does not: createProfile makes only the directory, so an independent profile that has nothing saved yet was skipped, and an associated workspace then opened with the default editor.editContext and files.simpleDialog.enable - the exact failure this script prevents. Only useDefaultFlags.settings redirects settingsResource to the default profile, so the cloned userDataProfiles metadata now decides, and unreadable or absent metadata skips nothing, which is the safe direction. The fixture models real metadata and covers all three cases. The Windows cleanup recipe validated the run directory by its leaf name, but launch.ps1 names it <temp>\\code-oss-dev\\<timestamp-pid>, so it rejected every genuine Windows launch and left the profile behind. It now requires a direct child of the launcher's base directory; verified that a real run is accepted while a traversal and an unrelated path are rejected. The survivor query used -like, which is a wildcard match: a temp path containing [ or ] is read as a character class. Verified that such a path fails -like but matches a literal comparison, so the query could have deleted a live instance's profile or killed unrelated processes. It now compares case-insensitively as a literal substring. Tests stay at 58; both directions of the discovery change are mutation-checked, and a launch plus chat round-trip still works. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
VS Code's JSONC scanner treats U+2028/U+2029 as line terminators and the full Unicode 3.0 space set as whitespace. codeMask only ended line comments at CR/LF and left the extra trivia in place, so JSON.parse rejected profiles that VS Code reads happily and every launch aborted. Fold scanner-only trivia to a plain space (1:1, offsets preserved) and end line comments at any scanner line break. attach() defaulted logsPath to a fixed shared directory, but PlaywrightDriver names traces and screenshots from per-process counters that both start at 1, so concurrent agents overwrote each other's artifacts. Use mkdtempSync instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
codeMask blanked an unterminated block comment through EOF, so the remaining text parsed cleanly and a broken settings.json was rewritten despite the fail-closed contract. Reject unterminated comments and strings from the scanner state instead. The cleanup recipe's basename check let a real directory such as ~/code-oss-dev-important reach `rm -rf`. Also require the canonical parent to be the launcher's temp root ($TMPDIR, or /tmp when launch.sh fell back to it). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reject a symlinked User/profiles or profile child before the inheritance skip. An empty or dangling link yields no entries to walk, and a linked child marked as inheriting is skipped, so in both cases the clone kept a directory resolving outside the throwaway profile. Non-directory entries are now ignored rather than turned into a settings.json path. Broaden the preservation assertion to collect block comments as well as line comments; dropping /* */ was previously invisible to the suite. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a find-widget recipe to automation-library.md: the visible-widget selector, the fact that fill works because the find input is a plain textarea, aria-label prefixes for the option toggles, and the need to read aria-checked before clicking one. Verified live against a running instance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Dogfooding showed keyboard.type lands text in both a Monaco editor and the chat input, contradicting the blanket "fill and type silently fail" claim. Verified against a live instance: fill times out on native-edit-context, while keyboard.type and locator.type dispatch real key events and work. Also record that focusing chat by clicking the input times out - use the command instead, since a failed focus makes typing look broken when it isn't. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remap legacy URI profile locations into the cloned user-data directory, preserve comment-only JSONC settings, reject workspace simple-dialog overrides, and keep run directories when process enumeration fails. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Recognize the nested builtin/agents profile without treating its namespace as a profile, and inspect code-workspace files opened through --file-uri. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Forwarded profile flags create profiles after automation settings have been normalized, so fail before launch instead of allowing native dialogs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Materialize a linked storage.json in the clone and reject linked ancestors before rewriting legacy profile URIs, preserving the source profile. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Set Match Case only when needed and require the launcher timestamp leaf before recursively deleting a Windows run directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reject forwarded options that can override isolation, preserve inert window settings in language blocks, and reference the actual VS Code JSON parser. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reject inspect-brk variants, remove every test fixture in an after hook, and avoid double-counting profiles when TMPDIR resolves to /tmp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Recognize native CLI option values, force no-path launches into an empty window, and retain launcher isolation while accepting valid forwarded args. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Accept trivia-only workspace settings, reject transient profiles, and avoid letting a missing string-option value swallow the next protected option. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Match minimist value-token behavior for URI and ordinary string options, and reject a second delimiter that would hide appended safety flags. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Classify positional arguments after parsing so code-workspace files opened as diff or merge inputs are not mistaken for active workspace configuration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirror VS Code value-tree ordering for workspace checks and rewrite nested editor settings at root and language-override scope without touching unrelated data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove owned attach log directories, preserve caller paths, and cover the deprecated debugger plus short diff and merge option spellings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Why
The
launchskill drove Code OSS exclusively through raw@playwright/cli, so every agent hand-writes selectors for surfaces thattest/automationalready models.That library is what the smoke tests use, and it encodes the knowledge these surfaces actually need. For example,
Chat.selectModelalready documents and retries the exact failure mode where a row click is "absorbed by the action-widget's animatingcontext-view-pointerBlockoverlay" — a failure I spent a long time rediscovering by hand.What
test/automationnormally spawns Electron viaplaywright._electron.launch(). ButPlaywrightDriveronly uses that object forwindows()/close()and already branches on'windows' in application, so aBrowserfromchromium.connectOverCDP()works in its place — letting the page objects attach to the instancelaunch.shalready started.scripts/attach.ts—attach(cdpPort, { window })returns{ workbench, code, page, browser, detach }automation-library.md— the companion doc: window choice, controls with no page object, the integrated browser, and the gotchasSKILL.md— a "Drive the UI" section pointing at bothWhy a module rather than a documented snippet
Three steps are invisible from the type definitions, and each fails in a way that does not point at its own cause:
test/automation/outis CommonJS, soimport { Code }throwsSyntaxError: Named export not foundQualityis aconst enum, erased at compile time, so it has no runtime value; this surfaces later asCannot read properties of undefinedwindow.driveronly exists with--enable-smoke-test-driver; without it, page objects hang or throw obscure errorsEach is checked up front and reported with the fix. This matches the precedent set by
monaco-paste.shin the same skill.Validation
Verified live against a real instance:
workbench.extensions.searchForExtensionexplicitOverrideagentsWindow.isSessionTypeAvailable--enable-smoke-test-driver/ unbuiltout/What the user studies changed
I ran fresh agents against the docs and measured. The headline result was that the companion doc alone did not work: in the first two studies no agent ever opened it, and one spent 7m40s and 40 raw CLI calls hand-rolling DOM queries for a task
workbench.extensions.searchForExtension()answers in one line.Two follow-up runs isolated why, and each finding became a doc change:
SKILL.mditselfnew Application(...)underts-node(the spawn API) → nameattach()as the entry point--enable-smoke-test-driver→ show the flag at launch time, where it must be passedDogfooding the condensed docs also caught two things worth having:
Set Session Targetis gated onchatSessionIsEmptyso it vanishes once a session has a turn, andsearchForExtensiontakes an extension id — passing a display name fails withExtension ... is not found, which reads like the surface is unsupported.automation-library.mdwas condensed from 267 to ~210 lines in the process, keeping the gotchas that cost multiple runs to discover and cutting the ones that were self-inflicted or instantly obvious.Launcher changes that came out of review
Copilot reviewed this a number of times, and the launcher-side findings turned out to be the substantive ones. Each was reproduced before being changed.
The
editor.editContextproblem. The page objects pick.native-edit-contextvstextareafromCode.editContextEnabled, which is unconditionallytruefor a dev build. A cloned profile that had seteditor.editContext: falserendered atextareawhile every text-input helper waited on the wrong selector and timed out 20s later, pointing at nothing. Both launchers now normalize that key alongsidefiles.simpleDialog.enable.Scope turned out to be the hard part, in three layers:
editor.*settings areLANGUAGE_OVERRIDABLE, so a"[typescript]"block outranks the root value — nested overrides are rewritten too, but only in real override blocks (OVERRIDE_PROPERTY_REGEX), not in unrelated nested data.User/profiles/<id>/settings.json, and the clone preserves the workspace→profile associations thatresolveProfileForBrowserWindowacts on. Every existing profile settings file is normalized; only profiles whose stored metadata setsuseDefaultFlags.settingsgenuinely inherit, and only those are left alone. A symlinkedUser/profilesor profile directory aborts the launch, sincersync -apreserves it and the clone would no longer be self-contained.attach()detects that case during preflight — afterwhenWorkbenchRestored(), once there are editors to inspect — and says where to look instead of letting the first helper time out.The settings merge itself. It has to be data-preserving (comments and user values survive), which rules out reparse-and-rewrite. It now lives in one
scripts/normalize-automation-settings.tsused by both launchers so the platforms cannot drift, and it fails closed: the file is parsed the way VS Code parses it before anything is written, so a malformedsettings.jsonis never clobbered. Along the way review caught symlinked settings files being written through (rsync -apreserves links, so this escaped the throwaway profile),"\u005btypescript\u005d"not being recognized as an override, and--sync=offbeing defeated by a forwarded--sync=onbecause VS Code keeps the last value of a string option.Parsing it the way VS Code does. Fail-closed validation only helps if it agrees with the real parser, and several rounds of review were spent closing that gap: VS Code's JSONC scanner ends a line comment at a bare
\rand at U+2028/U+2029, accepts the full Unicode 3.0 space set as whitespace, and treats an unterminated block comment as an error rather than as trivia running to EOF. Each mismatch either rejected a profile VS Code reads happily (aborting the launch) or accepted one it rejects (rewriting a broken file).scripts/normalize-automation-settings.test.tscovers this with 68node --testcases. Every guard was mutation-checked: removing it fails at least one test.Review also hardened the surrounding recipes — the cleanup snippets in
SKILL.mdnow verify that the directory they are about torm -rfreally is a launcher run directory under the launcher's own temp root, and a PowerShell equivalent was added for Windows (where$pidis read-only and-liketreats a temp path containing[1]as a wildcard pattern).