Skip to content

Commit c6047c9

Browse files
test: parity and input oracles (#140)
* test: parity and input oracles * style: fix ruff failures in parity oracle tests * clean up: shared component wired * address timon's feedback
1 parent f0fbb8e commit c6047c9

2 files changed

Lines changed: 401 additions & 0 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Part A — Golden-parity: JS renderer path for the adversarial bash tool_result.
3+
*
4+
* The Python md-exporter path is covered by
5+
* ``tests/test_dual_path_parity_oracle.py``. Both suites use the same
6+
* adversarial payload (``<img src=x onerror=alert(1)>``).
7+
*
8+
* These tests drive the **real** JS dispatch path via ``renderToolResult``
9+
* (registry.js) and the direct renderer ``renderBashResult`` (bash.js) — no
10+
* reimplementations.
11+
*
12+
* Oracle discipline (test-review C3/C8/C9):
13+
* Every assertion checks the real observable HTML output, never merely that
14+
* the function does not throw.
15+
*/
16+
17+
import { describe, it, expect } from 'vitest';
18+
import { renderToolResult } from '../registry.js';
19+
import { renderBashResult } from './bash.js';
20+
21+
// Shared adversarial payload — must match the Python constant in
22+
// tests/test_dual_path_parity_oracle.py.
23+
const PAYLOAD = '<img src=x onerror=alert(1)>';
24+
25+
// Hand-built parsed shape for the JS renderer only. Classification and fields from
26+
// ``_parse_tool_result`` are asserted in tests/test_dual_path_parity_oracle.py;
27+
// this file will not catch drift if the Python bash builder changes key names.
28+
const ADVERSARIAL_PARSED = {
29+
result_type: 'bash',
30+
slug: null,
31+
stdout: PAYLOAD,
32+
stderr: '',
33+
exit_code: 0,
34+
interrupted: false,
35+
is_error: false,
36+
return_code_interpretation: 'success',
37+
};
38+
39+
describe('Part A: golden-parity — adversarial bash result (JS renderer path)', () => {
40+
it('renders a non-empty HTML string for the adversarial payload', () => {
41+
// Oracle: output must not be empty — the payload is not silently dropped.
42+
const html = renderToolResult(ADVERSARIAL_PARSED);
43+
expect(typeof html).toBe('string');
44+
expect(html.length).toBeGreaterThan(0);
45+
});
46+
47+
it('HTML-escapes the adversarial payload — raw XSS tag must not appear', () => {
48+
// Oracle: the raw ``<img src=x onerror=`` string must be absent (escaped);
49+
// the HTML-entity form must be present.
50+
//
51+
// Negative control: if esc() were removed from renderBashResult, the raw
52+
// tag WOULD appear in the output and ``not.toContain`` would fail —
53+
// a regressed renderer cannot pass this test.
54+
const html = renderToolResult(ADVERSARIAL_PARSED);
55+
expect(html).not.toContain('<img src=x onerror=');
56+
expect(html).toContain('&lt;img src=x onerror=alert(1)&gt;');
57+
});
58+
59+
it('routes through the real renderBashResult dispatch path — no re-implementation', () => {
60+
// Driving both the registry dispatch (renderToolResult) and the direct
61+
// renderer (renderBashResult) confirms they produce the same output.
62+
// If registry.js ever wired a different renderer for result_type 'bash',
63+
// this equality assertion would fail.
64+
const viaRegistry = renderToolResult(ADVERSARIAL_PARSED);
65+
const viaDirect = renderBashResult(ADVERSARIAL_PARSED);
66+
expect(viaRegistry).toBe(viaDirect);
67+
});
68+
69+
it('negative control: the raw payload is a dangerous string without escaping', () => {
70+
// Documents why esc() is load-bearing: PAYLOAD itself contains the
71+
// executable XSS vector. If the renderer passed it through unchanged,
72+
// the browser could execute it.
73+
expect(PAYLOAD).toContain('<img');
74+
expect(PAYLOAD).toContain('onerror=');
75+
expect(PAYLOAD).not.toContain('&lt;');
76+
77+
// The renderer must NOT pass PAYLOAD through unchanged.
78+
const html = renderBashResult(ADVERSARIAL_PARSED);
79+
expect(html).not.toContain(PAYLOAD);
80+
expect(html).toContain('&lt;');
81+
});
82+
});

0 commit comments

Comments
 (0)