@@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
66import { clearLargeValueCacheForTests } from '@/lib/execution/payloads/cache'
77import { isLargeArrayManifest } from '@/lib/execution/payloads/large-array-manifest-metadata'
88import { isLargeValueRef } from '@/lib/execution/payloads/large-value-ref'
9+ import { REDACTION_FAILED_MARKER , redactObjectStrings } from '@/lib/logs/execution/pii-redaction'
910import { projectTraceSpansForSecrets } from '@/lib/logs/execution/trace-secret-projection'
1011import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
1112import { BlockType , EDGE } from '@/executor/constants'
@@ -1565,6 +1566,79 @@ describe('BlockExecutor streaming pump', () => {
15651566 )
15661567 } )
15671568
1569+ it ( 'fails empty structured streams when the terminal event reports a token limit' , async ( ) => {
1570+ const onFullContent = vi . fn ( )
1571+ const handler = createAgentEventsStreamingHandler ( {
1572+ events : [ { type : 'turn_end' , turn : 'final' , finishReason : 'length' } ] ,
1573+ onFullContent,
1574+ } )
1575+ const { executor, block, state } = createExecutor ( handler )
1576+ block . config . params = {
1577+ responseFormat : { type : 'object' , properties : { answer : { type : 'string' } } } ,
1578+ }
1579+ const ctx = createContext ( state )
1580+
1581+ await expect ( executor . execute ( ctx , createNode ( block ) , block ) ) . rejects . toThrow (
1582+ / m a x i m u m o u t p u t - t o k e n l i m i t / i
1583+ )
1584+
1585+ expect ( onFullContent ) . not . toHaveBeenCalled ( )
1586+ expect ( state . getBlockOutput ( block . id ) ) . toMatchObject ( {
1587+ content : '' ,
1588+ error : expect . stringMatching ( / m a x i m u m o u t p u t - t o k e n l i m i t / i) ,
1589+ } )
1590+ expect ( state . getBlockOutput ( block . id ) ?. providerTiming ?. timeSegments ?. [ 0 ] ?. finishReason ) . toBe (
1591+ 'length'
1592+ )
1593+ } )
1594+
1595+ it ( 'preserves the original failure when provider-diagnostic redaction scrubs' , async ( ) => {
1596+ const redactor = vi . mocked ( redactObjectStrings )
1597+ redactor
1598+ . mockImplementationOnce ( async ( value , options ) => {
1599+ expect ( options . onFailure ) . toBe ( 'throw' )
1600+ return value as never
1601+ } )
1602+ . mockImplementationOnce ( async ( value , options ) => {
1603+ expect ( options . onFailure ) . toBe ( 'scrub' )
1604+ return {
1605+ ...( value as Record < string , unknown > ) ,
1606+ content : REDACTION_FAILED_MARKER ,
1607+ model : REDACTION_FAILED_MARKER ,
1608+ } as never
1609+ } )
1610+
1611+ const onFullContent = vi . fn ( )
1612+ const handler = createAgentEventsStreamingHandler ( {
1613+ events : [ { type : 'text_delta' , text : '{"answer":"unfinished' , turn : 'final' } ] ,
1614+ finishReason : 'max_tokens' ,
1615+ onFullContent,
1616+ } )
1617+ const { executor, block, state } = createExecutor ( handler )
1618+ block . config . params = {
1619+ responseFormat : { type : 'object' , properties : { answer : { type : 'string' } } } ,
1620+ }
1621+ const ctx = createContext ( state )
1622+ ctx . piiBlockOutputRedaction = {
1623+ enabled : true ,
1624+ entityTypes : [ 'EMAIL_ADDRESS' ] ,
1625+ language : 'en' ,
1626+ }
1627+
1628+ await expect ( executor . execute ( ctx , createNode ( block ) , block ) ) . rejects . toThrow (
1629+ / m a x i m u m o u t p u t - t o k e n l i m i t / i
1630+ )
1631+
1632+ expect ( redactor ) . toHaveBeenCalledTimes ( 2 )
1633+ expect ( onFullContent ) . not . toHaveBeenCalled ( )
1634+ expect ( state . getBlockOutput ( block . id ) ) . toMatchObject ( {
1635+ content : REDACTION_FAILED_MARKER ,
1636+ model : REDACTION_FAILED_MARKER ,
1637+ tokens : { input : 1 , output : 2 , total : 3 } ,
1638+ error : expect . stringMatching ( / m a x i m u m o u t p u t - t o k e n l i m i t / i) ,
1639+ } )
1640+ } )
1641+
15681642 it ( 'soft-completes on user abort with drained answer text (no failed block)' , async ( ) => {
15691643 const abortController = new AbortController ( )
15701644 const handler = createAgentEventsStreamingHandler ( {
0 commit comments