mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
9 Commits
sqlite-str
...
v2026.5.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1824464bf2 | ||
|
|
6eebba3920 | ||
|
|
7b544a7976 | ||
|
|
441041f92d | ||
|
|
7284608461 | ||
|
|
56d96b3b8d | ||
|
|
41bf26ede3 | ||
|
|
6820d18160 | ||
|
|
e6fb7aa1a8 |
13
Dockerfile
13
Dockerfile
@@ -116,20 +116,25 @@ ENV OPENCLAW_PREFER_PNPM=1
|
||||
RUN pnpm_config_verify_deps_before_run=false pnpm ui:build
|
||||
RUN pnpm_config_verify_deps_before_run=false pnpm qa:lab:build
|
||||
|
||||
# Prune dev dependencies and strip build-only metadata before copying
|
||||
# Reinstall production dependencies and strip build-only metadata before copying
|
||||
# runtime assets into the final image.
|
||||
FROM build AS runtime-assets
|
||||
ARG OPENCLAW_EXTENSIONS
|
||||
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
|
||||
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||
CI=true pnpm prune --prod \
|
||||
--config.offline=true \
|
||||
RUN --mount=type=cache,id=openclaw-pnpm-runtime-store,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||
echo "==> runtime-assets: install prod dependencies" && \
|
||||
rm -rf node_modules && \
|
||||
NODE_OPTIONS=--max-old-space-size=2048 pnpm install --prod --frozen-lockfile --ignore-scripts \
|
||||
--config.supportedArchitectures.os=linux \
|
||||
--config.supportedArchitectures.cpu="$(node -p 'process.arch')" \
|
||||
--config.supportedArchitectures.libc=glibc && \
|
||||
echo "==> runtime-assets: refresh bundled plugin registry" && \
|
||||
node scripts/postinstall-bundled-plugins.mjs && \
|
||||
echo "==> runtime-assets: prune non-selected plugin dist" && \
|
||||
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" node scripts/prune-docker-plugin-dist.mjs && \
|
||||
echo "==> runtime-assets: remove dist type and sourcemap files" && \
|
||||
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
|
||||
echo "==> runtime-assets: check package dist imports" && \
|
||||
node scripts/check-package-dist-imports.mjs /app
|
||||
|
||||
# ── Runtime base image ──────────────────────────────────────────
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("nvidia onboard", () => {
|
||||
legacyModelName: "Custom",
|
||||
});
|
||||
expect(provider?.models.map((model) => model.id)).toEqual([
|
||||
"custom-model",
|
||||
"nvidia/custom-model",
|
||||
"nvidia/nemotron-3-super-120b-a12b",
|
||||
"moonshotai/kimi-k2.5",
|
||||
"minimaxai/minimax-m2.5",
|
||||
|
||||
@@ -25,7 +25,7 @@ describeLive("openai tts live", () => {
|
||||
cfg: { plugins: { enabled: true } } as never,
|
||||
providerConfig,
|
||||
target: "audio-file",
|
||||
timeoutMs: 45_000,
|
||||
timeoutMs: 90_000,
|
||||
});
|
||||
expect(audioFile.outputFormat).toBe("mp3");
|
||||
expect(audioFile.fileExtension).toBe(".mp3");
|
||||
@@ -35,10 +35,10 @@ describeLive("openai tts live", () => {
|
||||
text: "OpenClaw OpenAI telephony integration test OK.",
|
||||
cfg: { plugins: { enabled: true } } as never,
|
||||
providerConfig,
|
||||
timeoutMs: 45_000,
|
||||
timeoutMs: 90_000,
|
||||
});
|
||||
expect(telephony?.outputFormat).toBe("pcm");
|
||||
expect(telephony?.sampleRate).toBe(24_000);
|
||||
expect(telephony?.audioBuffer.byteLength).toBeGreaterThan(512);
|
||||
}, 60_000);
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ function createLiveTtsConfig(): ResolvedTtsConfig {
|
||||
},
|
||||
personas: {},
|
||||
maxTextLength: 4_000,
|
||||
timeoutMs: 30_000,
|
||||
timeoutMs: 90_000,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ describeLive("openai plugin live", () => {
|
||||
expect(telephony?.outputFormat).toBe("pcm");
|
||||
expect(telephony?.sampleRate).toBe(24_000);
|
||||
expect(telephony?.audioBuffer.byteLength).toBeGreaterThan(512);
|
||||
}, 45_000);
|
||||
}, 120_000);
|
||||
|
||||
it("transcribes synthesized speech through the registered media provider", async () => {
|
||||
const { speechProviders, mediaProviders } = await registerOpenAIPlugin();
|
||||
|
||||
@@ -1176,6 +1176,7 @@ describe("createTelegramBot", () => {
|
||||
]),
|
||||
});
|
||||
|
||||
const storePath = `/tmp/openclaw-telegram-model-display-names-${process.pid}-${Date.now()}.json`;
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -1188,8 +1189,12 @@ describe("createTelegramBot", () => {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
} satisfies NonNullable<Parameters<typeof createTelegramBot>[0]["config"]>;
|
||||
|
||||
await rm(storePath, { force: true });
|
||||
loadConfig.mockReturnValue(config);
|
||||
createTelegramBot({
|
||||
token: "tok",
|
||||
@@ -1234,6 +1239,7 @@ describe("createTelegramBot", () => {
|
||||
[{ text: "<< Back", callback_data: "mdl_back" }],
|
||||
]);
|
||||
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-model-display-names-1");
|
||||
await rm(storePath, { force: true });
|
||||
});
|
||||
|
||||
it("resets overrides when selecting the configured default model", async () => {
|
||||
|
||||
@@ -10,20 +10,29 @@ minimumReleaseAgeExclude:
|
||||
- "acpx"
|
||||
- "tokenjuice"
|
||||
- "@agentclientprotocol/sdk"
|
||||
- "@anthropic-ai/sdk"
|
||||
- "axios"
|
||||
- "basic-ftp"
|
||||
- "hono"
|
||||
- "openclaw"
|
||||
- "protobufjs"
|
||||
- "playwright-core"
|
||||
- "vite"
|
||||
- "vitest"
|
||||
- "@vitest/*"
|
||||
- "@copilotkit/aimock"
|
||||
- "yaml"
|
||||
- "@cloudflare/workers-types"
|
||||
- "@hono/node-server"
|
||||
- "@mariozechner/*"
|
||||
- "@openclaw/fs-safe"
|
||||
- "@openai/codex"
|
||||
- "@openai/codex-*"
|
||||
- "@smithy/*"
|
||||
- "@typescript/native-preview*"
|
||||
- "@types/node"
|
||||
- "@rolldown/*"
|
||||
- "oxlint"
|
||||
- "@oxlint/*"
|
||||
- "@oxfmt/*"
|
||||
- "oxfmt"
|
||||
|
||||
@@ -48,20 +48,15 @@ export OPENCLAW_NO_PROMPT=1
|
||||
package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
|
||||
git_root="/tmp/openclaw-git"
|
||||
mkdir -p "$git_root"
|
||||
echo "==> prepare package-derived git fixture"
|
||||
# Build the fake git install from the packed package contents, not the checkout.
|
||||
tar -xzf "$package_tgz" -C "$git_root" --strip-components=1
|
||||
# The package-derived fixture can carry patchedDependencies whose targets are
|
||||
# absent from the trimmed tarball install; that should not block update preflight.
|
||||
node scripts/e2e/lib/update-channel-switch/assertions.mjs prepare-git-fixture "$git_root"
|
||||
(
|
||||
cd "$git_root"
|
||||
if ! npm install --omit=optional --no-fund --no-audit >/tmp/openclaw-git-install.log 2>&1; then
|
||||
cat /tmp/openclaw-git-install.log >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
)
|
||||
node scripts/e2e/lib/update-channel-switch/assertions.mjs write-control-ui "$git_root"
|
||||
|
||||
echo "==> commit package-derived git fixture"
|
||||
git config --global user.email "docker-e2e@openclaw.local"
|
||||
git config --global user.name "OpenClaw Docker E2E"
|
||||
git config --global gc.auto 0
|
||||
@@ -74,6 +69,7 @@ fixture_sha="$(git -C "$git_root" rev-parse HEAD)"
|
||||
|
||||
pkg_tgz_path="$package_tgz"
|
||||
|
||||
echo "==> install package fixture"
|
||||
npm install -g --prefix /tmp/npm-prefix --omit=optional "$pkg_tgz_path"
|
||||
package_version="$(node -p "JSON.parse(require(\"node:fs\").readFileSync(\"/tmp/npm-prefix/lib/node_modules/openclaw/package.json\", \"utf8\")).version")"
|
||||
OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT="$(
|
||||
|
||||
@@ -187,7 +187,7 @@ openclaw_live_link_runtime_tree "$tmp_dir"
|
||||
openclaw_live_stage_state_dir "$tmp_dir/.openclaw-state"
|
||||
openclaw_live_prepare_staged_config
|
||||
cd "$tmp_dir"
|
||||
pnpm test:live:models-profiles
|
||||
pnpm_config_verify_deps_before_run=false pnpm test:live:models-profiles
|
||||
EOF
|
||||
|
||||
OPENCLAW_LIVE_DOCKER_REPO_ROOT="$ROOT_DIR" "$TRUSTED_HARNESS_DIR/scripts/test-live-build-docker.sh"
|
||||
|
||||
@@ -917,6 +917,7 @@ describe("update-cli", () => {
|
||||
|
||||
it("keeps downgrade post-update work in the current process", async () => {
|
||||
const downgradedRoot = createCaseDir("openclaw-downgraded-root");
|
||||
mockPackageInstallStatus(downgradedRoot);
|
||||
setupUpdatedRootRefresh({
|
||||
gatewayUpdateImpl: async () =>
|
||||
makeOkUpdateResult({
|
||||
@@ -958,6 +959,68 @@ describe("update-cli", () => {
|
||||
expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("same-process package updates avoid stale base hashes for plugin config writes", async () => {
|
||||
const downgradedRoot = createCaseDir("openclaw-downgraded-root");
|
||||
mockPackageInstallStatus(downgradedRoot);
|
||||
setupUpdatedRootRefresh({
|
||||
gatewayUpdateImpl: async () =>
|
||||
makeOkUpdateResult({
|
||||
mode: "npm",
|
||||
root: downgradedRoot,
|
||||
before: { version: "2026.4.14" },
|
||||
after: { version: "2026.4.10" },
|
||||
}),
|
||||
});
|
||||
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
|
||||
...baseSnapshot,
|
||||
parsed: { plugins: { entries: {} } },
|
||||
resolved: { plugins: { entries: {} } } as OpenClawConfig,
|
||||
sourceConfig: { plugins: { entries: {} } } as OpenClawConfig,
|
||||
runtimeConfig: { plugins: { entries: {} } } as OpenClawConfig,
|
||||
config: { plugins: { entries: {} } } as OpenClawConfig,
|
||||
hash: "pre-update-hash",
|
||||
});
|
||||
readPackageVersion.mockResolvedValue("2026.4.14");
|
||||
syncPluginsForUpdateChannel.mockImplementationOnce(async ({ config }) => ({
|
||||
changed: true,
|
||||
config: {
|
||||
...config,
|
||||
plugins: {
|
||||
...config.plugins,
|
||||
entries: {
|
||||
demo: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
summary: {
|
||||
switchedToBundled: [],
|
||||
switchedToNpm: [],
|
||||
warnings: [],
|
||||
errors: [],
|
||||
},
|
||||
}));
|
||||
updateNpmInstalledPlugins.mockImplementationOnce(async ({ config }) => ({
|
||||
changed: false,
|
||||
config,
|
||||
outcomes: [],
|
||||
}));
|
||||
|
||||
await updateCommand({ yes: true, tag: "2026.4.10", restart: false });
|
||||
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
const pluginWrite = vi.mocked(replaceConfigFile).mock.calls.at(-1)?.[0];
|
||||
expect(pluginWrite).toMatchObject({
|
||||
nextConfig: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(pluginWrite).not.toHaveProperty("baseHash");
|
||||
});
|
||||
|
||||
it("fails the update when the fresh process exits non-zero", async () => {
|
||||
setupUpdatedRootRefresh();
|
||||
spawn.mockImplementationOnce(() => {
|
||||
@@ -1168,6 +1231,63 @@ describe("update-cli", () => {
|
||||
expect(syncPluginCall()?.config?.update?.channel).toBe("dev");
|
||||
});
|
||||
|
||||
it("post-core resume mode avoids stale base hashes for plugin config writes", async () => {
|
||||
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
|
||||
...baseSnapshot,
|
||||
parsed: { plugins: { entries: {} } },
|
||||
resolved: { plugins: { entries: {} } } as OpenClawConfig,
|
||||
sourceConfig: { plugins: { entries: {} } } as OpenClawConfig,
|
||||
runtimeConfig: { plugins: { entries: {} } } as OpenClawConfig,
|
||||
config: { plugins: { entries: {} } } as OpenClawConfig,
|
||||
hash: "pre-post-core-hash",
|
||||
});
|
||||
syncPluginsForUpdateChannel.mockImplementationOnce(async ({ config }) => ({
|
||||
changed: true,
|
||||
config: {
|
||||
...config,
|
||||
plugins: {
|
||||
...config.plugins,
|
||||
entries: {
|
||||
demo: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
summary: {
|
||||
switchedToBundled: [],
|
||||
switchedToNpm: [],
|
||||
warnings: [],
|
||||
errors: [],
|
||||
},
|
||||
}));
|
||||
updateNpmInstalledPlugins.mockImplementationOnce(async ({ config }) => ({
|
||||
changed: false,
|
||||
config,
|
||||
outcomes: [],
|
||||
}));
|
||||
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_UPDATE_POST_CORE: "1",
|
||||
OPENCLAW_UPDATE_POST_CORE_CHANNEL: "stable",
|
||||
},
|
||||
async () => {
|
||||
await updateCommand({ restart: false });
|
||||
},
|
||||
);
|
||||
|
||||
const pluginWrite = vi.mocked(replaceConfigFile).mock.calls.at(-1)?.[0];
|
||||
expect(pluginWrite).toMatchObject({
|
||||
nextConfig: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(pluginWrite).not.toHaveProperty("baseHash");
|
||||
});
|
||||
|
||||
it("passes the update timeout budget into post-core plugin updates", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
|
||||
@@ -1156,6 +1156,7 @@ export async function updatePluginsAfterCoreUpdate(params: {
|
||||
root: string;
|
||||
channel: "stable" | "beta" | "dev";
|
||||
configSnapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>;
|
||||
updateMode?: UpdateRunResult["mode"];
|
||||
opts: UpdateCommandOptions;
|
||||
timeoutMs: number;
|
||||
pluginInstallRecords?: Record<string, PluginInstallRecord>;
|
||||
@@ -1378,7 +1379,11 @@ export async function updatePluginsAfterCoreUpdate(params: {
|
||||
previousInstallRecords: pluginInstallRecords,
|
||||
nextInstallRecords,
|
||||
nextConfig,
|
||||
baseHash: params.configSnapshot.hash,
|
||||
baseHash:
|
||||
process.env[POST_CORE_UPDATE_ENV] === "1" ||
|
||||
(params.updateMode ? isPackageManagerUpdateMode(params.updateMode) : false)
|
||||
? undefined
|
||||
: params.configSnapshot.hash,
|
||||
});
|
||||
await refreshPluginRegistryAfterConfigMutation({
|
||||
config: nextConfig,
|
||||
@@ -1738,6 +1743,7 @@ async function runPostCorePluginUpdate(params: {
|
||||
root: string;
|
||||
channel: "stable" | "beta" | "dev";
|
||||
configSnapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>;
|
||||
updateMode?: UpdateRunResult["mode"];
|
||||
opts: UpdateCommandOptions;
|
||||
timeoutMs: number;
|
||||
pluginInstallRecords?: Record<string, PluginInstallRecord>;
|
||||
@@ -1746,6 +1752,7 @@ async function runPostCorePluginUpdate(params: {
|
||||
root: params.root,
|
||||
channel: params.channel,
|
||||
configSnapshot: params.configSnapshot,
|
||||
updateMode: params.updateMode,
|
||||
opts: params.opts,
|
||||
timeoutMs: params.timeoutMs,
|
||||
pluginInstallRecords: params.pluginInstallRecords,
|
||||
@@ -2533,6 +2540,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
root: postUpdateRoot,
|
||||
channel,
|
||||
configSnapshot: postUpdateConfigSnapshot,
|
||||
updateMode: result.mode,
|
||||
opts,
|
||||
timeoutMs: updateStepTimeoutMs,
|
||||
pluginInstallRecords: preUpdatePluginInstallRecords,
|
||||
|
||||
@@ -88,12 +88,15 @@ describe("Dockerfile", () => {
|
||||
expect(dockerfile).toContain("apt-get install -y --no-install-recommends xvfb");
|
||||
});
|
||||
|
||||
it("uses the Docker target platform for pnpm install and prune", async () => {
|
||||
it("uses the Docker target platform for pnpm install and runtime dependency install", async () => {
|
||||
const dockerfile = await readFile(dockerfilePath, "utf8");
|
||||
|
||||
expect(dockerfile).toContain("pnpm install --frozen-lockfile \\");
|
||||
expect(dockerfile).toContain("CI=true pnpm prune --prod \\");
|
||||
expect(dockerfile).toContain("--config.offline=true");
|
||||
expect(dockerfile).toContain(
|
||||
"NODE_OPTIONS=--max-old-space-size=2048 pnpm install --prod --frozen-lockfile --ignore-scripts \\",
|
||||
);
|
||||
expect(dockerfile).not.toContain("pnpm prune --prod");
|
||||
expect(dockerfile).not.toContain("--config.offline=true");
|
||||
expect(dockerfile.split("--config.supportedArchitectures.os=linux").length - 1).toBe(2);
|
||||
expect(
|
||||
dockerfile.split("--config.supportedArchitectures.cpu=\"$(node -p 'process.arch')\"").length -
|
||||
@@ -156,7 +159,7 @@ describe("Dockerfile", () => {
|
||||
expect(dockerfile).toContain("pnpm_config_verify_deps_before_run=false pnpm qa:lab:build");
|
||||
});
|
||||
|
||||
it("prunes runtime dependencies after the build stage", async () => {
|
||||
it("installs runtime dependencies after the build stage", async () => {
|
||||
const dockerfile = await readFile(dockerfilePath, "utf8");
|
||||
expect(dockerfile).toContain("FROM build AS runtime-assets");
|
||||
expect(dockerfile).toContain("ARG OPENCLAW_EXTENSIONS");
|
||||
@@ -168,14 +171,19 @@ describe("Dockerfile", () => {
|
||||
'Example: docker build --build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,matrix" .',
|
||||
);
|
||||
expect(dockerfile).toContain(
|
||||
"RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \\",
|
||||
"RUN --mount=type=cache,id=openclaw-pnpm-runtime-store,target=/root/.local/share/pnpm/store,sharing=locked \\",
|
||||
);
|
||||
expect(dockerfile).toContain("COPY --from=workspace-deps /out/packages/ ./packages/");
|
||||
expect(dockerfile).toContain(
|
||||
"COPY --from=workspace-deps /out/${OPENCLAW_BUNDLED_PLUGIN_DIR}/ ./${OPENCLAW_BUNDLED_PLUGIN_DIR}/",
|
||||
);
|
||||
expect(dockerfile).toContain("CI=true pnpm prune --prod \\");
|
||||
expect(dockerfile).toContain("--config.offline=true");
|
||||
expect(dockerfile).toContain("runtime-assets: install prod dependencies");
|
||||
expect(dockerfile).toContain("rm -rf node_modules");
|
||||
expect(dockerfile).toContain(
|
||||
"NODE_OPTIONS=--max-old-space-size=2048 pnpm install --prod --frozen-lockfile --ignore-scripts \\",
|
||||
);
|
||||
expect(dockerfile).not.toContain("pnpm prune --prod");
|
||||
expect(dockerfile).not.toContain("--config.offline=true");
|
||||
expect(dockerfile).toContain("--config.supportedArchitectures.os=linux");
|
||||
expect(dockerfile).toContain(
|
||||
"--config.supportedArchitectures.cpu=\"$(node -p 'process.arch')\"",
|
||||
@@ -205,7 +213,8 @@ describe("Dockerfile", () => {
|
||||
const pnpmWorkspace = YAML.parse(await readFile(pnpmWorkspacePath, "utf8")) as {
|
||||
patchedDependencies?: Record<string, string>;
|
||||
};
|
||||
const pruneProd = "CI=true pnpm prune --prod";
|
||||
const runtimeInstall =
|
||||
"NODE_OPTIONS=--max-old-space-size=2048 pnpm install --prod --frozen-lockfile --ignore-scripts";
|
||||
const finalWorkspaceCopy =
|
||||
"COPY --from=runtime-assets --chown=node:node /app/pnpm-workspace.yaml .";
|
||||
|
||||
@@ -214,7 +223,7 @@ describe("Dockerfile", () => {
|
||||
expect(dockerfile).not.toContain("write-runtime-pnpm-workspace");
|
||||
expect(dockerfile).not.toContain("pnpm_config_frozen_lockfile=false");
|
||||
expect(dockerfile).toContain(finalWorkspaceCopy);
|
||||
expect(dockerfile.indexOf(pruneProd)).toBeLessThan(dockerfile.indexOf(finalWorkspaceCopy));
|
||||
expect(dockerfile.indexOf(runtimeInstall)).toBeLessThan(dockerfile.indexOf(finalWorkspaceCopy));
|
||||
expect(dockerfile).toContain(
|
||||
"COPY --from=runtime-assets --chown=node:node /app/pnpm-workspace.yaml .",
|
||||
);
|
||||
|
||||
@@ -38,9 +38,25 @@ async function addCompileCacheProbe(fixtureRoot: string): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForFile(filePath: string, timeoutMs: number): Promise<string> {
|
||||
function isJsonContent(content: string): boolean {
|
||||
try {
|
||||
return await fs.readFile(filePath, "utf8");
|
||||
JSON.parse(content);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForFile(
|
||||
filePath: string,
|
||||
timeoutMs: number,
|
||||
isReady: (content: string) => boolean = () => true,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf8");
|
||||
if (isReady(content)) {
|
||||
return content;
|
||||
}
|
||||
} catch {
|
||||
// Wait below.
|
||||
}
|
||||
@@ -59,8 +75,15 @@ async function waitForFile(filePath: string, timeoutMs: number): Promise<string>
|
||||
watcher?.close();
|
||||
};
|
||||
const tryRead = async () => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf8");
|
||||
if (!isReady(content)) {
|
||||
setTimeout(() => void tryRead(), 10);
|
||||
return;
|
||||
}
|
||||
cleanup();
|
||||
resolve(content);
|
||||
} catch {
|
||||
@@ -250,7 +273,9 @@ describe("openclaw launcher", () => {
|
||||
let respawnChildPid: number | undefined;
|
||||
|
||||
try {
|
||||
const childInfo = JSON.parse(await waitForFile(childInfoPath, 5000)) as { pid: number };
|
||||
const childInfo = JSON.parse(await waitForFile(childInfoPath, 5000, isJsonContent)) as {
|
||||
pid: number;
|
||||
};
|
||||
respawnChildPid = childInfo.pid;
|
||||
|
||||
launcher.kill("SIGTERM");
|
||||
@@ -301,7 +326,9 @@ describe("openclaw launcher", () => {
|
||||
let respawnChildPid: number | undefined;
|
||||
|
||||
try {
|
||||
const childInfo = JSON.parse(await waitForFile(childInfoPath, 5000)) as { pid: number };
|
||||
const childInfo = JSON.parse(await waitForFile(childInfoPath, 5000, isJsonContent)) as {
|
||||
pid: number;
|
||||
};
|
||||
respawnChildPid = childInfo.pid;
|
||||
|
||||
launcher.kill("SIGTERM");
|
||||
|
||||
@@ -253,6 +253,7 @@ function buildDockerE2eHarnessEntries(): Record<string, string> {
|
||||
"agents/pi-bundle-mcp-runtime": "src/agents/pi-bundle-mcp-runtime.ts",
|
||||
"agents/pi-embedded-runner/effective-tool-policy":
|
||||
"src/agents/pi-embedded-runner/effective-tool-policy.ts",
|
||||
"agents/pi-embedded-runner/tool-split": "src/agents/pi-embedded-runner/tool-split.ts",
|
||||
"agents/pi-embedded-runner/run/runtime-context-prompt":
|
||||
"src/agents/pi-embedded-runner/run/runtime-context-prompt.ts",
|
||||
"auto-reply/reply/commands-crestodian": "src/auto-reply/reply/commands-crestodian.ts",
|
||||
|
||||
Reference in New Issue
Block a user