pi-onlyne 1.1.2 → 1.2.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 +377 -43
- package/README.zh.md +73 -33
- package/package.json +1 -1
- package/src/agent.live.test.mjs +1 -0
- package/src/agent.mjs +233 -97
- package/src/agent.test.mjs +360 -86
- package/src/background-work.mjs +176 -0
- package/src/background-work.test.mjs +128 -0
- package/src/config.mjs +27 -5
- package/src/index.ts +42 -3
- package/src/pi-surface.mjs +40 -0
- package/src/protocol.mjs +41 -49
- package/src/protocol.test.mjs +58 -8
package/src/protocol.test.mjs
CHANGED
|
@@ -128,22 +128,38 @@ test("heartbeat carries a full legal observation", () => {
|
|
|
128
128
|
delivery: "none",
|
|
129
129
|
resource: "attached",
|
|
130
130
|
recovery: "none",
|
|
131
|
-
outcome: "pending",
|
|
132
|
-
public: "working",
|
|
133
131
|
});
|
|
134
132
|
});
|
|
135
133
|
|
|
136
134
|
test("every observation is a state tuple the reducer calls legal", () => {
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
// The tuple carries no derived view and no task result any more: `Observation`
|
|
136
|
+
// dropped `public` and `outcome`, so the projection mapping that once had to be
|
|
137
|
+
// mirrored here is gone, and the key set is the whole contract.
|
|
138
|
+
const keys = [
|
|
139
|
+
"version",
|
|
140
|
+
"generation_live",
|
|
141
|
+
"isolate_after",
|
|
142
|
+
"terminate_after",
|
|
143
|
+
"mismatch_count",
|
|
144
|
+
"agent",
|
|
145
|
+
"delivery",
|
|
146
|
+
"resource",
|
|
147
|
+
"recovery",
|
|
148
|
+
];
|
|
149
|
+
const agents = { booting: "booting", ready: "ready", running: "running", idle: "idle", gone: "booting" };
|
|
150
|
+
for (const [agent, state] of Object.entries(agents)) {
|
|
140
151
|
const observed = observationFor(agent, { generation: 1, seq: 1 });
|
|
141
|
-
assert.
|
|
152
|
+
assert.deepEqual(Object.keys(observed), keys, `agent=${agent}`);
|
|
153
|
+
assert.equal(observed.agent, state, `agent=${agent}: gone travels as booting; only the host's grace declares death`);
|
|
142
154
|
assert.notEqual(observed.isolate_after, 0);
|
|
143
155
|
assert.notEqual(observed.terminate_after, 0);
|
|
144
|
-
|
|
156
|
+
// The plugin can see neither the intent drain nor its own resource's close,
|
|
157
|
+
// and it does not pretend otherwise: the two placeholders below are
|
|
158
|
+
// overwritten by the client's composition, and `resource: attached` is the
|
|
159
|
+
// attach the host's dispatch path already recorded.
|
|
145
160
|
assert.equal(observed.delivery, "none");
|
|
146
161
|
assert.equal(observed.recovery, "none");
|
|
162
|
+
assert.equal(observed.resource, "attached");
|
|
147
163
|
}
|
|
148
164
|
});
|
|
149
165
|
|
|
@@ -238,7 +254,12 @@ test("the ledger head is capped at the plan's 200 characters", () => {
|
|
|
238
254
|
test("an assignment becomes one message naming its origin and payload", { skip: !hasVectors }, () => {
|
|
239
255
|
const assign = ASSIGN().args;
|
|
240
256
|
const text = injectionText({ assign, proseIsNew: true });
|
|
241
|
-
|
|
257
|
+
// The vector's causality names no hop budget, and the header is byte for byte
|
|
258
|
+
// the line this plugin has always injected.
|
|
259
|
+
assert.equal(
|
|
260
|
+
text.split("\n")[0],
|
|
261
|
+
"[onlyne] task 11111111-1111-4111-8111-111111111111 from role:planner (kind task)",
|
|
262
|
+
);
|
|
242
263
|
assert.match(text, /\[onlyne\] role prose from the spec:\nRead the incoming task/);
|
|
243
264
|
assert.match(text, /\nbuild it\n?$/);
|
|
244
265
|
|
|
@@ -247,6 +268,35 @@ test("an assignment becomes one message naming its origin and payload", { skip:
|
|
|
247
268
|
assert.match(repeat, /build it/);
|
|
248
269
|
});
|
|
249
270
|
|
|
271
|
+
test("the header names the hop and the budget once the family names a budget", () => {
|
|
272
|
+
const header = (causality) =>
|
|
273
|
+
injectionText({
|
|
274
|
+
assign: {
|
|
275
|
+
task_id: "t9",
|
|
276
|
+
envelope: {
|
|
277
|
+
id: "e9",
|
|
278
|
+
kind: "task",
|
|
279
|
+
from: { role: { role: "planner" } },
|
|
280
|
+
causality,
|
|
281
|
+
body: { text: "pass it on" },
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
proseIsNew: false,
|
|
285
|
+
}).split("\n")[0];
|
|
286
|
+
|
|
287
|
+
assert.equal(
|
|
288
|
+
header({ task: "t9", hop: 2, attempt: 0, family: "t1", hop_budget: 5 }),
|
|
289
|
+
"[onlyne] task t9 from role:planner (kind task, hop 2, hop budget 5)",
|
|
290
|
+
);
|
|
291
|
+
// A family may spend no further hop: zero is a figure the header carries.
|
|
292
|
+
assert.equal(
|
|
293
|
+
header({ task: "t9", hop: 0, attempt: 0, family: "t1", hop_budget: 0 }),
|
|
294
|
+
"[onlyne] task t9 from role:planner (kind task, hop 0, hop budget 0)",
|
|
295
|
+
);
|
|
296
|
+
// No budget named: the compat rule holds, hop or no hop.
|
|
297
|
+
assert.equal(header({ task: "t9", hop: 2, attempt: 0 }), "[onlyne] task t9 from role:planner (kind task)");
|
|
298
|
+
});
|
|
299
|
+
|
|
250
300
|
test("an empty-bodied assignment still produces an instruction", () => {
|
|
251
301
|
const text = injectionText({
|
|
252
302
|
assign: { task_id: "t9", envelope: { id: "e9", kind: "note", from: { gateway: { gateway: "fg1", channel: "fake", conversation: "c1" } }, body: {} } },
|