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
at incident-api
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476import crypto from "node:crypto";
import { db, eq } from "@openstatus/db";import { incident, incidentEvent, integration, slackUser, user, usersToWorkspaces,} from "@openstatus/db/src/schema";import { addUserToWorkspace, createTestWorkspace, createUser,} from "@openstatus/db/src/test/factories";import { afterEach, beforeEach, describe, expect, test,} from "@openstatus/test-utils";import { Hono } from "hono";
import { settleBackgroundTasks } from "@/libs/background";// 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 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, });}
// 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 new Promise((r) => setTimeout(r, 50));
slackTestState.calls = []; await signAndPost(app, body); await new Promise((r) => setTimeout(r, 50));
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: {} }); slackTestState.conversationsInfoImpl = () => Promise.resolve({ ok: true, channel: {} }); 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("rich_text wins over mrkdwn: emoji, styles, mentions and channels", async () => { // U1 is the pinner and must stay linked; U9 has no email, so the // profile name is the only thing we know about them. slackTestState.usersInfoImpl = (args) => Promise.resolve( args.user === "U9" ? { ok: true, user: { real_name: "Jane Doe", profile: {} } } : { ok: true, user: { profile: { email: "ping@openstatus.dev" } } }, ); slackTestState.conversationsInfoImpl = () => Promise.resolve({ ok: true, channel: { name: "inc-db" } }); slackTestState.historyImpl = () => Promise.resolve({ messages: [ { ts: "503.1", user: "U2", text: "*rolled back* :face_holding_back_tears: <@U9> see <#C7|inc-db>", blocks: [ { type: "rich_text", elements: [ { type: "rich_text_section", elements: [ { type: "text", text: "rolled back", style: { bold: true }, }, { type: "text", text: " " }, { type: "emoji", name: "face_holding_back_tears", unicode: "1f979", }, { type: "text", text: " " }, { type: "user", user_id: "U9" }, { type: "text", text: " see " }, { type: "channel", channel_id: "C7" }, ], }, ], }, ], }, ], }); await pin("503.1"); await waitForCall("reactions.add"); const rows = await notes(); expect(rows).toHaveLength(1); expect(rows[0].message).toContain( "**rolled back** ๐ฅน @Jane Doe see #inc-db", ); expect(rows[0].message).not.toContain(":face_holding_back_tears:"); });
test("a mention whose lookup fails keeps the raw id", async () => { slackTestState.usersInfoImpl = (args) => args.user === "U9" ? Promise.reject(new Error("user_not_found")) : Promise.resolve({ ok: true, user: { profile: { email: "ping@openstatus.dev" } }, }); slackTestState.historyImpl = () => Promise.resolve({ messages: [ { ts: "504.1", text: "<@U9> ack", blocks: [ { type: "rich_text", elements: [ { type: "rich_text_section", elements: [ { type: "user", user_id: "U9" }, { type: "text", text: " ack" }, ], }, ], }, ], }, ], }); await pin("504.1"); await waitForCall("reactions.add"); const rows = await notes(); expect(rows).toHaveLength(1); expect(rows[0].message).toContain("@U9 ack"); });
test("the note keeps the time the message was said", async () => { const saidAt = Math.floor(Date.now() / 1000) - 3600; const ts = `${saidAt}.000100`; slackTestState.historyImpl = () => Promise.resolve({ messages: [{ ts, text: "an hour ago" }] }); await pin(ts); await waitForCall("reactions.add"); const rows = await notes(); expect(rows).toHaveLength(1); expect(Math.abs(rows[0].createdAt.getTime() - saidAt * 1000)).toBeLessThan( 1000, ); });
test("the note belongs to the author, not the pinner", async () => { const author = await createUser(); await addUserToWorkspace(author.id, 1, "member"); const authorSlackId = `U_AUTHOR_${crypto.randomUUID()}`; slackTestState.usersInfoImpl = (args) => Promise.resolve({ ok: true, user: { profile: { email: args.user === authorSlackId ? author.email : "ping@openstatus.dev", }, }, }); slackTestState.historyImpl = () => Promise.resolve({ messages: [ { ts: "508.1", text: "I rolled it back", user: authorSlackId }, ], }); try { await pin("508.1"); await waitForCall("reactions.add"); const rows = await notes(); expect(rows).toHaveLength(1); expect(rows[0].createdBy).toBe(author.id); } finally { // The note references the author; drop it before the user. await db .delete(incidentEvent) .where(eq(incidentEvent.incidentId, incidentId)); await db .delete(slackUser) .where(eq(slackUser.slackUserId, authorSlackId)); await db .delete(usersToWorkspaces) .where(eq(usersToWorkspaces.userId, author.id)); await db.delete(user).where(eq(user.id, author.id)); } });
test("a bot message is attributed to the pinner without a lookup", async () => { // Apps with a bot user carry both `bot_id` and `user`. slackTestState.historyImpl = () => Promise.resolve({ messages: [ { ts: "509.1", text: "[FIRING] api 5xx", bot_id: "B1", user: "U_BOT", }, ], }); await pin("509.1"); await waitForCall("reactions.add"); const rows = await notes(); expect(rows).toHaveLength(1); const [pinner] = await db .select({ id: user.id }) .from(user) .where(eq(user.email, "ping@openstatus.dev")); expect(rows[0].createdBy).toBe(pinner.id); expect( slackTestState.calls.some( (m) => m.method === "users.info" && m.args.user === "U_BOT", ), ).toBe(false); });
test("a failed author lookup releases the pin for a retry", async () => { const author = await createUser(); await addUserToWorkspace(author.id, 1, "member"); const authorSlackId = `U_AUTHOR_${crypto.randomUUID()}`; let flaky = true; slackTestState.usersInfoImpl = (args) => { if (args.user !== authorSlackId) { return Promise.resolve({ ok: true, user: { profile: { email: "ping@openstatus.dev" } }, }); } if (flaky) { const err = new Error("An API error occurred: ratelimited"); Object.assign(err, { data: { ok: false, error: "ratelimited" } }); return Promise.reject(err); } return Promise.resolve({ ok: true, user: { profile: { email: author.email } }, }); }; slackTestState.historyImpl = () => Promise.resolve({ messages: [{ ts: "510.1", text: "flaky lookup", user: authorSlackId }], }); try { await pin("510.1"); await settleBackgroundTasks(); expect(await notes()).toHaveLength(0); expect(await waitForCall("reactions.add", 100)).toBeUndefined(); const notice = await waitForCall("postEphemeral"); expect(notice?.args.text).toContain("Pin it again");
// A second failure stays quiet; the pinner was already told. await pin("510.1"); await settleBackgroundTasks(); expect( slackTestState.calls.filter((c) => c.method === "postEphemeral"), ).toHaveLength(1);
flaky = false; await pin("510.1"); await waitForCall("reactions.add"); const rows = await notes(); expect(rows).toHaveLength(1); expect(rows[0].createdBy).toBe(author.id); } finally { await db .delete(incidentEvent) .where(eq(incidentEvent.incidentId, incidentId)); await db .delete(slackUser) .where(eq(slackUser.slackUserId, authorSlackId)); await db .delete(usersToWorkspaces) .where(eq(usersToWorkspaces.userId, author.id)); await db.delete(user).where(eq(user.id, author.id)); } });
test("a message with only attachments is noted from their fallback", async () => { slackTestState.historyImpl = () => Promise.resolve({ messages: [ { ts: "505.1", text: "", attachments: [{ fallback: "[FIRING] db latency > 2s" }], }, ], }); await pin("505.1"); await waitForCall("reactions.add"); const rows = await notes(); expect(rows).toHaveLength(1); expect(rows[0].message).toContain("[FIRING] db latency > 2s"); });
test("an attachment with an empty fallback is noted from its text", async () => { slackTestState.historyImpl = () => Promise.resolve({ messages: [ { ts: "507.1", text: "", attachments: [{ fallback: "", text: "Deploy 1234 failed" }], }, ], }); await pin("507.1"); await waitForCall("reactions.add"); const rows = await notes(); expect(rows).toHaveLength(1); expect(rows[0].message).toContain("Deploy 1234 failed"); });
test("a message with nothing to copy tells the pinner", async () => { slackTestState.historyImpl = () => Promise.resolve({ messages: [{ ts: "506.1", text: "" }] }); await pin("506.1"); const ephemeral = await waitForCall("postEphemeral"); expect(ephemeral?.args.text).toContain("Nothing to copy"); await new Promise((r) => setTimeout(r, 50)); expect(slackTestState.calls.some((c) => c.method === "reactions.add")).toBe( false, ); expect(await notes()).toHaveLength(0); });
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(); });});