initialize(); $database = (string) config('database.connections.' . config('database.default') . '.database'); aiMcpPerfExpect(str_ends_with($database, '_test'), "refusing to run on database '{$database}' (name must end with _test)"); aiMcpPerfExpect((int) Db::name('system_menu')->where('perms', 'ai.mcp/access')->count() === 1, 'run 2026_09_24_ai_mcp.sql on the test database first'); // ---------------------------------------------------------------- 夹具 $now = time(); $pwd = create_password('Test@123456', (string) config('project.unique_identification')); $roles = [197 => ['业绩经理', 2], 198 => ['业绩受限经理', 2], 199 => ['业绩医助附加', 4]]; Db::name('system_role')->whereIn('id', array_keys($roles))->delete(); foreach ($roles as $id => [$name, $scope]) { Db::name('system_role')->insert(['id' => $id, 'name' => $name, 'desc' => 'ai-mcp-perf-test', 'sort' => 0, 'data_scope' => $scope, 'create_time' => $now, 'update_time' => $now]); } Db::name('dept')->whereIn('id', [9921, 9922, 9923])->delete(); foreach ([9921 => ['AI业绩总部', 0], 9922 => ['AI业绩一中心', 9921], 9923 => ['AI业绩三中心', 9921]] as $id => [$name, $pid]) { Db::name('dept')->insert(['id' => $id, 'name' => $name, 'pid' => $pid, 'sort' => 0, 'leader' => '', 'mobile' => '', 'status' => 1, 'create_time' => $now, 'update_time' => $now]); } // 医助、医生必须用系统角色 2 / 1:医助排行榜和医生统计按这两个角色取人 $admins = [ 92001 => ['p_mgr', '业绩经理甲', [197], 9921], 92002 => ['p_doc_a', '业绩张医生', [1], 9922], 92003 => ['p_doc_b', '业绩李医生', [1], 9923], 92004 => ['p_asst_c', '业绩王医助', [2, 199], 9922], 92005 => ['p_lim', '业绩受限乙', [198], 9921], 92008 => ['p_root', '业绩超管', [], 9921], 92009 => ['p_asst_g', '业绩钱医助', [2], 9923], ]; Db::name('admin')->whereIn('id', array_keys($admins))->delete(); Db::name('admin_role')->whereIn('admin_id', array_keys($admins))->delete(); Db::name('admin_dept')->whereIn('admin_id', array_keys($admins))->delete(); Db::name('ai_grant')->whereIn('admin_id', array_keys($admins))->delete(); foreach ($admins as $id => [$account, $name, $roleIds, $dept]) { Db::name('admin')->insert(['id' => $id, 'root' => $account === 'p_root' ? 1 : 0, 'name' => $name, 'avatar' => '', 'account' => $account, 'password' => $pwd, 'multipoint_login' => 1, 'is_paw' => 1, 'work_wechat_userid' => '', 'disable' => 0, 'phone' => '1370000' . substr((string) $id, -4), 'create_time' => $now, 'update_time' => $now]); foreach ($roleIds as $roleId) { Db::name('admin_role')->insert(['admin_id' => $id, 'role_id' => $roleId]); } Db::name('admin_dept')->insert(['admin_id' => $id, 'dept_id' => $dept]); } $menuId = static function (string $perm) use ($now): int { $id = (int) Db::name('system_menu')->where('perms', $perm)->value('id'); return $id ?: (int) Db::name('system_menu')->insertGetId(['pid' => 0, 'type' => 'A', 'name' => 'AI测试 ' . $perm, 'icon' => '', 'sort' => 0, 'perms' => $perm, 'paths' => '', 'component' => '', 'selected' => '', 'params' => '', 'is_cache' => 0, 'is_show' => 0, 'is_disable' => 0, 'create_time' => $now, 'update_time' => $now]); }; $leaderboardMenu = $menuId('stats.yejiStats/leaderboard'); Db::name('system_menu')->where('id', $leaderboardMenu)->update(['is_disable' => 0]); $grantsByRole = [ 197 => ['ai.mcp/access', 'stats.yejiStats/leaderboard', 'stats.doctorDailyStats/overview', 'stats.yejiStats/overview', 'tcm.prescriptionOrder/lists'], 198 => ['ai.mcp/access', 'stats.yejiStats/overview', 'stats.yejiStats/tabLeaderboard'], 199 => ['ai.mcp/access', 'stats.yejiStats/leaderboard'], ]; Db::name('system_role_menu')->whereIn('role_id', array_keys($grantsByRole))->delete(); foreach ($grantsByRole as $role => $perms) { foreach ($perms as $perm) { Db::name('system_role_menu')->insert(['role_id' => $role, 'menu_id' => $menuId($perm)]); } } Db::name('tcm_diagnosis')->whereIn('id', [95011, 95012, 95013])->delete(); foreach ([95011 => ['业一', 92004], 95012 => ['业二', 92004], 95013 => ['业三', 92009]] as $id => [$name, $assistant]) { Db::name('tcm_diagnosis')->insert(['id' => $id, 'patient_id' => $id + 1000, 'patient_name' => $name, 'phone' => '1382222' . substr((string) $id, -4), 'id_card' => '', 'gender' => 1, 'age' => 40, 'status' => 1, 'assistant_id' => $assistant, 'create_time' => $now, 'update_time' => $now]); } Db::name('tcm_prescription')->whereIn('id', [8101, 8102, 8103, 8104])->delete(); foreach ([8101 => [95011, 92002], 8102 => [95012, 92003], 8103 => [95013, 92003], 8104 => [95012, 92002]] as $id => [$diag, $doctor]) { Db::name('tcm_prescription')->insert(['id' => $id, 'sn' => 'PERFRX' . $id, 'diagnosis_id' => $diag, 'creator_id' => $doctor, 'prescription_date' => '2031-02-01']); } Db::name('tcm_prescription_order')->whereLike('order_no', 'PERFT%')->delete(); $orders = [ // [处方, 诊单, 创建人(医助), 金额, 履约状态, 退款, 创建时间] ['PERFT1', 8101, 95011, 92004, 1000, 3, 0, '2031-02-03 10:00:00'], ['PERFT2', 8101, 95011, 92004, 500, 4, 0, '2031-02-04 10:00:00'], // 已取消:都不计 ['PERFT3', 8102, 95012, 92004, 700, 5, 0, '2031-02-10 09:00:00'], ['PERFT4', 8103, 95013, 92009, 2500, 3, 300, '2031-02-10 15:00:00'], // 部分退款:医助诊金计入,医生成交金额不计 ['PERFT5', 8104, 95012, 92009, 400, 1, 0, '2031-02-20 11:00:00'], ]; foreach ($orders as [$no, $rx, $diag, $creator, $amount, $status, $refund, $time]) { Db::name('tcm_prescription_order')->insert(['order_no' => $no, 'prescription_id' => $rx, 'diagnosis_id' => $diag, 'creator_id' => $creator, 'amount' => $amount, 'fulfillment_status' => $status, 'refund_amount' => $refund, 'create_time' => strtotime($time)]); } // 已完成的挂号(面诊完成数,后台列名“接诊单数”):王医助 2 人次、钱医助 1 人次——与订单数无关 Db::name('doctor_appointment')->whereIn('id', [96201, 96202, 96203])->delete(); foreach ([96201 => [95011, 92002, 92004, '2031-02-03'], 96202 => [95012, 92003, 92004, '2031-02-10'], 96203 => [95013, 92003, 92009, '2031-02-10']] as $id => [$diag, $doctor, $assistant, $day]) { Db::name('doctor_appointment')->insert(['id' => $id, 'patient_id' => $diag, 'doctor_id' => $doctor, 'assistant_id' => $assistant, 'roster_id' => 0, 'appointment_date' => $day, 'period' => 'morning', 'appointment_time' => '09:00:00', 'appointment_type' => 'video', 'status' => 3, 'remark' => '', 'channel_source' => '', 'create_time' => $now, 'update_time' => $now]); } \think\facade\Cache::clear(); // ---------------------------------------------------------------- HTTP 工具 function aiMcpPerfHttp(string $url, array $body, array $headers): array { $ch = curl_init($url); $lines = ['Content-Type: application/json']; foreach ($headers as $k => $v) { $lines[] = $k . ': ' . $v; } curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $lines, CURLOPT_TIMEOUT => 120, CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE)]); $raw = (string) curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); return [$status, json_decode($raw, true)]; } $grant = static function (string $account) use ($base): string { [, $body] = aiMcpPerfHttp($base . '/mcp/auth/grant', ['account' => $account, 'password' => 'Test@123456', 'client' => 'xingzhi', 'client_instance' => 'perf-test'], []); aiMcpPerfExpect(($body['code'] ?? null) === 1, "grant for {$account}: " . json_encode($body, JSON_UNESCAPED_UNICODE)); return (string) $body['data']['token']; }; $rpc = static function (string $token, string $method, array $params = []) use ($base): array { static $id = 0; [$status, $body] = aiMcpPerfHttp($base . '/mcp', ['jsonrpc' => '2.0', 'id' => ++$id, 'method' => $method, 'params' => $params], ['Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json, text/event-stream', 'X-Xingzhi-Task-Id' => 'perf-task']); aiMcpPerfExpect($status === 200 && isset($body['result']), "{$method} should return a result, got HTTP {$status}: " . json_encode($body, JSON_UNESCAPED_UNICODE)); return $body['result']; }; $toolNames = static fn (string $token): array => array_column($rpc($token, 'tools/list')['tools'] ?? [], 'name'); $call = static function (string $token, string $name, array $args) use ($rpc): array { return $rpc($token, 'tools/call', ['name' => $name, 'arguments' => $args]); }; $ok = static function (string $token, string $name, array $args) use ($call): array { $result = $call($token, $name, $args); aiMcpPerfExpect(empty($result['isError']), "{$name} " . json_encode($args, JSON_UNESCAPED_UNICODE) . ' should succeed: ' . ($result['content'][0]['text'] ?? '')); return $result; }; $byId = static fn (array $rows): array => array_column($rows, null, 'admin_id'); $feb = ['start_date' => '2031-02-01', 'end_date' => '2031-02-28']; $mgr = $grant('p_mgr'); $asst = $grant('p_asst_c'); $lim = $grant('p_lim'); $root = $grant('p_root'); // ---------------------------------------------------------------- 工具列表 $names = $toolNames($mgr); foreach (['zyt_perf_assistants', 'zyt_perf_doctors', 'zyt_stats_performance', 'zyt_perf_trend'] as $name) { aiMcpPerfExpect(in_array($name, $names, true), "manager sees {$name}"); } foreach ($rpc($mgr, 'tools/list')['tools'] as $definition) { aiMcpPerfExpect(($definition['annotations']['readOnlyHint'] ?? false) === true, $definition['name'] . ' is annotated read-only'); } $names = $toolNames($asst); aiMcpPerfExpect(in_array('zyt_perf_assistants', $names, true) && !in_array('zyt_perf_doctors', $names, true), 'assistant sees only the tools its permissions allow'); // ---------------------------------------------------------------- 医助排行 $result = $ok($mgr, 'zyt_perf_assistants', $feb); $data = $result['structuredContent']; $rows = $byId($data['rows']); aiMcpPerfExpect(array_keys($rows) === [92009, 92004], 'manager ranks its two assistants by fee: ' . json_encode(array_keys($rows))); aiMcpPerfExpect($rows[92009]['fee_amount'] == 2900 && $rows[92009]['deal_order_count'] === 2 && $rows[92009]['rank'] === 1, 'partially refunded order counts for the assistant fee'); aiMcpPerfExpect($rows[92004]['fee_amount'] == 1700 && $rows[92004]['deal_order_count'] === 2, 'cancelled order is excluded from the assistant fee'); aiMcpPerfExpect($data['totals']['fee_amount'] == 4600 && $data['totals']['deal_order_count'] === 4 && $data['assistants'] === 2, 'totals add up: ' . json_encode($data['totals'])); aiMcpPerfExpect(($data['chart']['type'] ?? '') === 'bar' && $data['chart']['labels'] === ['业绩钱医助', '业绩王医助'] && $data['chart']['series'][0]['data'] == [2900, 1700], 'ranking chart: ' . json_encode($data['chart'] ?? null, JSON_UNESCAPED_UNICODE)); aiMcpPerfExpect(str_contains($result['content'][0]['text'], "```chart\n{") && str_contains($result['content'][0]['text'], '业绩钱医助'), 'text result carries the chart block'); $data = $ok($mgr, 'zyt_perf_assistants', $feb + ['name' => '王'])['structuredContent']; aiMcpPerfExpect(count($data['rows']) === 1 && $data['rows'][0]['admin_id'] === 92004 && $data['rows'][0]['rank'] === 2 && !isset($data['chart']), 'name filter keeps the overall rank and draws no one-bar chart'); $data = $ok($mgr, 'zyt_perf_assistants', $feb + ['dept' => 'AI业绩一中心'])['structuredContent']; aiMcpPerfExpect(array_column($data['rows'], 'admin_id') === [92004] && $data['totals']['fee_amount'] == 1700, 'department name filter'); $data = $ok($mgr, 'zyt_perf_assistants', $feb + ['sort_by' => 'deal_order_count', 'top' => 1])['structuredContent']; aiMcpPerfExpect(count($data['rows']) === 1 && $data['rows'][0]['admin_id'] === 92009, 'sort_by with tie falls back to fee; top limits rows'); $data = $ok($root, 'zyt_perf_assistants', $feb + ['dept' => 'AI业绩一中心,AI业绩三中心'])['structuredContent']; $ids = array_column($data['rows'], 'admin_id'); sort($ids); aiMcpPerfExpect($ids === [92004, 92009], 'root with two department names sees both assistants'); $data = $ok($asst, 'zyt_perf_assistants', $feb)['structuredContent']; aiMcpPerfExpect(array_column($data['rows'], 'admin_id') === [92004] && $data['totals']['fee_amount'] == 1700, 'assistant with self data scope only sees itself'); // 指标名称:后台“接诊单数”是面诊完成人次,不能被说成订单数(线上把 96 人次面诊说成了 96 单) $result = $ok($mgr, 'zyt_perf_assistants', $feb + ['sort_by' => 'consult_count']); $data = $result['structuredContent']; aiMcpPerfExpect(str_contains($data['columns']['consult_count'], '面诊完成数') && str_contains($data['columns']['consult_count'], '接诊单数') && str_contains($data['columns']['deal_order_count'], '成交订单数') && str_contains($data['definitions']['consult_count'], '不是订单数'), 'metric names say what is counted: ' . json_encode($data['columns'], JSON_UNESCAPED_UNICODE)); aiMcpPerfExpect(($data['chart']['title'] ?? '') === '医助面诊完成数排行' && $data['chart']['unit'] === '人次' && $data['chart']['series'][0]['data'] == [2, 1] && str_contains($data['chart']['note'], '不是订单数'), 'completed consultations chart: ' . json_encode($data['chart'] ?? null, JSON_UNESCAPED_UNICODE)); aiMcpPerfExpect(str_contains($result['content'][0]['text'], '不是订单数') && str_contains($result['content'][0]['text'], '成交订单 4 单'), 'summary warns that consultations are not orders'); // 只看一个人:附上处方业务订单列表(按医助筛选)的数字,列表含已取消的 1 单,业绩不含 $result = $ok($mgr, 'zyt_perf_assistants', $feb + ['name' => '王']); $check = $result['structuredContent']['order_list'] ?? null; aiMcpPerfExpect($check !== null && $check['total'] === 3 && $check['amount'] == 2200 && $check['performance_amount'] == 1700 && $check['not_counted'] === 1, 'single assistant carries the order-list reconciliation: ' . json_encode($check, JSON_UNESCAPED_UNICODE)); aiMcpPerfExpect(str_contains($result['content'][0]['text'], '对账') && str_contains($result['content'][0]['text'], '成交订单是 2 单'), 'reconciliation is spelled out in the text'); $data = $ok($mgr, 'zyt_perf_doctors', $feb + ['name' => '张'])['structuredContent']; aiMcpPerfExpect(!isset($data['order_list']), 'doctor reconciliation is skipped for accounts that only see their own orders in the list'); $check = $ok($root, 'zyt_perf_doctors', $feb + ['name' => '业绩张'])['structuredContent']['order_list'] ?? null; aiMcpPerfExpect($check !== null && $check['total'] === 3 && $check['not_counted'] === 1, 'doctor reconciliation for accounts that see all orders: ' . json_encode($check, JSON_UNESCAPED_UNICODE)); // ---------------------------------------------------------------- 医生排行 $data = $ok($mgr, 'zyt_perf_doctors', $feb)['structuredContent']; $rows = $byId($data['rows']); aiMcpPerfExpect(array_keys($rows) === [92002, 92003], 'manager sees the two doctors of its department, ranked: ' . json_encode(array_keys($rows))); aiMcpPerfExpect($rows[92002]['deal_amount'] == 1400 && $rows[92002]['deal_order_count'] === 2, 'doctor amount excludes cancelled orders'); aiMcpPerfExpect($rows[92003]['deal_amount'] == 700 && $rows[92003]['deal_order_count'] === 1, 'doctor amount excludes refunded orders'); aiMcpPerfExpect(($data['chart']['labels'] ?? []) === ['业绩张医生', '业绩李医生'] && ($data['chart']['unit'] ?? '') === '元', 'doctor ranking chart'); // ---------------------------------------------------------------- 部门业绩看板 $data = $ok($mgr, 'zyt_stats_performance', $feb)['structuredContent']; $amounts = array_column($data['rows'], 'performance_amount', 'dept_id'); aiMcpPerfExpect(($amounts[9922] ?? null) == 1700 && ($amounts[9923] ?? null) == 2900, 'department board: ' . json_encode($amounts)); aiMcpPerfExpect(($data['chart']['series'][0]['data'] ?? []) == [2900, 1700], 'department chart sorted by amount'); // ---------------------------------------------------------------- 业绩走势 $data = $ok($mgr, 'zyt_perf_trend', $feb + ['names' => '业绩王医助,业绩钱医助', 'granularity' => 'day'])['structuredContent']; aiMcpPerfExpect(count($data['periods']) === 28 && $data['periods'][0] === '02-01' && count($data['series']) === 2, 'daily buckets for February'); [$wang, $qian] = $data['series']; aiMcpPerfExpect($wang['admin_id'] === 92004 && $wang['amount'][2] == 1000 && $wang['amount'][9] == 700 && $wang['total_amount'] == 1700 && $wang['total_orders'] === 2, 'assistant daily trend: ' . json_encode($wang)); aiMcpPerfExpect($qian['amount'][9] == 2500 && $qian['amount'][19] == 400 && $qian['total_amount'] == 2900, 'second series'); aiMcpPerfExpect(($data['chart']['type'] ?? '') === 'line' && count($data['chart']['series']) === 2 && count($data['chart']['labels']) === 28, 'trend chart is a two-series line'); $data = $ok($mgr, 'zyt_perf_trend', $feb)['structuredContent']; aiMcpPerfExpect(count($data['series']) === 1 && $data['series'][0]['total_amount'] == 4600 && $data['granularity'] === 'day', 'trend without names sums the visible scope'); $data = $ok($mgr, 'zyt_perf_trend', $feb + ['names' => '业绩王医助', 'granularity' => 'week'])['structuredContent']; aiMcpPerfExpect($data['periods'] === ['02-01~02-02', '02-03~02-09', '02-10~02-16', '02-17~02-23', '02-24~02-28'] && $data['series'][0]['amount'] == [0, 1000, 700, 0, 0], 'weekly buckets start on Monday: ' . json_encode($data['periods'])); $data = $ok($mgr, 'zyt_perf_trend', $feb + ['granularity' => 'month', 'metric' => 'orders'])['structuredContent']; aiMcpPerfExpect($data['periods'] === ['2月'] && ($data['chart']['type'] ?? '') === 'column' && $data['chart']['series'][0]['data'] == [4], 'single month becomes a column chart of order counts'); $data = $ok($mgr, 'zyt_perf_trend', $feb + ['by' => 'doctor'])['structuredContent']; aiMcpPerfExpect($data['series'][0]['total_amount'] == 2100 && $data['series'][0]['total_orders'] === 3, 'doctor trend uses the doctor amount rule'); $data = $ok($asst, 'zyt_perf_trend', $feb)['structuredContent']; aiMcpPerfExpect($data['series'][0]['name'] === '业绩王医助' && $data['series'][0]['total_amount'] == 1700, 'assistant trend is named after itself'); $result = $call($asst, 'zyt_perf_trend', $feb + ['names' => '业绩钱医助']); aiMcpPerfExpect(!empty($result['isError']) && str_contains($result['content'][0]['text'], '找不到'), 'assistant cannot look at a colleague outside its scope'); $result = $call($mgr, 'zyt_perf_trend', $feb + ['names' => 'a,b,c,d,e']); aiMcpPerfExpect(!empty($result['isError']) && str_contains($result['content'][0]['text'], '最多'), 'at most four people'); // ---------------------------------------------------------------- 参数校验 foreach ([['sort_by' => 'nope'], ['period' => 'someday'], ['start_date' => '2031-02-10', 'end_date' => '2031-02-01'], ['start_date' => '2031/02/01'], ['dept' => '不存在的部门']] as $bad) { $result = $call($mgr, 'zyt_perf_assistants', $bad); aiMcpPerfExpect(!empty($result['isError']), 'invalid arguments are rejected: ' . json_encode($bad, JSON_UNESCAPED_UNICODE)); } $data = $ok($mgr, 'zyt_perf_assistants', ['period' => 'this_month'])['structuredContent']; aiMcpPerfExpect($data['period']['start_date'] === date('Y-m-01') && $data['period']['end_date'] === date('Y-m-d'), 'period shortcut resolves on the server'); // ---------------------------------------------------------------- 权限:未登记时回退到 Tab 权限 aiMcpPerfExpect(!in_array('zyt_perf_assistants', $toolNames($lim), true), 'tab permission alone does not open the leaderboard while its own permission is registered'); $result = $call($lim, 'zyt_perf_assistants', $feb); aiMcpPerfExpect(!empty($result['isError']) && str_contains($result['content'][0]['text'], '无权限'), 'calling it anyway is denied'); Db::name('system_menu')->where('id', $leaderboardMenu)->update(['is_disable' => 1]); try { aiMcpPerfExpect(in_array('zyt_perf_assistants', $toolNames($lim), true), 'with the leaderboard permission unregistered, the tab permission applies'); $data = $ok($lim, 'zyt_perf_assistants', $feb)['structuredContent']; aiMcpPerfExpect(count($data['rows']) === 2, 'fallback permission returns the same data scope'); aiMcpPerfExpect(!in_array('zyt_perf_doctors', $toolNames($lim), true), 'no doctor tools without doctor permissions'); } finally { Db::name('system_menu')->where('id', $leaderboardMenu)->update(['is_disable' => 0]); } // ---------------------------------------------------------------- params 里写分页 $result = $ok($root, 'zyt_query', ['resource' => 'doctor.appointment/lists', 'params' => ['start_date' => '2026-01-01', 'end_date' => '2026-12-31', 'page_size' => 2, 'page' => 1]]); aiMcpPerfExpect(($result['structuredContent']['page_size'] ?? 0) === 2, 'page_size inside params is treated as paging'); // 自带分页的统计明细:外层 page/page_size 生效(以前被忽略,翻页永远拿到第一页,看起来像同一批记录反复出现) $lines = static fn (int $page) => $ok($root, 'zyt_query', ['resource' => 'stats.yejiStats/appointmentLines', 'params' => $feb + ['assistant_id' => 92004], 'page' => $page, 'page_size' => 1])['structuredContent']['result']; [$first, $second] = [$lines(1), $lines(2)]; aiMcpPerfExpect(($first['paging'] ?? null) == ['total' => 2, 'page' => 1, 'page_size' => 1, 'has_more' => true] && ($second['paging']['has_more'] ?? null) === false, 'report paging follows the outer page arguments: ' . json_encode([$first['paging'] ?? null, $second['paging'] ?? null])); aiMcpPerfExpect(count($first['lists']) === 1 && count($second['lists']) === 1 && $first['lists'][0]['id'] !== $second['lists'][0]['id'], 'page 2 returns the next record, not page 1 again'); // ---------------------------------------------------------------- 审计 $logged = Db::name('ai_access_log')->where('admin_id', 92001)->where('tool', 'zyt_perf_assistants')->where('client_task_id', 'perf-task')->order('id', 'desc')->find(); aiMcpPerfExpect($logged && $logged['resource'] === 'stats.yejiStats/leaderboard' && $logged['status'] === 'ok', 'performance calls are audited'); echo "AiMcpPerfTest OK\n";