Files
zyt/server/tests/FollowupAudioDateEvidenceTest.php
T

117 lines
9.5 KiB
PHP

<?php
declare(strict_types=1);
/** Deterministic synthetic policy tests: not ASR/model accuracy or clinical acceptance. */
require dirname(__DIR__) . '/vendor/autoload.php';
use app\common\service\followupaudio\FollowupAudioPolicy as Policy;
if (($baseline = getenv('FOLLOWUP_AUDIO_POLICY_OVERRIDE')) !== false && $baseline !== '') { require $baseline; }
$cases = [];
$failures = [];
$run = static function (string $id, string $transcript, array $extra, ?string $expectedDate,
bool $expectedReview, string $recordedAt = '2026-03-01 10:00:00', ?string $quote = null) use (&$cases, &$failures): array {
$item = array_replace(['kind' => 'blood', 'values' => ['systolic_pressure' => 120], 'record_date' => null,
'record_time' => '07:45', 'date_text' => '', 'time_text' => '七点四十五分',
'time_estimated' => false, 'evidence' => [['text' => $quote ?? $transcript]]], $extra);
$actual = Policy::normalizeExtraction(['transcript' => $transcript, 'summary' => '合成测试', 'items' => [$item]], $recordedAt);
$observed = $actual['items'][0] ?? null;
$pass = $observed !== null && $observed['record_date'] === $expectedDate && $observed['needs_review'] === $expectedReview;
$cases[] = ['id' => $id, 'input' => ['transcript' => $transcript, 'item' => $item, 'recorded_at' => $recordedAt],
'expected' => ['record_date' => $expectedDate, 'needs_review' => $expectedReview], 'actual' => $actual, 'passed' => $pass];
if (!$pass) { $failures[] = $id; }
return $actual;
};
$yesterday = '昨天七点四十五分收缩压一百二十。';
$run('omitted_date_text_cannot_hide_wrong_explicit', $yesterday, ['record_date' => '2026-02-27'], null, true);
$run('wrong_date_text_cannot_hide_wrong_explicit', $yesterday, ['date_text' => '前天', 'record_date' => '2026-02-27'], null, true);
$run('wrong_date_text_without_explicit_is_unresolved', $yesterday, ['date_text' => '今天'], null, true);
$run('legacy_relative_conflict_stays_unresolved', $yesterday, ['date_text' => '昨天', 'record_date' => '2026-03-01'], null, true);
$run('omitted_calendar_derived_from_verbatim_yesterday', $yesterday, [], '2026-02-28', false);
$run('day_before_yesterday_crosses_year', '前天七点四十五分收缩压一百二十。', [], '2025-12-30', false, '2026-01-01 10:00:00');
$run('yesterday_leap_day', $yesterday, [], '2024-02-29', false, '2024-03-01 10:00:00');
$run('relative_not_at_start', '我昨天七点四十五分收缩压一百二十。', [], '2026-02-28', false);
$run('shortened_quote_cannot_omit_relative_prefix', $yesterday, ['record_date' => '2026-02-27'], null, true,
'2026-03-01 10:00:00', '收缩压一百二十');
$run('legacy_joined_utterances_cannot_supply_missing_date', 'synthetic yesterday noon synthetic no date', [], null, true,
'2026-03-01 10:00:00', 'synthetic no date');
$run('quoted_date_does_not_take_other_event_date', '前天收缩压一百三十。' . $yesterday, [], '2026-02-28', false,
'2026-03-01 10:00:00', $yesterday);
$run('fuzzy_range_omitted_date_text', '最近几天七点四十五分收缩压一百二十。', ['record_date' => '2026-03-01'], null, true);
$range = $run('multiple_days_are_not_one_day', '昨天或前天七点四十五分收缩压一百二十。', ['record_date' => '2026-02-28'], null, true);
$pass = str_contains($range['items'][0]['date_text'], '昨天或前天');
$cases[] = ['id' => 'range_original_wording_retained', 'actual' => $range['items'][0]['date_text'], 'passed' => $pass];
if (!$pass) { $failures[] = 'range_original_wording_retained'; }
$run('date_absent_never_defaults_to_today', '七点四十五分收缩压一百二十。', [], null, true);
$run('unquoted_relative_cannot_ground_date', '七点四十五分收缩压一百二十。', [], null, true,
'2026-03-01 10:00:00', $yesterday);
$run('explicit_calendar_contradicts_evidence', '2026年2月28日七点四十五分收缩压一百二十。', ['record_date' => '2026-02-27'], null, true);
$run('family_quote_stays_manual', '我父亲昨天七点四十五分收缩压一百二十。', [], '2026-02-28', true);
$run('question_stays_manual', '昨天七点四十五分收缩压一百二十吗?', [], '2026-02-28', true);
$run('negation_stays_manual', '昨天七点四十五分收缩压不是一百二十。', [], '2026-02-28', true);
$run('uncertain_numeric_stays_manual', '昨天七点四十五分收缩压大概一百二十左右。', [], '2026-02-28', true);
$run('estimated_numeric_stays_manual', '昨天七点四十五分收缩压估计一百二十。', [], '2026-02-28', true);
$run('historical_medication_stays_manual', '以前用药甲,现在停用了。', ['kind' => 'diagnosis', 'values' => ['current_medications' => '药甲']], null, true);
$run('prior_year_not_current_day', '去年检查过收缩压一百二十。', ['date_text' => '今天', 'record_date' => '2026-03-01'], null, true);
$run('bare_year_is_not_specific_day', '2024年检查过收缩压一百二十。', ['date_text' => '', 'record_date' => '2024-03-01'], null, true);
$run('weekday_only_cannot_invent_day', '周五七点四十五分收缩压一百二十。', ['date_text' => '周五', 'record_date' => '2024-02-29'], null, true);
$run('weekday_conflicts_with_calendar', '2024年2月29日周五七点四十五分收缩压一百二十。', ['record_date' => '2024-02-29'], null, true);
$run('weekday_agrees_with_calendar', '2024年2月29日周四七点四十五分收缩压一百二十。', ['record_date' => '2024-02-29'], '2024-02-29', false);
$segment = ['id' => 'seg_2', 'start_ms' => 300000, 'end_ms' => 480000, 'text' => $yesterday];
$evidence = ['segment_id' => $segment['id'], 'text' => $yesterday, 'start_ms' => $segment['start_ms'],
'end_ms' => $segment['end_ms'], 'position_type' => 'segment'];
$canonical = ['transcript' => $yesterday, 'transcript_segments' => [$segment], 'summary' => '合成片段',
'items' => [['kind' => 'blood', 'values' => ['systolic_pressure' => 120], 'date_text' => '昨天',
'record_time' => '07:45', 'evidence' => [$evidence]]]];
$normalized = Policy::normalizeExtraction($canonical, '2026-03-01 10:00:00');
$pass = ($normalized['transcript_segments'] ?? null) === [$segment] && $normalized['items'][0]['evidence'][0] === $evidence;
$cases[] = ['id' => 'immutable_chunk_boundaries_preserved', 'input' => $canonical, 'actual' => $normalized, 'passed' => $pass];
if (!$pass) { $failures[] = 'immutable_chunk_boundaries_preserved'; }
$twice = Policy::normalizeExtraction($normalized, '2026-03-01 10:00:00');
$pass = $normalized === $twice;
$cases[] = ['id' => 'renormalization_retains_ids_and_metadata', 'actual' => $twice, 'passed' => $pass];
if (!$pass) { $failures[] = 'renormalization_retains_ids_and_metadata'; }
foreach ([
'unknown_segment' => ['segment_id' => 'seg_unknown'],
'invented_word_timestamp' => ['start_ms' => 320000, 'end_ms' => 325000],
'invented_word_alignment_type' => ['position_type' => 'word'],
'missing_boundary' => ['end_ms' => null],
'quote_not_in_cited_segment' => ['text' => '今天原话没有此句'],
] as $id => $bad) {
$input = $canonical;
$input['items'][0]['evidence'][0] = array_replace($evidence, $bad);
$actual = Policy::normalizeExtraction($input, '2026-03-01 10:00:00');
$pass = $actual['items'] === [] && count($actual['uncertainties']) === 1;
$cases[] = ['id' => $id, 'input' => $input, 'actual' => $actual, 'passed' => $pass];
if (!$pass) { $failures[] = $id; }
}
$input = $canonical;
$input['transcript'] = $input['transcript_segments'][0]['text'] = $input['items'][0]['evidence'][0]['text'] = '收缩压一百二十。';
$input['items'][0]['record_date'] = '2026-03-01';
$input['items'][0]['date_text'] = '今天';
$actual = Policy::normalizeExtraction($input, '2026-03-01 10:00:00');
$pass = $actual['items'][0]['record_date'] === null && $actual['items'][0]['needs_review'];
$cases[] = ['id' => 'strict_pipeline_rejects_ungrounded_model_day', 'input' => $input, 'actual' => $actual, 'passed' => $pass];
if (!$pass) { $failures[] = 'strict_pipeline_rejects_ungrounded_model_day'; }
$input['transcript_segments'][] = ['id' => 'seg_3', 'start_ms' => 480000, 'end_ms' => 600000,
'text' => '昨天收缩压一百二十。'];
$input['transcript'] .= "\n" . $input['transcript_segments'][1]['text'];
$input['items'][0]['record_date'] = null;
$input['items'][0]['date_text'] = '';
$actual = Policy::normalizeExtraction($input, '2026-03-01 10:00:00');
$pass = $actual['items'][0]['record_date'] === null && $actual['items'][0]['needs_review'];
$cases[] = ['id' => 'repeated_quote_cannot_borrow_date_from_other_segment', 'input' => $input, 'actual' => $actual, 'passed' => $pass];
if (!$pass) { $failures[] = 'repeated_quote_cannot_borrow_date_from_other_segment'; }
$withSilence = $canonical;
array_unshift($withSilence['transcript_segments'], ['id' => 'silent_chunk', 'start_ms' => 0, 'end_ms' => 10000, 'text' => '']);
$actual = null;
try { $actual = Policy::normalizeExtraction($withSilence, '2026-03-01 10:00:00'); } catch (DomainException $error) { /* observed rejection belongs in this regression */ }
$pass = $actual !== null && count($actual['items']) === 1 && ($actual['transcript_segments'][0]['text'] ?? null) === '';
$cases[] = ['id' => 'silent_asr_chunk_preserved_without_invented_text', 'passed' => $pass];
if (!$pass) { $failures[] = 'silent_asr_chunk_preserved_without_invented_text'; }
echo json_encode(['suite' => 'followup-audio-date-evidence', 'synthetic_only' => true,
'cases' => $cases, 'passed' => count($cases) - count($failures), 'total' => count($cases), 'failures' => $failures],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . PHP_EOL;
exit($failures === [] ? 0 : 1);