diff --git a/tests/runner-e2e/chat-catalog-contract.ts b/tests/runner-e2e/chat-catalog-contract.ts index 171bcc87d38..cc6077cbad4 100644 --- a/tests/runner-e2e/chat-catalog-contract.ts +++ b/tests/runner-e2e/chat-catalog-contract.ts @@ -11,6 +11,8 @@ export interface ChatCatalogContext { issue(): ChatIssue | undefined; allRuns(): Promise; refreshIssue(): Promise; + /** Available only to the two allowlisted native Slack ingress cases. */ + selectSlackSource?(issueId: string): Promise; } export type ChatCatalogTerminalRunStatus = "succeeded" | "cancelled" | "failed" | "timed_out"; export type ChatCatalogRunPolicy = @@ -37,6 +39,11 @@ export interface ChatCompanionResult { messages: Array<{ role: "system" | "user" | "assistant" | "tool"; content: string }>; providerResponseIds: string[]; toolCalls: Array<{ name: string; arguments: unknown; result: unknown }>; links: string[]; } +export interface ChatSlackIngress { kind: "native-slack-thread"; maximumProviderCalls: 20; ingressTimeoutMs: 120000 } +export interface ChatSlackIngressReceipt { + query: string; endpointId: string; conversationId: string; issueId: string; humanCommentId: string; linkedPaperclipUserId: string; providerCalls: number; + provider: { workspaceId: string; channelId: string; threadId: string; canonicalMessageId: string; senderUserId: string }; +} /** One concrete scenario, with production API setup and a case-specific independent oracle. */ export interface ChatCatalogCase { scenarioId: string; @@ -46,7 +53,9 @@ export interface ChatCatalogCase { requiredCredentials?: readonly CredentialName[]; externalInput?: ChatExternalInputRequirement; installedSource?: "published-global-npm"; - journeyKind?: "agent-chat" | "scoped-external-assistant"; + journeyKind?: "agent-chat" | "scoped-external-assistant" | "native-slack-thread"; + slackIngress?: ChatSlackIngress; + dispatchSlackQuery?(context: ChatCatalogContext, state: Record, input: { query: string; signal: AbortSignal }): Promise; companionSurface?: ChatCompanionSurface; runCompanionSurface?(context: ChatCatalogContext, state: Record, input: { query: string; signal: AbortSignal }): Promise; authentication?: "local-trusted" | "human-session"; diff --git a/tests/runner-e2e/chat-catalog-flow.ts b/tests/runner-e2e/chat-catalog-flow.ts index e6249f535d5..c312b69be8f 100644 --- a/tests/runner-e2e/chat-catalog-flow.ts +++ b/tests/runner-e2e/chat-catalog-flow.ts @@ -1,3 +1,4 @@ +import { dispatchNativeSlackIngress, slackNativeRunCheck } from "./chat-slack-ingress.js"; import { runScopedCompanionSurface } from "./chat-companion-surface.js"; import { chatQueryWithAttachments, resolveChatQueryAttachments, verifyChatQueryAttachments, type ChatQueryAttachmentReceipt } from "./chat-query-attachments.js"; import { chatCatalogAllowedRunStatuses, gradeChatCatalogRuns } from "./chat-catalog-run-policy.js"; @@ -18,6 +19,7 @@ export async function runChatCatalogFlow(context: ChatCatalogContext) { const fingerprint = chatCatalogCaseFingerprint(descriptor, normalizedChatCatalogExecution(input.execution)); const implementation = await loadChatCatalogImplementation(descriptor); if (implementation.companionSurface) return runScopedCompanionSurface(context, implementation, fingerprint); + const slack = implementation.journeyKind === "native-slack-thread"; const deadlineAt = Date.now() + implementation.attemptTimeoutMs - 30_000; let state: Record | undefined; let observation: unknown; @@ -60,6 +62,7 @@ export async function runChatCatalogFlow(context: ChatCatalogContext) { detail: "The persisted request preserves the catalog query with only audited public-fixture identity bindings; all human turns were declared." }, { id: "catalog-query-attachment-receipts", passed: attachmentsValid, detail: "Only declared production attachment suffixes are allowed, backed by same-source human receipts and independently retrieved matching bytes." }, + ...(slack ? [slackNativeRunCheck(context, evidence.runs)] : []), ...gradeChatCatalogRuns({ implementation: implementation!, companyId: input.fixtures.company.id, leadId: input.fixtures.agent.id, source: evidence.issue, runs: evidence.runs }).checks, ]; @@ -70,7 +73,7 @@ export async function runChatCatalogFlow(context: ChatCatalogContext) { } try { await input.evidence("chat-catalog-definition.json", { scenarioId: scenario.id, ...fingerprint, - suiteDefinitionHash: input.execution.suiteDefinitionHash, journeyKind: "agent-chat" }); + suiteDefinitionHash: input.execution.suiteDefinitionHash, journeyKind: implementation.journeyKind ?? "agent-chat" }); state = await implementation.setup(context); const query = await resolveChatCatalogQuery(context, implementation.queryBindings, state); renderedQuery = query.renderedQuery; @@ -113,8 +116,12 @@ export async function runChatCatalogFlow(context: ChatCatalogContext) { setupBoundary = { humanCommentIds: setupHuman.map(comment => comment.id), runIds: setupConversation.runs.map(run => run.id) }; await input.evidence("chat-catalog-setup-boundary.json", { ...setupBoundary, setupHumanMessageCount: implementation.setupHumanMessageCount ?? 0, expectedHumanMessageCount: implementation.expectedHumanMessageCount ?? 1, expectedRunCount: implementation.expectedRunCount }); - await input.evidence("chat-catalog-query.json", { query: scenario.query, expectedOutcome: implementation.queryOutcome ?? "persisted", journeyKind: "agent-chat" }); - await sendChatMessage(input.page, renderedQuery); + await input.evidence("chat-catalog-query.json", { query: scenario.query, expectedOutcome: implementation.queryOutcome ?? "persisted", journeyKind: implementation.journeyKind ?? "agent-chat" }); + if (slack) { + if (setupBoundary.runIds.length || setupBoundary.humanCommentIds.length) throw new Error("Native Slack setup cannot admit canonical or unrelated work"); + state.slackIngressReceipt = await dispatchNativeSlackIngress(context, implementation, state, deadlineAt); + await input.page.goto(new URL(`/${input.fixtures.company.issuePrefix}/issues/${context.issue()!.id}`, input.api.baseURL).href); + } else await sendChatMessage(input.page, renderedQuery); if (implementation.queryOutcome === "rejected") { const rejection = validateChatCatalogGrade(await implementation.verifyQueryRejection!(context, state)); await input.evidence("chat-catalog-query-rejection.json", rejection); @@ -140,7 +147,7 @@ export async function runChatCatalogFlow(context: ChatCatalogContext) { }); const originalIssueId = context.issue()?.id; await input.page.reload({ waitUntil: "domcontentloaded" }); - await expect(input.page.getByTestId("task-chat-composer-input")).toBeVisible(); + if (!slack) await expect(input.page.getByTestId("task-chat-composer-input")).toBeVisible(); const persisted = await refresh(); expect(context.issue()?.id, "Source chat survives browser reload").toBe(originalIssueId); expect(persisted.grade.passed, "Independent oracle survives browser reload").toBe(true); diff --git a/tests/runner-e2e/chat-catalog-registry.ts b/tests/runner-e2e/chat-catalog-registry.ts index 54e5c3c3c74..fdee905712e 100644 --- a/tests/runner-e2e/chat-catalog-registry.ts +++ b/tests/runner-e2e/chat-catalog-registry.ts @@ -1,3 +1,4 @@ +import { validSlackIngressDeclaration } from "./chat-slack-ingress.js"; import { validCompanionDeclaration } from "./chat-companion-surface.js"; import { installedChatScenarioIds } from "./chat-installed-lane.js"; import { existsSync, readFileSync } from "node:fs"; @@ -7,8 +8,8 @@ import { chatCatalogScenarios } from "./chat-catalog-inventory.js"; import type { IdentityBoundaryCase } from "./chat-catalog-domains/identity-boundary-contract.js"; import { identityBoundaryScenarioIds } from "./chat-catalog-domains/identity-boundary-contract.js"; -export type ChatCatalogDescriptor = Omit & { - journeyKind?: "agent-chat" | "browser-boundary" | "scoped-external-assistant"; +export type ChatCatalogDescriptor = Omit & { + journeyKind?: "agent-chat" | "browser-boundary" | "scoped-external-assistant" | "native-slack-thread"; boundaryTarget?: "authenticated-harness" | "empty-installed-target"; modulePath: string; exportName: string; @@ -24,6 +25,7 @@ export function findChatCatalogCase(scenarioId: string): ChatCatalogDescriptor | const descriptor = matches[0]; if (!descriptor) return; if ((descriptor.companionSurface || descriptor.journeyKind === "scoped-external-assistant") && !validCompanionDeclaration(descriptor)) throw new Error("Only the declared scoped-client catalog case may select an external assistant"); + if ((descriptor.slackIngress || descriptor.journeyKind === "native-slack-thread") && !validSlackIngressDeclaration(descriptor)) throw new Error("Only two declared Slack thread cases may use native ingress"); if (descriptor.browserTimezone && (scenarioId !== "routines.create-daily-brief" || descriptor.browserTimezone !== "America/Chicago")) throw new Error("Browser timezone is declared only for the canonical Chicago weekday routine"); if (descriptor.respondingAgent && (scenarioId !== "cases.handoff-same-task" || Object.keys(descriptor.respondingAgent).length !== 4 @@ -55,10 +57,12 @@ export async function loadChatCatalogImplementation( const selected = implementations.filter((row: ChatCatalogCase) => row.scenarioId === descriptor.scenarioId); if (selected.length !== 1) throw new Error(`Missing or duplicate runtime case: ${descriptor.scenarioId}`); const implementation = selected[0] as ChatCatalogCase; - for (const key of ["sourceFiles", "oracleVersion", "expectedRunCount", "minimumRunCount", "expectedHumanMessageCount", "setupHumanMessageCount", "turnTimeoutMs", "attemptTimeoutMs", "coverageGaps", "journeyKind", "authentication", "setupPolicy", "runPolicy", "queryOutcome", "queryBindings", "queryAttachments", "requiredCredentials", "externalInput", "installedSource", "browserTimezone", "respondingAgent", "companionSurface"] as const) { + for (const key of ["sourceFiles", "oracleVersion", "expectedRunCount", "minimumRunCount", "expectedHumanMessageCount", "setupHumanMessageCount", "turnTimeoutMs", "attemptTimeoutMs", "coverageGaps", "journeyKind", "authentication", "setupPolicy", "runPolicy", "queryOutcome", "queryBindings", "queryAttachments", "requiredCredentials", "externalInput", "installedSource", "browserTimezone", "respondingAgent", "companionSurface", "slackIngress"] as const) { if (JSON.stringify(implementation[key]) !== JSON.stringify(descriptor[key])) throw new Error(`Runtime descriptor drift: ${descriptor.scenarioId}.${key}`); } if ((descriptor.companionSurface || descriptor.journeyKind === "scoped-external-assistant") && (!validCompanionDeclaration(implementation) || typeof implementation.runCompanionSurface !== "function" || implementation.act)) throw new Error("Missing actual finite scoped assistant driver"); + if ((descriptor.slackIngress || descriptor.journeyKind === "native-slack-thread") && (!validSlackIngressDeclaration(implementation) || typeof implementation.dispatchSlackQuery !== "function" || implementation.act)) throw new Error("Missing finite actual Slack ingress driver"); + if (!descriptor.slackIngress && implementation.dispatchSlackQuery) throw new Error("Undeclared Slack ingress driver"); if (!descriptor.companionSurface && implementation.runCompanionSurface) throw new Error("Undeclared external assistant driver"); if ((descriptor.setupPolicy?.funding === "preserve-scenario" || descriptor.setupPolicy?.readiness === "preserve-scenario") && (!descriptor.setupPolicy.invariantCheckIds.length || typeof implementation.verifySetup !== "function")) diff --git a/tests/runner-e2e/chat-feature-flow.ts b/tests/runner-e2e/chat-feature-flow.ts index 97ed066baeb..1de906a1f32 100644 --- a/tests/runner-e2e/chat-feature-flow.ts +++ b/tests/runner-e2e/chat-feature-flow.ts @@ -10,7 +10,7 @@ import { CHAT_FEATURE_BUDGET_CENTS, CHAT_FEATURE_GRADER, chatFeatureScenarios, t import { gradeChatFeature, linksFeatureTask, type ChatFeatureObservation, type FeatureDocument, type FeatureTask } from "./chat-feature-scoring.js"; export async function runChatFeatureFlow(context: { - input: ChatFlowInput; issue(): ChatIssue | undefined; allRuns(): Promise; refreshIssue(): Promise; + input: ChatFlowInput; issue(): ChatIssue | undefined; allRuns(): Promise; refreshIssue(): Promise; selectSlackSource?(issueId: string): Promise; }) { const { input } = context; const { api, page, fixtures: f, execution, nonce } = input; diff --git a/tests/runner-e2e/chat-flow.ts b/tests/runner-e2e/chat-flow.ts index 1c8eca429d2..e66e192fde0 100644 --- a/tests/runner-e2e/chat-flow.ts +++ b/tests/runner-e2e/chat-flow.ts @@ -399,6 +399,19 @@ export async function runChatFlow(input: ChatFlowInput) { rows.map((row) => api.get(`/api/heartbeat-runs/${row.id}`)), ); }; + if (catalogDescriptor?.journeyKind === "native-slack-thread") { + let sourceId: string | undefined; + await runChatFeatureFlow({ input, issue: () => issue, allRuns, + refreshIssue: async () => { if (sourceId) issue = await api.get(`/api/issues/${sourceId}`); }, + selectSlackSource: async id => { + if (sourceId && sourceId !== id) throw new Error("Native Slack source cannot change after canonical ingress"); + const selected = await api.get(`/api/issues/${id}`); + if (selected.companyId !== f.company.id) throw new Error("Native Slack source escaped the owned company"); + sourceId = id; issue = selected; input.observe(issue, await allRuns()); + } }); + if (!sourceId) throw new Error("Native Slack ingress did not establish its real source"); + return { issue: await api.get(`/api/issues/${sourceId}`), runs: await allRuns() }; + } if (catalogDescriptor?.journeyKind === "scoped-external-assistant") { await runChatFeatureFlow({ input, issue: () => issue, allRuns, refreshIssue: async () => { issue = await api.get(chatPath); } }); diff --git a/tests/runner-e2e/chat-slack-ingress.test.ts b/tests/runner-e2e/chat-slack-ingress.test.ts new file mode 100644 index 00000000000..df3d94bac38 --- /dev/null +++ b/tests/runner-e2e/chat-slack-ingress.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { loadChatCatalogImplementation, type ChatCatalogDescriptor } from "./chat-catalog-registry.js"; +import { slackIngressReceiptMatches, validSlackIngressDeclaration, slackNativeRunCheck } from "./chat-slack-ingress.js"; +import type { ChatCatalogContext, ChatSlackIngressReceipt } from "./chat-catalog-contract.js"; +import type { ChatConversation, ChatEndpoint, ChatIdentityLink, ExternalChannelBindingSummary } from "../../packages/shared/src/types/chat-channels.js"; +import type { IssueComment } from "../../packages/shared/src/types/issue.js"; +const query = "What happened in this thread?"; +const metadata = { scenarioId: "chat-channels.reply-in-thread", journeyKind: "native-slack-thread", slackIngress: { kind: "native-slack-thread" as const, maximumProviderCalls: 20 as const, ingressTimeoutMs: 120000 as const }, expectedRunCount: 3, minimumRunCount: 1, expectedHumanMessageCount: 1, turnTimeoutMs: 120000, attemptTimeoutMs: 600000 }; +function input() { + const receipt: ChatSlackIngressReceipt = { query, endpointId: "endpoint", conversationId: "conversation", issueId: "issue", humanCommentId: "comment", linkedPaperclipUserId: "person", providerCalls: 2, provider: { workspaceId: "T123", channelId: "C123", threadId: "1234567890.123456", canonicalMessageId: "1234567891.123456", senderUserId: "U123" } }; + return { receipt, query, companyId: "company", agentId: "agent", endpoint: { id: "endpoint", companyId: "company", assignedAgentId: "agent", provider: "slack", providerAccountId: "T123" } as ChatEndpoint, + conversation: { id: "conversation", companyId: "company", endpointId: "endpoint", issueId: "issue", externalConversationId: "slack:C123", externalThreadId: "slack:C123:1234567890.123456" } as ChatConversation, + binding: { endpointId: "endpoint", conversationId: "conversation", provider: "slack", assignedAgentLocked: true } as ExternalChannelBindingSummary, + comment: { id: "comment", companyId: "company", issueId: "issue", authorAgentId: null, authorUserId: "person", body: query, metadata: { version: 1, sourceChannel: "slack", sections: [{ title: "Slack sender", rows: [{ type: "key_value", label: "Provider ID", value: "U123" }] }] } } as IssueComment, + links: [{ companyId: "company", endpointId: "endpoint", paperclipUserId: "person", status: "linked" }] as ChatIdentityLink[] }; +} +describe("only real declared Slack canonical ingress", () => { + it("rejects undeclared surfaces, unbounded lifecycle and hidden extra humans", () => { + expect(validSlackIngressDeclaration(metadata)).toBe(true); + expect(validSlackIngressDeclaration({ ...metadata, scenarioId: "tasks.create-backlog" })).toBe(false); + expect(validSlackIngressDeclaration({ ...metadata, journeyKind: "agent-chat" })).toBe(false); + expect(validSlackIngressDeclaration({ ...metadata, minimumRunCount: 0 })).toBe(false); + expect(validSlackIngressDeclaration({ ...metadata, expectedHumanMessageCount: 2 })).toBe(false); + expect(validSlackIngressDeclaration({ ...metadata, queryBindings: [{ catalogToken: "token", stateKey: "issue", resourceKind: "issue", valueField: "identifier" }] })).toBe(false); + }); + it("cross-checks actual exact canonical comment, linked human, endpoint, company and provider thread DTOs", () => { + expect(slackIngressReceiptMatches(input())).toBe(true); + for (const mutate of [ + (v: ReturnType) => { v.comment.body += " Try harder"; }, + (v: ReturnType) => { v.comment.authorAgentId = "agent"; }, + (v: ReturnType) => { v.receipt.provider.senderUserId = "U999"; }, + (v: ReturnType) => { v.links[0]!.status = "revoked"; }, + (v: ReturnType) => { v.conversation.externalThreadId = "slack:C123:9999999999.123456"; }, + (v: ReturnType) => { v.conversation.externalConversationId = "C123"; }, + (v: ReturnType) => { v.endpoint.companyId = "foreign"; }, + (v: ReturnType) => { v.binding.conversationId = "another"; }, + (v: ReturnType) => { v.receipt.providerCalls = 21; }, + ]) { const value = input(); mutate(value); expect(slackIngressReceiptMatches(value)).toBe(false); } + }); + it("requires the selected actual provider driver and rejects hidden scripted followups", async () => { + const descriptor: ChatCatalogDescriptor = { ...metadata, journeyKind: "native-slack-thread", modulePath: "./chat-catalog-domains/external-slack.js", exportName: "slackCases", sourceFiles: ["chat-slack-ingress.ts"], oracleVersion: "controls", turnTimeoutMs: 120000, attemptTimeoutMs: 600000 }; + const implementation = { ...descriptor, setup: async () => ({}), observe: async () => ({}), grade: () => ({ passed: false, checks: [{ id: "unobserved", passed: false, detail: "Not an observed outcome" }] }), verifyResult: async () => {} }; + await expect(loadChatCatalogImplementation(descriptor, async () => ({ slackCases: [implementation] }))).rejects.toThrow("actual Slack ingress"); + const actual = { ...implementation, dispatchSlackQuery: async () => input().receipt }; + await expect(loadChatCatalogImplementation(descriptor, async () => ({ slackCases: [actual] }))).resolves.toBe(actual); + await expect(loadChatCatalogImplementation(descriptor, async () => ({ slackCases: [{ ...actual, act: async () => {} }] }))).rejects.toThrow("actual Slack ingress"); + }); + it("cannot qualify a fast response, wrong agent or unrelated native run as the source execution", () => { + const ctx = { issue: () => ({ id: "issue", companyId: "company", assigneeAgentId: "agent" }), input: { fixtures: { company: { id: "company" }, agent: { id: "agent" } } } } as unknown as ChatCatalogContext; + const run = { id: "run", companyId: "company", agentId: "agent", runtimeMode: "native", status: "succeeded", contextSnapshot: { issueId: "issue" } }; + expect(slackNativeRunCheck(ctx, [run]).passed).toBe(true); + expect(slackNativeRunCheck(ctx, []).passed).toBe(false); + expect(slackNativeRunCheck(ctx, [{ ...run, agentId: "other" }]).passed).toBe(false); + expect(slackNativeRunCheck(ctx, [{ ...run, contextSnapshot: { issueId: "other" } }]).passed).toBe(false); + }); +}); diff --git a/tests/runner-e2e/chat-slack-ingress.ts b/tests/runner-e2e/chat-slack-ingress.ts new file mode 100644 index 00000000000..61c244ec16c --- /dev/null +++ b/tests/runner-e2e/chat-slack-ingress.ts @@ -0,0 +1,63 @@ +import type { ChatConversation, ChatEndpoint, ChatIdentityLink, ExternalChannelBindingSummary } from "../../packages/shared/src/types/chat-channels.js"; +import type { IssueComment } from "../../packages/shared/src/types/issue.js"; +import type { ChatCatalogCase, ChatCatalogContext, ChatSlackIngressReceipt } from "./chat-catalog-contract.js"; +import type { ChatRun } from "./chat-flow.js"; +export const nativeSlackScenarioIds = ["chat-channels.reply-in-thread", "chat-channels.channel-file"] as const; +export function validSlackIngressDeclaration(c: Pick & { journeyKind?: string }) { + return (nativeSlackScenarioIds as readonly string[]).includes(c.scenarioId) && c.journeyKind === "native-slack-thread" + && c.slackIngress?.kind === "native-slack-thread" && c.slackIngress.maximumProviderCalls === 20 && c.slackIngress.ingressTimeoutMs === 120000 + && Object.keys(c.slackIngress).sort().join(",") === "ingressTimeoutMs,kind,maximumProviderCalls" + && c.turnTimeoutMs === 120000 && c.attemptTimeoutMs === 600000 + && c.expectedRunCount === 3 && c.minimumRunCount === 1 && c.expectedHumanMessageCount === 1 && (c.setupHumanMessageCount ?? 0) === 0 + && !c.queryBindings?.length && !c.queryAttachments?.length && !c.queryOutcome && !c.companionSurface && !c.runPolicy; +} +export function slackIngressReceiptMatches(input: { receipt: ChatSlackIngressReceipt; query: string; companyId: string; agentId: string; + endpoint: ChatEndpoint; conversation: ChatConversation | undefined; binding: ExternalChannelBindingSummary; comment: IssueComment | undefined; links: ChatIdentityLink[] }) { + const { receipt: r, endpoint: e, conversation: c, binding: b, comment } = input; + return !!r && r.query === input.query && Number.isSafeInteger(r.providerCalls) && r.providerCalls >= 1 && r.providerCalls <= 20 + && /^T[A-Z0-9]+$/.test(r.provider.workspaceId) && /^[CG][A-Z0-9]+$/.test(r.provider.channelId) && /^[UW][A-Z0-9]+$/.test(r.provider.senderUserId) + && /^\d+\.\d{6}$/.test(r.provider.threadId) && /^\d+\.\d{6}$/.test(r.provider.canonicalMessageId) + && e.id === r.endpointId && e.companyId === input.companyId && e.assignedAgentId === input.agentId && e.provider === "slack" && e.providerAccountId === r.provider.workspaceId + && !!c && c.id === r.conversationId && c.endpointId === e.id && c.companyId === input.companyId && c.issueId === r.issueId + && c.externalThreadId === `slack:${r.provider.channelId}:${r.provider.threadId}` && c.externalConversationId === `slack:${r.provider.channelId}` + && b.provider === "slack" && b.endpointId === e.id && b.conversationId === c.id && b.assignedAgentLocked === true + && !!comment && comment.id === r.humanCommentId && comment.issueId === r.issueId && comment.companyId === input.companyId + && comment.body === input.query && !comment.authorAgentId && comment.authorUserId === r.linkedPaperclipUserId && comment.metadata?.sourceChannel === "slack" + && Array.isArray(comment.metadata.sections) && comment.metadata.sections.some(section => section.rows.some(row => row.type === "key_value" && row.label === "Provider ID" && row.value === r.provider.senderUserId)) + && input.links.some(link => link.companyId === input.companyId && link.endpointId === e.id && link.status === "linked" && link.paperclipUserId === r.linkedPaperclipUserId); +} +/** Provider transport belongs to the selected domain; public product identity + * and exact canonical persistence are independently checked here. */ +export async function dispatchNativeSlackIngress(ctx: ChatCatalogContext, c: ChatCatalogCase, state: Record, deadlineAt: number) { + if (!validSlackIngressDeclaration(c) || !c.dispatchSlackQuery || c.act || !ctx.selectSlackSource) throw new Error("Invalid actual native Slack ingress contract"); + const remaining = Math.min(c.slackIngress!.ingressTimeoutMs, deadlineAt - Date.now()); + if (remaining <= 0) throw new Error("Slack setup exhausted the actual attempt deadline"); + const signal = AbortSignal.timeout(remaining); let listener: (() => void) | undefined; + const interrupted = new Promise((_resolve, reject) => { listener = () => reject(new Error("Actual Slack ingress exceeded its declared deadline")); signal.addEventListener("abort", listener, { once: true }); }); + let receipt: ChatSlackIngressReceipt; + try { receipt = await Promise.race([c.dispatchSlackQuery(ctx, state, { query: ctx.scenario.query, signal }), interrupted]); } + finally { if (listener) signal.removeEventListener("abort", listener); } + // Fail before interpreting identifiers as API routes. + for (const id of [receipt?.endpointId, receipt?.conversationId, receipt?.issueId, receipt?.humanCommentId]) if (typeof id !== "string" || !/^[a-zA-Z0-9-]{1,100}$/.test(id)) throw new Error("Invalid Slack product receipt identity"); + const api = ctx.input.api; + const endpoint = await api.get(`/api/chat-endpoints/${receipt.endpointId}`); + const conversations = await api.get(`/api/chat-endpoints/${receipt.endpointId}/conversations`); + const binding = await api.get(`/api/issues/${receipt.issueId}/chat-binding`); + const comments = await api.get(`/api/issues/${receipt.issueId}/comments?order=asc`); + const links = await api.get(`/api/chat-endpoints/${receipt.endpointId}/principals`); + const matched = slackIngressReceiptMatches({ receipt, query: ctx.scenario.query, companyId: ctx.input.fixtures.company.id, agentId: ctx.input.fixtures.agent.id, + endpoint, conversation: conversations.find(row => row.id === receipt.conversationId), binding, comment: comments.find(row => row.id === receipt.humanCommentId), links }); + await ctx.input.evidence("chat-slack-ingress-binding.json", { matched, canonicalQuery: ctx.scenario.query, receipt, assignedAgentId: endpoint.assignedAgentId, + linkedIdentityConfirmed: matched, privateProviderConfiguration: "withheld", providerSenderMapping: "independent case oracle required" }); + if (signal.aborted || !matched) throw new Error("Slack ingress did not independently bind exact canonical human, linked identity, company and provider thread"); + await ctx.selectSlackSource(receipt.issueId); + return receipt; +} +export function slackNativeRunCheck(ctx: ChatCatalogContext, runs: ChatRun[]) { + const source = ctx.issue(); + const sourceRuns = runs.filter(run => run.contextSnapshot?.issueId === source?.id); + return { id: "catalog-real-slack-source-native", passed: !!source && source.companyId === ctx.input.fixtures.company.id && source.assigneeAgentId === ctx.input.fixtures.agent.id + && sourceRuns.length >= 1 && sourceRuns.every(run => run.agentId === ctx.input.fixtures.agent.id && run.runtimeMode === "native" && run.status === "succeeded") + && runs.every(run => typeof run.contextSnapshot?.issueId === "string"), + detail: "The actual inbound source has a genuine assigned-agent native admission; fast responses cannot substitute. The independent case oracle binds every downstream task and automatic source follow-up to this provider exchange." }; +}