@manifesto-ai/core 0.3.0 → 1.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/README.md +148 -498
- package/dist/__tests__/apply.test.d.ts +2 -0
- package/dist/__tests__/apply.test.d.ts.map +1 -0
- package/dist/__tests__/apply.test.js +144 -0
- package/dist/__tests__/apply.test.js.map +1 -0
- package/dist/__tests__/jcs.test.d.ts +2 -0
- package/dist/__tests__/jcs.test.d.ts.map +1 -0
- package/dist/__tests__/jcs.test.js +45 -0
- package/dist/__tests__/jcs.test.js.map +1 -0
- package/dist/core/apply.d.ts +17 -0
- package/dist/core/apply.d.ts.map +1 -0
- package/dist/core/apply.js +130 -0
- package/dist/core/apply.js.map +1 -0
- package/dist/core/compute.d.ts +17 -0
- package/dist/core/compute.d.ts.map +1 -0
- package/dist/core/compute.js +287 -0
- package/dist/core/compute.js.map +1 -0
- package/dist/core/compute.test.d.ts +2 -0
- package/dist/core/compute.test.d.ts.map +1 -0
- package/dist/core/compute.test.js +633 -0
- package/dist/core/compute.test.js.map +1 -0
- package/dist/core/explain.d.ts +14 -0
- package/dist/core/explain.d.ts.map +1 -0
- package/dist/core/explain.js +78 -0
- package/dist/core/explain.js.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/validate.d.ts +16 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +361 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/core/validate.test.d.ts +2 -0
- package/dist/core/validate.test.d.ts.map +1 -0
- package/dist/core/validate.test.js +638 -0
- package/dist/core/validate.test.js.map +1 -0
- package/dist/core/validation-utils.d.ts +20 -0
- package/dist/core/validation-utils.d.ts.map +1 -0
- package/dist/core/validation-utils.js +289 -0
- package/dist/core/validation-utils.js.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluator/computed.d.ts +14 -0
- package/dist/evaluator/computed.d.ts.map +1 -0
- package/dist/evaluator/computed.js +60 -0
- package/dist/evaluator/computed.js.map +1 -0
- package/dist/evaluator/context.d.ts +59 -0
- package/dist/evaluator/context.d.ts.map +1 -0
- package/dist/evaluator/context.js +41 -0
- package/dist/evaluator/context.js.map +1 -0
- package/dist/evaluator/dag.d.ts +30 -0
- package/dist/evaluator/dag.d.ts.map +1 -0
- package/dist/evaluator/dag.js +121 -0
- package/dist/evaluator/dag.js.map +1 -0
- package/dist/evaluator/expr.d.ts +11 -0
- package/dist/evaluator/expr.d.ts.map +1 -0
- package/dist/evaluator/expr.js +649 -0
- package/dist/evaluator/expr.js.map +1 -0
- package/dist/evaluator/expr.test.d.ts +2 -0
- package/dist/evaluator/expr.test.d.ts.map +1 -0
- package/dist/evaluator/expr.test.js +449 -0
- package/dist/evaluator/expr.test.js.map +1 -0
- package/dist/evaluator/flow.d.ts +35 -0
- package/dist/evaluator/flow.d.ts.map +1 -0
- package/dist/evaluator/flow.js +387 -0
- package/dist/evaluator/flow.js.map +1 -0
- package/dist/evaluator/flow.test.d.ts +2 -0
- package/dist/evaluator/flow.test.d.ts.map +1 -0
- package/dist/evaluator/flow.test.js +499 -0
- package/dist/evaluator/flow.test.js.map +1 -0
- package/dist/evaluator/index.d.ts +6 -0
- package/dist/evaluator/index.d.ts.map +1 -0
- package/dist/evaluator/index.js +6 -0
- package/dist/evaluator/index.js.map +1 -0
- package/dist/factories.d.ts +22 -0
- package/dist/factories.d.ts.map +1 -0
- package/dist/factories.js +44 -0
- package/dist/factories.js.map +1 -0
- package/dist/index.d.ts +47 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -45
- package/dist/index.js.map +1 -1
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +13 -0
- package/dist/index.test.js.map +1 -0
- package/dist/schema/action.d.ts +14 -0
- package/dist/schema/action.d.ts.map +1 -0
- package/dist/schema/action.js +30 -0
- package/dist/schema/action.js.map +1 -0
- package/dist/schema/common.d.ts +37 -0
- package/dist/schema/common.d.ts.map +1 -0
- package/dist/schema/common.js +20 -0
- package/dist/schema/common.js.map +1 -0
- package/dist/schema/computed.d.ts +23 -0
- package/dist/schema/computed.d.ts.map +1 -0
- package/dist/schema/computed.js +34 -0
- package/dist/schema/computed.js.map +1 -0
- package/dist/schema/domain.d.ts +50 -0
- package/dist/schema/domain.d.ts.map +1 -0
- package/dist/schema/domain.js +60 -0
- package/dist/schema/domain.js.map +1 -0
- package/dist/schema/expr.d.ts +303 -0
- package/dist/schema/expr.d.ts.map +1 -0
- package/dist/schema/expr.js +283 -0
- package/dist/schema/expr.js.map +1 -0
- package/dist/schema/field.d.ts +48 -0
- package/dist/schema/field.d.ts.map +1 -0
- package/dist/schema/field.js +31 -0
- package/dist/schema/field.js.map +1 -0
- package/dist/schema/flow.d.ts +103 -0
- package/dist/schema/flow.d.ts.map +1 -0
- package/dist/schema/flow.js +82 -0
- package/dist/schema/flow.js.map +1 -0
- package/dist/schema/host-context.d.ts +12 -0
- package/dist/schema/host-context.d.ts.map +1 -0
- package/dist/schema/host-context.js +23 -0
- package/dist/schema/host-context.js.map +1 -0
- package/dist/schema/index.d.ts +13 -2
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +25 -2
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/patch.d.ts +59 -0
- package/dist/schema/patch.d.ts.map +1 -0
- package/dist/schema/patch.js +60 -0
- package/dist/schema/patch.js.map +1 -0
- package/dist/schema/result.d.ts +142 -0
- package/dist/schema/result.d.ts.map +1 -0
- package/dist/schema/result.js +94 -0
- package/dist/schema/result.js.map +1 -0
- package/dist/schema/snapshot.d.ts +153 -0
- package/dist/schema/snapshot.d.ts.map +1 -0
- package/dist/schema/snapshot.js +160 -0
- package/dist/schema/snapshot.js.map +1 -0
- package/dist/schema/trace.d.ts +98 -0
- package/dist/schema/trace.d.ts.map +1 -0
- package/dist/schema/trace.js +90 -0
- package/dist/schema/trace.js.map +1 -0
- package/dist/schema/type-spec.d.ts +34 -0
- package/dist/schema/type-spec.d.ts.map +1 -0
- package/dist/schema/type-spec.js +40 -0
- package/dist/schema/type-spec.js.map +1 -0
- package/dist/utils/canonical.d.ts +37 -0
- package/dist/utils/canonical.d.ts.map +1 -0
- package/dist/utils/canonical.js +122 -0
- package/dist/utils/canonical.js.map +1 -0
- package/dist/utils/canonical.test.d.ts +2 -0
- package/dist/utils/canonical.test.d.ts.map +1 -0
- package/dist/utils/canonical.test.js +183 -0
- package/dist/utils/canonical.test.js.map +1 -0
- package/dist/utils/hash.d.ts +28 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +152 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/hash.test.d.ts +2 -0
- package/dist/utils/hash.test.d.ts.map +1 -0
- package/dist/utils/hash.test.js +170 -0
- package/dist/utils/hash.test.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/path.d.ts +40 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/dist/utils/path.js +132 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/path.test.d.ts +2 -0
- package/dist/utils/path.test.d.ts.map +1 -0
- package/dist/utils/path.test.js +191 -0
- package/dist/utils/path.test.js.map +1 -0
- package/package.json +28 -41
- package/LICENSE +0 -21
- package/dist/dag/graph.d.ts +0 -62
- package/dist/dag/graph.d.ts.map +0 -1
- package/dist/dag/graph.js +0 -244
- package/dist/dag/graph.js.map +0 -1
- package/dist/dag/index.d.ts +0 -4
- package/dist/dag/index.d.ts.map +0 -1
- package/dist/dag/index.js +0 -4
- package/dist/dag/index.js.map +0 -1
- package/dist/dag/propagation.d.ts +0 -58
- package/dist/dag/propagation.d.ts.map +0 -1
- package/dist/dag/propagation.js +0 -224
- package/dist/dag/propagation.js.map +0 -1
- package/dist/dag/topological.d.ts +0 -33
- package/dist/dag/topological.d.ts.map +0 -1
- package/dist/dag/topological.js +0 -173
- package/dist/dag/topological.js.map +0 -1
- package/dist/domain/define.d.ts +0 -82
- package/dist/domain/define.d.ts.map +0 -1
- package/dist/domain/define.js +0 -105
- package/dist/domain/define.js.map +0 -1
- package/dist/domain/index.d.ts +0 -4
- package/dist/domain/index.d.ts.map +0 -1
- package/dist/domain/index.js +0 -4
- package/dist/domain/index.js.map +0 -1
- package/dist/domain/types.d.ts +0 -203
- package/dist/domain/types.d.ts.map +0 -1
- package/dist/domain/types.js +0 -2
- package/dist/domain/types.js.map +0 -1
- package/dist/domain/validate.d.ts +0 -17
- package/dist/domain/validate.d.ts.map +0 -1
- package/dist/domain/validate.js +0 -204
- package/dist/domain/validate.js.map +0 -1
- package/dist/effect/index.d.ts +0 -4
- package/dist/effect/index.d.ts.map +0 -1
- package/dist/effect/index.js +0 -4
- package/dist/effect/index.js.map +0 -1
- package/dist/effect/result.d.ts +0 -100
- package/dist/effect/result.d.ts.map +0 -1
- package/dist/effect/result.js +0 -163
- package/dist/effect/result.js.map +0 -1
- package/dist/effect/runner.d.ts +0 -98
- package/dist/effect/runner.d.ts.map +0 -1
- package/dist/effect/runner.js +0 -321
- package/dist/effect/runner.js.map +0 -1
- package/dist/effect/types.d.ts +0 -169
- package/dist/effect/types.d.ts.map +0 -1
- package/dist/effect/types.js +0 -28
- package/dist/effect/types.js.map +0 -1
- package/dist/expression/analyzer.d.ts +0 -42
- package/dist/expression/analyzer.d.ts.map +0 -1
- package/dist/expression/analyzer.js +0 -166
- package/dist/expression/analyzer.js.map +0 -1
- package/dist/expression/evaluator.d.ts +0 -16
- package/dist/expression/evaluator.d.ts.map +0 -1
- package/dist/expression/evaluator.js +0 -382
- package/dist/expression/evaluator.js.map +0 -1
- package/dist/expression/index.d.ts +0 -5
- package/dist/expression/index.d.ts.map +0 -1
- package/dist/expression/index.js +0 -5
- package/dist/expression/index.js.map +0 -1
- package/dist/expression/parser.d.ts +0 -37
- package/dist/expression/parser.d.ts.map +0 -1
- package/dist/expression/parser.js +0 -201
- package/dist/expression/parser.js.map +0 -1
- package/dist/expression/types.d.ts +0 -123
- package/dist/expression/types.d.ts.map +0 -1
- package/dist/expression/types.js +0 -10
- package/dist/expression/types.js.map +0 -1
- package/dist/policy/field-policy.d.ts +0 -63
- package/dist/policy/field-policy.d.ts.map +0 -1
- package/dist/policy/field-policy.js +0 -138
- package/dist/policy/field-policy.js.map +0 -1
- package/dist/policy/index.d.ts +0 -3
- package/dist/policy/index.d.ts.map +0 -1
- package/dist/policy/index.js +0 -3
- package/dist/policy/index.js.map +0 -1
- package/dist/policy/precondition.d.ts +0 -58
- package/dist/policy/precondition.d.ts.map +0 -1
- package/dist/policy/precondition.js +0 -115
- package/dist/policy/precondition.js.map +0 -1
- package/dist/runtime/index.d.ts +0 -4
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js +0 -4
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/runtime.d.ts +0 -94
- package/dist/runtime/runtime.d.ts.map +0 -1
- package/dist/runtime/runtime.js +0 -294
- package/dist/runtime/runtime.js.map +0 -1
- package/dist/runtime/snapshot.d.ts +0 -39
- package/dist/runtime/snapshot.d.ts.map +0 -1
- package/dist/runtime/snapshot.js +0 -264
- package/dist/runtime/snapshot.js.map +0 -1
- package/dist/runtime/subscription.d.ts +0 -82
- package/dist/runtime/subscription.d.ts.map +0 -1
- package/dist/runtime/subscription.js +0 -222
- package/dist/runtime/subscription.js.map +0 -1
- package/dist/schema/integration.d.ts +0 -89
- package/dist/schema/integration.d.ts.map +0 -1
- package/dist/schema/integration.js +0 -171
- package/dist/schema/integration.js.map +0 -1
- package/dist/schema/validation.d.ts +0 -51
- package/dist/schema/validation.d.ts.map +0 -1
- package/dist/schema/validation.js +0 -212
- package/dist/schema/validation.js.map +0 -1
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { compute } 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);
|
|
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);
|
|
628
|
+
const result2 = await compute(schema, snapshot, intent);
|
|
629
|
+
expect(result1).toEqual(result2);
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
//# sourceMappingURL=compute.test.js.map
|