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

90 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
namespace app\adminapi\service {
// Isolate delivery mechanics from peers being changed independently.
final class TcmClinicalFactStateGuard {
public static function validate(array $ledger, string $text): array {
return ['valid' => !str_contains($text, '[FACT_REJECT]'), 'issues' => [['code' => 'FACT_TEST_FAILURE', 'source_id' => 's1']]];
}
}
final class TcmClinicalEvidenceContract {
public static function validate(array $index, string $text, bool $complete = false): array {
return ['valid' => !str_contains($text, '[SOURCE_REJECT]'), 'issues' => [['code' => 'SOURCE_TEST_FAILURE', 'source_id' => 's1']]];
}
}
}
namespace {
require dirname(__DIR__) . '/app/adminapi/service/TcmLongReportContract.php';
require dirname(__DIR__) . '/app/adminapi/service/TcmHerbalPremiseGuard.php';
require dirname(__DIR__) . '/app/adminapi/service/TcmClinicalReportStream.php';
use app\adminapi\service\TcmClinicalReportStream as Stream;
use app\adminapi\service\TcmLongReportContract as Shape;
$checks = 0;
function checkpointExpect(bool $ok, string $message): void { ++$GLOBALS['checks']; if (!$ok) throw new RuntimeException($message); }
$query = '<USER_QUESTION>生成1800至2200字完整中医辨证长报告。</USER_QUESTION>';
$engineering = '本段用于离线交付机制验证,文字不代表患者事实、诊断或处方,真实临床证据需独立审核。';
$first = "一、四诊\n\n望诊:未记录。\n\n闻诊:未记录。\n\n问诊:工程资料单列,本段只演示来源整理,所有临床结论均需独立核对,不以传输测试文字代替真实患者信息。\n\n切诊:其他触诊未记录,脉象见独立脉象项。\n\n舌象:工程来源仅演示独立记录。\n\n脉象:工程来源仅演示独立记录。\n\n";
$prefix = $first;
foreach (['二、八纲', '三、气血津液与脏腑', '四、证名', '五、治法'] as $heading) $prefix .= $heading . "\n\n" . str_repeat($engineering, 3) . "\n\n";
function checkpointTail(string $prefix, int $characters, string $intro = '主方方向:暂不列方,继续补采及监测。'): string {
$tail = "六、中药方药及复核监测\n\n" . $intro . "\n\n" . str_repeat($GLOBALS['engineering'], 3);
$tail .= str_repeat('核', max(0, $characters - mb_strlen($prefix . $tail, 'UTF-8')));
return $tail;
}
$tail = checkpointTail($prefix, 2000);
$whole = $prefix . $tail;
checkpointExpect(Shape::validate($whole, $query)['valid'], 'Engineering fixture satisfies the unchanged full range and section-body contract.');
$visible = ''; $rejected = [];
$stream = new Stream([], static function (string $s) use (&$visible): bool { $visible .= $s; return true; },
static function ($s, $issues) use (&$rejected): void { $rejected[] = [$s, $issues]; }, '', null, [], $query);
foreach (str_split($whole, 1) as $byte) checkpointExpect($stream->push($byte), 'One-byte UTF-8/header boundaries remain buffered correctly.');
checkpointExpect($visible === $prefix && $stream->acceptedContent() === $prefix, 'No byte of heading six or its tail is visible before completion.');
checkpointExpect($stream->finish() && $visible === $whole && $stream->acceptedContent() === $whole, 'Valid original model bytes flush unchanged after whole-report validation.');
checkpointExpect($rejected === [], 'Valid tail does not invoke repair feedback.');
$visible = ''; $bad = checkpointTail($prefix, 2000, '主方方向:甲乙散。关键方证证据不足,限医师复核。');
$stream = new Stream([], static function ($s) use (&$visible): bool { $visible .= $s; return true; },
static function ($s, $issues) use (&$rejected): void { $rejected = [$s, $issues]; }, '', null, [], $query);
checkpointExpect($stream->push($prefix), 'First five sections continue streaming.');
checkpointExpect(!$stream->push($bad . "\n\n") && $visible === $prefix, 'Complete contradiction paragraph rejects before any section-six byte is delivered.');
checkpointExpect(str_starts_with($rejected[0], '六、') && in_array('REPORT_FINAL_SECTION_UNDELIVERED', array_column($rejected[1], 'code'), true), 'Repair feedback includes the whole undelivered sixth-section draft.');
foreach ([2221, 1750] as $length) {
$visible = ''; $draft = checkpointTail($prefix, $length);
$stream = new Stream([], static function ($s) use (&$visible): bool { $visible .= $s; return true; }, null, '', null, [], $query);
checkpointExpect($stream->push($prefix . $draft), 'A shape-invalid final draft is privately buffered until completion.');
checkpointExpect(!$stream->finish() && $visible === $prefix && !$stream->finalContract()['valid'], 'Original lower/upper length failures never expose the buffered tail.');
}
$earlyPrefix = substr($prefix, 0, strpos($prefix, '三、'));
$visible = ''; $rejected = [];
$stream = new Stream([], static function ($s) use (&$visible): bool { $visible .= $s; return true; },
static function ($s, $issues) use (&$rejected): void { $rejected = [$s, $issues]; }, '', null, [], $query);
checkpointExpect($stream->push($earlyPrefix) && !$stream->finish() && $visible === $earlyPrefix,
'A naturally truncated first-two-section prefix is retained exactly and fails unchanged whole shape.');
checkpointExpect($rejected[0] === '' && in_array('REPORT_FINAL_CONTRACT_INVALID', array_column($rejected[1], 'code'), true)
&& !in_array('REPORT_FINAL_SECTION_UNDELIVERED', array_column($rejected[1], 'code'), true),
'No sixth-section restart marker is fabricated before a sixth heading was actually held.');
foreach (['[FACT_REJECT]', '[SOURCE_REJECT]'] as $marker) {
$visible = '';
$stream = new Stream([], static function ($s) use (&$visible): bool { $visible .= $s; return true; }, null, '', null, [], $query);
checkpointExpect(!$stream->push($prefix . "六、方药\n\n" . $marker . "\n\n") && $visible === $prefix, 'Existing fact/provenance rejection still protects the entire undelivered tail.');
}
$visible = ''; $abort = false;
$stream = new Stream([], static function ($s) use (&$visible): bool { $visible .= $s; return true; }, null, '', null, [], $query, static fn (): bool => $GLOBALS['abort']);
checkpointExpect($stream->push($whole), 'A valid tail can wait without client delivery.');
$abort = true;
checkpointExpect(!$stream->finish() && $visible === $prefix && !$stream->rejected(), 'Disconnect/cancellation before flush cannot release the tail or become a clinical repair.');
$visible = '';
$stream = new Stream([], static function ($s) use (&$visible): bool { if (str_starts_with($s, '六、')) return false; $visible .= $s; return true; }, null, '', null, [], $query);
checkpointExpect($stream->push($whole) && !$stream->finish() && $visible === $prefix && $stream->acceptedContent() === $prefix && !$stream->rejected(), 'Callback refusal during atomic tail flush retains exact accepted-prefix semantics.');
$visible = '';
$stream = new Stream([], static function ($s) use (&$visible): bool { $visible .= $s; return true; }, null, '', null, [], $query);
checkpointExpect($stream->push($prefix . "六、方药\n\n") && !$stream->push(str_repeat('x', 100001)), 'Deferred bytes count toward the original100k bound.');
checkpointExpect($visible === $prefix && $stream->issues()[0]['code'] === 'REPORT_BOUND_EXCEEDED', 'Bound overflow never flushes private tail bytes.');
$visible = '';
$ordinary = new Stream([], static function ($s) use (&$visible): bool { $visible .= $s; return true; });
checkpointExpect($ordinary->push("六、方药\n\n主方方向:甲乙散。关键方证证据不足。\n\n") && str_contains($visible, '甲乙散'), 'Non-opted ordinary streams retain the prior paragraph delivery behavior.');
echo "TCM final-section checkpoint: {$checks} assertions passed (isolated transport mechanics; no clinical-quality claim).\n";
}