mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-11 04:02:45 -10:00
* fix(web-shell): close managed host integration gaps from #15928 Follow-up to #15928. Each change below was reproduced against the merged head before fixing, and every new assertion was checked to fail without its fix. The host entry required a Java base URL even when a provider was injected, so the documented mount only compiled inside test files and a JavaScript host that dropped the provider silently addressed /undefined/api/... . The base URL is now optional and only the Java path constructs a client; with neither input the entry renders the unavailable notice. Returning to an unconfirmed request's owner was the only navigation that bypassed the shared open path: it dropped the owning workspace selection and carried the previous conversation's error into the newly opened one. Pending records now remember their workspace and the redirect uses the shared path, so the owner keeps its workspace even after the catalog stops listing it. An unconfirmed create still returns to the new-conversation page. The archived-status callback was forwarded by identity into an effect that emits a fresh object, so an inline host callback re-rendered without bound. It is routed through a ref like the other host callbacks. Host navigation slots sit outside the shell container, where the compiled stylesheet matches nothing. The entry now wraps each portaled list in the portal scope and syncs theme class, language and custom properties. The managed empty state centered every in-flow child, floating the session header mid-pane and pushing content out of scroll reach once it grew taller than the pane; it also stayed set while a transcript was loading or a first send was in flight. Centering moved to auto margins on the content wrapper and the marker now requires an idle, non-loading conversation. The welcome header no longer stacks with a failure banner. Host mode renders no session list, so both focus fallbacks became no-ops and dropped focus to the document body, and the pane measurement never re-ran after the waiting state. Focus falls back to the page wrapper and measurement re-runs when the host supplies a Session ID. Shared style release was not idempotent: disposing one mount twice stripped the stylesheet a sibling still used. Both release paths are now guarded. The managed-host build mode ran in no gate and wrote inside the directory the app build empties. It is part of the package build and writes to dist-managed-host, staying out of the published exports and files. Weak assertions from the original change were strengthened: the injected provider test now spies on Java construction, the host-scope teardown keeps the Session ID constant, the shadow-style test exercises the constructable-sheet path, and the pending-row exclusion names the real copy. New coverage pins the host pending key isolation, the notice reset on a host switch, and the empty failed conversation. Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): render the unavailable notice and gate the pending workspace fallback Address the two blocking review items on the #15928 managed-host follow-up: - The mount docs/README promise an unavailable notice when neither a provider nor a baseUrl is supplied, but the shell rendered an empty wrapper and the test only asserted the Java provider was not called. Render the notice via the existing managed.unavailable i18n key and strengthen the test to assert the notice text (red without the render, green with it). - The 'open the original conversation' handler fell back to the on-screen summary's workspace, which in the cross-conversation case is a foreign workspace. Gate that fallback on pending.sessionId === sessionId so the owner is never bound to the wrong workspace when its own workspace is unknown. The third review item (managed-host bundle in npm run build) is a maintainer call on build cost and is left unchanged pending that decision. ManagedAgentWebShell 18/18, ManagedSessionsPage 173/173; eslint exit 0, prettier clean. * fix(web-shell): theme only our own host slot wrappers and cover centering in a browser Two review items on this PR remained aftera61bfc04. The slot sync identified its wrapper as the target's first element child. That is wrong whenever the host slot already holds host chrome, and it is also wrong when both slots name the same element. A probe against the reviewed head showed the real consequence is worse than "fails silently": it wrote lang, the dark class and every custom property onto the host's own node (host chrome lang fr -> zh-CN) while leaving the actual list wrapper unstyled. The wrappers now register themselves through a ref, so the sync only ever touches nodes this entry rendered, and disconnected wrappers are dropped. Both cases are pinned by tests that fail on the first-child lookup. The centering claim had no layout evidence: jsdom performs no layout, so the component test can only pin data-managed-empty, and the visuals bot reported that no scenario renders this surface. Added a browser regression with its own host-slot fixture that measures the pane at a cramped viewport. With the previous justify-content rule the session header sits at -51px, above the scroll origin and permanently unreachable; with auto margins it sits at 0 and stays in the viewport. The test fails on the old rule and passes on the new one. The build-wiring cost stays unchanged pending the maintainer decision. For that decision, measured locally on this machine: the managed-host pass takes about 13-14s wall clock against about 9s for the default lib pass and about 3s for the transcript pass, and emits 38 MB. ManagedAgentWebShell 20/20, the five affected suites 212/212, the new browser regression 1/1; eslint exit 0, prettier clean. Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): keep the host nav slot from blocking session-list scroll R1-4: the portal-scope/theme wrapper added around the host navigation slot generates a block box, so in a bounded flex-column host sidebar it becomes the flex item and its default min-height:auto pins the nav to content height — the nav's overflow-y-auto never engages and the list is clipped. Make the wrapper display:contents: it stays in the DOM (portal-scope attributes and synced theme variables still apply and inherit) but generates no box, so the nav remains the host's flex item and scrolls within the bounded slot. Witness: browser A/B on this head — bounded 300px flex slot, 40 sessions: navClientHeight 300 / navScrollHeight 1516 (scrollable), last row reachable; the slot test asserts display:contents (red without it). Theme-sync suite 20/20. * ci(web-shell): gate the managed host bundle in its own lane, not every build Resolves the maintainer-call item on this PR. The managed-host mode externalizes nothing, so riding `npm run build` charged every local build and every fresh-clone `prepare` with a full bundle of React, xterm, mermaid, echarts, shiki and codemirror — measured locally at about 13-14s and 38 MB — for an artifact that stays out of `exports`/`files` and has no in-repo consumer. The pass bought rot detection and nothing else. Rot detection now lives in a dedicated workflow that runs the mode on pull requests and post-merge pushes touching the entry, its build config or the lockfile. It installs with QWEN_SKIP_PREPARE because the entry needs no sibling workspace dist/, so the lane costs one bundling pass rather than a monorepo rebuild first. It runs on an ephemeral hosted runner with a read-only token and no secrets, and produces no artifact. A green exit code alone would not catch the rot, so the lane asserts a non-empty entry containing the mount symbol and the scoped empty-state CSS, and checks the output stays outside the directory the app build empties and out of the published exports/files. The local entry point is now `npm run build:managed-host`. A workflow meta-test pins both halves of the split: putting the mode back into the default build fails it, and so does dropping the build config from the lane's path filter. Both were confirmed by mutation. The .ci-budget entry is added for the new PR-stage workflow. Verified locally: actionlint and yamllint clean, CI budget holds, the new meta-test 9/9, package-scripts and ci-platform-lanes 134 passed, the all-workflow expression-length assertion passes with the new file, the five affected web-shell suites 212/212, and `npm run build:managed-host` succeeds with the lane's verify assertions holding. Co-authored-by: Qwen-Coder <[email protected]> * ci(web-shell): build core before the managed host bundle The managed host entry imports one compiled core module, and the package `exports` map resolves it through `dist/src/`, which `QWEN_SKIP_PREPARE=1` never produces. The lane failed to resolve it before Rollup had anything to bundle. Build that single package instead of running the full `prepare`, and record the new workflow in the size baseline so the ratchet tracks it. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw0ykbnlbb * ci(web-shell): build the SDK dist the managed host entry imports The managed-host lane installs with QWEN_SKIP_PREPARE=1, so no workspace dist exists except the ones the lane builds explicitly. managed-host mode externalizes nothing (`vite.lib.config.ts` returns false), and the entry's module graph reaches a runtime value import of `@qwen-code/sdk/daemon`, whose exports map points into `packages/sdk-typescript/dist/`. Building only core therefore left the bundle step red on this head: [vite]: Rollup failed to resolve import "@qwen-code/sdk/daemon" from packages/web-shell/client/components/messages/tools/ToolDetailsContext.tsx Build `@qwen-code/sdk` after core (its `tsconfig.reference.json` only references `../core`, so the existing step already satisfies it) and pin the new step in the lane's workflow test. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw19a4jbbr * ci(web-shell): build acp-bridge before the SDK in the managed host lane The lane installs with QWEN_SKIP_PREPARE=1 and built only core, then the SDK. The SDK's daemon sources import `@qwen-code/acp-bridge/<subpath>`, which resolves through that package's `dist/` exports map, and `tsconfig.reference.json` references core alone, so `tsc --build` never produces acp-bridge: the lane died on 94 TS2307/TS2339 errors before it bundled anything. Build it between core and the SDK, the order live-host-release.yml already uses. Reproduced both arms locally: without acp-bridge/dist the SDK build fails with the same 94 errors as run 41732515178; with it, the build exits 0. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw1bfabvbu * fix(web-shell): close the managed host R1 criticals R1-2: the identity-switch branch dropped an explicit host sessionId when the provider arrived after mount, because `selection.storageKey` started as undefined. Guard on a previous identity existing; a real storageKey change still drops the carried id. R1-1: a wrapper that mounted in a commit the sync effect did not watch kept no theme at all. The ref callback now themes the element it just saw, and the effect discovers our own wrappers by their portal-scope marker instead of a registry that dropped a detached wrapper for good. `style` joins the effect deps so a host token change re-copies without a theme flip. R1-3: the page-wrapper focus fallback parked focus where the discard latch did not recognise it. The latch now accepts the node the discard parked on instead of the page wrapper wholesale. Tests: late-mount + style-only token arms and the late-provider sessionId arm in ManagedAgentWebShell.test.tsx (22/22 green). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw1bfabvbu * fix(web-shell): keep the managed empty marker after a failed pending restore A failed pending restore leaves `busy` set until the user discards, so the new `!busy` term dropped `data-managed-empty` in a state the base marker still covered, losing the centring and overflow rule this PR adds. The shell root ref is now a stable callback so a wrapper mounting in a commit the sync effect does not watch is still themed. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw1jzxglc6 * fix(web-shell): drop a carried session id across a provider-less identity switch Closes the R4 Critical and three review items that made this PR's own guarantees unverifiable. R4-1: an identity switch routed through a provider-less render handed the previous tenant's session to the next one. The provider-less render wrote `storageKey: undefined`, so the next real provider compared against undefined and kept the carried id: A -> none -> C mounted C's page on A's `s1`. The direct A -> C transition dropped it, so this was the one path around the drop-on-scope-change rule the design doc requires. The selection now retains the last real identity while no provider is mounted and compares against it. Both the branch condition and the drop predicate are gated on a provider being present — the review's suggested shape gated only the predicate, which I confirmed loops with "Too many re-renders" because the retained key keeps the condition permanently true. R2-3: the lane's scoped-CSS assertion could not fail. `data-managed-empty` is a JSX prop name that survives minification as an object key, so a stopped injection guard still passed it. It now keys on the injection preamble (`__qwenWebShellCss`, the per-entry `managed-host` guard) and the portal scope selector. Witness: renaming the entry in the plugin's guard produced a build that exits 0 with zero styles — the old grep passed, the new step fails. R3-3: the meta-test's no-secrets guard only matched dot access, so `secrets['TOKEN']` kept it green. It now covers bracket form too, still without matching the prose in the security comment. R3-1: the test named for building every workspace dist pinned only the SDK step. Core and acp-bridge are equally load-bearing, so all three are pinned along with their required order. Deleting the core step now reds it. R1-9: the README example imported the pre-move `./managed-host/index.js`, which the current flat output no longer produces. The stale comment claiming the entry needs no sibling workspace dist is corrected: the lane builds core, acp-bridge and the SDK precisely because it resolves through them. Verified: the five affected web-shell suites 216/216, the workflow meta-test 10/10, actionlint and yamllint clean, CI budget holds, eslint exit 0, prettier clean, and the lane's verify step run verbatim against a freshly built bundle. Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): make the managed-host bundle assertions able to fail R2-3: the portal wrapper ships in its own chunk, and every occurrence of `data-web-shell-portal-root` in the entry sits inside the injected stylesheet, so the entry grep passed on a bundle whose wrapper was deleted. Search the whole output for the quoted JSX prop instead, which the scoped CSS cannot produce. R5-1: a job-level `permissions:` block replaces the workflow default, so pin the job's effective scope rather than the workflow-level block. R5-2: match any expression that reads `secrets`; `${{ toJSON(secrets) }}` is as much a read as `secrets.TOKEN`. The workflow's own `${{ github.* }}` expressions are the control against over-broadening the guard. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw29pur5d6 * fix(ci): trigger the managed-host lane on every input it builds from The lane's `paths` filters listed the entry, its manifest, two build configs, the lockfile and the workflow itself — none of the workspace sources the non-externalized bundle is built from. A PR touching `packages/core/src/**`, `packages/acp-bridge/src/**` or `packages/sdk-typescript/src/**` reported this lane `skipped`, so a break inside the bundle surfaced only when a host loaded it. `packages/web-shell/vite.config.ts` was missing the same way even though `vite.lib.config.ts` imports `WEB_SHELL_BUILD_TARGET` from it and uses it as the bundle's `target`. Both triggers now list those four inputs. The meta-test also pinned the list with `arrayContaining`, which let two of the six entries be deleted with the suite green; it now pins the exact set, so extending or dropping an entry is a reviewed change rather than a silent one. Addresses review thread R2-2 on #16200 and the same gap reported as the only finding by the deep verification onb84f2339. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw420o4bfo * fix(web-shell): keep the managed-host bundle out of lint and clean its outDir The managed-host pass writes `dist-managed-host/`, which `packages/**/dist/**` does not match, so a local build followed by `npm run lint` linted the 1.25 MB bundle as source. Ignore the directory and let the managed-host pass empty its own outDir so rebuilds stop accumulating previous hashed chunks. Also drop the unreachable same-session workspace fallback in the pending-owner button (`pendingElsewhere` implies `pending.sessionId !== sessionId`), and restore the broad pending-row guard in the host dom test. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw4plfohgl * fix(web-shell): close managed-host correctness gaps from review round 8 Four review findings describe defects in behavior this PR introduced; each lands with a regression test confirmed red without its fix. - readPending now validates the new workspaceId field like its siblings, so a corrupt stored record cannot hand a non-string to the host's workspace selection (R1-7). - The managed page wrapper is a non-interactive focus parking spot; the composer's global keydown gate now treats a marked focus region like body focus, restoring type-anywhere after a click inside the pane (R1-11). - A host that swaps its archived-status handler now receives the current status: the shell replays it when the replaced callback was stable, while churning inline callbacks never trigger a replay, preserving the bounded render behavior the frozen forwarded identity provides (R1-10). - Slot token sync removes custom properties the shell root no longer defines, so a withdrawn host token cannot stay pinned inline above the host's cascade (R2-6). Deferred to #16471 with recorded reasoning: mount-specific unavailable copy (R1-5), real-page render-prop and busy-marker test pinning (R1-13, R1-14), and a mutation trigger for host class-driven token changes (R3-2). * fix(web-shell): read a parked focus region from the container root The focus-region arm added for the managed page wrapper read document.activeElement, which a shadow-root mount retargets to the host, so clicking the pane parked focus on the wrapper while the gate still dropped every printable keystroke. Read the active element from the container's own root and keep the document.body arm untouched. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw58vvp6hc * fix(web-shell): scope a parked focus region to its own shell Two managed shells can share one shadow root and park focus on sibling `data-web-shell-focus-region` wrappers. The global keydown gate accepted the attribute alone, so shell A captured a keystroke aimed at shell B and inserted it into the wrong conversation's editor before the session submitted it. Require the parked region to contain this composer before treating it as body focus, and reshape both fixtures so the wrapper is an ancestor of the composer as in the real managed DOM, with a regression witness for the sibling case. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw5d677mhi * fix(web-shell): drop the unreachable provider-less identity arm Main's `baseUrl` guard makes a provider-less render unreachable, so the sessionId-carry-over arm that passed through one has no producer left to pin. Prettier also collapses the i18n import. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw69blwmis * fix(web-shell): read the parked focus node from its own root (C2) In shadow-DOM portal mode document.activeElement retargets to the shadow host, so the page's parked-focus comparisons to body/sessions nav/discard node were always false and the composer latch treated a click inside the pane as a real move. Resolve the active element from the page wrapper's own root, as App and the other focus users already do. * test(web-shell): pin the real page as the composer focus region (C1) The attribute plus tabIndex pair is what useComposerCore keys type-anywhere off; the existing assertions all build their own region. Render the real page and pin both; deleting either side reds this test. * fix(web-shell): watch the workspace manifests in the managed-host lane (C4) The lane builds core, acp-bridge and the SDK from source, so a change to those workspace manifests can break the bundle while the filter reports skipped. Add the three package.json entries to both triggers and to the exact-set pin. * chore(web-shell): raise the managed-host lane size baseline (C3) Measured 7051 bytes for web-shell-managed-host.yml after C4; the baseline still recorded 5672, so the size gate would fail on the next run. * test(web-shell): pin the archived slot branch and the baseUrl guard (C5, R4-1) C5: the single shared target hid the archived branch — deleting it still left one wrapper inside the element. Use two distinct targets and assert each gets exactly one themed wrapper. R4-1: patrol removed the identity arm that passed through a provider-less render because the baseUrl guard makes that render unreachable. Pin the guard itself: a provider-less render must show the config fallback and commit no page render, and the next provider must still render. Relaxing the guard (or defaulting baseUrl) reds this test, so a future refactor cannot silently bring the arm back. * fix(web-shell): treat an unfocused shadow root as unclaimed in the discard latch The discard focus latch read the parked node from the page wrapper's own root, so a shadow-root mount whose discard dropped focus to the outer document body produced ShadowRoot.activeElement === null. Every `ours` operand was false, the latch was dropped, and the composer that just got the draft back was never focused. Treat an unfocused shadow root as unclaimed, matching the body-focus operand. The latch still dies when the user moves on (ManagedSessionsPage.test.tsx pins that). Pinned by a new shadow-root regression: the page mounts into host.attachShadow({mode:'open'}) with a host-side navigationTarget, a pending record is discarded, and shadow.activeElement must be the composer. The test is red without the fix and green with it. Addresses review thread R13-1 on #16200. Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): resolve host-page focus before judging the discard latch unclaimed ShadowRoot.activeElement is null both when nothing inside the root is focused and when focus sits on a host-page element outside the root, so the shadow-aware read alone collapsed the two states and let the discard latch steal focus back from a user who had demonstrably moved on. Fall through to document.activeElement when the root reports null so the latch sees the real focus holder, and pin the host-toolbar case with a shadow-root regression test. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: yiliang114 <[email protected]>
203 lines
8.9 KiB
JavaScript
203 lines
8.9 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
// The managed-host mount entry is unpublished: it is absent from the package
|
|
// `exports` and `files`, and no consumer in the repo imports it. Its only
|
|
// protection against rot is this lane. It previously rode `npm run build`,
|
|
// which charged every local build and every fresh-clone `prepare` for a full
|
|
// non-externalized bundle; moving it here is only a fix if BOTH halves hold —
|
|
// the lane really runs the mode, and the default build really stopped doing so.
|
|
// These assertions pin that split, because either half silently regressing
|
|
// leaves the entry unprotected or the tax back in place with every suite green.
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { parse } from 'yaml';
|
|
|
|
const repoRoot = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'../..',
|
|
);
|
|
const workflowSource = readFileSync(
|
|
path.join(repoRoot, '.github/workflows/web-shell-managed-host.yml'),
|
|
'utf8',
|
|
);
|
|
const workflow = parse(workflowSource);
|
|
// `on:` parses as the boolean true in YAML 1.1.
|
|
const triggers = workflow[true] ?? workflow['on'];
|
|
const job = workflow.jobs.build;
|
|
const pkg = JSON.parse(
|
|
readFileSync(path.join(repoRoot, 'packages/web-shell/package.json'), 'utf8'),
|
|
);
|
|
const stepNamed = (name) =>
|
|
job.steps.find((candidate) => candidate.name === name);
|
|
|
|
describe('managed host build lane', () => {
|
|
it('runs on pull requests, post-merge pushes and manual dispatch', () => {
|
|
expect(Object.keys(triggers).sort()).toEqual([
|
|
'pull_request',
|
|
'push',
|
|
'workflow_dispatch',
|
|
]);
|
|
expect(triggers.push.branches).toEqual(['main']);
|
|
expect(triggers.pull_request.branches).toContain('main');
|
|
});
|
|
|
|
it('triggers on the entry, its build wiring and itself', () => {
|
|
// A path filter that misses the config or the entry would report "skipped"
|
|
// on the very changes that can break the mode — agreement without a run.
|
|
// The set is exact rather than `arrayContaining`: this lane builds core,
|
|
// acp-bridge and the SDK from source and reads `WEB_SHELL_BUILD_TARGET`
|
|
// from `vite.config.ts`, so a filter that omits any of them reports
|
|
// "skipped" on the change that breaks it. An exact set also means an entry
|
|
// cannot be dropped from the workflow with this suite still green.
|
|
const expectedPaths = [
|
|
'.github/workflows/web-shell-managed-host.yml',
|
|
'packages/acp-bridge/package.json',
|
|
'packages/acp-bridge/src/**',
|
|
'packages/core/package.json',
|
|
'packages/core/src/**',
|
|
'packages/sdk-typescript/package.json',
|
|
'packages/sdk-typescript/src/**',
|
|
'packages/web-shell/client/**',
|
|
'packages/web-shell/package.json',
|
|
'packages/web-shell/pierre-build.ts',
|
|
'packages/web-shell/vite.config.ts',
|
|
'packages/web-shell/vite.lib.config.ts',
|
|
'pnpm-lock.yaml',
|
|
];
|
|
for (const trigger of [triggers.pull_request, triggers.push]) {
|
|
expect(trigger.paths).toHaveLength(expectedPaths.length);
|
|
expect([...trigger.paths].sort()).toEqual(expectedPaths);
|
|
}
|
|
});
|
|
|
|
it('builds untrusted PR code with a read-only token and no secrets', () => {
|
|
// A job-level `permissions:` block replaces the workflow default, so the
|
|
// job's effective scope is the one the untrusted build actually runs with.
|
|
expect(job.permissions ?? workflow.permissions).toEqual({
|
|
contents: 'read',
|
|
});
|
|
expect(workflowSource).not.toContain('pull_request_target');
|
|
// Match any expression that reads the context: `${{ toJSON(secrets) }}` is
|
|
// as much a read as `secrets.TOKEN`. The conflict check below is the
|
|
// control — the workflow's own `${{ github.* }}` expressions, which a guard
|
|
// broadened to every expression would red on instead of passing silently.
|
|
const secretRef = /\$\{\{[^}]*\bsecrets\b/;
|
|
expect(workflowSource).not.toMatch(secretRef);
|
|
expect(workflowSource).toMatch(/\$\{\{\s*github\./);
|
|
expect(job['runs-on']).toBe('ubuntu-latest');
|
|
expect(job.if).toContain("github.repository == 'QwenLM/qwen-code'");
|
|
const checkout = job.steps.find((step) =>
|
|
String(step.uses ?? '').startsWith('actions/checkout@'),
|
|
);
|
|
expect(checkout.with['persist-credentials']).toBe(false);
|
|
});
|
|
|
|
it('bounds the job and never cancels a post-merge witness', () => {
|
|
expect(job['timeout-minutes']).toBeGreaterThan(0);
|
|
expect(workflow.concurrency['cancel-in-progress']).toBe(
|
|
"${{ github.event_name == 'pull_request' }}",
|
|
);
|
|
});
|
|
|
|
it('builds the managed-host mode without the monorepo prepare build', () => {
|
|
expect(stepNamed('Build the managed host entry').run).toBe(
|
|
'npm run build:managed-host --workspace=@qwen-code/web-shell',
|
|
);
|
|
expect(pkg.scripts['build:managed-host']).toBe(
|
|
'vite build --config vite.lib.config.ts --mode managed-host',
|
|
);
|
|
// `prepare` would rebuild every workspace; the three targeted steps below
|
|
// build only the dists this entry actually resolves through.
|
|
expect(stepNamed('Install dependencies').env.QWEN_SKIP_PREPARE).toBe('1');
|
|
});
|
|
|
|
it('builds every workspace dist the entry imports', () => {
|
|
// The client imports `@qwen-code/sdk/daemon`, whose `exports` points into
|
|
// that package's `dist/`, and the SDK in turn resolves core and acp-bridge
|
|
// through their own `dist/`. All three are load-bearing: dropping any one
|
|
// reds the lane on an unresolved import, so pin each rather than just the
|
|
// last. `tsconfig.reference.json` references only ../core, so `tsc --build`
|
|
// never produces acp-bridge — hence its own step and this fixed order.
|
|
expect(stepNamed('Build core for the managed host entry').run).toBe(
|
|
'corepack pnpm --filter @qwen-code/qwen-code-core build',
|
|
);
|
|
expect(stepNamed('Build acp-bridge for the SDK reference build').run).toBe(
|
|
'corepack pnpm --filter @qwen-code/acp-bridge build',
|
|
);
|
|
expect(stepNamed('Build the SDK for the managed host entry').run).toBe(
|
|
'corepack pnpm --filter @qwen-code/sdk build',
|
|
);
|
|
const buildOrder = job.steps
|
|
.map((step) => step.name)
|
|
.filter((name) => String(name).startsWith('Build '));
|
|
expect(buildOrder).toEqual([
|
|
'Build core for the managed host entry',
|
|
'Build acp-bridge for the SDK reference build',
|
|
'Build the SDK for the managed host entry',
|
|
'Build the managed host entry',
|
|
]);
|
|
});
|
|
|
|
it('asserts an emitted bundle rather than trusting a green exit code', () => {
|
|
const verify = stepNamed('Verify the host entry emitted a usable bundle');
|
|
expect(verify.run).toContain('dist-managed-host/index.js');
|
|
expect(verify.run).toContain('test -s');
|
|
expect(verify.run).toContain('renderManagedAgentWebShell');
|
|
// The CSS half must key on the injection preamble. A JSX prop name such as
|
|
// `data-managed-empty` survives minification as an object key even when the
|
|
// plugin's per-entry guard stops matching and no stylesheet is injected,
|
|
// so grepping it would pass on an unstyled bundle.
|
|
expect(verify.run).toContain('__qwenWebShellCss');
|
|
expect(verify.run).toContain(
|
|
'grep -qF \'data-qwen-web-shell-entry="managed-host"\'',
|
|
);
|
|
// The portal wrapper ships in its own chunk, and every occurrence in the
|
|
// entry sits inside the injected stylesheet, so the check must search the
|
|
// whole output for the quoted JSX prop a CSS scope cannot produce.
|
|
expect(verify.run).toContain(
|
|
`grep -rqF 'data-web-shell-portal-root":' 'packages/web-shell/dist-managed-host'`,
|
|
);
|
|
expect(verify.run).not.toContain("grep -q 'data-managed-empty'");
|
|
});
|
|
|
|
it('keeps the output outside the directory the app build empties', () => {
|
|
const verify = stepNamed(
|
|
'Verify the host output stays outside the app build directory',
|
|
);
|
|
expect(verify.run).toContain('packages/web-shell/dist/managed-host');
|
|
const config = readFileSync(
|
|
path.join(repoRoot, 'packages/web-shell/vite.lib.config.ts'),
|
|
'utf8',
|
|
);
|
|
expect(config).toContain(
|
|
"mode === 'managed-host' ? 'dist-managed-host' : 'dist'",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('managed host stays out of the default build and the package', () => {
|
|
it('does not charge the default build for the host bundle', () => {
|
|
expect(pkg.scripts.build).not.toContain('managed-host');
|
|
// The default passes it replaces must survive the split.
|
|
expect(pkg.scripts.build).toContain('--mode transcript');
|
|
expect(pkg.scripts.build).toContain('tsc -p tsconfig.lib.json');
|
|
});
|
|
|
|
it('keeps the entry unpublished so the lane is its only gate', () => {
|
|
const targets = Object.values(pkg.exports).flatMap((entry) =>
|
|
typeof entry === 'string' ? [entry] : Object.values(entry),
|
|
);
|
|
expect(targets.some((target) => target.includes('managed-host'))).toBe(
|
|
false,
|
|
);
|
|
expect(pkg.files).toContain('!dist/types/managed-host.d.ts');
|
|
});
|
|
});
|