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
14 changes: 14 additions & 0 deletions src/domain/data/composite_name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ export function composeDataName(
`Vary value at index ${i} must be a non-empty string`,
);
}
if (/[\/\\]/.test(varyValues[i])) {
throw new Error(
`Vary value at index ${i} contains path separator characters: ${
varyValues[i]
}`,
);
}
if (varyValues[i] === "." || varyValues[i] === "..") {
throw new Error(
`Vary value at index ${i} must not be a relative path component: ${
varyValues[i]
}`,
);
}
}

return `${baseName}-${varyValues.join("-")}`;
Expand Down
32 changes: 32 additions & 0 deletions src/domain/data/composite_name_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,35 @@ Deno.test("composeDataName: throws on whitespace-only vary value", () => {
Deno.test("composeDataName: handles numeric-like vary values", () => {
assertEquals(composeDataName("result", ["42"]), "result-42");
});

Deno.test("composeDataName: throws on vary value with forward slash", () => {
assertThrows(
() => composeDataName("result", ["../../etc"]),
Error,
"Vary value at index 0 contains path separator characters",
);
});

Deno.test("composeDataName: throws on vary value with backslash", () => {
assertThrows(
() => composeDataName("result", ["foo\\bar"]),
Error,
"Vary value at index 0 contains path separator characters",
);
});

Deno.test("composeDataName: throws on dot-dot vary value", () => {
assertThrows(
() => composeDataName("result", [".."]),
Error,
"Vary value at index 0 must not be a relative path component",
);
});

Deno.test("composeDataName: throws on single-dot vary value", () => {
assertThrows(
() => composeDataName("result", ["."]),
Error,
"Vary value at index 0 must not be a relative path component",
);
});
60 changes: 60 additions & 0 deletions src/infrastructure/cel/cel_evaluator_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,3 +776,63 @@ Deno.test("CelEvaluator data.listVersions() with vary dimensions", () => {
);
assertEquals(result, [1, 2, 3]);
});

Deno.test("CelEvaluator data.version() converts CEL int (bigint) to number for cache lookup", () => {
// Regression test: CEL represents integers as bigint, but DataCache uses
// number keys. Map.get(1n) !== Map.get(1), so the CelDataNamespace must
// convert to Number() before delegating.
const evaluator = new CelEvaluator();

// Simulate a DataCache-like delegate that uses strict number equality
const versionStore = new Map<number, Record<string, unknown>>();
versionStore.set(1, { id: "d1", attributes: { value: "first" } });
versionStore.set(2, { id: "d2", attributes: { value: "second" } });

const context = {
data: {
version: (
_modelName: string,
_dataName: string,
version: number,
) => {
// This mirrors DataCache.getVersion() — uses Map.get with number keys
return versionStore.get(version) ?? null;
},
},
};

// 3-arg form: data.version(model, name, version)
const v1 = evaluator.evaluate(
'data.version("scanner", "result", 1).attributes.value',
context,
);
assertEquals(v1, "first");

const v2 = evaluator.evaluate(
'data.version("scanner", "result", 2).attributes.value',
context,
);
assertEquals(v2, "second");

// 4-arg form with vary: data.version(model, name, [vary], version)
const varyContext = {
data: {
version: (
_modelName: string,
dataName: string,
version: number,
) => {
if (dataName === "result-prod") {
return versionStore.get(version) ?? null;
}
return null;
},
},
};

const v1Vary = evaluator.evaluate(
'data.version("scanner", "result", ["prod"], 1).attributes.value',
varyContext,
);
assertEquals(v1Vary, "first");
});