Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TEST-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Automated regression: `test/questions.test.js`, `test/question-views.test.js`,
`test/question-interactions.test.js`, `test/question-continuation.test.js`, plus the gateway MCP
inventory/approval, folder settings, busy-thread and recovery suites. The four question suites
pass 37 tests using scratch SQLite, fake Slack interactions and fixture engines. They cover
pass 38 tests using scratch SQLite, fake Slack interactions and fixture engines. They cover
fresh-process draft retrieval, atomic submission/rollback, stale-card repair, serialized rendering,
the Slack acknowledgement deadline, requester authorization, queue/restart recovery, and stop/clear.

Expand Down
2 changes: 1 addition & 1 deletion src/slack/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1633,7 +1633,7 @@ async function connectAndWire(app) {
});
for (const a of APPROVAL_ACTIONS) app.action(a, handleApprovalClick);
registerBusyThreadChoiceActions(app, processMessageEvent);
registerQuestionActions(app, processMessageEvent);
registerQuestionActions(app, processMessageEvent, { botUserId, teamId });
registerEngineSwitchChoiceActions(app, processMessageEvent);
// Indexed ids (`cg_model_pick_2`) are the per-choice buttons; the bare id is the retired
// static_select, still clickable in Slack history. One pattern covers both.
Expand Down
11 changes: 7 additions & 4 deletions src/slack/questions.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,11 @@ export async function handleQuestionView({ ack, body, view = body?.view, client
}
}

export function registerQuestionActions(app, processMessage) {
app.action(/^cg_question_/, (payload) => handleQuestionAction(payload, { processMessage }));
app.view(QUESTION_FORM_CALLBACK, (payload) => handleQuestionView(payload, { processMessage }));
app.view(QUESTION_CUSTOM_CALLBACK, (payload) => handleQuestionView(payload, { processMessage }));
export function registerQuestionActions(app, processMessage, context = {}) {
// The live connection owns bot/workspace identity; the synthetic answer must retain it for
// mention hydration and Slack's recipient_team_id on streamed channel replies.
const continuation = (event, client, options) => processMessage(event, client, { ...context, ...options });
app.action(/^cg_question_/, (payload) => handleQuestionAction(payload, { processMessage: continuation }));
app.view(QUESTION_FORM_CALLBACK, (payload) => handleQuestionView(payload, { processMessage: continuation }));
app.view(QUESTION_CUSTOM_CALLBACK, (payload) => handleQuestionView(payload, { processMessage: continuation }));
}
21 changes: 20 additions & 1 deletion test/question-interactions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ ensureTestEnv();
const { setUser, upsertChannelEntry, saveChannelMeta, getChannelMeta } = await import("../src/config/store.js");
const { getDb } = await import("../src/db/index.js");
const { getQuestion, saveQuestionAnswers } = await import("../src/gateway/questions.js");
const { postQuestions, handleQuestionAction, handleQuestionView, refreshQuestionCard } = await import("../src/slack/questions.js");
const { postQuestions, handleQuestionAction, handleQuestionView, refreshQuestionCard, registerQuestionActions } = await import("../src/slack/questions.js");
const { buildQuestionCard, buildQuestionModal, buildCustomAnswerModal } = await import("../src/slack/question-views.js");

const OWNER = "UQUESTION_OWNER";
Expand Down Expand Up @@ -79,6 +79,25 @@ test("posting is idempotent and visible card carries the persisted revision", as
assert.equal(JSON.parse(elements(visible).find((el) => el.action_id === "cg_question_submit").value).revision, again.revision);
});

test("registered submit handlers preserve the live bot and workspace context", async () => {
const f = await fixture();
await f.act("cg_question_choose:access:0");
let handler;
let received;
registerQuestionActions({ action(_pattern, fn) { handler = fn; }, view() {} }, async (_event, _client, options) => {
received = options;
options.onQuestionSubmissionAccepted({ runId: randomUUID(), rec: f.context });
}, { botUserId: "U_BOT_CONTEXT", teamId: "T_WORKSPACE_CONTEXT" });
const record = f.current();
const action = elements(buildQuestionCard(record)).find((el) => el.action_id === "cg_question_submit");
await handler({ ack: async () => {}, action, client: f.client, body: { user: { id: OWNER }, channel: { id: CHANNEL }, message: { ts: record.messageTs } } });
await settle();
assert.equal(received.botUserId, "U_BOT_CONTEXT");
assert.equal(received.teamId, "T_WORKSPACE_CONTEXT");
assert.equal(received.bypassMention, true);
assert.equal(f.current().status, "submitted");
});

test("options and custom save are drafts; duplicate submit creates one durable continuation", async () => {
const f = await fixture();
await f.act("cg_question_choose:access:0");
Expand Down
Loading