@manifesto-ai/core 2.5.0 → 2.6.1
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 +8 -7
- package/dist/index.d.ts +1749 -19
- package/dist/index.js +18456 -36
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/__tests__/apply.test.d.ts +0 -2
- package/dist/__tests__/apply.test.d.ts.map +0 -1
- package/dist/__tests__/apply.test.js +0 -279
- package/dist/__tests__/apply.test.js.map +0 -1
- package/dist/__tests__/defaults.test.d.ts +0 -2
- package/dist/__tests__/defaults.test.d.ts.map +0 -1
- package/dist/__tests__/defaults.test.js +0 -74
- package/dist/__tests__/defaults.test.js.map +0 -1
- package/dist/__tests__/jcs.test.d.ts +0 -2
- package/dist/__tests__/jcs.test.d.ts.map +0 -1
- package/dist/__tests__/jcs.test.js +0 -45
- package/dist/__tests__/jcs.test.js.map +0 -1
- package/dist/core/apply.d.ts +0 -17
- package/dist/core/apply.d.ts.map +0 -1
- package/dist/core/apply.js +0 -198
- package/dist/core/apply.js.map +0 -1
- package/dist/core/compute.d.ts +0 -17
- package/dist/core/compute.d.ts.map +0 -1
- package/dist/core/compute.js +0 -305
- package/dist/core/compute.js.map +0 -1
- package/dist/core/compute.test.d.ts +0 -2
- package/dist/core/compute.test.d.ts.map +0 -1
- package/dist/core/compute.test.js +0 -950
- package/dist/core/compute.test.js.map +0 -1
- package/dist/core/explain.d.ts +0 -14
- package/dist/core/explain.d.ts.map +0 -1
- package/dist/core/explain.js +0 -78
- package/dist/core/explain.js.map +0 -1
- package/dist/core/index.d.ts +0 -5
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -5
- package/dist/core/index.js.map +0 -1
- package/dist/core/validate.d.ts +0 -16
- package/dist/core/validate.d.ts.map +0 -1
- package/dist/core/validate.js +0 -361
- package/dist/core/validate.js.map +0 -1
- package/dist/core/validate.test.d.ts +0 -2
- package/dist/core/validate.test.d.ts.map +0 -1
- package/dist/core/validate.test.js +0 -638
- package/dist/core/validate.test.js.map +0 -1
- package/dist/core/validation-utils.d.ts +0 -20
- package/dist/core/validation-utils.d.ts.map +0 -1
- package/dist/core/validation-utils.js +0 -292
- package/dist/core/validation-utils.js.map +0 -1
- package/dist/errors.d.ts +0 -30
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -51
- package/dist/errors.js.map +0 -1
- package/dist/evaluator/computed.d.ts +0 -14
- package/dist/evaluator/computed.d.ts.map +0 -1
- package/dist/evaluator/computed.js +0 -60
- package/dist/evaluator/computed.js.map +0 -1
- package/dist/evaluator/context.d.ts +0 -62
- package/dist/evaluator/context.d.ts.map +0 -1
- package/dist/evaluator/context.js +0 -44
- package/dist/evaluator/context.js.map +0 -1
- package/dist/evaluator/dag.d.ts +0 -30
- package/dist/evaluator/dag.d.ts.map +0 -1
- package/dist/evaluator/dag.js +0 -121
- package/dist/evaluator/dag.js.map +0 -1
- package/dist/evaluator/expr.d.ts +0 -11
- package/dist/evaluator/expr.d.ts.map +0 -1
- package/dist/evaluator/expr.js +0 -667
- package/dist/evaluator/expr.js.map +0 -1
- package/dist/evaluator/expr.test.d.ts +0 -2
- package/dist/evaluator/expr.test.d.ts.map +0 -1
- package/dist/evaluator/expr.test.js +0 -508
- package/dist/evaluator/expr.test.js.map +0 -1
- package/dist/evaluator/flow.d.ts +0 -36
- package/dist/evaluator/flow.d.ts.map +0 -1
- package/dist/evaluator/flow.js +0 -390
- package/dist/evaluator/flow.js.map +0 -1
- package/dist/evaluator/flow.test.d.ts +0 -2
- package/dist/evaluator/flow.test.d.ts.map +0 -1
- package/dist/evaluator/flow.test.js +0 -499
- package/dist/evaluator/flow.test.js.map +0 -1
- package/dist/evaluator/index.d.ts +0 -6
- package/dist/evaluator/index.d.ts.map +0 -1
- package/dist/evaluator/index.js +0 -6
- package/dist/evaluator/index.js.map +0 -1
- package/dist/factories.d.ts +0 -22
- package/dist/factories.d.ts.map +0 -1
- package/dist/factories.js +0 -44
- package/dist/factories.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -14
- package/dist/index.test.js.map +0 -1
- package/dist/schema/action.d.ts +0 -14
- package/dist/schema/action.d.ts.map +0 -1
- package/dist/schema/action.js +0 -30
- package/dist/schema/action.js.map +0 -1
- package/dist/schema/common.d.ts +0 -37
- package/dist/schema/common.d.ts.map +0 -1
- package/dist/schema/common.js +0 -20
- package/dist/schema/common.js.map +0 -1
- package/dist/schema/computed.d.ts +0 -23
- package/dist/schema/computed.d.ts.map +0 -1
- package/dist/schema/computed.js +0 -34
- package/dist/schema/computed.js.map +0 -1
- package/dist/schema/defaults.d.ts +0 -12
- package/dist/schema/defaults.d.ts.map +0 -1
- package/dist/schema/defaults.js +0 -19
- package/dist/schema/defaults.js.map +0 -1
- package/dist/schema/domain.d.ts +0 -50
- package/dist/schema/domain.d.ts.map +0 -1
- package/dist/schema/domain.js +0 -60
- package/dist/schema/domain.js.map +0 -1
- package/dist/schema/expr.d.ts +0 -310
- package/dist/schema/expr.d.ts.map +0 -1
- package/dist/schema/expr.js +0 -289
- package/dist/schema/expr.js.map +0 -1
- package/dist/schema/field.d.ts +0 -48
- package/dist/schema/field.d.ts.map +0 -1
- package/dist/schema/field.js +0 -31
- package/dist/schema/field.js.map +0 -1
- package/dist/schema/flow.d.ts +0 -103
- package/dist/schema/flow.d.ts.map +0 -1
- package/dist/schema/flow.js +0 -82
- package/dist/schema/flow.js.map +0 -1
- package/dist/schema/host-context.d.ts +0 -12
- package/dist/schema/host-context.d.ts.map +0 -1
- package/dist/schema/host-context.js +0 -23
- package/dist/schema/host-context.js.map +0 -1
- package/dist/schema/index.d.ts +0 -15
- package/dist/schema/index.d.ts.map +0 -1
- package/dist/schema/index.js +0 -28
- package/dist/schema/index.js.map +0 -1
- package/dist/schema/patch.d.ts +0 -59
- package/dist/schema/patch.d.ts.map +0 -1
- package/dist/schema/patch.js +0 -60
- package/dist/schema/patch.js.map +0 -1
- package/dist/schema/result.d.ts +0 -142
- package/dist/schema/result.d.ts.map +0 -1
- package/dist/schema/result.js +0 -94
- package/dist/schema/result.js.map +0 -1
- package/dist/schema/snapshot.d.ts +0 -153
- package/dist/schema/snapshot.d.ts.map +0 -1
- package/dist/schema/snapshot.js +0 -160
- package/dist/schema/snapshot.js.map +0 -1
- package/dist/schema/trace.d.ts +0 -98
- package/dist/schema/trace.d.ts.map +0 -1
- package/dist/schema/trace.js +0 -90
- package/dist/schema/trace.js.map +0 -1
- package/dist/schema/type-spec.d.ts +0 -34
- package/dist/schema/type-spec.d.ts.map +0 -1
- package/dist/schema/type-spec.js +0 -40
- package/dist/schema/type-spec.js.map +0 -1
- package/dist/utils/canonical.d.ts +0 -37
- package/dist/utils/canonical.d.ts.map +0 -1
- package/dist/utils/canonical.js +0 -122
- package/dist/utils/canonical.js.map +0 -1
- package/dist/utils/canonical.test.d.ts +0 -2
- package/dist/utils/canonical.test.d.ts.map +0 -1
- package/dist/utils/canonical.test.js +0 -183
- package/dist/utils/canonical.test.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -55
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js +0 -183
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/hash.test.d.ts +0 -2
- package/dist/utils/hash.test.d.ts.map +0 -1
- package/dist/utils/hash.test.js +0 -253
- package/dist/utils/hash.test.js.map +0 -1
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -4
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/path.d.ts +0 -40
- package/dist/utils/path.d.ts.map +0 -1
- package/dist/utils/path.js +0 -132
- package/dist/utils/path.js.map +0 -1
- package/dist/utils/path.test.d.ts +0 -2
- package/dist/utils/path.test.d.ts.map +0 -1
- package/dist/utils/path.test.js +0 -191
- package/dist/utils/path.test.js.map +0 -1
|
@@ -1,950 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { compute, computeSync } from "./compute.js";
|
|
3
|
-
import { createSnapshot, createIntent } from "../factories.js";
|
|
4
|
-
import { hashSchemaSync } from "../utils/hash.js";
|
|
5
|
-
const BASE_STATE_FIELDS = {
|
|
6
|
-
dummy: { type: "string", required: true },
|
|
7
|
-
count: { type: "number", required: true },
|
|
8
|
-
name: { type: "string", required: true },
|
|
9
|
-
balance: { type: "number", required: true },
|
|
10
|
-
a: { type: "number", required: true },
|
|
11
|
-
b: { type: "number", required: true },
|
|
12
|
-
loading: { type: "boolean", required: true },
|
|
13
|
-
started: { type: "boolean", required: true },
|
|
14
|
-
completed: { type: "boolean", required: true },
|
|
15
|
-
value: { type: "string", required: true },
|
|
16
|
-
todos: {
|
|
17
|
-
type: "array",
|
|
18
|
-
required: true,
|
|
19
|
-
items: {
|
|
20
|
-
type: "object",
|
|
21
|
-
required: true,
|
|
22
|
-
fields: {
|
|
23
|
-
completed: { type: "boolean", required: true },
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
fromBalance: { type: "number", required: true },
|
|
28
|
-
toBalance: { type: "number", required: true },
|
|
29
|
-
done: { type: "boolean", required: true },
|
|
30
|
-
};
|
|
31
|
-
const BASE_COMPUTED_FIELDS = {
|
|
32
|
-
"computed.dummy": {
|
|
33
|
-
expr: { kind: "get", path: "dummy" },
|
|
34
|
-
deps: ["dummy"],
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
const BASE_ACTIONS = {
|
|
38
|
-
noop: {
|
|
39
|
-
flow: { kind: "halt", reason: "noop" },
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
// Helper to create a minimal domain schema
|
|
43
|
-
function createTestSchema(overrides = {}) {
|
|
44
|
-
const { state, computed, actions: overrideActions, hash, types, ...restOverrides } = overrides;
|
|
45
|
-
const stateFields = {
|
|
46
|
-
...BASE_STATE_FIELDS,
|
|
47
|
-
...(state?.fields ?? {}),
|
|
48
|
-
};
|
|
49
|
-
const computedFields = {
|
|
50
|
-
...BASE_COMPUTED_FIELDS,
|
|
51
|
-
...(computed?.fields ?? {}),
|
|
52
|
-
};
|
|
53
|
-
const actions = {
|
|
54
|
-
...BASE_ACTIONS,
|
|
55
|
-
...(overrideActions ?? {}),
|
|
56
|
-
};
|
|
57
|
-
const schemaWithoutHash = {
|
|
58
|
-
id: "manifesto:test",
|
|
59
|
-
version: "1.0.0",
|
|
60
|
-
...restOverrides,
|
|
61
|
-
types: types ?? {},
|
|
62
|
-
state: { fields: stateFields },
|
|
63
|
-
computed: { fields: computedFields },
|
|
64
|
-
actions,
|
|
65
|
-
};
|
|
66
|
-
return {
|
|
67
|
-
...schemaWithoutHash,
|
|
68
|
-
hash: hash ?? hashSchemaSync(schemaWithoutHash),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
const HOST_CONTEXT = { now: 0, randomSeed: "seed" };
|
|
72
|
-
let intentCounter = 0;
|
|
73
|
-
const nextIntentId = () => `intent-${intentCounter++}`;
|
|
74
|
-
const createTestIntent = (type, input) => input === undefined
|
|
75
|
-
? createIntent(type, nextIntentId())
|
|
76
|
-
: createIntent(type, input, nextIntentId());
|
|
77
|
-
const createTestSnapshot = (data, schemaHash) => createSnapshot(data, schemaHash, HOST_CONTEXT);
|
|
78
|
-
const computeWithContext = (schema, snapshot, intent) => compute(schema, snapshot, intent, HOST_CONTEXT);
|
|
79
|
-
describe("compute", () => {
|
|
80
|
-
describe("Basic Intent Processing", () => {
|
|
81
|
-
it("should process a simple action", async () => {
|
|
82
|
-
const schema = createTestSchema({
|
|
83
|
-
actions: {
|
|
84
|
-
increment: {
|
|
85
|
-
flow: {
|
|
86
|
-
kind: "patch",
|
|
87
|
-
op: "set",
|
|
88
|
-
path: "count",
|
|
89
|
-
value: {
|
|
90
|
-
kind: "add",
|
|
91
|
-
left: { kind: "coalesce", args: [{ kind: "get", path: "count" }, { kind: "lit", value: 0 }] },
|
|
92
|
-
right: { kind: "lit", value: 1 },
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
|
|
99
|
-
const intent = createTestIntent("increment");
|
|
100
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
101
|
-
expect(result.status).toBe("complete");
|
|
102
|
-
expect(result.snapshot.data).toEqual({ count: 1 });
|
|
103
|
-
expect(result.snapshot.meta.version).toBe(1);
|
|
104
|
-
});
|
|
105
|
-
it("should handle unknown action", async () => {
|
|
106
|
-
const schema = createTestSchema();
|
|
107
|
-
const snapshot = createTestSnapshot({}, schema.hash);
|
|
108
|
-
const intent = createTestIntent("nonexistent");
|
|
109
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
110
|
-
expect(result.status).toBe("error");
|
|
111
|
-
expect(result.snapshot.system.lastError?.code).toBe("UNKNOWN_ACTION");
|
|
112
|
-
});
|
|
113
|
-
it("should handle action with input", async () => {
|
|
114
|
-
const schema = createTestSchema({
|
|
115
|
-
actions: {
|
|
116
|
-
setName: {
|
|
117
|
-
flow: {
|
|
118
|
-
kind: "patch",
|
|
119
|
-
op: "set",
|
|
120
|
-
path: "name",
|
|
121
|
-
value: { kind: "get", path: "input.name" },
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
const snapshot = createTestSnapshot({}, schema.hash);
|
|
127
|
-
const intent = createTestIntent("setName", { name: "Alice" });
|
|
128
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
129
|
-
expect(result.status).toBe("complete");
|
|
130
|
-
expect(result.snapshot.data).toEqual({ name: "Alice" });
|
|
131
|
-
});
|
|
132
|
-
it("should reject intent without intentId", async () => {
|
|
133
|
-
const schema = createTestSchema({
|
|
134
|
-
actions: {
|
|
135
|
-
increment: {
|
|
136
|
-
flow: {
|
|
137
|
-
kind: "patch",
|
|
138
|
-
op: "set",
|
|
139
|
-
path: "count",
|
|
140
|
-
value: { kind: "lit", value: 1 },
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
|
|
146
|
-
const intent = { type: "increment", input: undefined, intentId: "" };
|
|
147
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
148
|
-
expect(result.status).toBe("error");
|
|
149
|
-
expect(result.snapshot.system.lastError?.code).toBe("INVALID_INPUT");
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
describe("Meta Access", () => {
|
|
153
|
-
it("should expose meta intentId without mutating input", async () => {
|
|
154
|
-
const schema = createTestSchema({
|
|
155
|
-
actions: {
|
|
156
|
-
markIntent: {
|
|
157
|
-
flow: {
|
|
158
|
-
kind: "patch",
|
|
159
|
-
op: "set",
|
|
160
|
-
path: "value",
|
|
161
|
-
value: { kind: "get", path: "meta.intentId" },
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
const snapshot = createTestSnapshot({}, schema.hash);
|
|
167
|
-
const intent = createTestIntent("markIntent", { name: "Alice" });
|
|
168
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
169
|
-
expect(result.status).toBe("complete");
|
|
170
|
-
expect(result.snapshot.data).toEqual({ value: intent.intentId });
|
|
171
|
-
expect(result.snapshot.input).toEqual({ name: "Alice" });
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
describe("Availability Check", () => {
|
|
175
|
-
it("should check availability condition", async () => {
|
|
176
|
-
const schema = createTestSchema({
|
|
177
|
-
actions: {
|
|
178
|
-
withdraw: {
|
|
179
|
-
available: {
|
|
180
|
-
kind: "gt",
|
|
181
|
-
left: { kind: "get", path: "balance" },
|
|
182
|
-
right: { kind: "lit", value: 0 },
|
|
183
|
-
},
|
|
184
|
-
flow: {
|
|
185
|
-
kind: "patch",
|
|
186
|
-
op: "set",
|
|
187
|
-
path: "balance",
|
|
188
|
-
value: {
|
|
189
|
-
kind: "sub",
|
|
190
|
-
left: { kind: "get", path: "balance" },
|
|
191
|
-
right: { kind: "get", path: "input.amount" },
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
});
|
|
197
|
-
// Should succeed when balance > 0
|
|
198
|
-
const snapshot1 = createTestSnapshot({ balance: 100 }, schema.hash);
|
|
199
|
-
const intent1 = createTestIntent("withdraw", { amount: 50 });
|
|
200
|
-
const result1 = await computeWithContext(schema, snapshot1, intent1);
|
|
201
|
-
expect(result1.status).toBe("complete");
|
|
202
|
-
expect(result1.snapshot.data).toEqual({ balance: 50 });
|
|
203
|
-
// Should fail when balance = 0
|
|
204
|
-
const snapshot2 = createTestSnapshot({ balance: 0 }, schema.hash);
|
|
205
|
-
const intent2 = createTestIntent("withdraw", { amount: 50 });
|
|
206
|
-
const result2 = await computeWithContext(schema, snapshot2, intent2);
|
|
207
|
-
expect(result2.status).toBe("error");
|
|
208
|
-
expect(result2.snapshot.system.lastError?.code).toBe("ACTION_UNAVAILABLE");
|
|
209
|
-
});
|
|
210
|
-
it("should fail when availability does not return boolean", async () => {
|
|
211
|
-
const schema = createTestSchema({
|
|
212
|
-
actions: {
|
|
213
|
-
invalidAvailable: {
|
|
214
|
-
available: {
|
|
215
|
-
kind: "add",
|
|
216
|
-
left: { kind: "lit", value: 1 },
|
|
217
|
-
right: { kind: "lit", value: 2 },
|
|
218
|
-
},
|
|
219
|
-
flow: {
|
|
220
|
-
kind: "patch",
|
|
221
|
-
op: "set",
|
|
222
|
-
path: "count",
|
|
223
|
-
value: { kind: "lit", value: 1 },
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
|
|
229
|
-
const intent = createTestIntent("invalidAvailable");
|
|
230
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
231
|
-
expect(result.status).toBe("error");
|
|
232
|
-
expect(result.snapshot.system.lastError?.code).toBe("TYPE_MISMATCH");
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
describe("Input Validation", () => {
|
|
236
|
-
it("should reject invalid input types", async () => {
|
|
237
|
-
const schema = createTestSchema({
|
|
238
|
-
actions: {
|
|
239
|
-
setCount: {
|
|
240
|
-
input: {
|
|
241
|
-
type: "object",
|
|
242
|
-
required: true,
|
|
243
|
-
fields: {
|
|
244
|
-
value: { type: "number", required: true },
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
flow: {
|
|
248
|
-
kind: "patch",
|
|
249
|
-
op: "set",
|
|
250
|
-
path: "count",
|
|
251
|
-
value: { kind: "get", path: "input.value" },
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
|
|
257
|
-
const intent = createTestIntent("setCount", { value: "not-a-number" });
|
|
258
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
259
|
-
expect(result.status).toBe("error");
|
|
260
|
-
expect(result.snapshot.system.lastError?.code).toBe("INVALID_INPUT");
|
|
261
|
-
});
|
|
262
|
-
it("should reject missing required input fields", async () => {
|
|
263
|
-
const schema = createTestSchema({
|
|
264
|
-
actions: {
|
|
265
|
-
setCount: {
|
|
266
|
-
input: {
|
|
267
|
-
type: "object",
|
|
268
|
-
required: true,
|
|
269
|
-
fields: {
|
|
270
|
-
value: { type: "number", required: true },
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
flow: {
|
|
274
|
-
kind: "patch",
|
|
275
|
-
op: "set",
|
|
276
|
-
path: "count",
|
|
277
|
-
value: { kind: "get", path: "input.value" },
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
},
|
|
281
|
-
});
|
|
282
|
-
const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
|
|
283
|
-
const intent = createTestIntent("setCount", {});
|
|
284
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
285
|
-
expect(result.status).toBe("error");
|
|
286
|
-
expect(result.snapshot.system.lastError?.code).toBe("INVALID_INPUT");
|
|
287
|
-
});
|
|
288
|
-
it("should reject unknown input fields", async () => {
|
|
289
|
-
const schema = createTestSchema({
|
|
290
|
-
actions: {
|
|
291
|
-
setCount: {
|
|
292
|
-
input: {
|
|
293
|
-
type: "object",
|
|
294
|
-
required: true,
|
|
295
|
-
fields: {
|
|
296
|
-
value: { type: "number", required: true },
|
|
297
|
-
},
|
|
298
|
-
},
|
|
299
|
-
flow: {
|
|
300
|
-
kind: "patch",
|
|
301
|
-
op: "set",
|
|
302
|
-
path: "count",
|
|
303
|
-
value: { kind: "get", path: "input.value" },
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
const snapshot = createTestSnapshot({ count: 0 }, schema.hash);
|
|
309
|
-
const intent = createTestIntent("setCount", { value: 1, extra: 2 });
|
|
310
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
311
|
-
expect(result.status).toBe("error");
|
|
312
|
-
expect(result.snapshot.system.lastError?.code).toBe("INVALID_INPUT");
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
describe("Computed Values", () => {
|
|
316
|
-
it("should recompute computed values after action", async () => {
|
|
317
|
-
const schema = createTestSchema({
|
|
318
|
-
computed: {
|
|
319
|
-
fields: {
|
|
320
|
-
"computed.total": {
|
|
321
|
-
expr: {
|
|
322
|
-
kind: "add",
|
|
323
|
-
left: { kind: "coalesce", args: [{ kind: "get", path: "a" }, { kind: "lit", value: 0 }] },
|
|
324
|
-
right: { kind: "coalesce", args: [{ kind: "get", path: "b" }, { kind: "lit", value: 0 }] },
|
|
325
|
-
},
|
|
326
|
-
deps: ["a", "b"],
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
actions: {
|
|
331
|
-
setA: {
|
|
332
|
-
flow: {
|
|
333
|
-
kind: "patch",
|
|
334
|
-
op: "set",
|
|
335
|
-
path: "a",
|
|
336
|
-
value: { kind: "get", path: "input.value" },
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
},
|
|
340
|
-
});
|
|
341
|
-
const snapshot = createTestSnapshot({ a: 10, b: 20 }, schema.hash);
|
|
342
|
-
const intent = createTestIntent("setA", { value: 100 });
|
|
343
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
344
|
-
expect(result.status).toBe("complete");
|
|
345
|
-
expect(result.snapshot.data).toEqual({ a: 100, b: 20 });
|
|
346
|
-
expect(result.snapshot.computed["computed.total"]).toBe(120);
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
describe("Effects (Pending Status)", () => {
|
|
350
|
-
it("should return pending status when effect is encountered", async () => {
|
|
351
|
-
const schema = createTestSchema({
|
|
352
|
-
actions: {
|
|
353
|
-
fetchData: {
|
|
354
|
-
flow: {
|
|
355
|
-
kind: "seq",
|
|
356
|
-
steps: [
|
|
357
|
-
{ kind: "patch", op: "set", path: "loading", value: { kind: "lit", value: true } },
|
|
358
|
-
{
|
|
359
|
-
kind: "effect",
|
|
360
|
-
type: "http",
|
|
361
|
-
params: {
|
|
362
|
-
url: { kind: "lit", value: "https://api.example.com/data" },
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
|
-
],
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
},
|
|
369
|
-
});
|
|
370
|
-
const snapshot = createTestSnapshot({}, schema.hash);
|
|
371
|
-
const intent = createTestIntent("fetchData");
|
|
372
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
373
|
-
expect(result.status).toBe("pending");
|
|
374
|
-
expect(result.snapshot.data).toEqual({ loading: true });
|
|
375
|
-
expect(result.snapshot.system.pendingRequirements).toHaveLength(1);
|
|
376
|
-
expect(result.snapshot.system.pendingRequirements[0].type).toBe("http");
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
describe("Halt", () => {
|
|
380
|
-
it("should return halted status when halt is encountered", async () => {
|
|
381
|
-
const schema = createTestSchema({
|
|
382
|
-
actions: {
|
|
383
|
-
conditionalHalt: {
|
|
384
|
-
flow: {
|
|
385
|
-
kind: "seq",
|
|
386
|
-
steps: [
|
|
387
|
-
{ kind: "patch", op: "set", path: "started", value: { kind: "lit", value: true } },
|
|
388
|
-
{
|
|
389
|
-
kind: "if",
|
|
390
|
-
cond: { kind: "get", path: "input.shouldHalt" },
|
|
391
|
-
then: { kind: "halt", reason: "User requested halt" },
|
|
392
|
-
},
|
|
393
|
-
{ kind: "patch", op: "set", path: "completed", value: { kind: "lit", value: true } },
|
|
394
|
-
],
|
|
395
|
-
},
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
// With halt
|
|
400
|
-
const snapshot1 = createTestSnapshot({}, schema.hash);
|
|
401
|
-
const intent1 = createTestIntent("conditionalHalt", { shouldHalt: true });
|
|
402
|
-
const result1 = await computeWithContext(schema, snapshot1, intent1);
|
|
403
|
-
expect(result1.status).toBe("halted");
|
|
404
|
-
expect(result1.snapshot.data).toEqual({ started: true });
|
|
405
|
-
// Without halt
|
|
406
|
-
const snapshot2 = createTestSnapshot({}, schema.hash);
|
|
407
|
-
const intent2 = createTestIntent("conditionalHalt", { shouldHalt: false });
|
|
408
|
-
const result2 = await computeWithContext(schema, snapshot2, intent2);
|
|
409
|
-
expect(result2.status).toBe("complete");
|
|
410
|
-
expect(result2.snapshot.data).toEqual({ started: true, completed: true });
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
describe("Error Handling", () => {
|
|
414
|
-
it("should handle fail flow", async () => {
|
|
415
|
-
const schema = createTestSchema({
|
|
416
|
-
actions: {
|
|
417
|
-
validateInput: {
|
|
418
|
-
flow: {
|
|
419
|
-
kind: "if",
|
|
420
|
-
cond: { kind: "isNull", arg: { kind: "get", path: "input.value" } },
|
|
421
|
-
then: { kind: "fail", code: "MISSING_VALUE", message: { kind: "lit", value: "Value is required" } },
|
|
422
|
-
else: { kind: "patch", op: "set", path: "value", value: { kind: "get", path: "input.value" } },
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
},
|
|
426
|
-
});
|
|
427
|
-
// With null input
|
|
428
|
-
const snapshot1 = createTestSnapshot({}, schema.hash);
|
|
429
|
-
const intent1 = createTestIntent("validateInput", { value: null });
|
|
430
|
-
const result1 = await computeWithContext(schema, snapshot1, intent1);
|
|
431
|
-
expect(result1.status).toBe("error");
|
|
432
|
-
expect(result1.snapshot.system.lastError?.message).toBe("Value is required");
|
|
433
|
-
// With valid input
|
|
434
|
-
const snapshot2 = createTestSnapshot({}, schema.hash);
|
|
435
|
-
const intent2 = createTestIntent("validateInput", { value: "test" });
|
|
436
|
-
const result2 = await computeWithContext(schema, snapshot2, intent2);
|
|
437
|
-
expect(result2.status).toBe("complete");
|
|
438
|
-
expect(result2.snapshot.data).toEqual({ value: "test" });
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
describe("Trace Generation", () => {
|
|
442
|
-
it("should generate trace graph", async () => {
|
|
443
|
-
const schema = createTestSchema({
|
|
444
|
-
actions: {
|
|
445
|
-
simpleAction: {
|
|
446
|
-
flow: {
|
|
447
|
-
kind: "seq",
|
|
448
|
-
steps: [
|
|
449
|
-
{ kind: "patch", op: "set", path: "a", value: { kind: "lit", value: 1 } },
|
|
450
|
-
{ kind: "patch", op: "set", path: "b", value: { kind: "lit", value: 2 } },
|
|
451
|
-
],
|
|
452
|
-
},
|
|
453
|
-
},
|
|
454
|
-
},
|
|
455
|
-
});
|
|
456
|
-
const snapshot = createTestSnapshot({}, schema.hash);
|
|
457
|
-
const intent = createTestIntent("simpleAction");
|
|
458
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
459
|
-
expect(result.trace).toBeDefined();
|
|
460
|
-
expect(result.trace.intent).toEqual({ type: "simpleAction", input: undefined });
|
|
461
|
-
expect(result.trace.baseVersion).toBe(0);
|
|
462
|
-
expect(result.trace.resultVersion).toBe(1);
|
|
463
|
-
expect(result.trace.duration).toBeGreaterThanOrEqual(0);
|
|
464
|
-
expect(result.trace.terminatedBy).toBe("complete");
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
|
-
describe("Complex Scenarios", () => {
|
|
468
|
-
it("should handle a todo app workflow", async () => {
|
|
469
|
-
const schema = createTestSchema({
|
|
470
|
-
computed: {
|
|
471
|
-
fields: {
|
|
472
|
-
"computed.activeCount": {
|
|
473
|
-
expr: {
|
|
474
|
-
kind: "len",
|
|
475
|
-
arg: {
|
|
476
|
-
kind: "filter",
|
|
477
|
-
array: { kind: "coalesce", args: [{ kind: "get", path: "todos" }, { kind: "lit", value: [] }] },
|
|
478
|
-
predicate: { kind: "not", arg: { kind: "get", path: "$item.completed" } },
|
|
479
|
-
},
|
|
480
|
-
},
|
|
481
|
-
deps: ["todos"],
|
|
482
|
-
},
|
|
483
|
-
},
|
|
484
|
-
},
|
|
485
|
-
actions: {
|
|
486
|
-
addTodo: {
|
|
487
|
-
flow: {
|
|
488
|
-
kind: "patch",
|
|
489
|
-
op: "set",
|
|
490
|
-
path: "todos",
|
|
491
|
-
value: {
|
|
492
|
-
kind: "coalesce",
|
|
493
|
-
args: [
|
|
494
|
-
{
|
|
495
|
-
kind: "if",
|
|
496
|
-
cond: { kind: "isNull", arg: { kind: "get", path: "todos" } },
|
|
497
|
-
then: { kind: "lit", value: [] },
|
|
498
|
-
else: { kind: "get", path: "todos" },
|
|
499
|
-
},
|
|
500
|
-
{ kind: "lit", value: [] },
|
|
501
|
-
],
|
|
502
|
-
},
|
|
503
|
-
},
|
|
504
|
-
},
|
|
505
|
-
},
|
|
506
|
-
});
|
|
507
|
-
// First add - initialize todos array
|
|
508
|
-
const snapshot = createTestSnapshot({}, schema.hash);
|
|
509
|
-
const intent = createTestIntent("addTodo", { text: "Test todo" });
|
|
510
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
511
|
-
expect(result.status).toBe("complete");
|
|
512
|
-
expect(result.snapshot.computed["computed.activeCount"]).toBe(0);
|
|
513
|
-
});
|
|
514
|
-
it("should handle sequential operations with state dependencies", async () => {
|
|
515
|
-
const schema = createTestSchema({
|
|
516
|
-
actions: {
|
|
517
|
-
transfer: {
|
|
518
|
-
flow: {
|
|
519
|
-
kind: "seq",
|
|
520
|
-
steps: [
|
|
521
|
-
{
|
|
522
|
-
kind: "patch",
|
|
523
|
-
op: "set",
|
|
524
|
-
path: "fromBalance",
|
|
525
|
-
value: {
|
|
526
|
-
kind: "sub",
|
|
527
|
-
left: { kind: "get", path: "fromBalance" },
|
|
528
|
-
right: { kind: "get", path: "input.amount" },
|
|
529
|
-
},
|
|
530
|
-
},
|
|
531
|
-
{
|
|
532
|
-
kind: "patch",
|
|
533
|
-
op: "set",
|
|
534
|
-
path: "toBalance",
|
|
535
|
-
value: {
|
|
536
|
-
kind: "add",
|
|
537
|
-
left: { kind: "get", path: "toBalance" },
|
|
538
|
-
right: { kind: "get", path: "input.amount" },
|
|
539
|
-
},
|
|
540
|
-
},
|
|
541
|
-
],
|
|
542
|
-
},
|
|
543
|
-
},
|
|
544
|
-
},
|
|
545
|
-
});
|
|
546
|
-
const snapshot = createTestSnapshot({
|
|
547
|
-
fromBalance: 100,
|
|
548
|
-
toBalance: 50,
|
|
549
|
-
}, schema.hash);
|
|
550
|
-
const intent = createTestIntent("transfer", { amount: 30 });
|
|
551
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
552
|
-
expect(result.status).toBe("complete");
|
|
553
|
-
expect(result.snapshot.data).toEqual({
|
|
554
|
-
fromBalance: 70,
|
|
555
|
-
toBalance: 80,
|
|
556
|
-
});
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
describe("Version Management", () => {
|
|
560
|
-
it("should increment version on each compute", async () => {
|
|
561
|
-
const schema = createTestSchema({
|
|
562
|
-
actions: {
|
|
563
|
-
noop: {
|
|
564
|
-
flow: { kind: "seq", steps: [] },
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
});
|
|
568
|
-
const snapshot = createTestSnapshot({}, schema.hash);
|
|
569
|
-
expect(snapshot.meta.version).toBe(0);
|
|
570
|
-
const result1 = await computeWithContext(schema, snapshot, createTestIntent("noop"));
|
|
571
|
-
expect(result1.snapshot.meta.version).toBe(1);
|
|
572
|
-
const result2 = await computeWithContext(schema, result1.snapshot, createTestIntent("noop"));
|
|
573
|
-
expect(result2.snapshot.meta.version).toBe(2);
|
|
574
|
-
});
|
|
575
|
-
});
|
|
576
|
-
describe("System State", () => {
|
|
577
|
-
it("should track errors in system.errors", async () => {
|
|
578
|
-
const schema = createTestSchema({
|
|
579
|
-
actions: {
|
|
580
|
-
fail: {
|
|
581
|
-
flow: { kind: "fail", code: "TEST_ERROR" },
|
|
582
|
-
},
|
|
583
|
-
},
|
|
584
|
-
});
|
|
585
|
-
const snapshot = createTestSnapshot({}, schema.hash);
|
|
586
|
-
const result1 = await computeWithContext(schema, snapshot, createTestIntent("fail"));
|
|
587
|
-
expect(result1.snapshot.system.errors).toHaveLength(1);
|
|
588
|
-
expect(result1.snapshot.system.lastError?.code).toBe("VALIDATION_ERROR");
|
|
589
|
-
// Run again to accumulate errors
|
|
590
|
-
const result2 = await computeWithContext(schema, result1.snapshot, createTestIntent("fail"));
|
|
591
|
-
expect(result2.snapshot.system.errors).toHaveLength(2);
|
|
592
|
-
});
|
|
593
|
-
it("should reset currentAction after completion", async () => {
|
|
594
|
-
const schema = createTestSchema({
|
|
595
|
-
actions: {
|
|
596
|
-
test: {
|
|
597
|
-
flow: { kind: "patch", op: "set", path: "done", value: { kind: "lit", value: true } },
|
|
598
|
-
},
|
|
599
|
-
},
|
|
600
|
-
});
|
|
601
|
-
const snapshot = createTestSnapshot({}, schema.hash);
|
|
602
|
-
const result = await computeWithContext(schema, snapshot, createTestIntent("test"));
|
|
603
|
-
expect(result.snapshot.system.currentAction).toBeNull();
|
|
604
|
-
expect(result.snapshot.system.status).toBe("idle");
|
|
605
|
-
});
|
|
606
|
-
});
|
|
607
|
-
describe("Determinism", () => {
|
|
608
|
-
it("should produce identical results for same inputs", async () => {
|
|
609
|
-
const schema = createTestSchema({
|
|
610
|
-
actions: {
|
|
611
|
-
increment: {
|
|
612
|
-
flow: {
|
|
613
|
-
kind: "patch",
|
|
614
|
-
op: "set",
|
|
615
|
-
path: "count",
|
|
616
|
-
value: {
|
|
617
|
-
kind: "add",
|
|
618
|
-
left: { kind: "get", path: "count" },
|
|
619
|
-
right: { kind: "lit", value: 1 },
|
|
620
|
-
},
|
|
621
|
-
},
|
|
622
|
-
},
|
|
623
|
-
},
|
|
624
|
-
});
|
|
625
|
-
const snapshot = createTestSnapshot({ count: 1 }, schema.hash);
|
|
626
|
-
const intent = createIntent("increment", "intent-fixed");
|
|
627
|
-
const result1 = await compute(schema, snapshot, intent, HOST_CONTEXT);
|
|
628
|
-
const result2 = await compute(schema, snapshot, intent, HOST_CONTEXT);
|
|
629
|
-
expect(result1).toEqual(result2);
|
|
630
|
-
});
|
|
631
|
-
});
|
|
632
|
-
describe("Availability on Re-Entry (#134)", () => {
|
|
633
|
-
it("should keep action valid when re-entering after effect mutates available fields", async () => {
|
|
634
|
-
const schema = createTestSchema({
|
|
635
|
-
state: {
|
|
636
|
-
fields: {
|
|
637
|
-
pending: { type: "string", required: false },
|
|
638
|
-
result: { type: "string", required: false },
|
|
639
|
-
},
|
|
640
|
-
},
|
|
641
|
-
actions: {
|
|
642
|
-
run: {
|
|
643
|
-
available: {
|
|
644
|
-
kind: "and",
|
|
645
|
-
args: [
|
|
646
|
-
{ kind: "isNull", arg: { kind: "get", path: "pending" } },
|
|
647
|
-
{ kind: "isNull", arg: { kind: "get", path: "result" } },
|
|
648
|
-
],
|
|
649
|
-
},
|
|
650
|
-
flow: {
|
|
651
|
-
kind: "if",
|
|
652
|
-
cond: { kind: "isNull", arg: { kind: "get", path: "pending" } },
|
|
653
|
-
then: {
|
|
654
|
-
kind: "seq",
|
|
655
|
-
steps: [
|
|
656
|
-
{
|
|
657
|
-
kind: "patch",
|
|
658
|
-
op: "set",
|
|
659
|
-
path: "pending",
|
|
660
|
-
value: { kind: "get", path: "meta.intentId" },
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
kind: "effect",
|
|
664
|
-
type: "demo.exec",
|
|
665
|
-
params: {
|
|
666
|
-
into: { kind: "lit", value: "result" },
|
|
667
|
-
},
|
|
668
|
-
},
|
|
669
|
-
],
|
|
670
|
-
},
|
|
671
|
-
},
|
|
672
|
-
},
|
|
673
|
-
},
|
|
674
|
-
});
|
|
675
|
-
const pending = createTestSnapshot({ pending: null, result: null }, schema.hash);
|
|
676
|
-
const intent = createIntent("run", "intent-run-1");
|
|
677
|
-
const first = await compute(schema, pending, intent, HOST_CONTEXT);
|
|
678
|
-
expect(first.status).toBe("pending");
|
|
679
|
-
expect(first.snapshot.system.currentAction).toBe("run");
|
|
680
|
-
const afterEffect = {
|
|
681
|
-
...first.snapshot,
|
|
682
|
-
data: { ...first.snapshot.data, result: "done" },
|
|
683
|
-
system: {
|
|
684
|
-
...first.snapshot.system,
|
|
685
|
-
pendingRequirements: [],
|
|
686
|
-
},
|
|
687
|
-
};
|
|
688
|
-
const second = await compute(schema, afterEffect, intent, HOST_CONTEXT);
|
|
689
|
-
expect(second.status).toBe("complete");
|
|
690
|
-
expect(second.snapshot.system.lastError?.code).toBeUndefined();
|
|
691
|
-
});
|
|
692
|
-
it("should skip availability check on re-entry when currentAction matches", async () => {
|
|
693
|
-
// Simulates the issue #134 scenario:
|
|
694
|
-
// Action `run` has `available when and(isNull(result), isNull(pending))`
|
|
695
|
-
// First compute: available passes, patches pending, declares effect → status "pending"
|
|
696
|
-
// After effect fulfillment, Host calls compute again with updated snapshot
|
|
697
|
-
// where pending and result are non-null. Without the fix, this would fail
|
|
698
|
-
// with ACTION_UNAVAILABLE because `available` re-evaluates to false.
|
|
699
|
-
const schema = createTestSchema({
|
|
700
|
-
state: {
|
|
701
|
-
fields: {
|
|
702
|
-
pending: { type: "string", required: false },
|
|
703
|
-
result: { type: "string", required: false },
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
actions: {
|
|
707
|
-
run: {
|
|
708
|
-
available: {
|
|
709
|
-
kind: "and",
|
|
710
|
-
args: [
|
|
711
|
-
{ kind: "isNull", arg: { kind: "get", path: "pending" } },
|
|
712
|
-
{ kind: "isNull", arg: { kind: "get", path: "result" } },
|
|
713
|
-
],
|
|
714
|
-
},
|
|
715
|
-
flow: {
|
|
716
|
-
kind: "if",
|
|
717
|
-
cond: { kind: "isNull", arg: { kind: "get", path: "pending" } },
|
|
718
|
-
then: {
|
|
719
|
-
kind: "seq",
|
|
720
|
-
steps: [
|
|
721
|
-
{
|
|
722
|
-
kind: "patch",
|
|
723
|
-
op: "set",
|
|
724
|
-
path: "pending",
|
|
725
|
-
value: { kind: "get", path: "meta.intentId" },
|
|
726
|
-
},
|
|
727
|
-
{
|
|
728
|
-
kind: "effect",
|
|
729
|
-
type: "demo.exec",
|
|
730
|
-
params: {
|
|
731
|
-
into: { kind: "lit", value: "result" },
|
|
732
|
-
},
|
|
733
|
-
},
|
|
734
|
-
],
|
|
735
|
-
},
|
|
736
|
-
},
|
|
737
|
-
},
|
|
738
|
-
},
|
|
739
|
-
});
|
|
740
|
-
// 1st compute: initial invocation — available passes, effect declared
|
|
741
|
-
const snapshot1 = createTestSnapshot({ pending: null, result: null }, schema.hash);
|
|
742
|
-
const intent = createIntent("run", "intent-run-1");
|
|
743
|
-
const result1 = await compute(schema, snapshot1, intent, HOST_CONTEXT);
|
|
744
|
-
expect(result1.status).toBe("pending");
|
|
745
|
-
expect(result1.snapshot.data).toEqual(expect.objectContaining({ pending: "intent-run-1" }));
|
|
746
|
-
expect(result1.snapshot.system.currentAction).toBe("run");
|
|
747
|
-
// 2nd compute: simulate re-entry after effect fulfillment
|
|
748
|
-
// Host applied effect patches (result is now set) and calls compute again.
|
|
749
|
-
// The snapshot has currentAction === "run" (set during 1st compute pending).
|
|
750
|
-
const reEntrySnapshot = {
|
|
751
|
-
...result1.snapshot,
|
|
752
|
-
data: { ...result1.snapshot.data, result: "done" },
|
|
753
|
-
system: {
|
|
754
|
-
...result1.snapshot.system,
|
|
755
|
-
// currentAction remains "run" — this signals re-entry
|
|
756
|
-
pendingRequirements: [],
|
|
757
|
-
},
|
|
758
|
-
};
|
|
759
|
-
const result2 = await compute(schema, reEntrySnapshot, intent, HOST_CONTEXT);
|
|
760
|
-
// Should NOT fail with ACTION_UNAVAILABLE — availability is skipped on re-entry
|
|
761
|
-
expect(result2.status).not.toBe("error");
|
|
762
|
-
expect(result2.snapshot.system.lastError?.code).not.toBe("ACTION_UNAVAILABLE");
|
|
763
|
-
});
|
|
764
|
-
it("SCENARIO: different intentId same action type on pending snapshot bypasses availability", async () => {
|
|
765
|
-
// Models the reviewer's concern:
|
|
766
|
-
// Intent A (type "run") → pending → currentAction = "run"
|
|
767
|
-
// Then Intent B (type "run", DIFFERENT intentId) arrives on that snapshot.
|
|
768
|
-
// With current fix (type-only guard), B skips availability.
|
|
769
|
-
// This test documents whether this is reachable and what happens.
|
|
770
|
-
const schema = createTestSchema({
|
|
771
|
-
state: {
|
|
772
|
-
fields: {
|
|
773
|
-
pending: { type: "string", required: false },
|
|
774
|
-
result: { type: "string", required: false },
|
|
775
|
-
},
|
|
776
|
-
},
|
|
777
|
-
actions: {
|
|
778
|
-
run: {
|
|
779
|
-
available: {
|
|
780
|
-
kind: "and",
|
|
781
|
-
args: [
|
|
782
|
-
{ kind: "isNull", arg: { kind: "get", path: "pending" } },
|
|
783
|
-
{ kind: "isNull", arg: { kind: "get", path: "result" } },
|
|
784
|
-
],
|
|
785
|
-
},
|
|
786
|
-
flow: {
|
|
787
|
-
kind: "if",
|
|
788
|
-
cond: { kind: "isNull", arg: { kind: "get", path: "pending" } },
|
|
789
|
-
then: {
|
|
790
|
-
kind: "seq",
|
|
791
|
-
steps: [
|
|
792
|
-
{
|
|
793
|
-
kind: "patch",
|
|
794
|
-
op: "set",
|
|
795
|
-
path: "pending",
|
|
796
|
-
value: { kind: "get", path: "meta.intentId" },
|
|
797
|
-
},
|
|
798
|
-
{
|
|
799
|
-
kind: "effect",
|
|
800
|
-
type: "demo.exec",
|
|
801
|
-
params: {
|
|
802
|
-
into: { kind: "lit", value: "result" },
|
|
803
|
-
},
|
|
804
|
-
},
|
|
805
|
-
],
|
|
806
|
-
},
|
|
807
|
-
},
|
|
808
|
-
},
|
|
809
|
-
},
|
|
810
|
-
});
|
|
811
|
-
// Intent A: pending → currentAction = "run"
|
|
812
|
-
const snapshot1 = createTestSnapshot({ pending: null, result: null }, schema.hash);
|
|
813
|
-
const intentA = createIntent("run", "intent-A");
|
|
814
|
-
const resultA = await compute(schema, snapshot1, intentA, HOST_CONTEXT);
|
|
815
|
-
expect(resultA.status).toBe("pending");
|
|
816
|
-
expect(resultA.snapshot.system.currentAction).toBe("run");
|
|
817
|
-
// Intent B: different intentId, same action type, on A's pending snapshot
|
|
818
|
-
const intentB = createIntent("run", "intent-B");
|
|
819
|
-
const resultB = await compute(schema, resultA.snapshot, intentB, HOST_CONTEXT);
|
|
820
|
-
// With type-only guard, availability IS skipped (isReEntry = true).
|
|
821
|
-
// But the flow's own state guard (if isNull(pending)) prevents double-patching.
|
|
822
|
-
// pending is already "intent-A", so the if-branch is skipped → no patches, no effects.
|
|
823
|
-
// Result: completes as no-op, does NOT corrupt state.
|
|
824
|
-
expect(resultB.snapshot.system.lastError?.code).not.toBe("ACTION_UNAVAILABLE");
|
|
825
|
-
expect(resultB.snapshot.data.pending).toBe("intent-A"); // unchanged
|
|
826
|
-
});
|
|
827
|
-
it("SCENARIO: different action type on pending snapshot still checks availability", async () => {
|
|
828
|
-
// Ensure that a DIFFERENT action type is NOT treated as re-entry
|
|
829
|
-
// even when currentAction is set from a previous pending action.
|
|
830
|
-
const schema = createTestSchema({
|
|
831
|
-
state: {
|
|
832
|
-
fields: {
|
|
833
|
-
pending: { type: "string", required: false },
|
|
834
|
-
result: { type: "string", required: false },
|
|
835
|
-
count: { type: "number", required: false },
|
|
836
|
-
},
|
|
837
|
-
},
|
|
838
|
-
actions: {
|
|
839
|
-
run: {
|
|
840
|
-
available: {
|
|
841
|
-
kind: "isNull",
|
|
842
|
-
arg: { kind: "get", path: "pending" },
|
|
843
|
-
},
|
|
844
|
-
flow: {
|
|
845
|
-
kind: "seq",
|
|
846
|
-
steps: [
|
|
847
|
-
{
|
|
848
|
-
kind: "patch",
|
|
849
|
-
op: "set",
|
|
850
|
-
path: "pending",
|
|
851
|
-
value: { kind: "get", path: "meta.intentId" },
|
|
852
|
-
},
|
|
853
|
-
{
|
|
854
|
-
kind: "effect",
|
|
855
|
-
type: "demo.exec",
|
|
856
|
-
params: {},
|
|
857
|
-
},
|
|
858
|
-
],
|
|
859
|
-
},
|
|
860
|
-
},
|
|
861
|
-
increment: {
|
|
862
|
-
available: {
|
|
863
|
-
kind: "isNull",
|
|
864
|
-
arg: { kind: "get", path: "pending" },
|
|
865
|
-
},
|
|
866
|
-
flow: {
|
|
867
|
-
kind: "patch",
|
|
868
|
-
op: "set",
|
|
869
|
-
path: "count",
|
|
870
|
-
value: { kind: "lit", value: 1 },
|
|
871
|
-
},
|
|
872
|
-
},
|
|
873
|
-
},
|
|
874
|
-
});
|
|
875
|
-
// "run" → pending → currentAction = "run"
|
|
876
|
-
const snapshot1 = createTestSnapshot({ pending: null, result: null, count: 0 }, schema.hash);
|
|
877
|
-
const intentRun = createIntent("run", "intent-run");
|
|
878
|
-
const resultRun = await compute(schema, snapshot1, intentRun, HOST_CONTEXT);
|
|
879
|
-
expect(resultRun.status).toBe("pending");
|
|
880
|
-
expect(resultRun.snapshot.system.currentAction).toBe("run");
|
|
881
|
-
// "increment" on the pending snapshot — different type, should NOT skip availability
|
|
882
|
-
const intentInc = createIntent("increment", "intent-inc");
|
|
883
|
-
const resultInc = await compute(schema, resultRun.snapshot, intentInc, HOST_CONTEXT);
|
|
884
|
-
// currentAction is "run" but intent type is "increment" → isReEntry = false
|
|
885
|
-
// available when isNull(pending) → pending is "intent-run" → false → ACTION_UNAVAILABLE
|
|
886
|
-
expect(resultInc.status).toBe("error");
|
|
887
|
-
expect(resultInc.snapshot.system.lastError?.code).toBe("ACTION_UNAVAILABLE");
|
|
888
|
-
});
|
|
889
|
-
it("should still check availability on fresh invocation", async () => {
|
|
890
|
-
// Ensure the fix doesn't accidentally skip availability on initial calls
|
|
891
|
-
const schema = createTestSchema({
|
|
892
|
-
state: {
|
|
893
|
-
fields: {
|
|
894
|
-
pending: { type: "string", required: false },
|
|
895
|
-
result: { type: "string", required: false },
|
|
896
|
-
},
|
|
897
|
-
},
|
|
898
|
-
actions: {
|
|
899
|
-
run: {
|
|
900
|
-
available: {
|
|
901
|
-
kind: "and",
|
|
902
|
-
args: [
|
|
903
|
-
{ kind: "isNull", arg: { kind: "get", path: "pending" } },
|
|
904
|
-
{ kind: "isNull", arg: { kind: "get", path: "result" } },
|
|
905
|
-
],
|
|
906
|
-
},
|
|
907
|
-
flow: {
|
|
908
|
-
kind: "patch",
|
|
909
|
-
op: "set",
|
|
910
|
-
path: "pending",
|
|
911
|
-
value: { kind: "get", path: "meta.intentId" },
|
|
912
|
-
},
|
|
913
|
-
},
|
|
914
|
-
},
|
|
915
|
-
});
|
|
916
|
-
// Fresh invocation where available condition is false — should still fail
|
|
917
|
-
const snapshot = createTestSnapshot({ pending: "already-set", result: null }, schema.hash);
|
|
918
|
-
const intent = createTestIntent("run");
|
|
919
|
-
const result = await computeWithContext(schema, snapshot, intent);
|
|
920
|
-
expect(result.status).toBe("error");
|
|
921
|
-
expect(result.snapshot.system.lastError?.code).toBe("ACTION_UNAVAILABLE");
|
|
922
|
-
});
|
|
923
|
-
});
|
|
924
|
-
describe("computeSync", () => {
|
|
925
|
-
it("should match async compute for the same inputs", async () => {
|
|
926
|
-
const schema = createTestSchema({
|
|
927
|
-
actions: {
|
|
928
|
-
increment: {
|
|
929
|
-
flow: {
|
|
930
|
-
kind: "patch",
|
|
931
|
-
op: "set",
|
|
932
|
-
path: "count",
|
|
933
|
-
value: {
|
|
934
|
-
kind: "add",
|
|
935
|
-
left: { kind: "get", path: "count" },
|
|
936
|
-
right: { kind: "lit", value: 1 },
|
|
937
|
-
},
|
|
938
|
-
},
|
|
939
|
-
},
|
|
940
|
-
},
|
|
941
|
-
});
|
|
942
|
-
const snapshot = createTestSnapshot({ count: 1 }, schema.hash);
|
|
943
|
-
const intent = createIntent("increment", "intent-sync-1");
|
|
944
|
-
const asyncResult = await compute(schema, snapshot, intent, HOST_CONTEXT);
|
|
945
|
-
const syncResult = computeSync(schema, snapshot, intent, HOST_CONTEXT);
|
|
946
|
-
expect(syncResult).toEqual(asyncResult);
|
|
947
|
-
});
|
|
948
|
-
});
|
|
949
|
-
});
|
|
950
|
-
//# sourceMappingURL=compute.test.js.map
|