300]; } } namespace app\adminapi\logic\tcm { // Deterministic monotonic clock: no waiting, model, database, or transport calls. function hrtime(bool $asNumber = false) { return $GLOBALS['compactionNowNs']; } } namespace app\common\service { class DifyChatService { public static array $calls = []; public static $respond; public static function chat($profile, $inputs, $query, $user, $files = [], $options = []): array { self::$calls[] = compact('profile', 'inputs', 'query', 'user', 'files', 'options'); return (self::$respond)($inputs, $options, count(self::$calls)); } } } namespace { require dirname(__DIR__) . '/vendor/autoload.php'; use app\adminapi\logic\tcm\PatientAiReportLogic; use app\adminapi\service\AiContextCompactionCache; use app\common\service\DifyChatService; use think\facade\Cache; $assertions = 0; function compactionBudgetExpect(bool $ok, string $message): void { ++$GLOBALS['assertions']; if (!$ok) throw new RuntimeException($message); } function compactionBudgetReset(?callable $respond = null): void { $GLOBALS['compactionNowNs'] = 1000000000000; DifyChatService::$calls = []; DifyChatService::$respond = $respond ?? static fn (): array => ['ok' => true, 'content' => 'complete evidence']; Cache::$items = []; } function compactionBudgetFailure(callable $run, int $code, string $message): void { try { $run(); } catch (RuntimeException $failure) { compactionBudgetExpect($failure->getCode() === $code && $failure->getMessage() === $message, 'failure has the fixed public classification'); return; } throw new RuntimeException('Expected compaction failure'); } function compactionBudgetFragment(array $call, string $tag): string { $start = '<' . $tag . ">\n"; $end = "\n'; $offset = strpos($call['query'], $start); compactionBudgetExpect($offset !== false && str_ends_with($call['query'], $end), 'source frame is complete'); return substr($call['query'], $offset + strlen($start), -strlen($end)); } $source = str_repeat('a', 119999) . '中' . str_repeat('临床资料;', 9000) . 'TAIL-UNIQUE'; compactionBudgetReset(); $short = str_repeat('a', 120000); $untouched = PatientAiReportLogic::compactSourceForPrompt('qwen', $short); compactionBudgetExpect($untouched['text'] === $short && !$untouched['compacted'] && DifyChatService::$calls === [], 'exact boundary never calls the model or changes source bytes'); $boundary = PatientAiReportLogic::compactSourceForPrompt('qwen', $short . 'z'); compactionBudgetExpect($boundary['chunk_count'] === 2 && count(DifyChatService::$calls) === 2, 'one byte above boundary submits both complete source fragments'); compactionBudgetReset(static function (array $inputs, array $options): array { ($options['on_progress'])(['phase' => 'analysis', 'status' => 'submitted', 'completed' => 13, 'total' => 13, 'message' => 'PRIVATE-PROVIDER-TEXT', 'required_sections' => ['PRIVATE-MANIFEST']]); $GLOBALS['compactionNowNs'] += 125000000; return ['ok' => true, 'content' => 'complete evidence']; }); $progress = []; $noop = static function (): void {}; $abort = static fn (): bool => false; $gate = static fn (): bool => true; $options = ['timeout' => 3, 'timeout_ms' => 1600, 'upstream_user' => 'synthetic-context', 'should_abort' => $abort, 'can_start_attempt' => $gate, 'on_task' => $noop, 'on_stop' => $noop, 'on_transfer_end' => $noop, 'on_cancel_task' => $noop, 'on_terminal' => $noop, 'on_upstream_timing' => $noop, 'on_progress' => static function (array $event) use (&$progress): void { $progress[] = $event; }]; $produce = static fn (): array => PatientAiReportLogic::compactSourceForPrompt('qwen', $source, $options); $cold = AiContextCompactionCache::remember('synthetic-authorized-scope', 'qwen', $source, $produce, $abort); compactionBudgetExpect($cold['source_bytes'] === strlen($source) && $cold['chunk_count'] === 3 && !$cold['cache_hit'], 'actual cache producer records complete original bytes and chunk count'); $covered = ''; foreach (DifyChatService::$calls as $index => $call) { $fragment = compactionBudgetFragment($call, 'PATIENT_SOURCE_FRAGMENT'); $covered .= $fragment; compactionBudgetExpect(strlen($fragment) <= 120000 && preg_match('//u', $fragment) === 1, 'every source fragment fits the UTF-8 byte boundary'); compactionBudgetExpect($call['options']['timeout_ms'] === 1600 - 125 * $index && $call['options']['timeout'] === 2, 'all cached producer attempts use remaining shared milliseconds'); compactionBudgetExpect($call['options']['request_deadline_ns'] === 1001600000000, 'the same absolute deadline reaches all internal Dify attempts'); foreach (['should_abort', 'can_start_attempt', 'on_task', 'on_stop', 'on_transfer_end', 'on_cancel_task', 'on_terminal', 'on_upstream_timing'] as $callback) { compactionBudgetExpect($call['options'][$callback] === $options[$callback], "$callback reaches the actual evidence transport unchanged"); } compactionBudgetExpect($call['user'] === 'synthetic-context' && $call['files'] === [] && $call['options']['knowledge_mode'] === 'evidence', 'extractor identity and evidence-only mode are preserved'); } compactionBudgetExpect($covered === $source, 'all submitted source fragments concatenate to every exact original byte'); compactionBudgetExpect(count($progress) === 9, 'before, hidden transfer, and after progress remain visible per chunk'); foreach ($progress as $index => $event) { compactionBudgetExpect($event['phase'] === 'preparing' && $event['status'] === 'compacting' && $event['total'] === 3 && $event['completed'] === intdiv($index, 3) + ($index % 3 === 2 ? 1 : 0) && array_keys($event) === ['phase', 'status', 'message', 'completed', 'total'], 'hidden transfer progress reports only safe completed preparation counters'); } compactionBudgetExpect(!str_contains(json_encode($progress), 'PRIVATE-'), 'provider text and manifests never escape hidden extraction'); $warm = AiContextCompactionCache::remember('synthetic-authorized-scope', 'qwen', $source, $produce, $abort); compactionBudgetExpect($warm['cache_hit'] && count(DifyChatService::$calls) === 3 && count(Cache::$items) === 1, 'only complete evidence is cached and reused without new hidden calls'); foreach ([[], ['timeout' => 300], ['timeout' => 1], ['request_deadline_ns' => 1002500000000]] as $budgetOptions) { compactionBudgetReset(static function (): array { $GLOBALS['compactionNowNs'] += 100000000; return ['ok' => true, 'content' => 'complete evidence']; }); PatientAiReportLogic::compactSourceForPrompt('qwen', $source, $budgetOptions); $initialMs = isset($budgetOptions['request_deadline_ns']) ? 2500 : (isset($budgetOptions['timeout']) && $budgetOptions['timeout'] === 1 ? 1000 : 90000); foreach (DifyChatService::$calls as $index => $call) { compactionBudgetExpect($call['options']['timeout_ms'] === $initialMs - $index * 100, 'default, capped, seconds, and inherited deadlines are shared rather than reset'); compactionBudgetExpect($call['options']['request_deadline_ns'] === 1000000000000 + $initialMs * 1000000, 'default, capped, seconds, and inherited absolute deadlines reach the transport'); } } $timedOut = 0; compactionBudgetReset(); $expiryOptions = ['request_deadline_ns' => 1000000000000, 'on_timeout' => static function () use (&$timedOut): void { ++$timedOut; }]; compactionBudgetFailure(static fn () => PatientAiReportLogic::compactSourceForPrompt('qwen', $source, $expiryOptions), 408, 'Patient context preparation timed out'); compactionBudgetExpect($timedOut === 1 && DifyChatService::$calls === [], 'expired request never starts a chunk and notifies timeout'); compactionBudgetFailure(static fn () => PatientAiReportLogic::compactSourceForPrompt('qwen', $source, ['request_deadline_ns' => 1000000000000, 'on_timeout' => static function (): void { throw new RuntimeException('PRIVATE-TIMEOUT'); }]), 408, 'Patient context preparation timed out'); compactionBudgetReset(); compactionBudgetFailure(static fn () => PatientAiReportLogic::compactSourceForPrompt('qwen', $source, ['request_deadline_ns' => 1000000000000, 'should_abort' => static fn (): bool => true]), 0, 'Context operation cancelled'); compactionBudgetExpect(DifyChatService::$calls === [], 'user cancellation retains its existing classification'); foreach ([static fn (): bool => false, static function (): bool { throw new RuntimeException('PRIVATE-GATE'); }] as $notReady) { compactionBudgetReset(); compactionBudgetFailure(static fn () => PatientAiReportLogic::compactSourceForPrompt('qwen', $source, ['can_start_attempt' => $notReady]), 409, 'Patient context cleanup pending'); compactionBudgetExpect(DifyChatService::$calls === [], 'pending or failed cleanup gate never starts evidence'); } compactionBudgetReset(); compactionBudgetFailure(static fn () => PatientAiReportLogic::compactSourceForPrompt('qwen', $source, ['can_start_attempt' => static fn (): bool => count(DifyChatService::$calls) === 0]), 409, 'Patient context cleanup pending'); compactionBudgetExpect(count(DifyChatService::$calls) === 1, 'a pending first task blocks every subsequent chunk'); $timedOut = 0; $timeoutTask = ''; compactionBudgetReset(static function (array $inputs, array $options): array { ($options['on_timeout'])('synthetic-task'); return ['ok' => false, 'error_code' => 'UPSTREAM_TIMEOUT', 'error' => 'PRIVATE-UPSTREAM']; }); $timeoutOptions = ['on_timeout' => static function (string $task) use (&$timedOut, &$timeoutTask): void { ++$timedOut; $timeoutTask = $task; }]; compactionBudgetFailure(static fn () => PatientAiReportLogic::compactSourceForPrompt('qwen', $source, $timeoutOptions), 408, 'Patient context preparation timed out'); compactionBudgetExpect($timedOut === 1 && $timeoutTask === 'synthetic-task' && count(DifyChatService::$calls) === 1, 'Dify timeout receives its task and is reported once without launching another source chunk'); foreach (['UPSTREAM_TIMEOUT' => 408, 'UPSTREAM_CLEANUP_PENDING' => 409] as $failureCode => $exceptionCode) { compactionBudgetReset(static fn (): array => ['ok' => false, 'error_code' => $failureCode]); compactionBudgetFailure(static fn () => PatientAiReportLogic::compactSourceForPrompt('qwen', $source), $exceptionCode, $exceptionCode === 408 ? 'Patient context preparation timed out' : 'Patient context cleanup pending'); compactionBudgetExpect(count(DifyChatService::$calls) === 1, 'upstream terminal failures never advance to another source chunk'); } compactionBudgetReset(static function (): array { $GLOBALS['compactionNowNs'] += 1000000000; return ['ok' => true, 'content' => 'late incomplete evidence']; }); compactionBudgetFailure(static fn () => PatientAiReportLogic::compactSourceForPrompt('qwen', $source, ['timeout' => 1]), 408, 'Patient context preparation timed out'); compactionBudgetExpect(count(DifyChatService::$calls) === 1, 'late success cannot advance past the shared deadline'); compactionBudgetReset(static fn (): array => ['ok' => false, 'error_code' => 'UPSTREAM_REJECTED', 'error' => 'PRIVATE-FAILURE']); compactionBudgetFailure(static fn () => AiContextCompactionCache::remember('synthetic-scope', 'qwen', $source, static fn (): array => PatientAiReportLogic::compactSourceForPrompt('qwen', $source)), 0, 'Patient context chunk analysis failed'); compactionBudgetExpect(count(DifyChatService::$calls) === 1 && Cache::$items === [], 'a failed initial source fragment aborts the complete-only cache producer'); $reductionSource = str_repeat('s', 120001); compactionBudgetReset(static function (array $inputs): array { $GLOBALS['compactionNowNs'] += 100000000; return ['ok' => true, 'content' => $inputs['analysis_stage'] === 'evidence_chunk' ? str_repeat('e', 70000) : 'reduced complete evidence']; }); $reduced = PatientAiReportLogic::compactSourceForPrompt('qwen', $reductionSource, ['timeout_ms' => 1500]); compactionBudgetExpect($reduced['reduction_rounds'] === 1 && count(DifyChatService::$calls) === 4, 'oversized evidence is fully reduced without discarding chunks'); $reductionCovered = ''; foreach (DifyChatService::$calls as $index => $call) { compactionBudgetExpect($call['options']['timeout_ms'] === 1500 - $index * 100, 'reduction continues the original source deadline'); if ($index >= 2) $reductionCovered .= compactionBudgetFragment($call, 'EVIDENCE_SUMMARY_FRAGMENT'); } $expectedSummaries = json_encode([ ['part' => 1, 'total' => 2, 'summary' => str_repeat('e', 70000)], ['part' => 2, 'total' => 2, 'summary' => str_repeat('e', 70000)], ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); compactionBudgetExpect($reductionCovered === $expectedSummaries, 'reduction submits every exact intermediate evidence byte'); foreach (['timeout', 'cancel', 'cleanup', 'failure'] as $mode) { $cancelled = false; $timedOut = 0; compactionBudgetReset(static function (array $inputs) use ($mode, &$cancelled): array { if ($inputs['analysis_stage'] === 'evidence_chunk') return ['ok' => true, 'content' => str_repeat('e', 70000)]; if ($mode === 'cancel') $cancelled = true; return ['ok' => false, 'error_code' => $mode === 'timeout' ? 'UPSTREAM_TIMEOUT' : ($mode === 'cleanup' ? 'UPSTREAM_CLEANUP_PENDING' : 'UPSTREAM_REJECTED')]; }); $failureOptions = ['should_abort' => static function () use (&$cancelled): bool { return $cancelled; }, 'on_timeout' => static function () use (&$timedOut): void { ++$timedOut; }]; $run = static fn (): array => AiContextCompactionCache::remember('synthetic-scope', 'qwen', $reductionSource, static fn (): array => PatientAiReportLogic::compactSourceForPrompt('qwen', $reductionSource, $failureOptions)); compactionBudgetFailure($run, $mode === 'timeout' ? 408 : ($mode === 'cleanup' ? 409 : 0), ['timeout' => 'Patient context preparation timed out', 'cancel' => 'Context operation cancelled', 'cleanup' => 'Patient context cleanup pending', 'failure' => 'Patient context reduction failed'][$mode]); compactionBudgetExpect(count(DifyChatService::$calls) === 3 && Cache::$items === [] && $timedOut === ($mode === 'timeout' ? 1 : 0), 'failed, cancelled, or timed-out reductions never cache partial evidence or start another attempt'); } echo "Patient context compaction shared budget / progress / complete cache: {$assertions} assertions passed (offline stubs).\n"; }