@x12i/graphenix-executable-format 1.0.0 → 2.0.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/README.md +512 -341
- package/dist/api.d.ts +13 -35
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +13 -98
- package/dist/api.js.map +1 -1
- package/dist/constants.d.ts +1 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -18
- package/dist/constants.js.map +1 -1
- package/dist/index.js +1 -17
- package/dist/index.js.map +1 -1
- package/dist/test/executable-format.test.js +269 -307
- package/dist/test/executable-format.test.js.map +1 -1
- package/dist/test/executable-plan-v2.test.d.ts +2 -0
- package/dist/test/executable-plan-v2.test.d.ts.map +1 -0
- package/dist/test/executable-plan-v2.test.js +325 -0
- package/dist/test/executable-plan-v2.test.js.map +1 -0
- package/dist/test/execution-trace-v2.test.d.ts +2 -0
- package/dist/test/execution-trace-v2.test.d.ts.map +1 -0
- package/dist/test/execution-trace-v2.test.js +411 -0
- package/dist/test/execution-trace-v2.test.js.map +1 -0
- package/dist/test/phase-1-format.test.d.ts +2 -0
- package/dist/test/phase-1-format.test.d.ts.map +1 -0
- package/dist/test/phase-1-format.test.js +297 -0
- package/dist/test/phase-1-format.test.js.map +1 -0
- package/package.json +69 -56
- package/schema/graphenix-executable-format-1.0.0.schema.json +0 -40
|
@@ -1,62 +1,55 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
-
const node_test_1 = require("node:test");
|
|
8
|
-
const graphenix_core_1 = require("@x12i/graphenix-core");
|
|
9
|
-
const api_js_1 = require("../api.js");
|
|
10
|
-
const minimal_executable_graph_js_1 = require("../fixtures/minimal-executable-graph.js");
|
|
11
|
-
const graphenix_core_2 = require("@x12i/graphenix-core");
|
|
12
|
-
const constants_js_1 = require("../constants.js");
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { validateGraph } from "@x12i/graphenix-core";
|
|
4
|
+
import { assertExecutableGraph, validateExecutableGraph, validateStudioExecuteRequest, validateRuntimeObject, buildGraphExecutionRequestFromStudioExecute, resolveNodeAiPlan, explainNodeInheritance, resolveNodeModelSlot, normalizeExecutableGraph, validateCaseCondition, evaluateCaseCondition, isProfileChoiceKeyFormat, isKnownProfileChoice, buildDeterministicCaseContext, compileExecutablePlan, validateExecutablePlan, validateAuthoringGraph, createExecutionTrace, appendExecutionEvent, validateExecutionTrace, buildRuntimeObject, createMinimalExecutableGraph, createPlainGraphenixGraph } from "../api.js";
|
|
5
|
+
import { EXECUTABLE_PROFILE_NAMESPACE } from "@x12i/graphenix-executable-contracts";
|
|
13
6
|
function setGraphModelConfig(doc, modelConfig) {
|
|
14
7
|
const next = structuredClone(doc);
|
|
15
|
-
const ext = next.graph.metadata.extensions[
|
|
8
|
+
const ext = next.graph.metadata.extensions[EXECUTABLE_PROFILE_NAMESPACE];
|
|
16
9
|
ext.modelConfig = modelConfig;
|
|
17
10
|
return next;
|
|
18
11
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const doc =
|
|
22
|
-
const result =
|
|
23
|
-
|
|
12
|
+
describe("graphenix base validation", () => {
|
|
13
|
+
it("valid executable graph passes Graphenix validateGraph()", () => {
|
|
14
|
+
const doc = createMinimalExecutableGraph();
|
|
15
|
+
const result = validateGraph(doc);
|
|
16
|
+
assert.equal(result.valid, true);
|
|
24
17
|
});
|
|
25
18
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const doc =
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const doc =
|
|
33
|
-
const result =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const doc =
|
|
39
|
-
const ext = doc.graph.metadata.extensions[
|
|
19
|
+
describe("executable profile validation", () => {
|
|
20
|
+
it("valid executable graph passes assertExecutableGraph()", () => {
|
|
21
|
+
const doc = createMinimalExecutableGraph();
|
|
22
|
+
assert.doesNotThrow(() => assertExecutableGraph(doc));
|
|
23
|
+
});
|
|
24
|
+
it("plain Graphenix graph without executable profile fails", () => {
|
|
25
|
+
const doc = createPlainGraphenixGraph();
|
|
26
|
+
const result = validateExecutableGraph(doc);
|
|
27
|
+
assert.equal(result.valid, false);
|
|
28
|
+
assert.ok(result.errors.some((e) => e.code === "EXECUTABLE_PROFILE_MISSING"));
|
|
29
|
+
});
|
|
30
|
+
it("graph-level modelConfig is required", () => {
|
|
31
|
+
const doc = createMinimalExecutableGraph();
|
|
32
|
+
const ext = doc.graph.metadata.extensions[EXECUTABLE_PROFILE_NAMESPACE];
|
|
40
33
|
delete ext.modelConfig;
|
|
41
|
-
const result =
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
const result = validateExecutableGraph(doc);
|
|
35
|
+
assert.equal(result.valid, false);
|
|
36
|
+
assert.ok(result.errors.some((e) => e.code === "GRAPH_MODEL_CONFIG_MISSING"));
|
|
44
37
|
});
|
|
45
38
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const doc =
|
|
49
|
-
const plan =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const doc =
|
|
39
|
+
describe("model inheritance", () => {
|
|
40
|
+
it("node with no modelConfig inherits all slots", () => {
|
|
41
|
+
const doc = createMinimalExecutableGraph();
|
|
42
|
+
const plan = resolveNodeAiPlan(doc, "node:professional-answer");
|
|
43
|
+
assert.equal(plan.slots.preActionModel.inherited, true);
|
|
44
|
+
assert.equal(plan.slots.skillModel.inherited, true);
|
|
45
|
+
assert.equal(plan.slots.postActionModel.inherited, true);
|
|
46
|
+
assert.match(explainNodeInheritance(plan), /graph case default/);
|
|
47
|
+
});
|
|
48
|
+
it("node may override only skillModel", () => {
|
|
49
|
+
const doc = createMinimalExecutableGraph();
|
|
57
50
|
const taskNode = doc.graph.nodes.find((n) => n.id === "node:professional-answer");
|
|
58
51
|
const params = taskNode.parameters;
|
|
59
|
-
|
|
52
|
+
assert.ok(params && params.nodeType === "task");
|
|
60
53
|
taskNode.parameters = {
|
|
61
54
|
...params,
|
|
62
55
|
taskConfiguration: {
|
|
@@ -67,14 +60,14 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
67
60
|
}
|
|
68
61
|
}
|
|
69
62
|
};
|
|
70
|
-
const plan =
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
const plan = resolveNodeAiPlan(doc, "node:professional-answer");
|
|
64
|
+
assert.equal(plan.slots.skillModel.inherited, false);
|
|
65
|
+
assert.equal(plan.slots.preActionModel.inherited, true);
|
|
73
66
|
});
|
|
74
67
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const resolved =
|
|
68
|
+
describe("model fallback", () => {
|
|
69
|
+
it("fallback uses graph same-slot only", () => {
|
|
70
|
+
const resolved = resolveNodeModelSlot({
|
|
78
71
|
slot: "skillModel",
|
|
79
72
|
graphSelection: { kind: "profileChoice", key: "vol/default" },
|
|
80
73
|
nodeSelection: {
|
|
@@ -95,33 +88,33 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
95
88
|
graphCaseId: "default",
|
|
96
89
|
nodeId: "node:test"
|
|
97
90
|
});
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
assert.equal(resolved.source, "fallbackToGraphDefault");
|
|
92
|
+
assert.equal(resolved.selected.kind === "profileChoice" ? resolved.selected.key : "", "vol/default");
|
|
100
93
|
});
|
|
101
|
-
|
|
102
|
-
const runtime =
|
|
94
|
+
it("fallback does not use runtime or request model defaults", () => {
|
|
95
|
+
const runtime = validateRuntimeObject({
|
|
103
96
|
jobId: "job-1",
|
|
104
97
|
job: { id: "job-1", jobId: "job-1" },
|
|
105
98
|
graphDefaultModel: { kind: "profileChoice", key: "cheap/default" }
|
|
106
99
|
});
|
|
107
|
-
|
|
100
|
+
assert.equal(runtime.valid, false);
|
|
108
101
|
});
|
|
109
102
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const doc =
|
|
113
|
-
const result =
|
|
103
|
+
describe("studio adapter", () => {
|
|
104
|
+
it("studio request with graphDefaultModel fails", () => {
|
|
105
|
+
const doc = createMinimalExecutableGraph();
|
|
106
|
+
const result = validateStudioExecuteRequest({
|
|
114
107
|
mode: "graph",
|
|
115
108
|
jobId: "job-1",
|
|
116
109
|
graph: doc,
|
|
117
110
|
graphDefaultModel: {}
|
|
118
111
|
});
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
assert.equal(result.valid, false);
|
|
113
|
+
assert.ok(result.errors.some((e) => e.code === "STUDIO_GRAPH_DEFAULT_MODEL_FORBIDDEN"));
|
|
121
114
|
});
|
|
122
|
-
|
|
123
|
-
const doc =
|
|
124
|
-
const ext = doc.graph.metadata.extensions[
|
|
115
|
+
it("studio adapter outputs compiled { plan, runtime }", () => {
|
|
116
|
+
const doc = createMinimalExecutableGraph();
|
|
117
|
+
const ext = doc.graph.metadata.extensions[EXECUTABLE_PROFILE_NAMESPACE];
|
|
125
118
|
ext.modelConfig = {
|
|
126
119
|
version: "graph-model-config/v1",
|
|
127
120
|
default: {
|
|
@@ -139,87 +132,56 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
139
132
|
},
|
|
140
133
|
openrouterApiKey: "secret-should-not-copy"
|
|
141
134
|
};
|
|
142
|
-
const execution =
|
|
135
|
+
const execution = buildGraphExecutionRequestFromStudioExecute(request, {
|
|
143
136
|
agentId: "agent-1"
|
|
144
137
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const profile = execution.plan.graph.graph.metadata?.extensions?.[
|
|
151
|
-
|
|
152
|
-
|
|
138
|
+
assert.equal(execution.runtime.jobId, "job-123");
|
|
139
|
+
assert.equal(execution.plan.source.graphId, doc.id);
|
|
140
|
+
assert.equal(execution.plan.format, "graphenix.executable-plan/v1");
|
|
141
|
+
assert.equal("openrouterApiKey" in execution.runtime, false);
|
|
142
|
+
assert.equal("openrouterApiKey" in execution.plan, false);
|
|
143
|
+
const profile = execution.plan.graph.graph.metadata?.extensions?.[EXECUTABLE_PROFILE_NAMESPACE];
|
|
144
|
+
assert.equal(profile?.modelConfig?.cases?.[0]?.id, "default");
|
|
145
|
+
assert.equal(validateExecutablePlan(execution.plan).valid, true);
|
|
153
146
|
});
|
|
154
147
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const result =
|
|
148
|
+
describe("runtime validation", () => {
|
|
149
|
+
it("credentials are never copied into runtime", () => {
|
|
150
|
+
const result = validateRuntimeObject({
|
|
158
151
|
jobId: "job-1",
|
|
159
152
|
job: { id: "job-1", jobId: "job-1" },
|
|
160
153
|
credentials: { token: "x" }
|
|
161
154
|
});
|
|
162
|
-
|
|
155
|
+
assert.equal(result.valid, false);
|
|
163
156
|
});
|
|
164
|
-
|
|
165
|
-
const result =
|
|
157
|
+
it("runtime modelConfig is rejected", () => {
|
|
158
|
+
const result = validateRuntimeObject({
|
|
166
159
|
jobId: "job-1",
|
|
167
160
|
job: { id: "job-1", jobId: "job-1" },
|
|
168
161
|
modelConfig: {}
|
|
169
162
|
});
|
|
170
|
-
|
|
163
|
+
assert.equal(result.valid, false);
|
|
171
164
|
});
|
|
172
165
|
});
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
id: "graph:legacy",
|
|
177
|
-
version: "0.1.0",
|
|
178
|
-
nodes: [
|
|
179
|
-
{
|
|
180
|
-
id: "node:a",
|
|
181
|
-
kind: "task",
|
|
182
|
-
skillKey: "demo"
|
|
183
|
-
}
|
|
184
|
-
],
|
|
185
|
-
edges: [],
|
|
186
|
-
modelConfig: {
|
|
187
|
-
version: "graph-model-config/v1",
|
|
188
|
-
cases: [
|
|
189
|
-
{
|
|
190
|
-
id: "default",
|
|
191
|
-
modelConfig: {
|
|
192
|
-
preActionModel: { kind: "profile", profile: "economy" },
|
|
193
|
-
skillModel: { kind: "profile", profile: "balanced" },
|
|
194
|
-
postActionModel: { kind: "profile", profile: "economy" }
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
]
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
strict_1.default.equal(migrated.formatVersion, graphenix_core_2.GRAPHENIX_FORMAT_VERSION);
|
|
201
|
-
strict_1.default.doesNotThrow(() => (0, api_js_1.assertExecutableGraph)(migrated));
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
(0, node_test_1.describe)("node kinds", () => {
|
|
205
|
-
(0, node_test_1.it)("task nodes must use kind x12i:task", () => {
|
|
206
|
-
const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
|
|
166
|
+
describe("node kinds", () => {
|
|
167
|
+
it("task nodes must use kind task", () => {
|
|
168
|
+
const doc = createMinimalExecutableGraph();
|
|
207
169
|
doc.graph.nodes[0].kind = "task:wrong";
|
|
208
|
-
const result =
|
|
209
|
-
|
|
170
|
+
const result = validateExecutableGraph(doc);
|
|
171
|
+
assert.ok(result.errors.some((e) => e.code === "TASK_NODE_INVALID"));
|
|
210
172
|
});
|
|
211
173
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const errors =
|
|
174
|
+
describe("deterministic cases", () => {
|
|
175
|
+
it("AC-001: runtime.mode eq simulate is accepted", () => {
|
|
176
|
+
const errors = validateCaseCondition({
|
|
215
177
|
path: "runtime.mode",
|
|
216
178
|
op: "eq",
|
|
217
179
|
value: "simulate"
|
|
218
180
|
});
|
|
219
|
-
|
|
181
|
+
assert.equal(errors.length, 0);
|
|
220
182
|
});
|
|
221
|
-
|
|
222
|
-
const doc = setGraphModelConfig(
|
|
183
|
+
it("AC-007/008: first matching case is selected, else default", () => {
|
|
184
|
+
const doc = setGraphModelConfig(createMinimalExecutableGraph(), {
|
|
223
185
|
version: "graph-model-config/v1",
|
|
224
186
|
cases: [
|
|
225
187
|
{
|
|
@@ -241,12 +203,12 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
241
203
|
}
|
|
242
204
|
]
|
|
243
205
|
});
|
|
244
|
-
const context =
|
|
245
|
-
const plan =
|
|
246
|
-
|
|
206
|
+
const context = buildDeterministicCaseContext(doc, { mode: "simulate" });
|
|
207
|
+
const plan = resolveNodeAiPlan(doc, "node:professional-answer", context);
|
|
208
|
+
assert.equal(plan.graphCaseId, "simulate");
|
|
247
209
|
});
|
|
248
|
-
|
|
249
|
-
const noDefault =
|
|
210
|
+
it("AC-009/010: default case requirements", () => {
|
|
211
|
+
const noDefault = validateExecutableGraph(setGraphModelConfig(createMinimalExecutableGraph(), {
|
|
250
212
|
version: "graph-model-config/v1",
|
|
251
213
|
cases: [
|
|
252
214
|
{
|
|
@@ -260,51 +222,51 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
260
222
|
}
|
|
261
223
|
]
|
|
262
224
|
}));
|
|
263
|
-
|
|
225
|
+
assert.ok(noDefault.errors.some((e) => e.code === "CASE_SELECTION_DEFAULT_MISSING"));
|
|
264
226
|
});
|
|
265
227
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const errors =
|
|
269
|
-
|
|
228
|
+
describe("forbidden AI selectors", () => {
|
|
229
|
+
it("AC-020: ai.prompt is rejected", () => {
|
|
230
|
+
const errors = validateCaseCondition({ ai: { prompt: "complex?" } });
|
|
231
|
+
assert.ok(errors.some((e) => e.code === "CASE_CONDITION_FORBIDDEN_AI_SELECTOR"));
|
|
270
232
|
});
|
|
271
|
-
|
|
272
|
-
const errors =
|
|
233
|
+
it("AC-024: ai.classification path is rejected", () => {
|
|
234
|
+
const errors = validateCaseCondition({
|
|
273
235
|
path: "ai.classification.risk",
|
|
274
236
|
op: "eq",
|
|
275
237
|
value: "high"
|
|
276
238
|
});
|
|
277
|
-
|
|
239
|
+
assert.ok(errors.some((e) => e.code === "CASE_CONDITION_FORBIDDEN_PATH"));
|
|
278
240
|
});
|
|
279
|
-
|
|
280
|
-
const errors =
|
|
241
|
+
it("AC-025: node.output path is rejected", () => {
|
|
242
|
+
const errors = validateCaseCondition({
|
|
281
243
|
path: "node.output.riskClassifier.risk",
|
|
282
244
|
op: "eq",
|
|
283
245
|
value: "high"
|
|
284
246
|
});
|
|
285
|
-
|
|
247
|
+
assert.ok(errors.some((e) => e.code === "CASE_CONDITION_FORBIDDEN_PATH"));
|
|
286
248
|
});
|
|
287
249
|
});
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
});
|
|
295
|
-
|
|
250
|
+
describe("profile-choice validation", () => {
|
|
251
|
+
it("AC-030/031/032: valid profileChoice keys accepted", () => {
|
|
252
|
+
assert.equal(isProfileChoiceKeyFormat("cheap/default"), true);
|
|
253
|
+
assert.equal(isProfileChoiceKeyFormat("vol/default"), true);
|
|
254
|
+
assert.equal(isProfileChoiceKeyFormat("deep/openai_deep"), true);
|
|
255
|
+
assert.equal(isKnownProfileChoice("vol/pro"), true);
|
|
256
|
+
});
|
|
257
|
+
it("AC-033/034/035/036: bare aliases rejected", () => {
|
|
296
258
|
for (const key of ["cheap", "balanced", "cheapest", "default"]) {
|
|
297
|
-
|
|
259
|
+
assert.equal(isProfileChoiceKeyFormat(key), false);
|
|
298
260
|
}
|
|
299
261
|
});
|
|
300
|
-
|
|
262
|
+
it("AC-037/038: vendor slugs and legacy aliases rejected", () => {
|
|
301
263
|
// Syntactically valid profile/choice shape, but not a bundled registry key.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
264
|
+
assert.equal(isProfileChoiceKeyFormat("openai/gpt-4o-mini"), true);
|
|
265
|
+
assert.equal(isKnownProfileChoice("openai/gpt-4o-mini"), false);
|
|
266
|
+
assert.equal(isProfileChoiceKeyFormat("cheap@anthropic_cheap"), false);
|
|
305
267
|
});
|
|
306
|
-
|
|
307
|
-
const result =
|
|
268
|
+
it("AC-039: unknown choice rejected in graph validation", () => {
|
|
269
|
+
const result = validateExecutableGraph(setGraphModelConfig(createMinimalExecutableGraph(), {
|
|
308
270
|
version: "graph-model-config/v1",
|
|
309
271
|
cases: [
|
|
310
272
|
{
|
|
@@ -317,10 +279,10 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
317
279
|
}
|
|
318
280
|
]
|
|
319
281
|
}));
|
|
320
|
-
|
|
282
|
+
assert.ok(result.errors.some((e) => e.code === "PROFILE_CHOICE_KEY_UNKNOWN"));
|
|
321
283
|
});
|
|
322
|
-
|
|
323
|
-
const result =
|
|
284
|
+
it("deprecated kind profile is rejected", () => {
|
|
285
|
+
const result = validateExecutableGraph(setGraphModelConfig(createMinimalExecutableGraph(), {
|
|
324
286
|
version: "graph-model-config/v1",
|
|
325
287
|
cases: [
|
|
326
288
|
{
|
|
@@ -333,13 +295,13 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
333
295
|
}
|
|
334
296
|
]
|
|
335
297
|
}));
|
|
336
|
-
|
|
298
|
+
assert.ok(result.errors.some((e) => e.code === "PROFILE_SELECTION_KIND_DEPRECATED"));
|
|
337
299
|
});
|
|
338
300
|
});
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const doc =
|
|
342
|
-
const ext = doc.graph.metadata.extensions[
|
|
301
|
+
describe("normalization", () => {
|
|
302
|
+
it("AC-060: graph default shorthand normalizes to cases[0].id = default", () => {
|
|
303
|
+
const doc = createMinimalExecutableGraph();
|
|
304
|
+
const ext = doc.graph.metadata.extensions[EXECUTABLE_PROFILE_NAMESPACE];
|
|
343
305
|
ext.modelConfig = {
|
|
344
306
|
version: "graph-model-config/v1",
|
|
345
307
|
default: {
|
|
@@ -348,25 +310,25 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
348
310
|
postActionModel: { kind: "profileChoice", key: "cheap/default" }
|
|
349
311
|
}
|
|
350
312
|
};
|
|
351
|
-
const normalized =
|
|
352
|
-
const profile = normalized.graph.metadata?.extensions?.[
|
|
353
|
-
|
|
354
|
-
|
|
313
|
+
const normalized = normalizeExecutableGraph(doc);
|
|
314
|
+
const profile = normalized.graph.metadata?.extensions?.[EXECUTABLE_PROFILE_NAMESPACE];
|
|
315
|
+
assert.equal(profile?.modelConfig?.cases?.[0]?.id, "default");
|
|
316
|
+
assert.equal(profile?.modelConfig?.cases?.[0]?.when, undefined);
|
|
355
317
|
});
|
|
356
|
-
|
|
357
|
-
const doc =
|
|
318
|
+
it("AC-066: normalization does not mutate source graph", () => {
|
|
319
|
+
const doc = createMinimalExecutableGraph();
|
|
358
320
|
const before = structuredClone(doc);
|
|
359
|
-
|
|
360
|
-
|
|
321
|
+
normalizeExecutableGraph(doc);
|
|
322
|
+
assert.deepEqual(doc, before);
|
|
361
323
|
});
|
|
362
324
|
});
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const doc =
|
|
325
|
+
describe("full resolution example", () => {
|
|
326
|
+
it("matches CR/FR section 12 resolution", () => {
|
|
327
|
+
const doc = createMinimalExecutableGraph();
|
|
366
328
|
const taskNode = doc.graph.nodes.find((n) => n.id === "node:professional-answer");
|
|
367
329
|
const params = taskNode.parameters;
|
|
368
|
-
|
|
369
|
-
const ext = doc.graph.metadata.extensions[
|
|
330
|
+
assert.ok(params && params.nodeType === "task");
|
|
331
|
+
const ext = doc.graph.metadata.extensions[EXECUTABLE_PROFILE_NAMESPACE];
|
|
370
332
|
ext.modelConfig = {
|
|
371
333
|
version: "graph-model-config/v1",
|
|
372
334
|
cases: [
|
|
@@ -439,51 +401,51 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
439
401
|
}
|
|
440
402
|
}
|
|
441
403
|
};
|
|
442
|
-
const context =
|
|
404
|
+
const context = buildDeterministicCaseContext(doc, {
|
|
443
405
|
mode: "live",
|
|
444
406
|
input: { analysisDepth: "deep", riskLevel: "high" }
|
|
445
407
|
});
|
|
446
|
-
const plan =
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
408
|
+
const plan = resolveNodeAiPlan(doc, "node:professional-answer", context);
|
|
409
|
+
assert.equal(plan.graphCaseId, "deep-live");
|
|
410
|
+
assert.equal(plan.nodeCaseId, "high-risk-input");
|
|
411
|
+
assert.equal(plan.slots.preActionModel.selected.kind, "profileChoice");
|
|
450
412
|
if (plan.slots.preActionModel.selected.kind === "profileChoice") {
|
|
451
|
-
|
|
413
|
+
assert.equal(plan.slots.preActionModel.selected.key, "vol/default");
|
|
452
414
|
}
|
|
453
415
|
if (plan.slots.skillModel.selected.kind === "profileChoice") {
|
|
454
|
-
|
|
416
|
+
assert.equal(plan.slots.skillModel.selected.key, "deep/openai_deep");
|
|
455
417
|
}
|
|
456
|
-
|
|
457
|
-
|
|
418
|
+
assert.ok(plan.caseSelection.graph.matchedConditions.some((c) => c.includes("runtime.mode eq live")));
|
|
419
|
+
assert.ok(plan.caseSelection.node?.matchedConditions.some((c) => c.includes("runtime.input.riskLevel eq high")));
|
|
458
420
|
});
|
|
459
421
|
});
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const context =
|
|
422
|
+
describe("evaluateCaseCondition", () => {
|
|
423
|
+
it("AC-004/005/006: all, any, not supported", () => {
|
|
424
|
+
const context = buildDeterministicCaseContext(createMinimalExecutableGraph(), {
|
|
463
425
|
mode: "live",
|
|
464
426
|
input: { riskLevel: "high", tier: "gold" }
|
|
465
427
|
});
|
|
466
|
-
|
|
428
|
+
assert.equal(evaluateCaseCondition({
|
|
467
429
|
all: [
|
|
468
430
|
{ path: "runtime.mode", op: "eq", value: "live" },
|
|
469
431
|
{ path: "runtime.input.riskLevel", op: "eq", value: "high" }
|
|
470
432
|
]
|
|
471
433
|
}, context), true);
|
|
472
|
-
|
|
434
|
+
assert.equal(evaluateCaseCondition({
|
|
473
435
|
any: [
|
|
474
436
|
{ path: "runtime.input.tier", op: "eq", value: "silver" },
|
|
475
437
|
{ path: "runtime.input.tier", op: "eq", value: "gold" }
|
|
476
438
|
]
|
|
477
439
|
}, context), true);
|
|
478
|
-
|
|
440
|
+
assert.equal(evaluateCaseCondition({
|
|
479
441
|
not: { path: "runtime.mode", op: "eq", value: "simulate" }
|
|
480
442
|
}, context), true);
|
|
481
443
|
});
|
|
482
444
|
});
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const doc =
|
|
486
|
-
const ext = doc.graph.metadata.extensions[
|
|
445
|
+
describe("lifecycle artifacts", () => {
|
|
446
|
+
it("AC-A001/A005: authoring graph with modelConfig.default passes", () => {
|
|
447
|
+
const doc = createMinimalExecutableGraph();
|
|
448
|
+
const ext = doc.graph.metadata.extensions[EXECUTABLE_PROFILE_NAMESPACE];
|
|
487
449
|
ext.modelConfig = {
|
|
488
450
|
version: "graph-model-config/v1",
|
|
489
451
|
default: {
|
|
@@ -492,10 +454,10 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
492
454
|
postActionModel: { kind: "profileChoice", key: "cheap/default" }
|
|
493
455
|
}
|
|
494
456
|
};
|
|
495
|
-
|
|
457
|
+
assert.equal(validateAuthoringGraph(doc).valid, true);
|
|
496
458
|
});
|
|
497
|
-
|
|
498
|
-
const bareProfile =
|
|
459
|
+
it("AC-A003/A004: bare profile and kind profile fail authoring validation", () => {
|
|
460
|
+
const bareProfile = validateAuthoringGraph(setGraphModelConfig(createMinimalExecutableGraph(), {
|
|
499
461
|
version: "graph-model-config/v1",
|
|
500
462
|
cases: [
|
|
501
463
|
{
|
|
@@ -508,8 +470,8 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
508
470
|
}
|
|
509
471
|
]
|
|
510
472
|
}));
|
|
511
|
-
|
|
512
|
-
const kindProfile =
|
|
473
|
+
assert.ok(bareProfile.errors.some((e) => e.code === "PROFILE_CHOICE_KEY_FORMAT_INVALID"));
|
|
474
|
+
const kindProfile = validateAuthoringGraph(setGraphModelConfig(createMinimalExecutableGraph(), {
|
|
513
475
|
version: "graph-model-config/v1",
|
|
514
476
|
cases: [
|
|
515
477
|
{
|
|
@@ -522,11 +484,11 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
522
484
|
}
|
|
523
485
|
]
|
|
524
486
|
}));
|
|
525
|
-
|
|
487
|
+
assert.ok(kindProfile.errors.some((e) => e.code === "PROFILE_SELECTION_KIND_DEPRECATED"));
|
|
526
488
|
});
|
|
527
|
-
|
|
528
|
-
const doc =
|
|
529
|
-
const ext = doc.graph.metadata.extensions[
|
|
489
|
+
it("AC-P001/P003/P008/P009/P010: compile produces run-bound executable plan", () => {
|
|
490
|
+
const doc = createMinimalExecutableGraph();
|
|
491
|
+
const ext = doc.graph.metadata.extensions[EXECUTABLE_PROFILE_NAMESPACE];
|
|
530
492
|
ext.modelConfig = {
|
|
531
493
|
version: "graph-model-config/v1",
|
|
532
494
|
cases: [
|
|
@@ -549,57 +511,57 @@ function setGraphModelConfig(doc, modelConfig) {
|
|
|
549
511
|
}
|
|
550
512
|
]
|
|
551
513
|
};
|
|
552
|
-
const runtime =
|
|
514
|
+
const runtime = buildRuntimeObject({
|
|
553
515
|
jobId: "job-compile-1",
|
|
554
516
|
mode: "simulate",
|
|
555
517
|
input: { target_subnet_cidr: "10.0.0.0/24" }
|
|
556
518
|
});
|
|
557
519
|
const before = structuredClone(doc);
|
|
558
|
-
const plan =
|
|
520
|
+
const plan = compileExecutablePlan(doc, runtime, {
|
|
559
521
|
profileRegistry: { version: "3.0.0", registryHash: "sha256:profiles456" },
|
|
560
522
|
environment: "prod"
|
|
561
523
|
});
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const profileExt = plan.graph.graph.metadata?.extensions?.[
|
|
569
|
-
|
|
524
|
+
assert.deepEqual(doc, before);
|
|
525
|
+
assert.equal(plan.format, "graphenix.executable-plan/v1");
|
|
526
|
+
assert.equal(plan.source.graphId, doc.id);
|
|
527
|
+
assert.equal(plan.runtimeBinding.jobId, "job-compile-1");
|
|
528
|
+
assert.equal(plan.caseSelection.graph.caseId, "simulate");
|
|
529
|
+
assert.equal(plan.profileRegistry.version, "3.0.0");
|
|
530
|
+
const profileExt = plan.graph.graph.metadata?.extensions?.[EXECUTABLE_PROFILE_NAMESPACE];
|
|
531
|
+
assert.equal("default" in (profileExt?.modelConfig ?? {}), false);
|
|
570
532
|
const professionalPlan = plan.nodePlans["node:professional-answer"];
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
const doc =
|
|
576
|
-
const runtime =
|
|
577
|
-
const plan =
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
const doc =
|
|
584
|
-
const runtime =
|
|
585
|
-
const plan =
|
|
586
|
-
const trace =
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
533
|
+
assert.equal(professionalPlan?.modelSlots?.skillModel?.selection.kind, "profileChoice");
|
|
534
|
+
assert.equal(validateExecutablePlan(plan).valid, true);
|
|
535
|
+
});
|
|
536
|
+
it("AC-E003/E006/E008: executable plan is normalized with resolved node slots", () => {
|
|
537
|
+
const doc = createMinimalExecutableGraph();
|
|
538
|
+
const runtime = buildRuntimeObject({ jobId: "job-plan-1" });
|
|
539
|
+
const plan = compileExecutablePlan(doc, runtime);
|
|
540
|
+
assert.equal(validateExecutablePlan(plan).valid, true);
|
|
541
|
+
assert.ok(plan.nodePlans["node:professional-answer"]?.modelSlots?.preActionModel);
|
|
542
|
+
assert.equal("credentials" in plan, false);
|
|
543
|
+
});
|
|
544
|
+
it("AC-T001/T002/T011: execution trace references plan and keeps append-only events", () => {
|
|
545
|
+
const doc = createMinimalExecutableGraph();
|
|
546
|
+
const runtime = buildRuntimeObject({ jobId: "job-trace-1", mode: "live" });
|
|
547
|
+
const plan = compileExecutablePlan(doc, runtime);
|
|
548
|
+
const trace = createExecutionTrace(plan, runtime);
|
|
549
|
+
assert.equal(trace.source.graphId, plan.source.graphId);
|
|
550
|
+
assert.equal(trace.plan.planId, plan.planId);
|
|
551
|
+
assert.equal(trace.status, "queued");
|
|
590
552
|
const startedAt = "2026-06-06T12:00:01.000Z";
|
|
591
|
-
const updated =
|
|
553
|
+
const updated = appendExecutionEvent(trace, {
|
|
592
554
|
id: "evt:1",
|
|
593
555
|
ts: startedAt,
|
|
594
556
|
level: "info",
|
|
595
557
|
type: "graph.started",
|
|
596
558
|
message: "Graph execution started."
|
|
597
559
|
});
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
560
|
+
assert.equal(updated.events.length, 1);
|
|
561
|
+
assert.notEqual(updated, trace);
|
|
562
|
+
assert.equal(validateExecutionTrace(updated, plan.nodePlans).valid, true);
|
|
601
563
|
const nodeTrace = updated.nodeExecutions["node:professional-answer"];
|
|
602
|
-
|
|
564
|
+
assert.ok(nodeTrace?.units["unit:node:professional-answer:skill"]);
|
|
603
565
|
});
|
|
604
566
|
});
|
|
605
567
|
function getTaskNode(doc, nodeId = "node:professional-answer") {
|
|
@@ -608,7 +570,7 @@ function getTaskNode(doc, nodeId = "node:professional-answer") {
|
|
|
608
570
|
function setTaskConfiguration(doc, config) {
|
|
609
571
|
const node = getTaskNode(doc);
|
|
610
572
|
const params = node.parameters;
|
|
611
|
-
|
|
573
|
+
assert.ok(params && params.nodeType === "task");
|
|
612
574
|
node.parameters = {
|
|
613
575
|
...params,
|
|
614
576
|
taskConfiguration: config
|
|
@@ -616,22 +578,22 @@ function setTaskConfiguration(doc, config) {
|
|
|
616
578
|
return doc;
|
|
617
579
|
}
|
|
618
580
|
function setGraphPolicies(doc, policies) {
|
|
619
|
-
const ext = doc.graph.metadata.extensions[
|
|
581
|
+
const ext = doc.graph.metadata.extensions[EXECUTABLE_PROFILE_NAMESPACE];
|
|
620
582
|
ext.policies = policies;
|
|
621
583
|
return doc;
|
|
622
584
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const doc =
|
|
626
|
-
const runtime =
|
|
627
|
-
const plan =
|
|
585
|
+
describe("execution units", () => {
|
|
586
|
+
it("AC-001: task node with no preActions/postActions compiles into one skill unit", () => {
|
|
587
|
+
const doc = createMinimalExecutableGraph();
|
|
588
|
+
const runtime = buildRuntimeObject({ jobId: "job-units-1" });
|
|
589
|
+
const plan = compileExecutablePlan(doc, runtime);
|
|
628
590
|
const units = plan.nodePlans["node:professional-answer"].executionUnits;
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
591
|
+
assert.equal(units.length, 1);
|
|
592
|
+
assert.equal(units[0].unitKind, "skill");
|
|
593
|
+
assert.equal(units[0].unitId, "unit:node:professional-answer:skill");
|
|
632
594
|
});
|
|
633
|
-
|
|
634
|
-
const doc = setTaskConfiguration(
|
|
595
|
+
it("AC-002/003/004: preActions before skill and postActions after with stable order", () => {
|
|
596
|
+
const doc = setTaskConfiguration(createMinimalExecutableGraph(), {
|
|
635
597
|
preActions: [
|
|
636
598
|
{ id: "pre:a", actionKey: "prepare-input" },
|
|
637
599
|
{ id: "pre:b", actionKey: "normalize-input" }
|
|
@@ -641,10 +603,10 @@ function setGraphPolicies(doc, policies) {
|
|
|
641
603
|
{ id: "post:b", actionKey: "shape-response" }
|
|
642
604
|
]
|
|
643
605
|
});
|
|
644
|
-
const plan =
|
|
606
|
+
const plan = compileExecutablePlan(doc, buildRuntimeObject({ jobId: "job-units-2" }));
|
|
645
607
|
const units = plan.nodePlans["node:professional-answer"].executionUnits;
|
|
646
|
-
|
|
647
|
-
|
|
608
|
+
assert.equal(units.length, 5);
|
|
609
|
+
assert.deepEqual(units.map((unit) => [unit.order, unit.unitKind, unit.actionKey ?? unit.skillKey]), [
|
|
648
610
|
[0, "preAction", "prepare-input"],
|
|
649
611
|
[1, "preAction", "normalize-input"],
|
|
650
612
|
[2, "skill", "professional-answer"],
|
|
@@ -652,31 +614,31 @@ function setGraphPolicies(doc, policies) {
|
|
|
652
614
|
[4, "postAction", "shape-response"]
|
|
653
615
|
]);
|
|
654
616
|
});
|
|
655
|
-
|
|
656
|
-
const doc = setTaskConfiguration(
|
|
617
|
+
it("AC-005/006/007: units use resolved node model slots", () => {
|
|
618
|
+
const doc = setTaskConfiguration(createMinimalExecutableGraph(), {
|
|
657
619
|
preActions: [{ id: "pre:a", actionKey: "prepare-input" }],
|
|
658
620
|
postActions: [{ id: "post:a", actionKey: "validate-output" }]
|
|
659
621
|
});
|
|
660
|
-
const plan =
|
|
622
|
+
const plan = compileExecutablePlan(doc, buildRuntimeObject({ jobId: "job-units-3" }));
|
|
661
623
|
const nodePlan = plan.nodePlans["node:professional-answer"];
|
|
662
624
|
const pre = nodePlan.executionUnits.find((unit) => unit.unitKind === "preAction");
|
|
663
625
|
const skill = nodePlan.executionUnits.find((unit) => unit.unitKind === "skill");
|
|
664
626
|
const post = nodePlan.executionUnits.find((unit) => unit.unitKind === "postAction");
|
|
665
|
-
|
|
627
|
+
assert.equal(pre.modelSlot, "preActionModel");
|
|
666
628
|
if (pre.modelSelection?.kind === "profileChoice") {
|
|
667
|
-
|
|
629
|
+
assert.equal(pre.modelSelection.key, "cheap/default");
|
|
668
630
|
}
|
|
669
|
-
|
|
631
|
+
assert.equal(skill.modelSlot, "skillModel");
|
|
670
632
|
if (skill.modelSelection?.kind === "profileChoice") {
|
|
671
|
-
|
|
633
|
+
assert.equal(skill.modelSelection.key, "vol/default");
|
|
672
634
|
}
|
|
673
|
-
|
|
635
|
+
assert.equal(post.modelSlot, "postActionModel");
|
|
674
636
|
if (post.modelSelection?.kind === "profileChoice") {
|
|
675
|
-
|
|
637
|
+
assert.equal(post.modelSelection.key, "cheap/default");
|
|
676
638
|
}
|
|
677
639
|
});
|
|
678
|
-
|
|
679
|
-
const doc = setTaskConfiguration(
|
|
640
|
+
it("AC-008/009: node skillModel override does not affect pre/post units", () => {
|
|
641
|
+
const doc = setTaskConfiguration(createMinimalExecutableGraph(), {
|
|
680
642
|
modelConfig: {
|
|
681
643
|
inherit: true,
|
|
682
644
|
modelConfig: {
|
|
@@ -686,23 +648,23 @@ function setGraphPolicies(doc, policies) {
|
|
|
686
648
|
preActions: [{ id: "pre:a", actionKey: "prepare-input" }],
|
|
687
649
|
postActions: [{ id: "post:a", actionKey: "validate-output" }]
|
|
688
650
|
});
|
|
689
|
-
const plan =
|
|
651
|
+
const plan = compileExecutablePlan(doc, buildRuntimeObject({ jobId: "job-units-4" }));
|
|
690
652
|
const units = plan.nodePlans["node:professional-answer"].executionUnits;
|
|
691
653
|
const pre = units.find((unit) => unit.unitKind === "preAction");
|
|
692
654
|
const skill = units.find((unit) => unit.unitKind === "skill");
|
|
693
655
|
const post = units.find((unit) => unit.unitKind === "postAction");
|
|
694
656
|
if (pre.modelSelection?.kind === "profileChoice") {
|
|
695
|
-
|
|
657
|
+
assert.equal(pre.modelSelection.key, "cheap/default");
|
|
696
658
|
}
|
|
697
659
|
if (skill.modelSelection?.kind === "profileChoice") {
|
|
698
|
-
|
|
660
|
+
assert.equal(skill.modelSelection.key, "deep/openai_deep");
|
|
699
661
|
}
|
|
700
662
|
if (post.modelSelection?.kind === "profileChoice") {
|
|
701
|
-
|
|
663
|
+
assert.equal(post.modelSelection.key, "cheap/default");
|
|
702
664
|
}
|
|
703
665
|
});
|
|
704
|
-
|
|
705
|
-
const doc = setTaskConfiguration(
|
|
666
|
+
it("AC-010: action-level model override fails when policy disabled", () => {
|
|
667
|
+
const doc = setTaskConfiguration(createMinimalExecutableGraph(), {
|
|
706
668
|
preActions: [
|
|
707
669
|
{
|
|
708
670
|
id: "pre:a",
|
|
@@ -711,11 +673,11 @@ function setGraphPolicies(doc, policies) {
|
|
|
711
673
|
}
|
|
712
674
|
]
|
|
713
675
|
});
|
|
714
|
-
const result =
|
|
715
|
-
|
|
676
|
+
const result = validateAuthoringGraph(doc);
|
|
677
|
+
assert.ok(result.errors.some((e) => e.code === "TASK_ACTION_MODEL_OVERRIDE_FORBIDDEN"));
|
|
716
678
|
});
|
|
717
|
-
|
|
718
|
-
const doc = setGraphPolicies(setTaskConfiguration(
|
|
679
|
+
it("AC-011/012: action-level model override passes when policy enabled", () => {
|
|
680
|
+
const doc = setGraphPolicies(setTaskConfiguration(createMinimalExecutableGraph(), {
|
|
719
681
|
preActions: [
|
|
720
682
|
{
|
|
721
683
|
id: "pre:a",
|
|
@@ -724,16 +686,16 @@ function setGraphPolicies(doc, policies) {
|
|
|
724
686
|
}
|
|
725
687
|
]
|
|
726
688
|
}), { allowActionLevelModelOverride: true });
|
|
727
|
-
|
|
728
|
-
const plan =
|
|
689
|
+
assert.equal(validateAuthoringGraph(doc).valid, true);
|
|
690
|
+
const plan = compileExecutablePlan(doc, buildRuntimeObject({ jobId: "job-units-5" }));
|
|
729
691
|
const pre = plan.nodePlans["node:professional-answer"].executionUnits[0];
|
|
730
|
-
|
|
692
|
+
assert.equal(pre.modelSource, "actionOverride");
|
|
731
693
|
if (pre.modelSelection?.kind === "profileChoice") {
|
|
732
|
-
|
|
694
|
+
assert.equal(pre.modelSelection.key, "vol/pro");
|
|
733
695
|
}
|
|
734
696
|
});
|
|
735
|
-
|
|
736
|
-
const doc = setGraphPolicies(setTaskConfiguration(
|
|
697
|
+
it("AC-013: bare profile key in action-level override fails", () => {
|
|
698
|
+
const doc = setGraphPolicies(setTaskConfiguration(createMinimalExecutableGraph(), {
|
|
737
699
|
preActions: [
|
|
738
700
|
{
|
|
739
701
|
id: "pre:a",
|
|
@@ -742,11 +704,11 @@ function setGraphPolicies(doc, policies) {
|
|
|
742
704
|
}
|
|
743
705
|
]
|
|
744
706
|
}), { allowActionLevelModelOverride: true });
|
|
745
|
-
const result =
|
|
746
|
-
|
|
707
|
+
const result = validateAuthoringGraph(doc);
|
|
708
|
+
assert.ok(result.errors.some((e) => e.code === "PROFILE_CHOICE_KEY_FORMAT_INVALID"));
|
|
747
709
|
});
|
|
748
|
-
|
|
749
|
-
const resolvedPre =
|
|
710
|
+
it("AC-014/015/016: unit fallback stays on same slot only", () => {
|
|
711
|
+
const resolvedPre = resolveNodeModelSlot({
|
|
750
712
|
slot: "preActionModel",
|
|
751
713
|
graphSelection: { kind: "profileChoice", key: "cheap/default" },
|
|
752
714
|
nodeSelection: { kind: "profileChoice", key: "vol/pro" },
|
|
@@ -762,29 +724,29 @@ function setGraphPolicies(doc, policies) {
|
|
|
762
724
|
graphCaseId: "default",
|
|
763
725
|
nodeId: "node:professional-answer"
|
|
764
726
|
});
|
|
765
|
-
|
|
727
|
+
assert.equal(resolvedPre.source, "fallbackToGraphDefault");
|
|
766
728
|
if (resolvedPre.selected.kind === "profileChoice") {
|
|
767
|
-
|
|
729
|
+
assert.equal(resolvedPre.selected.key, "cheap/default");
|
|
768
730
|
}
|
|
769
|
-
const plan =
|
|
731
|
+
const plan = compileExecutablePlan(setTaskConfiguration(createMinimalExecutableGraph(), {
|
|
770
732
|
preActions: [{ id: "pre:a", actionKey: "prepare-input" }],
|
|
771
733
|
postActions: [{ id: "post:a", actionKey: "validate-output" }]
|
|
772
|
-
}),
|
|
734
|
+
}), buildRuntimeObject({ jobId: "job-units-fallback" }));
|
|
773
735
|
const units = plan.nodePlans["node:professional-answer"].executionUnits;
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
736
|
+
assert.ok(units.every((unit) => unit.modelSlot !== "skillModel" || unit.unitKind === "skill"));
|
|
737
|
+
assert.ok(units.every((unit) => unit.modelSlot !== "postActionModel" || unit.unitKind === "postAction"));
|
|
738
|
+
assert.ok(units.every((unit) => unit.modelSlot !== "preActionModel" || unit.unitKind === "preAction"));
|
|
777
739
|
});
|
|
778
|
-
|
|
779
|
-
const doc = setTaskConfiguration(
|
|
740
|
+
it("AC-017/018/019: execution trace records planned units and unit fallback", () => {
|
|
741
|
+
const doc = setTaskConfiguration(createMinimalExecutableGraph(), {
|
|
780
742
|
preActions: [{ id: "pre:a", actionKey: "prepare-input" }],
|
|
781
743
|
postActions: [{ id: "post:a", actionKey: "validate-output" }]
|
|
782
744
|
});
|
|
783
|
-
const runtime =
|
|
784
|
-
const plan =
|
|
785
|
-
const trace =
|
|
745
|
+
const runtime = buildRuntimeObject({ jobId: "job-units-6" });
|
|
746
|
+
const plan = compileExecutablePlan(doc, runtime);
|
|
747
|
+
const trace = createExecutionTrace(plan, runtime);
|
|
786
748
|
const nodeTrace = trace.nodeExecutions["node:professional-answer"];
|
|
787
|
-
|
|
749
|
+
assert.equal(Object.keys(nodeTrace.units).length, 3);
|
|
788
750
|
const skillUnitId = "unit:node:professional-answer:skill";
|
|
789
751
|
nodeTrace.units[skillUnitId] = {
|
|
790
752
|
...nodeTrace.units[skillUnitId],
|
|
@@ -811,8 +773,8 @@ function setGraphPolicies(doc, policies) {
|
|
|
811
773
|
enabled: true,
|
|
812
774
|
allowedTriggers: ["nodeModelUnavailable"]
|
|
813
775
|
};
|
|
814
|
-
|
|
815
|
-
const withEvent =
|
|
776
|
+
assert.equal(validateExecutionTrace(invalidFallback, invalidPolicyPlan.nodePlans).valid, false);
|
|
777
|
+
const withEvent = appendExecutionEvent(trace, {
|
|
816
778
|
id: "evt:unit:1",
|
|
817
779
|
ts: "2026-06-06T12:00:02.100Z",
|
|
818
780
|
level: "info",
|
|
@@ -826,18 +788,18 @@ function setGraphPolicies(doc, policies) {
|
|
|
826
788
|
source: "graphDefault"
|
|
827
789
|
}
|
|
828
790
|
});
|
|
829
|
-
|
|
791
|
+
assert.equal(withEvent.events[0].type, "executionUnit.model.resolved");
|
|
830
792
|
});
|
|
831
|
-
|
|
832
|
-
const doc = setTaskConfiguration(
|
|
793
|
+
it("AC-020: graph, plan, and trace remain implementation-agnostic", () => {
|
|
794
|
+
const doc = setTaskConfiguration(createMinimalExecutableGraph(), {
|
|
833
795
|
preActions: [{ id: "pre:a", actionKey: "prepare-input" }]
|
|
834
796
|
});
|
|
835
|
-
const runtime =
|
|
836
|
-
const plan =
|
|
837
|
-
const trace =
|
|
797
|
+
const runtime = buildRuntimeObject({ jobId: "job-units-7" });
|
|
798
|
+
const plan = compileExecutablePlan(doc, runtime);
|
|
799
|
+
const trace = createExecutionTrace(plan, runtime);
|
|
838
800
|
const serialized = JSON.stringify({ doc, plan, trace });
|
|
839
801
|
for (const forbidden of ["openrouter", "anthropic-sdk", "langchain"]) {
|
|
840
|
-
|
|
802
|
+
assert.equal(serialized.includes(forbidden), false);
|
|
841
803
|
}
|
|
842
804
|
});
|
|
843
805
|
});
|