test(ci): repair release validation gates (#108646)

* test(agents): harden Z.AI live output probe

* test(agents): classify live provider failures

* test(ci): strengthen package export contract

* chore(protocol): regenerate Swift session create params
This commit is contained in:
Peter Steinberger
2026-07-16 00:42:41 -07:00
committed by GitHub
parent 73ada8fd4c
commit b23292cc3f
3 changed files with 73 additions and 18 deletions
@@ -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",
+33 -11
View File
@@ -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", () => {
+10 -4
View File
@@ -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");