Skip to content

Commit b1230c9

Browse files
committed
Diagnose Gnippets E2E HTTP failures
1 parent 8e4abef commit b1230c9

3 files changed

Lines changed: 81 additions & 3 deletions

File tree

.github/workflows/cloud-e2e.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ on:
1818
description: Parallel incognito browsers
1919
required: true
2020
default: "2"
21+
scenario:
22+
description: Optional single scenario ID
23+
required: false
24+
type: string
2125

2226
permissions:
2327
contents: read
@@ -44,7 +48,15 @@ jobs:
4448
GNIPPETS_BASE_URL: ${{ vars.GNIPPETS_BASE_URL }}
4549
GNIPPETS_E2E_CONTROL_TOKEN: ${{ secrets.GNIPPETS_E2E_CONTROL_TOKEN }}
4650
CAPSOLVER_API_KEY: ${{ secrets.CAPSOLVER_API_KEY }}
47-
run: node ci/run.mjs --pack "${{ inputs.pack }}" --concurrency "${{ inputs.concurrency }}"
51+
E2E_PACK: ${{ inputs.pack }}
52+
E2E_CONCURRENCY: ${{ inputs.concurrency }}
53+
E2E_SCENARIO: ${{ inputs.scenario }}
54+
run: |
55+
args=(--pack "$E2E_PACK" --concurrency "$E2E_CONCURRENCY")
56+
if [[ -n "$E2E_SCENARIO" ]]; then
57+
args+=(--scenario "$E2E_SCENARIO")
58+
fi
59+
node ci/run.mjs "${args[@]}"
4860
4961
- name: Upload traces, recordings, and rubrics
5062
if: always()

ci/lib/webbrain-client.mjs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,38 @@ export class GnippetsE2EClient {
150150
},
151151
body: body === undefined ? undefined : JSON.stringify(body),
152152
});
153-
const value = await response.json().catch(() => ({}));
154-
if (!response.ok) throw new Error(value.error || `Gnippets E2E returned HTTP ${response.status}.`);
153+
const text = await response.text();
154+
let value;
155+
try { value = text ? JSON.parse(text) : {}; } catch { value = {}; }
156+
if (!response.ok) {
157+
const header = (name) => String(response.headers?.get?.(name) || 'unknown')
158+
.replace(/\s+/g, ' ')
159+
.slice(0, 160);
160+
let preview = text;
161+
if (this.controlToken) preview = preview.split(this.controlToken).join('[redacted]');
162+
preview = preview
163+
.replace(/Bearer\s+[^\s<]+/gi, 'Bearer [redacted]')
164+
.replace(/\s+/g, ' ')
165+
.trim()
166+
.slice(0, 240);
167+
const diagnostics = {
168+
server: header('server'),
169+
cf_ray: header('cf-ray'),
170+
cf_mitigated: header('cf-mitigated'),
171+
content_type: header('content-type'),
172+
body_preview: preview,
173+
};
174+
const summary = Object.entries(diagnostics)
175+
.filter(([, diagnostic]) => diagnostic && diagnostic !== 'unknown')
176+
.map(([name, diagnostic]) => `${name}=${diagnostic}`)
177+
.join('; ');
178+
const error = new Error(
179+
value.error || `Gnippets E2E returned HTTP ${response.status}${summary ? ` (${summary})` : ''}.`,
180+
);
181+
error.status = response.status;
182+
error.body = diagnostics;
183+
throw error;
184+
}
155185
return value;
156186
}
157187

ci/test.mjs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'node:path';
44
import { fileURLToPath } from 'node:url';
55
import { gradeScenario, inferStuckAt, renderSummary } from './lib/grader.mjs';
66
import { buildSessionSettings, resolveCloudRunId, suiteShouldFail } from './lib/suite.mjs';
7+
import { GnippetsE2EClient } from './lib/webbrain-client.mjs';
78

89
const root = path.dirname(fileURLToPath(import.meta.url));
910
const scenarios = JSON.parse(await fs.readFile(path.join(root, 'catalog', 'scenarios.json'), 'utf8'));
@@ -29,6 +30,41 @@ assert.deepEqual(
2930
{ enabled: true, key: 'captcha-key' },
3031
);
3132

33+
const diagnosticSecret = 'diagnostic-secret-that-must-not-leak';
34+
const diagnosticClient = new GnippetsE2EClient({
35+
baseUrl: 'https://gnippets.example',
36+
controlToken: diagnosticSecret,
37+
fetchImpl: async () => ({
38+
ok: false,
39+
status: 403,
40+
headers: {
41+
get(name) {
42+
return {
43+
server: 'cloudflare',
44+
'cf-ray': 'fixture-ray-IST',
45+
'cf-mitigated': 'challenge',
46+
'content-type': 'text/html',
47+
}[name.toLowerCase()] || null;
48+
},
49+
},
50+
async text() {
51+
return `<html><title>Attention Required</title><body>Bearer ${diagnosticSecret}</body></html>`;
52+
},
53+
}),
54+
});
55+
await assert.rejects(
56+
diagnosticClient.createRun('fixture'),
57+
(error) => {
58+
assert.equal(error.status, 403);
59+
assert.equal(error.body.server, 'cloudflare');
60+
assert.equal(error.body.cf_mitigated, 'challenge');
61+
assert.match(error.message, /fixture-ray-IST/);
62+
assert.match(error.message, /Attention Required/);
63+
assert.doesNotMatch(error.message, new RegExp(diagnosticSecret));
64+
return true;
65+
},
66+
);
67+
3268
const mountainScenario = scenarios.find((scenario) => scenario.id === 'wikipedia-table-extraction');
3369
const invalidMountainHeights = gradeScenario({
3470
scenario: mountainScenario,

0 commit comments

Comments
 (0)