mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
test: reduce auth and subagent control hotspots
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./cli-credentials.js", () => ({
|
||||
readCodexCliCredentialsCached: () => null,
|
||||
@@ -20,36 +20,51 @@ import { calculateAuthProfileCooldownMs, markAuthProfileFailure } from "./auth-p
|
||||
|
||||
type AuthProfileStore = ReturnType<typeof ensureAuthProfileStore>;
|
||||
|
||||
let tempRoot = "";
|
||||
let tempCaseIndex = 0;
|
||||
|
||||
beforeAll(() => {
|
||||
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function makeAgentDir(label = "case") {
|
||||
tempCaseIndex += 1;
|
||||
const agentDir = path.join(tempRoot, `${tempCaseIndex}-${label}`);
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
return agentDir;
|
||||
}
|
||||
|
||||
async function withAuthProfileStore(
|
||||
fn: (ctx: { agentDir: string; store: AuthProfileStore }) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
try {
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-default",
|
||||
},
|
||||
"openrouter:default": {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key: "sk-or-default",
|
||||
},
|
||||
const agentDir = makeAgentDir("store");
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-default",
|
||||
},
|
||||
}),
|
||||
);
|
||||
"openrouter:default": {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key: "sk-or-default",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
await fn({ agentDir, store });
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
await fn({ agentDir, store });
|
||||
}
|
||||
|
||||
function expectCooldownInRange(remainingMs: number, minMs: number, maxMs: number): void {
|
||||
@@ -59,24 +74,11 @@ function expectCooldownInRange(remainingMs: number, minMs: number, maxMs: number
|
||||
|
||||
describe("markAuthProfileFailure", () => {
|
||||
it("does not overwrite fresher on-disk credentials with a stale runtime snapshot", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
try {
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-expired-old",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const staleRuntimeStore: AuthProfileStore = {
|
||||
const agentDir = makeAgentDir("stale-snapshot");
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
@@ -85,47 +87,55 @@ describe("markAuthProfileFailure", () => {
|
||||
key: "sk-expired-old",
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-fresh-new",
|
||||
},
|
||||
const staleRuntimeStore: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-expired-old",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-fresh-new",
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const staleCredential = staleRuntimeStore.profiles["openai:default"];
|
||||
expect(staleCredential?.type).toBe("api_key");
|
||||
expect(staleCredential && "key" in staleCredential ? staleCredential.key : undefined).toBe(
|
||||
"sk-expired-old",
|
||||
);
|
||||
const staleCredential = staleRuntimeStore.profiles["openai:default"];
|
||||
expect(staleCredential?.type).toBe("api_key");
|
||||
expect(staleCredential && "key" in staleCredential ? staleCredential.key : undefined).toBe(
|
||||
"sk-expired-old",
|
||||
);
|
||||
|
||||
await markAuthProfileFailure({
|
||||
store: staleRuntimeStore,
|
||||
profileId: "openai:default",
|
||||
reason: "rate_limit",
|
||||
agentDir,
|
||||
});
|
||||
await markAuthProfileFailure({
|
||||
store: staleRuntimeStore,
|
||||
profileId: "openai:default",
|
||||
reason: "rate_limit",
|
||||
agentDir,
|
||||
});
|
||||
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
const reloaded = ensureAuthProfileStore(agentDir);
|
||||
const reloadedCredential = reloaded.profiles["openai:default"];
|
||||
expect(reloadedCredential?.type).toBe("api_key");
|
||||
expect(
|
||||
reloadedCredential && "key" in reloadedCredential ? reloadedCredential.key : undefined,
|
||||
).toBe("sk-fresh-new");
|
||||
expect(typeof reloaded.usageStats?.["openai:default"]?.cooldownUntil).toBe("number");
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
const reloaded = ensureAuthProfileStore(agentDir);
|
||||
const reloadedCredential = reloaded.profiles["openai:default"];
|
||||
expect(reloadedCredential?.type).toBe("api_key");
|
||||
expect(
|
||||
reloadedCredential && "key" in reloadedCredential ? reloadedCredential.key : undefined,
|
||||
).toBe("sk-fresh-new");
|
||||
expect(typeof reloaded.usageStats?.["openai:default"]?.cooldownUntil).toBe("number");
|
||||
});
|
||||
|
||||
it("disables billing failures for ~5 hours by default", async () => {
|
||||
@@ -255,99 +265,91 @@ describe("markAuthProfileFailure", () => {
|
||||
});
|
||||
});
|
||||
it("resets backoff counters outside the failure window", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
try {
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
const now = Date.now();
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-default",
|
||||
},
|
||||
const agentDir = makeAgentDir("reset-window");
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
const now = Date.now();
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-default",
|
||||
},
|
||||
usageStats: {
|
||||
"anthropic:default": {
|
||||
errorCount: 9,
|
||||
failureCounts: { billing: 3 },
|
||||
lastFailureAt: now - 48 * 60 * 60 * 1000,
|
||||
},
|
||||
},
|
||||
usageStats: {
|
||||
"anthropic:default": {
|
||||
errorCount: 9,
|
||||
failureCounts: { billing: 3 },
|
||||
lastFailureAt: now - 48 * 60 * 60 * 1000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
await markAuthProfileFailure({
|
||||
store,
|
||||
profileId: "anthropic:default",
|
||||
reason: "billing",
|
||||
agentDir,
|
||||
cfg: {
|
||||
auth: { cooldowns: { failureWindowHours: 24 } },
|
||||
} as never,
|
||||
});
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
await markAuthProfileFailure({
|
||||
store,
|
||||
profileId: "anthropic:default",
|
||||
reason: "billing",
|
||||
agentDir,
|
||||
cfg: {
|
||||
auth: { cooldowns: { failureWindowHours: 24 } },
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(1);
|
||||
expect(store.usageStats?.["anthropic:default"]?.failureCounts?.billing).toBe(1);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(1);
|
||||
expect(store.usageStats?.["anthropic:default"]?.failureCounts?.billing).toBe(1);
|
||||
});
|
||||
|
||||
it("resets error count when previous cooldown has expired to prevent escalation", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
try {
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
const now = Date.now();
|
||||
// Simulate state left on disk after 3 rapid failures within a 1-min cooldown
|
||||
// window. The cooldown has since expired, but clearExpiredCooldowns() only
|
||||
// ran in-memory and never persisted — so disk still carries errorCount: 3.
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-default",
|
||||
},
|
||||
const agentDir = makeAgentDir("expired-cooldown");
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
const now = Date.now();
|
||||
// Simulate state left on disk after 3 rapid failures within a 1-min cooldown
|
||||
// window. The cooldown has since expired, but clearExpiredCooldowns() only
|
||||
// ran in-memory and never persisted - so disk still carries errorCount: 3.
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-default",
|
||||
},
|
||||
usageStats: {
|
||||
"anthropic:default": {
|
||||
errorCount: 3,
|
||||
failureCounts: { rate_limit: 3 },
|
||||
lastFailureAt: now - 120_000, // 2 minutes ago
|
||||
cooldownUntil: now - 60_000, // expired 1 minute ago
|
||||
},
|
||||
},
|
||||
usageStats: {
|
||||
"anthropic:default": {
|
||||
errorCount: 3,
|
||||
failureCounts: { rate_limit: 3 },
|
||||
lastFailureAt: now - 120_000, // 2 minutes ago
|
||||
cooldownUntil: now - 60_000, // expired 1 minute ago
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
await markAuthProfileFailure({
|
||||
store,
|
||||
profileId: "anthropic:default",
|
||||
reason: "rate_limit",
|
||||
agentDir,
|
||||
});
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
await markAuthProfileFailure({
|
||||
store,
|
||||
profileId: "anthropic:default",
|
||||
reason: "rate_limit",
|
||||
agentDir,
|
||||
});
|
||||
|
||||
const stats = store.usageStats?.["anthropic:default"];
|
||||
// Error count should reset to 1 (not escalate to 4) because the
|
||||
// previous cooldown expired. Cooldown should be ~30s, not ~5 min.
|
||||
expect(stats?.errorCount).toBe(1);
|
||||
expect(stats?.failureCounts?.rate_limit).toBe(1);
|
||||
const cooldownMs = (stats?.cooldownUntil ?? 0) - now;
|
||||
// calculateAuthProfileCooldownMs(1) = 30_000 (stepped: 30s → 1m → 5m)
|
||||
expect(cooldownMs).toBeLessThan(60_000);
|
||||
expect(cooldownMs).toBeGreaterThan(0);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
const stats = store.usageStats?.["anthropic:default"];
|
||||
// Error count should reset to 1 (not escalate to 4) because the
|
||||
// previous cooldown expired. Cooldown should be ~30s, not ~5 min.
|
||||
expect(stats?.errorCount).toBe(1);
|
||||
expect(stats?.failureCounts?.rate_limit).toBe(1);
|
||||
const cooldownMs = (stats?.cooldownUntil ?? 0) - now;
|
||||
// calculateAuthProfileCooldownMs(1) = 30_000 (stepped: 30s -> 1m -> 5m)
|
||||
expect(cooldownMs).toBeLessThan(60_000);
|
||||
expect(cooldownMs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("does not persist cooldown windows for OpenRouter profiles", async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resetFileLockStateForTest } from "../../infra/file-lock.js";
|
||||
import { captureEnv } from "../../test-utils/env.js";
|
||||
import { resolveApiKeyForProfile, resetOAuthRefreshQueuesForTest } from "./oauth.js";
|
||||
@@ -107,6 +107,11 @@ describe("OAuth refresh in-process queue", () => {
|
||||
]);
|
||||
let tempRoot = "";
|
||||
let agentDir = "";
|
||||
let caseIndex = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-oauth-queue-"));
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
resetFileLockStateForTest();
|
||||
@@ -115,9 +120,9 @@ describe("OAuth refresh in-process queue", () => {
|
||||
formatProviderAuthProfileApiKeyWithPluginMock.mockReset();
|
||||
formatProviderAuthProfileApiKeyWithPluginMock.mockReturnValue(undefined);
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-oauth-queue-"));
|
||||
process.env.OPENCLAW_STATE_DIR = tempRoot;
|
||||
agentDir = path.join(tempRoot, "agents", "main", "agent");
|
||||
const caseRoot = path.join(tempRoot, `case-${++caseIndex}`);
|
||||
process.env.OPENCLAW_STATE_DIR = caseRoot;
|
||||
agentDir = path.join(caseRoot, "agents", "main", "agent");
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
@@ -129,57 +134,10 @@ describe("OAuth refresh in-process queue", () => {
|
||||
resetFileLockStateForTest();
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
resetOAuthRefreshQueuesForTest();
|
||||
if (tempRoot) {
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("serializes concurrent same-PID callers FIFO", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
const provider = "openai-codex";
|
||||
saveAuthProfileStore(createExpiredOauthStore({ profileId, provider }), agentDir);
|
||||
|
||||
const order: number[] = [];
|
||||
let seq = 0;
|
||||
refreshProviderOAuthCredentialWithPluginMock.mockImplementation(async () => {
|
||||
const n = ++seq;
|
||||
order.push(n);
|
||||
// Small delay so concurrent callers have time to interleave if they can.
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
return {
|
||||
type: "oauth",
|
||||
provider,
|
||||
access: `refreshed-${n}`,
|
||||
refresh: `refreshed-refresh-${n}`,
|
||||
// Each refresh returns a token already expired again, so the next
|
||||
// queued caller also proceeds to refresh (proves the queue releases
|
||||
// cleanly and the next caller actually runs).
|
||||
expires: Date.now() - 1_000,
|
||||
} as never;
|
||||
});
|
||||
|
||||
// Fire three resolves concurrently against the same agent+profile.
|
||||
const results = await Promise.all([
|
||||
resolveApiKeyForProfileInTest({
|
||||
store: ensureAuthProfileStore(agentDir),
|
||||
profileId,
|
||||
agentDir,
|
||||
}).catch((e) => e),
|
||||
resolveApiKeyForProfileInTest({
|
||||
store: ensureAuthProfileStore(agentDir),
|
||||
profileId,
|
||||
agentDir,
|
||||
}).catch((e) => e),
|
||||
resolveApiKeyForProfileInTest({
|
||||
store: ensureAuthProfileStore(agentDir),
|
||||
profileId,
|
||||
agentDir,
|
||||
}).catch((e) => e),
|
||||
]);
|
||||
|
||||
// All three should have completed in order (FIFO queue).
|
||||
expect(order).toEqual([1, 2, 3]);
|
||||
expect(results).toHaveLength(3);
|
||||
afterAll(async () => {
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("releases the queue even when the refresh throws", async () => {
|
||||
@@ -254,8 +212,8 @@ describe("OAuth refresh in-process queue", () => {
|
||||
startOrder.push(n);
|
||||
inFlight += 1;
|
||||
maxInFlight = Math.max(maxInFlight, inFlight);
|
||||
// Small delay so any non-serialized overlap would be observable.
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
// Yield once so any non-serialized overlap is observable without wall-clock sleep.
|
||||
await Promise.resolve();
|
||||
inFlight -= 1;
|
||||
endOrder.push(n);
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as sessionStore from "../config/sessions/store.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { CallGatewayOptions } from "../gateway/call.js";
|
||||
@@ -115,6 +115,34 @@ function setSubagentControlDepsForTest(
|
||||
});
|
||||
}
|
||||
|
||||
let tempRoot = "";
|
||||
let tempStoreIndex = 0;
|
||||
|
||||
beforeAll(() => {
|
||||
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-subagent-control-"));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function nextSessionStorePath(label: string) {
|
||||
tempStoreIndex += 1;
|
||||
return path.join(tempRoot, `${tempStoreIndex}-${label}.json`);
|
||||
}
|
||||
|
||||
function cfgWithSessionStore(storePath = nextSessionStorePath("sessions")): OpenClawConfig {
|
||||
return {
|
||||
session: { store: storePath },
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
function writeSessionStoreFixture(label: string, store: Record<string, unknown>) {
|
||||
const storePath = nextSessionStorePath(label);
|
||||
fs.writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
|
||||
return storePath;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setSubagentControlDepsForTest();
|
||||
});
|
||||
@@ -472,24 +500,13 @@ describe("killSubagentRunAdmin", () => {
|
||||
});
|
||||
|
||||
it("kills a subagent by session key without requester ownership checks", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-subagent-admin-kill-"));
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const childSessionKey = "agent:main:subagent:worker";
|
||||
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
[childSessionKey]: {
|
||||
sessionId: "sess-worker",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
const storePath = writeSessionStoreFixture("admin-kill", {
|
||||
[childSessionKey]: {
|
||||
sessionId: "sess-worker",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
addSubagentRunForTests({
|
||||
runId: "run-worker",
|
||||
@@ -503,9 +520,7 @@ describe("killSubagentRunAdmin", () => {
|
||||
startedAt: Date.now() - 4_000,
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { store: storePath },
|
||||
} as OpenClawConfig;
|
||||
const cfg = cfgWithSessionStore(storePath);
|
||||
|
||||
const result = await killSubagentRunAdmin({
|
||||
cfg,
|
||||
@@ -523,7 +538,7 @@ describe("killSubagentRunAdmin", () => {
|
||||
|
||||
it("returns found=false when the session key is not tracked as a subagent run", async () => {
|
||||
const result = await killSubagentRunAdmin({
|
||||
cfg: {} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(),
|
||||
sessionKey: "agent:main:subagent:missing",
|
||||
});
|
||||
|
||||
@@ -559,7 +574,7 @@ describe("killSubagentRunAdmin", () => {
|
||||
});
|
||||
|
||||
const result = await killSubagentRunAdmin({
|
||||
cfg: {} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(),
|
||||
sessionKey: childSessionKey,
|
||||
});
|
||||
|
||||
@@ -572,24 +587,13 @@ describe("killSubagentRunAdmin", () => {
|
||||
});
|
||||
|
||||
it("still terminates the run when session store persistence fails during kill", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-subagent-admin-kill-store-"));
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const childSessionKey = "agent:main:subagent:worker-store-fail";
|
||||
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
[childSessionKey]: {
|
||||
sessionId: "sess-worker-store-fail",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
const storePath = writeSessionStoreFixture("admin-kill-store-fail", {
|
||||
[childSessionKey]: {
|
||||
sessionId: "sess-worker-store-fail",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
addSubagentRunForTests({
|
||||
runId: "run-worker-store-fail",
|
||||
@@ -609,9 +613,7 @@ describe("killSubagentRunAdmin", () => {
|
||||
|
||||
try {
|
||||
const result = await killSubagentRunAdmin({
|
||||
cfg: {
|
||||
session: { store: storePath },
|
||||
} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(storePath),
|
||||
sessionKey: childSessionKey,
|
||||
});
|
||||
|
||||
@@ -635,23 +637,12 @@ describe("killControlledSubagentRun", () => {
|
||||
});
|
||||
|
||||
it("does not mutate the live session when the caller passes a stale run entry", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-subagent-stale-kill-"));
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const childSessionKey = "agent:main:subagent:stale-kill-worker";
|
||||
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
[childSessionKey]: {
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
const storePath = writeSessionStoreFixture("stale-kill", {
|
||||
[childSessionKey]: {
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
addSubagentRunForTests({
|
||||
runId: "run-current",
|
||||
@@ -666,9 +657,7 @@ describe("killControlledSubagentRun", () => {
|
||||
});
|
||||
|
||||
const result = await killControlledSubagentRun({
|
||||
cfg: {
|
||||
session: { store: storePath },
|
||||
} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(storePath),
|
||||
controller: {
|
||||
controllerSessionKey: "agent:main:main",
|
||||
callerSessionKey: "agent:main:main",
|
||||
@@ -758,7 +747,7 @@ describe("killControlledSubagentRun", () => {
|
||||
});
|
||||
|
||||
const result = await killControlledSubagentRun({
|
||||
cfg: {} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(),
|
||||
controller: {
|
||||
controllerSessionKey: "agent:main:main",
|
||||
callerSessionKey: "agent:main:main",
|
||||
@@ -859,7 +848,7 @@ describe("killControlledSubagentRun", () => {
|
||||
});
|
||||
|
||||
const result = await killControlledSubagentRun({
|
||||
cfg: {} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(),
|
||||
controller: {
|
||||
controllerSessionKey: "agent:main:main",
|
||||
callerSessionKey: "agent:main:main",
|
||||
@@ -899,23 +888,12 @@ describe("killAllControlledSubagentRuns", () => {
|
||||
});
|
||||
|
||||
it("ignores stale run snapshots in bulk kill requests", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-subagent-stale-kill-all-"));
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const childSessionKey = "agent:main:subagent:stale-kill-all-worker";
|
||||
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
[childSessionKey]: {
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
const storePath = writeSessionStoreFixture("stale-kill-all", {
|
||||
[childSessionKey]: {
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
addSubagentRunForTests({
|
||||
runId: "run-current-bulk",
|
||||
@@ -930,9 +908,7 @@ describe("killAllControlledSubagentRuns", () => {
|
||||
});
|
||||
|
||||
const result = await killAllControlledSubagentRuns({
|
||||
cfg: {
|
||||
session: { store: storePath },
|
||||
} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(storePath),
|
||||
controller: {
|
||||
controllerSessionKey: "agent:main:main",
|
||||
callerSessionKey: "agent:main:main",
|
||||
@@ -968,25 +944,12 @@ describe("killAllControlledSubagentRuns", () => {
|
||||
});
|
||||
|
||||
it("does not let a stale bulk entry suppress the current live entry for the same child key", async () => {
|
||||
const tmpDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "openclaw-subagent-stale-kill-all-shadow-"),
|
||||
);
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const childSessionKey = "agent:main:subagent:stale-kill-all-shadow-worker";
|
||||
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
[childSessionKey]: {
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
const storePath = writeSessionStoreFixture("stale-kill-all-shadow", {
|
||||
[childSessionKey]: {
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
addSubagentRunForTests({
|
||||
runId: "run-current-shadow",
|
||||
@@ -1001,9 +964,7 @@ describe("killAllControlledSubagentRuns", () => {
|
||||
});
|
||||
|
||||
const result = await killAllControlledSubagentRuns({
|
||||
cfg: {
|
||||
session: { store: storePath },
|
||||
} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(storePath),
|
||||
controller: {
|
||||
controllerSessionKey: "agent:main:main",
|
||||
callerSessionKey: "agent:main:main",
|
||||
@@ -1073,7 +1034,7 @@ describe("killAllControlledSubagentRuns", () => {
|
||||
});
|
||||
|
||||
const result = await killAllControlledSubagentRuns({
|
||||
cfg: {} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(),
|
||||
controller: {
|
||||
controllerSessionKey: "agent:main:main",
|
||||
callerSessionKey: "agent:main:main",
|
||||
@@ -1145,7 +1106,7 @@ describe("killAllControlledSubagentRuns", () => {
|
||||
});
|
||||
|
||||
const result = await killAllControlledSubagentRuns({
|
||||
cfg: {} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(),
|
||||
controller: {
|
||||
controllerSessionKey: "agent:main:main",
|
||||
callerSessionKey: "agent:main:main",
|
||||
@@ -1215,7 +1176,7 @@ describe("steerControlledSubagentRun", () => {
|
||||
|
||||
try {
|
||||
const result = await steerControlledSubagentRun({
|
||||
cfg: {} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(),
|
||||
controller: {
|
||||
controllerSessionKey: "agent:main:main",
|
||||
callerSessionKey: "agent:main:main",
|
||||
@@ -1260,7 +1221,7 @@ describe("steerControlledSubagentRun", () => {
|
||||
});
|
||||
|
||||
const result = await steerControlledSubagentRun({
|
||||
cfg: {} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(),
|
||||
controller: {
|
||||
controllerSessionKey: "agent:main:main",
|
||||
callerSessionKey: "agent:main:main",
|
||||
@@ -1340,7 +1301,7 @@ describe("steerControlledSubagentRun", () => {
|
||||
});
|
||||
|
||||
const result = await steerControlledSubagentRun({
|
||||
cfg: {} as OpenClawConfig,
|
||||
cfg: cfgWithSessionStore(),
|
||||
controller: {
|
||||
controllerSessionKey: "agent:main:main",
|
||||
callerSessionKey: "agent:main:main",
|
||||
|
||||
Reference in New Issue
Block a user