Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions packages/cli/src/serve/run-qwen-serve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
waitForRuntimeStartingForShutdown,
} from './run-qwen-serve.js';
import { isBrowserAutomationMcpAvailable } from './cdp-mcp-command.js';
import * as nativeDirectoryPicker from './native-directory-picker.js';
import { loadServeFastPathEnvironment } from './fast-path-settings.js';
import { loadEnvironment } from '../config/environment.js';
import { RUNTIME_STARTUP_CANCELLED_MESSAGE } from './runtime-startup-errors.js';
Expand Down Expand Up @@ -10615,6 +10616,61 @@ describe('runQwenServe runtime startup failures', () => {
}
});

it.each([true, false])(
'mirrors the native directory picker probe on the bootstrap envelopes (available: %s)',
async (available) => {
tmpDir = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'qws-bootstrap-picker-')),
);
// Keep the runtime from mounting so the bootstrap `/capabilities` and
// `/daemon/status` envelopes stay the ones being served.
vi.spyOn(acpBridge, 'createAcpSessionBridge').mockImplementation(() => {
throw new Error('runtime boom');
});
const probe = vi
.spyOn(nativeDirectoryPicker, 'isNativeDirectoryPickerAvailable')
.mockReturnValue(available);
const handle = await runQwenServe(
{
port: 0,
hostname: '127.0.0.1',
mode: 'http-bridge',
workspace: tmpDir,
maxSessions: 1,
serveWebShell: false,
},
{ resolveOnListen: true },
);
try {
await expect(handle.runtimeReady).rejects.toThrow('runtime boom');
const probeCallsAfterBoot = probe.mock.calls.length;
const capabilities = (await (
await fetch(`${handle.url}/capabilities`)
).json()) as { features: string[] };
const status = (await (
await fetch(`${handle.url}/daemon/status`)
).json()) as { capabilities: { features: string[] } };
if (available) {
expect(capabilities.features).toContain('native_directory_picker');
expect(status.capabilities.features).toContain(
'native_directory_picker',
);
} else {
expect(capabilities.features).not.toContain(
'native_directory_picker',
);
expect(status.capabilities.features).not.toContain(
'native_directory_picker',
);
}
// Probed once while the bootstrap app was built, not per request.
expect(probe.mock.calls.length).toBe(probeCallsAfterBoot);
} finally {
await handle.close();
}
},
);

it('shuts down a bridge when runtime mounting fails after bridge creation', async () => {
tmpDir = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'qws-runtime-partial-fail-')),
Expand Down
17 changes: 15 additions & 2 deletions packages/cli/src/serve/run-qwen-serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ import {
getServeProtocolVersions,
SERVE_CAPABILITY_REGISTRY,
} from './capabilities.js';
import { isNativeDirectoryPickerAvailable } from './native-directory-picker.js';
import {
EXTERNAL_TOOL_GUARD_PROVIDER_ATTACHED_VALUE,
EXTERNAL_TOOL_GUARD_REQUIRED_VALUE,
Expand Down Expand Up @@ -2289,6 +2290,7 @@ function currentServeFeaturesForRunQwenServe(
sessionArtifactsPersistenceAvailable: boolean,
currentSessionSchedulingAvailable: boolean,
env: Readonly<Record<string, string | undefined>>,
nativeDirectoryPickerAvailable: boolean,
): string[] {
return getAdvertisedServeFeatures(undefined, {
requireAuth: opts.requireAuth === true,
Expand All @@ -2315,8 +2317,10 @@ function currentServeFeaturesForRunQwenServe(
channelManagementAvailable: true,
persistentWorkspaceRegistrationAvailable: true,
workspaceRuntimeRemovalAvailable: true,
// Advertise the same WS feature flags as the runtime path (serve-features.ts)
// so the bootstrap `/capabilities` window doesn't briefly under-report them.
// Advertise the same host-conditional and WS feature flags as the runtime
// path (serve-features.ts) so the bootstrap `/capabilities` window doesn't
// briefly under-report them.
nativeDirectoryPickerAvailable,
clientMcpOverWsEnabled: opts.clientMcpOverWs === true,
cdpTunnelOverWsEnabled: opts.cdpTunnelOverWs === true,
browserAutomationMcpAvailable: isBrowserAutomationMcpAvailable(opts, env),
Expand All @@ -2332,6 +2336,7 @@ function createBootstrapCapabilities(input: {
currentSessionSchedulingAvailable: boolean;
permissionPolicy: PermissionPolicy | undefined;
env: Readonly<Record<string, string | undefined>>;
nativeDirectoryPickerAvailable: boolean;
}): CapabilitiesEnvelope {
return {
v: CAPABILITIES_SCHEMA_VERSION,
Expand All @@ -2346,6 +2351,7 @@ function createBootstrapCapabilities(input: {
input.sessionArtifactsPersistenceAvailable,
input.currentSessionSchedulingAvailable,
input.env,
input.nativeDirectoryPickerAvailable,
),
modelServices: [],
workspaceCwd: input.boundWorkspace,
Expand Down Expand Up @@ -2566,6 +2572,11 @@ function createBootstrapServeApp(input: {
onHealthServed,
} = input;
const app = express();
// The probe stats `/dev/console` (macOS) or scans `PATH` for `zenity`
// (Linux), and both bootstrap endpoints below rebuild their envelope per
// request, so evaluate it once here — the runtime path likewise probes once,
// at `createApp` time (server.ts).
const nativeDirectoryPickerAvailable = isNativeDirectoryPickerAvailable();

installSameOriginOriginStrip(app, getPort);
if (opts.allowOrigins && opts.allowOrigins.length > 0) {
Expand Down Expand Up @@ -2626,6 +2637,7 @@ function createBootstrapServeApp(input: {
currentSessionSchedulingAvailable,
permissionPolicy,
env: process.env,
nativeDirectoryPickerAvailable,
}),
);
});
Expand Down Expand Up @@ -2765,6 +2777,7 @@ function createBootstrapServeApp(input: {
sessionArtifactsPersistenceAvailable,
currentSessionSchedulingAvailable,
process.env,
nativeDirectoryPickerAvailable,
),
},
runtime: {
Expand Down
Loading