diff --git a/src/agents/anthropic-transport-stream.live.test.ts b/src/agents/anthropic-transport-stream.live.test.ts index ee3973335954..c52445e95e9e 100644 --- a/src/agents/anthropic-transport-stream.live.test.ts +++ b/src/agents/anthropic-transport-stream.live.test.ts @@ -9,6 +9,7 @@ import type { Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { createAnthropicMessagesTransportStreamFn } from "./anthropic-transport-stream.js"; import { isLiveTestEnabled } from "./live-test-helpers.js"; +import { shouldSkipLiveProviderDrift } from "./live-test-provider-drift.js"; import { isLiveBillingDrift } from "./live-test-provider-drift.test-support.js"; const LIVE = isLiveTestEnabled(["ANTHROPIC_TRANSPORT_LIVE_TEST"]); @@ -79,6 +80,23 @@ function skipAnthropicBillingDrift( return true; } +function classifyProviderError(errorMessage: string | undefined): string { + if (!errorMessage) { + return "none"; + } + return ( + shouldSkipLiveProviderDrift({ + allowAuth: true, + allowBilling: true, + allowModelNotFound: true, + allowProviderUnavailable: true, + allowRateLimit: true, + allowTimeout: true, + error: errorMessage, + })?.reason ?? "unclassified" + ); +} + describeLive("anthropic transport stream live", () => { it("cancels an in-flight SSE body read over a real HTTP stream", async () => { const controller = new AbortController(); @@ -217,7 +235,10 @@ describeOpusTupleLive("anthropic Opus tuple schema provider live", () => { (block) => block.type === "toolCall" && block.name === "tuple_probe", ); - expect(result.stopReason).toBe("toolUse"); + expect( + result.stopReason, + `tuple tool projection failed; errorClass=${classifyProviderError(result.errorMessage)}`, + ).toBe("toolUse"); expect(toolCall).toMatchObject({ type: "toolCall", name: "tuple_probe", @@ -282,7 +303,10 @@ describeProviderLive("anthropic transport stream provider live", () => { (block) => block.type === "toolCall" && block.name === "healthy_probe", ); - expect(result.stopReason).toBe("toolUse"); + expect( + result.stopReason, + `forced tool projection failed; errorClass=${classifyProviderError(result.errorMessage)}`, + ).toBe("toolUse"); expect(toolCall).toMatchObject({ type: "toolCall", name: "healthy_probe", @@ -344,7 +368,10 @@ describeProviderLive("anthropic transport stream provider live", () => { (block) => block.type === "toolCall" && block.name === "healthy_probe", ); - expect(result.stopReason).toBe("toolUse"); + expect( + result.stopReason, + `SDK forced tool projection failed; errorClass=${classifyProviderError(result.errorMessage)}`, + ).toBe("toolUse"); expect(toolCall).toMatchObject({ type: "toolCall", name: "healthy_probe", diff --git a/src/agents/zai.live.test.ts b/src/agents/zai.live.test.ts index 9c157939ec99..fe495ff3d7d8 100644 --- a/src/agents/zai.live.test.ts +++ b/src/agents/zai.live.test.ts @@ -8,6 +8,7 @@ import { extractNonEmptyAssistantText, isLiveTestEnabled, } from "./live-test-helpers.js"; +import { shouldSkipLiveProviderDrift } from "./live-test-provider-drift.js"; const ZAI_KEY = process.env.ZAI_API_KEY ?? process.env.Z_AI_API_KEY ?? ""; const LIVE = isLiveTestEnabled(["ZAI_LIVE_TEST"]); @@ -35,17 +36,38 @@ async function expectModelReturnsAssistantText( contextWindow: modelId === "glm-5.2" ? 1_000_000 : 202_800, maxTokens: modelId === "glm-5.2" ? 131_072 : 131_100, }; - // GLM-5 reasoning is enabled by default and consumes the same output budget. - // Keep enough headroom for a visible answer while retaining a bounded probe. - const res = await completeSimple( - model, - { - messages: createSingleUserPromptMessage(), - }, - { apiKey: ZAI_KEY, maxTokens: 1_024 }, - ); - const text = extractNonEmptyAssistantText(res.content); - expect(text.length).toBeGreaterThan(0); + const complete = (maxTokens: number) => + completeSimple( + model, + { + messages: createSingleUserPromptMessage(), + }, + { apiKey: ZAI_KEY, maxTokens }, + ); + + // A small probe cap can occasionally yield only hidden reasoning even though + // production allows much more output. Retry once, but still require visible text. + const initial = await complete(1_024); + let final = initial; + let text = extractNonEmptyAssistantText(final.content); + if (!text && (initial.stopReason === "stop" || initial.stopReason === "length")) { + final = await complete(8_192); + text = extractNonEmptyAssistantText(final.content); + } + const drift = shouldSkipLiveProviderDrift({ + allowAuth: true, + allowBilling: true, + allowModelNotFound: true, + allowProviderUnavailable: true, + allowRateLimit: true, + allowTimeout: true, + error: final.errorMessage ?? "", + }); + const errorClass = final.errorMessage ? (drift?.reason ?? "unclassified") : "none"; + expect( + text.length, + `${modelId} returned no assistant text; initialStopReason=${initial.stopReason}; finalStopReason=${final.stopReason}; errorClass=${errorClass}; contentTypes=${final.content.map((block) => block.type).join(",") || "none"}`, + ).toBeGreaterThan(0); } describeCodingLive("zai Coding Plan live", () => { diff --git a/test/scripts/parallels-smoke-model.test.ts b/test/scripts/parallels-smoke-model.test.ts index ed3d0c8ac4a7..43b576a75b51 100644 --- a/test/scripts/parallels-smoke-model.test.ts +++ b/test/scripts/parallels-smoke-model.test.ts @@ -421,10 +421,16 @@ describe("Parallels smoke model selection", () => { expect(common).toContain('export * from "./host-command.ts"'); expect(common).toContain('export * from "./lane-runner.ts"'); - // The deadcode hard-zero sweep narrowed this barrel to named exports; the - // shared contract is that package helpers stay routed through common. - expect(common).toContain('} from "./package-artifact.ts"'); - expect(common).toContain("packOpenClaw"); + const packageArtifactExports = new Set( + (common.match(/export \{([^}]*)\} from "\.\/package-artifact\.ts";/)?.[1] ?? "") + .split(",") + .map((name) => name.trim()) + .filter(Boolean), + ); + expect(packageArtifactExports).toContain("packOpenClaw"); + expect(packageArtifactExports).toContain("packageVersionFromTgz"); + expect(packageArtifactExports).toContain("resolveOpenClawRegistryVersion"); + expect(common).not.toContain('export * from "./package-artifact.ts"'); expect(common).toContain('export * from "./parallels-vm.ts"'); expect(common).toContain('export * from "./snapshots.ts"'); expect(hostCommand).toContain("export function shellQuote");