mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-05 03:41:32 +01:00
test(evals): bind canonical Slack ingress to actual native source
This commit is contained in:
1 parent
c2b8cb697a
commit
9c1e30e2c9
7 files changed
+161
-9
No files matched your search
@@ -11,6 +11,8 @@ export interface ChatCatalogContext {
|
||||
issue(): ChatIssue | undefined;
|
||||
allRuns(): Promise<ChatRun[]>;
|
||||
refreshIssue(): Promise<void>;
|
||||
/** Available only to the two allowlisted native Slack ingress cases. */
|
||||
selectSlackSource?(issueId: string): Promise<void>;
|
||||
}
|
||||
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<string, unknown>, input: { query: string; signal: AbortSignal }): Promise<ChatSlackIngressReceipt>;
|
||||
companionSurface?: ChatCompanionSurface;
|
||||
runCompanionSurface?(context: ChatCatalogContext, state: Record<string, unknown>, input: { query: string; signal: AbortSignal }): Promise<ChatCompanionResult>;
|
||||
authentication?: "local-trusted" | "human-session";
|
||||
|
||||
@@ -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<string, unknown> | 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);
|
||||
|
||||
@@ -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<ChatCatalogCase, "runCompanionSurface" | "journeyKind" | "setup" | "act" | "observe" | "grade" | "cleanup" | "verifyResult" | "verifySetup" | "verifyQueryRejection"> & {
|
||||
journeyKind?: "agent-chat" | "browser-boundary" | "scoped-external-assistant";
|
||||
export type ChatCatalogDescriptor = Omit<ChatCatalogCase, "dispatchSlackQuery" | "runCompanionSurface" | "journeyKind" | "setup" | "act" | "observe" | "grade" | "cleanup" | "verifyResult" | "verifySetup" | "verifyQueryRejection"> & {
|
||||
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"))
|
||||
|
||||
@@ -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<ChatRun[]>; refreshIssue(): Promise<void>;
|
||||
input: ChatFlowInput; issue(): ChatIssue | undefined; allRuns(): Promise<ChatRun[]>; refreshIssue(): Promise<void>; selectSlackSource?(issueId: string): Promise<void>;
|
||||
}) {
|
||||
const { input } = context;
|
||||
const { api, page, fixtures: f, execution, nonce } = input;
|
||||
|
||||
@@ -399,6 +399,19 @@ export async function runChatFlow(input: ChatFlowInput) {
|
||||
rows.map((row) => api.get<ChatRun>(`/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<ChatIssue>(`/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<ChatIssue>(`/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<ChatIssue>(`/api/issues/${sourceId}`), runs: await allRuns() };
|
||||
}
|
||||
if (catalogDescriptor?.journeyKind === "scoped-external-assistant") {
|
||||
await runChatFeatureFlow({ input, issue: () => issue, allRuns,
|
||||
refreshIssue: async () => { issue = await api.get<ChatIssue>(chatPath); } });
|
||||
|
||||
@@ -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<typeof input>) => { v.comment.body += " Try harder"; },
|
||||
(v: ReturnType<typeof input>) => { v.comment.authorAgentId = "agent"; },
|
||||
(v: ReturnType<typeof input>) => { v.receipt.provider.senderUserId = "U999"; },
|
||||
(v: ReturnType<typeof input>) => { v.links[0]!.status = "revoked"; },
|
||||
(v: ReturnType<typeof input>) => { v.conversation.externalThreadId = "slack:C123:9999999999.123456"; },
|
||||
(v: ReturnType<typeof input>) => { v.conversation.externalConversationId = "C123"; },
|
||||
(v: ReturnType<typeof input>) => { v.endpoint.companyId = "foreign"; },
|
||||
(v: ReturnType<typeof input>) => { v.binding.conversationId = "another"; },
|
||||
(v: ReturnType<typeof input>) => { 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);
|
||||
});
|
||||
});
|
||||
@@ -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<ChatCatalogCase, "scenarioId" | "slackIngress" | "expectedRunCount" | "minimumRunCount" | "expectedHumanMessageCount" | "setupHumanMessageCount" | "queryBindings" | "queryAttachments" | "queryOutcome" | "companionSurface" | "runPolicy" | "turnTimeoutMs" | "attemptTimeoutMs"> & { 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<string, unknown>, 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<never>((_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<ChatEndpoint>(`/api/chat-endpoints/${receipt.endpointId}`);
|
||||
const conversations = await api.get<ChatConversation[]>(`/api/chat-endpoints/${receipt.endpointId}/conversations`);
|
||||
const binding = await api.get<ExternalChannelBindingSummary>(`/api/issues/${receipt.issueId}/chat-binding`);
|
||||
const comments = await api.get<IssueComment[]>(`/api/issues/${receipt.issueId}/comments?order=asc`);
|
||||
const links = await api.get<ChatIdentityLink[]>(`/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." };
|
||||
}
|
||||
Reference in new issue
Block a user