80 lines
6.2 KiB
PHP
80 lines
6.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/** No app initialization, .env, network or real credentials. */
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
require dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php';
|
|
use app\common\service\followupaudio\FollowupAudioProviderConfig as Provider;
|
|
use app\common\service\followupaudio\FollowupAudioException as AudioError;
|
|
new think\App();
|
|
$config = new think\Config();
|
|
think\Container::getInstance()->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;
|