Files
zyt/server/tests/DiagnosisAiAssistantRetryIsolationTest.php
2026-10-27 14:03:35 +08:00

108 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
namespace think\facade {
class Log {
public static function warning(string $message, array $context = []): void {}
public static function write(string $message, string $level = ''): void {}
}
}
namespace app\adminapi\logic\tcm {
function config(string $key): array { return ['timeout' => 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' => '<USER_QUESTION>总结本次资料</USER_QUESTION>', '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";
}