feat: configure audio providers and add local sample acceptance

This commit is contained in:
2026-10-07 18:15:32 +08:00
parent 70be2fc70f
commit fea33285e8
21 changed files with 1103 additions and 131 deletions
+25 -7
View File
@@ -6,7 +6,7 @@ namespace app\common\service\followupaudio {
/** Command-state test double. Actual HTTP/audio behavior is covered by FollowupAudioDifyTest. */
final class FollowupAudioDify {
public static string $mode = 'uncertain';
public static string $fingerprint = 'synthetic-application-one';
public static string $fingerprint = '1111111111111111111111111111111111111111111111111111111111111111';
public static int $calls = 0;
public function configurationStatus(string $profile): array { return ['configured' => true, 'profile' => $profile, 'code' => 'OK', 'application_fingerprint' => self::$fingerprint]; }
public function probe(string $path, array $fixture, string $profile, callable $heartbeat): array {
@@ -24,6 +24,8 @@ namespace {
use app\command\FollowupAudioProbe as Probe;
use think\console\Input;
use think\console\Output;
new think\App(); // No initialization, real config or DB.
$safeConfig = new think\Config(); think\Container::getInstance()->instance('config', $safeConfig);
$directory = sys_get_temp_dir() . '/followup-audio-probe-state-' . bin2hex(random_bytes(6));
mkdir($directory, 0700, true);
$directory = realpath($directory);
@@ -52,26 +54,42 @@ namespace {
$expect($code === 1 && Dify::$calls === 0 && str_contains($stdout, 'SYNTHETIC_ACK_REQUIRED'), 'explicit synthetic acknowledgement required');
[$code, $stdout] = $run('qwen');
$expect($code === 2 && Dify::$calls === 1 && str_contains($stdout, 'needs_reconciliation'), 'unknown result stops next cases');
$report = json_decode(file_get_contents($directory . '/probe-qwen.json'), true);
$report = json_decode(file_get_contents($directory . '/probe-qwen-' . Dify::$fingerprint . '.json'), true);
$expect($report['cases']['short']['upstream_started_at'] > 0, 'intent persisted before request');
[$code, $stdout] = $run('qwen');
$expect($code === 2 && Dify::$calls === 1 && str_contains($stdout, 'NO_RESUBMISSION'), 'resume never recharges unknown result');
$currentQwenPath = $directory . '/probe-qwen-' . Dify::$fingerprint . '.json';
$legacyQwenPath = $directory . '/probe-qwen.json';
rename($currentQwenPath, $legacyQwenPath);
$legacyBytes = file_get_contents($legacyQwenPath);
[$code, $stdout] = $run('qwen');
$expect($code === 2 && Dify::$calls === 1 && str_contains($stdout, 'NO_RESUBMISSION'), 'matching legacy identity remains authoritative for unknown requests');
$expect(file_get_contents($legacyQwenPath) === $legacyBytes, 'legacy unknown evidence retained unchanged');
$legacyReport = json_decode($legacyBytes, true);
$legacyReport['configuration']['application_fingerprint'] = hash('sha256', "https://synthetic.invalid/v1\0synthetic-key");
file_put_contents($legacyQwenPath, json_encode($legacyReport));
$safeConfig->set(['providers' => ['qwen' => ['driver' => 'dify', 'base_url' => 'https://synthetic.invalid/v1', 'api_key' => 'synthetic-key']]], 'followup_audio');
[$code, $stdout] = $run('qwen');
$expect($code === 2 && Dify::$calls === 1 && str_contains($stdout, 'NO_RESUBMISSION'), 'legacy Dify hash upgrade cannot resubmit same-app unknown');
$safeConfig->set([], 'followup_audio');
Dify::$mode = 'success';
[$code, $stdout] = $run('openai');
$expect($code === 0 && Dify::$calls === 4, 'three lengths passed sequentially');
$bytes = file_get_contents($directory . '/probe-openai.json');
$bytes = file_get_contents($directory . '/probe-openai-' . Dify::$fingerprint . '.json');
$report = json_decode($bytes, true);
$expect($report['audio_verified'] && count($report['cases']) === 3, 'all three required before verification report');
$expect(!str_contains($bytes . $stdout, 'PRIVATE_TRANSCRIPT_NEVER_PRINTED'), 'no transcript in output or report');
[$code, $stdout] = $run('openai');
$expect($code === 0 && Dify::$calls === 4 && substr_count($stdout, 'retained_passed') === 3, 'passed gate results reused without resending');
Dify::$fingerprint = 'synthetic-application-two';
$oldPath = $directory . '/probe-openai-' . Dify::$fingerprint . '.json';
Dify::$fingerprint = '2222222222222222222222222222222222222222222222222222222222222222';
[$code, $stdout] = $run('openai');
$expect($code === 1 && Dify::$calls === 4 && str_contains($stdout, 'PROBE_APPLICATION_CHANGED'), 'new application cannot inherit old gate');
$expect(file_get_contents($directory . '/probe-openai.json') === $bytes, 'application mismatch leaves original evidence unchanged');
$expect($code === 0 && Dify::$calls === 7 && !str_contains($stdout, 'retained_passed'), 'new application executes its own gate');
$expect(file_get_contents($oldPath) === $bytes, 'application mismatch leaves original evidence unchanged');
file_put_contents($directory . '/short.mp3', 'tampered');
[$code, $stdout] = $run('qwen');
$expect($code === 1 && Dify::$calls === 4 && str_contains($stdout, 'SYNTHETIC_FIXTURE_REQUIRED'), 'tampered fixture cannot run');
$expect($code === 1 && Dify::$calls === 7 && str_contains($stdout, 'SYNTHETIC_FIXTURE_REQUIRED'), 'tampered fixture cannot run');
echo 'FOLLOWUP_AUDIO_PROBE_COMMAND assertions=' . $checks . ' PASS durable_no_resend=1 retained_results=1 app_identity=1' . PHP_EOL;
} finally {
foreach (glob($directory . '/*') ?: [] as $file) { if (is_file($file)) { unlink($file); } }