scripts/native-bridge-smoke.mjs
Rama master · réplica de lectura del repositorio original.
#!/usr/bin/env node
import { closeSync, existsSync, openSync, readSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
const baseUrl = (process.env.BASE_URL || process.env.WINDSURFAPI_BASE_URL || 'http://127.0.0.1:3003').replace(/\/+$/, '');
const apiKey = process.env.API_KEY || process.env.WINDSURFAPI_API_KEY || '';
const model = process.env.MODEL || process.env.WINDSURFAPI_SMOKE_MODEL || 'claude-sonnet-4.6';
const marker = `NATIVE_BRIDGE_SMOKE_${Date.now().toString(36)}`;
const streamEnabled = process.env.NATIVE_BRIDGE_SMOKE_STREAM !== '0';
const nonStreamEnabled = process.env.NATIVE_BRIDGE_SMOKE_NON_STREAM !== '0';
const noExitOnFailure = process.env.NATIVE_BRIDGE_SMOKE_NO_EXIT_ON_FAILURE === '1';
const requestTimeoutMs = Math.max(5_000, Number(process.env.NATIVE_BRIDGE_SMOKE_TIMEOUT_MS || 120_000));
const streamEarlyTool = process.env.NATIVE_BRIDGE_SMOKE_EARLY_TOOL !== '0';
const includeEnv = process.env.NATIVE_BRIDGE_SMOKE_ENV !== '0';
const includeHealth = process.env.NATIVE_BRIDGE_SMOKE_HEALTH !== '0';
const requireNativeBridgeTool = process.env.NATIVE_BRIDGE_SMOKE_REQUIRE_NATIVE !== '0';
const validateToolArgs = process.env.NATIVE_BRIDGE_SMOKE_VALIDATE_ARGS !== '0';
const enforceLsBudget = process.env.NATIVE_BRIDGE_SMOKE_LS_BUDGET !== '0';
const requireNativeBridgeEnabled = process.env.NATIVE_BRIDGE_SMOKE_REQUIRE_BRIDGE_ENABLED !== '0';
const includeProtoTraceSummary = process.env.NATIVE_BRIDGE_SMOKE_PROTO_TRACE_SUMMARY !== '0';
const protoTraceDir = process.env.NATIVE_BRIDGE_SMOKE_PROTO_TRACE_DIR
|| process.env.WINDSURFAPI_PROTO_TRACE_DIR
|| '/data/proto-trace';
async function sha256Hex(text) {
const bytes = new TextEncoder().encode(String(text || ''));
const digest = await crypto.subtle.digest('SHA-256', bytes);
return [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, '0')).join('');
}
const defaultWorkspaceId = apiKey ? (await sha256Hex(apiKey)).slice(0, 16) : '';
const defaultSmokeCwd = defaultWorkspaceId
? `/home/user/projects/workspace-${defaultWorkspaceId}`
: '/tmp/windsurf-workspace';
const smokeCwd = process.env.NATIVE_BRIDGE_SMOKE_CWD || defaultSmokeCwd;
const smokeFile = process.env.NATIVE_BRIDGE_SMOKE_FILE || `${smokeCwd.replace(/\/+$/, '')}/README.md`;
const requestedScenarios = String(process.env.NATIVE_BRIDGE_SMOKE_TOOLS || 'Bash')
.split(',')
.map(s => s.trim())
.filter(Boolean);
const webFetchPreflightEnvVars = [
'WINDSURFAPI_NATIVE_TOOL_BRIDGE_WEBFETCH_AUTO_APPROVE',
'WINDSURFAPI_NATIVE_TOOL_BRIDGE_WEBFETCH_AUTO_APPROVE_ORIGINS',
'WINDSURFAPI_NATIVE_TOOL_BRIDGE_POLL_AFTER_TOOL',
];
if (!apiKey) {
console.error('API_KEY is required. Enable native bridge with narrow gates before this smoke.');
process.exit(2);
}
function truncate(text, max = 1200) {
const s = String(text || '');
return s.length > max ? `${s.slice(0, max)}...<truncated ${s.length - max} chars>` : s;
}
function compactText(text, max = 1200) {
return truncate(String(text || '').replace(/\s+/g, ' ').trim(), max);
}
function smokeError(message, diagnostic = null) {
const err = new Error(message);
if (diagnostic) err.diagnostic = diagnostic;
return err;
}
function resultFromError(error) {
const out = { ok: false, error: String(error?.message || error) };
if (error?.diagnostic) out.diagnostic = error.diagnostic;
return out;
}
function fnTool(name, properties, required = []) {
return {
type: 'function',
function: {
name,
description: `${name} native bridge smoke tool.`,
parameters: {
type: 'object',
properties,
required,
additionalProperties: false,
},
},
};
}
const TOOL = {
Read: fnTool('Read', {
file_path: { type: 'string' },
offset: { type: 'number' },
limit: { type: 'number' },
}, ['file_path']),
Bash: fnTool('Bash', {
command: { type: 'string' },
cwd: { type: 'string' },
}, ['command']),
Grep: fnTool('Grep', {
pattern: { type: 'string' },
path: { type: 'string' },
glob: { type: 'string' },
output_mode: { type: 'string' },
}, ['pattern']),
Glob: fnTool('Glob', {
pattern: { type: 'string' },
path: { type: 'string' },
}, ['pattern']),
WebSearch: fnTool('WebSearch', {
query: { type: 'string' },
domains: { type: 'array', items: { type: 'string' } },
}, ['query']),
WebFetch: fnTool('WebFetch', {
url: { type: 'string' },
}, ['url']),
};
const SCENARIOS = {
Read: {
tools: [TOOL.Read],
choice: 'Read',
prompt: `Use the Read tool exactly once for ${smokeFile} with limit 20. Marker: ${marker}. Do not answer in prose.`,
expectArgs: 'file_path ending in README.md',
validateArgs(args) {
const filePath = String(args.file_path || args.path || '');
return /(^|[/\\])README\.md$/i.test(filePath);
},
},
Bash: {
tools: [TOOL.Bash],
choice: 'Bash',
prompt: `Use the Bash tool exactly once with command: printf ${marker}. Do not answer in prose.`,
expectArgs: `command containing ${marker}`,
validateArgs(args) {
const command = String(args.command || args.command_line || '');
return command.includes(marker);
},
},
Grep: {
tools: [TOOL.Grep],
choice: 'Grep',
prompt: `Use the Grep tool exactly once with pattern "Proxy workspace placeholder", path ${smokeCwd}, glob README.md, and output_mode files_with_matches. Marker: ${marker}. Do not answer in prose.`,
expectArgs: 'pattern "Proxy workspace placeholder"',
validateArgs(args) {
return String(args.pattern || '').trim() === 'Proxy workspace placeholder';
},
},
Glob: {
tools: [TOOL.Glob],
choice: 'Glob',
prompt: `Use the Glob tool exactly once with pattern README.md and path ${smokeCwd}. Marker: ${marker}. Do not answer in prose.`,
expectArgs: 'pattern exactly README.md',
validateArgs(args) {
return String(args.pattern || '').trim() === 'README.md';
},
},
mixed: {
tools: [TOOL.Read, TOOL.Bash, TOOL.Grep, TOOL.Glob],
choice: null,
prompt: `Choose exactly one appropriate tool from Read, Bash, Grep, Glob to inspect ${smokeFile} for ${marker}. Do not answer in prose.`,
},
WebSearch: {
tools: [TOOL.WebSearch],
choice: 'WebSearch',
prompt: `Use the WebSearch tool exactly once with query "WindsurfAPI native bridge ${marker}". Do not answer in prose.`,
expectArgs: `query containing ${marker}`,
validateArgs(args) {
return String(args.query || '').includes(marker);
},
},
WebFetch: {
tools: [TOOL.WebFetch],
choice: 'WebFetch',
prompt: `Use the WebFetch tool exactly once with url https://example.com/. Marker: ${marker}. Do not answer in prose.`,
expectArgs: 'url exactly https://example.com/',
validateArgs(args) {
return String(args.url || '').trim() === 'https://example.com/';
},
},
};
function expandScenarios(names) {
const out = [];
for (const name of names) {
if (name === 'all') out.push('Read', 'Bash', 'Grep', 'Glob', 'mixed');
else out.push(name);
}
return [...new Set(out)].filter(name => SCENARIOS[name]);
}
function requestBody(scenario, stream) {
const messages = [];
if (includeEnv) {
messages.push({
role: 'system',
content: [
'# Environment',
`- Working directory: ${smokeCwd}`,
'- Is directory a git repo: false',
'- Platform: linux',
].join('\n'),
});
}
messages.push({ role: 'user', content: scenario.prompt });
const body = {
model,
stream,
messages,
tools: scenario.tools,
max_tokens: 512,
};
if (scenario.choice) body.tool_choice = { type: 'function', function: { name: scenario.choice } };
return body;
}
async function post(body, { streamEarlyTool = false, expectedTool = '' } = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
let settledByEarlyTool = false;
let accumulated = '';
let res;
try {
res = await fetch(`${baseUrl}/v1/chat/completions`, {
method: 'POST',
signal: controller.signal,
headers: {
authorization: `Bearer ${apiKey}`,
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
if (!streamEarlyTool || !res.body) {
const text = await res.text();
return {
status: res.status,
text,
earlyTool: false,
seenDone: /(?:^|\n)data:\s*\[DONE\]/.test(text),
};
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
const streamCalls = [];
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
accumulated += chunk;
assertNoNativeXml(accumulated, 'stream');
streamCalls.splice(0, streamCalls.length, ...collectStreamToolCalls(accumulated));
const names = namesFromCalls(streamCalls);
if (names.length && (!expectedTool || names.includes(expectedTool))) {
settledByEarlyTool = true;
await reader.cancel().catch(() => {});
controller.abort();
break;
}
}
accumulated += decoder.decode();
return { status: res.status, text: accumulated, earlyTool: settledByEarlyTool, seenDone: /(?:^|\n)data:\s*\[DONE\]/.test(accumulated) };
} catch (error) {
if (error?.name === 'AbortError') {
if (settledByEarlyTool) return { status: res?.status || 0, text: accumulated, earlyTool: true, seenDone: false };
throw new Error(`request timed out after ${requestTimeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timer);
}
}
function assertNoNativeXml(text, label) {
if (/<\/?function_calls\b|<invoke\b/i.test(text)) {
throw new Error(`${label}: provider-native function XML leaked to the client`);
}
}
function parseSse(text) {
const frames = [];
for (const frame of text.split('\n\n')) {
const lines = frame.split('\n').filter(Boolean);
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const payload = line.slice(6);
if (payload === '[DONE]') continue;
try { frames.push(JSON.parse(payload)); } catch {}
}
}
return frames;
}
function streamDiagnostics(text, calls = []) {
const frames = parseSse(text);
const contents = [];
const reasoning = [];
const finishReasons = [];
const usageFrames = [];
const responseIds = [];
for (const frame of frames) {
if (frame?.id) responseIds.push(frame.id);
if (frame?.usage) usageFrames.push(frame.usage);
for (const choice of (frame?.choices || [])) {
if (choice?.finish_reason) finishReasons.push(choice.finish_reason);
if (choice?.delta?.content) contents.push(choice.delta.content);
if (choice?.delta?.reasoning_content) reasoning.push(choice.delta.reasoning_content);
if (choice?.message?.content) contents.push(choice.message.content);
}
}
return {
frameCount: frames.length,
responseIds: [...new Set(responseIds)].slice(0, 3),
finishReasons: [...new Set(finishReasons)],
toolCallNames: namesFromCalls(calls),
toolCallSources: calls.map(toolCallSource),
toolCallArguments: calls.map(toolCallArgumentPreview),
toolCallArgumentMeta: calls.map(toolCallArgumentMeta),
contentPreview: compactText(contents.join('')),
reasoningPreview: compactText(reasoning.join('')),
usage: usageFrames.at(-1) || null,
seenDone: /(?:^|\n)data:\s*\[DONE\]/.test(text),
rawPreview: compactText(text),
};
}
function nonStreamDiagnostics(json, rawText, calls = []) {
const choice = json?.choices?.[0] || {};
const message = choice.message || {};
return {
finishReason: choice.finish_reason || null,
toolCallNames: namesFromCalls(calls),
toolCallSources: calls.map(toolCallSource),
toolCallArguments: calls.map(toolCallArgumentPreview),
toolCallArgumentMeta: calls.map(toolCallArgumentMeta),
contentPreview: compactText(message.content || ''),
usage: json?.usage || null,
rawPreview: compactText(rawText),
};
}
function collectStreamToolCalls(text) {
return parseSse(text).flatMap(f => (f.choices || [])
.flatMap(choice => choice.delta?.tool_calls || []));
}
function namesFromCalls(calls) {
return calls.map(c => c.function?.name || c.name || '').filter(Boolean);
}
function toolCallSource(call) {
const id = String(call?.id || '');
if (id.startsWith('native:')) return 'cascade_native';
if (id.startsWith('call_native_')) return 'provider_xml';
if (id.startsWith('nlu_') || id.startsWith('nlu_retry_')) return 'nlu_recovery';
if (id.startsWith('call_')) return 'openai_tool_call';
return id ? 'unknown' : 'missing_id';
}
function isNativeBridgeToolCall(call) {
const source = toolCallSource(call);
return source === 'cascade_native' || source === 'provider_xml';
}
function matchingToolCalls(calls, expected) {
return (calls || []).filter(call => {
const name = call?.function?.name || call?.name || '';
return !expected || name === expected;
});
}
function parseToolCallArguments(call) {
const raw = call?.function?.arguments ?? call?.argumentsJson ?? call?.arguments ?? '';
if (raw && typeof raw === 'object') return raw;
if (typeof raw !== 'string') return {};
try {
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return { __parse_error: compactText(raw, 240) };
}
}
function toolCallArgumentPreview(call) {
const args = parseToolCallArguments(call);
const out = {};
for (const [key, value] of Object.entries(args).slice(0, 8)) {
out[key] = typeof value === 'string' ? compactText(value, 240) : value;
}
return out;
}
function toolCallArgumentMeta(call) {
const args = parseToolCallArguments(call);
const issues = [];
const fields = {};
for (const [key, value] of Object.entries(args).slice(0, 12)) {
if (typeof value !== 'string') {
fields[key] = { type: typeof value };
continue;
}
const trimmed = value.trim();
const meta = {
type: 'string',
length: value.length,
redactedWorkspacePath: trimmed === '<workspace>',
basename: (() => {
const normalized = trimmed.replace(/\\/g, '/').replace(/\/+$/, '');
return normalized.split('/').pop() || '';
})(),
};
fields[key] = meta;
if (meta.redactedWorkspacePath) {
issues.push({
field: key,
reason: 'redacted_workspace_path',
message: 'The proxy redacted an internal workspace path; this is protocol evidence, but not a client-usable local path.',
});
}
}
return { fields, issues };
}
function scenarioArgumentIssue(scenarioName, args) {
if (scenarioName !== 'Read') return null;
const filePath = String(args.file_path || args.path || '').trim();
if (filePath === '<workspace>') {
return 'Read returned the workspace redaction marker instead of a concrete README.md path';
}
if (filePath && !/(^|[/\\])README\.md$/i.test(filePath)) {
return `Read returned basename "${filePath.replace(/\\/g, '/').split('/').pop() || filePath}" instead of README.md`;
}
return null;
}
function assertExpectedTool(calls, scenarioName, scenario, diagnostic = null) {
const expected = scenario.choice || '';
const matches = matchingToolCalls(calls, expected);
const names = namesFromCalls(calls);
if (!names.length) throw smokeError(`${scenarioName}: produced no tool_calls`, diagnostic);
if (expected && !names.includes(expected)) {
throw smokeError(`${scenarioName}: expected ${expected}, got ${names.join(',')}`, diagnostic);
}
if (requireNativeBridgeTool && !matches.some(isNativeBridgeToolCall)) {
const sources = [...new Set(matches.map(toolCallSource))].join(',') || '(none)';
throw smokeError(`${scenarioName}: produced tool_calls but no native bridge tool_call (sources=${sources})`, diagnostic);
}
if (validateToolArgs && expected && typeof scenario.validateArgs === 'function') {
const eligible = requireNativeBridgeTool ? matches.filter(isNativeBridgeToolCall) : matches;
const ok = eligible.some(call => scenario.validateArgs(parseToolCallArguments(call)));
if (!ok) {
const expectedArgs = scenario.expectArgs || 'expected smoke arguments';
const issues = eligible
.map(call => scenarioArgumentIssue(scenarioName, parseToolCallArguments(call)))
.filter(Boolean);
const enriched = diagnostic ? { ...diagnostic, argumentValidationIssues: issues } : { argumentValidationIssues: issues };
const suffix = issues.length ? `; ${issues.join('; ')}` : '';
throw smokeError(`${scenarioName}: native bridge tool_call arguments did not match (${expectedArgs})${suffix}`, enriched);
}
}
return names;
}
async function runNonStream(name, scenario) {
const res = await post(requestBody(scenario, false));
if (res.status !== 200) {
throw smokeError(`${name} non-stream HTTP ${res.status}: ${res.text.slice(0, 800)}`, {
status: res.status,
rawPreview: compactText(res.text),
});
}
assertNoNativeXml(res.text, `${name} non-stream`);
let json;
try {
json = JSON.parse(res.text);
} catch (error) {
throw smokeError(`${name} non-stream invalid JSON: ${error.message}`, {
status: res.status,
rawPreview: compactText(res.text),
});
}
const calls = json.choices?.[0]?.message?.tool_calls || [];
const diagnostic = nonStreamDiagnostics(json, res.text, calls);
return { toolCalls: calls.length, names: assertExpectedTool(calls, name, scenario, diagnostic), diagnostic };
}
async function runStream(name, scenario) {
const res = await post(requestBody(scenario, true), {
streamEarlyTool,
expectedTool: scenario.choice || '',
});
if (res.status !== 200) {
throw smokeError(`${name} stream HTTP ${res.status}: ${res.text.slice(0, 800)}`, {
status: res.status,
rawPreview: compactText(res.text),
});
}
assertNoNativeXml(res.text, `${name} stream`);
const calls = collectStreamToolCalls(res.text);
const diagnostic = streamDiagnostics(res.text, calls);
return {
toolCalls: calls.length,
names: assertExpectedTool(calls, name, scenario, diagnostic),
earlyTool: res.earlyTool,
seenDone: !!res.seenDone,
diagnostic,
};
}
function summarizeLsPool(lsPool) {
if (!lsPool || typeof lsPool !== 'object') return null;
const pool = lsPool.pool || {};
const guard = pool.memoryGuard || lsPool.memoryGuard || {};
return {
running: !!lsPool.running,
maxInstances: lsPool.maxInstances,
totalRssBytes: lsPool.totalRssBytes,
pool: {
size: pool.size,
effectiveOccupancy: pool.effectiveOccupancy,
pending: pool.pending,
reservedPendingStarts: pool.reservedPendingStarts,
activeRequests: pool.activeRequests,
maintenanceRequests: pool.maintenanceRequests,
nonDefaultInstances: pool.nonDefaultInstances,
canStartNewNonDefault: pool.canStartNewNonDefault,
blockReason: pool.blockReason,
},
memoryGuard: {
enabled: guard.enabled,
availableBytes: guard.availableBytes,
minAvailableBytes: guard.minAvailableBytes,
reservedStarts: guard.reservedStarts,
okToSpawn: guard.okToSpawn,
minAvailableBytesSource: guard.minAvailableBytesSource,
},
admissionStats: lsPool.admissionStats || null,
};
}
async function fetchHealthSnapshot(label) {
if (!includeHealth) return null;
try {
const res = await fetch(`${baseUrl}/health?verbose=1`, {
headers: { authorization: `Bearer ${apiKey}` },
});
const text = await res.text();
let json;
try { json = JSON.parse(text); } catch {
return { ok: false, label, status: res.status, error: 'health returned non-JSON', rawPreview: compactText(text) };
}
return {
ok: res.ok,
label,
status: res.status,
version: json.version,
commit: json.commit,
accounts: json.accounts,
nativeBridge: json.nativeBridge || null,
nativeBridgeConfig: json.nativeBridgeConfig || null,
lsPool: summarizeLsPool(json.lsPool),
};
} catch (error) {
return { ok: false, label, error: String(error?.message || error) };
}
}
function lsBudgetBlockReason(health) {
if (!enforceLsBudget || !includeHealth) return null;
if (!health) return 'health_unavailable';
if (health.ok === false) return `health_${health.status || 'failed'}`;
if (requireNativeBridgeEnabled) {
if (!health.nativeBridgeConfig) return 'native_bridge_config_unavailable';
const cfg = health.nativeBridgeConfig;
if (cfg.off) return 'native_bridge_off';
if (!String(cfg.mode || '').trim()) return 'native_bridge_not_enabled';
}
const pool = health.lsPool?.pool;
if (!pool || typeof pool !== 'object') return null;
const busy = [];
for (const key of ['activeRequests', 'maintenanceRequests', 'pending', 'reservedPendingStarts']) {
const value = Number(pool[key] || 0);
if (value > 0) busy.push(`${key}=${value}`);
}
if (busy.length) return `ls_busy:${busy.join(',')}`;
if (pool.canStartNewNonDefault === false) return `ls_capacity:${pool.blockReason || 'cannot_start_non_default'}`;
return null;
}
function assertLsBudgetAvailable(health) {
const reason = lsBudgetBlockReason(health);
if (!reason) return;
throw smokeError(`preflight: LS budget unavailable (${reason}); refusing to run native bridge smoke against production capacity`, {
reason,
lsPool: health?.lsPool || null,
});
}
function counterDelta(before = {}, after = {}) {
const keys = new Set([...Object.keys(before || {}), ...Object.keys(after || {})]);
const out = {};
for (const key of keys) {
const delta = Number(after?.[key] || 0) - Number(before?.[key] || 0);
if (delta) out[key] = delta;
}
return out;
}
function nativeBridgeDecisionDelta(before, after) {
const b = before?.nativeBridge || {};
const a = after?.nativeBridge || {};
const recent = Array.isArray(a.recentDecisions) ? a.recentDecisions.slice(-8) : [];
return {
decisions: Number(a.decisions || 0) - Number(b.decisions || 0),
enabledDecisions: Number(a.enabledDecisions || 0) - Number(b.enabledDecisions || 0),
disabledDecisions: Number(a.disabledDecisions || 0) - Number(b.disabledDecisions || 0),
reasons: counterDelta(b.decisionReasons || {}, a.decisionReasons || {}),
lastDecision: a.lastDecision || null,
recentDecisions: recent.map(d => ({
at: d.at,
enabled: !!d.enabled,
reason: d.reason || '',
modelKey: d.modelKey || '',
route: d.route || '',
mappedTools: d.mappedTools || [],
unmappedTools: d.unmappedTools || [],
toolChoiceFiltered: !!d.toolChoiceFiltered,
})),
};
}
function readTailText(file, maxBytes = 2 * 1024 * 1024) {
const stat = statSync(file);
const size = stat.size;
const start = Math.max(0, size - maxBytes);
const length = size - start;
const fd = openSync(file, 'r');
try {
const buf = Buffer.alloc(length);
readSync(fd, buf, 0, length, start);
return buf.toString('utf8');
} finally {
closeSync(fd);
}
}
function summarizeWebFetchTraceDir(dir = protoTraceDir) {
try {
if (!includeProtoTraceSummary) return null;
if (!dir || !existsSync(dir)) return { available: false, dir, reason: 'trace_dir_missing' };
const files = readdirSync(dir)
.filter(name => /GetCascadeTrajectorySteps.*\.jsonl$/i.test(name))
.map(name => {
const path = join(dir, name);
const stat = statSync(path);
return { name, path, mtimeMs: stat.mtimeMs, size: stat.size };
})
.sort((a, b) => b.mtimeMs - a.mtimeMs)
.slice(0, 6);
const stateCounts = {};
const errorClassifications = {};
const recent = [];
let records = 0;
let parseErrors = 0;
for (const file of files) {
const lines = readTailText(file.path).split('\n').filter(Boolean);
for (const line of lines) {
let rec;
try {
rec = JSON.parse(line);
} catch {
parseErrors++;
continue;
}
records++;
const steps = rec?.semantic?.steps || [];
for (const step of steps) {
const trace = step?.webFetchTrace;
if (!trace?.state) continue;
stateCounts[trace.state] = (stateCounts[trace.state] || 0) + 1;
for (const [key, value] of Object.entries(trace.errorClassifications || {})) {
if (value) errorClassifications[key] = (errorClassifications[key] || 0) + 1;
}
recent.push({
file: file.name,
method: rec.method || '',
direction: rec.direction || '',
stepIndex: step.index,
state: trace.state,
stepType: trace.stepType,
status: trace.status,
hasRequestedInteraction: !!trace.hasRequestedInteraction,
hasReadUrlOneof: !!trace.hasReadUrlOneof,
hasWebDocument: !!trace.hasWebDocument,
errorClassifications: trace.errorClassifications || {},
});
}
}
}
return {
available: true,
dir,
files: files.map(f => ({ name: f.name, size: f.size })),
records,
parseErrors,
stateCounts,
errorClassifications,
recent: recent.slice(-12),
};
} catch (error) {
return { available: false, dir, reason: 'trace_summary_failed', error: String(error?.message || error) };
}
}
function missingWebFetchPreflightEnv() {
return webFetchPreflightEnvVars.filter(name => !(name in process.env));
}
function webFetchTraceVerdict(summary) {
if (!summary?.available) {
return {
ok: false,
level: 'fail',
reason: summary?.reason || 'trace_unavailable',
message: `WebFetch canary proto trace unavailable (${summary?.reason || 'trace_unavailable'})`,
};
}
const stateCounts = summary.stateCounts || {};
if (Number(stateCounts.completed_web_document || 0) > 0) {
return {
ok: true,
level: 'pass',
reason: 'completed_web_document',
message: 'WebFetch canary reached completed document',
};
}
const recentErrors = (summary.recent || [])
.filter(item => item.state === 'error')
.map(item => ({
stepIndex: item.stepIndex,
status: item.status,
errorClassifications: item.errorClassifications || {},
}));
const errorClassifications = summary.errorClassifications || {};
const hasPermissionDenied = Number(errorClassifications.permissionDenied || 0) > 0
|| recentErrors.some(item => item.errorClassifications.permissionDenied);
const hasFailedPrecondition = Number(errorClassifications.failedPrecondition || 0) > 0
|| recentErrors.some(item => item.errorClassifications.failedPrecondition);
if (Number(stateCounts.error || 0) > 0 || hasPermissionDenied || hasFailedPrecondition) {
const classes = [
hasPermissionDenied ? 'permissionDenied' : '',
hasFailedPrecondition ? 'failedPrecondition' : '',
].filter(Boolean);
return {
ok: false,
level: 'fail',
reason: classes.length ? `error:${classes.join(',')}` : 'error',
message: `WebFetch canary failed in proto trace (${classes.join(',') || 'error'})`,
errorClassifications,
recentErrors,
};
}
const nonZeroStates = Object.entries(stateCounts)
.filter(([, count]) => Number(count || 0) > 0)
.map(([state]) => state);
const onlyPendingOrDecision = nonZeroStates.length > 0
&& nonZeroStates.every(state => state === 'pending_permission' || state === 'auto_run_decision_only');
if (onlyPendingOrDecision) {
return {
ok: false,
level: 'warn',
reason: 'pending_or_auto_run_decision_only',
message: 'WebFetch canary did not reach completed document',
};
}
return {
ok: false,
level: 'fail',
reason: nonZeroStates.length ? `unexpected_states:${nonZeroStates.join(',')}` : 'no_webfetch_trace_states',
message: nonZeroStates.length
? `WebFetch canary did not reach completed document (states=${nonZeroStates.join(',')})`
: 'WebFetch canary produced no WebFetch proto trace states',
};
}
const selected = expandScenarios(requestedScenarios);
if (!selected.length) {
console.error(`No valid scenarios selected. Use one or more of: ${Object.keys(SCENARIOS).join(',')},all`);
process.exit(2);
}
const results = {};
const failures = [];
const warnings = [];
if (selected.includes('WebFetch')) {
const missingEnv = missingWebFetchPreflightEnv();
if (missingEnv.length) {
warnings.push(`WARN: WebFetch preflight missing env vars: ${missingEnv.join(', ')}`);
}
}
const healthBefore = await fetchHealthSnapshot('before');
try {
assertLsBudgetAvailable(healthBefore);
} catch (error) {
failures.push(String(error?.message || error));
results.preflight = resultFromError(error);
}
if (!failures.length) {
for (const name of selected) {
const scenario = SCENARIOS[name];
results[name] = {};
if (nonStreamEnabled) {
try {
results[name].nonStream = await runNonStream(name, scenario);
} catch (error) {
results[name].nonStream = resultFromError(error);
failures.push(`${name} non-stream: ${String(error?.message || error)}`);
}
}
if (streamEnabled) {
try {
results[name].stream = await runStream(name, scenario);
} catch (error) {
results[name].stream = resultFromError(error);
failures.push(`${name} stream: ${String(error?.message || error)}`);
}
}
}
}
const healthAfter = await fetchHealthSnapshot('after');
const protoTraceSummary = summarizeWebFetchTraceDir();
const webFetchTrace = selected.includes('WebFetch') ? webFetchTraceVerdict(protoTraceSummary) : null;
if (webFetchTrace) {
results.WebFetch = results.WebFetch || {};
results.WebFetch.protoTrace = webFetchTrace;
if (webFetchTrace.level === 'warn') {
warnings.push(`WARN: ${webFetchTrace.message}`);
}
if (!webFetchTrace.ok) {
failures.push(webFetchTrace.message);
}
}
console.log(JSON.stringify({
ok: failures.length === 0,
baseUrl,
model,
marker,
timeoutMs: requestTimeoutMs,
smokeCwd,
smokeFile,
includeEnv,
includeHealth,
enforceLsBudget,
requireNativeBridgeEnabled,
requireNativeBridgeTool,
validateToolArgs,
streamEarlyTool,
scenarios: selected,
results,
warnings,
failures,
nativeBridgeDecisionDelta: nativeBridgeDecisionDelta(healthBefore, healthAfter),
protoTraceSummary,
healthBefore,
healthAfter,
}, null, 2));
if (failures.length && !noExitOnFailure) process.exit(1);