diff --git a/test/e2e/live/openclaw-pairing-helpers.ts b/test/e2e/live/openclaw-pairing-helpers.ts index bffda07aba9..848c1acc135 100644 --- a/test/e2e/live/openclaw-pairing-helpers.ts +++ b/test/e2e/live/openclaw-pairing-helpers.ts @@ -32,6 +32,8 @@ export const PAIRING_USER = { discord: process.env.NEMOCLAW_DISCORD_PAIRING_USER ?? "1005536447329222676", }; +const SLACK_PAIRING_TEAM = "T3730E2E"; + export const DISCORD_DM_CHANNEL = process.env.NEMOCLAW_DISCORD_DM_CHANNEL ?? "1199988877766655554"; export function pairingEnv(options: { @@ -176,6 +178,7 @@ export async function startFakeSlackApi( FAKE_SLACK_API_EXPECTED_BOT_TOKEN: botToken, FAKE_SLACK_API_EXPECTED_APP_TOKEN: appToken, FAKE_SLACK_API_SOCKET_USER_ID: PAIRING_USER.slack, + FAKE_SLACK_API_SOCKET_TEAM_ID: SLACK_PAIRING_TEAM, }, env, redactionValues: redactions, @@ -501,13 +504,13 @@ function postPairingReply(text, channel) { const { issuePairingChallenge, upsertChannelPairingRequest } = await loadConversationRuntime(); const envelope = await receiveSlackSocketEvent(); const event = envelope?.payload?.event; -if (!event || event.type !== "message" || !event.user || !event.channel) throw new Error("unexpected fake Slack envelope: " + JSON.stringify(envelope).slice(0, 400)); +if (!event || event.type !== "message" || !event.user || !event.channel || !envelope.payload?.team_id) throw new Error("unexpected fake Slack envelope: " + JSON.stringify(envelope).slice(0, 400)); if (event.user !== process.env.SLACK_PAIRING_USER) throw new Error("unexpected fake Slack user: " + event.user); const result = await issuePairingChallenge({ channel: "slack", - senderId: event.user, + senderId: "team:" + encodeURIComponent(envelope.payload.team_id) + ":user:" + encodeURIComponent(event.user), senderIdLine: "Slack user ID: " + event.user, - meta: { accountId: "default", channelId: event.channel, teamId: envelope.payload?.team_id || "" }, + meta: { accountId: "default", channelId: event.channel, teamId: envelope.payload.team_id, senderId: event.user }, upsertPairingRequest: async ({ id, meta }) => upsertChannelPairingRequest({ channel: "slack", id, accountId: "default", meta }), sendPairingReply: async (text) => { await postPairingReply(text, event.channel); }, }); @@ -604,12 +607,16 @@ function buildPairingStateCommand( return `python3 -c ${shellQuote(PAIRING_STATE_PROBE_SOURCE)} ${shellQuote(query)} ${shellQuote(JSON.stringify(parameters))}`; } +function pairingIdentity(channel: PairingChannel, user: string): string { + return channel === "slack" ? `team:${SLACK_PAIRING_TEAM}:user:${encodeURIComponent(user)}` : user; +} + export function buildPairingPendingCommand( channel: PairingChannel, code: string, user: string, ): string { - return buildPairingStateCommand("pending", channel, code, user); + return buildPairingStateCommand("pending", channel, code, pairingIdentity(channel, user)); } export function buildPairingApproveCommand(channel: PairingChannel, code: string): string { @@ -617,7 +624,7 @@ export function buildPairingApproveCommand(channel: PairingChannel, code: string } export function buildPairingAllowFromCommand(channel: PairingChannel, user: string): string { - return buildPairingStateCommand("allowed", channel, "", user); + return buildPairingStateCommand("allowed", channel, "", pairingIdentity(channel, user)); } export async function approveAndAssertPairing(options: { diff --git a/test/e2e/support/openclaw-discord-pairing-helpers.test.ts b/test/e2e/support/openclaw-discord-pairing-helpers.test.ts index e49a2e684e1..8dc602bc3e3 100644 --- a/test/e2e/support/openclaw-discord-pairing-helpers.test.ts +++ b/test/e2e/support/openclaw-discord-pairing-helpers.test.ts @@ -197,6 +197,115 @@ describe("OpenClaw Discord pairing helper contracts", () => { expect(approveCommand).not.toContain('"abc$(touch /tmp/e2e-should-not-run)"'); }); + it.each([ + ["slack", "team:T3730E2E:user:U3730E2E", "U3730E2E", 0], + ["slack", "team:TOTHER:user:U3730E2E", "U3730E2E", 1], + ["slack", "U3730E2E", "U3730E2E", 1], + ["discord", "1005536447329222676", "1005536447329222676", 0], + ] as const)( + "checks the exact %s pending and approval identity %s in SQLite", + (channel, storedEntry, user, expectedStatus) => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pairing-approval-state-")); + try { + const database = path.join(tmp, "openclaw.sqlite"); + const setup = spawnSync( + "python3", + [ + "-c", + [ + "import sqlite3, sys", + "db = sqlite3.connect(sys.argv[1])", + "db.execute('CREATE TABLE channel_pairing_allow_entries (channel_key TEXT, entry TEXT)')", + "db.execute('INSERT INTO channel_pairing_allow_entries VALUES (?, ?)', sys.argv[2:])", + "db.execute('CREATE TABLE channel_pairing_requests (channel_key TEXT, code TEXT, request_id TEXT)')", + "db.execute('INSERT INTO channel_pairing_requests VALUES (?, ?, ?)', (sys.argv[2], 'CODE1234', sys.argv[3]))", + "db.commit()", + "db.close()", + ].join("\n"), + database, + channel, + storedEntry, + ], + { encoding: "utf8" }, + ); + expect(setup.status, setup.stderr).toBe(0); + const pending = spawnSync( + "sh", + [ + "-c", + buildPairingPendingCommand(channel, "CODE1234", user).replace( + "/sandbox/.openclaw/state/openclaw.sqlite", + database, + ), + ], + { encoding: "utf8" }, + ); + expect(pending.status, pending.stderr).toBe(expectedStatus); + const approved = spawnSync( + "sh", + [ + "-c", + buildPairingAllowFromCommand(channel, user).replace( + "/sandbox/.openclaw/state/openclaw.sqlite", + database, + ), + ], + { encoding: "utf8" }, + ); + expect(approved.status, approved.stderr).toBe(expectedStatus); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }, + ); + + it.each(["T3730E2E", "TOTHER"])( + "issues a Slack request scoped to the event workspace %s", + (teamId) => { + const start = SLACK_PAIRING_SCRIPT.indexOf( + "const { issuePairingChallenge, upsertChannelPairingRequest } = await loadConversationRuntime();", + ); + const probe = SLACK_PAIRING_SCRIPT.slice(start, SLACK_PAIRING_SCRIPT.lastIndexOf("\nNODE")); + const result = spawnSync(process.execPath, ["--input-type=module"], { + input: ` +async function loadConversationRuntime() { + return { + issuePairingChallenge: async (params) => { + const result = await params.upsertPairingRequest({ id: params.senderId, meta: params.meta }); + await params.sendPairingReply(params.senderIdLine); + return result; + }, + upsertChannelPairingRequest: async (params) => { + console.log("REQUEST " + JSON.stringify(params)); + return { code: "CODE1234", created: true }; + }, + }; +} +async function receiveSlackSocketEvent() { + return ${JSON.stringify({ payload: { team_id: teamId, event: { type: "message", user: "U3730E2E", channel: "D3730E2E" } } })}; +} +async function postPairingReply() {} +${probe} +`, + encoding: "utf8", + env: { ...process.env, SLACK_PAIRING_USER: "U3730E2E" }, + }); + expect(result.status, result.stderr).toBe(0); + const request = result.stdout.split("\n").find((line) => line.startsWith("REQUEST ")); + expect(JSON.parse(request?.slice("REQUEST ".length) ?? "null")).toEqual({ + channel: "slack", + id: `team:${teamId}:user:U3730E2E`, + accountId: "default", + meta: { + accountId: "default", + channelId: "D3730E2E", + teamId, + senderId: "U3730E2E", + }, + }); + }, + ); + it("loads the managed OpenClaw package without starting a child shell", () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-runtime-managed-root-")); try {