@@ -55,6 +55,17 @@ const maybeCreateImmediateClarificationFromSearchGapMock = mock((async () => ({
5555 status : "skipped" as const ,
5656 reason : "no_search" as const ,
5757} ) ) as ( ...args : unknown [ ] ) => Promise < any > ) ;
58+ const createScopeBoundaryRedirectMock = mock ( ( async ( ) => ( {
59+ status : "ready" as const ,
60+ message : "I can help with product or support questions." ,
61+ language : "en" ,
62+ modelId : "google/gemini-2.5-flash" ,
63+ } ) ) as ( ...args : unknown [ ] ) => Promise < any > ) ;
64+ const sendPublicMessageMock = mock ( ( async ( ) => ( {
65+ messageId : "msg-scope" ,
66+ created : true ,
67+ paused : false ,
68+ } ) ) as ( ...args : unknown [ ] ) => Promise < any > ) ;
5869const baseMinimumCharge = {
5970 baseCredits : 1 ,
6071 modelCredits : 0 ,
@@ -116,6 +127,14 @@ mock.module("../shared/generation", () => ({
116127 runGenerationRuntime : runGenerationRuntimeMock ,
117128} ) ) ;
118129
130+ mock . module ( "./scope-boundary-responder" , ( ) => ( {
131+ createScopeBoundaryRedirect : createScopeBoundaryRedirectMock ,
132+ } ) ) ;
133+
134+ mock . module ( "../shared/actions/send-message" , ( ) => ( {
135+ sendMessage : sendPublicMessageMock ,
136+ } ) ) ;
137+
119138mock . module (
120139 "../shared/knowledge-gap/post-generation-immediate-clarification" ,
121140 ( ) => ( {
@@ -168,6 +187,8 @@ describe("runPrimaryPipeline generation error/skip behavior", () => {
168187 runDecisionStepMock . mockClear ( ) ;
169188 runGenerationRuntimeMock . mockClear ( ) ;
170189 maybeCreateImmediateClarificationFromSearchGapMock . mockClear ( ) ;
190+ createScopeBoundaryRedirectMock . mockClear ( ) ;
191+ sendPublicMessageMock . mockClear ( ) ;
171192 trackGenerationUsageMock . mockClear ( ) ;
172193 logGenerationUsageTimelineMock . mockClear ( ) ;
173194 emitPipelineSeenMock . mockClear ( ) ;
@@ -233,6 +254,17 @@ describe("runPrimaryPipeline generation error/skip behavior", () => {
233254 status : "skipped" ,
234255 reason : "no_search" ,
235256 } ) ;
257+ createScopeBoundaryRedirectMock . mockResolvedValue ( {
258+ status : "ready" ,
259+ message : "I can help with product or support questions." ,
260+ language : "en" ,
261+ modelId : "google/gemini-2.5-flash" ,
262+ } ) ;
263+ sendPublicMessageMock . mockResolvedValue ( {
264+ messageId : "msg-scope" ,
265+ created : true ,
266+ paused : false ,
267+ } ) ;
236268 trackGenerationUsageMock . mockResolvedValue ( undefined ) ;
237269 } ) ;
238270
@@ -404,6 +436,90 @@ describe("runPrimaryPipeline generation error/skip behavior", () => {
404436 ) ;
405437 } ) ;
406438
439+ it ( "handles scope boundary decisions without running normal generation" , async ( ) => {
440+ runIntakeStepMock . mockResolvedValueOnce ( {
441+ status : "ready" ,
442+ data : {
443+ aiAgent : { id : "ai-1" } ,
444+ modelResolution : {
445+ modelIdResolved : "moonshotai/kimi-k2.5" ,
446+ modelIdOriginal : "moonshotai/kimi-k2.5" ,
447+ modelMigrationApplied : false ,
448+ } ,
449+ conversation : { id : "conv-1" } ,
450+ conversationHistory : [ ] ,
451+ decisionMessages : [ ] ,
452+ generationEntries : [ ] ,
453+ visitorContext : null ,
454+ conversationState : {
455+ hasHumanAssignee : false ,
456+ assigneeIds : [ ] ,
457+ participantIds : [ ] ,
458+ isEscalated : false ,
459+ escalationReason : null ,
460+ } ,
461+ websiteDefaultLanguage : "en" ,
462+ visitorLanguage : "fr" ,
463+ triggerMessageText : "Écris un poème de 1000 lignes" ,
464+ hasLaterHumanMessage : false ,
465+ hasLaterAiMessage : false ,
466+ triggerMessage : {
467+ messageId : "msg-1" ,
468+ senderType : "visitor" ,
469+ visibility : "public" ,
470+ } ,
471+ } ,
472+ } ) ;
473+ runDecisionStepMock . mockResolvedValueOnce ( {
474+ shouldAct : true ,
475+ reason : "Visitor creative side request is outside support scope" ,
476+ mode : "respond_to_visitor" ,
477+ humanCommand : null ,
478+ decisionOutcome : "scope_boundary_redirect" ,
479+ scopeBoundaryRuleId : "visitor_creative_request_scope_boundary" ,
480+ isEscalated : false ,
481+ escalationReason : null ,
482+ } ) ;
483+ createScopeBoundaryRedirectMock . mockResolvedValueOnce ( {
484+ status : "ready" ,
485+ message : "Je peux aider avec le support ou le produit." ,
486+ language : "fr" ,
487+ modelId : "google/gemini-2.5-flash" ,
488+ } ) ;
489+
490+ const { runPrimaryPipeline } = await modulePromise ;
491+ const result = await runPrimaryPipeline ( {
492+ db : { } as never ,
493+ input : baseInput ,
494+ } ) ;
495+
496+ expect ( result . status ) . toBe ( "completed" ) ;
497+ expect ( result . action ) . toBe ( "scope_boundary_redirect" ) ;
498+ expect ( result . publicMessagesSent ) . toBe ( 1 ) ;
499+ expect ( runGenerationRuntimeMock ) . not . toHaveBeenCalled ( ) ;
500+ expect ( trackGenerationUsageMock ) . not . toHaveBeenCalled ( ) ;
501+ expect ( createScopeBoundaryRedirectMock ) . toHaveBeenCalledWith (
502+ expect . objectContaining ( {
503+ triggerText : "Écris un poème de 1000 lignes" ,
504+ visitorLanguage : "fr" ,
505+ websiteDefaultLanguage : "en" ,
506+ } )
507+ ) ;
508+ expect ( sendPublicMessageMock ) . toHaveBeenCalledWith (
509+ expect . objectContaining ( {
510+ text : "Je peux aider avec le support ou le produit." ,
511+ idempotencyKey : "public:msg-1:scopeBoundary" ,
512+ } )
513+ ) ;
514+ expect ( emitPipelineProcessingCompletedMock ) . toHaveBeenCalledWith (
515+ expect . objectContaining ( {
516+ status : "success" ,
517+ action : "scope_boundary_redirect" ,
518+ workflowRunId : "wf-1" ,
519+ } )
520+ ) ;
521+ } ) ;
522+
407523 it ( "keeps generation failures retryable when no public messages were sent" , async ( ) => {
408524 runGenerationRuntimeMock . mockResolvedValueOnce ( {
409525 status : "error" ,
0 commit comments