Files
zyt/server/tests/AssistantSubmissionIndependentR26cTest.php
2026-10-27 14:03:35 +08:00

160 lines
14 KiB
PHP

<?php
declare(strict_types=1);
// New independent finite protocol controls. Real monotonic/wall clocks; local
// cURL/config stubs only, with literal canonical lifecycle closures.
namespace think\facade { class Log { public static function warning(...$unused): void {} } }
namespace app\common\service {
require __DIR__ . '/fixtures/CancellableCurlMultiStub.php';
function config(string $unused): array { return ['enable' => true, 'base_url' => 'https://independent.invalid/v1/chat-messages',
'timeout' => 90, 'models' => ['qwen' => ['name' => 'independent', 'api_key' => 'synthetic-only']]]; }
function curl_init(): \stdClass { return (object) ['errno' => 0]; }
function curl_setopt_array(\stdClass $h, array $o): bool { $h->options = $o; return true; }
function curl_exec(\stdClass $h): bool {
++$GLOBALS['i26bPosts']; $n = $GLOBALS['i26bPosts']; $GLOBALS['i26bInCurl'] = true;
try {
\i26bCheck($GLOBALS['i26bControl']->publicState()['submitted_pending_attempt_count'] === 1,
'owned pending record precedes actual stub execution');
\i26bCheck(!$GLOBALS['i26bControl']->canStartAttempt(), 'admission blocks a racing same-request generation');
($h->options[CURLOPT_HEADERFUNCTION])($h, "HTTP/1.1 200 OK\r\n");
if ($n > 1) $frames = [['event' => 'workflow_started', 'task_id' => 'synthetic-task', 'workflow_run_id' => 'synthetic-run'],
['event' => 'message', 'task_id' => 'synthetic-task', 'answer' => "原字节\r\n乙。"],
['event' => 'workflow_finished', 'task_id' => 'synthetic-task', 'workflow_run_id' => 'synthetic-run', 'data' => ['status' => 'succeeded']],
['event' => 'message_end']];
else {
$error = ['event' => 'error', 'code' => 'invalid_inputs', 'message' => 'synthetic validation rejection'];
$mode = $GLOBALS['i26bMode'];
$frames = [];
if ($mode === 'error_original_run') $error['workflow_run_id'] = 'synthetic-unbound-run';
if ($mode === 'error_answer') $error['answer'] = 'synthetic execution evidence';
if ($mode === 'error_outputs') $error['outputs'] = ['answer' => 'synthetic execution evidence'];
if ($mode === 'error_choices') $error['choices'] = [['delta' => ['content' => 'synthetic execution evidence']]];
if ($mode === 'upper_terminal') $frames[] = ['event' => 'WORKFLOW_FINISHED', 'task_id' => 'synthetic-unbound-task',
'workflow_run_id' => 'synthetic-unbound-run', 'data' => ['status' => 'stopped']];
if ($mode === 'lower_terminal') $frames[] = ['event' => 'workflow_finished', 'task_id' => 'synthetic-unbound-task',
'workflow_run_id' => 'synthetic-unbound-run', 'data' => ['status' => 'stopped']];
if ($mode === 'node') $frames[] = ['event' => 'node_started'];
$frames[] = $mode === 'quiet' ? ['event' => 'message_end'] : $error;
}
foreach ($frames as $frame) ($h->options[CURLOPT_WRITEFUNCTION])($h,
'data: ' . json_encode($frame, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n\n");
return true;
} finally { $GLOBALS['i26bInCurl'] = false; }
}
function curl_errno(\stdClass $h): int { return 0; }
function curl_getinfo(\stdClass $h, int $o): int|float { return $o === CURLINFO_HTTP_CODE ? 200 : 0.001; }
function curl_close(\stdClass $h): void { $GLOBALS['i26bClosed'] = $GLOBALS['i26bPosts']; }
class DifyWorkflowRunStatus {
public static function lookup(array $id, int $ms): array {
++$GLOBALS['i26bGets'];
\i26bCheck(!$GLOBALS['i26bInCurl'] && $GLOBALS['i26bClosed'] === $GLOBALS['i26bPosts'], 'lookup follows actual handle close');
\i26bCheck(count($GLOBALS['i26bTiming']) === $GLOBALS['i26bPosts'], 'timing precedes verifier I/O');
\i26bCheck(array_keys($id) === ['profile', 'user', 'task_id', 'workflow_run_id', 'generation'], 'no private token reaches adapter');
\i26bCheck($id['task_id'] === 'synthetic-task' && $id['workflow_run_id'] === 'synthetic-run', 'no borrowed unbound metadata reaches adapter');
\i26bCheck($ms > 0 && $ms <= 1000, 'lookup I/O cap remains finite');
return ['ok' => true, 'workflow_run_id' => $id['workflow_run_id'], 'status' => 'succeeded'];
}
}
}
namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\service\AssistantRequestControl as Control;
use app\common\service\DifyChatService;
$checks = 0; $failures = []; $observations = []; $number = 0;
function i26bCheck(bool $ok, string $label): void { ++$GLOBALS['checks']; if (!$ok) $GLOBALS['failures'][] = $label; }
$root = dirname(__DIR__, 2);
$pins = ['server/app/adminapi/service/AssistantRequestControl.php' => 'e7a43edad2c4c64ba968d52b991b7f0b3dba12de0165a01062cf9be2bcd3ee7d',
'server/app/common/service/DifyChatService.php' => 'dcbe424f38075e34d4128dc0b8fccc1b0fbae6349397bba7c1fbb6253f86410f',
'server/app/adminapi/controller/tcm/DiagnosisController.php' => '83d47bf0ffb47c0f06a0cd69db910d28c0e790ccbfb5e5d3e5c931bf84f72568'];
foreach ($pins as $p => $hash) i26bCheck(hash_file('sha256', $root . '/' . $p) === $hash, 'exact candidate pin ' . $p);
$dir = sys_get_temp_dir() . '/r26b-independent-submission-' . bin2hex(random_bytes(8));
$read = static fn (string $p): array => json_decode(file_get_contents($p), true, 64, JSON_THROW_ON_ERROR);
$write = static fn (string $p, array $s): int|false => file_put_contents($p, json_encode($s, JSON_THROW_ON_ERROR));
$make = static function () use ($dir, &$number): array {
$uuid = sprintf('e5678901-1234-4234-8234-%012d', ++$number); $c = new Control(71, 11, $uuid, $dir);
i26bCheck($c->begin(), 'new isolated control begins'); return [$c, $dir . '/' . hash('sha256', '71:11:' . $uuid) . '.json', $uuid];
};
$source = file_get_contents($root . '/server/app/adminapi/controller/tcm/DiagnosisController.php'); $literals = [];
foreach (['on_submission', 'on_task', 'on_terminal', 'on_transfer_end', 'can_start_attempt', 'on_timeout'] as $key) {
$pattern = in_array($key, ['on_submission', 'on_task'], true)
? '/\$options\[\'' . preg_quote($key, '/') . '\'\] = (static function.*?\n });/s'
: '/\'' . preg_quote($key, '/') . '\' => (static function.*?\n }),/s';
if (preg_match_all($pattern, $source, $m) !== 1) throw new RuntimeException('Canonical callback not unique');
$literals[$key] = $m[1][0];
}
try {
foreach (['pure_rejection', 'error_original_run', 'error_answer', 'error_outputs', 'error_choices', 'upper_terminal', 'lower_terminal', 'node', 'quiet'] as $mode) {
[$control, $path] = $make(); $GLOBALS['i26bControl'] = $control; $GLOBALS['i26bMode'] = $mode;
$GLOBALS['i26bPosts'] = $GLOBALS['i26bGets'] = $GLOBALS['i26bClosed'] = 0;
$GLOBALS['i26bInCurl'] = false; $GLOBALS['i26bTiming'] = []; $profile = 'qwen'; $upstreamUser = 'synthetic-user'; $options = [];
foreach ($literals as $key => $literal) $options[$key] = eval('namespace app\\adminapi\\controller\\tcm; use app\\common\\service\\DifyWorkflowRunStatus; return ' . $literal . ';');
$options['on_upstream_timing'] = static function ($timing): void { $GLOBALS['i26bTiming'][] = $timing->snapshot(); };
$options['knowledge_mode'] = 'evidence'; $options['timeout_ms'] = 90000; $options['request_deadline_ns'] = hrtime(true) + 90000000000;
$clinical = '';
$out = DifyChatService::streamChat('qwen', ['fixture' => 'input'], 'synthetic-query', 'synthetic-user',
static function (string $text) use (&$clinical): bool { $clinical .= $text; return true; }, static fn (): bool => false, [], $options);
$p = $control->publicState(); $s = $read($path); $first = array_values($s['submitted_attempts'])[0];
$observations[] = ['case' => $mode, 'posts' => $GLOBALS['i26bPosts'], 'gets' => $GLOBALS['i26bGets'],
'pending' => $p['upstream_pending_task_count'], 'rejected_before_execution' => $p['rejected_before_execution_attempt_count'],
'can_start' => $control->canStartAttempt(), 'first_disposition' => $first['disposition']];
if ($mode === 'pure_rejection') {
i26bCheck($GLOBALS['i26bPosts'] === 2 && $GLOBALS['i26bGets'] === 1 && !empty($out['ok']), 'plain finite SSE input rejection retains compatibility');
i26bCheck($clinical === "原字节\r\n乙。", 'positive compatibility preserves clinical bytes');
} else {
i26bCheck($GLOBALS['i26bPosts'] === 1 && $GLOBALS['i26bGets'] === 0, 'no successor or guessed GET after execution evidence ' . $mode);
i26bCheck($p['upstream_pending_task_count'] === 1 && !$control->canStartAttempt(), 'execution evidence remains pending ' . $mode);
i26bCheck($first['disposition'] === 'unconfirmed' && $p['rejected_before_execution_attempt_count'] === 0, 'execution evidence cannot become pre-execution rejection ' . $mode);
}
$encoded = json_encode($p, JSON_THROW_ON_ERROR);
foreach (['synthetic-task', 'synthetic-run', 'synthetic-user', 'synthetic-query', 'synthetic-only', $first === [] ? '' : array_key_first($s['submitted_attempts'])] as $secret)
i26bCheck($secret === '' || !str_contains($encoded, $secret), 'public state excludes synthetic identity/input/token');
}
// Real clock-domain checks: admission/observations are epoch wall time;
// persisted budget origins are host-monotonic and not comparable to it.
[$c, $path] = $make(); $wallBefore = microtime(true); $monoBefore = hrtime(true) / 1e9;
$token = $c->beginSubmission('qwen', 'clock-user'); $c->bindTask('qwen', 'clock-user', 'clock-task', 'clock-run', $token);
$c->markTerminal('clock-task', 'stopped', 'clock-run'); $gets = 0;
$r = $c->closeSubmittedGeneration($token, 'clock-task', 'unconfirmed', static function (array $id, int $ms) use (&$gets, $dir, $path): array {
++$gets;
foreach ([$dir . '/.gc.lock', $path] as $file) { $h = fopen($file, 'c+'); $ok = flock($h, LOCK_EX | LOCK_NB);
i26bCheck($ok, 'no GC/request lock across actual injected lookup'); if ($ok) flock($h, LOCK_UN); fclose($h); }
return ['ok' => true, 'workflow_run_id' => $id['workflow_run_id'], 'status' => 'failed'];
});
$wallAfter = microtime(true); $monoAfter = hrtime(true) / 1e9; $s = $read($path); $a = $s['completed_generations'][1];
foreach ([$s['submitted_attempts'][$token]['submitted_at'], $s['submitted_attempts'][$token]['transfer_closed_at'],
$a['producer_terminal_observed_at'], $a['persisted_run_observed_at'], $a['transfer_closed_at'], $a['run_status_observations'][0]['observed_at']] as $time)
i26bCheck($time >= $wallBefore && $time <= $wallAfter, 'observation remains actual wall clock');
i26bCheck($a['verification_started_at'] >= $monoBefore && $a['verification_started_at'] <= $monoAfter, 'verification budget origin remains actual monotonic clock');
i26bCheck($r['confirmed'] && $gets === 1 && $c->canStartAttempt() && $c->publicState()['lifecycle_conflict'], 'exact differing terminal enum resolves with retained conflict');
i26bCheck($a['producer_terminal_status'] === 'stopped' && $a['persisted_run_status'] === 'failed', 'both actual status observations retained');
$observations[] = ['case' => 'real_clock_domains', 'wall_observations_in_wall_interval' => true,
'monotonic_budget_origin_in_monotonic_interval' => true, 'wall_monotonic_gap_exceeds_one_day' => abs($wallAfter - $monoAfter) > 86400];
// Unknown work survives expiry, cross-worker reads, stale EOF and a new
// unrelated UUID; explicit pre-execution resolution cannot rewrite it.
[$c, $path, $uuid] = $make(); $token = $c->beginSubmission('qwen', 'isolated-user');
$never = static function (): array { i26bCheck(false, 'no lookup without original pair'); return []; };
$c->closeSubmittedGeneration($token, '', 'unconfirmed', $never);
$c->closeSubmittedGeneration($token, '', 'rejected_before_execution', $never);
i26bCheck($c->publicState()['submitted_without_identity_count'] === 1 && !$c->canStartAttempt(), 'late EOF cannot reclassify unknown submission');
$s = $read($path); $s['expires_at'] = time() - 2000; $write($path, $s); touch($path, time() - 2000);
$peer = new Control(71, 11, $uuid, $dir);
i26bCheck(is_file($path) && !$peer->begin() && $peer->publicState()['upstream_pending_task_count'] === 1, 'expired unknown attempt survives GC and cross-worker replay');
[$other] = $make(); i26bCheck($other->canStartAttempt(), 'unrelated UUID has no global cleanup gate');
$foreign = $other->closeSubmittedGeneration($token, '', 'not_submitted', $never);
i26bCheck($foreign['reason'] === 'stale_submission' && $peer->publicState()['submitted_without_identity_count'] === 1, 'foreign EOF cannot affect original ownership');
foreach ($pins as $p => $hash) i26bCheck(hash_file('sha256', $root . '/' . $p) === $hash, 'exact candidate pin after ' . $p);
echo json_encode(['scope' => 'new independent r26b production finite submission controls', 'assertions' => $checks,
'failures' => $failures, 'observations' => $observations, 'real_network_calls' => 0, 'real_model_calls' => 0], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n";
} finally {
$real = realpath($dir); $temp = realpath(sys_get_temp_dir());
if ($real !== false) {
if ($temp === false || !str_starts_with(str_replace('\\', '/', $real), rtrim(str_replace('\\', '/', $temp), '/') . '/r26b-independent-submission-')) throw new RuntimeException('Cleanup boundary');
foreach (new DirectoryIterator($real) as $f) { if ($f->isDot()) continue; if ($f->isLink() || !$f->isFile()) throw new RuntimeException('Unexpected temp entry'); unlink($f->getPathname()); }
rmdir($real);
}
}
exit($failures === [] ? 0 : 1);
}