@axiom-lattice/protocols 3.0.4 → 4.0.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.
@@ -0,0 +1,421 @@
1
+ import {
2
+ parseTaskBeliefState,
3
+ replaceTaskBeliefState,
4
+ taskBeliefStatesEqual,
5
+ type TaskBeliefState,
6
+ } from "../TaskBeliefProtocol";
7
+
8
+ const HEADER = "| Belief Key | Probability | Target | Basis |";
9
+ const SEPARATOR = "|---|---:|---:|---|";
10
+
11
+ function beliefSection(rows: string[]): string {
12
+ return ["## Belief State", "", HEADER, SEPARATOR, ...rows].join("\n");
13
+ }
14
+
15
+ describe("parseTaskBeliefState", () => {
16
+ it("parses the canonical four-column Belief State", () => {
17
+ const markdown = [
18
+ "## Objective",
19
+ "Ship",
20
+ "",
21
+ beliefSection(["| `input-valid` | 60% | 90% | File exists |"]),
22
+ ].join("\n");
23
+
24
+ expect(parseTaskBeliefState(markdown)).toEqual({
25
+ success: true,
26
+ state: {
27
+ entries: [
28
+ { key: "input-valid", probability: 60, target: 90, basis: "File exists" },
29
+ ],
30
+ },
31
+ });
32
+ });
33
+
34
+ it("accepts escaped pipes in Basis", () => {
35
+ const result = parseTaskBeliefState(
36
+ beliefSection(["| `input-valid` | 60% | 90% | JSON parser A \\| B passed |"]),
37
+ );
38
+
39
+ expect(result).toEqual({
40
+ success: true,
41
+ state: {
42
+ entries: [
43
+ {
44
+ key: "input-valid",
45
+ probability: 60,
46
+ target: 90,
47
+ basis: "JSON parser A | B passed",
48
+ },
49
+ ],
50
+ },
51
+ });
52
+ });
53
+
54
+ it("stops the section at an ATX heading with up to three leading spaces", () => {
55
+ const markdown = [
56
+ beliefSection(["| `input-valid` | 60% | 90% | File exists |"]),
57
+ "",
58
+ " ## Notes",
59
+ "This is not a belief row.",
60
+ ].join("\n");
61
+
62
+ expect(parseTaskBeliefState(markdown)).toEqual({
63
+ success: true,
64
+ state: {
65
+ entries: [
66
+ { key: "input-valid", probability: 60, target: 90, basis: "File exists" },
67
+ ],
68
+ },
69
+ });
70
+ });
71
+
72
+ it("rejects a row whose apparent closing pipe is escaped", () => {
73
+ const result = parseTaskBeliefState(
74
+ beliefSection(["| `input-valid` | 60% | 90% | basis \\|"]),
75
+ );
76
+
77
+ expect(result).toMatchObject({
78
+ success: false,
79
+ code: "MALFORMED_BELIEF_ROW",
80
+ });
81
+ });
82
+
83
+ it("parses a Basis ending in a literal pipe followed by a closing delimiter", () => {
84
+ const result = parseTaskBeliefState(
85
+ beliefSection(["| `input-valid` | 60% | 90% | basis \\| |"]),
86
+ );
87
+
88
+ expect(result).toEqual({
89
+ success: true,
90
+ state: {
91
+ entries: [
92
+ { key: "input-valid", probability: 60, target: 90, basis: "basis |" },
93
+ ],
94
+ },
95
+ });
96
+ });
97
+
98
+ it("ignores a fake section inside a code fence", () => {
99
+ const markdown = [
100
+ "```markdown",
101
+ beliefSection(["| `fake-key` | 1% | 2% | fake |"]),
102
+ "```",
103
+ "",
104
+ beliefSection(["| `real-key` | 40% | 80% | observed |"]),
105
+ ].join("\n");
106
+
107
+ expect(parseTaskBeliefState(markdown)).toMatchObject({
108
+ success: true,
109
+ state: { entries: [{ key: "real-key" }] },
110
+ });
111
+ });
112
+
113
+ it.each([
114
+ ["backtick", "```markdown", "```not-a-close", "```"],
115
+ ["tilde", "~~~markdown", "~~~not-a-close", "~~~"],
116
+ ])("keeps headings fenced after a malformed %s close candidate", (_, opener, malformedClose, close) => {
117
+ const markdown = [
118
+ opener,
119
+ malformedClose,
120
+ beliefSection(["| `fake-key` | 1% | 2% | fake |"]),
121
+ close,
122
+ ].join("\n");
123
+
124
+ expect(parseTaskBeliefState(markdown)).toMatchObject({
125
+ success: false,
126
+ code: "MISSING_BELIEF_STATE",
127
+ });
128
+ });
129
+
130
+ it("reports a missing non-fenced section", () => {
131
+ const markdown = [
132
+ "## Objective",
133
+ "Ship",
134
+ "",
135
+ "~~~markdown",
136
+ beliefSection(["| `fake-key` | 1% | 2% | fake |"]),
137
+ "~~~",
138
+ ].join("\n");
139
+
140
+ expect(parseTaskBeliefState(markdown)).toMatchObject({
141
+ success: false,
142
+ code: "MISSING_BELIEF_STATE",
143
+ });
144
+ });
145
+
146
+ it("reports duplicate non-fenced sections", () => {
147
+ const markdown = [
148
+ beliefSection(["| `first-key` | 10% | 90% | first |"]),
149
+ "",
150
+ beliefSection(["| `second-key` | 20% | 90% | second |"]),
151
+ ].join("\n");
152
+
153
+ expect(parseTaskBeliefState(markdown)).toMatchObject({
154
+ success: false,
155
+ code: "DUPLICATE_BELIEF_STATE",
156
+ });
157
+ });
158
+
159
+ it("reports duplicate keys", () => {
160
+ const result = parseTaskBeliefState(
161
+ beliefSection([
162
+ "| `same-key` | 10% | 90% | one |",
163
+ "| `same-key` | 20% | 90% | two |",
164
+ ]),
165
+ );
166
+
167
+ expect(result).toMatchObject({
168
+ success: false,
169
+ code: "DUPLICATE_BELIEF_KEY",
170
+ key: "same-key",
171
+ });
172
+ });
173
+
174
+ it("reports localized or reordered headers", () => {
175
+ const markdown = [
176
+ "## Belief State",
177
+ "",
178
+ "| Belief Key | Target | Probability | Basis |",
179
+ SEPARATOR,
180
+ "| `input-valid` | 90% | 60% | exists |",
181
+ ].join("\n");
182
+
183
+ expect(parseTaskBeliefState(markdown)).toMatchObject({
184
+ success: false,
185
+ code: "INVALID_BELIEF_HEADERS",
186
+ });
187
+ });
188
+
189
+ it.each(["60.5%", "-1%", "101%", "60", " 60 % "])(
190
+ "reports malformed probability %s",
191
+ (probability) => {
192
+ const result = parseTaskBeliefState(
193
+ beliefSection([`| \`input-valid\` | ${probability} | 90% | exists |`]),
194
+ );
195
+
196
+ expect(result).toMatchObject({
197
+ success: false,
198
+ code: "INVALID_BELIEF_PERCENT",
199
+ column: "probability",
200
+ });
201
+ },
202
+ );
203
+
204
+ it("reports malformed target percentages", () => {
205
+ const result = parseTaskBeliefState(
206
+ beliefSection(["| `input-valid` | 60% | nope | exists |"]),
207
+ );
208
+
209
+ expect(result).toMatchObject({
210
+ success: false,
211
+ code: "INVALID_BELIEF_PERCENT",
212
+ column: "target",
213
+ });
214
+ });
215
+
216
+ it.each(["input-valid", "`Input-valid`", "`input_valid`", "`-input-valid`"]) (
217
+ "reports malformed belief key %s",
218
+ (key) => {
219
+ const result = parseTaskBeliefState(
220
+ beliefSection([`| ${key} | 60% | 90% | exists |`]),
221
+ );
222
+
223
+ expect(result).toMatchObject({
224
+ success: false,
225
+ code: "INVALID_BELIEF_KEY",
226
+ });
227
+ },
228
+ );
229
+
230
+ it("reports malformed rows", () => {
231
+ const result = parseTaskBeliefState(
232
+ beliefSection(["| `input-valid` | 60% | 90% | basis | extra |"]),
233
+ );
234
+
235
+ expect(result).toMatchObject({
236
+ success: false,
237
+ code: "MALFORMED_BELIEF_ROW",
238
+ });
239
+ });
240
+ });
241
+
242
+ describe("taskBeliefStatesEqual", () => {
243
+ it("ignores row order and insignificant whitespace", () => {
244
+ const left: TaskBeliefState = {
245
+ entries: [
246
+ { key: "alpha-ready", probability: 20, target: 80, basis: " first observation " },
247
+ { key: "beta-ready", probability: 30, target: 90, basis: "line one\n line two" },
248
+ ],
249
+ };
250
+ const right: TaskBeliefState = {
251
+ entries: [
252
+ { key: "beta-ready", probability: 30, target: 90, basis: "line one line two" },
253
+ { key: "alpha-ready", probability: 20, target: 80, basis: "first observation" },
254
+ ],
255
+ };
256
+
257
+ expect(taskBeliefStatesEqual(left, right)).toBe(true);
258
+ });
259
+
260
+ it.each(["probability", "target", "basis"] as const)("compares %s", (field) => {
261
+ const left: TaskBeliefState = {
262
+ entries: [{ key: "input-valid", probability: 60, target: 90, basis: "exists" }],
263
+ };
264
+ const changed = {
265
+ probability: 61,
266
+ target: 91,
267
+ basis: "verified",
268
+ }[field];
269
+ const right: TaskBeliefState = {
270
+ entries: [{ ...left.entries[0], [field]: changed }],
271
+ };
272
+
273
+ expect(taskBeliefStatesEqual(left, right)).toBe(false);
274
+ });
275
+ });
276
+
277
+ describe("replaceTaskBeliefState", () => {
278
+ const replacement: TaskBeliefState = {
279
+ entries: [
280
+ { key: "input-valid", probability: 75, target: 90, basis: "A | B verified" },
281
+ ],
282
+ };
283
+
284
+ it.each([
285
+ ["literal pipe", "|", "\\|"],
286
+ ["backslash before pipe", "\\|", "\\\\\\|"],
287
+ ["trailing backslash and pipe", "evidence \\|", "evidence \\\\\\|"],
288
+ ["multiple backslashes before pipe", "\\\\|", "\\\\\\\\\\|"],
289
+ ["plain backslash", "path\\segment", "path\\\\segment"],
290
+ ])("round-trips a Basis containing %s", (_, basis, escapedBasis) => {
291
+ const state: TaskBeliefState = {
292
+ entries: [{ key: "input-valid", probability: 75, target: 90, basis }],
293
+ };
294
+
295
+ const replaced = replaceTaskBeliefState("# Task", state);
296
+ expect(replaced).toContain(`| \`input-valid\` | 75% | 90% | ${escapedBasis} |`);
297
+
298
+ const parsed = parseTaskBeliefState(replaced);
299
+ expect(parsed.success).toBe(true);
300
+ if (parsed.success) {
301
+ expect(taskBeliefStatesEqual(parsed.state, state)).toBe(true);
302
+ }
303
+ });
304
+
305
+ it("replaces only the existing Belief State section", () => {
306
+ const before = [
307
+ "# Task",
308
+ "",
309
+ "## Objective",
310
+ "Keep this text exactly. ",
311
+ "",
312
+ beliefSection(["| `old-key` | 10% | 80% | old |"]),
313
+ "",
314
+ "## Acceptance Criteria",
315
+ "- [ ] Preserve this | text",
316
+ "",
317
+ ].join("\n");
318
+
319
+ const replaced = replaceTaskBeliefState(before, replacement);
320
+
321
+ expect(replaced).toBe([
322
+ "# Task",
323
+ "",
324
+ "## Objective",
325
+ "Keep this text exactly. ",
326
+ "",
327
+ beliefSection(["| `input-valid` | 75% | 90% | A \\| B verified |"]),
328
+ "",
329
+ "## Acceptance Criteria",
330
+ "- [ ] Preserve this | text",
331
+ "",
332
+ ].join("\n"));
333
+ });
334
+
335
+ it("preserves a following ATX heading with leading spaces", () => {
336
+ const before = [
337
+ beliefSection(["| `old-key` | 10% | 80% | old |"]),
338
+ "",
339
+ " ## Notes",
340
+ "Keep this text exactly.",
341
+ ].join("\n");
342
+
343
+ expect(replaceTaskBeliefState(before, replacement)).toBe([
344
+ beliefSection(["| `input-valid` | 75% | 90% | A \\| B verified |"]),
345
+ "",
346
+ " ## Notes",
347
+ "Keep this text exactly.",
348
+ ].join("\n"));
349
+ });
350
+
351
+ it("inserts after Acceptance Criteria content and before the next section", () => {
352
+ const before = [
353
+ "## Objective",
354
+ "Ship",
355
+ "",
356
+ "## Acceptance Criteria",
357
+ "- [ ] Tests pass",
358
+ "",
359
+ "## Notes",
360
+ "Do not damage this text.",
361
+ ].join("\n");
362
+
363
+ expect(replaceTaskBeliefState(before, replacement)).toBe([
364
+ "## Objective",
365
+ "Ship",
366
+ "",
367
+ "## Acceptance Criteria",
368
+ "- [ ] Tests pass",
369
+ "",
370
+ beliefSection(["| `input-valid` | 75% | 90% | A \\| B verified |"]),
371
+ "",
372
+ "## Notes",
373
+ "Do not damage this text.",
374
+ ].join("\n"));
375
+ });
376
+
377
+ it("inserts at the end when Acceptance Criteria is the final section", () => {
378
+ const before = "## Acceptance Criteria\n\n- [ ] Tests pass\n";
379
+
380
+ expect(replaceTaskBeliefState(before, replacement)).toBe(
381
+ `${before}\n${beliefSection(["| `input-valid` | 75% | 90% | A \\| B verified |"])}`,
382
+ );
383
+ });
384
+
385
+ it.each(["Input-valid", "`input-valid`", "input_valid", "input-valid\nextra"])(
386
+ "rejects invalid state key %j",
387
+ (key) => {
388
+ expect(() => replaceTaskBeliefState("", {
389
+ entries: [{ ...replacement.entries[0], key }],
390
+ })).toThrow("Belief keys must be kebab-case without backticks or newlines.");
391
+ },
392
+ );
393
+
394
+ it.each([
395
+ ["probability", 60.5],
396
+ ["probability", -1],
397
+ ["probability", 101],
398
+ ["target", 90.5],
399
+ ["target", -1],
400
+ ["target", 101],
401
+ ] as const)("rejects invalid state %s %s", (field, value) => {
402
+ expect(() => replaceTaskBeliefState("", {
403
+ entries: [{ ...replacement.entries[0], [field]: value }],
404
+ })).toThrow(`Belief ${field} must be an integer from 0 to 100.`);
405
+ });
406
+
407
+ it.each(["", " ", "line one\nline two", "line one\rline two"])(
408
+ "rejects blank or multiline state basis %j",
409
+ (basis) => {
410
+ expect(() => replaceTaskBeliefState("", {
411
+ entries: [{ ...replacement.entries[0], basis }],
412
+ })).toThrow("Belief basis must be a nonempty single line.");
413
+ },
414
+ );
415
+
416
+ it("rejects duplicate state keys", () => {
417
+ expect(() => replaceTaskBeliefState("", {
418
+ entries: [replacement.entries[0], { ...replacement.entries[0], probability: 80 }],
419
+ })).toThrow("Belief key 'input-valid' appears more than once.");
420
+ });
421
+ });
@@ -0,0 +1,30 @@
1
+ import type { TaskFileRef, CreateTaskRequest } from "../TaskStoreProtocol";
2
+ import type { A2AApiKeyRecord, CreateA2AApiKeyInput } from "../A2AApiKeyStoreProtocol";
3
+ import type { A2AExposure } from "../A2AProtocol";
4
+
5
+ describe("A2A protocol types", () => {
6
+ it("TaskFileRef supports mimeType", () => {
7
+ const ref: TaskFileRef = { uri: "/project/uploads/a.pdf", name: "a.pdf", mimeType: "application/pdf", addedBy: "user" };
8
+ expect(ref.mimeType).toBe("application/pdf");
9
+ });
10
+
11
+ it("CreateTaskRequest supports caller-provided id", () => {
12
+ const req: CreateTaskRequest = { id: "a2a-task-1", title: "t" };
13
+ expect(req.id).toBe("a2a-task-1");
14
+ });
15
+
16
+ it("A2A key model requires projectId and supports assistantIds, no workspaceId", () => {
17
+ const input: CreateA2AApiKeyInput = { tenantId: "t1", projectId: "p1", assistantIds: ["a1"], label: "l" };
18
+ expect(input.projectId).toBe("p1");
19
+ const rec: A2AApiKeyRecord = {
20
+ id: "k1", key: "a2a_x", tenantId: "t1", projectId: "p1",
21
+ assistantIds: ["a1"], enabled: true, createdAt: new Date(), updatedAt: new Date(),
22
+ };
23
+ expect("workspaceId" in rec).toBe(false);
24
+ });
25
+
26
+ it("A2AExposure shape", () => {
27
+ const exp: A2AExposure = { enabled: true, skills: [{ id: "s", name: "n", description: "d" }] };
28
+ expect(exp.enabled).toBe(true);
29
+ });
30
+ });