diff --git a/src/plugin-sdk/discord.test.ts b/src/plugin-sdk/discord.test.ts index 3eab89b4541f..89b4393c4dbc 100644 --- a/src/plugin-sdk/discord.test.ts +++ b/src/plugin-sdk/discord.test.ts @@ -1,10 +1,34 @@ +import { describe, expect, expectTypeOf, it, vi } from "vitest"; +import type { MessageReceipt } from "./channel-outbound.js"; /** * Tests Discord SDK helpers and Discord-facing compatibility behavior. */ -import { describe, expect, it, vi } from "vitest"; +import type { + DiscordComponentSendOpts, + DiscordComponentSendResult, + OpenClawConfig, +} from "./discord.js"; const mocks = vi.hoisted(() => { const runtimeConfig = { channels: { discord: { token: "token" } } }; + const componentEditResult = { + channelId: "channel", + messageId: "message", + receipt: { + parts: [ + { + index: 0, + kind: "card", + platformMessageId: "message", + raw: { channel: "discord", channelId: "channel", messageId: "message" }, + }, + ], + platformMessageIds: ["message"], + primaryPlatformMessageId: "message", + raw: [{ channel: "discord", channelId: "channel", messageId: "message" }], + sentAt: 0, + }, + }; const apiModule = { buildDiscordComponentMessage: vi.fn((params: { spec: { text?: string } }) => ({ components: [], @@ -40,7 +64,7 @@ const mocks = vi.hoisted(() => { cfg: params.cfg, })), collectDiscordAuditChannelIds: vi.fn(() => ({ channelIds: [], unresolvedChannels: [] })), - editDiscordComponentMessage: vi.fn(async () => ({ id: "message" })), + editDiscordComponentMessage: vi.fn(async () => componentEditResult), listThreadBindingsBySessionKey: vi.fn(() => []), registerBuiltDiscordComponentMessage: vi.fn(), unbindThreadBindingsBySessionKey: vi.fn(() => []), @@ -48,6 +72,7 @@ const mocks = vi.hoisted(() => { return { apiModule, + componentEditResult, runtimeModule, runtimeConfig, loadBundledPluginPublicSurfaceModuleSync: vi.fn((params: { artifactBasename: string }) => { @@ -128,7 +153,7 @@ describe("discord plugin-sdk facade", () => { } = await import("./discord.js"); const built = buildDiscordComponentMessage({ spec: { text: "hello" } }); - await editDiscordComponentMessage( + const editResult = await editDiscordComponentMessage( "channel", "message", { text: "edited" }, @@ -148,12 +173,28 @@ describe("discord plugin-sdk facade", () => { { text: "edited" }, { cfg: mocks.runtimeConfig }, ); + expect(editResult).toEqual(mocks.componentEditResult); expect(mocks.runtimeModule.registerBuiltDiscordComponentMessage).toHaveBeenCalledWith({ buildResult: built, messageId: "message", }); }); + it("types Discord component edit options and normalized result", () => { + type IsCfgOptional = object extends Pick ? true : false; + + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + messageId: string; + channelId: string; + receipt: MessageReceipt; + }>(); + expectTypeOf().toEqualTypeOf< + "messageId" | "channelId" | "receipt" + >(); + }); + it("fills runtime config for Discord subagent auto-bind calls without cfg", async () => { const { autoBindSpawnedDiscordSubagent } = await import("./discord.js"); diff --git a/src/plugin-sdk/discord.ts b/src/plugin-sdk/discord.ts index a4c15acd0e2f..639fd5a7ee62 100644 --- a/src/plugin-sdk/discord.ts +++ b/src/plugin-sdk/discord.ts @@ -9,6 +9,7 @@ import type { ChannelStatusIssue, } from "./channel-contract.js"; import type { ChannelPlugin } from "./channel-core.js"; +import type { MessageReceipt } from "./channel-outbound.js"; import type { OpenClawConfig } from "./config-types.js"; import { createLazyFacadeObjectValue, @@ -67,7 +68,7 @@ export type DiscordComponentBuildResult = { /** Send/edit options for Discord component messages. */ export type DiscordComponentSendOpts = { - cfg?: OpenClawConfig; + cfg: OpenClawConfig; accountId?: string; replyTo?: string; files?: unknown; @@ -80,11 +81,11 @@ export type DiscordComponentSendOpts = { [key: string]: unknown; }; -/** Minimal Discord API message result returned by component send/edit helpers. */ +/** Normalized Discord message result returned by component send/edit helpers. */ export type DiscordComponentSendResult = { - id?: string; - channel_id?: string; - [key: string]: unknown; + messageId: string; + channelId: string; + receipt: MessageReceipt; }; /** Resolved Discord account with token source metadata for status and runtime checks. */ diff --git a/test/scripts/upgrade-survivor-assertions.test.ts b/test/scripts/upgrade-survivor-assertions.test.ts index 40384ca3da93..27f63e55521b 100644 --- a/test/scripts/upgrade-survivor-assertions.test.ts +++ b/test/scripts/upgrade-survivor-assertions.test.ts @@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { DatabaseSync } from "node:sqlite"; import { describe, expect, it } from "vitest"; const ASSERTIONS_PATH = "scripts/e2e/lib/upgrade-survivor/assertions.mjs"; @@ -11,6 +12,70 @@ function writeJson(path: string, value: unknown): void { writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`); } +function writeMigratedSessionState(stateDir: string): void { + const agentSessionsDir = join(stateDir, "agents", "main", "sessions"); + const agentDbDir = join(stateDir, "agents", "main", "agent"); + const mainSessionFile = join(agentSessionsDir, "upgrade-main-session.jsonl"); + const directSessionFile = join(agentSessionsDir, "upgrade-direct-session.jsonl"); + const groupSessionFile = join(agentSessionsDir, "upgrade-group-session.jsonl"); + mkdirSync(agentSessionsDir, { recursive: true }); + mkdirSync(agentDbDir, { recursive: true }); + writeFileSync(mainSessionFile, '{"type":"main"}\n'); + writeFileSync(directSessionFile, '{"type":"direct"}\n'); + writeFileSync(groupSessionFile, '{"type":"group"}\n'); + + const db = new DatabaseSync(join(agentDbDir, "openclaw-agent.sqlite")); + try { + db.exec(` + CREATE TABLE IF NOT EXISTS cache_entries ( + scope TEXT NOT NULL, + key TEXT NOT NULL, + value_json TEXT, + blob BLOB, + expires_at INTEGER, + updated_at INTEGER NOT NULL, + PRIMARY KEY (scope, key) + ); + `); + const insert = db.prepare(` + INSERT INTO cache_entries (scope, key, value_json, updated_at) + VALUES (?, ?, ?, ?) + `); + insert.run( + "session_entries", + "agent:main:main", + JSON.stringify({ + sessionFile: mainSessionFile, + sessionId: "upgrade-main-session", + skillsSnapshot: { + prompt: "legacy prompt survives as metadata", + }, + }), + 1710000000000, + ); + insert.run( + "session_entries", + "agent:main:+15551234567", + JSON.stringify({ + sessionFile: directSessionFile, + sessionId: "upgrade-direct-session", + }), + 1710000000100, + ); + insert.run( + "session_entries", + "agent:main:slack:channel:cupgrade", + JSON.stringify({ + sessionFile: groupSessionFile, + sessionId: "upgrade-group-session", + }), + 1710000000200, + ); + } finally { + db.close(); + } +} + function assertConfiguredPluginState(params: { installPath?: string } = {}): void { const root = mkdtempSync(join(tmpdir(), "openclaw-upgrade-survivor-")); try { @@ -25,6 +90,7 @@ function assertConfiguredPluginState(params: { installPath?: string } = {}): voi writeJson(join(stateDir, "agents", "main", "sessions", "legacy-session.json"), { id: "legacy-session", }); + writeMigratedSessionState(stateDir); writeJson(join(matrixInstallDir, "package.json"), { name: "@openclaw/matrix", });