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

194 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
// Independent, in-process GET/config/clock stubs and handcrafted audit fixtures.
// Do not load the probe, application, original credentials or runtime audits.
namespace PrivateTcmR25 {
function hrtime(bool $number = false): int { return $GLOBALS['iiClock']; }
function curl_init(): object {
++$GLOBALS['iiCreated'];
$scenario = array_shift($GLOBALS['iiScenarios']);
if (!is_array($scenario)) throw new \RuntimeException('Unexpected synthetic GET');
return (object) ['scenario' => $scenario, 'options' => [], 'errno' => 0];
}
function curl_setopt_array(object $handle, array $options): bool {
$handle->options = $options; $GLOBALS['iiOptions'][] = $options;
$GLOBALS['iiClock'] += $handle->scenario['setup_ns'] ?? 0;
return ($handle->scenario['set_ok'] ?? true) === true;
}
function curl_exec(object $handle): bool {
++$GLOBALS['iiExecuted'];
$GLOBALS['iiClock'] += $handle->scenario['advance_ns'] ?? 0;
if ($handle->scenario['throw'] ?? false) throw new \RuntimeException('private-independent-provider-error');
foreach ($handle->scenario['chunks'] ?? [$handle->scenario['body']] as $chunk) {
if (($handle->options[CURLOPT_WRITEFUNCTION])($handle, $chunk) !== strlen($chunk)) {
$handle->errno = CURLE_WRITE_ERROR; return false;
}
}
return true;
}
function curl_errno(object $handle): int { return $handle->errno; }
function curl_getinfo(object $handle, int $option): int { return $handle->scenario['status'] ?? 200; }
function curl_close(object $handle): void { ++$GLOBALS['iiClosed']; }
}
namespace {
require dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/private_tcm_r25_run_input_inspector.php';
use PrivateTcmR25\RunInputInspector as Inspector;
$checks = 0; $iiClock = 10000000000; $iiCreated = 0; $iiClosed = 0; $iiExecuted = 0; $iiScenarios = []; $iiOptions = [];
$expect = static function (bool $value, string $label) use (&$checks): void { ++$checks; if (!$value) throw new RuntimeException($label); };
$json = static fn (mixed $value): string => json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
$base = sys_get_temp_dir() . '/tcm-r25-inspector-independent-' . bin2hex(random_bytes(8));
$root = $base . '/server'; $auditDir = $root . '/runtime/tcm-r25-wire-audits'; $resultDir = $base . '/app/artifacts/ai_concurrency';
mkdir($auditDir, 0700, true); mkdir($resultDir, 0700, true);
$number = 0;
$query = "independent-private-query/中文\nsecond-line\r\n";
$config = ['enable' => true, 'base_url' => 'http://independent.synthetic.invalid:8088/v1/chat-messages',
'models' => ['qwen' => ['api_key' => 'private-independent-qwen-key']]];
$fixtureHash = 'b54c47ec11f6a99dc87bdda78629e18c8a6f37a64017e42a72db399c727cb1c6';
$questionHash = '28fbaee8585d4fbde3be3b1a6ee046c1a60f18a1fe67c0769211b08afdd5d306';
$make = static function (int $count = 1) use (&$number, $query, $fixtureHash, $questionHash): array {
$id = sprintf('44444444-4444-4444-8444-%012d', ++$number);
$records = [['event' => 'request', 'contract' => 'v43r25_private_run_wire', 'request_id' => $id,
'request_label' => 'independent_row_qwen', 'profile' => 'qwen']];
for ($i = 1; $i <= $count; ++$i) {
$body = json_encode(['inputs' => (object) [], 'query' => $query], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
$records[] = ['event' => 'serialized_request', 'generation' => $i, 'report_stage' => $i === 1 ? 1 : 2,
'stage_attempt' => $i <= 2 ? 1 : $i - 1, 'protocol' => 'dify', 'body_sha256' => hash('sha256', $body), 'body_bytes' => strlen($body),
'query_sha256' => hash('sha256', $query), 'query_bytes' => strlen($query),
'inputs_sha256' => hash('sha256', '{}'), 'inputs_bytes' => 2, 'report_continuation_query' => $i > 1];
$records[] = ['event' => 'original_identity', 'generation' => $i, 'original_task_id' => 'private-independent-task-' . $i, 'original_workflow_run_id' => ''];
$records[] = ['event' => 'original_identity', 'generation' => $i, 'original_task_id' => 'private-independent-task-' . $i, 'original_workflow_run_id' => 'private-independent-run-' . $i];
}
$records[] = ['event' => 'after_control_cleanup', 'state' => ['transport_closed' => true, 'upstream_terminal_confirmed' => true,
'upstream_stop_confirmed' => false, 'cancel_requested' => false, 'timeout_reached' => false, 'upstream_pending_task_count' => 0]];
return ['records' => $records, 'summary' => ['persisted' => true, 'serialized_request_count' => $count,
'original_task_count' => $count, 'original_run_count' => $count], 'id' => $id, 'number' => $number,
'fixture' => $fixtureHash, 'question' => $questionHash];
};
$save = static function (array $fixture) use ($auditDir, $resultDir, $json): array {
$content = implode("\n", array_map($json, $fixture['records'])) . "\n";
$summary = $fixture['summary'] + ['audit_bytes' => strlen($content), 'audit_sha256' => hash('sha256', $content)];
$auditPath = $auditDir . '/' . hash('sha256', $fixture['id']) . '.private.jsonl';
file_put_contents($auditPath, $content);
$row = ['test_label' => 'independent_row', 'profile' => 'qwen', 'private_wire_audit' => $summary,
'harness_contract' => 'v43r25_private_run_wire', 'synthetic' => true, 'preparation_isolated' => true,
'preparation_isolation_verified' => true, 'production_acceptance_path' => true, 'diagnostic_only' => false, 'case_id' => 11,
'production_hashes' => ['fixture' => $fixture['fixture'], 'question' => $fixture['question']]];
$document = ['harness_contract' => 'v43r25_private_run_wire', 'synthetic_only' => true, 'preparation_isolated' => true,
'fixture_sha256' => $fixture['fixture'], 'canonical_question_sha256' => $fixture['question'], 'rows' => [$row]];
$resultFile = 'tcm_complete_independent_inspector_' . $fixture['number'] . '.safe.json';
$resultPath = $resultDir . '/' . $resultFile; file_put_contents($resultPath, $json($document));
return [['result_file' => $resultFile, 'test_label' => 'independent_row', 'profile' => 'qwen', 'audit_sha256' => $summary['audit_sha256']], $auditPath, $resultPath];
};
$response = static fn (int $i = 1): array => ['id' => 'private-independent-run-' . $i, 'status' => 'succeeded',
'inputs' => ['sys.query' => $query], 'outputs' => ['secret' => 'private-independent-output']];
$call = static function (array $selector, array $scenarios, ?array $override = null) use ($root, $config): array {
$GLOBALS['iiScenarios'] = $scenarios; $configCalls = 0;
$value = Inspector::inspectResultRow($root, $selector, static function () use (&$configCalls, $config, $override): array { ++$configCalls; return $override ?? $config; });
return [$value, $configCalls];
};
$privacy = static function (array $value) use ($expect, $json, $query): void {
$text = $json($value);
foreach (['private-independent-', 'independent.synthetic', 'independent_row', '44444444-', $query] as $secret)
$expect(!str_contains($text, $secret), 'safe reply excludes private identity/body/config/selector text');
$expect(($value['model_calls'] ?? null) === 0 && ($value['llm_expanded_prompt_observed'] ?? null) === false, 'query evidence never becomes model/node visibility');
};
try {
[$selected, , $resultPath] = $save($make(2));
[$ok, $configured] = $call($selected, [['body' => $json($response())], ['body' => $json($response(2))]]);
$expect($ok['ok'] && $configured === 1 && $ok['inspected_run_count'] === 2 && $ok['matched_run_count'] === 2, 'full saved-row bridge selects exactly two original runs');
$expect($ok['result_file_sha256'] === hash_file('sha256', $resultPath) && $ok['audit_sha256'] === $selected['audit_sha256'], 'whole result and audit digests retained');
$privacy($ok);
foreach ($iiOptions as $options) {
$expect($options[CURLOPT_HTTPGET] === true && !isset($options[CURLOPT_POSTFIELDS]) && !isset($options[CURLOPT_POST]), 'GET only');
$expect($options[CURLOPT_PROTOCOLS] === CURLPROTO_HTTP && $options[CURLOPT_REDIR_PROTOCOLS] === CURLPROTO_HTTP
&& str_starts_with($options[CURLOPT_URL], 'http://independent.synthetic.invalid:8088/v1/workflows/run/'), 'exact configured HTTP scheme/host/port/path only');
$expect($options[CURLOPT_TIMEOUT_MS] > 0 && $options[CURLOPT_TIMEOUT_MS] <= 2000 && $options[CURLOPT_CONNECTTIMEOUT_MS] <= 2000
&& !$options[CURLOPT_FOLLOWLOCATION] && $options[CURLOPT_MAXREDIRS] === 0, 'bounded deadline and redirects disabled');
}
foreach (['open_transport', 'no_terminal', 'pending', 'pending_string', 'extra_footer', 'header_upper_uuid', 'header_neighbor_label',
'identity_downgrade', 'identity_other_task', 'identity_before_serialization', 'wrong_query_hash_type', 'stage_backwards', 'non_dify',
'summary_bool_count', 'summary_byte_mismatch', 'summary_run_mismatch', 'too_many_runs'] as $kind) {
$fixture = $make(in_array($kind, ['stage_backwards', 'too_many_runs'], true) ? 3 : 1);
$last = count($fixture['records']) - 1;
if ($kind === 'open_transport') $fixture['records'][$last]['state']['transport_closed'] = false;
if ($kind === 'no_terminal') $fixture['records'][$last]['state']['upstream_terminal_confirmed'] = false;
if ($kind === 'pending') $fixture['records'][$last]['state']['upstream_pending_task_count'] = 1;
if ($kind === 'pending_string') $fixture['records'][$last]['state']['upstream_pending_task_count'] = '0';
if ($kind === 'extra_footer') $fixture['records'][$last]['state']['raw_body'] = 'private-independent-injection';
if ($kind === 'header_upper_uuid') $fixture['records'][0]['request_id'] = strtoupper(str_replace('44444444', 'abcdefab', $fixture['id']));
if ($kind === 'header_neighbor_label') $fixture['records'][0]['request_label'] = 'neighbor_qwen';
if ($kind === 'identity_downgrade') $fixture['records'][3]['original_workflow_run_id'] = '';
if ($kind === 'identity_other_task') $fixture['records'][3]['original_task_id'] = 'private-independent-other-task';
if ($kind === 'identity_before_serialization') [$fixture['records'][1], $fixture['records'][2]] = [$fixture['records'][2], $fixture['records'][1]];
if ($kind === 'wrong_query_hash_type') $fixture['records'][1]['query_sha256'] = ['private-independent-injection'];
if ($kind === 'stage_backwards') { $fixture['records'][7]['report_stage'] = 1; $fixture['records'][7]['report_continuation_query'] = false; }
if ($kind === 'non_dify') $fixture['records'][1]['protocol'] = 'openai';
if ($kind === 'summary_bool_count') $fixture['summary']['original_task_count'] = true;
if ($kind === 'summary_byte_mismatch') $fixture['summary']['audit_bytes'] = 1;
if ($kind === 'summary_run_mismatch') $fixture['summary']['original_run_count'] = 0;
[$selector] = $save($fixture); $before = $iiExecuted;
[$value, $configured] = $call($selector, []);
$expect(!$value['ok'] && $configured === 0 && $iiExecuted === $before, 'invalid exact-own bridge fails before config/GET: ' . $kind);
$privacy($value);
}
foreach (['object_sys', 'lookalike_dot', 'trailing_space', 'wrong_case', 'array_inputs', 'null_query', 'different_crlf',
'different_unicode', 'wrong_byte_count', 'optional_null_run', 'optional_null_task', 'output_only', 'both_same', 'running_input_only'] as $kind) {
$fixture = $make(); $value = $response();
if ($kind === 'object_sys') $value['inputs'] = ['sys' => ['query' => $query]];
if ($kind === 'lookalike_dot') $value['inputs'] = ['sys.query' => $query];
if ($kind === 'trailing_space') $value['inputs'] = ['sys.query ' => $query];
if ($kind === 'wrong_case') $value['inputs'] = ['SYS.QUERY' => $query];
if ($kind === 'array_inputs') $value['inputs'] = [['sys.query' => $query]];
if ($kind === 'null_query') $value['inputs']['query'] = null;
if ($kind === 'different_crlf') $value['inputs']['sys.query'] = str_replace("\r\n", "\n", $query);
if ($kind === 'different_unicode') $value['inputs']['sys.query'] .= "\u{0301}";
if ($kind === 'wrong_byte_count') ++$fixture['records'][1]['query_bytes'];
if ($kind === 'optional_null_run') $value['workflow_run_id'] = null;
if ($kind === 'optional_null_task') $value['task_id'] = null;
if ($kind === 'output_only') { $value['inputs'] = (object) []; $value['outputs']['query'] = $query; }
if ($kind === 'both_same') $value['inputs']['query'] = $query;
if ($kind === 'running_input_only') $value['status'] = 'running';
[$selector] = $save($fixture); $before = $iiExecuted;
[$answer] = $call($selector, [['body' => $json($value)]]);
$expect($answer['ok'] === in_array($kind, ['both_same', 'running_input_only'], true) && $iiExecuted === $before + 1,
'only exact whitelisted byte-identical query evidence: ' . $kind);
if ($kind === 'running_input_only') $expect($answer['runs'][0]['run_status'] === 'running', 'matching query does not assert a fresh terminal or cleanup proof');
$privacy($answer);
}
[$selector] = $save($make(2)); $before = $iiExecuted;
[$answer] = $call($selector, [['body' => $json($response()), 'advance_ns' => 2000000000], ['body' => $json($response(2))]]);
$expect(!$answer['ok'] && $iiExecuted === $before + 2 && $answer['matched_run_count'] === 1
&& $answer['runs'][0]['status'] === 'run_timeout' && $answer['runs'][1]['query_matches_wire'], 'timeout plus successor uses exactly two original GETs and cannot hide first failure');
$privacy($answer);
foreach (['setup_deadline', 'setopt_failure', 'chunk_overflow', 'throw'] as $kind) {
[$selector] = $save($make()); $scenario = ['body' => $json($response())];
if ($kind === 'setup_deadline') $scenario['setup_ns'] = 2000000000;
if ($kind === 'setopt_failure') $scenario['set_ok'] = false;
if ($kind === 'chunk_overflow') $scenario['chunks'] = [str_repeat('x', 1048576), str_repeat('x', 1048576), 'x'];
if ($kind === 'throw') $scenario['throw'] = true;
$before = $iiExecuted; [$answer] = $call($selector, [$scenario]);
$expect(!$answer['ok'] && $iiExecuted === $before + (in_array($kind, ['setup_deadline', 'setopt_failure'], true) ? 0 : 1), 'bounded setup/write/error closes without retry: ' . $kind);
$privacy($answer);
}
[$selector, $auditPath] = $save($make());
$lock = fopen($auditPath, 'rb');
$expect(flock($lock, LOCK_EX | LOCK_NB), 'synthetic exclusive lock acquired');
$before = $iiExecuted; [$answer, $configured] = $call($selector, []);
$expect(!$answer['ok'] && $configured === 0 && $iiExecuted === $before, 'locked exact private audit never reaches config/GET');
flock($lock, LOCK_UN); fclose($lock);
$expect($iiCreated === $iiClosed, 'every fake GET handle closed');
$expect($iiScenarios === [], 'all and only declared fake scenarios consumed');
$expect(!class_exists('think\\App', false), 'application bootstrap never loaded');
echo "Independent exact-run input inspector: {$checks} checks passed (handcrafted private fixtures, in-memory GET/config/time; no auth/network/bootstrap).\n";
} finally {
$real = realpath($base); $temp = realpath(sys_get_temp_dir());
if ($real === false || $temp === false || !str_starts_with(str_replace('\\', '/', $real), rtrim(str_replace('\\', '/', $temp), '/') . '/tcm-r25-inspector-independent-'))
throw new RuntimeException('Independent synthetic cleanup boundary');
$entries = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($real, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
foreach ($entries as $entry) { if ($entry->isLink()) throw new RuntimeException('Independent synthetic cleanup symlink'); if ($entry->isDir()) rmdir($entry->getPathname()); else unlink($entry->getPathname()); }
rmdir($real);
}
}