Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"scripts": {
"build": "tsc -p src",
"test": "npm run build && tsc -p test && node --test out/validate.test.js out/zod.test.js tests/model.test.mjs",
"test": "npm run build && tsc -p test && node --test out/validate.test.js out/zod.test.js out/program.test.js tests/model.test.mjs",
"build-all": "npm run build --workspaces",
"prepare": "npm run build-all",
"prepublishOnly": "node -e \"require('fs').copyFileSync('../SECURITY.md','SECURITY.md')\"",
Expand Down
5 changes: 3 additions & 2 deletions typescript/src/ts/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function createModuleTextFromProgram(jsonObject: object): Result<string>
function objectToString(obj: Record<string, unknown>) {
if (obj.hasOwnProperty("@ref")) {
const index = obj["@ref"];
if (typeof index === "number" && index < currentStep && Object.keys(obj).length === 1) {
if (typeof index === "number" && Number.isInteger(index) && index >= 0 && index < currentStep && Object.keys(obj).length === 1) {
return `step${index + 1}`;
}
}
Expand Down Expand Up @@ -160,9 +160,10 @@ export async function evaluateJsonProgram(program: Program, onCall: (func: strin
async function evaluateObject(obj: Record<string, unknown>) {
if (obj.hasOwnProperty("@ref")) {
const index = obj["@ref"];
if (typeof index === "number" && index < results.length) {
if (Object.keys(obj).length === 1 && typeof index === "number" && Number.isInteger(index) && index >= 0 && index < results.length) {
return results[index];
}
throw new Error(`Invalid result reference: ${String(index)}`);
}
Comment thread
TalZaccai marked this conversation as resolved.
else if (obj.hasOwnProperty("@func")) {
const func = obj["@func"];
Expand Down
106 changes: 106 additions & 0 deletions typescript/test/program.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { evaluateJsonProgram, createModuleTextFromProgram, Program } from "../dist/ts/index.js";

// ---------------------------------------------------------------------------
// evaluateJsonProgram result-reference bounds checking
// ---------------------------------------------------------------------------

describe("evaluateJsonProgram result references", () => {
async function run(program: Program) {
const calls: Array<{ func: string; args: unknown[] }> = [];
const result = await evaluateJsonProgram(program, async (func, args) => {
calls.push({ func, args });
return { func, args };
});
return { result, calls };
}

it("resolves a valid reference to a preceding step", async () => {
const program: Program = {
"@steps": [
{ "@func": "first" },
{ "@func": "second", "@args": [{ "@ref": 0 }] },
],
};
const { calls } = await run(program);
assert.deepEqual(calls[1].args, [{ func: "first", args: [] }]);
});

it("throws on a negative reference instead of yielding undefined", async () => {
const program: Program = {
"@steps": [
{ "@func": "first" },
{ "@func": "second", "@args": [{ "@ref": -1 }] },
],
};
await assert.rejects(run(program), /Invalid result reference/);
});

it("throws on a non-integer reference", async () => {
const program: Program = {
"@steps": [
{ "@func": "first" },
{ "@func": "second", "@args": [{ "@ref": 0.5 }] },
],
};
await assert.rejects(run(program), /Invalid result reference/);
});

it("throws on an out-of-upper-bound reference", async () => {
const program: Program = {
"@steps": [
{ "@func": "first" },
{ "@func": "second", "@args": [{ "@ref": 5 }] },
],
};
await assert.rejects(run(program), /Invalid result reference/);
});
});

// ---------------------------------------------------------------------------
// createModuleTextFromProgram result-reference bounds checking
// ---------------------------------------------------------------------------

describe("createModuleTextFromProgram result references", () => {
it("emits a step reference for a valid index", () => {
const result = createModuleTextFromProgram({
"@steps": [
{ "@func": "first" },
{ "@func": "second", "@args": [{ "@ref": 0 }] },
],
});
assert.equal(result.success, true);
assert.match((result as { data: string }).data, /step1/);
});

it("rejects a negative index", () => {
const result = createModuleTextFromProgram({
"@steps": [
{ "@func": "first" },
{ "@func": "second", "@args": [{ "@ref": -1 }] },
],
});
assert.equal(result.success, false);
});

it("rejects a non-integer index", () => {
const result = createModuleTextFromProgram({
"@steps": [
{ "@func": "first" },
{ "@func": "second", "@args": [{ "@ref": 0.5 }] },
],
});
assert.equal(result.success, false);
});
Comment thread
Copilot marked this conversation as resolved.

it("rejects an out-of-upper-bound index", () => {
const result = createModuleTextFromProgram({
"@steps": [
{ "@func": "first" },
{ "@func": "second", "@args": [{ "@ref": 5 }] },
],
});
assert.equal(result.success, false);
});
});