Skip to content

Commit de5c921

Browse files
authored
Merge pull request #425 from webbrain-one/codex/diagnose-gnippets-e2e-403
Codex/diagnose gnippets e2e 403
2 parents 83e7c00 + bb4e46f commit de5c921

3 files changed

Lines changed: 89 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: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,44 @@ export class GnippetsE2EClient {
146146
method,
147147
headers: {
148148
authorization: `Bearer ${this.controlToken}`,
149+
accept: 'application/json',
150+
'user-agent': 'Mozilla/5.0 (compatible; WebBrainCloudE2E/1.0; +https://webbrain.cloud)',
149151
...(body === undefined ? {} : { 'content-type': 'application/json' }),
150152
},
151153
body: body === undefined ? undefined : JSON.stringify(body),
152154
});
153-
const value = await response.json().catch(() => ({}));
154-
if (!response.ok) throw new Error(value.error || `Gnippets E2E returned HTTP ${response.status}.`);
155+
const text = await response.text();
156+
let value;
157+
try { value = text ? JSON.parse(text) : {}; } catch { value = {}; }
158+
if (!response.ok) {
159+
const header = (name) => String(response.headers?.get?.(name) || 'unknown')
160+
.replace(/\s+/g, ' ')
161+
.slice(0, 160);
162+
let preview = text;
163+
if (this.controlToken) preview = preview.split(this.controlToken).join('[redacted]');
164+
preview = preview
165+
.replace(/Bearer\s+[^\s<]+/gi, 'Bearer [redacted]')
166+
.replace(/\s+/g, ' ')
167+
.trim()
168+
.slice(0, 240);
169+
const diagnostics = {
170+
server: header('server'),
171+
cf_ray: header('cf-ray'),
172+
cf_mitigated: header('cf-mitigated'),
173+
content_type: header('content-type'),
174+
body_preview: preview,
175+
};
176+
const summary = Object.entries(diagnostics)
177+
.filter(([, diagnostic]) => diagnostic && diagnostic !== 'unknown')
178+
.map(([name, diagnostic]) => `${name}=${diagnostic}`)
179+
.join('; ');
180+
const error = new Error(
181+
value.error || `Gnippets E2E returned HTTP ${response.status}${summary ? ` (${summary})` : ''}.`,
182+
);
183+
error.status = response.status;
184+
error.body = diagnostics;
185+
throw error;
186+
}
155187
return value;
156188
}
157189

ci/test.mjs

Lines changed: 42 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,47 @@ assert.deepEqual(
2930
{ enabled: true, key: 'captcha-key' },
3031
);
3132

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

0 commit comments

Comments
 (0)