Files
qwen-code/integration-tests/cli
Shaojin Wenandverify 3eff5f7793 feat(web-shell): create workspaces from a Git URL (#15361)
* feat(web-shell): create workspaces from a Git URL

The Add-workspace dialog could only register an existing directory or
create an empty scratch directory, so starting from a repository meant
leaving the browser, cloning on the daemon host, and registering the
path afterwards.

Add a Git source that starts an asynchronous clone job, reports progress,
and supports cancellation. A successful clone is registered through the
existing workspace route, so capacity, nesting, persistence, and the
daemon's trust policy stay in one place.

* fix(cli): classify git_workspace_clone in the capability parity matrix

The parity matrix landed on main after this branch, and every capability
tag needs a row. Managed workspace creation only creates empty
directories, so record the clone surface as an explicit Managed gap
rather than claiming parity.

* fix(cli): reserve active clone targets before overlapping work

An outer clone's failure/cancel cleanup removes its target recursively,
so a nested clone or workspace registration must not start underneath a
running job. Reserve the canonical target for the job lifetime, reject
overlapping clone and registration paths, and release the reservation
only after cleanup settles.

Also resolve the success path before publishing `succeeded`, so a reader
never observes a terminal snapshot without the `cwd` it registers.

* fix(web-shell): keep clone polling recoverable

A failed poll left the last known status running, so the panel offered
Cancel instead of the recheck that can recover the existing job. Prefer
the unknown-state action.

Reset the unmounted guard in the effect setup: StrictMode's development
replay otherwise leaves it set and discards every later poll, including a
successful clone.

* feat(web-shell): reuse the directory path as the clone parent

When the source switches to Git, treat the directory path as the clone
parent and append the repository name from the URL. A path whose basename
already matches stays as-is, and a manually edited or browsed target is
never overwritten by later URL edits.

* feat(web-shell): suggest folders for the clone target

Headless daemons do not provide a native directory picker, so the clone
target had to be typed in full. Reuse the directory suggestion list:
selecting an existing folder uses it as the parent and appends the
repository name, while a manually edited target still wins.

* fix(cli): keep clone progress monotonic

git restarts the percentage for some clone phases (remote counting,
then receiving), so the Web Shell progress bar could jump backwards.
Clamp the reported percent to the running maximum; the status message
still follows the current git line.

Also pin the clone admission guards left unpinned by review: reserved
roots, unknown request keys, promotion of a workspace under an active
clone target, file URLs that name a host, percentages above 100, and
the dialog fallback when the daemon capability disappears.

* fix(cli): reserve the clone target before claiming it

The claim created the target with mkdir, then awaited the existence check
and the final realpath, and only reserved the canonical path afterwards.
A concurrent ordinary registration could admit the freshly created
directory in that window and then lose it to the clone's cancel/failure
cleanup.

Reserve the canonical target inside claimCloneTarget before mkdir, share
the reservation with registration, and release it on every claim failure
and on post-claim admission failures. Add a regression test that overlaps
the registration with the blocked claim and a unit test that asserts the
reservation exists before the directory does.

Also update the serve integration capability envelope for the
host-conditional git_workspace_clone tag, mirroring the daemon's probe.

* fix(cli): register git_workspace_clone in serve capability guards

Test (ubuntu) failed five capability-registry guards because the new
git_workspace_clone tag was not registered alongside the existing
conditional features:

- document it in the qwen serve protocol table;
- allow the clone spawner's process-environment copy and SystemRoot
  lookup in the serve process.env guard;
- add it to EXPECTED_REGISTERED_FEATURES and the conditional-feature
  drift-insurance branch.

Verified: capabilities-docs-contract, process-env-guard and server
suite 1399/1399; typecheck, ESLint, Prettier clean.

* fix(cli): keep clone targets away from reserved and Live paths

A clone target that contains the reserved Conversations root was accepted
when that root did not exist yet: creating the target made the root's
parent part of the clone, and the Live runtime could then be published
inside it. Because the Live exemption skipped the "canonical inside
target" direction for active clone targets, cancelling or failing the
clone deleted the registered Conversations workspace.

Reject clone targets that contain a reserved root, even before the root
exists, and apply strict two-way nesting to active clone targets in the
owned-runtime admission regardless of provenance.

Verified: workspace-git-clone + workspace-management 196/196;
typecheck, ESLint, Prettier clean.

* fix(cli): resolve reserved roots through existing ancestors

The reserved-root containment check fell back to the unresolved spelling
when the Conversations root did not exist yet. A symlinked ancestor such
as ~/Documents then made the canonical clone target and the lexical
reserved path disagree: the clone was accepted, the Live runtime created
the root inside it, and cancelling the clone left ConversationWorkspace
with a cached identity for a directory that no longer existed.

Resolve each path through its nearest existing ancestor and append the
missing suffix before comparing, so a clone target containing a missing
reserved root is rejected before mkdir even when ancestors are symlinks.

Verified: workspace-git-clone + workspace-management 197/197;
typecheck, ESLint, Prettier clean.

* fix(cli): cover case aliases and dangling links in reserved roots

The reserved-root guard compared the reconstructed missing suffix with
case-sensitive isWithinRoot, so a case-variant ancestor such as
`Documents/qwen code` did not match `Documents/Qwen Code/Conversations`
on a case-insensitive filesystem. It also walked past a dangling symlink
ancestor, so a clone target that was the link's missing destination
could later make the link valid inside the clone's cleanup scope.

Compare canonical paths case-insensitively after resolving through the
nearest existing ancestor, and follow dangling symlinks while
reconstructing the missing suffix. Add regressions for both aliases.

Verified: workspace-git-clone + workspace-management 199/199;
typecheck, ESLint, Prettier clean.

* fix(cli): harden git clone credentials, config, and admission

- Strip URL credentials before git's argv and supply them through a
  command-line credential helper backed by the child environment, so the
  secret no longer appears in /proc/<pid>/cmdline or .git/config.
- Disable global/system git config for the clone child so a repository's
  .gitattributes cannot run host-configured filter commands during checkout.
- Keep the first fatal/error stderr line for clone failures instead of the
  trailing SSH boilerplate.
- Only treat Receiving objects/Resolving deltas as progress phases, so
  remote counting cannot pin the bar at 100.
- Strip OSC/C1 and all other control characters from clone messages; add
  the missing Windows taskkill 'error' listener.
- Require operator authority for clone cancellation; reject clone targets
  inside the managed scratch root or equal to a registered/in-flight
  workspace; keep case-insensitive containment scoped to clone targets.

Verified: workspace-git-clone + workspace-management 205/205;
typecheck, ESLint, Prettier clean.

* fix(web-shell): gate the clone flow during submit and IME input

- Disable the post-clone Back button while registration is in flight and
  guard backToForm, so it cannot reset a submitting flag owned by onAdd.
- Ignore clone-target key handling while an IME composition is active.
- Dismiss the clone-target suggestion list when the git form is submitted,
  so its Escape interceptor cannot stay armed over the progress view.

Verified: AddWorkspaceDialog 57/57; Prettier and ESLint clean.

* docs: align git clone guarantees with credential and config isolation

- Document the `{ kind: "git" }` form and its 202 job response in the
  `POST /workspaces` reference.
- State that URL credentials are moved out of git argv and the stored remote
  URL, and that global/system git config is disabled for the clone child so
  committed filters cannot run host commands during checkout.
- Mirror both changes in the Chinese design document.

* fix(cli): use bracket access for clone credential env

The strict index signature on NodeJS.ProcessEnv rejects dot access to
QWEN_GIT_USERNAME/QWEN_GIT_PASSWORD; all CI jobs that build the CLI
failed at tsc with TS4111 before running any test.

* fix(web-shell): cancel the running clone from the left action

- Route the progress panel's left Cancel through cancelClone while the job
  is running, so it stops the daemon job instead of only closing the dialog.
- Clear the pending poll timer at pollCloneJob entry and before cancel, and
  guard registration per job id, so a terminal snapshot cannot register the
  same clone twice.

Verified: AddWorkspaceDialog 57/57; Prettier and ESLint clean.

* fix(web-shell): harden git clone env and clone-job dialog states

Pin LC_ALL/LANG for the clone child and sweep host Git config channels (GIT_CONFIG_*, GIT_ASKPASS, GIT_SSH*, GIT_PROXY_COMMAND) so a localized git still reports parseable progress and a browser-chosen URL cannot borrow host credentials. Decode URL credentials defensively, reject control characters, and print the inline credential helper with printf so dash does not mangle tokens.

In the add-workspace dialog, tear the clone state down when the capability disappears, report a pruned clone job instead of offering recheck forever, honour a cancel that loses the race with a finished clone, and close instead of cancelling an unconfirmed job. Correct the host-authentication prose in the design docs, README and URL hint to match what the isolated clone child actually supports.

* fix(cli): resolve relative symlinks physically and keep cancel intent

- Resolve a relative symlink target from its physical parent before appending
  the missing suffix, so a dangling relative Documents link cannot hide the
  reserved Conversations root from clone admission.
- Keep the per-job cancellation intent across overlapping poll responses, so
  a second succeeded poll cannot auto-register after Cancel. Clear it only on
  an explicit Back/new submission.

Verified: route + clone 209/209; AddWorkspaceDialog 61/61; typecheck with a
6 GB heap; Prettier and ESLint clean.

---------

Co-authored-by: verify <verify@local>
2026-10-25 10:16:38 +00:00
..