mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(cli): report honest SQLite store labels and consistent build SHA in help (#111659)
This commit is contained in:
@@ -778,6 +778,8 @@ export async function writeCliStartupMetadata(options?: {
|
||||
: undefined;
|
||||
const reusableBrowserHelpText =
|
||||
reusableExisting &&
|
||||
bundleIdentity &&
|
||||
reusableExisting.rootHelpBundleSignature === bundleIdentity.signature &&
|
||||
reusableExisting.browserHelpSourceSignature === browserHelpSourceSignature &&
|
||||
typeof reusableExisting.browserHelpText === "string" &&
|
||||
reusableExisting.browserHelpText.length > 0
|
||||
@@ -785,6 +787,8 @@ export async function writeCliStartupMetadata(options?: {
|
||||
: undefined;
|
||||
const reusableSecretsHelpText =
|
||||
reusableExisting &&
|
||||
bundleIdentity &&
|
||||
reusableExisting.rootHelpBundleSignature === bundleIdentity.signature &&
|
||||
reusableExisting.secretsHelpSourceSignature === secretsHelpSourceSignature &&
|
||||
typeof reusableExisting.secretsHelpText === "string" &&
|
||||
reusableExisting.secretsHelpText.length > 0
|
||||
@@ -792,6 +796,8 @@ export async function writeCliStartupMetadata(options?: {
|
||||
: undefined;
|
||||
const reusableNodesHelpText =
|
||||
reusableExisting &&
|
||||
bundleIdentity &&
|
||||
reusableExisting.rootHelpBundleSignature === bundleIdentity.signature &&
|
||||
reusableExisting.nodesHelpSourceSignature === nodesHelpSourceSignature &&
|
||||
typeof reusableExisting.nodesHelpText === "string" &&
|
||||
reusableExisting.nodesHelpText.length > 0
|
||||
@@ -799,6 +805,8 @@ export async function writeCliStartupMetadata(options?: {
|
||||
: undefined;
|
||||
const reusableSubcommandHelpText =
|
||||
reusableExisting &&
|
||||
bundleIdentity &&
|
||||
reusableExisting.rootHelpBundleSignature === bundleIdentity.signature &&
|
||||
reusableExisting.subcommandHelpSourceSignature === subcommandHelpSourceSignature &&
|
||||
hasAllPrecomputedSubcommandHelpText(reusableExisting.subcommandHelpText)
|
||||
? (reusableExisting.subcommandHelpText as PrecomputedSubcommandHelpText)
|
||||
|
||||
@@ -227,7 +227,7 @@ describe("sessionsCleanupCommand", () => {
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(JSON.parse(logs[0] ?? "{}")).toEqual({
|
||||
agentId: "main",
|
||||
storePath: "/resolved/sessions.json",
|
||||
storePath: "/resolved/openclaw-agent.sqlite",
|
||||
mode: "enforce",
|
||||
dryRun: false,
|
||||
beforeCount: 3,
|
||||
@@ -262,9 +262,10 @@ describe("sessionsCleanupCommand", () => {
|
||||
});
|
||||
|
||||
it("delegates non-store enforcing cleanup through the Gateway writer when reachable", async () => {
|
||||
const remoteStorePath = "C:\\Users\\gateway\\.openclaw\\agents\\main\\sessions\\sessions.json";
|
||||
mocks.callGateway.mockResolvedValue({
|
||||
agentId: "main",
|
||||
storePath: "/resolved/sessions.json",
|
||||
storePath: remoteStorePath,
|
||||
mode: "enforce",
|
||||
dryRun: false,
|
||||
beforeCount: 3,
|
||||
@@ -298,7 +299,7 @@ describe("sessionsCleanupCommand", () => {
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(JSON.parse(logs[0] ?? "{}")).toEqual({
|
||||
agentId: "main",
|
||||
storePath: "/resolved/sessions.json",
|
||||
storePath: remoteStorePath,
|
||||
mode: "enforce",
|
||||
dryRun: false,
|
||||
beforeCount: 3,
|
||||
@@ -315,6 +316,32 @@ describe("sessionsCleanupCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves a Gateway-owned store path in human output", async () => {
|
||||
const remoteStorePath = "C:\\Users\\gateway\\.openclaw\\openclaw-agent.sqlite";
|
||||
mocks.callGateway.mockResolvedValue({
|
||||
agentId: "main",
|
||||
storePath: remoteStorePath,
|
||||
mode: "enforce",
|
||||
dryRun: false,
|
||||
beforeCount: 3,
|
||||
afterCount: 1,
|
||||
missing: 0,
|
||||
dmScopeRetired: 0,
|
||||
modelRunPruned: 0,
|
||||
pruned: 2,
|
||||
capped: 0,
|
||||
diskBudget: null,
|
||||
wouldMutate: true,
|
||||
applied: true,
|
||||
appliedCount: 1,
|
||||
});
|
||||
|
||||
const { runtime, logs } = makeRuntime();
|
||||
await sessionsCleanupCommand({ enforce: true }, runtime);
|
||||
|
||||
expectLogsToInclude(logs, `Session store: ${remoteStorePath}`);
|
||||
});
|
||||
|
||||
it("returns dry-run JSON without mutating the store", async () => {
|
||||
mocks.runSessionsCleanup.mockResolvedValue({
|
||||
mode: "warn",
|
||||
@@ -368,7 +395,7 @@ describe("sessionsCleanupCommand", () => {
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(JSON.parse(logs[0] ?? "{}")).toEqual({
|
||||
agentId: "main",
|
||||
storePath: "/resolved/sessions.json",
|
||||
storePath: "/resolved/openclaw-agent.sqlite",
|
||||
mode: "warn",
|
||||
dryRun: true,
|
||||
beforeCount: 2,
|
||||
@@ -440,7 +467,7 @@ describe("sessionsCleanupCommand", () => {
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(JSON.parse(logs[0] ?? "{}")).toEqual({
|
||||
agentId: "main",
|
||||
storePath: "/resolved/sessions.json",
|
||||
storePath: "/resolved/openclaw-agent.sqlite",
|
||||
mode: "warn",
|
||||
dryRun: true,
|
||||
beforeCount: 1,
|
||||
@@ -505,6 +532,7 @@ describe("sessionsCleanupCommand", () => {
|
||||
runtime,
|
||||
);
|
||||
|
||||
expectLogsToInclude(logs, "Session store: /resolved/openclaw-agent.sqlite");
|
||||
expectLogsToInclude(logs, "Planned session actions:");
|
||||
expectLogsToInclude(logs, "Would prune unreferenced artifacts: 2");
|
||||
const tableHeaderLines = logs.filter((line) => line.includes("Action") && line.includes("Key"));
|
||||
@@ -694,7 +722,7 @@ describe("sessionsCleanupCommand", () => {
|
||||
stores: [
|
||||
{
|
||||
agentId: "main",
|
||||
storePath: "/resolved/main-sessions.json",
|
||||
storePath: "/resolved/main-sessions.sqlite",
|
||||
mode: "warn",
|
||||
dryRun: true,
|
||||
beforeCount: 1,
|
||||
@@ -709,7 +737,7 @@ describe("sessionsCleanupCommand", () => {
|
||||
},
|
||||
{
|
||||
agentId: "work",
|
||||
storePath: "/resolved/work-sessions.json",
|
||||
storePath: "/resolved/work-sessions.work.sqlite",
|
||||
mode: "warn",
|
||||
dryRun: true,
|
||||
beforeCount: 1,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type SessionsCleanupOptions,
|
||||
type SessionsCleanupResult,
|
||||
} from "../config/sessions.js";
|
||||
import { resolveSqliteTargetFromSessionStorePath } from "../config/sessions/session-sqlite-target.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { callGateway, isGatewayTransportError } from "../gateway/call.js";
|
||||
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
|
||||
@@ -141,6 +142,15 @@ function renderLabelSummaries(params: {
|
||||
params.runtime.log(`Total: ${totalKept} kept, ${totalPruned} pruned`);
|
||||
}
|
||||
|
||||
function toDisplayedCleanupSummary(summary: SessionCleanupSummary): SessionCleanupSummary {
|
||||
return {
|
||||
...summary,
|
||||
storePath: resolveSqliteTargetFromSessionStorePath(summary.storePath, {
|
||||
agentId: summary.agentId,
|
||||
}).path,
|
||||
};
|
||||
}
|
||||
|
||||
function renderStoreDryRunPlan(params: {
|
||||
cfg: OpenClawConfig;
|
||||
summary: SessionCleanupSummary;
|
||||
@@ -149,10 +159,11 @@ function renderStoreDryRunPlan(params: {
|
||||
showAgentHeader: boolean;
|
||||
}) {
|
||||
const rich = isRich();
|
||||
const displaySummary = toDisplayedCleanupSummary(params.summary);
|
||||
if (params.showAgentHeader) {
|
||||
params.runtime.log(`Agent: ${params.summary.agentId}`);
|
||||
}
|
||||
params.runtime.log(`Session store: ${params.summary.storePath}`);
|
||||
params.runtime.log(`Session store: ${displaySummary.storePath}`);
|
||||
params.runtime.log(`Maintenance mode: ${params.summary.mode}`);
|
||||
params.runtime.log(
|
||||
`Entries: ${params.summary.beforeCount} -> ${params.summary.afterCount} (remove ${params.summary.beforeCount - params.summary.afterCount})`,
|
||||
@@ -202,6 +213,7 @@ function renderStoreDryRunPlan(params: {
|
||||
function renderAppliedSummaries(params: {
|
||||
summaries: SessionCleanupSummary[];
|
||||
runtime: RuntimeEnv;
|
||||
locallyOwned: boolean;
|
||||
}) {
|
||||
for (let i = 0; i < params.summaries.length; i += 1) {
|
||||
const summary = params.summaries[i];
|
||||
@@ -214,7 +226,10 @@ function renderAppliedSummaries(params: {
|
||||
if (params.summaries.length > 1) {
|
||||
params.runtime.log(`Agent: ${summary.agentId}`);
|
||||
}
|
||||
params.runtime.log(`Session store: ${summary.storePath}`);
|
||||
const storePath = params.locallyOwned
|
||||
? toDisplayedCleanupSummary(summary).storePath
|
||||
: summary.storePath;
|
||||
params.runtime.log(`Session store: ${storePath}`);
|
||||
params.runtime.log(`Applied maintenance. Current entries: ${summary.appliedCount ?? 0}`);
|
||||
if (summary.unreferencedArtifacts?.removedFiles) {
|
||||
params.runtime.log(
|
||||
@@ -226,14 +241,14 @@ function renderAppliedSummaries(params: {
|
||||
|
||||
async function maybeRunGatewayCleanup(
|
||||
opts: SessionsCleanupOptions,
|
||||
): Promise<SessionsCleanupResult | null> {
|
||||
): Promise<{ delegated: true; result: SessionsCleanupResult } | { delegated: false }> {
|
||||
if (opts.store || opts.dryRun) {
|
||||
// Explicit store paths and dry-runs must stay local; the gateway only owns
|
||||
// live in-process cleanup for default stores.
|
||||
return null;
|
||||
return { delegated: false };
|
||||
}
|
||||
try {
|
||||
return await callGateway<SessionsCleanupResult>({
|
||||
const result = await callGateway<SessionsCleanupResult>({
|
||||
method: "sessions.cleanup",
|
||||
params: {
|
||||
agent: opts.agent,
|
||||
@@ -247,11 +262,12 @@ async function maybeRunGatewayCleanup(
|
||||
clientName: GATEWAY_CLIENT_NAMES.CLI,
|
||||
requiredMethods: ["sessions.cleanup"],
|
||||
});
|
||||
return { delegated: true, result };
|
||||
} catch (error) {
|
||||
if (isGatewayTransportError(error)) {
|
||||
// A stopped gateway should not block local maintenance; fall back to the
|
||||
// on-disk session stores when transport is unavailable.
|
||||
return null;
|
||||
return { delegated: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -259,15 +275,19 @@ async function maybeRunGatewayCleanup(
|
||||
|
||||
/** Runs session cleanup, optionally using the live gateway for active stores. */
|
||||
export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runtime: RuntimeEnv) {
|
||||
const gatewayResult = await maybeRunGatewayCleanup(opts);
|
||||
if (gatewayResult) {
|
||||
const gatewayCleanup = await maybeRunGatewayCleanup(opts);
|
||||
if (gatewayCleanup.delegated) {
|
||||
// The Gateway owns this path. Preserve its syntax because resolving a remote
|
||||
// Windows path on a POSIX client (or vice versa) would fabricate a local path.
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, gatewayResult);
|
||||
writeRuntimeJson(runtime, gatewayCleanup.result);
|
||||
return;
|
||||
}
|
||||
renderAppliedSummaries({
|
||||
summaries: "stores" in gatewayResult ? gatewayResult.stores : [gatewayResult],
|
||||
summaries:
|
||||
"stores" in gatewayCleanup.result ? gatewayCleanup.result.stores : [gatewayCleanup.result],
|
||||
runtime,
|
||||
locallyOwned: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -298,7 +318,7 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
|
||||
serializeSessionCleanupResult({
|
||||
mode,
|
||||
dryRun: true,
|
||||
summaries: previewResults.map((result) => result.summary),
|
||||
summaries: previewResults.map((result) => toDisplayedCleanupSummary(result.summary)),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
@@ -325,11 +345,11 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
|
||||
serializeSessionCleanupResult({
|
||||
mode,
|
||||
dryRun: false,
|
||||
summaries: appliedSummaries,
|
||||
summaries: appliedSummaries.map(toDisplayedCleanupSummary),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
renderAppliedSummaries({ summaries: appliedSummaries, runtime });
|
||||
renderAppliedSummaries({ summaries: appliedSummaries, runtime, locallyOwned: true });
|
||||
}
|
||||
|
||||
@@ -156,8 +156,8 @@ describe("sessionsCommand default store agent selection", () => {
|
||||
expect(payload.count).toBe(2);
|
||||
expect(payload.allAgents).toBe(true);
|
||||
expect(payload.stores).toEqual([
|
||||
{ agentId: "main", path: "/tmp/shared-sessions.json" },
|
||||
{ agentId: "voice", path: "/tmp/shared-sessions.json" },
|
||||
{ agentId: "main", path: "/tmp/shared-sessions.sqlite" },
|
||||
{ agentId: "voice", path: "/tmp/shared-sessions.voice.sqlite" },
|
||||
]);
|
||||
expect(payload.sessions?.map((session) => session.agentId).toSorted()).toEqual([
|
||||
"main",
|
||||
@@ -177,7 +177,7 @@ describe("sessionsCommand default store agent selection", () => {
|
||||
agentId: "voice",
|
||||
storePath: "/tmp/sessions-voice.json",
|
||||
});
|
||||
expect(logs[0]).toContain("Session store: /tmp/sessions-voice.json");
|
||||
expect(logs[0]).toContain("Session store: /tmp/sessions-voice.voice.sqlite");
|
||||
});
|
||||
|
||||
it("uses all configured agent stores with --all-agents", async () => {
|
||||
|
||||
@@ -182,6 +182,28 @@ describe("sessionsCommand", () => {
|
||||
expect(group?.totalTokensFresh).toBe(false);
|
||||
});
|
||||
|
||||
it("reports the SQLite database for the store and SQLite-backed sessionFile", async () => {
|
||||
const store = await writeStore({
|
||||
main: {
|
||||
sessionId: "abc123",
|
||||
sessionFile: "sqlite:main:abc123:/tmp/openclaw/agents/main/sessions/sessions.json",
|
||||
updatedAt: Date.now() - 10 * 60_000,
|
||||
model: "test:opus",
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await runSessionsJson<{
|
||||
path?: string;
|
||||
sessions?: Array<{ key: string; sessionFile?: string }>;
|
||||
}>(sessionsCommand, store);
|
||||
|
||||
expect(payload.path).toMatch(/openclaw-agent\.sqlite$/u);
|
||||
expect(payload.path).not.toContain("sessions.json");
|
||||
expect(payload.sessions?.find((row) => row.key === "main")?.sessionFile).toBe(
|
||||
"sqlite:main:abc123:/tmp/openclaw/agents/main/agent/openclaw-agent.sqlite",
|
||||
);
|
||||
});
|
||||
|
||||
it("exports subagent lineage metadata in JSON output", async () => {
|
||||
const store = await writeStore({
|
||||
"agent:child:main": {
|
||||
|
||||
@@ -17,6 +17,11 @@ import { normalizeChatType } from "../channels/chat-type.js";
|
||||
import { getRuntimeConfig } from "../config/config.js";
|
||||
import { resolveSessionTotalTokens } from "../config/sessions.js";
|
||||
import { listSessionEntries } from "../config/sessions/session-accessor.js";
|
||||
import { resolveSqliteTargetFromSessionStorePath } from "../config/sessions/session-sqlite-target.js";
|
||||
import {
|
||||
formatSqliteSessionFileMarker,
|
||||
parseSqliteSessionFileMarker,
|
||||
} from "../config/sessions/sqlite-marker.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveStoredSessionKeyForAgentStore } from "../gateway/session-store-key.js";
|
||||
@@ -229,9 +234,24 @@ function formatRuntimeCell(runtimeLabel: string, rich: boolean): string {
|
||||
return rich ? theme.info(label) : label;
|
||||
}
|
||||
|
||||
function resolveSessionStoreDisplayPath(target: { agentId: string; storePath: string }): string {
|
||||
return resolveSqliteTargetFromSessionStorePath(target.storePath, {
|
||||
agentId: target.agentId,
|
||||
}).path;
|
||||
}
|
||||
|
||||
function toJsonSessionRow(row: SessionRow): Omit<SessionRow, "runtimeLabel"> {
|
||||
const { runtimeLabel, ...jsonRow } = row;
|
||||
void runtimeLabel;
|
||||
const marker = parseSqliteSessionFileMarker(jsonRow.sessionFile);
|
||||
if (marker) {
|
||||
jsonRow.sessionFile = formatSqliteSessionFileMarker({
|
||||
...marker,
|
||||
storePath: resolveSqliteTargetFromSessionStorePath(marker.storePath, {
|
||||
agentId: marker.agentId,
|
||||
}).path,
|
||||
});
|
||||
}
|
||||
return jsonRow;
|
||||
}
|
||||
|
||||
@@ -429,11 +449,11 @@ export async function sessionsCommand(
|
||||
const multi = targets.length > 1;
|
||||
const aggregate = aggregateAgents || multi;
|
||||
writeRuntimeJson(runtime, {
|
||||
path: aggregate ? null : (targets[0]?.storePath ?? null),
|
||||
path: aggregate || !targets[0] ? null : resolveSessionStoreDisplayPath(targets[0]),
|
||||
stores: aggregate
|
||||
? targets.map((target) => ({
|
||||
agentId: target.agentId,
|
||||
path: target.storePath,
|
||||
path: resolveSessionStoreDisplayPath(target),
|
||||
}))
|
||||
: undefined,
|
||||
allAgents: aggregateAgents ? true : undefined,
|
||||
@@ -476,8 +496,9 @@ export async function sessionsCommand(
|
||||
return;
|
||||
}
|
||||
|
||||
if (targets.length === 1 && !aggregateAgents) {
|
||||
runtime.log(info(`Session store: ${targets[0]?.storePath}`));
|
||||
const primaryTarget = targets[0];
|
||||
if (primaryTarget && targets.length === 1 && !aggregateAgents) {
|
||||
runtime.log(info(`Session store: ${resolveSessionStoreDisplayPath(primaryTarget)}`));
|
||||
} else {
|
||||
runtime.log(
|
||||
info(`Session stores: ${targets.length} (${targets.map((t) => t.agentId).join(", ")})`),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../routing/session-key.js
|
||||
/** SQLite database target resolved from a legacy session store path. */
|
||||
type ResolvedSqliteStoreTarget = {
|
||||
agentId?: string;
|
||||
path?: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
function resolveCustomStoreSqlitePath(params: {
|
||||
|
||||
@@ -64,12 +64,12 @@ function createDeferredIsolatedRun() {
|
||||
|
||||
function expectCronStatus(
|
||||
status: Awaited<ReturnType<CronService["status"]>>,
|
||||
params: { storePath: string; jobs: number },
|
||||
params: { jobs: number },
|
||||
) {
|
||||
expect(status.enabled).toBe(true);
|
||||
expect(status.storePath).toBe(params.storePath);
|
||||
expect(status.storage).toBe("sqlite");
|
||||
expect(status.sqlitePath).toContain("openclaw.sqlite");
|
||||
expect(status.storePath).toBe(status.sqlitePath);
|
||||
expect(status.jobs).toBe(params.jobs);
|
||||
if (status.nextWakeAtMs !== null) {
|
||||
expect(status.nextWakeAtMs).toBeTypeOf("number");
|
||||
@@ -129,7 +129,7 @@ describe("CronService read ops while job is running", () => {
|
||||
expect(isolatedRun.runIsolatedAgentJob).toHaveBeenCalledTimes(1);
|
||||
|
||||
await expect(cron.list({ includeDisabled: true })).resolves.toHaveLength(1);
|
||||
expectCronStatus(await cron.status(), { storePath: store.storePath, jobs: 1 });
|
||||
expectCronStatus(await cron.status(), { jobs: 1 });
|
||||
|
||||
const running = await cron.list({ includeDisabled: true });
|
||||
expect(running[0]?.state.runningAtMs).toBeTypeOf("number");
|
||||
@@ -205,7 +205,6 @@ describe("CronService read ops while job is running", () => {
|
||||
message: "cron.status during cron.run timed out",
|
||||
}),
|
||||
{
|
||||
storePath: store.storePath,
|
||||
jobs: 1,
|
||||
},
|
||||
);
|
||||
@@ -274,7 +273,6 @@ describe("CronService read ops while job is running", () => {
|
||||
message: "cron.status during startup timed out",
|
||||
}),
|
||||
{
|
||||
storePath: store.storePath,
|
||||
jobs: 1,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -320,11 +320,12 @@ export function resumeScheduling(state: CronServiceState) {
|
||||
export async function status(state: CronServiceState) {
|
||||
return await locked(state, async () => {
|
||||
await ensureLoadedForRead(state);
|
||||
const sqlitePath = resolveOpenClawStateSqlitePath();
|
||||
return {
|
||||
enabled: state.deps.cronEnabled,
|
||||
storePath: state.deps.storePath,
|
||||
storePath: sqlitePath,
|
||||
storage: "sqlite" as const,
|
||||
sqlitePath: resolveOpenClawStateSqlitePath(),
|
||||
sqlitePath,
|
||||
jobs: state.store?.jobs.length ?? 0,
|
||||
nextWakeAtMs: state.deps.cronEnabled ? (nextWakeAtMs(state) ?? null) : null,
|
||||
};
|
||||
|
||||
@@ -307,7 +307,7 @@ export type CronWakeMode = "now" | "next-heartbeat";
|
||||
/** Lightweight service status returned to gateway/control surfaces. */
|
||||
export type CronStatusSummary = {
|
||||
enabled: boolean;
|
||||
/** @deprecated Legacy partition key; actual storage is SQLite. Use `sqlitePath`. */
|
||||
/** @deprecated Alias for `sqlitePath`. */
|
||||
storePath: string;
|
||||
/** Storage backend identifier. */
|
||||
storage: "sqlite";
|
||||
|
||||
@@ -1395,7 +1395,7 @@ describe("gateway server cron", () => {
|
||||
| undefined;
|
||||
expect(statusPayload?.enabled).toBe(true);
|
||||
const storePath = typeof statusPayload?.storePath === "string" ? statusPayload.storePath : "";
|
||||
expect(storePath).toContain("jobs.json");
|
||||
expect(storePath).toContain("openclaw.sqlite");
|
||||
|
||||
const autoRes = await directCronReq(cronState, "cron.add", {
|
||||
name: "auto run test",
|
||||
|
||||
@@ -628,6 +628,23 @@ describe("write-cli-startup-metadata", () => {
|
||||
let renderCount = 0;
|
||||
let commandRenderCount = 0;
|
||||
|
||||
const renderSubcommandHelp = () => {
|
||||
commandRenderCount += 1;
|
||||
const buildInfo = JSON.parse(readFileSync(path.join(distDir, "build-info.json"), "utf8")) as {
|
||||
commit: string;
|
||||
version: string;
|
||||
};
|
||||
const banner = `OpenClaw ${buildInfo.version} (${buildInfo.commit.slice(0, 7)})`;
|
||||
return {
|
||||
doctor: `${banner}\nUsage: openclaw doctor\n`,
|
||||
gateway: `${banner}\nUsage: openclaw gateway\n`,
|
||||
models: `${banner}\nUsage: openclaw models\n`,
|
||||
plugins: `${banner}\nUsage: openclaw plugins\n`,
|
||||
sessions: `${banner}\nUsage: openclaw sessions\n`,
|
||||
tasks: `${banner}\nUsage: openclaw tasks\n`,
|
||||
};
|
||||
};
|
||||
|
||||
writeStartupMetadataSourceSignatureFixture(tempRoot);
|
||||
writeFixtureFile(distDir, "root-help-fixture.js", "export function outputRootHelp() {}\n");
|
||||
|
||||
@@ -653,17 +670,7 @@ describe("write-cli-startup-metadata", () => {
|
||||
commandRenderCount += 1;
|
||||
return "Usage: openclaw nodes\n";
|
||||
},
|
||||
renderSourceSubcommandHelpTextRecord: () => {
|
||||
commandRenderCount += 1;
|
||||
return {
|
||||
doctor: "Usage: openclaw doctor\n",
|
||||
gateway: "Usage: openclaw gateway\n",
|
||||
models: "Usage: openclaw models\n",
|
||||
plugins: "Usage: openclaw plugins\n",
|
||||
sessions: "Usage: openclaw sessions\n",
|
||||
tasks: "Usage: openclaw tasks\n",
|
||||
};
|
||||
},
|
||||
renderSourceSubcommandHelpTextRecord: renderSubcommandHelp,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -676,6 +683,7 @@ describe("write-cli-startup-metadata", () => {
|
||||
await writeMetadata();
|
||||
expect(renderCount).toBe(1);
|
||||
expect(commandRenderCount).toBe(4);
|
||||
expect(readFileSync(outputPath, "utf8")).toContain("OpenClaw 2026.7.2 (aaaaaaa)");
|
||||
|
||||
writeFixtureFile(
|
||||
distDir,
|
||||
@@ -684,7 +692,8 @@ describe("write-cli-startup-metadata", () => {
|
||||
);
|
||||
await writeMetadata();
|
||||
expect(renderCount).toBe(2);
|
||||
expect(commandRenderCount).toBe(4);
|
||||
expect(commandRenderCount).toBe(8);
|
||||
expect(readFileSync(outputPath, "utf8")).toContain("OpenClaw 2026.7.2 (bbbbbbb)");
|
||||
|
||||
writeFixtureFile(
|
||||
distDir,
|
||||
@@ -693,6 +702,10 @@ describe("write-cli-startup-metadata", () => {
|
||||
);
|
||||
await writeMetadata();
|
||||
expect(renderCount).toBe(3);
|
||||
expect(commandRenderCount).toBe(4);
|
||||
expect(commandRenderCount).toBe(12);
|
||||
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
|
||||
subcommandHelpText: { models: string };
|
||||
};
|
||||
expect(written.subcommandHelpText.models).toContain("OpenClaw 2026.7.3 (bbbbbbb)");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user