PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.22
MxChat – AI Chatbot & Content Generation for WordPress v3.2.22
3.2.25 3.2.24 3.2.23 3.2.22 3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 All 156 releases
mxchat-basic / tests / verify-aitools-default-off.php

verify-aitools-default-off.php in MxChat – AI Chatbot & Content Generation for WordPress 3.2.22, at tests/verify-aitools-default-off.php

137 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * RELEASE GUARD — AI Tools default-off invariant.
4 *
5 * AI Tools (function calling) MUST default to ZERO active tools on a fresh
6 * install. A site that has never configured the AI Tools section has an empty
7 * mxchat_function_calling_tools map, and that MUST resolve to BOTH:
8 * (a) 0 enabled tools in available_tools(), AND
9 * (b) MxChat_Tool_Registry::is_enabled() === false
10 * These are SEPARATE code paths and CAN diverge — see "why both halves" below.
11 *
12 * Run before releasing mxchat-basic, or after ANY change to
13 * class-mxchat-tool-registry.php or the function-calling defaults:
14 *
15 * wp eval-file wp-content/plugins/mxchat-basic/tests/verify-aitools-default-off.php
16 *
17 * A build that prints FAIL is NOT releasable.
18 *
19 * ---------------------------------------------------------------------------
20 * Why both halves (plan-mxchat-20260716-71525f)
21 *
22 * The 3.2.10 regression: resolve_tool_setting() defaulted an unconfigured tool
23 * to !$is_cautious (every non-sensitive tool ON), so a fresh install rendered
24 * ~10 "active" tools and was one accidental save from turning function calling
25 * fully on. The master gate happened to read the raw (empty) map, so the loop
26 * was NOT firing — the UI was wrong while the gate was right. That is the proof
27 * these two halves diverge: is_enabled() -> count_enabled(enabled_map()) is a
28 * different path from the count check -> available_tools($map). This guard
29 * previously tested only the second one and printed "PASS ... releasable"
30 * having never exercised the half that decides whether the loop runs at all.
31 *
32 * Why this script does NOT write the option
33 *
34 * is_enabled() takes NO ARGUMENTS — it reads the live option, so is_enabled(array())
35 * silently ignores the argument and reports on whatever the install happens to
36 * have. The obvious way to test it is to empty the real option and restore it,
37 * but that makes a release guard into a mutating script (an aborted run could
38 * leave a site's tools wiped, and it would be dangerous on prod). enabled_map()
39 * reads through get_option(), so a read-time filter substitutes the map for the
40 * duration of one call WITHOUT touching the database. Nothing is written; there
41 * is nothing to restore; it is safe on any install, including production.
42 *
43 * Why `pre_option_` and NOT `option_` (plan-mxchat-20260717-bc8bba)
44 *
45 * get_option() applies `option_{$name}` ONLY when the option EXISTS in the
46 * database; an absent option returns early through `default_option_{$name}`
47 * and the `option_` filter never fires. On a genuinely fresh install — the
48 * exact state this guard exists to assert — an `option_` substitution
49 * silently no-ops: half 2 reads the real (absent) option and passes untested,
50 * and the positive controls FAIL on a healthy build (the injected map never
51 * arrives, so is_enabled() stays false). `pre_option_` fires unconditionally
52 * and short-circuits get_option() on any non-false return — an empty array()
53 * is not false, so even the empty-map substitution takes. Do NOT "simplify"
54 * this back to `option_`; it only appears to work on installs where the
55 * option already exists.
56 * ---------------------------------------------------------------------------
57 */
58
59 // Never web-accessible: this file lives under wp-content/plugins and would
60 // otherwise be reachable by URL.
61 if (!defined('ABSPATH')) {
62 exit;
63 }
64
65 if (!class_exists('MxChat_Tool_Registry')) {
66 echo "FAIL: MxChat_Tool_Registry not loaded — is mxchat-basic active?\n";
67 return;
68 }
69
70 $opt = MxChat_Tool_Registry::OPT_TOOLS;
71 $pass = true;
72
73 /** Run $fn with the tools option filtered to $map. Reads only — no DB write.
74 * pre_option_ (NOT option_): it fires even when the option is absent from the
75 * DB, which is precisely the fresh-install state under test. See docblock. */
76 $with_map = function (array $map, callable $fn) use ($opt) {
77 $sub = function () use ($map) { return $map; };
78 add_filter('pre_option_' . $opt, $sub, 99);
79 try {
80 return $fn();
81 } finally {
82 remove_filter('pre_option_' . $opt, $sub, 99);
83 }
84 };
85
86 $count_enabled = function (array $tools) {
87 return count(array_filter($tools, function ($t) { return !empty($t['enabled']); }));
88 };
89
90 echo "Site: " . home_url() . "\n";
91 echo "Live option is READ-ONLY here — the map is substituted via a pre_option_ filter.\n\n";
92
93 /* ---- HALF 1: the empty map must resolve to 0 enabled tools ---- */
94 $fresh = MxChat_Tool_Registry::available_tools(array());
95 $fresh_enabled = $count_enabled($fresh);
96 $ok = ($fresh_enabled === 0);
97 $pass = $pass && $ok;
98 printf("%s empty map -> %d of %d tools enabled (MUST be 0)\n",
99 $ok ? 'PASS' : 'FAIL', $fresh_enabled, count($fresh));
100
101 /* ---- HALF 2: the empty map must leave the MASTER GATE off ---- */
102 /* This is the half the guard never tested. is_enabled() reads the live option,
103 so the map has to be substituted for real — passing it an argument does nothing. */
104 $gate = $with_map(array(), function () {
105 return MxChat_Tool_Registry::is_enabled();
106 });
107 $ok = ($gate === false);
108 $pass = $pass && $ok;
109 printf("%s empty map -> is_enabled() === %s (MUST be false)\n",
110 $ok ? 'PASS' : 'FAIL', var_export($gate, true));
111
112 /* ---- POSITIVE CONTROLS ---- */
113 /* Without these, a hard-coded `return false` / `return 0` would pass everything
114 above. A guard that can only ever say "off" is not a guard. */
115 $one = MxChat_Tool_Registry::available_tools(array('mxchat_handle_search_request' => array('enabled' => true)));
116 $one_enabled = $count_enabled($one);
117 $ok = ($one_enabled === 1);
118 $pass = $pass && $ok;
119 printf("%s one tool added -> %d enabled (MUST be 1 — positive control)\n",
120 $ok ? 'PASS' : 'FAIL', $one_enabled);
121
122 $gate_on = $with_map(array('mxchat_handle_search_request' => array('enabled' => true)), function () {
123 return MxChat_Tool_Registry::is_enabled();
124 });
125 $ok = ($gate_on === true);
126 $pass = $pass && $ok;
127 printf("%s one tool added -> is_enabled() === %s (MUST be true — positive control)\n",
128 $ok ? 'PASS' : 'FAIL', var_export($gate_on, true));
129
130 /* ---- Proof we never wrote anything ---- */
131 $live = get_option($opt, array());
132 printf("\nLive option untouched: %d tool row(s) still stored.\n", is_array($live) ? count($live) : 0);
133
134 echo $pass
135 ? "PASS: AI Tools default-off invariant holds on BOTH halves — releasable.\n"
136 : "FAIL: default-off invariant BROKEN — DO NOT release.\n";
137