jev-layer 0.1.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.
- package/CHANGELOG.md +73 -34
- package/CONTRIBUTING.md +146 -146
- package/LICENSE +21 -21
- package/README.md +125 -126
- package/README.ru.md +140 -117
- package/README.zh-CN.md +139 -117
- package/RELEASE.md +53 -53
- package/SECURITY.md +64 -56
- package/bin/jev.mjs +215 -213
- package/config/codex.mcp.toml +8 -8
- package/config/generic-mcp.json +13 -13
- package/config/hermes.mcp.yaml +9 -9
- package/config/jev.example.json +23 -22
- package/config/omp.mcp.json +14 -14
- package/config/providers.env.example +13 -13
- package/docs/AGENT-IMPLEMENTATION.md +306 -0
- package/docs/COMPATIBILITY.md +24 -15
- package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +187 -0
- package/docs/PROVIDERS.md +109 -0
- package/docs/SCHEMA-VERSIONING.md +101 -94
- package/docs/architecture.svg +77 -0
- package/docs/jev-layer-banner.svg +38 -0
- package/examples/capabilities.json +39 -39
- package/examples/route-request.json +28 -28
- package/integrations/codex/.codex-plugin/plugin.json +19 -19
- package/integrations/codex/.mcp.json +11 -11
- package/integrations/codex/AGENTS.md +1 -1
- package/integrations/codex/run-mcp.mjs +9 -9
- package/integrations/codex/skills/jev-route/SKILL.md +17 -17
- package/integrations/hermes/__init__.py +156 -51
- package/integrations/hermes/plugin.yaml +11 -5
- package/integrations/hermes/schemas.py +120 -12
- package/integrations/omp/extension.js +106 -105
- package/integrations/template/README.md +10 -10
- package/integrations/template/adapter.mjs +87 -87
- package/package.json +60 -59
- package/scripts/benchmark.mjs +35 -35
- package/scripts/browser-benchmark.mjs +92 -92
- package/scripts/browser-e2e.mjs +117 -117
- package/scripts/capability-e2e.mjs +31 -31
- package/scripts/clean-install-smoke.mjs +176 -162
- package/scripts/codex-mcp-smoke.mjs +119 -119
- package/scripts/context-filter-e2e.mjs +45 -45
- package/scripts/fail-open-smoke.mjs +120 -120
- package/scripts/feature-flags-smoke.mjs +46 -46
- package/scripts/mcp-receipt-smoke.mjs +115 -98
- package/scripts/model-route-report.mjs +15 -0
- package/scripts/openrouter-choice.mjs +59 -59
- package/scripts/replay-eval.mjs +124 -124
- package/scripts/smoke.mjs +34 -34
- package/scripts/supervision-e2e.mjs +138 -109
- package/src/browser.mjs +583 -569
- package/src/cli.mjs +33 -33
- package/src/computer.mjs +196 -0
- package/src/config.mjs +86 -75
- package/src/context-filter.mjs +56 -56
- package/src/contract.mjs +74 -72
- package/src/discovery.mjs +65 -65
- package/src/hermes-adapter.mjs +134 -0
- package/src/images.mjs +98 -0
- package/src/mcp-server.mjs +313 -210
- package/src/model-route-metrics.mjs +89 -0
- package/src/model-routing.mjs +87 -0
- package/src/providers/demo.mjs +52 -52
- package/src/providers/index.mjs +12 -0
- package/src/providers/typesafe.mjs +206 -126
- package/src/receipts.mjs +228 -226
- package/src/registry.mjs +109 -109
- package/src/relevance-filter.mjs +99 -99
- package/src/route.mjs +228 -221
- package/src/shadow-compaction.mjs +170 -0
- package/src/supervision.mjs +268 -244
- package/test/browser.test.mjs +222 -199
- package/test/capability.test.mjs +54 -54
- package/test/computer.test.mjs +195 -0
- package/test/context-filter.test.mjs +65 -65
- package/test/hermes-adapter.test.mjs +63 -0
- package/test/image-fixture.mjs +2 -0
- package/test/images.test.mjs +85 -0
- package/test/model-route-metrics.test.mjs +61 -0
- package/test/model-routing.test.mjs +59 -0
- package/test/openrouter-provider.test.mjs +55 -55
- package/test/provider-transports.test.mjs +205 -0
- package/test/providers.test.mjs +125 -0
- package/test/receipts.test.mjs +71 -71
- package/test/route.test.mjs +86 -74
- package/test/shadow-compaction.test.mjs +67 -0
- package/test/supervision.test.mjs +117 -99
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import { decideComputerStep } from "../src/computer.mjs";
|
|
5
|
+
import { PNG } from "./image-fixture.mjs";
|
|
6
|
+
|
|
7
|
+
const SCREENSHOT = { format: "png", data_base64: PNG, width: 400, height: 300 };
|
|
8
|
+
const SCROLL = { id: "scroll-down", operation: "scroll", args: { direction: "down", amount: 120 } };
|
|
9
|
+
const CLICK = { id: "save", operation: "click", target: "the Save button", args: { button: "left" }, risk: "low" };
|
|
10
|
+
const BOX = { left: 0.2, right: 0.6, top: 0.1, bottom: 0.5 };
|
|
11
|
+
|
|
12
|
+
function input(overrides = {}) {
|
|
13
|
+
return { goal: "Inspect the application", observation: { screenshot: SCREENSHOT, visible_text: "Save", application: "Editor" }, actions: [SCROLL], actor_permissions: ["computer"], ...overrides };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function grounding(visibility = 0.99, confidence = 0.95, box = BOX) {
|
|
17
|
+
return { answers: { target_visible: { type: "noul", noul: visibility }, target_box: { type: "box", box, confidence } } };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function provider({ choice, confidence = 1, result = grounding(), error = false } = {}) {
|
|
21
|
+
return {
|
|
22
|
+
name: "computer-test", supportsImages: true, choices: [], groundings: [],
|
|
23
|
+
async decide(request) {
|
|
24
|
+
this.choices.push(request);
|
|
25
|
+
const selected = choice ?? request.candidates[0].id;
|
|
26
|
+
return { answers: { tool: { type: "choice", choice: selected, confidence, probabilities: { [selected]: confidence } } } };
|
|
27
|
+
},
|
|
28
|
+
async evaluate(request) {
|
|
29
|
+
this.groundings.push(request);
|
|
30
|
+
if (error) throw new Error("offline");
|
|
31
|
+
return result;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function assertFallback(result, type) {
|
|
37
|
+
assert.equal(result.action, null);
|
|
38
|
+
assert.equal(result.decision.status, "fallback");
|
|
39
|
+
if (type) assert.equal(result.decision.fallback.type, type);
|
|
40
|
+
assert.equal(result.decision.execution.enabled, false);
|
|
41
|
+
assert.equal(result.decision.correlation_id, result.decision.receipt.correlation_id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("computer advice is opt-in and an explicit disable overrides the environment", async () => {
|
|
45
|
+
const previous = process.env.JEV_COMPUTER_USE;
|
|
46
|
+
const engine = provider();
|
|
47
|
+
try {
|
|
48
|
+
delete process.env.JEV_COMPUTER_USE;
|
|
49
|
+
assertFallback(await decideComputerStep(input(), { provider: engine }), "computer_use_disabled");
|
|
50
|
+
process.env.JEV_COMPUTER_USE = "1";
|
|
51
|
+
assertFallback(await decideComputerStep(input(), { enabled: false, provider: engine }), "computer_use_disabled");
|
|
52
|
+
assert.equal(engine.choices.length, 0);
|
|
53
|
+
const enabled = await decideComputerStep(input(), { provider: engine });
|
|
54
|
+
assert.equal(enabled.decision.status, "selected");
|
|
55
|
+
assert.deepEqual(enabled.action.args, SCROLL.args);
|
|
56
|
+
assert.deepEqual(enabled.decision.execution, { enabled: false, status: "not_started" });
|
|
57
|
+
} finally {
|
|
58
|
+
if (previous === undefined) delete process.env.JEV_COMPUTER_USE;
|
|
59
|
+
else process.env.JEV_COMPUTER_USE = previous;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("malformed screenshots and non-closed action arguments fail open before provider calls", async () => {
|
|
64
|
+
const engine = provider();
|
|
65
|
+
for (const candidate of [
|
|
66
|
+
input({ observation: {} }),
|
|
67
|
+
input({ observation: { screenshot: { ...SCREENSHOT, data_base64: "file:///screen.png" } } }),
|
|
68
|
+
input({ observation: { screenshot: { ...SCREENSHOT, data_base64: Buffer.from("not an image").toString("base64") } } }),
|
|
69
|
+
input({ observation: { screenshot: { ...SCREENSHOT, width: 800 } } }),
|
|
70
|
+
input({ actions: [{ ...SCROLL, operation: "shell", args: { command: "anything" } }] }),
|
|
71
|
+
input({ actions: [{ ...SCROLL, args: { direction: "down", command: "anything" } }] }),
|
|
72
|
+
input({ actions: [{ ...CLICK, target: undefined }] }),
|
|
73
|
+
input({ actions: [{ ...CLICK, args: { x: 400, y: 20 } }] }),
|
|
74
|
+
input({ actions: [{ ...CLICK, args: { x: 20 } }] }),
|
|
75
|
+
input({ actions: [SCROLL, SCROLL] }),
|
|
76
|
+
input({ actions: Array.from({ length: 33 }, (_, index) => ({ ...SCROLL, id: `scroll-${index}` })) }),
|
|
77
|
+
input({ policy: { min_target_visibility: Number.NaN } }),
|
|
78
|
+
input({ policy: { min_box_confidence: 0 } }),
|
|
79
|
+
]) assertFallback(await decideComputerStep(candidate, { enabled: true, provider: engine }), "invalid_input");
|
|
80
|
+
assert.equal(engine.choices.length, 0);
|
|
81
|
+
assert.equal(engine.groundings.length, 0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("registry filters enforce explicit computer permission, availability, extra permissions, and risk", async () => {
|
|
85
|
+
const engine = provider();
|
|
86
|
+
for (const candidate of [
|
|
87
|
+
input({ actor_permissions: undefined }),
|
|
88
|
+
input({ actor_permissions: [] }),
|
|
89
|
+
input({ actions: [{ ...SCROLL, permissions: ["administrator"] }] }),
|
|
90
|
+
input({ actions: [{ ...SCROLL, available: false }] }),
|
|
91
|
+
input({ actions: [{ ...SCROLL, availability: { available: false, reason: "offline" } }] }),
|
|
92
|
+
input({ actions: [{ ...SCROLL, risk: "high" }], policy: { max_risk: "low" } }),
|
|
93
|
+
]) {
|
|
94
|
+
const result = await decideComputerStep(candidate, { enabled: true, provider: engine });
|
|
95
|
+
assert.equal(result.decision.status, "no_decision");
|
|
96
|
+
assert.equal(result.action, null);
|
|
97
|
+
assert.equal(result.decision.execution.enabled, false);
|
|
98
|
+
assert.ok(result.decision.candidates.every((item) => item.filtered));
|
|
99
|
+
}
|
|
100
|
+
assert.equal(engine.choices.length, 0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("consequential actions require confirmation even at low risk", async () => {
|
|
104
|
+
for (const action of [
|
|
105
|
+
{ id: "click", operation: "click", args: { x: 17, y: 29, button: "right" }, risk: "low" },
|
|
106
|
+
{ id: "type", operation: "type", args: { text: "host supplied text" }, risk: "low" },
|
|
107
|
+
{ id: "key", operation: "key", args: { keys: ["Control", "S"] }, risk: "low" },
|
|
108
|
+
{ ...SCROLL, consequential: true },
|
|
109
|
+
{ ...SCROLL, requires_confirmation: true },
|
|
110
|
+
{ ...SCROLL, policy: { requires_confirmation: true } },
|
|
111
|
+
]) {
|
|
112
|
+
const engine = provider();
|
|
113
|
+
const result = await decideComputerStep(input({ actions: [action], policy: { confirmation_risk_levels: [] } }), { enabled: true, provider: engine });
|
|
114
|
+
assert.equal(result.decision.status, "needs_confirmation");
|
|
115
|
+
assert.equal(result.decision.candidates[0].requires_confirmation, true);
|
|
116
|
+
assert.equal(result.decision.execution.enabled, false);
|
|
117
|
+
assert.equal(engine.groundings.length, 0);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("only the chosen described click is grounded, once, and mapped to bound screenshot pixels", async () => {
|
|
122
|
+
const engine = provider({ choice: "save" });
|
|
123
|
+
const result = await decideComputerStep(input({ actions: [SCROLL, CLICK, { ...CLICK, id: "other", target: "Cancel button" }] }), { enabled: true, provider: engine });
|
|
124
|
+
assert.equal(result.decision.status, "needs_confirmation");
|
|
125
|
+
assert.deepEqual(result.action.args, { button: "left", x: 160, y: 90 });
|
|
126
|
+
assert.equal(result.action.target, CLICK.target);
|
|
127
|
+
assert.deepEqual(result.action.grounding, { box: BOX, visibility: 0.99, confidence: 0.95, coordinate_space: "screenshot_pixels" });
|
|
128
|
+
assert.equal(result.action.screenshot.sha256, `sha256:${createHash("sha256").update(Buffer.from(PNG, "base64")).digest("hex")}`);
|
|
129
|
+
assert.equal(result.action.screenshot.requires_current, true);
|
|
130
|
+
assert.equal(result.action.screenshot.width, 400);
|
|
131
|
+
assert.equal(result.action.screenshot.height, 300);
|
|
132
|
+
assert.equal(engine.groundings.length, 1);
|
|
133
|
+
assert.equal(JSON.stringify(result.request.context).includes(PNG), false);
|
|
134
|
+
assert.equal(JSON.stringify(result.decision).includes(PNG), false);
|
|
135
|
+
assert.deepEqual(CLICK.args, { button: "left" });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("absent, malformed, and uncertain grounding yields no action while retaining routing correlation", async () => {
|
|
139
|
+
const badResults = [
|
|
140
|
+
[grounding(0), "target_not_visible"],
|
|
141
|
+
[grounding(0.899), "target_not_visible"],
|
|
142
|
+
[grounding(0.99, 0.799), "low_grounding_confidence"],
|
|
143
|
+
[grounding(Number.NaN), "malformed_grounding"],
|
|
144
|
+
[grounding(Infinity), "malformed_grounding"],
|
|
145
|
+
[grounding("0.99"), "malformed_grounding"],
|
|
146
|
+
[grounding(0.99, Infinity), "malformed_grounding"],
|
|
147
|
+
[{ answers: { target_box: grounding().answers.target_box } }, "malformed_grounding"],
|
|
148
|
+
[grounding(0.99, 0.95, { ...BOX, left: -0.1 }), "malformed_grounding"],
|
|
149
|
+
[grounding(0.99, 0.95, { ...BOX, right: 1.1 }), "malformed_grounding"],
|
|
150
|
+
[grounding(0.99, 0.95, { ...BOX, left: BOX.right }), "malformed_grounding"],
|
|
151
|
+
[grounding(0.99, 0.95, { ...BOX, top: BOX.bottom + 0.1 }), "malformed_grounding"],
|
|
152
|
+
[grounding(0.99, 0.95, { ...BOX, bottom: Number.NaN }), "malformed_grounding"],
|
|
153
|
+
];
|
|
154
|
+
for (const [raw, type] of badResults) {
|
|
155
|
+
const result = await decideComputerStep(input({ actions: [CLICK] }), { enabled: true, provider: provider({ result: raw }) });
|
|
156
|
+
assertFallback(result, type);
|
|
157
|
+
assert.equal(result.decision.selected, CLICK.id);
|
|
158
|
+
assert.equal(result.decision.receipt.provider, "computer-test");
|
|
159
|
+
assert.equal(result.decision.receipt.candidate_count, 1);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("host grounding thresholds are inclusive and edge pixels remain inside the screenshot", async () => {
|
|
164
|
+
const engine = provider({ result: grounding(0.95, 0.9, { left: 0.999, right: 1, top: 0.999, bottom: 1 }) });
|
|
165
|
+
const result = await decideComputerStep(input({ actions: [CLICK], policy: { min_target_visibility: 0.95, min_box_confidence: 0.9 } }), { enabled: true, provider: engine });
|
|
166
|
+
assert.deepEqual(result.action.args, { button: "left", x: 399, y: 299 });
|
|
167
|
+
const stricter = await decideComputerStep(input({ actions: [CLICK] }), { enabled: true, provider: engine, policy: { min_target_visibility: 0.96 } });
|
|
168
|
+
assertFallback(stricter, "target_not_visible");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("route fallback, provider failures, unsupported images, and unknown selections never leak actions", async () => {
|
|
172
|
+
assertFallback(await decideComputerStep(input(), { enabled: true, provider: provider({ confidence: 0.2 }) }), "low_confidence");
|
|
173
|
+
assertFallback(await decideComputerStep(input(), { enabled: true, provider: provider({ choice: "not-host-supplied" }) }), "invalid_selection");
|
|
174
|
+
assertFallback(await decideComputerStep(input({ actions: [CLICK] }), { enabled: true, provider: provider({ error: true }) }), "provider_error");
|
|
175
|
+
const unavailable = provider();
|
|
176
|
+
unavailable.supportsImages = false;
|
|
177
|
+
assertFallback(await decideComputerStep(input(), { enabled: true, provider: unavailable }), "unsupported_images");
|
|
178
|
+
assert.equal(unavailable.choices.length, 0);
|
|
179
|
+
const noGrounding = provider();
|
|
180
|
+
delete noGrounding.evaluate;
|
|
181
|
+
assertFallback(await decideComputerStep(input({ actions: [CLICK] }), { enabled: true, provider: noGrounding }), "grounding_unavailable");
|
|
182
|
+
const broken = provider();
|
|
183
|
+
broken.decide = async () => { throw new Error("offline"); };
|
|
184
|
+
assertFallback(await decideComputerStep(input(), { enabled: true, provider: broken }), "provider_error");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("handoff is advisory and nonselected described targets are never grounded", async () => {
|
|
188
|
+
const engine = provider({ choice: "handoff" });
|
|
189
|
+
const action = { id: "handoff", operation: "handoff", args: { reason: "Host planning needed" }, available: true, risk: "low" };
|
|
190
|
+
const result = await decideComputerStep(input({ actions: [CLICK, action] }), { enabled: true, provider: engine });
|
|
191
|
+
assert.equal(result.decision.status, "selected");
|
|
192
|
+
assert.equal(result.action.operation, "handoff");
|
|
193
|
+
assert.equal(engine.groundings.length, 0);
|
|
194
|
+
assert.equal(result.decision.execution.enabled, false);
|
|
195
|
+
});
|
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
import { filterContext } from "../src/relevance-filter.mjs";
|
|
4
|
-
import { projectState } from "../src/context-filter.mjs";
|
|
5
|
-
import { routeRequest } from "../src/route.mjs";
|
|
6
|
-
|
|
7
|
-
const capabilities = [{ id: "inspect", kind: "tool", name: "Inspect", description: "Inspect the supplied context", risk: "low" }];
|
|
8
|
-
|
|
9
|
-
function messages() {
|
|
10
|
-
return [
|
|
11
|
-
...Array.from({ length: 10 }, (_, index) => `old unrelated note ${index}`),
|
|
12
|
-
"Requirement: preserve /workspace/project/config.json exactly",
|
|
13
|
-
"command: `npm test` failed with exit status 1; Error: fixture failed",
|
|
14
|
-
"recent worker update",
|
|
15
|
-
];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
test("shadow mode never mutates context and reports evidence", () => {
|
|
19
|
-
const context = { messages: messages() };
|
|
20
|
-
const before = JSON.stringify(context);
|
|
21
|
-
const result = filterContext(context, { mode: "shadow", recent: 2 });
|
|
22
|
-
assert.strictEqual(result.context, context);
|
|
23
|
-
assert.equal(JSON.stringify(context), before);
|
|
24
|
-
assert.equal(result.report.mode, "shadow");
|
|
25
|
-
assert.equal(result.report.changed, false);
|
|
26
|
-
assert.ok(result.report.pinned >= 2);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("conservative mode drops stale noise but preserves exact evidence", () => {
|
|
30
|
-
const result = filterContext({ messages: messages() }, { mode: "conservative", recent: 2 });
|
|
31
|
-
assert.equal(result.report.mode, "conservative");
|
|
32
|
-
assert.equal(result.report.changed, true);
|
|
33
|
-
assert.ok(result.report.dropped > 0);
|
|
34
|
-
assert.ok(result.context.messages.includes("Requirement: preserve /workspace/project/config.json exactly"));
|
|
35
|
-
assert.ok(result.context.messages.includes("command: `npm test` failed with exit status 1; Error: fixture failed"));
|
|
36
|
-
assert.ok(!result.context.messages.includes("old unrelated note 0"));
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("route projection applies explicit filtering without losing pinned evidence", async () => {
|
|
40
|
-
let captured;
|
|
41
|
-
const decision = await routeRequest({
|
|
42
|
-
intent: "inspect project state",
|
|
43
|
-
policy: { context_filter_mode: "conservative" },
|
|
44
|
-
context: { messages: messages() },
|
|
45
|
-
capabilities,
|
|
46
|
-
}, {
|
|
47
|
-
provider: {
|
|
48
|
-
name: "context-filter-test-provider",
|
|
49
|
-
async decide({ state }) {
|
|
50
|
-
captured = state;
|
|
51
|
-
return { answers: { tool: { type: "choice", choice: "inspect", probabilities: { inspect: 1 }, confidence: 1 } } };
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
assert.equal(decision.status, "selected");
|
|
56
|
-
assert.equal(captured.context_filter.mode, "conservative");
|
|
57
|
-
assert.ok(captured.context.messages.includes("Requirement: preserve /workspace/project/config.json exactly"));
|
|
58
|
-
assert.ok(captured.context.messages.includes("command: `npm test` failed with exit status 1; Error: fixture failed"));
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("no mode keeps the existing projection contract", () => {
|
|
62
|
-
const result = projectState({ intent: "inspect", context: { messages: ["plain"] } }, capabilities, 6_000);
|
|
63
|
-
assert.equal(result.state.context_filter, undefined);
|
|
64
|
-
assert.deepEqual(result.state.context, { messages: ["plain"] });
|
|
65
|
-
});
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { filterContext } from "../src/relevance-filter.mjs";
|
|
4
|
+
import { projectState } from "../src/context-filter.mjs";
|
|
5
|
+
import { routeRequest } from "../src/route.mjs";
|
|
6
|
+
|
|
7
|
+
const capabilities = [{ id: "inspect", kind: "tool", name: "Inspect", description: "Inspect the supplied context", risk: "low" }];
|
|
8
|
+
|
|
9
|
+
function messages() {
|
|
10
|
+
return [
|
|
11
|
+
...Array.from({ length: 10 }, (_, index) => `old unrelated note ${index}`),
|
|
12
|
+
"Requirement: preserve /workspace/project/config.json exactly",
|
|
13
|
+
"command: `npm test` failed with exit status 1; Error: fixture failed",
|
|
14
|
+
"recent worker update",
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("shadow mode never mutates context and reports evidence", () => {
|
|
19
|
+
const context = { messages: messages() };
|
|
20
|
+
const before = JSON.stringify(context);
|
|
21
|
+
const result = filterContext(context, { mode: "shadow", recent: 2 });
|
|
22
|
+
assert.strictEqual(result.context, context);
|
|
23
|
+
assert.equal(JSON.stringify(context), before);
|
|
24
|
+
assert.equal(result.report.mode, "shadow");
|
|
25
|
+
assert.equal(result.report.changed, false);
|
|
26
|
+
assert.ok(result.report.pinned >= 2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("conservative mode drops stale noise but preserves exact evidence", () => {
|
|
30
|
+
const result = filterContext({ messages: messages() }, { mode: "conservative", recent: 2 });
|
|
31
|
+
assert.equal(result.report.mode, "conservative");
|
|
32
|
+
assert.equal(result.report.changed, true);
|
|
33
|
+
assert.ok(result.report.dropped > 0);
|
|
34
|
+
assert.ok(result.context.messages.includes("Requirement: preserve /workspace/project/config.json exactly"));
|
|
35
|
+
assert.ok(result.context.messages.includes("command: `npm test` failed with exit status 1; Error: fixture failed"));
|
|
36
|
+
assert.ok(!result.context.messages.includes("old unrelated note 0"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("route projection applies explicit filtering without losing pinned evidence", async () => {
|
|
40
|
+
let captured;
|
|
41
|
+
const decision = await routeRequest({
|
|
42
|
+
intent: "inspect project state",
|
|
43
|
+
policy: { context_filter_mode: "conservative" },
|
|
44
|
+
context: { messages: messages() },
|
|
45
|
+
capabilities,
|
|
46
|
+
}, {
|
|
47
|
+
provider: {
|
|
48
|
+
name: "context-filter-test-provider",
|
|
49
|
+
async decide({ state }) {
|
|
50
|
+
captured = state;
|
|
51
|
+
return { answers: { tool: { type: "choice", choice: "inspect", probabilities: { inspect: 1 }, confidence: 1 } } };
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
assert.equal(decision.status, "selected");
|
|
56
|
+
assert.equal(captured.context_filter.mode, "conservative");
|
|
57
|
+
assert.ok(captured.context.messages.includes("Requirement: preserve /workspace/project/config.json exactly"));
|
|
58
|
+
assert.ok(captured.context.messages.includes("command: `npm test` failed with exit status 1; Error: fixture failed"));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("no mode keeps the existing projection contract", () => {
|
|
62
|
+
const result = projectState({ intent: "inspect", context: { messages: ["plain"] } }, capabilities, 6_000);
|
|
63
|
+
assert.equal(result.state.context_filter, undefined);
|
|
64
|
+
assert.deepEqual(result.state.context, { messages: ["plain"] });
|
|
65
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
|
|
9
|
+
const adapter = fileURLToPath(new URL("../src/hermes-adapter.mjs", import.meta.url));
|
|
10
|
+
|
|
11
|
+
function run(lines, replayPath) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const child = spawn(process.execPath, [adapter], { env: { ...process.env, JEV_REPLAY_CASES: replayPath } });
|
|
14
|
+
let out = "";
|
|
15
|
+
let err = "";
|
|
16
|
+
child.stdout.on("data", (chunk) => { out += chunk; });
|
|
17
|
+
child.stderr.on("data", (chunk) => { err += chunk; });
|
|
18
|
+
child.on("error", reject);
|
|
19
|
+
child.on("close", (code) => {
|
|
20
|
+
if (code !== 0) return reject(new Error(err || `adapter exited ${code}`));
|
|
21
|
+
resolve(out.trim().split(/\r?\n/).filter(Boolean).map(JSON.parse));
|
|
22
|
+
});
|
|
23
|
+
child.stdin.end(lines.map((line) => JSON.stringify(line)).join("\n") + "\n");
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("Hermes adapter routes a closed candidate set and records a correlated receipt", async () => {
|
|
28
|
+
const dir = await mkdtemp(join(tmpdir(), "jev-hermes-adapter-"));
|
|
29
|
+
const replayPath = join(dir, "cases.jsonl");
|
|
30
|
+
const routeRequest = { operation: "route", args: { harness: "hermes-test", intent: "Read a source file", provider: "demo", actor_permissions: ["read"], capabilities: [
|
|
31
|
+
{ id: "read", kind: "tool", name: "Read file", description: "Read a local text file", permissions: ["read"], risk: "low", available: true },
|
|
32
|
+
{ id: "write", kind: "tool", name: "Write file", description: "Mutate a local file", permissions: ["write"], risk: "high", available: true },
|
|
33
|
+
] } };
|
|
34
|
+
const [decision] = await run([routeRequest], replayPath);
|
|
35
|
+
assert.equal(decision.status, "selected");
|
|
36
|
+
assert.equal(decision.selected, "read");
|
|
37
|
+
assert.ok(decision.correlation_id);
|
|
38
|
+
assert.ok(decision._jev_request);
|
|
39
|
+
const [receipt] = await run([{ operation: "record_execution", decision, args: { correlation_id: decision.correlation_id, harness: "hermes-test", capability_id: "read", status: "completed", result: { lines: 3 }, duration_ms: 4 } }], replayPath);
|
|
40
|
+
assert.equal(receipt.record_type, "execution_receipt");
|
|
41
|
+
assert.equal(receipt.correlation_id, decision.correlation_id);
|
|
42
|
+
assert.equal(receipt.host.status, "completed");
|
|
43
|
+
const records = (await readFile(replayPath, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
|
|
44
|
+
assert.equal(records.filter((record) => record.record_type === "routing_case").length, 1);
|
|
45
|
+
assert.equal(records.filter((record) => record.record_type === "execution_receipt").length, 1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("Hermes adapter exposes supervision, shadow-compaction, and browser surfaces without execution", async () => {
|
|
49
|
+
const dir = await mkdtemp(join(tmpdir(), "jev-hermes-adapter-"));
|
|
50
|
+
const replayPath = join(dir, "cases.jsonl");
|
|
51
|
+
const [supervision, shadow, browser] = await run([
|
|
52
|
+
{ operation: "supervise", args: { provider: "demo", enabled: true, harness: "hermes-test", job: { goal: "verify" }, observation: { state: "done" }, evidence: { tests_passed: true } } },
|
|
53
|
+
{ operation: "shadow_compaction", args: { provider: "demo", intent: "review context", context: { messages: ["old note", "Requirement: preserve /workspace/project/config.json"] } } },
|
|
54
|
+
{ operation: "browser_step", args: { provider: "demo", enabled: true, harness: "hermes-test", goal: "Open the visible docs link", observation: { url: "https://example.test", targets: [{ id: "docs", name: "Docs", href: "/docs", visible: true, clickable: true }], tabs: [], scroll: { down: false, up: false } } } },
|
|
55
|
+
], replayPath);
|
|
56
|
+
assert.equal(supervision.status, "judged");
|
|
57
|
+
assert.ok(["continue", "verify", "retry", "finish", "escalate"].includes(supervision.action));
|
|
58
|
+
assert.equal(shadow.mode, "jev_shadow");
|
|
59
|
+
assert.equal(shadow.changed, false);
|
|
60
|
+
assert.equal(shadow.protected_count, 1);
|
|
61
|
+
assert.ok(["selected", "fallback", "no_decision", "needs_confirmation"].includes(browser.status));
|
|
62
|
+
assert.equal(browser.execution.enabled, false);
|
|
63
|
+
});
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// Complete 400x300 still RGB PNG, generated with zlib and valid chunk CRCs.
|
|
2
|
+
export const PNG = "iVBORw0KGgoAAAANSUhEUgAAAZAAAAEsCAIAAABi1XKVAAABdElEQVR4nO3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+Bn+3AAEkkD9cAAAAAElFTkSuQmCC";
|
|
@@ -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
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { buildModelRouteReport } from "../src/model-route-metrics.mjs";
|
|
4
|
+
|
|
5
|
+
const rows = [
|
|
6
|
+
{
|
|
7
|
+
record_type: "routing_case",
|
|
8
|
+
correlation_id: "one",
|
|
9
|
+
request: { context: { model_route: { mode: "shadow", profiles: [{ id: "fast" }, { id: "frontier" }] } } },
|
|
10
|
+
decision: { status: "selected", selected: "fast", receipt: { latency_ms: 4, cost_usd: 0.001 } },
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
record_type: "execution_receipt",
|
|
14
|
+
correlation_id: "one",
|
|
15
|
+
host: {
|
|
16
|
+
status: "completed",
|
|
17
|
+
duration_ms: 20,
|
|
18
|
+
result: { model_route: { actual_model_id: "fast", retry_count: 1, outcome: "verified" } },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
record_type: "routing_case",
|
|
23
|
+
correlation_id: "two",
|
|
24
|
+
request: { context: { model_route: { mode: "shadow", profiles: [{ id: "fast" }, { id: "frontier" }] } } },
|
|
25
|
+
decision: { status: "selected", selected: "frontier", receipt: { latency_ms: 6, cost_usd: 0.002 } },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
record_type: "execution_receipt",
|
|
29
|
+
correlation_id: "two",
|
|
30
|
+
host: {
|
|
31
|
+
status: "failed",
|
|
32
|
+
duration_ms: 30,
|
|
33
|
+
result: { model_route: { actual_model_id: "fast", retry_count: 2, outcome: "failed" } },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
test("model route report joins shadow recommendation to host outcome without treating it as a route change", () => {
|
|
39
|
+
const report = buildModelRouteReport(rows);
|
|
40
|
+
assert.equal(report.schema_version, 1);
|
|
41
|
+
assert.equal(report.summary.decisions, 2);
|
|
42
|
+
assert.equal(report.summary.with_host_outcome, 2);
|
|
43
|
+
assert.equal(report.summary.recommendation_match_rate, 0.5);
|
|
44
|
+
assert.equal(report.summary.verified_outcomes, 1);
|
|
45
|
+
assert.equal(report.summary.failed_outcomes, 1);
|
|
46
|
+
assert.equal(report.summary.total_retry_count, 3);
|
|
47
|
+
assert.deepEqual(report.models.map((row) => row.recommended_model_id), ["fast", "frontier"]);
|
|
48
|
+
assert.equal(report.models[0].host_actual_model_ids.fast, 1);
|
|
49
|
+
assert.equal(report.models[1].host_actual_model_ids.fast, 1);
|
|
50
|
+
assert.equal(report.models[1].recommendation_matches, 0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("model route report fails open on incomplete or unrelated receipts", () => {
|
|
54
|
+
const report = buildModelRouteReport([
|
|
55
|
+
{ record_type: "routing_case", correlation_id: "unrelated", request: { context: {} }, decision: { selected: "tool" } },
|
|
56
|
+
{ record_type: "routing_case", correlation_id: "pending", request: { context: { model_route: { mode: "shadow" } } }, decision: { selected: null } },
|
|
57
|
+
]);
|
|
58
|
+
assert.equal(report.summary.decisions, 1);
|
|
59
|
+
assert.equal(report.summary.with_host_outcome, 0);
|
|
60
|
+
assert.equal(report.models[0].recommended_model_id, "unrecommended");
|
|
61
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { recommendModelRoute } from "../src/model-routing.mjs";
|
|
4
|
+
|
|
5
|
+
const models = [
|
|
6
|
+
{
|
|
7
|
+
id: "fast",
|
|
8
|
+
provider: "openrouter",
|
|
9
|
+
model: "small-model",
|
|
10
|
+
reasoning_effort: "low",
|
|
11
|
+
description: "fast and cheap for simple classification or file inspection",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "frontier",
|
|
15
|
+
provider: "openai-codex",
|
|
16
|
+
model: "frontier-model",
|
|
17
|
+
reasoning_effort: "high",
|
|
18
|
+
description: "strong model for difficult implementation and ambiguous reasoning",
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
test("model routing returns an advisory profile and never changes the host route", async () => {
|
|
23
|
+
let captured;
|
|
24
|
+
const result = await recommendModelRoute({
|
|
25
|
+
intent: "classify a small batch of already structured feedback",
|
|
26
|
+
context: { task_size: "small", external_side_effects: false },
|
|
27
|
+
models,
|
|
28
|
+
provider: {
|
|
29
|
+
name: "model-route-test-provider",
|
|
30
|
+
async decide(input) {
|
|
31
|
+
captured = input;
|
|
32
|
+
return {
|
|
33
|
+
answers: { tool: { type: "choice", choice: "fast", probabilities: { fast: 0.92, frontier: 0.08 }, confidence: 0.92 } },
|
|
34
|
+
usage: { cost: 0.0001 },
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.equal(result.route_mode, "shadow");
|
|
41
|
+
assert.equal(result.status, "selected");
|
|
42
|
+
assert.equal(result.selected, "fast");
|
|
43
|
+
assert.equal(result.recommended_model.model, "small-model");
|
|
44
|
+
assert.equal(result.execution.enabled, false);
|
|
45
|
+
assert.equal(captured.candidates.every((candidate) => candidate.kind === "model"), true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("model routing excludes unavailable profiles and fails open with no selected profile", async () => {
|
|
49
|
+
const result = await recommendModelRoute({
|
|
50
|
+
intent: "inspect a file",
|
|
51
|
+
models: [{ ...models[0], available: false }],
|
|
52
|
+
provider: "demo",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
assert.equal(result.status, "no_decision");
|
|
56
|
+
assert.equal(result.selected, null);
|
|
57
|
+
assert.equal(result.recommended_model, null);
|
|
58
|
+
assert.equal(result.execution.enabled, false);
|
|
59
|
+
});
|