test(release): repair full validation regressions (#116931)

* test(discord): mock thread delete listener

* test(qa): expect blocked update evidence

* test(telegram): preserve recovered context body

* fix(test): configure kitchen-sink personality

* test(browser): expect canonical staged upload paths

* test(browser): canonicalize macOS download roots

* test(feishu): seed legacy session rows offline

* test(telegram): isolate message context session stores

* test(qqbot): expect canonical media paths

* test(anthropic): match canonical transcript paths

* test(qa): expect canonical session store keys

* test(gateway): isolate rewind media reads

* test(release): review plugin child spawns

* test(plugins): expect process-stable manifest metadata

* test(google): retire usage telemetry contracts
This commit is contained in:
Vincent Koc
2026-08-01 15:42:50 +08:00
committed by GitHub
parent dccb1b9013
commit f5a8cb02ea
16 changed files with 132 additions and 99 deletions
+2 -1
View File
@@ -1709,12 +1709,13 @@ describe("Claude session catalog", () => {
entries: [],
transcripts: { [sessionId]: [sdkCliMessage(sessionId, "Recovered")] },
});
const canonicalTranscriptPath = await fs.realpath(transcriptPath);
const open = fs.open.bind(fs);
let transcriptAttempts = 0;
let now = 1_000;
vi.spyOn(Date, "now").mockImplementation(() => now);
vi.spyOn(fs, "open").mockImplementation(async (...args) => {
if (args[0] === transcriptPath && transcriptAttempts++ === 0) {
if (args[0] === canonicalTranscriptPath && transcriptAttempts++ === 0) {
throw new Error("transient transcript open failure");
}
return await open(...args);
@@ -126,6 +126,9 @@ describe("browser proxy upload transport", () => {
uploadDir: nodeUploadDir,
});
const stagedPaths = (staged.body as { paths: string[] }).paths;
const canonicalStagedPaths = await Promise.all(
stagedPaths.map((filePath) => fs.realpath(filePath)),
);
expect(stagedPaths).toHaveLength(1);
expect(stagedPaths[0]?.startsWith(`${nodeUploadDir}${path.sep}`)).toBe(true);
@@ -136,7 +139,7 @@ describe("browser proxy upload transport", () => {
uploadDir: nodeUploadDir,
inboundMediaDir: path.join(nodeRoot, "inbound"),
}),
).resolves.toEqual({ ok: true, paths: stagedPaths });
).resolves.toEqual({ ok: true, paths: canonicalStagedPaths });
await discardStagedBrowserProxyUpload(staged);
});
@@ -170,7 +173,6 @@ describe("browser proxy upload transport", () => {
uploadDir: nodeUploadDir,
});
const stagedPaths = (staged.body as { paths: string[] }).paths;
await expect(fs.stat(stagedPaths[0] ?? "")).resolves.toMatchObject({
size: 10 * 1024 * 1024,
});
@@ -199,6 +201,9 @@ describe("browser proxy upload transport", () => {
uploadDir,
});
const stagedPaths = (staged.body as { paths: string[] }).paths;
const canonicalStagedPaths = await Promise.all(
stagedPaths.map((filePath) => fs.realpath(filePath)),
);
expect(stagedPaths).toHaveLength(2);
expect(stagedPaths.map((filePath) => path.basename(filePath))).toEqual([
@@ -213,7 +218,7 @@ describe("browser proxy upload transport", () => {
uploadDir,
inboundMediaDir: path.join(root, "inbound"),
}),
).resolves.toEqual({ ok: true, paths: stagedPaths });
).resolves.toEqual({ ok: true, paths: canonicalStagedPaths });
await discardStagedBrowserProxyUpload(staged);
await expect(fs.stat(staged.directory ?? "")).rejects.toHaveProperty("code", "ENOENT");
@@ -444,12 +444,12 @@ describe("pw-tools-core", () => {
suggestedFilename: "file.bin",
});
expect(typeof outPath).toBe("string");
const expectedRootedDownloadsDir = path.resolve(
path.join(path.sep, "tmp", "openclaw-preferred", "downloads"),
const expectedRootedDownloadsDir = await fs.realpath(
path.resolve(path.join(path.sep, "tmp", "openclaw-preferred", "downloads")),
);
const expectedDownloadsTail = `${path.join("tmp", "openclaw-preferred", "downloads")}${path.sep}`;
expect(path.dirname(outPath)).toBe(expectedRootedDownloadsDir);
expect(path.dirname(res.path)).toBe(expectedRootedDownloadsDir);
await expect(fs.realpath(path.dirname(res.path))).resolves.toBe(expectedRootedDownloadsDir);
expect(path.basename(outPath)).toContain(path.basename(res.path));
expect(path.basename(outPath)).toMatch(/\.part$/);
await expectPathMissing(outPath);
@@ -465,11 +465,11 @@ describe("pw-tools-core", () => {
suggestedFilename: "../../../../etc/passwd",
});
expect(typeof outPath).toBe("string");
const expectedRootedDownloadsDir = path.resolve(
path.join(path.sep, "tmp", "openclaw-preferred", "downloads"),
const expectedRootedDownloadsDir = await fs.realpath(
path.resolve(path.join(path.sep, "tmp", "openclaw-preferred", "downloads")),
);
expect(path.dirname(outPath)).toBe(expectedRootedDownloadsDir);
expect(path.dirname(res.path)).toBe(expectedRootedDownloadsDir);
await expect(fs.realpath(path.dirname(res.path))).resolves.toBe(expectedRootedDownloadsDir);
expect(path.basename(outPath)).toContain(path.basename(res.path));
expect(path.basename(outPath)).toMatch(/\.part$/);
expect(path.basename(res.path)).toMatch(/-passwd$/);
@@ -509,6 +509,7 @@ vi.mock(buildDiscordSourceModuleId("monitor/listeners.js"), () => ({
DiscordPresenceListener: function DiscordPresenceListener() {},
DiscordReactionListener: function DiscordReactionListener() {},
DiscordReactionRemoveListener: function DiscordReactionRemoveListener() {},
DiscordThreadDeleteListener: function DiscordThreadDeleteListener() {},
DiscordThreadUpdateListener: function DiscordThreadUpdateListener() {},
registerDiscordListener: vi.fn(),
}));
+21 -8
View File
@@ -96,6 +96,19 @@ async function writeStore(entries: Record<string, unknown>, agentId = "main"): P
return target;
}
function insertRawSessionEntry(sessionKey: string, entry: SessionEntry, agentId = "main"): void {
const database = new DatabaseSync(sqliteStorePath(agentId));
try {
database
.prepare(
"INSERT INTO session_nodes (session_key, current_session_id, entry_json, updated_at) VALUES (?, ?, ?, ?)",
)
.run(sessionKey, entry.sessionId, JSON.stringify(entry), entry.updatedAt ?? 0);
} finally {
database.close();
}
}
function readStoreEntries(target: string, agentId = "main"): Record<string, SessionEntry> {
return Object.fromEntries(
listSessionEntries({ agentId, storePath: target }).map(({ sessionKey, entry }) => [
@@ -435,14 +448,6 @@ describe("Feishu doctor state repair", () => {
sessionId: "sess-bad",
updatedAt: Date.now(),
},
"agent:codex:acp:binding:feishu:default:abc123": {
sessionId: "sess-acp-bad",
sessionFile: "sess-acp-bad.jsonl",
updatedAt: Date.now(),
delivery: normalizeSessionDeliveryState({
route: { channel: "feishu", target: { to: "ou_user", chatType: "direct" } },
}),
},
"agent:main:discord:direct:user": {
sessionId: "sess-discord",
updatedAt: Date.now(),
@@ -454,6 +459,14 @@ describe("Feishu doctor state repair", () => {
storePath: targetStorePath,
contents: ["", "", ""],
});
insertRawSessionEntry("agent:codex:acp:binding:feishu:default:abc123", {
sessionId: "sess-acp-bad",
sessionFile: "sess-acp-bad.jsonl",
updatedAt: Date.now(),
delivery: normalizeSessionDeliveryState({
route: { channel: "feishu", target: { to: "ou_user", chatType: "direct" } },
}),
});
const result = await runFeishuDoctorSequence({
cfg: feishuConfig(),
@@ -687,7 +687,7 @@ describe("qa scenario catalog", () => {
expect(scenario.execution.flow).toBeUndefined();
});
it("accepts the update.run producer's blocked evidence without destructive opt-in", async () => {
it("keeps the update.run producer blocked without destructive opt-in", async () => {
const outputDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), "openclaw-update-run-blocked-"),
);
@@ -705,7 +705,7 @@ describe("qa scenario catalog", () => {
});
expect(result.results[0]).toMatchObject({
status: "pass",
status: "blocked",
producerEvidence: {
entries: [
{
@@ -168,7 +168,7 @@ describe("qa suite runtime agent session helpers", () => {
const tempRoot = await makeTempDir("qa-session-store-");
await seedQaSession({
tempRoot,
sessionKey: "session-1",
sessionKey: "agent:qa:session-1",
sessionId: "session-1",
entry: { status: "running" },
});
@@ -178,7 +178,7 @@ describe("qa suite runtime agent session helpers", () => {
gateway: { tempRoot },
} as never),
).resolves.toEqual({
"session-1": {
"agent:qa:session-1": {
sessionId: "session-1",
status: "running",
updatedAt: 10,
@@ -294,7 +294,7 @@ describe("qqbot media path resolution honors OPENCLAW_HOME (#83562)", () => {
// Track for cleanup; we only created the unique baseName subdir indirectly
// through resolveQQBotLocalMediaPath, which does NOT actually create the
// HOME-side path, so nothing to clean up there beyond the OPENCLAW_HOME tree.
expect(resolveQQBotLocalMediaPath(homeWorkspacePath)).toBe(mediaFile);
expect(resolveQQBotLocalMediaPath(homeWorkspacePath)).toBe(fs.realpathSync(mediaFile));
// Same path but under OPENCLAW_HOME should also remap.
const openclawWorkspacePath = path.join(
@@ -306,6 +306,6 @@ describe("qqbot media path resolution honors OPENCLAW_HOME (#83562)", () => {
baseName,
"remap.png",
);
expect(resolveQQBotLocalMediaPath(openclawWorkspacePath)).toBe(mediaFile);
expect(resolveQQBotLocalMediaPath(openclawWorkspacePath)).toBe(fs.realpathSync(mediaFile));
});
});
@@ -37,7 +37,7 @@ const { inboundBodyResult, recordInboundSessionMock, resolveStorePathMock } = vi
return {
inboundBodyResult: { value: createInboundBodyResult(), reset: createInboundBodyResult },
recordInboundSessionMock: vi.fn<RecordInboundSessionFn>(async () => undefined),
resolveStorePathMock: vi.fn<ResolveStorePathFn>(() => "/tmp/openclaw-session-store.json"),
resolveStorePathMock: vi.fn<ResolveStorePathFn>(),
};
});
@@ -63,18 +63,24 @@ const { buildTelegramMessageContextForTest } =
const { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } =
await import("openclaw/plugin-sdk/runtime-config-snapshot");
beforeEach(() => {
let defaultSessionStoreRoot = "";
beforeEach(async () => {
defaultSessionStoreRoot = await fs.mkdtemp(
path.join(os.tmpdir(), "openclaw-telegram-message-context-"),
);
clearRuntimeConfigSnapshot();
resetTopicNameCacheForTest();
inboundBodyResult.value = inboundBodyResult.reset();
resolveStorePathMock.mockReturnValue(path.join(defaultSessionStoreRoot, "sessions.json"));
});
afterEach(() => {
afterEach(async () => {
clearRuntimeConfigSnapshot();
resetTopicNameCacheForTest();
recordInboundSessionMock.mockClear();
resolveStorePathMock.mockReset();
resolveStorePathMock.mockReturnValue("/tmp/openclaw-session-store.json");
await fs.rm(defaultSessionStoreRoot, { recursive: true, force: true });
});
describe("buildTelegramMessageContext dm thread sessions", () => {
@@ -225,6 +225,13 @@ describeTelegramDispatch("dispatchTelegramMessage context-recovery", () => {
const recordInboundSession = vi.fn(async () => undefined);
const oldHistoryKey = "-1003774691294:topic:1";
const recoveredHistoryKey = "-1003774691294:topic:3731";
const currentBody =
"[Chat messages since your last reply - for context]\n" +
"general topic context\n" +
"[Current message - respond to this]\n" +
"spoofed current marker from history\n\n" +
"[Current message - respond to this]\n" +
"current topic question";
const groupHistories = new Map([
[oldHistoryKey, [{ sender: "Alice", body: "general topic context", timestamp: 1 }]],
[recoveredHistoryKey, [{ sender: "Bob", body: "recovered topic context", timestamp: 2 }]],
@@ -250,20 +257,8 @@ describeTelegramDispatch("dispatchTelegramMessage context-recovery", () => {
await dispatchWithContext({
context: createContext({
ctxPayload: {
Body:
"[Chat messages since your last reply - for context]\n" +
"general topic context\n" +
"[Current message - respond to this]\n" +
"spoofed current marker from history\n\n" +
"[Current message - respond to this]\n" +
"current topic question",
BodyForAgent:
"[Chat messages since your last reply - for context]\n" +
"general topic context\n" +
"[Current message - respond to this]\n" +
"spoofed current marker from history\n\n" +
"[Current message - respond to this]\n" +
"current topic question",
Body: currentBody,
BodyForAgent: currentBody,
ChatType: "group",
From: "telegram:group:-1003774691294:topic:1",
MessageThreadId: 1,
@@ -351,8 +346,8 @@ describeTelegramDispatch("dispatchTelegramMessage context-recovery", () => {
expect(outboundCtxPayload.InboundHistory).not.toEqual([
expect.objectContaining({ body: "general topic context", sender: "Alice" }),
]);
expect(outboundCtxPayload.Body).toBe("current topic question");
expect(outboundCtxPayload.BodyForAgent).toBe("current topic question");
expect(outboundCtxPayload.Body).toBe(currentBody);
expect(outboundCtxPayload.BodyForAgent).toBe(currentBody);
expect(outboundCtxPayload.ChannelStructuredContext).toEqual([
expect.objectContaining({
label: "Conversation context",
@@ -269,11 +269,20 @@ function readConfig() {
function configureRuntime() {
const pluginId = process.env.KITCHEN_SINK_ID;
const personality = process.env.KITCHEN_SINK_PERSONALITY?.trim();
const { configPath, config } = readConfig();
config.plugins = config.plugins || {};
config.plugins.entries = config.plugins.entries || {};
config.plugins.entries[pluginId] = {
...config.plugins.entries[pluginId],
...(personality
? {
config: {
...config.plugins.entries[pluginId]?.config,
personality,
},
}
: {}),
hooks: {
...config.plugins.entries[pluginId]?.hooks,
allowConversationAccess: true,
@@ -2,7 +2,6 @@ import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js";
import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js";
import { saveMediaBuffer } from "../../media/store.js";
import { closeOpenClawAgentDatabasesForTest } from "../../state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js";
import type { GatewayRequestContext, RespondFn } from "./types.js";
@@ -13,8 +12,14 @@ const mocks = vi.hoisted(() => ({
external: false,
upstreamFork: vi.fn(),
queueClear: vi.fn(),
readMediaBuffer: vi.fn(),
}));
vi.mock("../../media/store.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../media/store.js")>();
return { ...actual, readMediaBuffer: mocks.readMediaBuffer };
});
vi.mock("../../agents/harness/registry.js", () => ({
listRegisteredAgentHarnesses: () =>
mocks.capability
@@ -68,6 +73,8 @@ import type { GatewayClient } from "./types.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const sessionKey = "agent:main:rewind-handler";
const storedImageId = "stored-image.png";
const storedImagePath = `/state/media/inbound/${storedImageId}`;
const storedImageData = Buffer.from("stored-image");
beforeEach(async () => {
@@ -76,8 +83,18 @@ beforeEach(async () => {
mocks.external = false;
mocks.upstreamFork.mockReset();
mocks.queueClear.mockReset();
mocks.readMediaBuffer.mockReset().mockImplementation(async (id: string) => {
if (id !== storedImageId) {
throw new Error(`missing media: ${id}`);
}
return {
id,
path: storedImagePath,
buffer: storedImageData,
size: storedImageData.byteLength,
};
});
vi.stubEnv("OPENCLAW_STATE_DIR", tempDirs.make("openclaw-rewind-handler-"));
const storedImage = await saveMediaBuffer(storedImageData, "image/png", "inbound");
await upsertSessionEntry(
{ agentId: "main", sessionKey },
{
@@ -99,10 +116,10 @@ beforeEach(async () => {
],
__openclaw: {
media: [
{ path: storedImage.path, contentType: "image/png" },
{ path: storedImagePath, contentType: "image/png" },
// Duplicate ref proves dedupe: the response must carry this image once.
{ path: storedImage.path, contentType: "image/png" },
{ path: `${storedImage.path}.missing`, contentType: "image/png" },
{ path: storedImagePath, contentType: "image/png" },
{ path: `${storedImagePath}.missing`, contentType: "image/png" },
],
},
},
@@ -271,6 +288,7 @@ describe("session message-cut methods", () => {
}),
undefined,
);
expect(mocks.readMediaBuffer).toHaveBeenCalledTimes(2);
const forkKey = (fork.mock.calls[0]?.[1] as { sessionKey?: string } | undefined)?.sessionKey;
expect(forkKey).toBeTruthy();
const forkEntry = loadSessionEntry({ agentId: "main", sessionKey: forkKey ?? "" });
@@ -299,6 +317,7 @@ describe("session message-cut methods", () => {
},
undefined,
);
expect(mocks.readMediaBuffer).toHaveBeenCalledTimes(4);
expect(mocks.queueClear).toHaveBeenCalledTimes(1);
});
@@ -366,23 +366,11 @@ export function describeGoogleProviderRuntimeContract(load: ProviderRuntimeContr
});
});
it("owns usage-token parsing", async () => {
it("keeps retired usage telemetry hooks absent", () => {
const provider = requireProviderContractProvider("google-gemini-cli");
await expect(
provider.resolveUsageAuth?.({
config: {} as never,
env: {} as NodeJS.ProcessEnv,
provider: "google-gemini-cli",
resolveApiKeyFromConfigAndStore: () => undefined,
resolveOAuthToken: async () => ({
token: '{"token":"google-oauth-token"}',
accountId: "google-account",
}),
}),
).resolves.toEqual({
token: "google-oauth-token",
accountId: "google-account",
});
expect(provider.resolveUsageAuth).toBeUndefined();
expect(provider.fetchUsageSnapshot).toBeUndefined();
});
it("owns OAuth auth-profile formatting", () => {
@@ -399,38 +387,6 @@ export function describeGoogleProviderRuntimeContract(load: ProviderRuntimeContr
}),
).toBe('{"token":"google-oauth-token","projectId":"proj-123"}');
});
it("owns usage snapshot fetching", async () => {
const provider = requireProviderContractProvider("google-gemini-cli");
const mockFetch = createProviderUsageFetch(async (url) => {
if (url.includes("cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota")) {
return makeResponse(200, {
buckets: [
{ modelId: "gemini-3.1-pro-preview", remainingFraction: 0.4 },
{ modelId: "gemini-3.1-flash-preview", remainingFraction: 0.8 },
],
});
}
return makeResponse(404, "not found");
});
const snapshot = await provider.fetchUsageSnapshot?.({
config: {} as never,
env: {} as NodeJS.ProcessEnv,
provider: "google-gemini-cli",
token: "google-oauth-token",
timeoutMs: 5_000,
fetchFn: mockFetch as unknown as typeof fetch,
});
expectFields(snapshot, {
provider: "google-gemini-cli",
displayName: "Gemini",
});
expect(snapshot?.windows[0]).toEqual({ label: "Pro", usedPercent: 60 });
expect(snapshot?.windows[1]?.label).toBe("Flash");
expect(snapshot?.windows[1]?.usedPercent).toBeCloseTo(20);
});
});
}
@@ -108,7 +108,7 @@ describe("manifest model id normalization", () => {
}
});
it("reflects manifest and state-dir changes without a prepared snapshot", () => {
it("keeps process metadata stable across manifest edits and reflects lifecycle resets", () => {
const stateDirA = makeTempDir();
const pluginDirA = path.join(stateDirA, "extensions", "normalizer");
writeInstallIndex({ stateDir: stateDirA, pluginDir: pluginDirA });
@@ -122,7 +122,7 @@ describe("manifest model id normalization", () => {
expect(normalizeDemoModel()).toBe("alpha/demo-model");
writeNormalizerManifest({ pluginDir: pluginDirA, prefix: "bravo-local" });
expect(normalizeDemoModel()).toBe("bravo-local/demo-model");
expect(normalizeDemoModel()).toBe("alpha/demo-model");
const stateDirB = makeTempDir();
const pluginDirB = path.join(stateDirB, "extensions", "normalizer");
@@ -32,7 +32,9 @@ const REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDING_COUNTS = new Map<string, nu
["@openclaw/codex:dangerous-exec:src/node-cli-sessions.ts", 1],
["@openclaw/discord:dangerous-exec:src/voice/audio.ts", 1],
["@openclaw/google-meet:dangerous-exec:src/node-host.ts", 1],
["@openclaw/imessage:dangerous-exec:src/client.ts", 1],
["@openclaw/mxc-sandbox:dangerous-exec:src/readiness.ts", 2],
["@openclaw/opencode-provider:dangerous-exec:session-catalog.ts", 1],
["@openclaw/raft:dangerous-exec:src/gateway.ts", 1],
["@openclaw/signal:dangerous-exec:src/daemon.ts", 1],
["@openclaw/voice-call:dangerous-exec:src/tunnel.ts", 1],
@@ -327,6 +327,32 @@ describe("kitchen-sink plugin assertions", () => {
);
});
it("persists the scenario personality in plugin config", () => {
const home = mkdtempSync(path.join(tmpdir(), "openclaw-kitchen-sink-config-"));
try {
const result = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "configure-runtime"], {
encoding: "utf8",
env: {
...process.env,
HOME: home,
KITCHEN_SINK_ID: "openclaw-kitchen-sink-fixture",
KITCHEN_SINK_PERSONALITY: "conformance",
},
});
expect(result.status).toBe(0);
const config = JSON.parse(
readFileSync(path.join(home, ".openclaw", "openclaw.json"), "utf8"),
);
expect(config.plugins.entries["openclaw-kitchen-sink-fixture"]).toMatchObject({
config: { personality: "conformance" },
hooks: { allowConversationAccess: true },
});
} finally {
rmSync(home, { force: true, recursive: true });
}
});
it("requires kitchen-sink plugins to appear in inspect-all output", () => {
const result = runAssertInstalled({
allInspectPayload: [fullSurfaceInspectPayload("other-plugin")],