76 lines
4.9 KiB
PHP
76 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/** No autoloader, app, .env, filesystem inputs, network or DB: public gate must fail closed before I/O. */
|
|
namespace think\facade {
|
|
final class Db
|
|
{
|
|
public static int $calls = 0;
|
|
public static function __callStatic(string $name, array $args): never
|
|
{
|
|
self::$calls++;
|
|
throw new \RuntimeException('DATABASE_MUST_NOT_BE_USED');
|
|
}
|
|
}
|
|
}
|
|
namespace {
|
|
$settings = [];
|
|
function config(string $key, $default = null)
|
|
{
|
|
global $settings;
|
|
$value = $settings;
|
|
foreach (explode('.', $key) as $part) {
|
|
if (!is_array($value) || !array_key_exists($part, $value)) { return $default; }
|
|
$value = $value[$part];
|
|
}
|
|
return $value;
|
|
}
|
|
$root = dirname(__DIR__) . '/app/common/service/followupaudio/';
|
|
foreach (['Exception', 'ProviderConfig', 'Gate', 'Store'] as $class) { require $root . 'FollowupAudio' . $class . '.php'; }
|
|
require dirname(__DIR__) . '/app/adminapi/logic/tcm/FollowupAudioLogic.php';
|
|
use app\common\service\followupaudio\FollowupAudioStore as Store;
|
|
use app\common\service\followupaudio\FollowupAudioProviderConfig as Providers;
|
|
use app\adminapi\logic\tcm\FollowupAudioLogic as Logic;
|
|
$checks = 0;
|
|
$expect = static function (bool $ok, string $why) use (&$checks): void {
|
|
if (!$ok) { throw new RuntimeException($why); }
|
|
$checks++;
|
|
};
|
|
$expect(!Store::enabled() && !Store::verified() && !Store::verified('qwen'), 'Missing app configuration stays disabled and unverified');
|
|
$expect(Store::claim() === null, 'Missing app configuration never enters queue transaction');
|
|
$expect(Providers::publicModels() === [], 'Missing provider configuration publishes no models');
|
|
try { Store::assertEnabled('qwen'); throw new RuntimeException('Expected disabled gate'); }
|
|
catch (DomainException $error) { $expect($error->getMessage() === 'FOLLOWUP_AUDIO_DISABLED_OR_UNVERIFIED', 'Stable disabled error'); }
|
|
try { Logic::requireEnabled(true); throw new RuntimeException('Expected public disabled gate'); }
|
|
catch (DomainException $error) { $expect($error->getMessage() === '回访录音功能尚未启用', 'Public gate avoids database'); }
|
|
$settings['followup_audio'] = ['enabled' => true, 'audio_verified' => true, 'verified_profiles' => ['qwen']];
|
|
$expect(!Store::verified(), 'Unbound flags alone never grant capability');
|
|
try { Logic::requireEnabled(true); throw new RuntimeException('Expected public unverified gate'); }
|
|
catch (DomainException $error) {
|
|
$expect(str_contains($error->getMessage(), '当前服务与模型') && !str_contains($error->getMessage(), 'Dify'), 'Public unverified error is provider-neutral');
|
|
}
|
|
$expect(Providers::publicModels() === [], 'Unconfigured flags do not advertise models');
|
|
$provider = ['driver' => 'openai_audio', 'base_url' => 'https://synthetic.invalid/v1',
|
|
'api_key' => 'synthetic-test-only', 'model' => 'audio-fixture-v1', 'label' => 'Synthetic audio model'];
|
|
$settings['followup_audio']['providers']['qwen'] = $provider;
|
|
$fingerprint = Providers::resolve('qwen')['fingerprint'];
|
|
$settings['followup_audio']['providers']['qwen']['verified_fingerprint'] = $fingerprint;
|
|
$task = ['model_key' => 'qwen', 'upstream_ids_json' => json_encode(['provider_fingerprint' => $fingerprint]), 'upstream_started_at' => 0];
|
|
$expect(Store::verified('qwen') && Store::providerMatches($task), 'Exact approved provider matches bound task');
|
|
$expect(Providers::publicModels() === [['value' => 'qwen', 'label' => 'Synthetic audio model']], 'Public model list contains only configured value/label');
|
|
$public = json_encode(Providers::publicModels());
|
|
$expect(!str_contains($public, 'synthetic.invalid') && !str_contains($public, $provider['api_key']), 'Public labels never expose server URL/key');
|
|
foreach (['{}', 'not-json', '{"provider_fingerprint":null}', '{"provider_fingerprint":123}'] as $ids) {
|
|
$unbound = array_replace($task, ['upstream_ids_json' => $ids]);
|
|
$expect(!Store::providerMatches($unbound), 'Missing malformed or invalid fingerprint cannot be trusted');
|
|
}
|
|
foreach (['driver' => 'dify', 'base_url' => 'https://changed.invalid/v1', 'api_key' => 'rotated-synthetic-key', 'model' => 'audio-fixture-v2'] as $field => $changed) {
|
|
$settings['followup_audio']['providers']['qwen'] = $provider + ['verified_fingerprint' => $fingerprint];
|
|
$settings['followup_audio']['providers']['qwen'][$field] = $changed;
|
|
$expect(!Store::verified('qwen') && !Store::providerMatches($task), 'Provider identity drift invalidates capability and task binding: ' . $field);
|
|
}
|
|
$expect(\think\facade\Db::$calls === 0, 'All missing-config and provider-binding checks performed without database I/O');
|
|
echo 'FOLLOWUP_AUDIO_PROVIDER_INTEGRATION assertions=' . $checks . " PASS database_calls=0 app_initialized=0 network_calls=0\n";
|
|
}
|