|
| 1 | +import { describe, expect, it } from "@effect/vitest"; |
| 2 | + |
| 3 | +import { jsLiteral, jsString, writeJsonResult } from "./codex-repl"; |
| 4 | + |
| 5 | +// --------------------------------------------------------------------------- |
| 6 | +// Caller arguments are embedded into a JavaScript source text that runs in |
| 7 | +// Codex's REPL. The encoding is the whole security boundary, so it is tested |
| 8 | +// on the values that break naive embeddings rather than on happy text. |
| 9 | +// --------------------------------------------------------------------------- |
| 10 | + |
| 11 | +/** The value the emitted expression produces, without evaluating JS: the |
| 12 | + * expression is `JSON.parse(<js string literal>)`, so unwrap the literal and |
| 13 | + * parse it the same way the REPL would. */ |
| 14 | +const evaluate = (expression: string): unknown => { |
| 15 | + expect(expression.startsWith("JSON.parse(")).toBe(true); |
| 16 | + const literal = expression.slice("JSON.parse(".length, -1); |
| 17 | + // oxlint-disable-next-line executor/no-json-parse -- the subject under test IS the JSON embedding; decoding it exactly as the REPL would is the assertion, and a schema decode would hide the prototype behaviour being checked |
| 18 | + return JSON.parse(JSON.parse(literal) as string); |
| 19 | +}; |
| 20 | + |
| 21 | +describe("jsLiteral", () => { |
| 22 | + it("keeps a prototype-named key as an own key", () => { |
| 23 | + // Built by parsing, not as an object literal: writing `{ __proto__: … }` |
| 24 | + // in source sets the prototype, so a literal-based fixture would test |
| 25 | + // nothing. This is the shape that actually arrives over the wire. |
| 26 | + // oxlint-disable-next-line executor/no-json-parse -- an object literal would assign the PROTOTYPE, so parsing is the only way to build the own key this test is about |
| 27 | + const args: unknown = JSON.parse('{"__proto__":{"polluted":"yes"},"url":"https://x/"}'); |
| 28 | + |
| 29 | + const value = evaluate(jsLiteral(args)) as Record<string, unknown>; |
| 30 | + |
| 31 | + expect(Object.hasOwn(value, "__proto__"), "carried as data").toBe(true); |
| 32 | + expect(Object.getPrototypeOf(value), "and not onto the prototype chain").toBe(Object.prototype); |
| 33 | + expect((value as { polluted?: unknown }).polluted).toBeUndefined(); |
| 34 | + }); |
| 35 | + |
| 36 | + it("survives quotes, newlines, and JS line terminators", () => { |
| 37 | + // U+2028/U+2029 are legal raw inside a JSON string but END A STATEMENT in |
| 38 | + // a JS source text — the case that silently truncates a program. |
| 39 | + const args = { text: 'he said "hi"\n\u2028\u2029 and left', path: "C:\\tmp" }; |
| 40 | + |
| 41 | + expect(evaluate(jsLiteral(args))).toEqual(args); |
| 42 | + }); |
| 43 | + |
| 44 | + it("renders an absent value as null rather than a hole", () => { |
| 45 | + expect(evaluate(jsLiteral(undefined))).toBeNull(); |
| 46 | + }); |
| 47 | +}); |
| 48 | + |
| 49 | +describe("jsString", () => { |
| 50 | + it("escapes line terminators that would end the statement", () => { |
| 51 | + expect(jsString("a\u2028b")).toBe('"a\\u2028b"'); |
| 52 | + // oxlint-disable-next-line executor/no-json-parse -- decoding the emitted JS string literal is the assertion |
| 53 | + expect(JSON.parse(jsString('quote " here')) as string).toBe('quote " here'); |
| 54 | + }); |
| 55 | +}); |
| 56 | + |
| 57 | +describe("writeJsonResult", () => { |
| 58 | + it("scopes the per-call body so a reused REPL session does not redeclare", () => { |
| 59 | + const program = writeJsonResult(["const a = 1;"], "await f(a)"); |
| 60 | + |
| 61 | + expect(program.startsWith("await (async () => {")).toBe(true); |
| 62 | + expect(program).toContain("nodeRepl.write(JSON.stringify(result ?? null));"); |
| 63 | + }); |
| 64 | +}); |
0 commit comments