mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-31 20:47:00 +00:00
* feat(workers): provision offline node runtime bundles
Preserve explicit npm profiles and their lock/reuse path while carrying qualified target dependencies in bundle archives.
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* feat: provision cloud worker runtimes without registry access
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: 65d6807d-2851-4dba-9916-f4bc1496596b
* feat: expose offline worker runtime artifact preparation
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* feat: export and install offline worker runtime artifacts
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: 3654bba2-696b-4a60-8fb9-a99de032625b
* fix(workers): retain repository package-manager prerequisites
Scope Node-only setup to bundle enrollment, fix narrowed bundle fixture metadata, and reuse the validated package manifest owner instead of unsafe assertions. Prove missing npm/Corepack repair and retained npm runtime compatibility.
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* Publish openclaw/p2c-offline-runtime-split
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: 92b05551-9794-42c5-b0ea-0dbd82702c99
* fix(test): satisfy toolchain fixture lint
Keep fixture loops explicit without changing their behavior, and account for their exact test-line delta.
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* Publish openclaw/p2c-offline-runtime-split
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: 4b152785-ec9c-414f-90c7-2368b3a060b3
* Publish openclaw/p2c-runtime-api-final
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: 6d2083e0-fbd1-4410-b43b-515f5b21a52c
* test: keep declaration coverage at its authoritative boundary
Remove the duplicate config.entry assertion and shrink the API test allocation by its exact six-line delta. The canonical dts.entry test still covers the public runtime-artifacts declaration.
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* Publish openclaw/p2c-runtime-api-final
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: 13b4786d-9391-4ac9-a9f8-779960f09c7f
* Publish openclaw/p2c-runtime-api-final
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: d7740df3-4673-4768-bdba-c59f6d7bf07e
* fix: package npm dependencies named after Node builtins
Resolve declared package names and ancestor placements with package-style lookups. Preserve nested versions, source links and required dependency guards.
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* fix(codex): align bundled fs-safe with the host runtime
Use the already-admitted root fs-safe0.26.15 pin so selected Codex packages satisfy the canonical single-version contract. Regenerate only the Codex lockfile importer.
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* fix: retain installed dependencies for built bundled plugins
Validate source metadata for built plugin selections and use that installation's dependency graph. Published packages without source metadata keep their installed built root; mismatched and malformed generations remain rejected.
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* Publish openclaw/p2c-runtime-api-final
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: c3c308fe-c901-4c1d-902a-3ccef24575a6
* test: account for formatted runtime artifact regressions
Allocate exactly four new LF-counted API test lines plus the separate35-line package-cycle allocation. Preserve inherited test debt and leave the shared baseline and comparator unchanged.
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* Publish openclaw/p2c-runtime-api-final
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: ed6b8a9e-b71d-42d1-ad1d-5c0400e84abd
* test: qualify rollout catalog watcher delivery
Keep real rollout filesystem changes and exact final membership assertions, but reconcile after explicit observer delivery. Exercise cached leaves with coarse directory timestamps and use controlled healthy watchers for loss/cleanup proof.
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* Publish openclaw/p2c-runtime-api-final
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: 1451be59-4dd0-4d32-bbb5-907edc3c8d33
* docs: clarify offline runtime enrollment prerequisites
Ignore target-specific node runtime dependency inputs. Distinguish bundle enrollment Node and tar prerequisites from explicit npm installs and repository npm/Corepack setup.
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* Publish openclaw/p2c-runtime-api-final
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: 97f753f5-6efb-4e82-bf16-b34a75076a1d
* test(gateway): complete shared diagnostics logger
Reuse the canonical Workboard fixture repair from 5b08bece95. Exercise terminal hooks with diagnostics explicitly enabled, restore the prior flag, and allocate only the seven added regression lines.
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
* Publish openclaw/p2c-runtime-api-final
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: 885d6f42-0ec8-4cda-9cd3-a894a26e0861
* Publish openclaw/p2c-runtime-api-final
Worked on by:
- @galiniliev
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
OpenClaw-Publication: d139f994-a008-4727-a871-cfb342706b2b
---------
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
272 lines
11 KiB
TypeScript
272 lines
11 KiB
TypeScript
// Covers bundling rules encoded in the root tsdown config.
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { expectDefined } from "@openclaw/normalization-core/expect";
|
|
import type { TsdownPluginOption, UserConfig } from "tsdown";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { buildPluginSdkPackageExports } from "../../scripts/lib/plugin-sdk-entries.mts";
|
|
import { importFreshModule } from "../../src/plugin-sdk/test-helpers/import-fresh.js";
|
|
import { OPENCLAW_STATE_SCHEMA_SQL } from "../../src/state/openclaw-state-schema.js";
|
|
import tsdownConfig, {
|
|
createStateSchemaInlinePlugin,
|
|
STATE_SCHEMA_INLINE_PLUGIN_NAME,
|
|
} from "../../tsdown.config.ts";
|
|
|
|
// Keep config assertions aligned with tsdown's nested, async plugin slots.
|
|
async function resolvePluginNames(plugins: TsdownPluginOption): Promise<string[]> {
|
|
const resolved = await plugins;
|
|
if (!resolved) {
|
|
return [];
|
|
}
|
|
if (Array.isArray(resolved)) {
|
|
return (await Promise.all(resolved.map(resolvePluginNames))).flat();
|
|
}
|
|
if (!("name" in resolved)) {
|
|
throw new Error("expected a named plugin in build config assertions");
|
|
}
|
|
return [resolved.name];
|
|
}
|
|
|
|
function entrySources(config: UserConfig): Record<string, string | string[]> {
|
|
if (!config.entry || typeof config.entry !== "object" || Array.isArray(config.entry)) {
|
|
return {};
|
|
}
|
|
return config.entry;
|
|
}
|
|
|
|
function runtimeGraph(entry: string) {
|
|
const graphs = tsdownConfig.filter(
|
|
(config) =>
|
|
!(typeof config.dts === "object" && config.dts.emitDtsOnly) &&
|
|
Object.hasOwn(entrySources(config), entry),
|
|
);
|
|
expect(graphs).toHaveLength(1);
|
|
return expectDefined(graphs[0], `${entry} runtime graph`);
|
|
}
|
|
|
|
async function inputOptions(config: UserConfig) {
|
|
if (typeof config.inputOptions !== "function") {
|
|
throw new Error("expected runtime input options factory");
|
|
}
|
|
return config.inputOptions({}, "es", { cjsDts: false });
|
|
}
|
|
|
|
afterEach(() => vi.unstubAllEnvs());
|
|
|
|
describe("tsdown config", () => {
|
|
it("builds the public runtime-artifacts export", () => {
|
|
const manifest = JSON.parse(readFileSync("package.json", "utf8"));
|
|
expect(manifest.exports["./runtime-artifacts"]).toEqual({
|
|
types: "./dist/runtime-artifacts.d.ts",
|
|
default: "./dist/runtime-artifacts.js",
|
|
});
|
|
expect(entrySources(runtimeGraph("runtime-artifacts"))["runtime-artifacts"]).toBe(
|
|
"src/runtime-artifacts.ts",
|
|
);
|
|
});
|
|
|
|
it("keeps isolated and unselected plugin workers out of the host build graph", async () => {
|
|
vi.stubEnv("OPENCLAW_BUILD_PRIVATE_QA", "0");
|
|
const { default: selectedConfigs } = await importFreshModule<
|
|
typeof import("../../tsdown.config.ts")
|
|
>(import.meta.url, "../../tsdown.config.ts?isolated-worker-build");
|
|
const host = expectDefined(
|
|
selectedConfigs.find((config) =>
|
|
Object.hasOwn(entrySources(config), "plugins/runtime/index"),
|
|
),
|
|
"host build graph",
|
|
);
|
|
const sources = Object.values(entrySources(host))
|
|
.flat()
|
|
.map((source) =>
|
|
path.relative(process.cwd(), path.resolve(source)).replaceAll(path.sep, "/"),
|
|
);
|
|
for (const plugin of [
|
|
"codex",
|
|
"discord",
|
|
"imessage",
|
|
"memory-lancedb",
|
|
"onnx",
|
|
"team-reports",
|
|
"qa-lab",
|
|
]) {
|
|
expect(sources.filter((source) => source.startsWith(`extensions/${plugin}/`))).toEqual([]);
|
|
}
|
|
});
|
|
|
|
it.each(["0", "1"])(
|
|
"emits QA transport facades only for private QA builds (%s)",
|
|
async (mode) => {
|
|
vi.stubEnv("OPENCLAW_BUILD_PRIVATE_QA", mode);
|
|
const { default: selectedConfigs } = await importFreshModule<
|
|
typeof import("../../tsdown.config.ts")
|
|
>(import.meta.url, `../../tsdown.config.ts?private-qa=${mode}`);
|
|
const runtimeEntries = selectedConfigs
|
|
.filter((config) => !(typeof config.dts === "object" && config.dts.emitDtsOnly))
|
|
.flatMap((config) => Object.entries(entrySources(config)));
|
|
const packageExports = buildPluginSdkPackageExports();
|
|
for (const subpath of ["qa-channel", "qa-channel-protocol", "qa-lab", "qa-runtime"]) {
|
|
const matches = runtimeEntries.filter(([name]) => name === `plugin-sdk/${subpath}`);
|
|
expect(matches).toEqual(
|
|
mode === "1" ? [[`plugin-sdk/${subpath}`, `src/plugin-sdk/${subpath}.ts`]] : [],
|
|
);
|
|
expect(Object.hasOwn(packageExports, `./plugin-sdk/${subpath}`)).toBe(false);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("inlines canonical schema bytes and invalidates the transform cache", () => {
|
|
const rootDir = process.cwd();
|
|
const modulePath = path.resolve("src/state/openclaw-state-schema.ts");
|
|
const schemaPath = path.resolve("src/state/openclaw-state-schema.sql");
|
|
const watchedPaths: string[] = [];
|
|
const plugin = createStateSchemaInlinePlugin(rootDir);
|
|
let cacheKeyGenerator: ((context: { id: string }) => string | undefined) | undefined;
|
|
plugin.configureVitest({
|
|
defineCacheKeyGenerator: (generator) => {
|
|
cacheKeyGenerator = generator;
|
|
},
|
|
});
|
|
const result = plugin.load.call(
|
|
{ addWatchFile: (filePath: string) => watchedPaths.push(filePath) },
|
|
modulePath,
|
|
);
|
|
const canonicalSql = readFileSync(schemaPath, "utf8");
|
|
|
|
const match = result?.code.match(/^export const OPENCLAW_STATE_SCHEMA_SQL = (.*);\n$/su);
|
|
expect(match?.[1]).toBeDefined();
|
|
expect(JSON.parse(match?.[1] ?? "null")).toBe(canonicalSql);
|
|
expect(OPENCLAW_STATE_SCHEMA_SQL).toBe(canonicalSql);
|
|
expect(watchedPaths).toEqual([schemaPath]);
|
|
expect(cacheKeyGenerator?.({ id: modulePath })).toBe(canonicalSql);
|
|
expect(cacheKeyGenerator?.({ id: path.resolve(rootDir, "src/index.ts") })).toBeUndefined();
|
|
});
|
|
|
|
it("installs schema inlining only on executable runtime graphs", async () => {
|
|
for (const entry of [
|
|
"plugins/runtime/index",
|
|
"worker/worker",
|
|
"managed-handoff-runtime",
|
|
"package-update-activation-recovery",
|
|
"native-hook-relay/entry",
|
|
"infra/sqlite-readonly-location.worker",
|
|
// Regression: the transcript reader moved out of the shared graph (#182259).
|
|
"config/sessions/session-transcript.worker",
|
|
]) {
|
|
const inlinePlugins = (await resolvePluginNames(runtimeGraph(entry).plugins)).filter(
|
|
(name) => name === STATE_SCHEMA_INLINE_PLUGIN_NAME,
|
|
);
|
|
expect(inlinePlugins, entry).toHaveLength(1);
|
|
}
|
|
for (const config of tsdownConfig.filter(
|
|
(candidate) => typeof candidate.dts === "object" && candidate.dts.emitDtsOnly,
|
|
)) {
|
|
expect(await resolvePluginNames(config.plugins)).not.toContain(
|
|
STATE_SCHEMA_INLINE_PLUGIN_NAME,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("isolates relay startup from shared runtime chunks while retaining lazy fallback", async () => {
|
|
const relay = runtimeGraph("native-hook-relay/entry");
|
|
expect(entrySources(relay)).toEqual({
|
|
"native-hook-relay/entry": "src/cli/native-hook-relay-entry.ts",
|
|
});
|
|
expect(relay).not.toBe(runtimeGraph("plugins/runtime/index"));
|
|
expect(relay.dts).toBe(false);
|
|
expect(relay.outputOptions).not.toHaveProperty("codeSplitting", false);
|
|
expect(relay.outputOptions).toHaveProperty(
|
|
"chunkFileNames",
|
|
"native-hook-relay/[name]-[hash].mjs",
|
|
);
|
|
// Only the shared graph may publish the global plugin ownership manifest.
|
|
expect(await resolvePluginNames(relay.plugins)).not.toContain(
|
|
"openclaw:runtime-dependency-ownership",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"plugins/plugin-generation-preparation.worker",
|
|
"infra/sqlite-readonly-location.worker",
|
|
"infra/sqlite-source-revision.worker",
|
|
])("emits %s once without sealing its package loaders", (entry) => {
|
|
const child = runtimeGraph(entry);
|
|
expect(entrySources(child)).toEqual({ [entry]: path.resolve(`src/${entry}.ts`) });
|
|
expect(child.outputOptions).toMatchObject({
|
|
codeSplitting:
|
|
entry === "infra/sqlite-readonly-location.worker" ||
|
|
entry === "plugins/plugin-generation-preparation.worker",
|
|
});
|
|
if (entry === "plugins/plugin-generation-preparation.worker") {
|
|
expect(child.outputOptions).toHaveProperty(
|
|
"chunkFileNames",
|
|
"plugins/plugin-preparation-[name]-[hash].mjs",
|
|
);
|
|
}
|
|
expect(child.outExtensions?.({ options: {}, format: "es" })?.js).toBe(".js");
|
|
expect(child.define?.SEALED_RUNTIME_BUILD).toBeUndefined();
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"config/sessions/session-transcript-reconcile",
|
|
"src/config/sessions/session-transcript-reconcile.ts",
|
|
],
|
|
[
|
|
"telegram-ingress-worker.runtime",
|
|
"extensions/telegram/src/telegram-ingress-worker.runtime.ts",
|
|
],
|
|
])("keeps %s behind its stable root dist entry", (entry, source) => {
|
|
const actual = entrySources(runtimeGraph("plugins/runtime/index"))[entry];
|
|
if (typeof actual !== "string") {
|
|
throw new Error(`expected one source for ${entry}`);
|
|
}
|
|
expect(path.resolve(actual)).toBe(path.resolve(source));
|
|
});
|
|
|
|
it("bundles SDK-owned helpers while retaining native package ownership", async () => {
|
|
for (const [graph, bundleFsSafe] of [
|
|
[runtimeGraph("plugins/runtime/index"), false],
|
|
[runtimeGraph("infra/sqlite-readonly-location.worker"), true],
|
|
] as const) {
|
|
const alwaysBundle = graph.deps?.alwaysBundle;
|
|
const external = (await inputOptions(graph))?.external;
|
|
if (typeof alwaysBundle !== "function" || typeof external !== "function") {
|
|
throw new Error("expected runtime graph dependency predicates");
|
|
}
|
|
|
|
expect(alwaysBundle("@openclaw/fs-safe/path", undefined)).toBe(bundleFsSafe);
|
|
expect(external("@openclaw/fs-safe/path", undefined, false)).toBe(!bundleFsSafe);
|
|
for (const specifier of ["@openclaw/proc-safe/windows-job"]) {
|
|
expect(alwaysBundle(specifier.split("/").slice(0, 2).join("/"), undefined)).toBe(false);
|
|
expect(alwaysBundle(specifier, undefined)).toBe(false);
|
|
expect(external(specifier, undefined, false)).toBe(true);
|
|
}
|
|
expect(alwaysBundle("openclaw/plugin-sdk/ssrf-runtime-internal", undefined)).toBe(true);
|
|
expect(alwaysBundle("openclaw/plugin-sdk/ssrf-runtime", undefined)).toBe(false);
|
|
expect(alwaysBundle("zod", undefined)).toBe(true);
|
|
expect(alwaysBundle("zod/v4/core", undefined)).toBe(true);
|
|
for (const id of ["typebox", "typebox/schema"]) {
|
|
expect(alwaysBundle(id, undefined)).toBe(false);
|
|
expect(external(id, undefined, false)).toBe(true);
|
|
}
|
|
expect(alwaysBundle("not-a-runtime-dependency", undefined)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("suppresses unresolved imports from extension source", async () => {
|
|
const configured = (await inputOptions(runtimeGraph("plugins/runtime/index")))?.onLog;
|
|
const handled: unknown[] = [];
|
|
|
|
configured?.(
|
|
"warn",
|
|
{
|
|
code: "UNRESOLVED_IMPORT",
|
|
message: "Could not resolve '@azure/identity' in extensions/msteams/src/sdk.ts",
|
|
},
|
|
(_level, log) => handled.push(log),
|
|
);
|
|
|
|
expect(handled).toStrictEqual([]);
|
|
});
|
|
});
|