修复
This commit is contained in:
@@ -70,6 +70,54 @@ $now += 10; $openai->frame(['choices' => [['delta' => ['reasoning_content' => 'p
|
||||
streamTimingExpect(!isset($openai->snapshot()['first_answer_ms']), 'reasoning content is not a visible first token');
|
||||
$now += 20; $openai->frame(['choices' => [['delta' => ['content' => 'private-answer']]]], 'openai');
|
||||
streamTimingExpect($openai->snapshot()['first_answer_ms'] === 30, 'Chat Completions first content is timed');
|
||||
$openai->frame(['event' => 'node_retry', 'data' => ['retry_index' => 2, 'error' => 'private-provider-error']], 'dify');
|
||||
$openai->frame(['choices' => [], 'usage' => ['completion_tokens_details' => ['reasoning_tokens' => 1234]]], 'openai');
|
||||
$diagnostic = $openai->snapshot();
|
||||
streamTimingExpect($diagnostic['reasoning_frame_count'] === 1 && $diagnostic['reasoning_tokens'] === 1234
|
||||
&& $diagnostic['max_node_retry_index'] === 2 && isset($diagnostic['events']['node_retry']), 'reasoning and internal retry signals retain numeric diagnostic metadata only');
|
||||
streamTimingExpect(!str_contains(json_encode($diagnostic), 'private-'), 'reasoning/retry diagnostics never retain content or provider errors');
|
||||
|
||||
$cleanup = new DifyStreamTiming($clock);
|
||||
$now += 5; $cleanup->interrupted('private-abort-reason');
|
||||
$now += 5; $cleanup->interrupted('timeout');
|
||||
$cleanup->stopIoBegin();
|
||||
$now += 7; $cleanup->stopIoEnd(false);
|
||||
$now += 3; $cleanup->stopIoBegin();
|
||||
$now += 7; $cleanup->stopIoEnd(true);
|
||||
$now += 5; $cleanup->stopAccepted('coordinator');
|
||||
$cleanup->workflowTerminal('private-terminal');
|
||||
streamTimingExpect(!isset($cleanup->snapshot()['workflow_terminal_ms']), 'unknown workflow status cannot become terminal proof');
|
||||
$cleanup->workflowTerminal('stopped');
|
||||
$now += 5; $cleanup->workflowTerminal('succeeded');
|
||||
$cleanup->transportReturned('natural_eof');
|
||||
$now += 1; $cleanup->transportClosed();
|
||||
$cleanupSnapshot = $cleanup->snapshot();
|
||||
streamTimingExpect($cleanupSnapshot['interruption_detected_ms'] === 5 && $cleanupSnapshot['interruption_trigger'] === 'other'
|
||||
&& $cleanupSnapshot['stop_io_begin_ms'] === 10 && $cleanupSnapshot['last_stop_io_begin_ms'] === 20
|
||||
&& $cleanupSnapshot['stop_io_end_ms'] === 17 && $cleanupSnapshot['last_stop_io_end_ms'] === 27
|
||||
&& $cleanupSnapshot['stop_io_attempt_count'] === 2 && $cleanupSnapshot['stop_io_completed_count'] === 2
|
||||
&& $cleanupSnapshot['stop_io_accepted_count'] === 1 && $cleanupSnapshot['stop_accepted_ms'] === 27
|
||||
&& $cleanupSnapshot['stop_accepted_source'] === 'local_io' && $cleanupSnapshot['workflow_terminal_ms'] === 32
|
||||
&& $cleanupSnapshot['workflow_terminal_status'] === 'stopped' && $cleanupSnapshot['natural_eof_ms'] === 37
|
||||
&& $cleanupSnapshot['transport_closed_ms'] === 38, 'bounded first/last I/O timeline separates attempts, acceptance, terminal, EOF and close');
|
||||
streamTimingExpect(!str_contains(json_encode($cleanupSnapshot), 'private-'), 'cleanup keeps only numeric timestamps/counts and fixed safe enums');
|
||||
$now -= 20;
|
||||
streamTimingExpect($cleanup->snapshot()['duration_ms'] === 38, 'injected clock regression cannot move timestamps backwards');
|
||||
$now += 1000000000;
|
||||
streamTimingExpect($cleanup->snapshot()['duration_ms'] === 3600000, 'timestamps saturate at a bounded one-hour telemetry horizon');
|
||||
$metricsProperty = new ReflectionProperty(DifyStreamTiming::class, 'metrics');
|
||||
$boundedMetrics = $metricsProperty->getValue($cleanup);
|
||||
$boundedMetrics['stop_io_attempt_count'] = 1000000000;
|
||||
$metricsProperty->setValue($cleanup, $boundedMetrics);
|
||||
$cleanup->stopIoBegin();
|
||||
streamTimingExpect($cleanup->snapshot()['stop_io_attempt_count'] === 1000000000, 'actual I/O counter saturates without unbounded growth');
|
||||
$unknownExit = new DifyStreamTiming($clock);
|
||||
$unknownExit->transportReturned('private-transport-error');
|
||||
$unknownExit->stopAccepted('private-peer-name');
|
||||
streamTimingExpect($unknownExit->snapshot()['transport_exit'] === 'other'
|
||||
&& $unknownExit->snapshot()['stop_accepted_source'] === 'other'
|
||||
&& !isset($unknownExit->snapshot()['natural_eof_ms'])
|
||||
&& !str_contains(json_encode($unknownExit->snapshot()), 'private-'), 'unknown source/outcome labels never leak or fabricate EOF');
|
||||
|
||||
// Exercise the real parser, so telemetry agrees with content the consumer sees.
|
||||
$parser = new ReflectionClass(DifyChatService::class);
|
||||
@@ -94,6 +142,29 @@ streamTimingExpect($received === 'private-answer-suffix' && $partsSnapshot['firs
|
||||
&& $partsSnapshot['first_delivered_ms'] === 30, 'text-part array first answer and delivery agree with the real parser');
|
||||
streamTimingExpect(!str_contains(json_encode($partsSnapshot), 'private-'), 'text-part timing retains no content');
|
||||
|
||||
$terminalState = $parser->getMethod('newStreamState')->invoke(null);
|
||||
$terminalTiming = new DifyStreamTiming($clock);
|
||||
$terminalState['timing'] = $terminalTiming;
|
||||
foreach ([
|
||||
['event' => 'workflow_finished', 'data' => ['status' => 'stopped']],
|
||||
['event' => 'workflow_started', 'task_id' => 'private-bound-task'],
|
||||
['event' => 'workflow_finished', 'task_id' => 'private-bound-task', 'data' => ['status' => 'running']],
|
||||
['event' => 'workflow_finished', 'task_id' => 'private-foreign-task', 'data' => ['status' => 'stopped']],
|
||||
['event' => 'message_end'],
|
||||
] as $frame) {
|
||||
$consume->invokeArgs(null, ['dify', 'data: ' . json_encode($frame), &$terminalState, $accept]);
|
||||
}
|
||||
streamTimingExpect(!isset($terminalTiming->snapshot()['workflow_terminal_ms']), 'unbound/foreign/invalid terminal and message_end do not manufacture terminal timing');
|
||||
$now += 5;
|
||||
$consume->invokeArgs(null, ['dify', 'data: ' . json_encode(['event' => 'workflow_finished',
|
||||
'task_id' => 'private-bound-task', 'data' => ['status' => 'cancelled', 'error' => 'private-provider-error']]), &$terminalState, $accept]);
|
||||
$now += 5;
|
||||
$consume->invokeArgs(null, ['dify', 'data: ' . json_encode(['event' => 'workflow_finished',
|
||||
'task_id' => 'private-bound-task', 'data' => ['status' => 'succeeded']]), &$terminalState, $accept]);
|
||||
streamTimingExpect($terminalTiming->snapshot()['workflow_terminal_ms'] === 5
|
||||
&& $terminalTiming->snapshot()['workflow_terminal_status'] === 'stopped'
|
||||
&& !str_contains(json_encode($terminalTiming->snapshot()), 'private-'), 'first allowed real workflow terminal remains immutable after message_end');
|
||||
|
||||
$app = new \think\App(dirname(__DIR__) . '/');
|
||||
$directory = sys_get_temp_dir() . '/zyt-upstream-timing-' . bin2hex(random_bytes(8));
|
||||
$app->config->set(['default' => 'timing', 'channels' => ['timing' => ['type' => 'File', 'path' => $directory,
|
||||
|
||||
Reference in New Issue
Block a user