mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
test(evals): bind one genuine Slack distractor setup exchange
This commit is contained in:
1 parent
9d850991d9
commit
05b29a381c
4 files changed
+66
-22
No files matched your search
@@ -40,7 +40,7 @@ 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 ChatSlackIngress { kind: "native-slack-thread"; maximumProviderCalls: 20; ingressTimeoutMs: 120000; setupThread?: "same-agent-distractor" }
|
||||
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 };
|
||||
@@ -56,6 +56,8 @@ export interface ChatCatalogCase {
|
||||
installedSource?: "published-global-npm";
|
||||
journeyKind?: "agent-chat" | "scoped-external-assistant" | "native-slack-thread";
|
||||
slackIngress?: ChatSlackIngress;
|
||||
/** Only reply-in-thread: one real other-source setup exchange, included in total native bounds. */
|
||||
dispatchSlackOtherThread?(context: ChatCatalogContext, state: Record<string, unknown>, input: { query: string; signal: AbortSignal }): Promise<ChatSlackIngressReceipt & { runId: string }>;
|
||||
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>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { completeNativeSlackFileConditional, dispatchNativeSlackIngress, slackNativeRunCheck } from "./chat-slack-ingress.js";
|
||||
import { completeNativeSlackFileConditional, dispatchNativeSlackIngress, prepareNativeSlackOtherThread, slackOtherThreadBoundaryMatches, 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";
|
||||
@@ -100,6 +100,7 @@ export async function runChatCatalogFlow(context: ChatCatalogContext) {
|
||||
&& policy.amount === 500 && policy.hardStopEnabled && policy.isActive && policy.unpricedUsagePolicy === "allow")), "Public funding policy persisted").toBe(true);
|
||||
await input.evidence("chat-catalog-funding.json", { scopes, mode: implementation.setupPolicy?.funding ?? "standard",
|
||||
policies: funding.policies.filter(policy => scopes.some(scope => scope.scopeType === policy.scopeType && scope.scopeId === policy.scopeId)), setupProbeUsage: "unknown; production hello probes are outside heartbeat accounting" });
|
||||
if (slack && implementation.slackIngress?.setupThread === "same-agent-distractor") await prepareNativeSlackOtherThread(context,implementation,state,deadlineAt);
|
||||
await input.evidence("chat-catalog-setup.json", { scenario, state, inventoryDigest: chatCatalogInventoryDigest,
|
||||
oracleVersion: implementation.oracleVersion, expectedRunCount: implementation.expectedRunCount,
|
||||
minimumRunCount: implementation.minimumRunCount, turnTimeoutMs: implementation.turnTimeoutMs,
|
||||
@@ -118,7 +119,9 @@ export async function runChatCatalogFlow(context: ChatCatalogContext) {
|
||||
expectedHumanMessageCount: implementation.expectedHumanMessageCount ?? 1, expectedRunCount: implementation.expectedRunCount });
|
||||
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");
|
||||
const other=state.slackOtherThreadSetupReceipt as (import("./chat-catalog-contract.js").ChatSlackIngressReceipt & {runId:string})|undefined;
|
||||
const declaredOther=implementation.scenarioId==="chat-channels.reply-in-thread"&&implementation.slackIngress?.setupThread==="same-agent-distractor";
|
||||
if (setupBoundary.humanCommentIds.length || (declaredOther ? !other || !slackOtherThreadBoundaryMatches({receipt:other,runs:setupConversation.runs,companyId:input.fixtures.company.id,agentId:input.fixtures.agent.id}) : setupBoundary.runIds.length!==0)) throw new Error("Native Slack setup cannot admit undeclared 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);
|
||||
if (implementation.scenarioId === "chat-channels.channel-file") await completeNativeSlackFileConditional(context, implementation, state, deadlineAt);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { loadChatCatalogImplementation, type ChatCatalogDescriptor } from "./chat-catalog-registry.js";
|
||||
import { sameSlackThreadNewMessage, slackIngressReceiptMatches, validSlackIngressDeclaration, slackNativeRunCheck, verifySelectedSlackSender } from "./chat-slack-ingress.js";
|
||||
import { sameSlackThreadNewMessage, slackIngressReceiptMatches, validSlackIngressDeclaration, slackNativeRunCheck, verifySelectedSlackSender, slackOtherThreadBoundaryMatches, slackOtherThreadSetupQuery } from "./chat-slack-ingress.js";
|
||||
import type { ChatCatalogContext, ChatSlackIngressReceipt } from "./chat-catalog-contract.js";
|
||||
import type { ChatConversation, ChatEndpoint, 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 };
|
||||
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, setupThread:"same-agent-distractor" as const }, expectedRunCount: 4, minimumRunCount: 2, 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,
|
||||
@@ -60,7 +60,7 @@ describe("only real declared Slack canonical 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");
|
||||
const fileDescriptor: ChatCatalogDescriptor = { ...descriptor, scenarioId: "chat-channels.channel-file", expectedRunCount: 6, minimumRunCount: 2, expectedHumanMessageCount: 2, attemptTimeoutMs: 900000 };
|
||||
const fileDescriptor: ChatCatalogDescriptor = { ...descriptor, scenarioId: "chat-channels.channel-file", slackIngress:{kind:"native-slack-thread" as const,maximumProviderCalls:20 as const,ingressTimeoutMs:120000 as const}, expectedRunCount: 6, minimumRunCount: 2, expectedHumanMessageCount: 2, attemptTimeoutMs: 900000 };
|
||||
const file = { ...actual, ...fileDescriptor };
|
||||
await expect(loadChatCatalogImplementation(fileDescriptor, async () => ({ slackCases: [file] }))).rejects.toThrow("supported and unsupported transfer phases");
|
||||
const completeFile = { ...file, verifySlackSupportedTransfer: async () => ({ passed: true, checks: [{ id: "actual-transfer", passed: true, detail: "Independent phase-specific byte/egress evidence" }] }), dispatchSlackUnsupportedFile: async () => input().receipt };
|
||||
@@ -68,7 +68,7 @@ describe("only real declared Slack canonical ingress", () => {
|
||||
|
||||
});
|
||||
it("requires a separate same-thread canonical upload and declared two-phase bounds for the file conditional", () => {
|
||||
const file = { ...metadata, scenarioId: "chat-channels.channel-file", expectedRunCount: 6, minimumRunCount: 2, expectedHumanMessageCount: 2, attemptTimeoutMs: 900000 };
|
||||
const file = { ...metadata, scenarioId: "chat-channels.channel-file", slackIngress:{kind:"native-slack-thread" as const,maximumProviderCalls:20 as const,ingressTimeoutMs:120000 as const}, expectedRunCount: 6, minimumRunCount: 2, expectedHumanMessageCount: 2, attemptTimeoutMs: 900000 };
|
||||
expect(validSlackIngressDeclaration(file)).toBe(true);
|
||||
expect(validSlackIngressDeclaration({ ...file, expectedHumanMessageCount: 1 })).toBe(false);
|
||||
expect(validSlackIngressDeclaration({ ...metadata, scenarioId: "chat-channels.channel-file" })).toBe(false);
|
||||
@@ -89,3 +89,13 @@ describe("only real declared Slack canonical ingress", () => {
|
||||
expect(slackNativeRunCheck(ctx, [{ ...run, contextSnapshot: { issueId: "other" } }]).passed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("binds exactly one other-source setup native without treating that source's human as canonical setup history",()=>{
|
||||
const receipt={...input().receipt,query:slackOtherThreadSetupQuery,runId:"setup-native"};
|
||||
const run={id:"setup-native",companyId:"company",agentId:"agent",runtimeMode:"native",status:"succeeded",contextSnapshot:{issueId:"issue"}};
|
||||
const value={receipt,runs:[run],companyId:"company",agentId:"agent"};
|
||||
expect(slackOtherThreadBoundaryMatches(value)).toBe(true);
|
||||
for(const wrong of [{...value,runs:[]},{...value,runs:[run,{...run,id:"unrelated"}]},{...value,runs:[{...run,contextSnapshot:{issueId:"other-source"}}]},{...value,runs:[{...run,agentId:"other-agent"}]},{...value,runs:[{...run,status:"running"}]},{...value,receipt:{...receipt,query:"Undeclared setup instruction"}},{...value,sourceIssueId:"issue"}])expect(slackOtherThreadBoundaryMatches(wrong)).toBe(false);
|
||||
expect(validSlackIngressDeclaration({...metadata,slackIngress:{kind:"native-slack-thread",maximumProviderCalls:20,ingressTimeoutMs:120000}})).toBe(false);
|
||||
expect(validSlackIngressDeclaration({...metadata,expectedRunCount:3})).toBe(false);
|
||||
});
|
||||
@@ -24,10 +24,10 @@ export async function verifySelectedSlackSender(ctx:ChatCatalogContext,receipt:C
|
||||
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"
|
||||
&& Object.keys(c.slackIngress).sort().join(",") === (c.scenarioId === "chat-channels.reply-in-thread" ? "ingressTimeoutMs,kind,maximumProviderCalls,setupThread" : "ingressTimeoutMs,kind,maximumProviderCalls")
|
||||
&& c.turnTimeoutMs === 120000
|
||||
&& (c.scenarioId === "chat-channels.channel-file" ? c.attemptTimeoutMs === 900000 && c.expectedRunCount === 6 && c.minimumRunCount === 2 && c.expectedHumanMessageCount === 2
|
||||
: c.attemptTimeoutMs === 600000 && c.expectedRunCount === 3 && c.minimumRunCount === 1 && c.expectedHumanMessageCount === 1) && (c.setupHumanMessageCount ?? 0) === 0
|
||||
&& (c.scenarioId === "chat-channels.channel-file" ? !c.slackIngress.setupThread && c.attemptTimeoutMs === 900000 && c.expectedRunCount === 6 && c.minimumRunCount === 2 && c.expectedHumanMessageCount === 2
|
||||
: c.attemptTimeoutMs === 600000 && c.expectedRunCount === 4 && c.minimumRunCount === 2 && c.expectedHumanMessageCount === 1 && c.slackIngress.setupThread === "same-agent-distractor") && (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;
|
||||
@@ -52,6 +52,39 @@ export function sameSlackThreadNewMessage(first: ChatSlackIngressReceipt, next:
|
||||
&& next.provider.workspaceId === first.provider.workspaceId && next.provider.channelId === first.provider.channelId && next.provider.threadId === first.provider.threadId
|
||||
&& next.provider.senderUserId === first.provider.senderUserId && next.provider.canonicalMessageId !== first.provider.canonicalMessageId;
|
||||
}
|
||||
export const slackOtherThreadSetupQuery = "Please summarize the current company project status in this separate discussion.";
|
||||
async function independentlyBoundSlackReceipt(ctx:ChatCatalogContext,receipt:ChatSlackIngressReceipt,query:string,signal:AbortSignal){
|
||||
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<SlackPrincipalLink[]>(`/api/chat-endpoints/${receipt.endpointId}/principals`);
|
||||
const senderIdentity=await verifySelectedSlackSender(ctx,receipt,signal);
|
||||
const matched = slackIngressReceiptMatches({ receipt, query: 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,linksEndpointId:receipt.endpointId,senderIdentity })&&receipt.providerCalls+2<=20;
|
||||
return matched;
|
||||
}
|
||||
export function slackOtherThreadBoundaryMatches(input:{receipt:ChatSlackIngressReceipt & {runId:string};runs:ChatRun[];companyId:string;agentId:string;sourceIssueId?:string}){
|
||||
const {receipt:r,runs}=input;
|
||||
return r.query===slackOtherThreadSetupQuery&&runs.length===1&&runs[0]?.id===r.runId&&runs[0].companyId===input.companyId&&runs[0].agentId===input.agentId&&runs[0].runtimeMode==="native"&&runs[0].status==="succeeded"&&runs[0].contextSnapshot?.issueId===r.issueId&&(!input.sourceIssueId||input.sourceIssueId!==r.issueId);
|
||||
}
|
||||
/** One exact precondition exchange on another actual Slack source. Its human
|
||||
* comment never enters the canonical source's setup-human history. */
|
||||
export async function prepareNativeSlackOtherThread(ctx:ChatCatalogContext,c:ChatCatalogCase,state:Record<string,unknown>,deadlineAt:number){
|
||||
if(c.scenarioId!=="chat-channels.reply-in-thread"||c.slackIngress?.setupThread!=="same-agent-distractor"||!validSlackIngressDeclaration(c)||!c.dispatchSlackOtherThread||ctx.issue()||(await ctx.allRuns()).length)throw new Error("Invalid or nonempty declared Slack other-thread setup");
|
||||
const signal=AbortSignal.timeout(Math.min(120000,deadlineAt-Date.now()));
|
||||
let listener:(()=>void)|undefined;
|
||||
const timeout=new Promise<never>((_resolve,reject)=>{listener=()=>reject(new Error("Slack other-thread setup exceeded its declared deadline"));signal.addEventListener("abort",listener,{once:true});});
|
||||
let receipt:ChatSlackIngressReceipt & {runId:string};
|
||||
try{receipt=await Promise.race([c.dispatchSlackOtherThread(ctx,state,{query:slackOtherThreadSetupQuery,signal}),timeout]);}finally{if(listener)signal.removeEventListener("abort",listener);}
|
||||
|
||||
const matched=await independentlyBoundSlackReceipt(ctx,receipt,slackOtherThreadSetupQuery,signal)&&slackOtherThreadBoundaryMatches({receipt,runs:await ctx.allRuns(),companyId:ctx.input.fixtures.company.id,agentId:ctx.input.fixtures.agent.id});
|
||||
await ctx.input.evidence("chat-slack-other-thread-setup.json",{receipt,matched,canonicalSourceHumanCount:0,includedInTotalNativeBound:true});
|
||||
if(!matched||signal.aborted)throw new Error("Actual Slack other-thread setup did not prove its sole linked-human/company/source/native execution");
|
||||
state.slackOtherThreadSetupReceipt=receipt;
|
||||
}
|
||||
/** 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, phase: "canonical" | "unsupported-file" = "canonical") {
|
||||
@@ -67,20 +100,14 @@ export async function dispatchNativeSlackIngress(ctx: ChatCatalogContext, c: Cha
|
||||
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<SlackPrincipalLink[]>(`/api/chat-endpoints/${receipt.endpointId}/principals`);
|
||||
const senderIdentity=await verifySelectedSlackSender(ctx,receipt,signal);
|
||||
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,linksEndpointId:receipt.endpointId,senderIdentity })&&receipt.providerCalls+2<=c.slackIngress!.maximumProviderCalls;
|
||||
const matched = await independentlyBoundSlackReceipt(ctx,receipt,ctx.scenario.query,signal);
|
||||
const first = state.slackIngressReceipt as ChatSlackIngressReceipt | undefined;
|
||||
const sameThreadNewMessage = phase === "canonical" || !!first && sameSlackThreadNewMessage(first, receipt);
|
||||
await ctx.input.evidence(`chat-slack-ingress-${phase}-binding.json`, { matched, sameThreadNewMessage, phase, canonicalQuery: ctx.scenario.query, receipt, assignedAgentId: endpoint.assignedAgentId,
|
||||
await ctx.input.evidence(`chat-slack-ingress-${phase}-binding.json`, { matched, sameThreadNewMessage, phase, canonicalQuery: ctx.scenario.query, receipt, assignedAgentId: ctx.input.fixtures.agent.id,
|
||||
linkedIdentityConfirmed: matched, independentProviderIdentityCalls:2,privateProviderConfiguration: "withheld", providerSenderMapping: "private official human token/profile plus scoped public principal and actual comment" });
|
||||
if (signal.aborted || !matched || !sameThreadNewMessage) throw new Error("Slack ingress did not independently bind exact canonical human, linked identity, company and provider thread");
|
||||
const other=state.slackOtherThreadSetupReceipt as ChatSlackIngressReceipt|undefined;
|
||||
const otherThreadDistinct=ctx.scenario.id!=="chat-channels.reply-in-thread"||!!other&&other.endpointId===receipt.endpointId&&other.issueId!==receipt.issueId&&other.conversationId!==receipt.conversationId&&other.provider.threadId!==receipt.provider.threadId&&other.provider.workspaceId===receipt.provider.workspaceId&&other.provider.channelId===receipt.provider.channelId&&other.provider.senderUserId===receipt.provider.senderUserId&&other.linkedPaperclipUserId===receipt.linkedPaperclipUserId;
|
||||
if (signal.aborted || !matched || !sameThreadNewMessage || !otherThreadDistinct) 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;
|
||||
}
|
||||
@@ -96,7 +123,9 @@ export async function completeNativeSlackFileConditional(ctx: ChatCatalogContext
|
||||
export function slackNativeRunCheck(ctx: ChatCatalogContext, runs: ChatRun[], state?: Record<string, unknown>) {
|
||||
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
|
||||
const other=state?.slackOtherThreadSetupReceipt as (ChatSlackIngressReceipt & {runId:string})|undefined;
|
||||
const otherStillBound=!other||slackOtherThreadBoundaryMatches({receipt:other,runs:runs.filter(run=>run.contextSnapshot?.issueId===other.issueId),companyId:ctx.input.fixtures.company.id,agentId:ctx.input.fixtures.agent.id,sourceIssueId:source?.id});
|
||||
return { id: "catalog-real-slack-source-native", passed: otherStillBound && !!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")
|
||||
&& (!state?.slackUnsupportedIngressReceipt || Array.isArray(state.slackSupportedTransferRunIds) && sourceRuns.some(run => !(state.slackSupportedTransferRunIds as unknown[]).includes(run.id))),
|
||||
|
||||
Reference in new issue
Block a user