Skip to content

Commit b04c938

Browse files
committed
fix(ci): harden full release live checks
1 parent 43fa40a commit b04c938

11 files changed

Lines changed: 321 additions & 47 deletions

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ RUN printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
130130
cp /tmp/pnpm-workspace.runtime.yaml pnpm-workspace.yaml && \
131131
CI=true NPM_CONFIG_FROZEN_LOCKFILE=false pnpm prune --prod && \
132132
node scripts/postinstall-bundled-plugins.mjs && \
133-
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete
133+
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
134+
node scripts/check-package-dist-imports.mjs /app
134135

135136
# ── Runtime base image ──────────────────────────────────────────
136137
FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-runtime

extensions/video-generation-providers.live.test.ts

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,7 @@ import {
1818
getShellEnvAppliedKeys,
1919
isLiveProfileKeyModeEnabled,
2020
isLiveTestEnabled,
21-
isModelNotFoundErrorMessage,
2221
isTruthyEnvValue,
23-
isAuthErrorMessage,
24-
isBillingErrorMessage,
25-
isOverloadedErrorMessage,
26-
isServerErrorMessage,
27-
isTimeoutErrorMessage,
2822
normalizeVideoGenerationDuration,
2923
parseCsvFilter,
3024
parseProviderModelMap,
@@ -42,6 +36,7 @@ import type {
4236
VideoGenerationRequest,
4337
} from "openclaw/plugin-sdk/test-env";
4438
import { describe, expect, it } from "vitest";
39+
import { resolveLiveVideoSkipReason } from "../test/helpers/media-generation/live-video-skip-reason.js";
4540
import alibabaPlugin from "./alibaba/index.js";
4641
import byteplusPlugin from "./byteplus/index.js";
4742
import deepinfraPlugin from "./deepinfra/index.js";
@@ -77,7 +72,7 @@ const LIVE_VIDEO_OPERATION_TIMEOUT_MS = readPositiveIntegerEnv(
7772
const LIVE_VIDEO_TEST_TIMEOUT_MS =
7873
(RUN_FULL_VIDEO_MODES ? 3 : 1) * LIVE_VIDEO_OPERATION_TIMEOUT_MS + 30_000;
7974
const LIVE_VIDEO_SMOKE_PROMPT =
80-
"A one-second low-motion video of a lobster walking across wet sand, no text.";
75+
"A one-second low-motion video of a blue cube sliding across a clean studio floor.";
8176

8277
type LiveProviderCase = {
8378
plugin: Parameters<typeof registerProviderPlugin>[0]["plugin"];
@@ -230,39 +225,6 @@ function buildLiveCapabilityOverrides(params: {
230225
};
231226
}
232227

233-
function resolveLiveVideoSkipReason(message: string): string | null {
234-
if (isAuthErrorMessage(message)) {
235-
return "auth drift";
236-
}
237-
if (isModelNotFoundErrorMessage(message)) {
238-
return "model drift";
239-
}
240-
if (isBillingErrorMessage(message)) {
241-
return "billing drift";
242-
}
243-
if (
244-
isTimeoutErrorMessage(message) ||
245-
/did not finish in time/i.test(message) ||
246-
/last status:\s*in_progress/i.test(message)
247-
) {
248-
return "provider timeout";
249-
}
250-
if (isOverloadedErrorMessage(message) || isServerErrorMessage(message)) {
251-
return "provider outage";
252-
}
253-
if (
254-
/HTTP\s+404/i.test(message) &&
255-
/Invalid URL/i.test(message) &&
256-
/\/platform\/video_gen/i.test(message)
257-
) {
258-
return "provider endpoint drift";
259-
}
260-
if (/access denied|not authorized|not enabled|permission denied/i.test(message)) {
261-
return "provider/model drift";
262-
}
263-
return null;
264-
}
265-
266228
async function runLiveVideoAttempt(params: {
267229
authLabel: string;
268230
attempted: string[];

scripts/check-openclaw-package-tarball.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { spawnSync } from "node:child_process";
66
import fs from "node:fs";
77
import { LOCAL_BUILD_METADATA_DIST_PATHS } from "./lib/local-build-metadata-paths.mjs";
8+
import { collectPackageDistImportErrors } from "./lib/package-dist-imports.mjs";
89

910
function usage() {
1011
return "Usage: node scripts/check-openclaw-package-tarball.mjs <openclaw.tgz>";
@@ -195,6 +196,13 @@ if (entrySet.has("dist/postinstall-inventory.json")) {
195196
}
196197
}
197198

199+
errors.push(
200+
...collectPackageDistImportErrors({
201+
files: normalized,
202+
readText: readTarEntry,
203+
}),
204+
);
205+
198206
if (errors.length > 0) {
199207
fail(`OpenClaw package tarball integrity failed:\n${errors.join("\n")}`);
200208
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env node
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import { collectPackageDistImportErrors } from "./lib/package-dist-imports.mjs";
5+
6+
function usage() {
7+
return "Usage: node scripts/check-package-dist-imports.mjs [package-root]";
8+
}
9+
10+
function fail(message) {
11+
console.error(message);
12+
process.exit(1);
13+
}
14+
15+
const packageRoot = path.resolve(process.argv[2] ?? process.cwd());
16+
if (process.argv.length > 3) {
17+
fail(usage());
18+
}
19+
20+
const distRoot = path.join(packageRoot, "dist");
21+
if (!fs.existsSync(distRoot)) {
22+
fail(`missing dist directory: ${distRoot}`);
23+
}
24+
25+
function collectFiles(rootDir) {
26+
const pending = [rootDir];
27+
const files = [];
28+
while (pending.length > 0) {
29+
const dir = pending.pop();
30+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
31+
const entryPath = path.join(dir, entry.name);
32+
if (entry.isDirectory()) {
33+
pending.push(entryPath);
34+
continue;
35+
}
36+
if (entry.isFile()) {
37+
files.push(path.relative(packageRoot, entryPath).replace(/\\/gu, "/"));
38+
}
39+
}
40+
}
41+
return files;
42+
}
43+
44+
const errors = collectPackageDistImportErrors({
45+
files: collectFiles(distRoot),
46+
readText(relativePath) {
47+
return fs.readFileSync(path.join(packageRoot, relativePath), "utf8");
48+
},
49+
});
50+
51+
if (errors.length > 0) {
52+
fail(`OpenClaw package dist import closure failed:\n${errors.join("\n")}`);
53+
}
54+
55+
console.log("OpenClaw package dist import closure passed.");
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import path from "node:path";
2+
3+
const JS_DIST_FILE_RE = /^dist\/.*\.(?:cjs|js|mjs)$/u;
4+
5+
function normalizePackagePath(value) {
6+
return value.replace(/\\/gu, "/").replace(/^package\//u, "");
7+
}
8+
9+
function stripSpecifierSuffix(value) {
10+
return value.replace(/[?#].*$/u, "");
11+
}
12+
13+
function resolveDistImportPath(importerPath, specifier) {
14+
if (!specifier.startsWith(".")) {
15+
return null;
16+
}
17+
const stripped = stripSpecifierSuffix(specifier);
18+
if (!stripped) {
19+
return null;
20+
}
21+
return path.posix.normalize(path.posix.join(path.posix.dirname(importerPath), stripped));
22+
}
23+
24+
function findStatementStart(source, index) {
25+
return (
26+
Math.max(
27+
source.lastIndexOf(";", index),
28+
source.lastIndexOf("{", index),
29+
source.lastIndexOf("}", index),
30+
source.lastIndexOf("\n", index),
31+
source.lastIndexOf("\r", index),
32+
) + 1
33+
);
34+
}
35+
36+
function isImportSpecifierContext(source, index) {
37+
const dynamicPrefix = source.slice(Math.max(0, index - 32), index);
38+
if (/\bimport\s*\(\s*$/u.test(dynamicPrefix)) {
39+
return true;
40+
}
41+
const statementPrefix = source.slice(findStatementStart(source, index), index).trimStart();
42+
return (
43+
/^(?:import|export)\b[\s\S]*\bfrom\s*$/u.test(statementPrefix) ||
44+
/^import\s*$/u.test(statementPrefix)
45+
);
46+
}
47+
48+
function collectImportSpecifiers(source) {
49+
const specifiers = [];
50+
let inBlockComment = false;
51+
let inLineComment = false;
52+
for (let index = 0; index < source.length; index += 1) {
53+
if (inBlockComment) {
54+
if (source[index] === "*" && source[index + 1] === "/") {
55+
inBlockComment = false;
56+
index += 1;
57+
}
58+
continue;
59+
}
60+
if (inLineComment) {
61+
if (source[index] === "\n" || source[index] === "\r") {
62+
inLineComment = false;
63+
}
64+
continue;
65+
}
66+
if (source[index] === "/" && source[index + 1] === "*") {
67+
inBlockComment = true;
68+
index += 1;
69+
continue;
70+
}
71+
if (source[index] === "/" && source[index + 1] === "/") {
72+
inLineComment = true;
73+
index += 1;
74+
continue;
75+
}
76+
77+
const quote = source[index];
78+
if (quote !== '"' && quote !== "'") {
79+
continue;
80+
}
81+
82+
let cursor = index + 1;
83+
let value = "";
84+
while (cursor < source.length) {
85+
const char = source[cursor];
86+
if (char === "\\") {
87+
value += source.slice(cursor, cursor + 2);
88+
cursor += 2;
89+
continue;
90+
}
91+
if (char === quote) {
92+
break;
93+
}
94+
value += char;
95+
cursor += 1;
96+
}
97+
if (cursor >= source.length) {
98+
break;
99+
}
100+
101+
if (value.startsWith(".")) {
102+
if (isImportSpecifierContext(source, index)) {
103+
specifiers.push(value);
104+
}
105+
}
106+
index = cursor;
107+
}
108+
return specifiers;
109+
}
110+
111+
export function collectPackageDistImportErrors(params) {
112+
const files = [...new Set(params.files.map(normalizePackagePath))];
113+
const fileSet = new Set(files);
114+
const errors = [];
115+
116+
for (const importerPath of files.toSorted((left, right) => left.localeCompare(right))) {
117+
if (!JS_DIST_FILE_RE.test(importerPath) || importerPath.includes("/node_modules/")) {
118+
continue;
119+
}
120+
const source = params.readText(importerPath);
121+
for (const specifier of collectImportSpecifiers(source)) {
122+
const importedPath = resolveDistImportPath(importerPath, specifier);
123+
if (!importedPath || fileSet.has(importedPath)) {
124+
continue;
125+
}
126+
errors.push(`${importerPath} imports missing ${importedPath}`);
127+
}
128+
}
129+
130+
return errors;
131+
}

scripts/test-install-sh-docker.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,15 @@ restore_local_dist_from_image() {
226226
docker rm -f "$container_id" >/dev/null
227227
}
228228

229+
ensure_local_update_dist_import_closure() {
230+
if node scripts/check-package-dist-imports.mjs "$ROOT_DIR"; then
231+
return 0
232+
fi
233+
echo "WARN: reused Docker image dist failed import-closure check; rebuilding local release artifacts" >&2
234+
pnpm build
235+
pnpm ui:build
236+
}
237+
229238
prepare_update_tarball() {
230239
local pack_json
231240
local baseline_pack_json
@@ -241,6 +250,7 @@ prepare_update_tarball() {
241250
echo "==> Build local release artifacts for update smoke"
242251
if [[ -n "$UPDATE_DIST_IMAGE" ]]; then
243252
restore_local_dist_from_image "$UPDATE_DIST_IMAGE"
253+
ensure_local_update_dist_import_closure
244254
elif [[ "$UPDATE_SKIP_LOCAL_BUILD" != "1" ]]; then
245255
pnpm build
246256
pnpm ui:build
@@ -249,6 +259,7 @@ prepare_update_tarball() {
249259
node -p 'JSON.parse(require("node:fs").readFileSync("package.json", "utf8")).version'
250260
)"
251261
node --import tsx scripts/write-package-dist-inventory.ts
262+
node scripts/check-package-dist-imports.mjs "$ROOT_DIR"
252263
quiet_npm pack --ignore-scripts --json --pack-destination "$UPDATE_DIR" >"$pack_json_file"
253264
fi
254265
UPDATE_TGZ_FILE="$(
@@ -262,6 +273,9 @@ if (!last || typeof last.filename !== "string" || last.filename.length === 0) {
262273
process.stdout.write(last.filename);
263274
' "$pack_json_file"
264275
)"
276+
if [[ -z "$UPDATE_PACKAGE_SPEC" ]]; then
277+
node scripts/check-openclaw-package-tarball.mjs "${UPDATE_DIR}/${UPDATE_TGZ_FILE}"
278+
fi
265279
print_pack_audit "update" "$pack_json_file"
266280
assert_pack_unpacked_size_budget "update" "$pack_json_file"
267281
packed_update_version="$(

src/gateway/gateway-cli-backend.live.test.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -354,11 +354,14 @@ describeLive("gateway live (cli backend)", () => {
354354
{
355355
sessionKey,
356356
idempotencyKey: `idem-${randomUUID()}`,
357-
message: enableCliModelSwitchProbe
358-
? `Please include the token CLI-BACKEND-${nonce} in your reply.` +
359-
` Also remember this session note for later: ${memoryToken}.` +
360-
" Do not include the note in your reply."
361-
: `Please include the token CLI-BACKEND-${nonce} in your reply.`,
357+
message:
358+
providerId === "codex-cli"
359+
? `Do not inspect files or run tools. Reply with exactly: CLI-BACKEND-${nonce}.`
360+
: enableCliModelSwitchProbe
361+
? `Please include the token CLI-BACKEND-${nonce} in your reply.` +
362+
` Also remember this session note for later: ${memoryToken}.` +
363+
" Do not include the note in your reply."
364+
: `Please include the token CLI-BACKEND-${nonce} in your reply.`,
362365
deliver: false,
363366
timeout: CLI_BACKEND_AGENT_TIMEOUT_SECONDS,
364367
},
@@ -457,7 +460,7 @@ describeLive("gateway live (cli backend)", () => {
457460
idempotencyKey: `idem-${randomUUID()}`,
458461
message:
459462
providerId === "codex-cli"
460-
? `Please include the token CLI-RESUME-${resumeNonce} in your reply.`
463+
? `Do not inspect files or run tools. Reply with exactly: CLI-RESUME-${resumeNonce}.`
461464
: `Reply with exactly: CLI backend RESUME OK ${resumeNonce}.`,
462465
deliver: false,
463466
timeout: CLI_BACKEND_AGENT_TIMEOUT_SECONDS,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { describe, expect, it } from "vitest";
2+
import { resolveLiveVideoSkipReason } from "./live-video-skip-reason.js";
3+
4+
describe("resolveLiveVideoSkipReason", () => {
5+
it("classifies provider policy moderation blocks as skip-worthy drift", () => {
6+
expect(resolveLiveVideoSkipReason("Your request was blocked by our moderation system.")).toBe(
7+
"provider policy drift",
8+
);
9+
});
10+
11+
it("does not hide ordinary provider failures", () => {
12+
expect(resolveLiveVideoSkipReason("video generation returned an empty asset")).toBeNull();
13+
});
14+
});

0 commit comments

Comments
 (0)