mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-27 06:31:13 +00:00
* feat(gateway-protocol): add session placement schema Closed state discriminator for session execution placement (local/requested/provisioning/syncing/starting/active/draining/reconciling/reclaimed/failed), sessions.dispatch params, and worker-admission transcript/live cursor extensions. Swift protocol models mirror the schema. * feat(state): add worker session placement table worker_session_placements rows carry placement state, transition generation, worker ownership metadata, ACK cursors, and the turn claim columns used for atomic admission. * feat(cloud-workers): add durable placement state machine store SQLite-backed placement store split by concern: state table (placement-state), discriminated record types + shape invariants (placement-record), row codec + CAS transition values (placement-row-codec), atomic turn-claim admission/release/waiters (placement-turn-claims), and lifecycle CAS transitions (placement-store). * feat(cloud-workers): sync workspaces and attach sessions to worker environments Environment service session attachment + turn credentials, tunnel workspace commands over a dedicated SSH runner, and git/plain workspace sync into $HOME/.openclaw-worker/workspaces with an immutable manifest. Symlink escapes are rejected locally before transfer (macOS openrsync stat-fails them opaquely) and again by the remote manifest guard. * feat(worker): run one-shot embedded turns from launch descriptors Worker runtime executes a single embedded turn from a stdin launch descriptor and reports completed/failed/fenced on stdout for the gateway launcher. Terminal lifecycle live events are deferred past the final transcript flush; transcript projection helpers are shared via transcript-message instead of duplicated in the runtime. * feat(cloud-workers): dispatch placements and route worker turns Dispatch service drives local->requested->provisioning->syncing->starting->active with failure teardown (placement-dispatch-failure) and restart/runtime recovery incl. lost-worker reclaim (placement-dispatch-recovery). Worker turn launcher claims the placement turn atomically, builds a windowed launch descriptor (worker-turn-payload), runs the remote one-shot worker, and reconciles the committed transcript; agent runners route turns through the session placement admission provider. * feat(gateway): expose session placement RPCs and startup reconciliation sessions.dispatch RPC with lifecycle admission barriers, operator-facing placement projection on session listings, placement-aware session reset guard, and startup/interval reconciliation wiring for worker placements.
270 lines
7.5 KiB
TypeScript
270 lines
7.5 KiB
TypeScript
// Command startup policy tests cover which CLI commands require startup side effects.
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
resolveCliStartupPolicy,
|
|
shouldBypassConfigGuardForCommandPath,
|
|
} from "./command-startup-policy.js";
|
|
|
|
function resolvePolicy(params: {
|
|
argv?: string[];
|
|
commandPath: string[];
|
|
jsonOutputMode?: boolean;
|
|
env?: NodeJS.ProcessEnv;
|
|
routeMode?: boolean;
|
|
}) {
|
|
return resolveCliStartupPolicy({
|
|
jsonOutputMode: false,
|
|
...params,
|
|
});
|
|
}
|
|
|
|
describe("command-startup-policy", () => {
|
|
it("matches config guard bypass commands", () => {
|
|
expect(shouldBypassConfigGuardForCommandPath(["backup", "create"])).toBe(true);
|
|
expect(shouldBypassConfigGuardForCommandPath(["config"])).toBe(true);
|
|
expect(shouldBypassConfigGuardForCommandPath(["config", "validate"])).toBe(true);
|
|
expect(shouldBypassConfigGuardForCommandPath(["config", "schema"])).toBe(true);
|
|
expect(shouldBypassConfigGuardForCommandPath(["config", "set"])).toBe(false);
|
|
expect(shouldBypassConfigGuardForCommandPath(["status"])).toBe(false);
|
|
});
|
|
|
|
it("matches route-first config guard skip policy", () => {
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["status"],
|
|
jsonOutputMode: true,
|
|
routeMode: true,
|
|
}).skipConfigGuard,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["gateway", "status"],
|
|
routeMode: true,
|
|
}).skipConfigGuard,
|
|
).toBe(true);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["status"],
|
|
routeMode: true,
|
|
}).skipConfigGuard,
|
|
).toBe(false);
|
|
});
|
|
|
|
it("matches plugin preload policy", () => {
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["status"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["status"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["health"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["channels", "status"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["channels", "list"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["channels", "add"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["channels", "logs"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["message", "send"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["message", "send"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
argv: ["node", "openclaw", "agent", "--json"],
|
|
commandPath: ["agent"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
argv: ["node", "openclaw", "agent", "--json", "--local"],
|
|
commandPath: ["agent"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(true);
|
|
expect(
|
|
resolvePolicy({
|
|
argv: ["node", "openclaw", "agent"],
|
|
commandPath: ["agent"],
|
|
}).loadPlugins,
|
|
).toBe(true);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "list"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "list"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "bind"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "bindings"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "unbind"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "set-identity"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "delete"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
});
|
|
|
|
it("matches banner suppression policy", () => {
|
|
expect(resolvePolicy({ commandPath: ["update", "status"], env: {} }).hideBanner).toBe(true);
|
|
expect(resolvePolicy({ commandPath: ["completion"], env: {} }).hideBanner).toBe(true);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["status"],
|
|
env: {
|
|
...process.env,
|
|
OPENCLAW_HIDE_BANNER: "1",
|
|
},
|
|
}).hideBanner,
|
|
).toBe(true);
|
|
expect(resolvePolicy({ commandPath: ["status"], env: {} }).hideBanner).toBe(false);
|
|
});
|
|
|
|
it("uses process env banner suppression when startup env is omitted", () => {
|
|
const originalHideBanner = process.env.OPENCLAW_HIDE_BANNER;
|
|
try {
|
|
process.env.OPENCLAW_HIDE_BANNER = "1";
|
|
|
|
expect(
|
|
resolveCliStartupPolicy({
|
|
commandPath: ["status"],
|
|
jsonOutputMode: false,
|
|
}).hideBanner,
|
|
).toBe(true);
|
|
expect(
|
|
resolveCliStartupPolicy({
|
|
commandPath: ["status"],
|
|
jsonOutputMode: false,
|
|
env: {},
|
|
}).hideBanner,
|
|
).toBe(false);
|
|
} finally {
|
|
if (originalHideBanner === undefined) {
|
|
delete process.env.OPENCLAW_HIDE_BANNER;
|
|
} else {
|
|
process.env.OPENCLAW_HIDE_BANNER = originalHideBanner;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("aggregates startup policy for commander and route-first callers", () => {
|
|
expect(
|
|
resolveCliStartupPolicy({
|
|
commandPath: ["status"],
|
|
jsonOutputMode: true,
|
|
env: {},
|
|
}),
|
|
).toEqual({
|
|
suppressDoctorStdout: true,
|
|
hideBanner: false,
|
|
skipConfigGuard: false,
|
|
loadPlugins: false,
|
|
pluginRegistry: { scope: "channels" },
|
|
});
|
|
|
|
expect(
|
|
resolveCliStartupPolicy({
|
|
commandPath: ["status"],
|
|
jsonOutputMode: true,
|
|
env: {},
|
|
routeMode: true,
|
|
}),
|
|
).toEqual({
|
|
suppressDoctorStdout: true,
|
|
hideBanner: false,
|
|
skipConfigGuard: false,
|
|
loadPlugins: false,
|
|
pluginRegistry: { scope: "channels" },
|
|
});
|
|
});
|
|
|
|
it("suppresses startup stdout for the mcp serve protocol", () => {
|
|
expect(resolvePolicy({ commandPath: ["mcp", "serve"] }).suppressDoctorStdout).toBe(true);
|
|
});
|
|
|
|
it("reserves stdout for the node worker protocol", () => {
|
|
const policy = resolvePolicy({ commandPath: ["node", "worker"] });
|
|
|
|
expect(policy.hideBanner).toBe(true);
|
|
expect(policy.loadPlugins).toBe(false);
|
|
expect(policy.suppressDoctorStdout).toBe(true);
|
|
});
|
|
|
|
it("isolates cloud worker startup", () => {
|
|
const policy = resolvePolicy({ commandPath: ["worker"] });
|
|
|
|
expect(shouldBypassConfigGuardForCommandPath(["worker"])).toBe(true);
|
|
expect(policy.hideBanner).toBe(true);
|
|
expect(policy.loadPlugins).toBe(false);
|
|
expect(policy.suppressDoctorStdout).toBe(true);
|
|
});
|
|
|
|
it("suppresses startup stdout for the bare acp protocol", () => {
|
|
expect(resolvePolicy({ commandPath: ["acp"] }).suppressDoctorStdout).toBe(true);
|
|
});
|
|
|
|
it("keeps startup stdout for non-protocol commands", () => {
|
|
expect(resolvePolicy({ commandPath: ["mcp", "list"] }).suppressDoctorStdout).toBe(false);
|
|
expect(resolvePolicy({ commandPath: ["acp", "client"] }).suppressDoctorStdout).toBe(false);
|
|
expect(resolvePolicy({ commandPath: ["status"] }).suppressDoctorStdout).toBe(false);
|
|
});
|
|
});
|