instance('config', $config); $checks = 0; $expect = static function (bool $ok, string $why) use (&$checks): void { if (!$ok) { throw new RuntimeException($why); } $checks++; }; $expectError = static function (callable $call, string $code) use ($expect): void { try { $call(); } catch (AudioError $error) { $expect($error->errorCode === $code, $code); return; } throw new RuntimeException('Expected ' . $code); }; $legacy = ['base_url' => 'https://legacy.invalid/v1', 'models' => ['qwen' => ['api_key' => 'synthetic-legacy-key', 'name' => 'must-not-inherit']]]; $dify = Provider::resolve('qwen', [], $legacy); $expect($dify['driver'] === 'dify' && $dify['base_url'] === $legacy['base_url'] && $dify['model'] === '', 'legacy Dify app identity allowed without fake internal model'); $slot = ['driver' => 'openai_audio', 'base_url' => 'https://audio.invalid/v1/chat/completions', 'api_key' => 'synthetic-provider-key', 'model' => 'configurable-audio-model', 'label' => 'Audio label']; $settings = ['enabled' => false, 'audio_verified' => true, 'verified_profiles' => ['qwen'], 'providers' => ['qwen' => $slot]]; $resolved = Provider::resolve('qwen', $settings, $legacy); $expect($resolved['base_url'] === 'https://audio.invalid/v1' && $resolved['model'] === $slot['model'], 'explicit selected driver model and normalized endpoint'); $settings['providers']['qwen']['verified_fingerprint'] = $resolved['fingerprint']; $config->set($settings, 'followup_audio'); $config->set($legacy, 'prescription_ai'); $expect(Provider::isVerified('qwen'), 'verified state independent of disabled operational switch'); $expect(Provider::publicModels() === [['value' => 'qwen', 'label' => 'Audio label']], 'only verified slots exposed'); $status = Provider::status('qwen', $settings, $legacy); $expect(array_keys($status) === ['configured', 'profile', 'code', 'application_fingerprint'], 'safe exact status keys'); $public = json_encode([$status, Provider::publicModels()]); $expect(!str_contains($public, 'synthetic-provider-key') && !str_contains($public, 'audio.invalid'), 'public response contains no endpoint or key'); foreach (['driver' => 'dify', 'base_url' => 'https://changed.invalid/v1', 'model' => 'another-model', 'api_key' => 'other-synthetic-key'] as $field => $value) { $changed = $settings; $changed['providers']['qwen'][$field] = $value; $expect(!Provider::verified('qwen', $changed, $legacy), 'identity change invalidates ' . $field); } $changed = $settings; $changed['providers']['qwen']['label'] = 'Renamed display'; $expect(Provider::verified('qwen', $changed, $legacy), 'display-only label does not change request identity'); $changed = $settings; $changed['providers']['qwen']['label'] = ''; $expect(Provider::resolve('qwen', $changed, $legacy)['label'] === $slot['model'], 'empty label falls back to configured model'); $changed = $settings; $changed['providers']['qwen']['base_url'] = 'http://audio.invalid/v1'; $changed['providers']['qwen']['verified_fingerprint'] = Provider::resolve('qwen', $changed, $legacy)['fingerprint']; $changed['allow_insecure_synthetic'] = true; $expect(!Provider::verified('qwen', $changed, $legacy), 'HTTP never becomes production-verified'); foreach (['model', 'base_url', 'api_key'] as $field) { $changed = $settings; $changed['providers']['qwen'][$field] = ''; $expectError(static fn () => Provider::resolve('qwen', $changed, $legacy), 'CONFIG_MISSING'); } foreach (['ftp://audio.invalid/v1', 'https://user:password@audio.invalid/v1', 'https://audio.invalid/v1?token=x', "https://audio.invalid/v1\n", 'https://audio.invalid/v1#fragment'] as $url) { $changed = $settings; $changed['providers']['qwen']['base_url'] = $url; $expectError(static fn () => Provider::resolve('qwen', $changed, []), 'CONFIG_INVALID'); } $changed = $settings; $changed['providers']['qwen']['driver'] = 'automatic'; $expectError(static fn () => Provider::resolve('qwen', $changed, []), 'CONFIG_INVALID'); $changed = $settings; $changed['providers']['qwen']['verified_fingerprint'] = ''; $expect(!Provider::verified('qwen', $changed, $legacy), 'old unbound flags do not enable audio'); $expect(Provider::status('invalid', $settings, $legacy)['code'] === 'INVALID_PROFILE', 'logical slot allowlist retained'); // The configured default controls new-upload choices without rebinding historical slots. $ordering = $settings; $ordering['verified_profiles'] = ['qwen', 'openai']; $ordering['providers']['openai'] = $slot; $ordering['providers']['openai']['label'] = 'GPT extraction'; $ordering['providers']['openai']['verified_fingerprint'] = Provider::resolve('openai', $ordering, [])['fingerprint']; $ordering['profile'] = 'openai'; $config->set($ordering, 'followup_audio'); $expect(array_column(Provider::readyModels(), 'value') === ['openai', 'qwen'], 'configured GPT default comes first while historical Qwen remains ready'); $expect(Provider::resolve('qwen')['fingerprint'] === $resolved['fingerprint'], 'default selection does not rebind the historical provider identity'); $ordering['profile'] = 'qwen'; $config->set($ordering, 'followup_audio'); $expect(array_column(Provider::readyModels(), 'value') === ['qwen', 'openai'], 'configured Qwen priority remains supported'); $ordering['profile'] = 'unknown'; $config->set($ordering, 'followup_audio'); $expect(array_column(Provider::readyModels(), 'value') === ['qwen', 'openai'], 'invalid default never inserts an unrecognized profile'); $ordering['profile'] = 'openai'; $ordering['providers']['openai']['verified_fingerprint'] = ''; $config->set($ordering, 'followup_audio'); $expect(array_column(Provider::readyModels(), 'value') === ['qwen'], 'sorting does not manufacture readiness for an unverified profile'); $config->set($settings, 'followup_audio'); echo 'FOLLOWUP_AUDIO_PROVIDER_CONFIG assertions=' . $checks . ' PASS explicit_driver=1 model_configurable=1 exact_fingerprint=1 https_verified=1 secrets_hidden=1' . PHP_EOL;