@respira/wordpress-mcp-server 8.3.19 → 8.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/TOOL_CATALOG.md +36 -36
- package/dist/__tests__/0e6bd33c-redeem-preserves-per-site-settings.test.d.ts +2 -0
- package/dist/__tests__/0e6bd33c-redeem-preserves-per-site-settings.test.d.ts.map +1 -0
- package/dist/__tests__/0e6bd33c-redeem-preserves-per-site-settings.test.js +91 -0
- package/dist/__tests__/0e6bd33c-redeem-preserves-per-site-settings.test.js.map +1 -0
- package/dist/__tests__/139f7353-put-fallback-dedicated-methods.test.d.ts +30 -0
- package/dist/__tests__/139f7353-put-fallback-dedicated-methods.test.d.ts.map +1 -0
- package/dist/__tests__/139f7353-put-fallback-dedicated-methods.test.js +162 -0
- package/dist/__tests__/139f7353-put-fallback-dedicated-methods.test.js.map +1 -0
- package/dist/__tests__/44fcf6b5-basic-auth-is-not-a-blocked-verb.test.d.ts +2 -0
- package/dist/__tests__/44fcf6b5-basic-auth-is-not-a-blocked-verb.test.d.ts.map +1 -0
- package/dist/__tests__/44fcf6b5-basic-auth-is-not-a-blocked-verb.test.js +207 -0
- package/dist/__tests__/44fcf6b5-basic-auth-is-not-a-blocked-verb.test.js.map +1 -0
- package/dist/__tests__/e105ed25-report-issue-builder-context.test.d.ts +2 -0
- package/dist/__tests__/e105ed25-report-issue-builder-context.test.d.ts.map +1 -0
- package/dist/__tests__/e105ed25-report-issue-builder-context.test.js +115 -0
- package/dist/__tests__/e105ed25-report-issue-builder-context.test.js.map +1 -0
- package/dist/__tests__/respira-outage-blamed-on-the-site.test.d.ts +2 -0
- package/dist/__tests__/respira-outage-blamed-on-the-site.test.d.ts.map +1 -0
- package/dist/__tests__/respira-outage-blamed-on-the-site.test.js +83 -0
- package/dist/__tests__/respira-outage-blamed-on-the-site.test.js.map +1 -0
- package/dist/__tests__/v2-negotiation-ttl-retry.test.d.ts +2 -0
- package/dist/__tests__/v2-negotiation-ttl-retry.test.d.ts.map +1 -0
- package/dist/__tests__/v2-negotiation-ttl-retry.test.js +108 -0
- package/dist/__tests__/v2-negotiation-ttl-retry.test.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +59 -4
- package/dist/config.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +25 -3
- package/dist/server.js.map +1 -1
- package/dist/wordpress-client.d.ts +77 -1
- package/dist/wordpress-client.d.ts.map +1 -1
- package/dist/wordpress-client.js +385 -74
- package/dist/wordpress-client.js.map +1 -1
- package/package.json +2 -2
- package/skills/woo-marketing-campaigns/SKILL.md +128 -0
- package/skills/woo-pricing-promotions/SKILL.md +119 -0
- package/tool-capabilities.json +92 -344
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket 44fcf6b5 — a site behind server-level HTTP Basic Auth was diagnosed
|
|
3
|
+
* as a firewall blocking HTTP verbs, and one of the connector's own probes
|
|
4
|
+
* was the only request that did not send the password.
|
|
5
|
+
*
|
|
6
|
+
* Three separate confident-but-wrong statements, all of the same shape: the
|
|
7
|
+
* connector reported a conclusion it had no evidence for, and withheld the
|
|
8
|
+
* evidence it did have.
|
|
9
|
+
*
|
|
10
|
+
* 1. `isRespiraNamespaceRegistered()` probed /wp-json/respira/v2 with
|
|
11
|
+
* `{ Accept: 'application/json' }` and nothing else. Every other request
|
|
12
|
+
* carries `defaultHeaders`, which is where the site's configured
|
|
13
|
+
* `httpAuth` credential lives, so this one request went out logged out. It
|
|
14
|
+
* came back 401, the probe returned `false`, and the connector told the
|
|
15
|
+
* operator the Respira namespace is not registered and the plugin is
|
|
16
|
+
* "deactivated, deleted, or failing to load". The plugin was active the
|
|
17
|
+
* whole time.
|
|
18
|
+
*
|
|
19
|
+
* 2. The 401 error message asserted that "a firewall, CDN, or maintenance
|
|
20
|
+
* mode may be blocking API access". A 401 is an authentication challenge.
|
|
21
|
+
* Maintenance mode is a 503. Nothing in the response said anything about
|
|
22
|
+
* maintenance.
|
|
23
|
+
*
|
|
24
|
+
* 3. The same message told the operator to add `httpAuth` to their site
|
|
25
|
+
* config without checking whether it was already there. On the reported
|
|
26
|
+
* site it was, and it had been sent on the failing request.
|
|
27
|
+
*
|
|
28
|
+
* 4. `diagnose_connection` graded a 401 on OPTIONS/PUT as
|
|
29
|
+
* `write_method_verdict: 'blocked'` and recommended CDN/WAF method rules,
|
|
30
|
+
* never mentioning Basic Auth and never recording the `WWW-Authenticate`
|
|
31
|
+
* header that says exactly what happened.
|
|
32
|
+
*
|
|
33
|
+
* These assertions fail on unmodified main.
|
|
34
|
+
*/
|
|
35
|
+
import { test, after } from 'node:test';
|
|
36
|
+
import assert from 'node:assert/strict';
|
|
37
|
+
import http from 'node:http';
|
|
38
|
+
import { WordPressClient } from '../wordpress-client.js';
|
|
39
|
+
const servers = [];
|
|
40
|
+
after(() => { for (const s of servers)
|
|
41
|
+
s.close(); });
|
|
42
|
+
const CHALLENGE_HTML = '<!DOCTYPE html><html><head><title>401 Authorization Required</title></head><body>Authorization Required</body></html>';
|
|
43
|
+
const USER = 'staging';
|
|
44
|
+
const PASS = 'hunter2';
|
|
45
|
+
const EXPECTED = 'Basic ' + Buffer.from(`${USER}:${PASS}`).toString('base64');
|
|
46
|
+
/**
|
|
47
|
+
* A site whose Basic Auth block is scoped by method — the shape that produces
|
|
48
|
+
* exactly the reported symptom, where reads work and every write is
|
|
49
|
+
* challenged (Apache `<Limit>` inside the auth block, nginx `limit_except`
|
|
50
|
+
* inside the protected location).
|
|
51
|
+
*/
|
|
52
|
+
async function startMethodScopedBasicAuthSite(opts) {
|
|
53
|
+
const seen = [];
|
|
54
|
+
const server = http.createServer((req, res) => {
|
|
55
|
+
seen.push({
|
|
56
|
+
method: req.method || '',
|
|
57
|
+
url: req.url || '',
|
|
58
|
+
authorization: req.headers.authorization || null,
|
|
59
|
+
});
|
|
60
|
+
if (!['GET', 'HEAD'].includes(req.method || '')) {
|
|
61
|
+
res.writeHead(401, { 'Content-Type': 'text/html', 'WWW-Authenticate': 'Basic realm="Staging"' });
|
|
62
|
+
res.end(CHALLENGE_HTML);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
66
|
+
res.end(JSON.stringify({ ok: true, method: req.method }));
|
|
67
|
+
});
|
|
68
|
+
servers.push(server);
|
|
69
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
70
|
+
const { port } = server.address();
|
|
71
|
+
const client = new WordPressClient({
|
|
72
|
+
id: 'staging',
|
|
73
|
+
name: 'staging',
|
|
74
|
+
url: `http://127.0.0.1:${port}`,
|
|
75
|
+
apiKey: `respira_site_${'x'.repeat(24)}`,
|
|
76
|
+
...(opts.withCredentials ? { httpAuth: { username: USER, password: PASS } } : {}),
|
|
77
|
+
});
|
|
78
|
+
return { client, seen };
|
|
79
|
+
}
|
|
80
|
+
test('the namespace probe sends the configured Basic Auth credential', async () => {
|
|
81
|
+
// A site where EVERY request needs the password, and only /respira/v2
|
|
82
|
+
// exists. The probe is the request under test.
|
|
83
|
+
const seen = [];
|
|
84
|
+
const server = http.createServer((req, res) => {
|
|
85
|
+
seen.push({ method: req.method || '', url: req.url || '', authorization: req.headers.authorization || null });
|
|
86
|
+
if ((req.headers.authorization || '') !== EXPECTED) {
|
|
87
|
+
res.writeHead(401, { 'Content-Type': 'text/html', 'WWW-Authenticate': 'Basic realm="Staging"' });
|
|
88
|
+
res.end(CHALLENGE_HTML);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
92
|
+
res.end(JSON.stringify({ namespace: 'respira/v2', routes: {} }));
|
|
93
|
+
});
|
|
94
|
+
servers.push(server);
|
|
95
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
96
|
+
const { port } = server.address();
|
|
97
|
+
const client = new WordPressClient({
|
|
98
|
+
id: 's', name: 's', url: `http://127.0.0.1:${port}`,
|
|
99
|
+
apiKey: `respira_site_${'x'.repeat(24)}`,
|
|
100
|
+
httpAuth: { username: USER, password: PASS },
|
|
101
|
+
});
|
|
102
|
+
const present = await client.isRespiraNamespaceRegistered();
|
|
103
|
+
const probe = seen.find((s) => s.url.includes('/wp-json/respira/v2'));
|
|
104
|
+
assert.ok(probe, 'the namespace probe must have been issued');
|
|
105
|
+
assert.equal(probe.authorization, EXPECTED, 'the namespace probe went out without the site\'s configured Basic Auth credential');
|
|
106
|
+
assert.equal(present, true, 'and so reported the namespace absent on a site where it is present');
|
|
107
|
+
});
|
|
108
|
+
test('a challenged namespace probe reports "could not tell", not "plugin missing"', async () => {
|
|
109
|
+
// No credential configured: the probe is challenged and genuinely cannot
|
|
110
|
+
// answer. `false` here is a claim about the customer's plugin that the
|
|
111
|
+
// probe has no basis for.
|
|
112
|
+
const server = http.createServer((_req, res) => {
|
|
113
|
+
res.writeHead(401, { 'Content-Type': 'text/html', 'WWW-Authenticate': 'Basic realm="Staging"' });
|
|
114
|
+
res.end(CHALLENGE_HTML);
|
|
115
|
+
});
|
|
116
|
+
servers.push(server);
|
|
117
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
118
|
+
const { port } = server.address();
|
|
119
|
+
const client = new WordPressClient({
|
|
120
|
+
id: 's', name: 's', url: `http://127.0.0.1:${port}`, apiKey: `respira_site_${'x'.repeat(24)}`,
|
|
121
|
+
});
|
|
122
|
+
const present = await client.isRespiraNamespaceRegistered();
|
|
123
|
+
assert.equal(present, null, 'a 401 is the server declining to answer, not an answer of "absent"');
|
|
124
|
+
});
|
|
125
|
+
test('a 401 on a write does not blame maintenance mode', async () => {
|
|
126
|
+
const { client } = await startMethodScopedBasicAuthSite({ withCredentials: true });
|
|
127
|
+
const err = await client.updateMenuItem(42, { position: 2 }).then(() => null, (e) => e);
|
|
128
|
+
assert.ok(err, 'the write must still fail — this is about what it says, not about hiding it');
|
|
129
|
+
assert.equal(err.name, 'http_401_non_wordpress');
|
|
130
|
+
assert.ok(!/maintenance/i.test(err.message), `a 401 is an authentication challenge; maintenance mode is a 503. Message was:\n${err.message}`);
|
|
131
|
+
});
|
|
132
|
+
test('a 401 with no WWW-Authenticate header still does not invent maintenance mode', async () => {
|
|
133
|
+
// This is the branch that literally said "a firewall, CDN, or maintenance
|
|
134
|
+
// mode may be blocking API access". Some proxies strip WWW-Authenticate on
|
|
135
|
+
// the way back, which is exactly when the operator most needs the message
|
|
136
|
+
// to stick to what was observed.
|
|
137
|
+
const server = http.createServer((req, res) => {
|
|
138
|
+
if (!['GET', 'HEAD'].includes(req.method || '')) {
|
|
139
|
+
res.writeHead(401, { 'Content-Type': 'text/html' }); // no WWW-Authenticate
|
|
140
|
+
res.end(CHALLENGE_HTML);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
144
|
+
res.end(JSON.stringify({ ok: true }));
|
|
145
|
+
});
|
|
146
|
+
servers.push(server);
|
|
147
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
148
|
+
const { port } = server.address();
|
|
149
|
+
const client = new WordPressClient({
|
|
150
|
+
id: 's', name: 's', url: `http://127.0.0.1:${port}`, apiKey: `respira_site_${'x'.repeat(24)}`,
|
|
151
|
+
});
|
|
152
|
+
const err = await client.updateMenuItem(1, { position: 1 }).then(() => null, (e) => e);
|
|
153
|
+
assert.ok(!/maintenance/i.test(err.message), `nothing in a 401 is evidence of maintenance mode. Message was:\n${err.message}`);
|
|
154
|
+
assert.match(err.message, /no WWW-Authenticate header/i, 'say what was actually missing instead of guessing what it means');
|
|
155
|
+
});
|
|
156
|
+
test('a 401 on a write says the configured credential was sent', async () => {
|
|
157
|
+
const { client, seen } = await startMethodScopedBasicAuthSite({ withCredentials: true });
|
|
158
|
+
const err = await client.updateMenuItem(42, { position: 2 }).then(() => null, (e) => e);
|
|
159
|
+
// The credential really was on the wire; the message must not pretend otherwise.
|
|
160
|
+
const write = seen.find((s) => s.method === 'PUT' || s.method === 'POST');
|
|
161
|
+
assert.equal(write?.authorization, EXPECTED, 'precondition: the write did carry the credential');
|
|
162
|
+
assert.match(err.message, /httpAuth IS configured/, `the message must say the credential was already configured and sent, not tell the operator to add it. Message was:\n${err.message}`);
|
|
163
|
+
assert.ok(!/^\s*"httpAuth": \{ "username": "your-user"/m.test(err.message), 'and must not hand back the boilerplate "add httpAuth" snippet they already followed');
|
|
164
|
+
});
|
|
165
|
+
test('a 401 with no httpAuth configured still tells you to add it', async () => {
|
|
166
|
+
const { client } = await startMethodScopedBasicAuthSite({ withCredentials: false });
|
|
167
|
+
const err = await client.updateMenuItem(42, { position: 2 }).then(() => null, (e) => e);
|
|
168
|
+
assert.match(err.message, /No httpAuth is configured/);
|
|
169
|
+
assert.match(err.message, /"httpAuth": \{ "username"/);
|
|
170
|
+
assert.match(err.message, /realm "Staging"/, 'the realm the server named is evidence and belongs in the message');
|
|
171
|
+
});
|
|
172
|
+
test('diagnose_connection calls a Basic Auth challenge an auth challenge, not a blocked verb', async () => {
|
|
173
|
+
const { client } = await startMethodScopedBasicAuthSite({ withCredentials: true });
|
|
174
|
+
const report = await client.diagnoseConnection({ probe_timeout_ms: 5000 });
|
|
175
|
+
assert.equal(report.write_method_verdict, 'auth_challenge', 'a 401 on PUT/OPTIONS is the server asking for a password, not the edge blocking a verb');
|
|
176
|
+
assert.equal(report.write_method_blocked, null, 'the verb never reached WordPress, so nothing here proves it is blocked');
|
|
177
|
+
assert.equal(report.http_auth_configured, true, 'whether a credential was configured is part of the evidence');
|
|
178
|
+
assert.equal(report.write_method_probe.put_auth_challenged, true);
|
|
179
|
+
assert.match(String(report.write_method_probe.put_www_authenticate || ''), /Basic/i, 'the WWW-Authenticate header is the discriminator and must be recorded');
|
|
180
|
+
const joined = report.recommendations.join('\n');
|
|
181
|
+
assert.match(joined, /authentication challenge, not a blocked HTTP verb/);
|
|
182
|
+
assert.ok(!/Cloudflare custom rule|Bad Bot - Action Block|Whitelisted URLs/i.test(joined), `must not send the operator into WAF method rules for a password prompt. Recommendations were:\n${joined}`);
|
|
183
|
+
assert.ok(!/^Write-method state is unknown/m.test(joined), 'and must not fall through to the generic unknown branch');
|
|
184
|
+
});
|
|
185
|
+
test('a genuinely blocked verb is still reported as blocked', async () => {
|
|
186
|
+
// Guard against the new branch swallowing the case it sits in front of:
|
|
187
|
+
// a 403 HTML rejection with no WWW-Authenticate is still a method block.
|
|
188
|
+
const server = http.createServer((req, res) => {
|
|
189
|
+
if (!['GET', 'HEAD'].includes(req.method || '')) {
|
|
190
|
+
res.writeHead(403, { 'Content-Type': 'text/html' });
|
|
191
|
+
res.end('<!DOCTYPE html><html><head><title>403 Forbidden</title></head><body>Forbidden</body></html>');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
195
|
+
res.end(JSON.stringify({ ok: true }));
|
|
196
|
+
});
|
|
197
|
+
servers.push(server);
|
|
198
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
199
|
+
const { port } = server.address();
|
|
200
|
+
const client = new WordPressClient({
|
|
201
|
+
id: 's', name: 's', url: `http://127.0.0.1:${port}`, apiKey: `respira_site_${'x'.repeat(24)}`,
|
|
202
|
+
});
|
|
203
|
+
const report = await client.diagnoseConnection({ probe_timeout_ms: 5000 });
|
|
204
|
+
assert.equal(report.write_method_verdict, 'blocked');
|
|
205
|
+
assert.equal(report.write_method_blocked, true);
|
|
206
|
+
});
|
|
207
|
+
//# sourceMappingURL=44fcf6b5-basic-auth-is-not-a-blocked-verb.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"44fcf6b5-basic-auth-is-not-a-blocked-verb.test.js","sourceRoot":"","sources":["../../src/__tests__/44fcf6b5-basic-auth-is-not-a-blocked-verb.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e105ed25-report-issue-builder-context.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/e105ed25-report-issue-builder-context.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for ticket e105ed25: `respira_report_issue` auto-attaches
|
|
3
|
+
* `builder: null` and `builder_version: null` even when the site has an
|
|
4
|
+
* unambiguously detected builder.
|
|
5
|
+
*
|
|
6
|
+
* Reported repro: `respira_get_builder_info` (no args) correctly returns
|
|
7
|
+
* `active_builder: { name: "Beaver Builder", version: "2.11.0.3" }` — the
|
|
8
|
+
* detection works — but the report `reportIssue()` files afterward carries
|
|
9
|
+
* `builder: null, builder_version: null` regardless.
|
|
10
|
+
*
|
|
11
|
+
* Root cause: `/context/builder-info` (see
|
|
12
|
+
* wordpress-plugin/respira-for-wordpress/includes/class-respira-context.php,
|
|
13
|
+
* `get_builder_info()`) returns `{ active_builder: { name, version },
|
|
14
|
+
* detected_builders: [...] }` — there is no top-level `name`/`version` on the
|
|
15
|
+
* response. `reportIssue()` in server.ts read `info?.name` / `info?.version`
|
|
16
|
+
* directly, which is always `undefined`. This is the exact mcp-6.19.6-era
|
|
17
|
+
* mistake already fixed in `extractBuilderContent` and `detectDiviVersion`
|
|
18
|
+
* (wordpress-client.ts), which both read `info.active_builder.name` — just
|
|
19
|
+
* never applied to reportIssue().
|
|
20
|
+
*
|
|
21
|
+
* Mock-adapter plumbing mirrors 429299b5-delete-post-empty-envelope.test.ts.
|
|
22
|
+
* global.fetch is mocked separately because reportIssue() posts to
|
|
23
|
+
* respira.press via fetch(), not through the axios-based WordPressClient.
|
|
24
|
+
*/
|
|
25
|
+
import { test } from 'node:test';
|
|
26
|
+
import assert from 'node:assert/strict';
|
|
27
|
+
import axios from 'axios';
|
|
28
|
+
import { RespiraWordPressServer } from '../server.js';
|
|
29
|
+
let currentHandler = null;
|
|
30
|
+
const originalAdapter = axios.defaults.adapter;
|
|
31
|
+
function installMockAdapter(handler) {
|
|
32
|
+
currentHandler = handler;
|
|
33
|
+
axios.defaults.adapter = ((config) => {
|
|
34
|
+
if (!currentHandler) {
|
|
35
|
+
return Promise.reject(new Error('no mock handler installed'));
|
|
36
|
+
}
|
|
37
|
+
const r = currentHandler(config);
|
|
38
|
+
const response = {
|
|
39
|
+
status: r.status,
|
|
40
|
+
statusText: 'OK',
|
|
41
|
+
headers: { 'content-type': 'application/json' },
|
|
42
|
+
config,
|
|
43
|
+
data: r.data,
|
|
44
|
+
request: {},
|
|
45
|
+
};
|
|
46
|
+
if (r.status >= 200 && r.status < 300) {
|
|
47
|
+
return Promise.resolve(response);
|
|
48
|
+
}
|
|
49
|
+
const err = new Error(`Request failed with status code ${r.status}`);
|
|
50
|
+
err.isAxiosError = true;
|
|
51
|
+
err.response = response;
|
|
52
|
+
err.config = config;
|
|
53
|
+
return Promise.reject(err);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function restoreAdapter() {
|
|
57
|
+
axios.defaults.adapter = originalAdapter;
|
|
58
|
+
currentHandler = null;
|
|
59
|
+
}
|
|
60
|
+
const SITE = {
|
|
61
|
+
id: 'bb-site',
|
|
62
|
+
name: 'Beaver Builder Test Site',
|
|
63
|
+
url: 'https://beaver-example.test',
|
|
64
|
+
apiKey: 'respira_site_testkey1234567890',
|
|
65
|
+
default: true,
|
|
66
|
+
};
|
|
67
|
+
/** What the real plugin returns for a site with an unambiguous active builder. */
|
|
68
|
+
function builderInfoHandler(config) {
|
|
69
|
+
const url = config.url || '';
|
|
70
|
+
if (url.includes('/context/builder-info')) {
|
|
71
|
+
return {
|
|
72
|
+
status: 200,
|
|
73
|
+
data: {
|
|
74
|
+
detected_builders: [
|
|
75
|
+
{ name: 'Beaver Builder', version: '2.11.0.3', confidence: 'high' },
|
|
76
|
+
{ name: 'Gutenberg', version: '18.4', confidence: 'low' },
|
|
77
|
+
],
|
|
78
|
+
active_builder: { name: 'Beaver Builder', version: '2.11.0.3' },
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Any other probe (version negotiation, compatibility) answers harmlessly.
|
|
83
|
+
return { status: 200, data: {} };
|
|
84
|
+
}
|
|
85
|
+
test('respira_report_issue attaches the detected builder instead of null (e105ed25)', async (t) => {
|
|
86
|
+
installMockAdapter(builderInfoHandler);
|
|
87
|
+
const originalFetch = globalThis.fetch;
|
|
88
|
+
let capturedBody = null;
|
|
89
|
+
globalThis.fetch = (async (_url, init) => {
|
|
90
|
+
capturedBody = JSON.parse(init.body);
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
status: 201,
|
|
94
|
+
json: async () => ({ success: true, id: 42, message: 'Filed.' }),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
t.after(() => {
|
|
98
|
+
restoreAdapter();
|
|
99
|
+
globalThis.fetch = originalFetch;
|
|
100
|
+
});
|
|
101
|
+
const server = new RespiraWordPressServer([SITE]);
|
|
102
|
+
const result = await server.dispatchToolCall('wordpress_report_issue', {
|
|
103
|
+
title: 'Builder writes silently no-op on this page',
|
|
104
|
+
brief: 'Building a page on this Beaver Builder site returns success but nothing renders.',
|
|
105
|
+
steps_to_reproduce: 'Called build_page with a hero section, got success: true, page stayed blank.',
|
|
106
|
+
severity: 'major',
|
|
107
|
+
});
|
|
108
|
+
assert.ok(capturedBody, 'the report POST body must have been captured');
|
|
109
|
+
assert.equal(capturedBody.builder, 'Beaver Builder', `builder must be the detected active builder name, not null (got ${JSON.stringify(capturedBody.builder)})`);
|
|
110
|
+
assert.equal(capturedBody.builder_version, '2.11.0.3', `builder_version must be the detected active builder version, not null (got ${JSON.stringify(capturedBody.builder_version)})`);
|
|
111
|
+
assert.equal(result.success, true);
|
|
112
|
+
assert.equal(result.context_attached.builder, 'Beaver Builder');
|
|
113
|
+
assert.equal(result.context_attached.builder_version, '2.11.0.3');
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=e105ed25-report-issue-builder-context.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e105ed25-report-issue-builder-context.test.js","sourceRoot":"","sources":["../../src/__tests__/e105ed25-report-issue-builder-context.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"respira-outage-blamed-on-the-site.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/respira-outage-blamed-on-the-site.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { classifyV2Failure, describeV2Failure } from '../wordpress-client.js';
|
|
4
|
+
/**
|
|
5
|
+
* A customer's agent printed this, and he spent hours acting on it:
|
|
6
|
+
*
|
|
7
|
+
* respira/v2 returned HTTP 500 on this site.
|
|
8
|
+
* The site said: Could not query the database for the schema cache. Retrying.
|
|
9
|
+
* The namespace is registered but the request failed server-side.
|
|
10
|
+
* Check wp-content/debug.log, then open .../wp-json/respira/v2/status.
|
|
11
|
+
*
|
|
12
|
+
* "Could not query the database for the schema cache. Retrying." is PostgREST
|
|
13
|
+
* error PGRST002. It is raised inside the Respira Supabase, reaches the plugin
|
|
14
|
+
* from respira.press/api/mcp/site-token/introspect, and appears nowhere in the
|
|
15
|
+
* WordPress plugin or in the customer's database. Every instruction in that
|
|
16
|
+
* message pointed at the one machine that was working.
|
|
17
|
+
*
|
|
18
|
+
* The mechanical cause here: the body carried
|
|
19
|
+
* `code: "respira_invalid_site_token"`, which names the Respira account service
|
|
20
|
+
* as the source, and classifyV2Failure kept `code` on the `auth` branch but
|
|
21
|
+
* dropped it on the `server` branch. describeV2Failure therefore could not tell
|
|
22
|
+
* a Respira outage from a genuine site-side 500.
|
|
23
|
+
*/
|
|
24
|
+
const SITE = 'https://customer.example';
|
|
25
|
+
const err = (status, data) => ({ response: { status, data } });
|
|
26
|
+
/** The reported payload, byte for byte. */
|
|
27
|
+
const REPORTED_500 = err(500, {
|
|
28
|
+
code: 'respira_invalid_site_token',
|
|
29
|
+
message: 'Could not query the database for the schema cache. Retrying.',
|
|
30
|
+
});
|
|
31
|
+
describe('a Respira account-service failure is not the customer\'s site', () => {
|
|
32
|
+
it('carries the error code through the server branch, as the auth branch already did', () => {
|
|
33
|
+
const failure = classifyV2Failure(REPORTED_500);
|
|
34
|
+
assert.equal(failure.kind, 'server');
|
|
35
|
+
assert.equal(failure.code, 'respira_invalid_site_token', 'the code names the source of the failure and must survive classification');
|
|
36
|
+
});
|
|
37
|
+
it('does not send the reader to their own debug.log for a Respira outage', () => {
|
|
38
|
+
const msg = describeV2Failure(classifyV2Failure(REPORTED_500), SITE);
|
|
39
|
+
assert.ok(!/debug\.log/.test(msg), `sent the customer to their own logs:\n${msg}`);
|
|
40
|
+
});
|
|
41
|
+
it('says plainly that this is a Respira problem, not a site problem', () => {
|
|
42
|
+
const msg = describeV2Failure(classifyV2Failure(REPORTED_500), SITE);
|
|
43
|
+
assert.match(msg, /Respira account-service problem/i);
|
|
44
|
+
assert.match(msg, /not a problem with this WordPress site/i);
|
|
45
|
+
});
|
|
46
|
+
it('attributes the upstream sentence to respira.press rather than to the site', () => {
|
|
47
|
+
const msg = describeV2Failure(classifyV2Failure(REPORTED_500), SITE);
|
|
48
|
+
assert.ok(!/The site said/.test(msg), `misattributed a Respira error to the customer's site:\n${msg}`);
|
|
49
|
+
assert.match(msg, /respira\.press reported/i);
|
|
50
|
+
});
|
|
51
|
+
it('recognises the post-fix code the plugin now returns', () => {
|
|
52
|
+
const msg = describeV2Failure(classifyV2Failure(err(503, {
|
|
53
|
+
code: 'respira_site_token_validation_failed',
|
|
54
|
+
message: 'Respira could not reach its account service to validate this site. This is not a fault on your WordPress site.',
|
|
55
|
+
})), SITE);
|
|
56
|
+
assert.match(msg, /Respira account-service problem/i);
|
|
57
|
+
assert.ok(!/debug\.log/.test(msg), msg);
|
|
58
|
+
});
|
|
59
|
+
it('recognises the introspection endpoint\'s own unavailable code', () => {
|
|
60
|
+
const msg = describeV2Failure(classifyV2Failure(err(503, { code: 'introspection_unavailable', message: 'Retry in a moment.' })), SITE);
|
|
61
|
+
assert.match(msg, /Respira account-service problem/i);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('the site-side 500 message is untouched', () => {
|
|
65
|
+
it('a genuine site-side 500 still gets site-side guidance', () => {
|
|
66
|
+
const msg = describeV2Failure(classifyV2Failure(err(500, { code: 'internal_server_error', message: 'Fatal error in theme functions.php' })), SITE);
|
|
67
|
+
assert.match(msg, /debug\.log/, 'a real site fault still belongs in the site owner\'s logs');
|
|
68
|
+
assert.match(msg, /The site said: Fatal error in theme functions\.php/);
|
|
69
|
+
});
|
|
70
|
+
it('a 500 with no code at all keeps the old behaviour', () => {
|
|
71
|
+
const msg = describeV2Failure(classifyV2Failure(err(500)), SITE);
|
|
72
|
+
assert.match(msg, /debug\.log/);
|
|
73
|
+
});
|
|
74
|
+
it('a 401 carrying the same code is still an auth problem, not an outage', () => {
|
|
75
|
+
// A genuinely invalid or rotated token comes back 401/403. That is a real
|
|
76
|
+
// verdict about the credential and keeps the reconnect instructions.
|
|
77
|
+
const msg = describeV2Failure(classifyV2Failure(err(401, { code: 'respira_invalid_site_token', message: 'token out of date' })), SITE);
|
|
78
|
+
assert.match(msg, /authentication result/i);
|
|
79
|
+
assert.match(msg, /reconnect the site/i);
|
|
80
|
+
assert.ok(!/Respira account-service problem/i.test(msg), msg);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=respira-outage-blamed-on-the-site.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"respira-outage-blamed-on-the-site.test.js","sourceRoot":"","sources":["../../src/__tests__/respira-outage-blamed-on-the-site.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v2-negotiation-ttl-retry.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/v2-negotiation-ttl-retry.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import { WordPressClient } from '../wordpress-client.js';
|
|
5
|
+
/**
|
|
6
|
+
* The permanent-latch bug (tickets 7dce1a8a, f3310791).
|
|
7
|
+
*
|
|
8
|
+
* `getApiVersion()` used to cache a FAILED v2 negotiation exactly like a
|
|
9
|
+
* successful one: `this.negotiatedApiVersion = 'v1'` for the life of the
|
|
10
|
+
* client, with no expiry. A transient 500 on the customer's host — proven
|
|
11
|
+
* transient because their own server logs showed nothing wrong a few minutes
|
|
12
|
+
* later — latched the client to 'v1' and every v2-gated tool then threw the
|
|
13
|
+
* SAME cached error for the rest of the session, without ever sending
|
|
14
|
+
* another HTTP request. 15+ minutes of retries, zero new requests, nothing
|
|
15
|
+
* in the site's server logs to explain why.
|
|
16
|
+
*
|
|
17
|
+
* This test runs a real HTTP server (same pattern as
|
|
18
|
+
* v2-negotiation-through-real-client.test.ts) that always answers the
|
|
19
|
+
* `/wp-json/respira/v2/status` probe with a 500, counts how many times that
|
|
20
|
+
* probe is actually hit, and drives three calls to a v2-gated method
|
|
21
|
+
* spanning a short negotiation-failure TTL (set via
|
|
22
|
+
* RESPIRA_V2_NEGOTIATION_TTL_MS so the test doesn't need to wait a full
|
|
23
|
+
* production-length TTL):
|
|
24
|
+
*
|
|
25
|
+
* call 1 (t=0): no cache yet -> probes for real (count -> 1)
|
|
26
|
+
* call 2 (t=0, again): within the TTL -> reuses the cache (count stays 1)
|
|
27
|
+
* call 3 (t=TTL+): TTL has expired -> probes again (count -> 2)
|
|
28
|
+
*
|
|
29
|
+
* Without the fix, `negotiatedApiVersion` never clears, so call 3 also
|
|
30
|
+
* reuses the cache and the probe count stays at 1 forever — this is the bug.
|
|
31
|
+
*/
|
|
32
|
+
const servers = [];
|
|
33
|
+
const originalTtlEnv = process.env.RESPIRA_V2_NEGOTIATION_TTL_MS;
|
|
34
|
+
after(() => {
|
|
35
|
+
for (const s of servers)
|
|
36
|
+
s.close();
|
|
37
|
+
if (originalTtlEnv === undefined)
|
|
38
|
+
delete process.env.RESPIRA_V2_NEGOTIATION_TTL_MS;
|
|
39
|
+
else
|
|
40
|
+
process.env.RESPIRA_V2_NEGOTIATION_TTL_MS = originalTtlEnv;
|
|
41
|
+
});
|
|
42
|
+
function sleep(ms) {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
}
|
|
45
|
+
/** A server that always fails the v2 status probe and counts how often it's hit. */
|
|
46
|
+
async function serverAlwaysFailingV2Status() {
|
|
47
|
+
let probes = 0;
|
|
48
|
+
const server = http.createServer((req, res) => {
|
|
49
|
+
if (req.url?.startsWith('/wp-json/respira/v2/status'))
|
|
50
|
+
probes++;
|
|
51
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
52
|
+
res.end(JSON.stringify({ code: 'internal_server_error', message: 'transient failure' }));
|
|
53
|
+
});
|
|
54
|
+
servers.push(server);
|
|
55
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
56
|
+
const { port } = server.address();
|
|
57
|
+
const client = new WordPressClient({
|
|
58
|
+
id: 'probe',
|
|
59
|
+
name: 'probe',
|
|
60
|
+
url: `http://127.0.0.1:${port}`,
|
|
61
|
+
apiKey: 'respira_test_key',
|
|
62
|
+
});
|
|
63
|
+
return { client, probeCount: () => probes };
|
|
64
|
+
}
|
|
65
|
+
async function v2FailureMessage(client) {
|
|
66
|
+
try {
|
|
67
|
+
await client.listSnapshots({ post_id: 1 });
|
|
68
|
+
return '(no error thrown)';
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
return String(err?.message ?? err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
describe('v2 negotiation failure TTL', () => {
|
|
75
|
+
it('a failed negotiation is retried after the TTL, not latched for the life of the client', async () => {
|
|
76
|
+
const TTL_MS = 150;
|
|
77
|
+
process.env.RESPIRA_V2_NEGOTIATION_TTL_MS = String(TTL_MS);
|
|
78
|
+
const { client, probeCount } = await serverAlwaysFailingV2Status();
|
|
79
|
+
// Call 1: nothing cached yet — must hit the server.
|
|
80
|
+
const msg1 = await v2FailureMessage(client);
|
|
81
|
+
assert.equal(probeCount(), 1, 'first call must send a real probe');
|
|
82
|
+
assert.match(msg1, /returned HTTP 500/);
|
|
83
|
+
// Call 2: well within the TTL — must reuse the cached failure, NOT
|
|
84
|
+
// send a second request.
|
|
85
|
+
const msg2 = await v2FailureMessage(client);
|
|
86
|
+
assert.equal(probeCount(), 1, 'a call inside the TTL window must not re-probe');
|
|
87
|
+
assert.match(msg2, /returned HTTP 500/);
|
|
88
|
+
// Past the TTL: the cached failure must be discarded and a fresh probe
|
|
89
|
+
// sent. This is the assertion that fails without the fix — the old code
|
|
90
|
+
// never clears `negotiatedApiVersion` on failure, so probeCount stays 1
|
|
91
|
+
// forever no matter how long the test waits.
|
|
92
|
+
await sleep(TTL_MS + 150);
|
|
93
|
+
const msg3 = await v2FailureMessage(client);
|
|
94
|
+
assert.equal(probeCount(), 2, 'a call after the TTL must send a new probe instead of reusing the stale failure');
|
|
95
|
+
assert.match(msg3, /returned HTTP 500/);
|
|
96
|
+
});
|
|
97
|
+
it('a cached failure says it is cached and when it was first observed; a fresh one does not', async () => {
|
|
98
|
+
const TTL_MS = 500;
|
|
99
|
+
process.env.RESPIRA_V2_NEGOTIATION_TTL_MS = String(TTL_MS);
|
|
100
|
+
const { client } = await serverAlwaysFailingV2Status();
|
|
101
|
+
const fresh = await v2FailureMessage(client);
|
|
102
|
+
assert.doesNotMatch(fresh, /cached result/i, 'the call that actually made the request must not call itself cached');
|
|
103
|
+
const cached = await v2FailureMessage(client);
|
|
104
|
+
assert.match(cached, /cached result/i, 'a call reusing the cached failure must say so');
|
|
105
|
+
assert.match(cached, /first seen failing at/i, 'it must say when the failure was first observed');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=v2-negotiation-ttl-retry.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v2-negotiation-ttl-retry.test.js","sourceRoot":"","sources":["../../src/__tests__/v2-negotiation-ttl-retry.test.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,eAAO,MAAM,UAAU,QAA8B,CAAC;AACtD,eAAO,MAAM,WAAW,QAAkC,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAmBpD;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAErD;AAaD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,mBAAmB,EAAE,EACpC,QAAQ,EAAE,mBAAmB,EAAE,GAC9B,mBAAmB,EAAE,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|