Files
zyt/server/tests/FollowupAudioWorkerTest.php
T

91 lines
6.3 KiB
PHP

<?php
declare(strict_types=1);
namespace app\common\service\prescriptionai {
final class PrescriptionAiAccess {
public static bool $active = true;
public static function actor(int $id): ?array { return self::$active ? ['id' => $id] : null; }
}
}
namespace app\common\service\followupaudio {
final class FollowupAudioStore {
public static bool $enabled = true;
public static bool $verified = true;
public static array $events = [];
public static bool $lease = true;
public static bool $providerMatches = true;
public static function enabled(): bool { return self::$enabled; }
public static function verified(?string $profile = null): bool { return self::$verified; }
public static function ready(?string $profile = null): bool { return self::$verified; }
public static function claim(): ?array { self::$events[] = 'claim'; return self::$verified ? ['id' => 1, 'actor_id' => 1, 'diagnosis_id' => 1, 'lease_token' => 'test', 'model_key' => 'qwen'] : null; }
public static function assertTaskProvider(array $task): void { if (!self::$providerMatches) { throw new FollowupAudioException('PROVIDER_CONFIGURATION_CHANGED'); } }
public static function heartbeat(int $id, string $token): bool { self::$events[] = 'heartbeat'; return self::$lease; }
public static function checkpoint(int $id, string $token, array $fields): bool { self::$events[] = ['checkpoint' => $fields]; return self::$lease; }
public static function complete(int $id, string $token, array $extraction): bool { self::$events[] = ['complete' => $extraction]; return self::$lease; }
public static function fail(int $id, string $token, string $code, string $message, bool $uncertain): bool { self::$events[] = ['fail' => $code, 'uncertain' => $uncertain, 'message' => $message]; return true; }
}
final class FollowupAudioAccess {
public static bool $allowed = true;
public static function task(int $id, int $actor, array $info): array { if (!self::$allowed) { throw new \RuntimeException('private-patient'); } return ['id' => $id]; }
}
final class FollowupAudioDify {
public static string $mode = 'success';
public function analyze(array $task, callable $heartbeat): array {
if (self::$mode === 'change-before-send') { FollowupAudioStore::$providerMatches = false; }
if ($heartbeat(['upstream_started_at' => 1, 'stage' => 'uploading']) === false) { throw new FollowupAudioException('LEASE_LOST'); }
if (self::$mode === 'uncertain') { throw new FollowupAudioException('UPSTREAM_UNCERTAIN', true); }
if (self::$mode === 'internal') { throw new \RuntimeException('private-patient'); }
if (self::$mode === 'change-after-send') { FollowupAudioStore::$providerMatches = false; }
if (self::$mode === 'revoke') { \app\common\service\prescriptionai\PrescriptionAiAccess::$active = false; }
return ['summary' => 'synthetic', 'items' => []];
}
}
}
namespace {
require dirname(__DIR__) . '/app/common/service/followupaudio/FollowupAudioException.php';
require dirname(__DIR__) . '/app/common/service/followupaudio/FollowupAudioWorker.php';
use app\common\service\followupaudio\FollowupAudioStore as Store;
use app\common\service\followupaudio\FollowupAudioDify as Dify;
use app\common\service\followupaudio\FollowupAudioWorker as Worker;
use app\common\service\followupaudio\FollowupAudioAccess as Access;
use app\common\service\prescriptionai\PrescriptionAiAccess as Actors;
$checks = 0;
$expect = static function (bool $ok, string $why) use (&$checks): void { if (!$ok) { throw new \RuntimeException($why); } $checks++; };
$worker = new Worker(new Dify());
Store::$enabled = false;
$expect(!$worker->runOnce() && Store::$events === [], 'disabled before claim');
Store::$enabled = true; Store::$verified = false;
$expect(!$worker->runOnce() && Store::$events === ['claim'], 'unverified task is not consumed; queue may fence stale bindings');
Store::$events = []; Store::$verified = true;
$expect($worker->runOnce(), 'success consumed task');
$expect(isset(Store::$events[count(Store::$events)-1]['complete']), 'success completed after authorization');
foreach (['uncertain' => 'UPSTREAM_UNCERTAIN', 'internal' => 'INTERNAL_ERROR', 'revoke' => 'LEASE_LOST'] as $mode => $code) {
Dify::$mode = $mode; Store::$events = []; Actors::$active = true;
$expect($worker->runOnce(), $mode . ' consumed');
$last = end(Store::$events);
$expect($last['fail'] === $code && $last['uncertain'] === true, $mode . ' reconciliation required');
$expect(!str_contains($last['message'], 'private-patient'), 'sanitized ' . $mode);
}
Actors::$active = false; Store::$events = [];
$worker->runOnce(); $last = end(Store::$events);
$expect($last['fail'] === 'ACCESS_REVOKED' && !$last['uncertain'], 'inactive actor before upstream');
Actors::$active = true; Access::$allowed = false; Store::$events = [];
$worker->runOnce(); $last = end(Store::$events);
$expect($last['fail'] === 'INTERNAL_ERROR' && !$last['uncertain'], 'scope denied before upstream');
Access::$allowed = true; Actors::$active = true;
foreach (['change-before-send' => false, 'change-after-send' => true] as $mode => $uncertain) {
Dify::$mode = $mode; Store::$events = []; Store::$providerMatches = true;
$worker->runOnce(); $last = end(Store::$events);
$expect($last['fail'] === 'PROVIDER_CONFIGURATION_CHANGED' && $last['uncertain'] === $uncertain,
$mode . ' is fenced with correct upstream uncertainty');
$expect(!array_filter(Store::$events, static fn ($event): bool => is_array($event) && isset($event['complete'])),
$mode . ' never completes result under a changed provider');
}
Store::$providerMatches = false; Store::$events = []; Dify::$mode = 'success';
$worker->runOnce(); $last = end(Store::$events);
$expect($last['fail'] === 'PROVIDER_CONFIGURATION_CHANGED' && !$last['uncertain'] && count(Store::$events) === 2,
'Initial missing or stale task fingerprint is rejected before any heartbeat or transport');
echo 'FOLLOWUP_AUDIO_WORKER assertions=' . $checks . ' PASS gate=1 actor_recheck=1 reconciliation=1' . PHP_EOL;
}