90]; } function hrtime(bool $number = false): int|array { $now = $GLOBALS['retryClockNs']; return $number ? $now : [intdiv($now, 1000000000), $now % 1000000000]; } } namespace app\adminapi\service { function hrtime(bool $number = false): int|array { return \app\adminapi\logic\tcm\hrtime($number); } } namespace app\common\service { class DifyChatService { public static array $calls = []; public static array $stream = []; public static array $blocking = []; public static function streamChat($profile, $inputs, $query, $user, $onDelta, $abort, $files, $options): array { self::$calls[] = ['kind' => 'stream', 'profile' => $profile, 'options' => $options]; $GLOBALS['retryClockNs'] += (self::$stream['advance_ms'] ?? 0) * 1000000; foreach (self::$stream['chunks'] ?? [] as $chunk) $onDelta($chunk); return self::$stream; } public static function chat($profile, $inputs, $query, $user, $files, $options): array { self::$calls[] = ['kind' => 'blocking', 'profile' => $profile, 'options' => $options]; return self::$blocking; } } } namespace { require dirname(__DIR__) . '/vendor/autoload.php'; use app\adminapi\logic\tcm\DiagnosisAiLogic as Logic; use app\adminapi\service\TcmEvidenceGuard; use app\common\service\DifyChatService as Transport; $assertions = 0; function retryCheck(bool $condition, string $message): void { ++$GLOBALS['assertions']; if (!$condition) throw new \RuntimeException($message); } $guard = TcmEvidenceGuard::assess(['id' => 1, 'chief_complaint' => '口干', 'present_illness' => '口干,未记录固定刺痛。', 'tongue' => '舌淡红苔少', 'pulse' => '脉细弱']); $prepared = ['task' => 'summary', 'profile' => 'qwen', 'admin_id' => 1, 'query' => '总结本次资料', 'inputs' => [], 'files' => [], 'user' => 'synthetic-offline-retry', 'upstream_timeout' => 90, 'tcm_evidence_guard' => $guard]; foreach (['qwen', 'openai'] as $profile) { $prepared['profile'] = $profile; foreach (['NEW final', "NEW final\n\nsecond paragraph"] as $new) { $GLOBALS['retryClockNs'] = 1000000000; Transport::$calls = []; Transport::$stream = ['ok' => false, 'error_code' => 'INCOMPLETE_RESPONSE', 'chunks' => ['OLD buffered']]; Transport::$blocking = ['ok' => true, 'content' => $new]; $visible = ''; $out = Logic::streamPreparedAssistant($prepared, static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; }); retryCheck($out !== null && $visible === $new && $out['answer'] === $new, 'Only the exact replacement answer can leave the compatibility attempt.'); retryCheck(count(Transport::$calls) === 2 && !str_contains($visible, 'OLD'), 'No failed attempt buffer survives.'); } $GLOBALS['retryClockNs'] = 1000000000; Transport::$calls = []; Transport::$stream = ['ok' => false, 'error_code' => 'INCOMPLETE_RESPONSE', 'chunks' => ['OLD buffered']]; Transport::$blocking = ['ok' => true, 'content' => '当前无固定刺痛。']; $visible = ''; $out = Logic::streamPreparedAssistant($prepared, static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; }); retryCheck($out === null && Logic::getAssistantErrorCode() === 'TCM_FACT_STATE_CONFLICT' && $visible === '', 'The new answer is independently checked; neither its conflict nor the old pending text is exposed.'); $GLOBALS['retryClockNs'] = 1000000000; Transport::$calls = []; Transport::$stream = ['ok' => false, 'error_code' => 'INCOMPLETE_RESPONSE', 'chunks' => ['OLD buffered'], 'advance_ms' => 9000]; Transport::$blocking = ['ok' => true, 'content' => 'NEW final']; $out = Logic::streamPreparedAssistant($prepared, static fn (string $text): bool => true, null, null, ['timeout' => 90, 'timeout_ms' => 12500]); retryCheck($out !== null && Transport::$calls[0]['options']['timeout_ms'] === 12500 && Transport::$calls[1]['options']['timeout_ms'] === 3500, 'An ordinary compatibility retry shares the original precise budget.'); $GLOBALS['retryClockNs'] = 1000000000; Transport::$calls = []; Transport::$stream = ['ok' => false, 'error_code' => 'INCOMPLETE_RESPONSE', 'chunks' => ['OLD buffered']]; $readyCalls = 0; $out = Logic::streamPreparedAssistant($prepared, static fn (string $text): bool => true, null, null, ['can_start_attempt' => static function () use (&$readyCalls): bool { return ++$readyCalls === 1; }]); retryCheck($out === null && Logic::getAssistantErrorCode() === 'UPSTREAM_CLEANUP_PENDING' && count(Transport::$calls) === 1, 'No compatibility generation starts while prior cleanup is unresolved.'); Transport::$calls = []; $out = Logic::streamPreparedAssistant($prepared, static fn (string $text): bool => true, null, null, ['can_start_attempt' => static fn (): bool => false]); retryCheck($out === null && Transport::$calls === [], 'Readiness is checked before the first network attempt.'); } echo "Assistant retry isolation: {$assertions} assertions passed (offline transport).\n"; }