jev-layer 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { normalizeImageAssets } from "../src/images.mjs";
4
+ import { OpenRouterDecisionsProvider, TypeSafeProvider } from "../src/providers/typesafe.mjs";
5
+ import { routeRequest } from "../src/route.mjs";
6
+ import { PNG as png } from "./image-fixture.mjs";
7
+
8
+ const reference = "frame-6e1d0b7c";
9
+ const image = { type: "image", format: "png", data_base64: png };
10
+ const assets = { [reference]: image };
11
+ const candidates = [{ id: "search", name: "Search", kind: "mcp", description: "Search files", permissions: ["read"], risk: "low" }];
12
+ const request = { intent: "Inspect the image and choose a capability", actor_permissions: ["read"], capabilities: candidates, assets };
13
+ const questions = { visible: { type: "noul", instructions: "Is the target visible?" }, target: { type: "box", instructions: "the target in `screenshot`" } };
14
+
15
+ // Keep provider resolution deterministic without using developer credentials.
16
+ for (const key of Object.keys(process.env)) {
17
+ if (/^(JEV_|TYPESAFE_|OPENROUTER_)/.test(key)) delete process.env[key];
18
+ }
19
+
20
+ test("image reference names cannot change the output object's prototype", () => {
21
+ const special = normalizeImageAssets(JSON.parse(`{"__proto__":${JSON.stringify(image)}}`));
22
+ assert.equal(Object.hasOwn(special, "__proto__"), true);
23
+ assert.equal(Object.getPrototypeOf(special), Object.prototype);
24
+ });
25
+
26
+ test("image trust boundary rejects malformed maps, references, formats and noncanonical base64", () => {
27
+ for (const input of [null, [], "file.png", { "": image }, { " ": image }, { ["é".repeat(513)]: image }]) {
28
+ assert.throws(() => normalizeImageAssets(input));
29
+ }
30
+ for (const invalid of [
31
+ { ...image, format: "webp" }, { ...image, format: "jpg" }, { ...image, type: "video" },
32
+ { ...image, part: "file" }, { ...image, url: "https://example.test/image.png" },
33
+ { ...image, data_base64: `data:image/png;base64,${png}` },
34
+ { ...image, data_base64: `${png}\n` }, { ...image, data_base64: "AB==" },
35
+ { ...image, data_base64: "!!!!" }, { ...image, data_base64: png.slice(0, -4) },
36
+ ]) assert.throws(() => normalizeImageAssets({ [reference]: invalid }));
37
+ const manyReferences = Object.fromEntries(Array.from({ length: 65 }, (_, i) => [`${i}-${"x".repeat(1018)}`, image]));
38
+ assert.throws(() => normalizeImageAssets(manyReferences), /references/);
39
+ });
40
+
41
+ test("image dimensions, animation and decoded byte limits are checked locally", () => {
42
+ for (const [width, height] of [[256, 256], [255, 400], [400, 1569]]) {
43
+ const bytes = Buffer.from(png, "base64");
44
+ bytes.writeUInt32BE(width, 16);
45
+ bytes.writeUInt32BE(height, 20);
46
+ assert.throws(() => normalizeImageAssets({ [reference]: { ...image, data_base64: bytes.toString("base64") } }), /dimensions/);
47
+ }
48
+ const bytes = Buffer.from(png, "base64");
49
+ const animation = Buffer.alloc(20);
50
+ animation.writeUInt32BE(8);
51
+ animation.write("acTL", 4);
52
+ const animated = Buffer.concat([bytes.subarray(0, 33), animation, bytes.subarray(33)]).toString("base64");
53
+ assert.throws(() => normalizeImageAssets({ [reference]: { ...image, data_base64: animated } }), /animated/);
54
+ const tooLarge = "A".repeat(4 * Math.ceil((16 * 1024 * 1024 + 1) / 3));
55
+ assert.throws(() => normalizeImageAssets({ [reference]: { ...image, data_base64: tooLarge } }), /16 MiB/);
56
+ });
57
+
58
+ for (const Provider of [TypeSafeProvider, OpenRouterDecisionsProvider]) {
59
+ test(`${Provider.name} rejects unreferenced images and oversized requests before HTTP`, async () => {
60
+ let calls = 0;
61
+ const provider = new Provider({ apiKey: "fixture-key", fetchImpl: async () => { calls += 1; return Response.json({ answers: {} }); } });
62
+ await assert.rejects(provider.evaluate({ state: { [reference]: "not an image reference" }, questions, assets }), /must be referenced/);
63
+ await assert.rejects(provider.evaluate({ state: "x".repeat(32 * 1024 * 1024), questions: {} }), /32 MiB/);
64
+ assert.equal(calls, 0);
65
+ });
66
+ }
67
+
68
+ for (const Provider of [TypeSafeProvider, OpenRouterDecisionsProvider]) {
69
+ test(`${Provider.name} image errors remain fail-open without leaking bytes`, async () => {
70
+ for (const settings of [
71
+ { apiKey: "", fetchImpl: () => { throw new Error("must not call HTTP"); } },
72
+ { apiKey: "fixture-key", fetchImpl: async () => new Response(png, { status: 400 }) },
73
+ { apiKey: "fixture-key", fetchImpl: async () => new Response(png) },
74
+ { apiKey: "fixture-key", fetchImpl: async () => { throw new Error(png); } },
75
+ { apiKey: "fixture-key", fetchImpl: async () => { throw new DOMException("fixture timeout", "TimeoutError"); } },
76
+ ]) {
77
+ const result = await routeRequest(request, { provider: new Provider(settings) });
78
+ assert.equal(result.status, "fallback");
79
+ assert.equal(result.fallback.type, "provider_error");
80
+ assert.equal(result.execution.enabled, false);
81
+ assert.equal(JSON.stringify(result).includes(png), false);
82
+ }
83
+ });
84
+ }
85
+
@@ -29,11 +29,11 @@ test("demo routes deterministically without a network call or credentials", asyn
29
29
  assert.equal(globalThis.fetch.mock.callCount(), 0);
30
30
  });
31
31
 
32
- for (const [provider, endpoint, model] of [
33
- ["typesafe", "https://api.typesafe.ai/v1/systemone", "jev-latest"],
34
- ["openrouter", "https://openrouter.ai/api/alpha/decisions", "typesafe/jev-1.13"],
32
+ for (const [provider, endpoint] of [
33
+ ["typesafe", "https://api.typesafe.ai/v1/systemone"],
34
+ ["openrouter", "https://openrouter.ai/api/alpha/decisions"],
35
35
  ]) {
36
- test(`${provider} routes with the documented endpoint, model, auth and Choice contract`, async () => {
36
+ test(`${provider} routes with the documented endpoint, auth and Choice contract`, async () => {
37
37
  let calls = 0;
38
38
  const decision = await routeRequest(request, {
39
39
  provider,
@@ -53,13 +53,12 @@ for (const [provider, endpoint, model] of [
53
53
  assert.equal(init.headers["X-Title"], "Jev fixture");
54
54
  }
55
55
  const body = JSON.parse(init.body);
56
- assert.equal(body.model, model);
57
56
  assert.equal(body.state.intent, request.intent);
58
57
  assert.equal(body.questions.tool.type, "choice");
59
58
  assert.deepEqual(Object.keys(body.questions.tool.criteria), ["search", "clock"]);
60
59
  assert.equal(body.questions.tool.criteria.search.description, request.capabilities[0].description);
61
60
  assert.equal("messages" in body, false);
62
- return Response.json({ model, answers: { tool: answer }, usage: { input_tokens: 10, output_tokens: 2 } });
61
+ return Response.json({ model: body.model, answers: { tool: answer }, usage: { input_tokens: 10, output_tokens: 2 } });
63
62
  },
64
63
  },
65
64
  });
@@ -72,3 +72,15 @@ test("honors explicit disablement with fail-open fallback", async () => {
72
72
  else process.env.JEV_LAYER_ENABLED = previous;
73
73
  }
74
74
  });
75
+
76
+ test("visual replay cannot silently substitute text-only routing", async () => {
77
+ let calls = 0;
78
+ const decision = await routeRequest(request({
79
+ assets_omitted: true,
80
+ policy: { deterministic: true, deterministic_capability_id: "search" },
81
+ }), { provider: { name: "no-image-replay", async decide() { calls++; } } });
82
+ assert.equal(decision.status, "fallback");
83
+ assert.equal(decision.selected, null);
84
+ assert.match(decision.reason, /original images/);
85
+ assert.equal(calls, 0);
86
+ });