pi-onlyne 0.9.1 → 1.1.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 +338 -199
- package/README.zh.md +338 -0
- package/onlyne.json.example +6 -0
- package/package.json +23 -35
- package/relay.toml.example +13 -0
- package/src/activity.mjs +96 -0
- package/src/activity.test.mjs +69 -0
- package/src/agent.live.test.mjs +112 -0
- package/src/agent.mjs +963 -0
- package/src/agent.test.mjs +1160 -0
- package/src/config.mjs +74 -0
- package/src/config.test.mjs +88 -0
- package/src/frame.mjs +99 -0
- package/src/frame.test.mjs +97 -0
- package/src/index.ts +303 -0
- package/src/pi-surface.mjs +153 -0
- package/src/protocol.mjs +364 -0
- package/src/protocol.test.mjs +296 -0
- package/src/relay.mjs +299 -0
- package/src/relay.test.mjs +210 -0
- package/LICENSE +0 -21
- package/SPEC.md +0 -124
- package/dist/config.d.ts +0 -32
- package/dist/config.js +0 -19
- package/dist/index.d.ts +0 -27
- package/dist/index.js +0 -780
- package/dist/onlyne.d.ts +0 -70
- package/dist/onlyne.js +0 -194
- package/dist/session.d.ts +0 -82
- package/dist/session.js +0 -91
- package/dist/swarm-prompt.d.ts +0 -12
- package/dist/swarm-prompt.js +0 -45
- package/dist/swarm-slot.d.ts +0 -43
- package/dist/swarm-slot.js +0 -76
- package/dist/swarm.d.ts +0 -96
- package/dist/swarm.js +0 -296
- package/dist/workspace.d.ts +0 -6
- package/dist/workspace.js +0 -14
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Integration: the plugin against a really-running `onlyne-client`.
|
|
2
|
+
//
|
|
3
|
+
// Everything else in this suite talks to a fake host built from the same
|
|
4
|
+
// framing code, which cannot catch a disagreement with the shipped binary. This
|
|
5
|
+
// case starts the real client's adapter socket (a workspace from
|
|
6
|
+
// `onlyne-client init`), opens the handshake, and reads the welcome the Rust
|
|
7
|
+
// side actually writes.
|
|
8
|
+
//
|
|
9
|
+
// The client's adapter socket binds before its server link, so no server and no
|
|
10
|
+
// task are needed to prove the live handshake. When the binaries are not built
|
|
11
|
+
// the case skips rather than failing: it verifies a build artifact, not source.
|
|
12
|
+
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
15
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { test } from "node:test";
|
|
20
|
+
|
|
21
|
+
import { OnlyneAgent } from "./agent.mjs";
|
|
22
|
+
|
|
23
|
+
const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
|
|
24
|
+
const BIN_DIR = join(REPO_ROOT, "target", "debug");
|
|
25
|
+
const CLIENT = join(BIN_DIR, "onlyne-client");
|
|
26
|
+
const SERVER = join(BIN_DIR, "onlyne-server");
|
|
27
|
+
const hasBinaries = existsSync(CLIENT) && existsSync(SERVER);
|
|
28
|
+
const TASK_ID = "11111111-1111-4111-8111-111111111111";
|
|
29
|
+
const SESSION_ID = "8b1c0d5e-2222-4222-8222-222222222222";
|
|
30
|
+
|
|
31
|
+
/** Wait until `predicate` holds; throws when the window closes. */
|
|
32
|
+
async function waitFor(predicate, { timeoutMs = 15_000, stepMs = 20 } = {}) {
|
|
33
|
+
const deadline = Date.now() + timeoutMs;
|
|
34
|
+
for (;;) {
|
|
35
|
+
const value = await predicate();
|
|
36
|
+
if (value) return value;
|
|
37
|
+
if (Date.now() > deadline) throw new Error("timed out waiting for the real client");
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, stepMs));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("a real onlyne-client answers hello with a welcome", { skip: !hasBinaries }, async () => {
|
|
43
|
+
const tmp = mkdtempSync(join(tmpdir(), "pi-onlyne-live-"));
|
|
44
|
+
const serverRoot = join(tmp, "server");
|
|
45
|
+
const workspace = join(tmp, "planner");
|
|
46
|
+
// `init` reads the server's spec.toml for the listen address and cert pin, so
|
|
47
|
+
// the server root is bootstrapped first; the server itself never runs, which
|
|
48
|
+
// is the point: the adapter socket is served independently of the link.
|
|
49
|
+
execFileSync(SERVER, ["init", "--root", serverRoot, "--listen", "127.0.0.1:7899"], { stdio: "pipe" });
|
|
50
|
+
execFileSync(CLIENT, ["init", "--workspace", workspace, "--role", "planner", "--server-root", serverRoot], {
|
|
51
|
+
stdio: "pipe",
|
|
52
|
+
});
|
|
53
|
+
const child = spawn(CLIENT, ["run", "--workspace", workspace], { stdio: ["ignore", "pipe", "pipe"] });
|
|
54
|
+
let clientLog = "";
|
|
55
|
+
child.stdout.on("data", (chunk) => { clientLog += chunk; });
|
|
56
|
+
child.stderr.on("data", (chunk) => { clientLog += chunk; });
|
|
57
|
+
|
|
58
|
+
const socketPath = join(workspace, ".onlyne", "run", "s");
|
|
59
|
+
const surface = {
|
|
60
|
+
available: { wakeUser: true },
|
|
61
|
+
calls: [],
|
|
62
|
+
wakeUser(text) { this.calls.push(text); return true; },
|
|
63
|
+
proseContext: () => true,
|
|
64
|
+
customEntry: () => true,
|
|
65
|
+
status: () => {},
|
|
66
|
+
welcome: () => {},
|
|
67
|
+
isIdle: () => true,
|
|
68
|
+
exit: () => {},
|
|
69
|
+
};
|
|
70
|
+
const logs = [];
|
|
71
|
+
const agent = new OnlyneAgent({
|
|
72
|
+
socketPath,
|
|
73
|
+
cwd: workspace,
|
|
74
|
+
role: "planner",
|
|
75
|
+
sessionId: SESSION_ID,
|
|
76
|
+
taskId: TASK_ID,
|
|
77
|
+
surface,
|
|
78
|
+
log: (line) => logs.push(line),
|
|
79
|
+
heartbeatMs: 60_000,
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
await waitFor(() => existsSync(socketPath));
|
|
83
|
+
agent.start();
|
|
84
|
+
await waitFor(() => agent.status().connected, { timeoutMs: 15_000 });
|
|
85
|
+
|
|
86
|
+
const welcome = agent.welcome;
|
|
87
|
+
assert.equal(welcome.role, "planner", `client log: ${clientLog}`);
|
|
88
|
+
assert.equal(welcome.protocol, 1);
|
|
89
|
+
assert.equal(welcome.generation, 1);
|
|
90
|
+
assert.equal(welcome.sessionId, SESSION_ID, "the client echoes the mounted session");
|
|
91
|
+
assert.deepEqual(welcome.hostCapabilities, ["probe", "recycle"]);
|
|
92
|
+
assert.equal(typeof welcome.server.connected, "boolean");
|
|
93
|
+
|
|
94
|
+
// The client refuses a `ready` for a task it never staged, and that refusal
|
|
95
|
+
// is the proof the frame loop round-trips: it is a reply to a request this
|
|
96
|
+
// plugin sent after welcome, carrying a real error payload.
|
|
97
|
+
await waitFor(() => logs.some((line) => line.includes("ready refused: internal: unknown session for")), {
|
|
98
|
+
timeoutMs: 10_000,
|
|
99
|
+
});
|
|
100
|
+
assert.equal(agent.status().lastError, null);
|
|
101
|
+
|
|
102
|
+
} finally {
|
|
103
|
+
agent.stop("test");
|
|
104
|
+
child.kill("SIGTERM");
|
|
105
|
+
await Promise.race([
|
|
106
|
+
new Promise((resolve) => child.once("exit", resolve)),
|
|
107
|
+
new Promise((resolve) => setTimeout(resolve, 3_000)),
|
|
108
|
+
]);
|
|
109
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
110
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
});
|