qwen-code/integration-tests
tanzhenxin afd631335d
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
feat: add Agent Team experimental feature for parallel sub-agent coordination (#4844)
* feat(core): add Agent Team foundation (experimental, flag-gated)

First stage of re-porting the Agent Team feature (originally PR #2886) onto
current main. The branch had diverged 362 commits behind a parallel rewrite
of the agent runtime, so the feature is being re-applied stage by stage
rather than merged.

This stage lands the self-contained agents/team/ subsystem (TeamManager,
mailbox, identity, tasks, leader permission bridge, test-utils) plus the
team_create/team_delete and task_create/task_update/task_list tools, with
the additive plumbing they need:

- Config: TeamManager/TeamContext accessors, cleanupTeamRuntime, and
  isAgentTeamEnabled (settings or QWEN_CODE_ENABLE_AGENT_TEAM=1).
- Tool registry: team/task tools registered lazily, gated on the flag.
- Runtime hooks: completeOnIdle for one-shot teammates; args on the
  agent approval event; teammate-aware tool exclusion sets.
- Backend types: TeamAgentHandle, optional getAgent, completeOnIdle.
- New experimental.agentTeam setting.

Everything is gated behind the experimental flag and inert by default.
Build is green; team unit tests and all touched-file regressions pass.

* feat(core): add send_message team routing (experimental)

Stage 1 of the Agent Team re-port. Extends the send_message tool so it can
route to a teammate (or "*" for broadcast) via TeamManager in addition to
its existing background-task path, and supports the shutdown_request control
message (leader-only). Recipient selection is a oneOf over `to`/`task_id`.

Layered on top of main's classifier integration: send_message keeps its
'ask' default permission and forwards the routing fields + message to the
AUTO classifier, since the message is an instruction the recipient executes.

Re-adds the team-lifecycle E2E test, which now passes end to end
(create -> tasks -> messages -> list -> update -> delete).

* feat(core): let the Agent tool spawn named teammates (experimental)

Stage 3 of the Agent Team re-port. Adds the `name` parameter to the Agent
tool: when a team is active and a name is given, the call routes through
TeamManager.spawnTeammate instead of launching a one-shot subagent. Without
a team the call is rejected up front rather than silently falling back. The
tool description advertises team coordination only when the experimental
flag is on. Ported onto main's rewritten Agent tool.

* feat(cli): render team_result/task_list tool displays (experimental)

Stage 5 (partial) of the Agent Team re-port. Teaches the ToolMessage
result renderer about the TeamResultDisplay and TaskListResultDisplay
shapes so the team/task tools' output is shown via their returnDisplay
text instead of a stringified object, and adds a JSON.stringify safeguard
for any other non-string display object.

The remaining CLI wiring (nonInteractiveCli + useGeminiStream team
drivers, permissionController.handleTeammateApproval) is coupled to the
turn-loop Teammate handling and will land together with Stage 4.

* feat(core): treat teammate messages as top-level turns (experimental)

Stage 4 of the Agent Team re-port. Adds SendMessageType.Teammate and
includes it in isTopLevelInteraction so that a teammate message delivered
to the leader resets the loop detector and opens an interaction span, the
same as a user/cron/notification turn.

Per the agreed minimal integration, teammate turns deliberately do NOT run
the UserQuery/Cron block — they don't bump commit attribution, aren't
recorded as user messages, and don't trigger auto-memory prefetch. That
keeps the edit to main's restructured turn loop to a single condition.

* feat(cli): drive teammates from the headless run loop (experimental)

Stage 5 of the Agent Team re-port. Wires the non-interactive/headless run
loop to the active team: it subscribes to TeamManager changes, drains
teammate messages into the leader's conversation as SendMessageType.Teammate
turns, waits for teammate activity when the leader has no pending tool calls,
and routes teammate tool-approval requests through the session's permission
channel (SDK in stream-json mode; YOLO/cancel fallback otherwise).

Adds PermissionController.handleTeammateApproval and exposes it on the
ControlService permission facade. Ported onto main's restructured run loop
(which added its own cron/notification drain mechanism).

* feat(cli): drive teammates from the interactive turn loop (experimental)

Final Stage 5 piece of the Agent Team re-port. Wires the interactive (TUI)
useGeminiStream hook to the active team: it subscribes to TeamManager,
queues teammate messages, and drains them into the conversation as
SendMessageType.Teammate turns when idle, guarded against racing the
notification drain. Treats teammate turns like user/cron for image-format
checks and new-prompt stats. Ported onto main's rewritten hook.

* fix(core): declare proper-lockfile dependency for the team subsystem

The Agent Team mailbox and task files import proper-lockfile, but the
dependency was never declared in package.json, so a clean `npm ci` (as CI
runs) failed to resolve the module — cascading into implicit-any and
possibly-undefined errors in the same files. It built locally only because
the working tree's node_modules already had the package from an earlier
install.

Adds proper-lockfile to packages/core dependencies and @types/proper-lockfile
to root devDependencies (matching the original feature branch), and
regenerates the lockfile. Build and typecheck are clean.

* fix(team): address review findings on the agent-team subsystem (experimental)

Triaged the unresolved review threads from the superseded PR #2886 against
the re-ported code and applied the valid fixes:

- task_update(status:'deleted') now enforces the same ownership guard as
  updateTask, so a teammate cannot delete another teammate's task.
- Completing a task and adding a blocks edge in the same call no longer
  leaves the dependent permanently blocked by the just-completed task.
- listTasks treats a momentarily-empty (mid-create) task file as a create
  in flight and skips it instead of quarantining and losing the task.
- Fire-and-forget coordination calls (flush, auto-claim, unassign, poll)
  log rejections instead of surfacing as unhandled rejections.
- pollLeaderInbox re-checks the leader callback after the awaited read so a
  detach during the read cannot throw or drop the batch.
- scanIdleAgentsForTasks skips teammates with a pending shutdown.
- broadcast uses allSettled so one terminated recipient does not fail the
  whole broadcast.
- Hybrid tool-response+teammate turns reset the loop detector, preventing a
  false LoopDetected when a polling leader merges teammate messages.
- useGeminiStream drains its teammate queue on a manager swap; the join
  event carries the teammate model for the UI tab label.
- Removed dead consumeUnreadByType and an unreachable ENOENT branch.

Verified: core unit tests (incl. new regressions for the delete guard, the
complete+addBlocks re-block, and the empty-file create race) plus live L3
(3-agent) and L4 (4-agent) E2E, both clean.

* fix(core): close ownership TOCTOU and lock-ordering hazard in agent-team tasks

deleteTask checked ownership against a pre-lock read, so a concurrent
claimTask/updateTask could reassign the owner between the check and the
unlink — silently destroying another teammate's task. Acquire the lock
first, then re-read and re-check ownership inside it before unlinking,
mirroring updateTask. Reciprocal edge cleanup now runs after the lock is
released (never holding two per-task locks at once) but before the single
tasks-updated notification, so no listener observes a phantom blocker.

blockTask issued its two updateTask writes via Promise.all; two calls over
the same pair in opposite directions could deadlock on per-task locks.
Serialize the writes to remove the lock-ordering hazard.

* fix(core): harden agent-team message handling and auto-claim

- Cap per-agent pending messages (MAX_PENDING_MESSAGES). The queue only
  drains when its recipient goes IDLE, so an unbounded queue let a single
  looping teammate balloon a busy teammate's memory; sendMessage now
  applies backpressure once the cap is reached.
- Wrap auto-claimed task content (subject/description, authored by another
  agent) in a <task_content> envelope with a defensive instruction so it
  is treated as data, not as instructions to obey.
- Surface fire-and-forget coordination failures (flush, auto-claim,
  unassign) to the leader's conversation. They were only logged via a
  namespaced debug logger, i.e. invisible in production, despite mapping
  to silent stuck-teammate / stuck-task symptoms.

* fix(core): require approval for agent-team task_create/task_update

A task's subject/description becomes the prompt an idle teammate
auto-claims and executes with full tool access — the same privileged-sink
shape as send_message. Both tools inherited the base default 'allow',
which short-circuits the classifier in AUTO mode. Override
getDefaultPermission to 'ask' so that injection path stays under the
classifier / human-in-the-loop, matching send_message.

* docs(core): correct completeOnIdle JSDoc for team teammates

The JSDoc cited team teammates as the use-case for completeOnIdle:true,
but teammates set it to false so they settle to IDLE (not COMPLETED) and
stay alive for follow-up messages and auto-claim. Document the actual
semantics and the invariant the leader's wait loop relies on.

* fix(core): harden agent-team leader callback and task envelope

- fireAndForget: wrap leaderMessageCallback in try/catch so a throwing
  callback cannot re-introduce the unhandled rejection the wrapper exists
  to prevent (enforces the documented 'must not throw from this catch').
- tryAutoClaimTask: nonce-tag the <task_content> envelope with the
  per-session envelopeNonce (same pattern as formatLeaderEnvelope) so a
  teammate-authored description cannot forge the closing tag and break
  out of the protected zone via a </task_content> payload.

* fix(core): make deleteTask edge cleanup resilient to partial failure

Use Promise.allSettled (was Promise.all) for post-unlink edge cleanup so
a single failing dependent (corrupt JSON, EACCES, lock exhaustion) no
longer skips notifyTasksUpdated for the dependents that were cleaned.
Without this their blockedBy is cleared but scanIdleAgentsForTasks never
re-runs, leaving them stuck idle with no recovery (the task file is
already unlinked, so a retry returns false). Per-failure warnings are
logged.

* fix(cli): mount useTeamInProcess so teammate tabs render

The hook bridging team TEAMMATE_JOINED events to agent-tab registration
(useTeamInProcess) was authored but never mounted in AgentViewProvider —
only useArenaInProcess was. As a result teammate tabs never registered and
the teammate tab bar never appeared during in-process team runs.

Mount useTeamInProcess alongside useArenaInProcess, and label teammate tabs
by name rather than model (teammates inherit the leader's model, so a model
label collapses to a generic "teammate" and is identical across the team).
Add a regression test asserting the provider mounts the team bridge.

* test(terminal-capture): add agent-team feature demo + capture fixes

Add a standalone streaming demo of the agent-team feature that captures the
full lifecycle and the teammate tab navigation into a single GIF
(scenarios/agent-team-demo.ts).

Supporting engine fixes:
- capture(): scroll the xterm viewport to the live bottom before
  screenshotting, so a capture taken after an idle period shows the current
  state instead of stale top-of-buffer scrollback.
- scenario-runner: skip scenarios/*.ts files with no default export (driver
  scripts that guard their own entrypoint), so batch runs don't choke.

* fix(core): serialize in-process mailbox writers to fix Windows lock flakiness

The concurrent-write test fired 10 writeMessage() calls at one inbox,
each contending for the same proper-lockfile lock with a fixed,
non-randomized backoff. On Windows, slower fs syscalls let the tail
writers exhaust the retry budget before winning the lock, throwing
ELOCKED ("Lock file is already being held") — a flaky failure that
alternated pass/fail across CI runs.

Add a per-inbox in-process Mutex (async-mutex, the pattern already used
in jsonl-utils and writeContextFile) so same-process writers serialize
in memory and only one reaches for the file lock at a time. The
proper-lockfile lock stays inside the mutex to preserve cross-process
safety between agent processes. Also randomize the lock backoff to
de-synchronize genuine cross-process contenders.

* feat(team): render teammate reports as a compact notification line

A teammate's report was injected into the leader's conversation as a
raw <teammate_message_<nonce>> envelope and rendered verbatim as a user
bubble — a large, scaffolding-heavy block on screen for what is often
the biggest payload in the feature.

Adopt the two-text split the notification queue already uses: the full
nonce-tagged envelope still goes to the leader's model, but the user now
sees a compact "● <name> reported back" line in its place. The verbatim
USER bubble is suppressed for SendMessageType.Teammate exactly as it is
for Cron, and coordination-error notices get the same treatment.

The leader callback now delivers both the model text and a display
string built in TeamManager (where the structured sender/summary live),
so the UI never parses the envelope. Headless is unchanged — it ignores
the extra arg.

* fix(terminal-capture): widen agent-team-demo Phase C budget so the GIF doesn't cut off

The leader sits idle (no Main-view output) while teammates read their
files, so Phase C captures no frames until a report lands — making
maxPolls the real wall-clock budget. At 80 polls (~112s) a slow second
scout could exhaust it before reporting, ending the GIF mid-run. Bump to
200 polls (~5min) so the capture outlasts the slowest scout plus the
combined summary and delete; the loop still exits early on `deleted` and
idle polls capture no frames, so the GIF doesn't bloat.

* fix(core): separate task-content nonce; forward send_message summary

Address two review findings on the agent-team messaging path:

- The <task_content_…> envelope reused envelopeNonce — the per-session
  nonce the leader trusts to authenticate <teammate_message_…> blocks.
  Because the task-content prompt is delivered to the claiming teammate,
  a teammate could learn the nonce and forge a leader-trusted envelope.
  Use a dedicated taskContentNonce so the leader-trust nonce stays secret
  from teammates.

- The SendMessage 'summary' param was dropped between the tool and the
  mailbox, so the leader UI always showed the '{name} reported back'
  fallback. Thread summary through sendMessage → writeMessage so it
  reaches formatLeaderDisplay.

Adds regression tests for both.

* fix(core,cli): harden agent-team messaging per review round 4

- task-content envelope uses a fresh per-claim nonce instead of a shared
  per-session one, so a teammate that learns one task's nonce can't forge
  a later task's closing tag to inject the next claimant.
- team_delete wraps manager.cleanup() in try/catch and always resets the
  Config team state, so a cleanup failure no longer permanently wedges
  team_create for the rest of the session.
- unassignTeammateTasks uses Promise.allSettled so one corrupt/locked task
  file no longer strands the remaining tasks on a terminated teammate; the
  caller's re-scan still fires.
- non-interactive teammate-approval responses .catch() rejections to avoid
  an unhandledRejection if the teammate terminates mid-approval.
- setupEventBridge warns when the backend can't provide an agent handle or
  event emitter instead of returning silently.

* fix(core): don't let a failed dependent unblock abort task completion

unblockDependents used Promise.all, so a single dependent failing
(corrupt JSON, EACCES, lock exhaustion) rejected out of updateTask
before the completed status was persisted — the task stayed
in_progress on disk while already-processed dependents were
unblocked, leaving the dependency graph inconsistent. Switch to
Promise.allSettled with a debug warning per failure, mirroring the
best-effort edge cleanup in deleteTask and unassignTeammateTasks.

* fix(core): quarantine corrupt teammate inboxes; skip task scan when no agent is idle

Review round 7. A corrupt teammate inbox previously made every
writeMessage/consumeUnread re-throw on the same file, so the teammate
could never receive another message (including shutdown requests) —
while the leader inbox already self-healed via quarantine. readInboxRaw
now renames the corrupt file to .corrupt-{ts} and continues on a fresh
inbox; the leader-side offset clamps to 0 if the inbox shrank behind
the poller so messages are re-surfaced rather than silently skipped.

scanIdleAgentsForTasks now checks for idle members before reading the
task board, avoiding a full tasks-directory scan on every task update
while all agents are busy. Also document the restrictsOwnership field
enumeration hazard and the intentional metadata/activeForm exclusion.

* fix(core): re-check task ownership under the lock when unassigning a terminated teammate

Review round 8. unassignTeammateTasks snapshotted in_progress tasks
and then blind-wrote {status: pending, owner: null} per task, so a
leader reassignment (or the dying teammate's final completion) landing
between the snapshot and the per-task lock was silently reverted.
Releases now go through an in-lock compare-and-set that skips the task
when its owner or status no longer matches the snapshot.

Also isolate task-update listeners (one throwing listener no longer
starves the rest) and drop the lone const enum for subsystem
consistency.

* fix(core): harden team task file layer against partial writes and transient I/O

- createTask claims the ID with an empty O_EXCL placeholder and fills
  it via temp-file + rename, so concurrent readers never see partial
  JSON (which the quarantine would have destroyed mid-create)
- listTasks quarantines only on parse failures; transient read errors
  (EMFILE/EIO/EACCES) skip the file for one round instead of renaming
  a healthy task away, and the read fan-out is capped at 16
- updateTask / claimTask / releaseOwnedTask guard the in-lock readFile
  against ENOENT (resetTaskList and the quarantine rename run without
  per-task locks), mirroring deleteTask
- cover releaseOwnedTask's three defensive branches with tests

* fix(core): drain messages enqueued during the IDLE transition; settle abort on idle agents

- a message enqueued from inside the synchronous IDLE STATUS_CHANGE
  emit (TeamManager's flush) landed after the run loop's final empty
  check while `processing` was still true — enqueueMessage would not
  restart the loop and the message stranded in a dead queue; the loop
  now re-checks the queue after `processing` flips false
- abort() on an idle/initializing agent only set the signal: no loop
  was running to observe it, so the agent never reached a terminal
  status and allTeammatesTerminated()-style gates never fired; abort
  now settles CANCELLED directly when no loop is in flight
- regression tests drive the real AgentInteractive (stub model, real
  loop) through send-during-idle-emit and abort-while-idle

* test(core): align FakeAgent queue and abort semantics with AgentInteractive

FakeAgent modeled a friendlier runtime than the one that ships:
enqueueMessage processed inline (no queue, no processing flag,
resurrecting terminal agents via unconditional RUNNING), which is
exactly what masked the flush-into-dead-queue bug. It now queues
while a round is in flight, drains before settling IDLE, drops
messages after abort()/shutdown() like the real drained queue, and
never resurrects a terminal agent.

* fix(core): surface spawn failures, handle shutdown_rejected, envelope peer messages

- spawnTeammate now checks the agent's status after spawnAgent
  resolves: start() reports chat-creation failure via FAILED without
  throwing, so the leader was told the teammate joined while sends
  were accepted into a queue that could never flush; a failed spawn
  now rolls back and surfaces the reason (with a terminal-status
  replay in setupEventBridge for the attach race)
- shutdown_rejected now clears _shutdownPending: a teammate that
  declined once stayed excluded from auto-claim and kill-armed on any
  later "shutdown_approved" mention
- peer-to-peer deliveries get a fresh-nonce envelope like leader
  deliveries, closing inline leader impersonation between teammates
  (deliberately not the leader-trust nonce, which must never reach
  teammate context)

* fix(core): exclude workflow tool from teammates

The teammate ALS identity propagates into anything a teammate spawns,
so prepareTools() keeps choosing the teammate exclusion set for nested
agents — without WORKFLOW in it, a teammate-launched workflow re-arms
the O(k^n) recursive fan-out the subagent exclusion set prevents.

* fix(core): make task tools visible to permission review; reject dependency cycles

- task_create / task_update now project their content (subject,
  description, status, owner, edges) to the AUTO classifier — the base
  '' sentinel projected to an empty object, so the classifier ruled on
  task_create({}) and the 'ask' override was blind; the interactive
  confirmation now shows the description (truncated), since that text
  is what a claiming teammate executes
- task_update rejects self-edges and dependency cycles instead of
  silently persisting a graph that auto-claim can never unblock
- regression tests pin the 'ask' default and a non-empty classifier
  projection for both tools

* fix(core): reclaim stale teams on team_create instead of wedging the name

Nothing deletes team dirs on normal exit (only an explicit team_delete
does), so every Ctrl+C, completed headless run, or crash permanently
wedged the team name behind createTeamFile's wx-exclusive create, with
manual rm -rf as the default recovery. team_create now records the
owner identity (leadSessionId + leadPid) and, on EEXIST, reclaims the
team when the recorded lead process is gone (or is this process);
only a live concurrent owner keeps the name refused.

* fix(cli): pass teammate envelopes straight to the model, skipping shell/@/slash preprocessing

Teammate envelopes are model-authored text already rendered as a
notification line by the teammate drain, but they still flowed through
the user-input preprocessing: with shell mode active a teammate report
was EXECUTED as a shell command, and a leading / or an @path was
reinterpreted against the leader's session. They now early-return like
Notification.

* fix(core): exclude Teammate from UserPromptSubmit hooks and record it in chat history

Teammate envelopes are machine-driven re-entries like Cron and
Notification: user-authored UserPromptSubmit hooks must not fire on
(or block) internal coordination traffic. They also never reached any
chat-recording path — record them like notifications so a resumed
session restores the same compact info line the live UI rendered.

* fix(cli): stop teammate-approval rejections from escaping as unhandled rejections

The stream-json listener voided handleTeammateApproval's promise while
the handler's own error path re-issues a respond() that can reject
(teammate terminated mid-request) — an unhandledRejection that can take
down an SDK session. The call site now catches like its headless
siblings, and the controller's catch-path respond(Cancel) is wrapped so
the method never rejects out of its own error path.

* test(core): add getSessionId to team-lifecycle mock config
2026-06-11 07:58:32 +08:00
..
baselines test(perf): add daemon baseline harness (#4175 Wave 1 PR 1) (#4205) 2026-05-17 00:41:26 +08:00
cli test(integration): harden flaky sleep-interception e2e against skipped tool calls (#4936) 2026-06-11 00:08:26 +08:00
concurrent-runner feat(web-search): remove built-in web_search tool, replace with MCP-based approach (#3502) 2026-04-24 11:29:02 +08:00
fixtures test(perf): add daemon baseline harness (#4175 Wave 1 PR 1) (#4205) 2026-05-17 00:41:26 +08:00
hook-integration feat(hooks): Add HTTP Hook, Function Hook and Async Hook support (#2827) 2026-04-16 10:10:33 +08:00
interactive feat(core): enable loop/cron tools by default (#4950) 2026-06-10 23:21:53 +08:00
sdk-typescript fix(test): count result messages instead of assistant messages in multi-model E2E test (#4341) 2026-05-20 10:42:51 +08:00
terminal-bench Terminal Bench Integration Test (#521) 2025-09-05 17:02:03 +08:00
terminal-capture feat: add Agent Team experimental feature for parallel sub-agent coordination (#4844) 2026-06-11 07:58:32 +08:00
channel-plugin.test.ts docs(channels): add plugin developer guide and rename mock to plugin-example 2026-03-27 03:19:34 +00:00
globalSetup.ts feat(core): support QWEN_HOME env var to customize config directory (#2953) 2026-05-09 15:51:52 +08:00
test-helper.ts test(integration): harden flaky sleep-interception e2e against skipped tool calls (#4936) 2026-06-11 00:08:26 +08:00
test-mcp-server.ts # 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483) 2025-09-01 14:48:55 +08:00
tsconfig.json refactor(serve): 1 daemon = 1 workspace (#3803 §02) (#4113) 2026-05-15 12:44:36 +08:00
vitest.config.ts add doc for hooks and skip integration test 2026-03-12 07:44:26 -07:00
vitest.terminal-bench.config.ts Fix E2E caused by Terminal Bench test (#529) 2025-09-08 10:51:14 +08:00