Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
TypeScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186import crypto from "node:crypto";
import { db, eq } from "@openstatus/db";import { incident, incidentEvent, integration,} from "@openstatus/db/src/schema";import { createTestWorkspace } from "@openstatus/db/src/test/factories";import { afterEach, beforeEach, describe, expect, test,} from "@openstatus/test-utils";import { Hono } from "hono";
// workspace-resolver / @slack/web-api / agent are swapped for doubles via the// test import map; behavior is driven through this shared mutable state.import { slackTestState } from "@/libs/test/doubles/slack-test-state";import { TEST_SIGNING_SECRET as SIGNING_SECRET, withSlackConfig,} from "@/libs/test/slack-config";
import { settleBackgroundTasks } from "./background";import type { SlackEnv } from "./config";import { handleSlackEvent, isAnswerToAgent, looksLikeUncardedDraft, toolTaskTitle,} from "./handler";import { abortTurn, broadcastStop, endTurn, startTurn } from "./running-turns";import { verifySlackSignature } from "./verify";
function createTestApp() { const app = withSlackConfig(new Hono<SlackEnv>()); app.post("/slack/events", verifySlackSignature, handleSlackEvent); return app;}
function signAndPost( app: ReturnType<typeof createTestApp>, body: Record<string, unknown>,) { const rawBody = JSON.stringify(body); const timestamp = Math.floor(Date.now() / 1000); const basestring = `v0:${timestamp}:${rawBody}`; const sig = crypto .createHmac("sha256", SIGNING_SECRET) .update(basestring) .digest("hex");
return app.request("/slack/events", { method: "POST", headers: { "Content-Type": "application/json", "x-slack-request-timestamp": String(timestamp), "x-slack-signature": `v0=${sig}`, }, body: rawBody, });}
/** Waits until the background turn stops making Slack calls. */async function settleCalls(quietMs = 150, timeoutMs = 3000) { const deadline = Date.now() + timeoutMs; let seen = -1; while (Date.now() < deadline) { if (slackTestState.calls.length === seen) return; seen = slackTestState.calls.length; await new Promise((r) => setTimeout(r, quietMs)); }}
// Generous: the member gate hits the DB, which is slow under `--parallel`.async function waitForCall(method: string, timeoutMs = 5000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const call = slackTestState.calls.find((m) => m.method === method); if (call) return call; await new Promise((r) => setTimeout(r, 10)); } return undefined;}
function resetSlackTestState() { slackTestState.calls = []; // The seeded member of workspace 1, so every test user is linked. slackTestState.usersInfoImpl = () => Promise.resolve({ ok: true, user: { profile: { email: "ping@openstatus.dev" } }, }); slackTestState.postMessageOverride = null; slackTestState.updateOverride = null; slackTestState.postEphemeralOverride = null; slackTestState.runAgentOverride = null; slackTestState.renameOverride = null; slackTestState.chatStreamEnabled = true; slackTestState.historyImpl = () => Promise.resolve({ messages: [{ user: "U1", text: "channel message", ts: "1.1" }], }); slackTestState.streamAppendFailAfter = null; slackTestState.streamStopFail = false; slackTestState.repliesImpl = () => Promise.resolve({ messages: [{ user: "U1", text: "test message", ts: "1.1" }], }); // Default to a workspace without agent sessions so the tests below cover // the fallback indicators; the session path opts back in explicitly. slackTestState.sessionStatusOverride = () => { const err = new Error("An API error occurred: feature_disabled"); Object.assign(err, { code: "slack_webapi_platform_error", data: { ok: false, error: "feature_disabled" }, }); return Promise.reject(err); }; slackTestState.resolveWorkspace = (teamId: string) => { if (teamId === "T_KNOWN") { return Promise.resolve({ workspace: { id: 1, name: "Test Workspace", slug: "test", plan: "free", limits: { "slack-agent": true }, }, botToken: "xoxb-test", botUserId: "UBOT", }); } return Promise.resolve(null); };}
describe("handleSlackEvent", () => { const app = createTestApp();
beforeEach(resetSlackTestState);
test("responds to url_verification challenge", async () => { const res = await signAndPost(app, { type: "url_verification", challenge: "test-challenge-123", });
expect(res.status).toBe(200); const json = (await res.json()) as { challenge: string }; expect(json.challenge).toBe("test-challenge-123"); });
test("returns ok for non-event_callback types", async () => { const res = await signAndPost(app, { type: "app_rate_limited", });
expect(res.status).toBe(200); const json = (await res.json()) as { ok: boolean }; expect(json.ok).toBe(true); });
test("returns ok for event_callback", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_${Date.now()}_1`, event: { type: "app_mention", text: "<@UBOT> create an incident", user: "U1", channel: "C1", ts: "100.1", }, });
expect(res.status).toBe(200); const json = (await res.json()) as { ok: boolean }; expect(json.ok).toBe(true); });
test("publishes the home view on app_home_opened", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_home_${Date.now()}`, event: { type: "app_home_opened", tab: "home", user: "U1", }, });
expect(res.status).toBe(200); const publish = await waitForCall("views.publish"); expect(publish).toBeDefined(); expect((publish?.args.view as { type: string }).type).toBe("home"); expect(publish?.args.user_id).toBe("U1"); });
test("ignores app_home_opened for the messages tab", async () => { // Earlier tests' events may still be posting; only this one's calls count. await settleBackgroundTasks(); slackTestState.calls = []; const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_home_msgs_${Date.now()}`, event: { type: "app_home_opened", tab: "messages", user: "U1", }, });
expect(res.status).toBe(200); await settleBackgroundTasks(); expect(slackTestState.calls.length).toBe(0); });
test("responds once when a mention arrives as app_mention and message", async () => { const ts = `${Date.now()}.55`; const base = { type: "event_callback", team_id: "T_KNOWN", event: { text: "<@UBOT> create an incident", user: "U1", channel: "C1", channel_type: "channel", ts, }, };
// Same underlying message, delivered as two distinct events. await signAndPost(app, { ...base, event_id: `evt_mention_${ts}`, event: { ...base.event, type: "app_mention" }, }); await signAndPost(app, { ...base, event_id: `evt_message_${ts}`, event: { ...base.event, type: "message" }, }); await new Promise((r) => setTimeout(r, 100));
const thinking = slackTestState.calls.filter( (m) => m.method === "postMessage", ); expect(thinking.length).toBe(1); });
test("handles app_uninstalled event", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_uninstall_${Date.now()}`, event: { type: "app_uninstalled", }, });
expect(res.status).toBe(200); });
test("handles tokens_revoked event", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_revoked_${Date.now()}`, event: { type: "tokens_revoked", }, });
expect(res.status).toBe(200); });
test("deduplicates events with same event_id", async () => { const eventId = `evt_dedup_${Date.now()}`; const body = { type: "event_callback", team_id: "T_KNOWN", event_id: eventId, event: { type: "app_mention", text: "<@UBOT> hello", user: "U1", channel: "C1", ts: `${Date.now()}.1`, }, };
await signAndPost(app, body); await settleCalls();
slackTestState.calls = []; await signAndPost(app, body); await new Promise((r) => setTimeout(r, 150));
expect(slackTestState.calls.length).toBe(0); });
test("ignores events from unknown teams", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_UNKNOWN", event_id: `evt_unknown_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> hello", user: "U1", channel: "C1", ts: `${Date.now()}.2`, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.length).toBe(0); });
test("ignores message events from bots", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_bot_${Date.now()}`, event: { type: "message", text: "bot message", bot_id: "B123", channel: "C1", ts: `${Date.now()}.3`, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.length).toBe(0); });
test("ignores channel message without bot mention", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_nomention_${Date.now()}`, event: { type: "message", text: "just a regular message", user: "U1", channel: "C1", channel_type: "channel", ts: `${Date.now()}.4`, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.length).toBe(0); });
test("marks the agent session as processing instead of posting Thinking", async () => { slackTestState.sessionStatusOverride = null; const ts = `${Date.now()}.60`;
await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_session_${ts}`, event: { type: "app_mention", text: "<@UBOT> which reports are open?", user: "U1", channel: "C1", ts, }, }); await new Promise((r) => setTimeout(r, 100));
const statuses = slackTestState.calls .filter((m) => m.method === "agents.sessions.setStatus") .map((m) => m.args); expect(statuses).toEqual([ { channel_id: "C1", thread_ts: ts, status: "processing", initiator_user_id: "U1", }, { channel_id: "C1", thread_ts: ts, status: "active" }, ]);
const posts = slackTestState.calls.filter( (m) => m.method === "postMessage", ); expect(posts.length).toBe(1); expect(posts[0].args).toMatchObject({ channel: "C1", thread_ts: ts, text: "Here is my response", }); expect(slackTestState.calls.some((m) => m.method === "update")).toBe(false); });
test("hands the agent session back as active when runAgent throws", async () => { slackTestState.sessionStatusOverride = null; slackTestState.runAgentOverride = () => Promise.reject(new Error("agent exploded")); const ts = `${Date.now()}.61`;
await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_session_err_${ts}`, event: { type: "app_mention", text: "<@UBOT> hello", user: "U1", channel: "C1", ts, }, }); await new Promise((r) => setTimeout(r, 100));
const errorPost = slackTestState.calls.find( (m) => m.method === "postMessage" && typeof m.args.text === "string" && m.args.text.includes("Something went wrong"), ); expect(errorPost).toBeDefined(); const last = slackTestState.calls .filter((m) => m.method === "agents.sessions.setStatus") .at(-1); expect(last?.args.status).toBe("active"); });
test("answers an untagged reply to its own question", async () => { const ts = `${Date.now()}.70`; slackTestState.repliesImpl = () => Promise.resolve({ messages: [ { user: "U1", text: "<@UBOT> update my status page", ts: "5.1" }, { user: "UBOT", bot_id: "B1", text: "Which page?", ts: "5.2" }, { user: "U1", text: "acme, id 1", ts }, ], });
await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_untagged_${ts}`, event: { type: "message", text: "acme, id 1", user: "U1", channel: "C1", channel_type: "channel", ts, thread_ts: "5.1", }, }); await new Promise((r) => setTimeout(r, 100));
const answer = slackTestState.calls.find( (m) => (m.method === "update" || m.method === "postMessage") && m.args.text === "Here is my response", ); expect(answer).toBeDefined(); });
test("ignores an untagged thread reply that isn't answering the agent", async () => { const ts = `${Date.now()}.71`; slackTestState.repliesImpl = () => Promise.resolve({ messages: [ { user: "U1", text: "<@UBOT> update my status page", ts: "6.1" }, { user: "UBOT", bot_id: "B1", text: "Which page?", ts: "6.2" }, { user: "U2", text: "I'll check the logs", ts }, ], });
await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_untagged_other_${ts}`, event: { type: "message", text: "I'll check the logs", user: "U2", channel: "C1", channel_type: "channel", ts, thread_ts: "6.1", }, }); await new Promise((r) => setTimeout(r, 100));
expect(slackTestState.calls.length).toBe(0); });
test("replies in the agent pane without a mention", async () => { const ts = `${Date.now()}.5`; const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_dm_${ts}`, event: { type: "message", text: "which reports are open?", user: "U1", channel: "D1", channel_type: "im", ts, thread_ts: "1.1", }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 100));
const status = slackTestState.calls.find( (m) => m.method === "assistant.threads.setStatus", ); expect(status?.args).toMatchObject({ channel_id: "D1", thread_ts: "1.1" });
// Native status replaces the "Thinking..." placeholder: the answer is // a single fresh message in the thread, never an update. const posts = slackTestState.calls.filter( (m) => m.method === "postMessage", ); expect(posts.length).toBe(1); expect(posts[0].args).toMatchObject({ channel: "D1", thread_ts: "1.1", text: "Here is my response", }); expect(slackTestState.calls.some((m) => m.method === "update")).toBe(false); });
test("ignores the agent pane thread root and edits", async () => { for (const subtype of ["assistant_app_thread", "message_changed"]) { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_dm_${subtype}_${Date.now()}`, event: { type: "message", subtype, text: "hello", user: "U1", channel: "D1", channel_type: "im", ts: `${Date.now()}.6`, }, }); expect(res.status).toBe(200); }
await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.length).toBe(0); });
test("posts the error in the agent pane when runAgent throws", async () => { slackTestState.runAgentOverride = () => Promise.reject(new Error("agent exploded"));
await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_dm_err_${Date.now()}`, event: { type: "message", text: "hello", user: "U1", channel: "D1", channel_type: "im", ts: `${Date.now()}.7`, thread_ts: "1.1", }, }); await new Promise((r) => setTimeout(r, 100));
const errorPost = slackTestState.calls.find( (m) => m.method === "postMessage" && typeof m.args.text === "string" && m.args.text.includes("Something went wrong"), ); expect(errorPost).toBeDefined(); });
test("greets in the thread on the legacy assistant_thread_started", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_thread_started_${Date.now()}`, event: { type: "assistant_thread_started", assistant_thread: { user_id: "U1", channel_id: "D1", thread_ts: "2.2", context: {}, }, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 50));
// The greeting lands in the thread that was just opened, and states the // approval guarantee where it matters rather than only in App Home. const welcome = slackTestState.calls.find( (m) => m.method === "postMessage", ); expect(welcome?.args).toMatchObject({ channel: "D1", thread_ts: "2.2" }); expect(welcome?.args.text as string).toContain("Approve"); expect((welcome?.args.blocks as unknown[]).length).toBeGreaterThan(0); });
test("ignores events without channel", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_nochan_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> hello", user: "U1", ts: `${Date.now()}.6`, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.length).toBe(0); });
test("ignores events without timestamp", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_nots_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> hello", user: "U1", channel: "C1", }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.length).toBe(0); });
test("ignores events without team_id", async () => { const res = await signAndPost(app, { type: "event_callback", event_id: `evt_noteam_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> hello", user: "U1", channel: "C1", ts: `${Date.now()}.7`, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.length).toBe(0); });
test("ignores unsupported event types", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_unsupported_${Date.now()}`, event: { type: "channel_created", }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.length).toBe(0); });
test("ignores channel_join system messages", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_join_${Date.now()}`, event: { type: "message", subtype: "channel_join", text: "<@U1> has joined the channel", user: "U1", channel: "C1", ts: `${Date.now()}.10`, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.length).toBe(0); });
test("ignores channel_leave system messages", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_leave_${Date.now()}`, event: { type: "message", subtype: "channel_leave", text: "<@U1> has left the channel", user: "U1", channel: "C1", ts: `${Date.now()}.11`, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.length).toBe(0); });
test("ignores events with no event payload", async () => { const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_noevent_${Date.now()}`, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.length).toBe(0); });
test("falls back to top-level message on cannot_reply_to_message", async () => { let callCount = 0; slackTestState.postMessageOverride = (args: Record<string, unknown>) => { callCount++; if (callCount === 1) { const err = new Error("An API error occurred: cannot_reply_to_message"); Object.assign(err, { code: "slack_webapi_platform_error", data: { ok: false, error: "cannot_reply_to_message" }, }); return Promise.reject(err); } slackTestState.calls.push({ method: "postMessage", args }); return Promise.resolve({ ts: "fallback.ts" }); };
const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_cantreply_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> hello", user: "U1", channel: "C1", ts: `${Date.now()}.20`, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 100));
const fallbackPost = slackTestState.calls.find( (m) => m.method === "postMessage" && !m.args.thread_ts, ); expect(fallbackPost).toBeDefined(); });
test("returns early on non-recoverable postMessage error", async () => { slackTestState.postMessageOverride = () => { const err = new Error("An API error occurred: channel_not_found"); Object.assign(err, { code: "slack_webapi_platform_error", data: { ok: false, error: "channel_not_found" }, }); return Promise.reject(err); };
const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_channotfound_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> hello", user: "U1", channel: "C1", ts: `${Date.now()}.21`, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 100));
const updateMessages = slackTestState.calls.filter( (m) => m.method === "update", ); expect(updateMessages.length).toBe(0); });
test("shows error message when runAgent throws", async () => { slackTestState.runAgentOverride = () => Promise.reject(new Error("agent exploded"));
const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_agenterr_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> hello", user: "U1", channel: "C1", ts: `${Date.now()}.30`, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 100));
// Delivered as a fresh message when streaming, or by overwriting the // "Thinking..." placeholder when it isn't โ either way the user sees it. const errorMessage = slackTestState.calls.find( (m) => (m.method === "update" || m.method === "postMessage") && typeof m.args.text === "string" && m.args.text.includes("Something went wrong"), ); expect(errorMessage).toBeDefined(); });
test("does not throw when both runAgent and error update fail", async () => { slackTestState.runAgentOverride = () => Promise.reject(new Error("agent exploded")); slackTestState.updateOverride = () => { const err = new Error("An API error occurred: channel_not_found"); Object.assign(err, { code: "slack_webapi_platform_error", data: { ok: false, error: "channel_not_found" }, }); return Promise.reject(err); };
const res = await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_doublefail_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> hello", user: "U1", channel: "C1", ts: `${Date.now()}.31`, }, });
expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 100)); // No unhandled rejection โ the .catch() in the error handler swallows it });});
describe("looksLikeUncardedDraft", () => { test("flags a prose draft that ends by asking permission", () => { expect( looksLikeUncardedDraft( `There's one status page ("Inter.link"). Here's the draft incident report I'll create:
**Title:** Traffic Forwarding Issue โ POP FRA1-DE**Status:** Investigating**Message:** We are currently investigating a traffic forwarding issue.
Shall I go ahead and publish this, or would you like to adjust anything?`, ), ).toBe(true); });
test("flags the other permission phrasings", () => { const draft = "*Title:* API outage\n*Message:* We're on it.\n"; expect(looksLikeUncardedDraft(`${draft}Want me to create this?`)).toBe( true, ); expect( looksLikeUncardedDraft(`${draft}Would you like me to post it now?`), ).toBe(true); });
test("ignores a plain answer that happens to end in a question", () => { expect( looksLikeUncardedDraft( "You have no active incidents. Want me to create one?", ), ).toBe(false); });
test("ignores a listing with field labels but no permission question", () => { expect( looksLikeUncardedDraft( "*Title:* API outage\n*Status:* investigating\n*Message:* We're on it.", ), ).toBe(false); });
test("ignores empty text", () => { expect(looksLikeUncardedDraft("")).toBe(false); });});
describe("isAnswerToAgent", () => { const starter = { user: "U1", text: "<@UBOT> open an incident", ts: "1" }; const question = { user: "UBOT", bot_id: "B1", text: "Which page?", ts: "2" };
test("answers the session starter replying right after the agent", () => { const thread = [starter, question, { user: "U1", text: "API", ts: "3" }]; expect(isAnswerToAgent(thread, { ts: "3", user: "U1" }, "UBOT")).toBe(true); });
test("ignores someone other than the session starter", () => { const thread = [starter, question, { user: "U2", text: "API", ts: "3" }]; expect(isAnswerToAgent(thread, { ts: "3", user: "U2" }, "UBOT")).toBe( false, ); });
test("ignores the starter once a human spoke after the agent", () => { const thread = [ starter, question, { user: "U2", text: "it's the API", ts: "3" }, { user: "U1", text: "yes, the API", ts: "4" }, ]; expect(isAnswerToAgent(thread, { ts: "4", user: "U1" }, "UBOT")).toBe( false, ); });
test("ignores threads where the agent was never mentioned", () => { const thread = [ { user: "U1", text: "anyone seeing errors?", ts: "1" }, question, { user: "U1", text: "API", ts: "3" }, ]; expect(isAnswerToAgent(thread, { ts: "3", user: "U1" }, "UBOT")).toBe( false, ); });
test("ignores other bots' messages as the previous message", () => { const thread = [ starter, { user: "UOTHER", bot_id: "B2", text: "Deploy finished", ts: "2" }, { user: "U1", text: "API", ts: "3" }, ]; expect(isAnswerToAgent(thread, { ts: "3", user: "U1" }, "UBOT")).toBe( false, ); });});
describe("streaming the agent's answer", () => { const app = createTestApp();
beforeEach(resetSlackTestState);
/** Drives the agent mock's `events` so the handler sees a real stream. */ function streamTurn( drive: (events: { onTextDelta(delta: string): Promise<void>; onToolCall(c: { id: string; toolName: string }): Promise<void>; onToolResult(r: { id: string; toolName: string }): Promise<void>; }) => Promise<void>, text = "All five monitors are healthy.", ) { slackTestState.runAgentOverride = async (options: unknown) => { // biome-ignore lint/suspicious/noExplicitAny: test double plumbing await drive((options as any).events); return { text, toolResults: [], finishReason: "stop", stepCount: 1, hitStepLimit: false, aborted: false, }; }; }
function mention(suffix: string) { return signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_stream_${suffix}`, event: { type: "app_mention", text: "<@UBOT> what's broken?", user: "U1", channel: "C1", ts: `${Date.now()}.${suffix}`, }, }); }
test("streams the answer instead of posting it", async () => { streamTurn(async (events) => { await events.onTextDelta("All five monitors "); await events.onTextDelta("are healthy."); });
const res = await mention("1"); expect(res.status).toBe(200); await new Promise((r) => setTimeout(r, 100));
const appended = slackTestState.calls .filter((m) => m.method === "stream.append") .map((m) => m.args.markdown_text); expect(appended).toEqual(["All five monitors ", "are healthy."]);
// The stream carried the answer, so it is finalized rather than re-posted. expect(slackTestState.calls.some((m) => m.method === "stream.stop")).toBe( true, ); expect(slackTestState.calls.some((m) => m.method === "postMessage")).toBe( false, ); });
test("reports each tool call as a task", async () => { streamTurn(async (events) => { await events.onToolCall({ id: "t1", toolName: "list_status_pages" }); await events.onToolResult({ id: "t1", toolName: "list_status_pages" }); await events.onTextDelta("Done."); });
await mention("2"); await new Promise((r) => setTimeout(r, 100));
const tasks = slackTestState.calls .filter((m) => m.method === "stream.append" && m.args.chunks) .flatMap((m) => m.args.chunks as Record<string, unknown>[]); expect(tasks).toEqual([ { type: "task_update", id: "t1", title: "Reading status pages", status: "in_progress", }, { type: "task_update", id: "t1", title: "Reading status pages", status: "complete", }, ]); });
test("falls back to the thread status when the workspace has no streaming", async () => { slackTestState.chatStreamEnabled = false; streamTurn(async () => {});
await mention("3"); await new Promise((r) => setTimeout(r, 100));
// No agent session and no stream leaves the thread status as the only // loading indicator; the answer is posted as a message of its own. expect( slackTestState.calls.some( (m) => m.method === "assistant.threads.setStatus", ), ).toBe(true); const posts = slackTestState.calls.filter( (m) => m.method === "postMessage", ); expect(posts.map((m) => m.args.text)).toEqual([ "All five monitors are healthy.", ]); expect(slackTestState.calls.some((m) => m.method === "update")).toBe(false); expect(slackTestState.calls.some((m) => m.method === "chatStream")).toBe( false, ); });
test("rewrites the partial message when the stream breaks mid-turn", async () => { // The first append lands, the second fails โ Slack is left holding half // an answer, so the whole answer has to replace it. slackTestState.streamAppendFailAfter = 1; streamTurn(async (events) => { await events.onTextDelta("All five "); await events.onTextDelta("monitors are healthy."); });
await mention("4"); await new Promise((r) => setTimeout(r, 100));
const rewrite = slackTestState.calls.find((m) => m.method === "update"); expect(rewrite?.args).toMatchObject({ channel: "C1", ts: "stream.ts", text: "All five monitors are healthy.", }); // A stream that was opened is closed even though a later append failed โ // otherwise Slack shows that message as streaming for good. expect(slackTestState.calls.some((m) => m.method === "stream.stop")).toBe( true, ); // Nothing is posted alongside it: one message, one answer. expect(slackTestState.calls.some((m) => m.method === "postMessage")).toBe( false, ); });
test("rewrites the message when the stream cannot be closed", async () => { // Slack has closed the stream on its side, so `stop` rejects โ the // half-written message is ours to finish. slackTestState.streamStopFail = true; streamTurn(async (events) => { await events.onTextDelta("All five monitors are healthy."); });
await mention("5"); await new Promise((r) => setTimeout(r, 100));
const rewrite = slackTestState.calls.find((m) => m.method === "update"); expect(rewrite?.args).toMatchObject({ channel: "C1", ts: "stream.ts", text: "All five monitors are healthy.", }); });
test("names tasks after the tool's verb", () => { expect(toolTaskTitle("list_status_pages")).toBe("Reading status pages"); expect(toolTaskTitle("get_monitor_status")).toBe("Reading monitor status"); expect(toolTaskTitle("create_status_report")).toBe( "Drafting status report", ); expect(toolTaskTitle("search_docs")).toBe("Searching docs"); // An unknown verb still reads as words rather than a tool name. expect(toolTaskTitle("frobnicate_widgets")).toBe("frobnicate widgets"); expect(toolTaskTitle("ping")).toBe("ping"); });});
describe("running turns", () => { test("aborts only the thread it was asked about", () => { const turn = startTurn("C_RT", "1.1"); expect(abortTurn("C_RT", "9.9")).toBe(false); expect(turn.signal.aborted).toBe(false);
expect(abortTurn("C_RT", "1.1")).toBe(true); expect(turn.signal.aborted).toBe(true);
endTurn("C_RT", "1.1", turn); expect(abortTurn("C_RT", "1.1")).toBe(false); });
test("a finished turn does not deregister the one that replaced it", () => { const first = startTurn("C_RT2", "2.2"); const second = startTurn("C_RT2", "2.2"); endTurn("C_RT2", "2.2", first);
expect(abortTurn("C_RT2", "2.2")).toBe(true); expect(second.signal.aborted).toBe(true); endTurn("C_RT2", "2.2", second); });
test("stops every turn running on the thread", () => { // Two messages sent in quick succession overlap; stop means stop the // thread, not only its newest turn. const first = startTurn("C_RT3", "3.3"); const second = startTurn("C_RT3", "3.3");
expect(abortTurn("C_RT3", "3.3")).toBe(true); expect(first.signal.aborted).toBe(true); expect(second.signal.aborted).toBe(true);
endTurn("C_RT3", "3.3", first); endTurn("C_RT3", "3.3", second); expect(abortTurn("C_RT3", "3.3")).toBe(false); });});
describe("stopping a turn", () => { const app = createTestApp();
beforeEach(() => { resetSlackTestState(); // A workspace with agent sessions โ the surface the stop button lives on. slackTestState.sessionStatusOverride = null; });
function stopEvent(channel: string, threadTs: string) { return signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_stop_${channel}_${threadTs}`, event: { type: "agent_session_stopped", channel, thread_ts: threadTs, user: "U1", streaming_message_ts: [], event_ts: "1.1", }, }); }
function activeStatusCalls() { return slackTestState.calls.filter( (m) => m.method === "agents.sessions.setStatus" && m.args.status === "active", ); }
test("aborts the run, confirms the stop, and clears the status", async () => { let signal: AbortSignal | undefined; slackTestState.runAgentOverride = (options: unknown) => { signal = (options as { signal: AbortSignal }).signal; return new Promise((resolve) => { signal?.addEventListener("abort", () => resolve({ text: "Looking at the API monit", toolResults: [], finishReason: "abort", stepCount: 0, hitStepLimit: false, aborted: true, }), ); }); };
const ts = "6001.1"; await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_stopme_${ts}`, event: { type: "app_mention", text: "<@UBOT> what's broken?", user: "U1", channel: "C_STOP", ts, }, }); await new Promise((r) => setTimeout(r, 50)); expect(signal).toBeDefined(); expect(signal?.aborted).toBe(false);
await stopEvent("C_STOP", ts); await new Promise((r) => setTimeout(r, 100));
expect(signal?.aborted).toBe(true); // Twice over: the aborted turn's own cleanup, and the stop branch, which // can't rely on that turn โ it may be running on another instance. const cleared = activeStatusCalls().filter( (m) => m.args.channel_id === "C_STOP" && m.args.thread_ts === ts, ); expect(cleared.length).toBeGreaterThanOrEqual(2);
const notice = slackTestState.calls.find( (m) => m.method === "postMessage" && typeof m.args.text === "string" && m.args.text.includes("Stopped"), ); expect(notice).toBeDefined();
// The half-written answer is never delivered as if it were finished. const answer = slackTestState.calls.find( (m) => typeof m.args.text === "string" && m.args.text.includes("Looking at the API monit"), ); expect(answer).toBeUndefined(); });
test("clears the status even when no turn is running here", async () => { await stopEvent("C_STOP2", "7001.1"); await new Promise((r) => setTimeout(r, 50));
// Nothing to abort โ another instance may hold the turn โ but the user // still has to get out of the loading state. expect(activeStatusCalls()).toHaveLength(1); expect(activeStatusCalls()[0].args).toMatchObject({ channel_id: "C_STOP2", thread_ts: "7001.1", }); });});
describe("titling a thread", () => { const app = createTestApp(); const redisStore = (globalThis as Record<string, unknown>) .__testRedisStore as Map<string, string>;
beforeEach(() => { resetSlackTestState(); redisStore.clear(); });
function renameCalls() { return slackTestState.calls.filter( (m) => m.method === "agents.sessions.rename", ); }
function paneMessage(ts: string, text: string, threadTs?: string) { return signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_title_${ts}`, event: { type: "message", channel_type: "im", text, user: "U1", channel: "D_TITLE", ts, ...(threadTs ? { thread_ts: threadTs } : {}), }, }); }
test("names the pane thread after what the user asked", async () => { await paneMessage("8001.1", "which reports are currently open?"); await new Promise((r) => setTimeout(r, 100));
expect(renameCalls()).toHaveLength(1); expect(renameCalls()[0].args).toMatchObject({ channel_id: "D_TITLE", thread_ts: "8001.1", title: "which reports are currently open?", }); });
test("names it once and leaves it alone after that", async () => { await paneMessage("8002.1", "is the checkout monitor healthy?"); await new Promise((r) => setTimeout(r, 100)); expect(renameCalls()).toHaveLength(1);
// A second turn on the same thread: the subject hasn't changed, and the // name shouldn't follow whatever was asked next. await paneMessage("8002.2", "and what about billing?", "8002.1"); await new Promise((r) => setTimeout(r, 100)); expect(renameCalls()).toHaveLength(1); });
test("stops renaming once a person has named it", async () => { await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: "evt_renamed_8003", event: { type: "agent_session_title_changed", channel: "D_TITLE", thread_ts: "8003.1", user: "U1", title: "Tuesday's Stripe outage", event_ts: "1.1", }, }); await new Promise((r) => setTimeout(r, 50));
await paneMessage("8003.2", "any update on this?", "8003.1"); await new Promise((r) => setTimeout(r, 100));
expect(renameCalls()).toHaveLength(0); });
test("leaves channel threads alone", async () => { // `agents.sessions.rename` also renames the channel for session channels โ // not worth risking on a shared incident channel for a name nobody lists. await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: "evt_title_channel", event: { type: "app_mention", text: "<@UBOT> what's broken?", user: "U1", channel: "C_TITLE", ts: "8004.1", }, }); await new Promise((r) => setTimeout(r, 100));
expect(renameCalls()).toHaveLength(0); });
test("keeps the answer when renaming fails", async () => { slackTestState.renameOverride = () => Promise.reject(new Error("feature_disabled"));
await paneMessage("8005.1", "which reports are open?"); await new Promise((r) => setTimeout(r, 100));
const answered = slackTestState.calls.some( (m) => typeof m.args.text === "string" && m.args.text.includes("Here is my response"), ); expect(answered).toBe(true); // A failed rename must not mark the thread as named. expect(redisStore.has("slack:title:D_TITLE:8005.1")).toBe(false); });});
describe("greeting on first contact", () => { const app = createTestApp(); const redisStore = (globalThis as Record<string, unknown>) .__testRedisStore as Map<string, string>;
beforeEach(() => { resetSlackTestState(); redisStore.clear(); });
function homeOpened(tab: string, userId = "U1") { return signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_home_${tab}_${userId}_${Math.random()}`, event: { type: "app_home_opened", user: userId, channel: "D_WELCOME", tab, event_ts: "1.1", }, }); }
function welcomes() { return slackTestState.calls.filter( (m) => m.method === "postMessage" && typeof m.args.text === "string" && m.args.text.includes("Approve"), ); }
test("greets when the Messages tab is opened", async () => { await homeOpened("messages"); await settleBackgroundTasks();
expect(welcomes()).toHaveLength(1); // Top-level in the DM: the agent experience has no thread to greet into. expect(welcomes()[0].args.thread_ts).toBeUndefined(); });
test("greets a person once, however often they open it", async () => { await homeOpened("messages"); await settleBackgroundTasks(); await homeOpened("messages"); await settleBackgroundTasks();
expect(welcomes()).toHaveLength(1); });
test("greets each person separately", async () => { await homeOpened("messages", "U1"); await settleBackgroundTasks(); await homeOpened("messages", "U2"); await settleBackgroundTasks();
expect(welcomes()).toHaveLength(2); });
test("publishes the home view on the Home tab without greeting", async () => { await homeOpened("home"); await settleBackgroundTasks();
expect(slackTestState.calls.some((m) => m.method === "views.publish")).toBe( true, ); expect(welcomes()).toHaveLength(0); });
test("does not mark someone greeted when the greeting fails", async () => { slackTestState.postMessageOverride = () => Promise.reject(new Error("channel_not_found"));
await homeOpened("messages"); await settleBackgroundTasks();
// Otherwise a transient failure costs them the greeting permanently. expect(redisStore.has("slack:greeted:T_KNOWN:U1")).toBe(false); });});
describe("the channel the user is viewing", () => { const app = createTestApp(); const redisStore = (globalThis as Record<string, unknown>) .__testRedisStore as Map<string, string>;
beforeEach(() => { resetSlackTestState(); redisStore.clear(); });
function contextChanged( entities: Array<Record<string, string>> | undefined, userId = "U1", ) { return signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_ctx_${userId}_${Math.random()}`, event: { type: "app_context_changed", context: entities ? { entities } : {}, }, authorizations: [ { user_id: "B0", is_bot: true }, { user_id: userId, is_bot: false }, ], }); }
/** Captures what the handler handed the agent for this turn. */ function captureAgentOptions() { const seen: { tools?: Record<string, unknown>; contextNote?: string }[] = []; slackTestState.runAgentOverride = (options: unknown) => { seen.push(options as { contextNote?: string }); return Promise.resolve({ text: "Here is my response", toolResults: [], finishReason: "stop", stepCount: 1, hitStepLimit: false, aborted: false, }); }; return seen; }
test("remembers it for the authorizing human", async () => { await contextChanged([ { type: "slack#/types/channel_id", value: "C_INCIDENT" }, ]); await new Promise((r) => setTimeout(r, 50));
expect(redisStore.get("slack:context:T_KNOWN:U1")).toBe("C_INCIDENT"); // Never attributed to the bot authorization. expect(redisStore.has("slack:context:T_KNOWN:B0")).toBe(false); });
test("forgets it when the context empties", async () => { await contextChanged([ { type: "slack#/types/channel_id", value: "C_INCIDENT" }, ]); await new Promise((r) => setTimeout(r, 50)); await contextChanged(undefined); await new Promise((r) => setTimeout(r, 50));
// A channel they left is worse than no context at all. expect(redisStore.has("slack:context:T_KNOWN:U1")).toBe(false); });
test("offers the channel to the agent on a pane turn", async () => { await contextChanged([ { type: "slack#/types/channel_id", value: "C_INCIDENT" }, ]); await new Promise((r) => setTimeout(r, 50));
const seen = captureAgentOptions(); await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: "evt_ctx_turn", event: { type: "message", channel_type: "im", text: "draft an update for this", user: "U1", channel: "D_CTX", ts: "9101.1", }, }); await new Promise((r) => setTimeout(r, 100));
expect(seen).toHaveLength(1); expect(seen[0].contextNote).toContain("<#C_INCIDENT>"); expect(Object.keys(seen[0].tools ?? {})).toContain("read_slack_channel"); // Nothing is read until the model decides the request calls for it. expect( slackTestState.calls.some((m) => m.method === "conversations.history"), ).toBe(false); });
test("leaves channel turns alone", async () => { await contextChanged([ { type: "slack#/types/channel_id", value: "C_INCIDENT" }, ]); await new Promise((r) => setTimeout(r, 50));
const seen = captureAgentOptions(); await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: "evt_ctx_channel_turn", event: { type: "app_mention", text: "<@UBOT> what's broken?", user: "U1", channel: "C_OTHER", ts: "9102.1", }, }); await new Promise((r) => setTimeout(r, 100));
// In a channel the agent already has the thread it was called into. expect(seen).toHaveLength(1); expect(seen[0].contextNote).toBeUndefined(); expect(seen[0].tools).toBeUndefined(); });});
describe("confirmation cards", () => { const app = createTestApp();
beforeEach(resetSlackTestState);
test("posts a card for every draft in the turn", async () => { const draft = (toolName: string, input: Record<string, unknown>) => ({ toolName, result: { needsConfirmation: true, toolName, input, displayInput: input }, }); slackTestState.runAgentOverride = () => Promise.resolve({ text: "Two cards for you.", toolResults: [ draft("update_status_report", { statusReportId: 7, title: "Elevated API error rate", }), draft("add_status_report_update", { statusReportId: 7, status: "investigating", message: "Some requests return 500s.", }), ], finishReason: "stop", stepCount: 2, hitStepLimit: false, aborted: false, });
await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_two_cards_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> rename it and post an update", user: "U1", channel: "C1", ts: `${Date.now()}.30`, }, }); await new Promise((r) => setTimeout(r, 100));
const actionIds = slackTestState.calls .filter((m) => m.method === "update" && Array.isArray(m.args.blocks)) .flatMap((m) => ( m.args.blocks as { type: string; elements?: { action_id: string }[]; }[] ) .filter((b) => b.type === "actions") .flatMap((b) => b.elements?.map((e) => e.action_id) ?? []), ); // Rename: approve + cancel. Update: approve, approve & notify, cancel. expect(actionIds.filter((id) => id.startsWith("cancel_"))).toHaveLength(2); expect( actionIds.filter((id) => id.startsWith("approve_flag_")), ).toHaveLength(1); // The second card is a message of its own, not an overwrite of the first. expect( slackTestState.calls.filter((m) => m.method === "postMessage").length, ).toBeGreaterThanOrEqual(2); });});
describe("members only", () => { const app = createTestApp(); const redisStore = (globalThis as Record<string, unknown>) .__testRedisStore as Map<string, string>;
beforeEach(() => { resetSlackTestState(); slackTestState.usersInfoImpl = () => Promise.resolve({ ok: true, user: { profile: {} } }); });
const unlinked = () => `U_UNLINKED_${crypto.randomUUID()}`; let agentRuns = 0;
test("a mention from an unlinked user gets a link card, not an answer", async () => { agentRuns = 0; slackTestState.runAgentOverride = () => { agentRuns++; return Promise.resolve({ text: "should not run", toolResults: [], finishReason: "stop", stepCount: 1, hitStepLimit: false, aborted: false, }); }; const user = unlinked(); await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_unlinked_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> what is down?", user, channel: "C1", channel_type: "channel", ts: `${Date.now()}.71`, }, }); const card = await waitForCall("postEphemeral"); expect(card?.args.user).toBe(user); expect(String(card?.args.text)).toContain("Link your openstatus account"); const blocks = card?.args.blocks as { type: string; elements?: { url?: string }[]; }[]; const url = blocks.find((b) => b.type === "actions")?.elements?.[0] ?.url as string; expect(url).toContain("/settings/integrations/slack/link?token="); expect(agentRuns).toBe(0); });
test("the link card is sent once per window on passive surfaces", async () => { const user = unlinked(); for (const suffix of ["81", "82"]) { await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_unlinked_twice_${suffix}_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> hello", user, channel: "C1", channel_type: "channel", ts: `${Date.now()}.${suffix}`, }, }); await settleBackgroundTasks(); } expect( slackTestState.calls.filter((c) => c.method === "postEphemeral"), ).toHaveLength(1); expect(redisStore.has(`slack:linkcard:T_KNOWN:${user}`)).toBe(true); });
test("an unlinked user in the agent pane gets the card in the thread", async () => { const user = unlinked(); await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_unlinked_im_${Date.now()}`, event: { type: "message", text: "hi", user, channel: "D1", channel_type: "im", ts: `${Date.now()}.91`, }, }); const card = await waitForCall("postMessage"); expect(String(card?.args.text)).toContain("Link your openstatus account"); expect(card?.args.channel).toBe("D1"); });
test("the home tab shows the link view to an unlinked user", async () => { await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_unlinked_home_${Date.now()}`, event: { type: "app_home_opened", tab: "home", user: unlinked() }, }); const publish = await waitForCall("views.publish"); const view = publish?.args.view as { blocks: { type: string }[] }; expect(view.blocks.some((b) => b.type === "actions")).toBe(true); });});
describe("hardening", () => { const app = createTestApp(); const redisStore = (globalThis as Record<string, unknown>) .__testRedisStore as Map<string, string>;
beforeEach(resetSlackTestState);
test("a workspace without the Slack agent gets a plan notice", async () => { slackTestState.resolveWorkspace = () => Promise.resolve({ workspace: { id: 1, name: "Free", slug: "free", plan: "free", limits: {}, }, botToken: "xoxb-test", botUserId: "UBOT", }); const user = `U_FREE_${crypto.randomUUID()}`; await signAndPost(app, { type: "event_callback", team_id: "T_FREE", event_id: `evt_free_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> hi", user, channel: "C1", channel_type: "channel", ts: `${Date.now()}.61`, }, }); const notice = await waitForCall("postEphemeral"); expect(String(notice?.args.text)).toContain("/settings/billing"); // The plan notice must not use up the account-link card's window. expect(redisStore.has(`slack:linkcard:T_FREE:${user}`)).toBe(false); });
test("the same event id is processed once, across instances", async () => { const eventId = `evt_dup_${Date.now()}`; const body = { type: "event_callback", team_id: "T_KNOWN", event_id: eventId, event: { type: "app_mention", text: "<@UBOT> status?", user: "U1", channel: "C1", channel_type: "channel", ts: `${Date.now()}.62`, }, }; await signAndPost(app, body); await signAndPost(app, body); expect(redisStore.has(`slack:event:${eventId}`)).toBe(true); await new Promise((r) => setTimeout(r, 150)); const thinking = slackTestState.calls.filter( (c) => c.method === "chatStream" || (c.method === "postMessage" && String(c.args.text).includes("Thinking")), ); expect(thinking).toHaveLength(1); });
test("a stop from another instance aborts the running turn", async () => { const turn = startTurn("C_REMOTE", "7.7"); await new Promise((r) => setTimeout(r, 10)); await broadcastStop("C_REMOTE", "7.7"); await new Promise((r) => setTimeout(r, 1_200)); expect(turn.signal.aborted).toBe(true); endTurn("C_REMOTE", "7.7", turn); });
test("an earlier stop does not abort a newer turn", async () => { await broadcastStop("C_OLD", "8.8"); const turn = startTurn("C_OLD", "8.8"); await new Promise((r) => setTimeout(r, 1_200)); expect(turn.signal.aborted).toBe(false); endTurn("C_OLD", "8.8", turn); });
async function seedSlackInstall() { const teamId = `T_REVOKED_${crypto.randomUUID()}`; const { workspace } = await createTestWorkspace(); const row = await db .insert(integration) .values({ name: "slack-agent", workspaceId: workspace.id, externalId: teamId, data: {}, }) .returning() .get(); return { teamId, id: row.id }; }
async function revokeTokens( teamId: string, tokens: Record<string, string[]>, ) { const res = await signAndPost(app, { type: "event_callback", team_id: teamId, event_id: `evt_revoked_${crypto.randomUUID()}`, event: { type: "tokens_revoked", tokens }, }); expect(res.status).toBe(200); await settleBackgroundTasks(); }
function findIntegration(id: number) { return db.select().from(integration).where(eq(integration.id, id)).get(); }
test("tokens_revoked without a bot token leaves the install alone", async () => { const install = await seedSlackInstall(); try { await revokeTokens(install.teamId, { oauth: ["U1"] }); expect(await findIntegration(install.id)).toBeDefined(); } finally { await db.delete(integration).where(eq(integration.id, install.id)); } });
test("tokens_revoked with a bot token uninstalls", async () => { const install = await seedSlackInstall(); try { await revokeTokens(install.teamId, { bot: ["UBOT"] }); expect(await findIntegration(install.id)).toBeUndefined(); } finally { await db.delete(integration).where(eq(integration.id, install.id)); } });});
describe("reconnect banner", () => { const app = createTestApp();
beforeEach(resetSlackTestState);
test("the home tab asks an old install to reconnect", async () => { slackTestState.resolveWorkspace = () => Promise.resolve({ workspace: { id: 1, name: "Test Workspace", slug: "test", plan: "team", limits: { "slack-agent": true }, }, botToken: "xoxb-test", botUserId: "UBOT", scopes: "chat:write,users:read,users:read.email", }); await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_reconnect_${Date.now()}`, event: { type: "app_home_opened", tab: "home", user: "U1" }, }); const publish = await waitForCall("views.publish"); const view = publish?.args.view as { blocks: { type: string; text?: { text: string } }[]; }; expect(view.blocks[0].text?.text).toContain("Reconnect openstatus"); });});
describe("incident channel context", () => { const app = createTestApp();
beforeEach(resetSlackTestState);
test("a mention in a bound channel tells the agent which incident it is", async () => { const channelId = `C_CTX_${crypto.randomUUID()}`; const [row] = await db .insert(incident) .values({ workspaceId: 1, title: "Context incident", severity: "major", declaredAt: new Date(), startedAt: new Date(), slackTeamId: "T_KNOWN", slackChannelId: channelId, }) .returning(); let contextNote: string | undefined; slackTestState.runAgentOverride = (options) => { contextNote = (options as { contextNote?: string } | undefined) ?.contextNote; return Promise.resolve({ text: "ok", toolResults: [], finishReason: "stop", stepCount: 1, hitStepLimit: false, aborted: false, }); }; try { await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_ctx_${Date.now()}`, event: { type: "app_mention", text: "<@UBOT> note that we rolled back", user: "U1", channel: channelId, channel_type: "channel", ts: `${Date.now()}.71`, }, }); const deadline = Date.now() + 2000; while (contextNote === undefined && Date.now() < deadline) { await new Promise((r) => setTimeout(r, 20)); } expect(contextNote).toContain(`id ${row.id}`); expect(contextNote).toContain("Context incident"); } finally { await db.delete(incident).where(eq(incident.id, row.id)); } });});
describe("incident channel events", () => { const app = createTestApp(); let incidentId: number; let channelId: string;
beforeEach(async () => { resetSlackTestState(); slackTestState.reactionsGetImpl = () => Promise.resolve({ ok: true, message: {} }); channelId = `C_PIN_${crypto.randomUUID()}`; const [row] = await db .insert(incident) .values({ workspaceId: 1, title: "Pinned incident", severity: "major", declaredAt: new Date(), startedAt: new Date(), slackTeamId: "T_KNOWN", slackChannelId: channelId, }) .returning(); incidentId = row.id; });
afterEach(async () => { await db .delete(incidentEvent) .where(eq(incidentEvent.incidentId, incidentId)); await db.delete(incident).where(eq(incident.id, incidentId)); });
async function notes() { return db .select() .from(incidentEvent) .where(eq(incidentEvent.incidentId, incidentId)) .all(); }
function pin(ts: string, user = "U1") { return signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_pin_${crypto.randomUUID()}`, event: { type: "reaction_added", user, reaction: "pushpin", item: { type: "message", channel: channelId, ts }, }, }); }
test("a ๐ copies the message onto the timeline and confirms with โ
", async () => { slackTestState.historyImpl = () => Promise.resolve({ messages: [{ ts: "500.1", text: "Rolled back to v41", user: "U2" }], }); await pin("500.1"); await waitForCall("reactions.add"); const rows = await notes(); expect(rows).toHaveLength(1); expect(rows[0].message).toContain("Rolled back to v41"); expect(rows[0].message).toContain("https://slack.test/archives/"); });
test("a message already confirmed is not noted twice", async () => { slackTestState.historyImpl = () => Promise.resolve({ messages: [{ ts: "501.1", text: "Twice" }] }); slackTestState.reactionsGetImpl = () => Promise.resolve({ ok: true, message: { reactions: [{ name: "white_check_mark", users: ["UBOT"] }], }, }); await pin("501.1"); expect(await waitForCall("reactions.get")).toBeDefined(); // The handler returns right after reactions.get; give it a tick to settle. await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.some((c) => c.method === "reactions.add")).toBe( false, ); expect(await notes()).toHaveLength(0); });
test("two ๐ racing on one message note it once", async () => { slackTestState.historyImpl = () => Promise.resolve({ messages: [{ ts: "502.1", text: "Once" }] }); await Promise.all([pin("502.1"), pin("502.1", "U3")]); await waitForCall("reactions.add"); await new Promise((r) => setTimeout(r, 50)); expect(await notes()).toHaveLength(1); });
test("a pinned thread reply is found through the thread", async () => { slackTestState.historyImpl = () => Promise.resolve({ messages: [{ ts: "400.0", text: "parent" }] }); let repliesArgs: Record<string, unknown> | undefined; slackTestState.repliesImpl = (args) => { repliesArgs = args; return Promise.resolve({ messages: [ { ts: "400.0", text: "parent" }, { ts: "400.5", text: "the reply" }, ], }); }; await pin("400.5"); await waitForCall("reactions.add"); const rows = await notes(); expect(rows[0]?.message).toContain("the reply"); // Slack returns the parent first, so the range must start at the reply. expect(repliesArgs).toMatchObject({ oldest: "400.5", latest: "400.5" }); });
test("archiving the channel unbinds the incident", async () => { await signAndPost(app, { type: "event_callback", team_id: "T_KNOWN", event_id: `evt_archive_${crypto.randomUUID()}`, event: { type: "channel_archive", channel: channelId, user: "U1" }, }); const deadline = Date.now() + 2000; let bound: string | null = channelId; while (bound && Date.now() < deadline) { await new Promise((r) => setTimeout(r, 20)); const row = await db .select({ slackChannelId: incident.slackChannelId }) .from(incident) .where(eq(incident.id, incidentId)) .get(); bound = row?.slackChannelId ?? null; } expect(bound).toBeNull(); });});