mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 08:10:43 +00:00
Merge origin/main into nix-store plugin hardlinks
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -654,6 +654,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/current-time: split UTC into a separate `Reference UTC:` prompt line so local `Current time:` stays anchored to the user's timezone. (#42654) Thanks @chencheng-li.
|
||||
- Agents/reasoning: keep embedded reasoning deltas raw for correct same-line streaming while preserving formatted Telegram, Feishu, Discord, and heartbeat delivery at the channel edge. (#78397) Thanks @medns.
|
||||
- Agents/failover: rotate auth profiles before deferred cooldown marking on rate-limit failures, so file-lock contention cannot stall profile failover. Fixes #57281. (#57283) Thanks @jeremyknows.
|
||||
- Gateway/sessions: when `session.dmScope: "main"` is configured, route a bare webchat `/new` against the agent's main session (`sessions.create` with `emitCommandHooks=true`) to an in-place reset instead of creating a parallel `dashboard:` child, matching `/new` behavior on Telegram/Discord. Fixes #77434. (#71170) Thanks @statxc.
|
||||
|
||||
## 2026.5.3-1
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ Current source-of-truth:
|
||||
<AccordionGroup>
|
||||
<Accordion title="Sessions and runs">
|
||||
- `/new [model]` starts a new session; `/reset` is the reset alias.
|
||||
- Control UI intercepts typed `/new` to create and switch to a fresh dashboard session; typed `/reset` still runs the Gateway's in-place reset.
|
||||
- Control UI intercepts typed `/new` to create and switch to a fresh dashboard session, except when `session.dmScope: "main"` is configured and the current parent is the agent's main session; in that case `/new` resets the main session in place. Typed `/reset` still runs the Gateway's in-place reset.
|
||||
- `/reset soft [message]` keeps the current transcript, drops reused CLI backend session ids, and reruns startup/system-prompt loading in-place.
|
||||
- `/compact [instructions]` compacts the session context. See [Compaction](/concepts/compaction).
|
||||
- `/stop` aborts the current run.
|
||||
|
||||
@@ -165,7 +165,7 @@ Imported themes are stored only in the current browser profile. They are not wri
|
||||
- Consecutive duplicate text-only messages render as one bubble with a count badge. Messages that carry images, attachments, tool output, or canvas previews are left uncollapsed.
|
||||
- The chat header model and thinking pickers patch the active session immediately through `sessions.patch`; they are persistent session overrides, not one-turn-only send options.
|
||||
- If you send a message while a model picker change for the same session is still saving, the composer waits for that session patch before calling `chat.send` so the send uses the selected model.
|
||||
- Typing `/new` in the Control UI creates and switches to the same fresh dashboard session as New Chat. Typing `/reset` keeps the Gateway's explicit in-place reset for the current session.
|
||||
- Typing `/new` in the Control UI creates and switches to the same fresh dashboard session as New Chat, except when `session.dmScope: "main"` is configured and the current parent is the agent's main session; in that case it resets the main session in place. Typing `/reset` keeps the Gateway's explicit in-place reset for the current session.
|
||||
- The chat model picker requests the Gateway's configured model view. If `agents.defaults.models` is present, that allowlist drives the picker. Otherwise the picker shows explicit `models.providers.*.models` entries plus providers with usable auth. The full catalog stays available through the debug `models.list` RPC with `view: "all"`.
|
||||
- When fresh Gateway session usage reports include current context tokens, the chat composer area shows a compact context usage indicator. It switches to warning styling at high context pressure and, at recommended compaction levels, shows a compact button that runs the normal session compaction path. Stale token snapshots are hidden until the Gateway reports fresh usage again.
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ describeLive("browser (live): remote CDP tab persistence", () => {
|
||||
await pw.closePlaywrightBrowserConnection().catch(() => {});
|
||||
|
||||
const created = await pw.createPageViaPlaywright({ cdpUrl: CDP_URL, url: "about:blank" });
|
||||
expect(created.targetId).toEqual(expect.any(String));
|
||||
expect(created.targetId).toBeTypeOf("string");
|
||||
expect(created.targetId).not.toBe("");
|
||||
try {
|
||||
await waitFor(
|
||||
async () => {
|
||||
|
||||
@@ -3,8 +3,9 @@ import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import { createWikiApplyTool } from "./tool.js";
|
||||
|
||||
function asSchemaObject(value: unknown): Record<string, unknown> {
|
||||
expect(value).toBeTypeOf("object");
|
||||
expect(typeof value).toBe("object");
|
||||
expect(value).not.toBeNull();
|
||||
expect(Array.isArray(value)).toBe(false);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1226,7 +1226,8 @@ describe("slack slash command session metadata", () => {
|
||||
};
|
||||
expect(call.ctx?.OriginatingChannel).toBe("slack");
|
||||
expect(call.ctx?.GroupSpace).toBe("T1");
|
||||
expect(call.sessionKey).toEqual(expect.any(String));
|
||||
expect(call.sessionKey).toBeTypeOf("string");
|
||||
expect(call.sessionKey).not.toBe("");
|
||||
});
|
||||
|
||||
it("awaits session metadata persistence before dispatch", async () => {
|
||||
|
||||
@@ -271,6 +271,7 @@ function expectCopilotProviderFromPlan(
|
||||
plan.action === "write"
|
||||
? (JSON.parse(plan.contents) as { providers?: Record<string, unknown> })
|
||||
: {};
|
||||
expect(parsed.providers?.["github-copilot"]).toEqual(expect.any(Object));
|
||||
expect(parsed.providers?.["github-copilot"]).toBeDefined();
|
||||
expect(parsed.providers?.["github-copilot"]).not.toBeNull();
|
||||
return expect(parsed.providers?.["github-copilot"]);
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ describeLive("pi embedded extra params (live)", () => {
|
||||
}
|
||||
}
|
||||
|
||||
expect(stopReason).toEqual(expect.any(String));
|
||||
expect(outputTokens).toEqual(expect.any(Number));
|
||||
expect(stopReason).toBeTypeOf("string");
|
||||
expect(outputTokens).toBeTypeOf("number");
|
||||
// Should respect maxTokens from config (16) — allow a small buffer for provider rounding.
|
||||
expect(outputTokens ?? 0).toBeLessThanOrEqual(20);
|
||||
}, 30_000);
|
||||
|
||||
@@ -201,7 +201,8 @@ describe("buildContextEngineMaintenanceRuntimeContext", () => {
|
||||
{ entryId: "entry-1", message: { role: "user", content: "hi", timestamp: 1 } },
|
||||
],
|
||||
});
|
||||
expect(rewritePromise).toEqual(expect.any(Promise));
|
||||
expect(rewritePromise).toBeDefined();
|
||||
expect(rewritePromise?.then).toBeTypeOf("function");
|
||||
|
||||
await flushAsyncWork();
|
||||
expect(rewriteTranscriptEntriesInSessionFileMock).not.toHaveBeenCalled();
|
||||
|
||||
@@ -156,7 +156,7 @@ describe("AgentRuntimePlan", () => {
|
||||
|
||||
expect(normalized).toHaveLength(1);
|
||||
expect(normalized[0]?.name).toBe("ping");
|
||||
expect(normalized[0]?.parameters).toBeTypeOf("object");
|
||||
expect(normalized[0]?.parameters).toStrictEqual({});
|
||||
});
|
||||
|
||||
it("does not forward OpenAI API-key profiles into the Codex harness auth slot", () => {
|
||||
|
||||
@@ -119,7 +119,7 @@ describeLive("xai live", () => {
|
||||
const doneMessage = await collectDoneMessage(
|
||||
stream as AsyncIterable<{ type: string; message?: AssistantLikeMessage }>,
|
||||
);
|
||||
expect(doneMessage.content).toEqual(expect.any(Array));
|
||||
expect(Array.isArray(doneMessage.content)).toBe(true);
|
||||
const payload = requireLiveValue(capturedPayload, "captured xAI payload");
|
||||
if ("tool_stream" in payload) {
|
||||
expect(payload.tool_stream).toBe(true);
|
||||
|
||||
@@ -389,7 +389,7 @@ describe("buildInboundUserContextPrefix", () => {
|
||||
} as TemplateContext);
|
||||
|
||||
const conversationInfo = parseConversationInfoPayload(text);
|
||||
expect(conversationInfo["timestamp"]).toBe("Sun 2026-02-15 13:35 GMT");
|
||||
expect(conversationInfo["timestamp"]).toMatch(/^Sun 2026-02-15 13:35 (?:GMT|UTC)$/);
|
||||
});
|
||||
|
||||
it("honors envelope user timezone for conversation timestamps", () => {
|
||||
|
||||
@@ -673,8 +673,10 @@ describe("config cli", () => {
|
||||
properties?: Record<string, unknown>;
|
||||
};
|
||||
expect(payload.properties?.$schema).toEqual({ type: "string" });
|
||||
expect(payload.properties?.channels).toBeTypeOf("object");
|
||||
expect(payload.properties?.channels).not.toBeNull();
|
||||
expect(payload.properties?.channels).toMatchObject({
|
||||
type: "object",
|
||||
properties: { telegram: { type: "object" } },
|
||||
});
|
||||
expect(payload.properties?.plugins).toBeUndefined();
|
||||
expect(mockError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -174,7 +174,8 @@ describe("doctor command", () => {
|
||||
throw new Error("Expected doctor to write migrated auth profiles");
|
||||
}
|
||||
const profiles = (written.auth as { profiles: Record<string, unknown> }).profiles;
|
||||
expect(profiles["anthropic:me@example.com"]).toEqual(expect.any(Object));
|
||||
expect(profiles).toHaveProperty("anthropic:me@example.com");
|
||||
expect(profiles["anthropic:me@example.com"]).not.toBeNull();
|
||||
expect(profiles["anthropic:default"]).toBeUndefined();
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
@@ -20,6 +20,15 @@ function createRuntime(): RuntimeEnv {
|
||||
} as unknown as RuntimeEnv;
|
||||
}
|
||||
|
||||
const zeroTaskAuditCounts = {
|
||||
delivery_failed: 0,
|
||||
inconsistent_timestamps: 0,
|
||||
lost: 0,
|
||||
missing_cleanup: 0,
|
||||
stale_queued: 0,
|
||||
stale_running: 0,
|
||||
};
|
||||
|
||||
async function withTaskCommandStateDir(run: () => Promise<void>): Promise<void> {
|
||||
await withOpenClawTestState(
|
||||
{ layout: "state-only", prefix: "openclaw-tasks-command-" },
|
||||
@@ -150,11 +159,9 @@ describe("tasks commands", () => {
|
||||
|
||||
expect(payload.mode).toBe("preview");
|
||||
expect(payload.maintenance.taskFlows.pruned).toBe(1);
|
||||
expect(payload.auditBefore.byCode).toBeTypeOf("object");
|
||||
expect(Array.isArray(payload.auditBefore.byCode)).toBe(false);
|
||||
expect(payload.auditBefore.byCode).toStrictEqual(zeroTaskAuditCounts);
|
||||
expect(payload.auditBefore.taskFlows.byCode.stale_running).toBe(0);
|
||||
expect(payload.auditAfter.byCode).toBeTypeOf("object");
|
||||
expect(Array.isArray(payload.auditAfter.byCode)).toBe(false);
|
||||
expect(payload.auditAfter.byCode).toStrictEqual(zeroTaskAuditCounts);
|
||||
expect(payload.auditAfter.taskFlows.byCode.stale_running).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,8 +128,10 @@ describe("CronService read ops while job is running", () => {
|
||||
await isolatedRun.runStarted;
|
||||
expect(isolatedRun.runIsolatedAgentJob).toHaveBeenCalledTimes(1);
|
||||
|
||||
await expect(cron.list({ includeDisabled: true })).resolves.toBeTypeOf("object");
|
||||
await expect(cron.status()).resolves.toBeTypeOf("object");
|
||||
await expect(cron.list({ includeDisabled: true })).resolves.toHaveLength(1);
|
||||
await expect(cron.status()).resolves.toEqual(
|
||||
expect.objectContaining({ enabled: true, storePath: store.storePath }),
|
||||
);
|
||||
|
||||
const running = await cron.list({ includeDisabled: true });
|
||||
expect(running[0]?.state.runningAtMs).toBeTypeOf("number");
|
||||
@@ -197,7 +199,7 @@ describe("CronService read ops while job is running", () => {
|
||||
|
||||
await expect(
|
||||
withTimeout(cron.list({ includeDisabled: true }), 300, "cron.list during cron.run"),
|
||||
).resolves.toBeTypeOf("object");
|
||||
).resolves.toHaveLength(1);
|
||||
await expect(withTimeout(cron.status(), 300, "cron.status during cron.run")).resolves.toEqual(
|
||||
expect.objectContaining({ enabled: true, storePath: store.storePath }),
|
||||
);
|
||||
@@ -258,7 +260,7 @@ describe("CronService read ops while job is running", () => {
|
||||
|
||||
await expect(
|
||||
withTimeout(cron.list({ includeDisabled: true }), 300, "cron.list during startup"),
|
||||
).resolves.toBeTypeOf("object");
|
||||
).resolves.toHaveLength(1);
|
||||
await expect(withTimeout(cron.status(), 300, "cron.status during startup")).resolves.toEqual(
|
||||
expect.objectContaining({ enabled: true, storePath: store.storePath }),
|
||||
);
|
||||
|
||||
@@ -42,8 +42,9 @@ describe("ClawHub plugin docs", () => {
|
||||
|
||||
expect(validateExternalCodePluginPackageJson(packageJson).issues).toEqual([]);
|
||||
expect(typeof pluginManifest.id).toBe("string");
|
||||
expect(pluginManifest.configSchema).toBeTypeOf("object");
|
||||
expect(typeof pluginManifest.configSchema).toBe("object");
|
||||
expect(pluginManifest.configSchema).not.toBeNull();
|
||||
expect(Array.isArray(pluginManifest.configSchema)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not tell plugin authors to use bare clawhub publish", async () => {
|
||||
|
||||
@@ -47,7 +47,8 @@ function asRecord(value: unknown): Record<string, unknown> {
|
||||
}
|
||||
|
||||
function expectRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
expect(value, label).toEqual(expect.any(Object));
|
||||
expect(typeof value, label).toBe("object");
|
||||
expect(value, label).not.toBeNull();
|
||||
expect(Array.isArray(value), label).toBe(false);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,10 @@ describe("gateway cli backend live helpers", () => {
|
||||
token: "gateway-token",
|
||||
});
|
||||
|
||||
expect(client).toEqual(expect.any(Object));
|
||||
expect(typeof client).toBe("object");
|
||||
expect(client).not.toBeNull();
|
||||
expect(typeof (client as { start?: unknown }).start).toBe("function");
|
||||
expect(typeof (client as { stopAndWait?: unknown }).stopAndWait).toBe("function");
|
||||
expect(gatewayClientState.lastOptions).toMatchObject({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
token: "gateway-token",
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
type SessionEntry,
|
||||
updateSessionStore,
|
||||
} from "../../config/sessions.js";
|
||||
import { resolveAgentMainSessionKey } from "../../config/sessions/main-session.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import {
|
||||
createInternalHookEvent,
|
||||
@@ -1023,6 +1024,46 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
canonicalParentSessionKey = parent.canonicalKey;
|
||||
}
|
||||
if (
|
||||
canonicalParentSessionKey &&
|
||||
p.emitCommandHooks === true &&
|
||||
!requestedKey &&
|
||||
!resolveOptionalInitialSessionMessage(p) &&
|
||||
cfg.session?.dmScope === "main"
|
||||
) {
|
||||
const parentAgentId = normalizeAgentId(
|
||||
resolveAgentIdFromSessionKey(canonicalParentSessionKey) ?? resolveDefaultAgentId(cfg),
|
||||
);
|
||||
const parentMainKey = resolveAgentMainSessionKey({ cfg, agentId: parentAgentId });
|
||||
if (canonicalParentSessionKey === parentMainKey) {
|
||||
const { performGatewaySessionReset } = await loadSessionsRuntimeModule();
|
||||
const resetResult = await performGatewaySessionReset({
|
||||
key: canonicalParentSessionKey,
|
||||
reason: "new",
|
||||
commandSource: "webchat",
|
||||
});
|
||||
if (!resetResult.ok) {
|
||||
respond(false, undefined, resetResult.error);
|
||||
return;
|
||||
}
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
key: resetResult.key,
|
||||
sessionId: resetResult.entry.sessionId,
|
||||
entry: resetResult.entry,
|
||||
runStarted: false,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
emitSessionsChanged(context, {
|
||||
sessionKey: resetResult.key,
|
||||
reason: "new",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (canonicalParentSessionKey && p.emitCommandHooks === true) {
|
||||
const { entry: parentEntry } = loadSessionEntry(canonicalParentSessionKey);
|
||||
const parentAgentId = normalizeAgentId(
|
||||
|
||||
@@ -217,7 +217,7 @@ describe("gateway auth compatibility baseline", () => {
|
||||
});
|
||||
expect(rotated.ok).toBe(true);
|
||||
const rotatedToken = rotated.ok ? rotated.entry.token : "";
|
||||
expect(rotatedToken).toEqual(expect.any(String));
|
||||
expect(rotatedToken).toBeTypeOf("string");
|
||||
expect(rotatedToken.length).toBeGreaterThan(0);
|
||||
|
||||
const ws = await openWs(port);
|
||||
|
||||
@@ -167,7 +167,7 @@ describe("gateway server health/presence", () => {
|
||||
await localHarness.close();
|
||||
const evt = await shutdownP;
|
||||
const evtPayload = evt.payload as { reason?: unknown } | undefined;
|
||||
expect(evtPayload?.reason).toEqual(expect.any(String));
|
||||
expect(evtPayload?.reason).toBe("gateway stopping");
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { embeddedRunMock, writeSessionStore } from "./test-helpers.js";
|
||||
import { embeddedRunMock, testState, writeSessionStore } from "./test-helpers.js";
|
||||
import {
|
||||
setupGatewaySessionsTestHarness,
|
||||
bootstrapCacheMocks,
|
||||
@@ -410,6 +410,68 @@ test("sessions.create with emitCommandHooks=true emits reset lifecycle hooks aga
|
||||
);
|
||||
});
|
||||
|
||||
test("sessions.create with emitCommandHooks=true resets parent in place when session.dmScope is 'main' (#77434)", async () => {
|
||||
const { dir } = await createSessionStoreDir();
|
||||
const transcriptPath = path.join(dir, "sess-parent-dms.jsonl");
|
||||
await fs.writeFile(
|
||||
transcriptPath,
|
||||
`${JSON.stringify({
|
||||
type: "message",
|
||||
id: "m1",
|
||||
message: { role: "user", content: "hello before /new" },
|
||||
})}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
testState.sessionConfig = { dmScope: "main" };
|
||||
try {
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-parent-dms",
|
||||
sessionFile: transcriptPath,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await directSessionReq<{
|
||||
ok: boolean;
|
||||
key: string;
|
||||
sessionId: string;
|
||||
runStarted: boolean;
|
||||
}>("sessions.create", {
|
||||
parentSessionKey: "main",
|
||||
emitCommandHooks: true,
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
// Reset-in-place: response key matches the parent main key, NOT a dashboard child.
|
||||
expect(result.payload?.key).toBe("agent:main:main");
|
||||
expect(result.payload?.runStarted).toBe(false);
|
||||
expect(result.payload?.sessionId).not.toBe("sess-parent-dms");
|
||||
|
||||
expect(sessionLifecycleHookMocks.runSessionEnd).toHaveBeenCalledTimes(1);
|
||||
expect(sessionLifecycleHookMocks.runSessionStart).toHaveBeenCalledTimes(1);
|
||||
const [endEvent] = (
|
||||
sessionLifecycleHookMocks.runSessionEnd.mock.calls as unknown as Array<[unknown, unknown]>
|
||||
)[0] ?? [undefined, undefined];
|
||||
const [startEvent] = (
|
||||
sessionLifecycleHookMocks.runSessionStart.mock.calls as unknown as Array<[unknown, unknown]>
|
||||
)[0] ?? [undefined, undefined];
|
||||
expect(endEvent).toMatchObject({
|
||||
sessionId: "sess-parent-dms",
|
||||
sessionKey: "agent:main:main",
|
||||
reason: "new",
|
||||
});
|
||||
expect(startEvent).toMatchObject({
|
||||
sessionKey: "agent:main:main",
|
||||
resumedFrom: "sess-parent-dms",
|
||||
});
|
||||
} finally {
|
||||
testState.sessionConfig = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
test("sessions.create without emitCommandHooks does not fire command:new hook (#76957)", async () => {
|
||||
const { dir } = await createSessionStoreDir();
|
||||
await writeSingleLineSession(dir, "sess-parent2", "hello from parent 2");
|
||||
|
||||
@@ -383,8 +383,11 @@ describe("gateway talk.config", () => {
|
||||
// the UI keeps the SecretRef context, but every field becomes the
|
||||
// sentinel so no credential material leaks to read-scope callers.
|
||||
const redactedApiKey = talk?.providers?.[GENERIC_TALK_PROVIDER_ID]?.apiKey;
|
||||
expect(redactedApiKey).toBeTypeOf("object");
|
||||
expect((redactedApiKey as SecretRef).id).toBe("__OPENCLAW_REDACTED__");
|
||||
expect(redactedApiKey).toEqual({
|
||||
id: "__OPENCLAW_REDACTED__",
|
||||
provider: "__OPENCLAW_REDACTED__",
|
||||
source: "__OPENCLAW_REDACTED__",
|
||||
});
|
||||
expect(talk?.resolved?.config?.apiKey).toEqual(redactedApiKey);
|
||||
});
|
||||
|
||||
|
||||
@@ -74,8 +74,7 @@ function getDispatcherClassName(value: unknown): string | null {
|
||||
}
|
||||
|
||||
function expectDispatcherAttached(value: unknown): void {
|
||||
expect(value).toBeTypeOf("object");
|
||||
expect(value).not.toBeNull();
|
||||
expect(getDispatcherClassName(value)).toMatch(/^(Agent|Mock)$/u);
|
||||
}
|
||||
|
||||
function getSecondRequestHeaders(fetchImpl: ReturnType<typeof vi.fn>): Headers {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { VERSION } from "../version.js";
|
||||
import {
|
||||
composeProviderStreamWrappers as composeProviderStreamWrappersShared,
|
||||
createMoonshotThinkingWrapper as createMoonshotThinkingWrapperShared,
|
||||
@@ -239,8 +240,11 @@ describe("buildProviderStreamFamilyHooks", () => {
|
||||
config: { thinkingConfig: { thinkingBudget: -1 } },
|
||||
service_tier: "flex",
|
||||
});
|
||||
expect(capturedHeaders).toBeTypeOf("object");
|
||||
expect(capturedHeaders).not.toBeNull();
|
||||
expect(capturedHeaders).toEqual({
|
||||
"User-Agent": `openclaw/${VERSION}`,
|
||||
originator: "openclaw",
|
||||
version: VERSION,
|
||||
});
|
||||
|
||||
const openRouterHooks = OPENROUTER_THINKING_STREAM_HOOKS;
|
||||
void requireStreamFn(
|
||||
|
||||
@@ -454,8 +454,9 @@ describe("bundled plugin metadata", () => {
|
||||
|
||||
it("keeps config schemas on all bundled plugin manifests", () => {
|
||||
for (const entry of listRepoBundledPluginMetadata()) {
|
||||
expect(entry.manifest.configSchema).toBeTypeOf("object");
|
||||
expect(typeof entry.manifest.configSchema).toBe("object");
|
||||
expect(entry.manifest.configSchema).not.toBeNull();
|
||||
expect(Array.isArray(entry.manifest.configSchema)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ describe("cli json stdout contract", () => {
|
||||
delete env.OPENCLAW_CONFIG_PATH;
|
||||
delete env.VITEST;
|
||||
|
||||
const entry = path.resolve(process.cwd(), "openclaw.mjs");
|
||||
const entry = path.resolve(process.cwd(), "src/entry.ts");
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[entry, "update", "status", "--json", "--timeout", "1"],
|
||||
["--import", "tsx", entry, "update", "status", "--json", "--timeout", "1"],
|
||||
{ cwd: process.cwd(), env, encoding: "utf8" },
|
||||
);
|
||||
|
||||
@@ -34,7 +34,14 @@ describe("cli json stdout contract", () => {
|
||||
const stdout = result.stdout.trim();
|
||||
expect(stdout.length).toBeGreaterThan(0);
|
||||
const parsed = JSON.parse(stdout) as unknown;
|
||||
expect(parsed).toEqual(expect.any(Object));
|
||||
expect(typeof parsed).toBe("object");
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(Array.isArray(parsed)).toBe(false);
|
||||
expect(Object.keys(parsed as Record<string, unknown>).sort()).toEqual([
|
||||
"availability",
|
||||
"channel",
|
||||
"update",
|
||||
]);
|
||||
expect(stdout).not.toContain("Doctor warnings");
|
||||
expect(stdout).not.toContain("Doctor changes");
|
||||
expect(stdout).not.toContain("Config invalid");
|
||||
|
||||
@@ -481,7 +481,7 @@ console.log(JSON.stringify(result));
|
||||
) as { status: number; stdout: string };
|
||||
|
||||
expect(result.status).toBe(124);
|
||||
expect(result.stdout).toEqual(expect.any(String));
|
||||
expect(result.stdout).toBeTypeOf("string");
|
||||
});
|
||||
|
||||
it("runs the Windows agent turn through the detached done-file runner", () => {
|
||||
|
||||
@@ -571,8 +571,12 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
|
||||
const persisted = JSON.parse(localStorage.getItem(scopedKey) ?? "{}");
|
||||
|
||||
expect(persisted.sessionsByGateway).toEqual(expect.any(Object));
|
||||
const scopes = Object.keys(persisted.sessionsByGateway);
|
||||
const sessionsByGateway = persisted.sessionsByGateway as unknown;
|
||||
expect(typeof sessionsByGateway).toBe("object");
|
||||
expect(sessionsByGateway).not.toBeNull();
|
||||
expect(Array.isArray(sessionsByGateway)).toBe(false);
|
||||
const scopedSessions = sessionsByGateway as Record<string, unknown>;
|
||||
const scopes = Object.keys(scopedSessions);
|
||||
expect(scopes).toHaveLength(10);
|
||||
// oldest stale entries should be evicted
|
||||
expect(scopes).not.toContain("wss://stale-0.example:8443");
|
||||
@@ -580,7 +584,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
// newest stale entries and the current gateway should be retained
|
||||
expect(scopes).toContain("wss://stale-10.example:8443");
|
||||
expect(scopes).toContain("wss://gateway.example:8443");
|
||||
expect(persisted.sessionsByGateway["wss://gateway.example:8443"]).toEqual({
|
||||
expect(scopedSessions["wss://gateway.example:8443"]).toEqual({
|
||||
sessionKey: "agent:current:main",
|
||||
lastActiveSessionKey: "agent:current:main",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user