@danypops/pi-packed 0.19.7 → 0.19.10
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/dist/client.d.ts +109 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1 -0
- package/dist/protocol.d.ts +221 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +1 -0
- package/extension/src/{permission.ts → approval/permission.ts} +1 -1
- package/extension/src/index.ts +1 -1
- package/extension/src/packed.ts +2 -2
- package/extension/src/{discover.ts → tabs/discover.ts} +4 -4
- package/extension/src/{resource-config.ts → tabs/resource-config.ts} +4 -4
- package/extension/src/{security-tui.ts → tabs/security-tui.ts} +3 -3
- package/extension/src/tool-output.ts +1 -1
- package/extension/src/tools.ts +2 -2
- package/extension/src/tui.ts +4 -4
- package/package.json +31 -8
- package/service/schema/pi-setup-v1.schema.json +70 -0
- package/service/setup/danypops-ecosystem.pi-setup.json +15 -0
- package/service/src/adoption/advisories.ts +268 -0
- package/service/src/adoption/check.ts +872 -0
- package/service/src/adoption/commit-freshness.ts +167 -0
- package/service/src/adoption/doctor.ts +135 -0
- package/service/src/adoption/install-validation.ts +187 -0
- package/service/src/adoption/pack.ts +291 -0
- package/service/src/adoption/score.ts +466 -0
- package/service/src/adoption/smoke-child.ts +113 -0
- package/service/src/adoption/smoke.ts +282 -0
- package/service/src/cli/cli.ts +926 -0
- package/service/src/daemon/cleanup.ts +76 -0
- package/service/src/daemon/client.ts +412 -0
- package/service/src/daemon/daemon-service.ts +249 -0
- package/service/src/daemon/daemon.ts +110 -0
- package/service/src/daemon/service.ts +664 -0
- package/service/src/daemon/watcher.ts +92 -0
- package/service/src/index/build-index.ts +256 -0
- package/service/src/packages/catalog.ts +61 -0
- package/service/src/packages/db.ts +224 -0
- package/service/src/packages/install.ts +60 -0
- package/service/src/packages/installed.ts +123 -0
- package/service/src/packages/package.ts +141 -0
- package/service/src/packages/resources.ts +203 -0
- package/service/src/pi/pi-version.ts +171 -0
- package/service/src/public/atomic-json.ts +32 -0
- package/service/src/public/client.ts +277 -0
- package/service/src/public/protocol.ts +169 -0
- package/service/src/publish/publish.ts +855 -0
- package/service/src/registry/registry.ts +246 -0
- package/service/src/security/security.ts +128 -0
- package/service/src/self-update/self-update.ts +148 -0
- package/service/src/setup/setup.ts +761 -0
- package/service/src/shared/atomic-json.ts +33 -0
- package/service/src/shared/cache.ts +21 -0
- package/service/src/shared/constants.ts +73 -0
- package/service/src/shared/log.ts +21 -0
- package/service/src/shared/paths.ts +88 -0
- package/service/src/shared/state.ts +15 -0
- package/service/src/shared/version.ts +46 -0
- package/service/test/advisories.test.ts +287 -0
- package/service/test/check.test.ts +368 -0
- package/service/test/cleanup.test.ts +220 -0
- package/service/test/cli.test.ts +1303 -0
- package/service/test/core.test.ts +181 -0
- package/service/test/daemon-kit-migration.test.ts +181 -0
- package/service/test/daemon-service.test.ts +238 -0
- package/service/test/db.test.ts +178 -0
- package/service/test/doctor.test.ts +234 -0
- package/service/test/domain.test.ts +291 -0
- package/service/test/fixtures/install-validation/broken-package/extension/index.ts +3 -0
- package/service/test/fixtures/install-validation/broken-package/package.json +8 -0
- package/service/test/fixtures/install-validation/healthy-package/extension/index.ts +3 -0
- package/service/test/fixtures/install-validation/healthy-package/package.json +8 -0
- package/service/test/fixtures/install-validation/no-manifest-package/package.json +5 -0
- package/service/test/index.test.ts +353 -0
- package/service/test/install-validation.test.ts +114 -0
- package/service/test/install.test.ts +113 -0
- package/service/test/log.test.ts +42 -0
- package/service/test/pack-score.test.ts +513 -0
- package/service/test/pi-version.test.ts +318 -0
- package/service/test/public-boundary.test.ts +54 -0
- package/service/test/public-client.test.ts +127 -0
- package/service/test/public-consumer.ts +8 -0
- package/service/test/publish.test.ts +333 -0
- package/service/test/registry-contract.test.ts +148 -0
- package/service/test/resources.test.ts +255 -0
- package/service/test/security.test.ts +89 -0
- package/service/test/self-update.test.ts +257 -0
- package/service/test/service.test.ts +555 -0
- package/service/test/setup.test.ts +375 -0
- package/service/test/smoke.test.ts +118 -0
- package/service/test/version.test.ts +37 -0
- package/service/tsconfig.consumer.json +13 -0
- package/service/tsconfig.public.json +12 -0
- /package/extension/src/{reload.ts → approval/reload.ts} +0 -0
- /package/extension/src/{discover-model.ts → tabs/discover-model.ts} +0 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { AuthenticatedRpcClient } from "@danypops/vehicle-client/rpc-client";
|
|
6
|
+
import type { Server } from "bun";
|
|
7
|
+
import { createApp, type OperationInputs, type OperationName, type OperationOutputs } from "../src/daemon/service.ts";
|
|
8
|
+
import type { Installer, PkgInfo, Registry, SearchPage } from "../src/packages/package.ts";
|
|
9
|
+
import {
|
|
10
|
+
checkPiVersion,
|
|
11
|
+
createFetchLatestPiRelease,
|
|
12
|
+
type PiReleaseInfo,
|
|
13
|
+
type PiVersionReport,
|
|
14
|
+
piUpdateSelfArgs,
|
|
15
|
+
runPiStatusInteractive,
|
|
16
|
+
runPiUpdateSelf,
|
|
17
|
+
} from "../src/pi/pi-version.ts";
|
|
18
|
+
import type { VersionCommand } from "../src/publish/publish.ts";
|
|
19
|
+
|
|
20
|
+
class NoopRegistry implements Registry {
|
|
21
|
+
async search(): Promise<SearchPage> {
|
|
22
|
+
return { results: [], total: 0 };
|
|
23
|
+
}
|
|
24
|
+
async searchPage(): Promise<SearchPage> {
|
|
25
|
+
return { results: [], total: 0 };
|
|
26
|
+
}
|
|
27
|
+
async searchAll() {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
async info(name: string): Promise<PkgInfo> {
|
|
31
|
+
return { name, version: "1.0.0" };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class NoopInstaller implements Installer {
|
|
36
|
+
async install() {
|
|
37
|
+
return "ok";
|
|
38
|
+
}
|
|
39
|
+
async remove() {
|
|
40
|
+
return "ok";
|
|
41
|
+
}
|
|
42
|
+
async update() {
|
|
43
|
+
return { output: "ok", reloadRequired: false, alreadyUpToDate: true, pinned: false };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const okVersion: VersionCommand = async () => ({ code: 0, stdout: "0.82.1\n", stderr: "" });
|
|
48
|
+
const noPi: VersionCommand = async () => ({ code: 127, stdout: "", stderr: "command not found: pi" });
|
|
49
|
+
const garbled: VersionCommand = async () => ({ code: 0, stdout: "not-a-version\n", stderr: "" });
|
|
50
|
+
|
|
51
|
+
function fakeLatest(release: PiReleaseInfo | undefined) {
|
|
52
|
+
return async () => release;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("checkPiVersion", () => {
|
|
56
|
+
it("reports up to date when current already satisfies the latest release", async () => {
|
|
57
|
+
const report = await checkPiVersion({ versionCommand: okVersion, fetchLatest: fakeLatest({ version: "0.82.1" }) });
|
|
58
|
+
expect(report).toEqual({ current: "0.82.1", latest: "0.82.1", upToDate: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("reports behind when current is older than the latest release", async () => {
|
|
62
|
+
const report = await checkPiVersion({ versionCommand: okVersion, fetchLatest: fakeLatest({ version: "0.83.0" }) });
|
|
63
|
+
expect(report).toEqual({ current: "0.82.1", latest: "0.83.0", upToDate: false });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("never guesses upToDate when the current version is unknown", async () => {
|
|
67
|
+
const report = await checkPiVersion({ versionCommand: noPi, fetchLatest: fakeLatest({ version: "0.83.0" }) });
|
|
68
|
+
expect(report).toEqual({ latest: "0.83.0" });
|
|
69
|
+
expect(report.upToDate).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("never guesses upToDate when the latest-version check fails", async () => {
|
|
73
|
+
const report = await checkPiVersion({ versionCommand: okVersion, fetchLatest: fakeLatest(undefined) });
|
|
74
|
+
expect(report).toEqual({ current: "0.82.1" });
|
|
75
|
+
expect(report.upToDate).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("treats unparseable version-command output the same as pi being absent", async () => {
|
|
79
|
+
const report = await checkPiVersion({ versionCommand: garbled, fetchLatest: fakeLatest({ version: "0.83.0" }) });
|
|
80
|
+
expect(report.current).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("never reports a rename when packageName merely echoes the current package (confirmed live: the real API always includes this field)", async () => {
|
|
84
|
+
const report = await checkPiVersion({
|
|
85
|
+
versionCommand: okVersion,
|
|
86
|
+
fetchLatest: fakeLatest({ version: "0.83.0", packageName: "@earendil-works/pi-coding-agent" }),
|
|
87
|
+
});
|
|
88
|
+
expect(report.packageName).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("passes through packageName (rename migration signal) and note (release message)", async () => {
|
|
92
|
+
const report = await checkPiVersion({
|
|
93
|
+
versionCommand: okVersion,
|
|
94
|
+
fetchLatest: fakeLatest({ version: "0.83.0", packageName: "@earendil-works/pi", note: "Requires a fresh login." }),
|
|
95
|
+
});
|
|
96
|
+
expect(report.packageName).toBe("@earendil-works/pi");
|
|
97
|
+
expect(report.note).toBe("Requires a fresh login.");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("never throws when the version command itself throws", async () => {
|
|
101
|
+
const throwing: VersionCommand = async () => {
|
|
102
|
+
throw new Error("spawn failed");
|
|
103
|
+
};
|
|
104
|
+
const report = await checkPiVersion({ versionCommand: throwing, fetchLatest: fakeLatest({ version: "0.83.0" }) });
|
|
105
|
+
expect(report.current).toBeUndefined();
|
|
106
|
+
expect(report.latest).toBe("0.83.0");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Real HTTP adapter against a fake pi.dev upstream (Bun.serve), matching
|
|
111
|
+
// this codebase's existing HttpRegistry test convention -- never mocks
|
|
112
|
+
// global fetch, never touches the real pi.dev/api/latest-version endpoint.
|
|
113
|
+
describe("createFetchLatestPiRelease", () => {
|
|
114
|
+
let server: Server<undefined> | undefined;
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
server?.stop(true);
|
|
118
|
+
server = undefined;
|
|
119
|
+
delete process.env.PI_SKIP_VERSION_CHECK;
|
|
120
|
+
delete process.env.PI_OFFLINE;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("parses a real successful response", async () => {
|
|
124
|
+
server = Bun.serve({ port: 0, fetch: () => Response.json({ version: "0.83.0" }) });
|
|
125
|
+
const fetchLatest = createFetchLatestPiRelease(`http://127.0.0.1:${server.port}`);
|
|
126
|
+
expect(await fetchLatest()).toEqual({ version: "0.83.0" });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("includes packageName and note only when present and non-empty", async () => {
|
|
130
|
+
server = Bun.serve({ port: 0, fetch: () => Response.json({ version: "0.83.0", packageName: " ", note: "Read the changelog." }) });
|
|
131
|
+
const fetchLatest = createFetchLatestPiRelease(`http://127.0.0.1:${server.port}`);
|
|
132
|
+
expect(await fetchLatest()).toEqual({ version: "0.83.0", note: "Read the changelog." });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("resolves to undefined on a non-2xx response, never throws", async () => {
|
|
136
|
+
server = Bun.serve({ port: 0, fetch: () => new Response("nope", { status: 500 }) });
|
|
137
|
+
const fetchLatest = createFetchLatestPiRelease(`http://127.0.0.1:${server.port}`);
|
|
138
|
+
expect(await fetchLatest()).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("resolves to undefined on a malformed body, never throws", async () => {
|
|
142
|
+
server = Bun.serve({ port: 0, fetch: () => new Response("not json", { status: 200 }) });
|
|
143
|
+
const fetchLatest = createFetchLatestPiRelease(`http://127.0.0.1:${server.port}`);
|
|
144
|
+
expect(await fetchLatest()).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("resolves to undefined on an unreachable host, never throws or hangs", async () => {
|
|
148
|
+
const fetchLatest = createFetchLatestPiRelease("http://127.0.0.1:1");
|
|
149
|
+
expect(await fetchLatest({ timeoutMs: 500 })).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("PI_SKIP_VERSION_CHECK short-circuits before any network call", async () => {
|
|
153
|
+
server = Bun.serve({
|
|
154
|
+
port: 0,
|
|
155
|
+
fetch: () => {
|
|
156
|
+
throw new Error("must never be called");
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
process.env.PI_SKIP_VERSION_CHECK = "1";
|
|
160
|
+
const fetchLatest = createFetchLatestPiRelease(`http://127.0.0.1:${server.port}`);
|
|
161
|
+
expect(await fetchLatest()).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("PI_OFFLINE short-circuits before any network call", async () => {
|
|
165
|
+
server = Bun.serve({
|
|
166
|
+
port: 0,
|
|
167
|
+
fetch: () => {
|
|
168
|
+
throw new Error("must never be called");
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
process.env.PI_OFFLINE = "1";
|
|
172
|
+
const fetchLatest = createFetchLatestPiRelease(`http://127.0.0.1:${server.port}`);
|
|
173
|
+
expect(await fetchLatest()).toBeUndefined();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("piUpdateSelfArgs / runPiUpdateSelf", () => {
|
|
178
|
+
const originalPiBin = process.env.PI_PACKED_PI_BIN;
|
|
179
|
+
afterEach(() => {
|
|
180
|
+
if (originalPiBin === undefined) delete process.env.PI_PACKED_PI_BIN;
|
|
181
|
+
else process.env.PI_PACKED_PI_BIN = originalPiBin;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("builds the exact self-update argv using the same pi-binary resolution as install.ts", () => {
|
|
185
|
+
process.env.PI_PACKED_PI_BIN = "/opt/pi/bin/pi";
|
|
186
|
+
expect(piUpdateSelfArgs()).toEqual(["/opt/pi/bin/pi", "update", "--self"]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("exposes runPiUpdateSelf as a real, separately invocable subprocess orchestration function -- never invoked by any test", () => {
|
|
190
|
+
expect(typeof runPiUpdateSelf).toBe("function");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("runPiStatusInteractive", () => {
|
|
195
|
+
function fakeCheck(reports: PiVersionReport[]): () => Promise<PiVersionReport> {
|
|
196
|
+
const queue = [...reports];
|
|
197
|
+
return async () => queue.shift() ?? reports[reports.length - 1]!;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
it("never confirms or updates when already up to date", async () => {
|
|
201
|
+
const confirms: string[] = [];
|
|
202
|
+
let updateCalls = 0;
|
|
203
|
+
const report = await runPiStatusInteractive({
|
|
204
|
+
check: fakeCheck([{ current: "0.82.1", latest: "0.82.1", upToDate: true }]),
|
|
205
|
+
confirm: async (q) => {
|
|
206
|
+
confirms.push(q);
|
|
207
|
+
return true;
|
|
208
|
+
},
|
|
209
|
+
runUpdate: async () => {
|
|
210
|
+
updateCalls++;
|
|
211
|
+
return { ok: true };
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
expect(confirms).toEqual([]);
|
|
215
|
+
expect(updateCalls).toBe(0);
|
|
216
|
+
expect(report).toEqual({ current: "0.82.1", latest: "0.82.1", upToDate: true });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("never confirms or updates when upToDate is unknown (current or latest missing)", async () => {
|
|
220
|
+
let confirmCalls = 0;
|
|
221
|
+
const report = await runPiStatusInteractive({
|
|
222
|
+
check: fakeCheck([{ latest: "0.83.0" }]),
|
|
223
|
+
confirm: async () => {
|
|
224
|
+
confirmCalls++;
|
|
225
|
+
return true;
|
|
226
|
+
},
|
|
227
|
+
runUpdate: async () => ({ ok: true }),
|
|
228
|
+
});
|
|
229
|
+
expect(confirmCalls).toBe(0);
|
|
230
|
+
expect(report).toEqual({ latest: "0.83.0" });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("asks, and never updates when the human declines", async () => {
|
|
234
|
+
let updateCalls = 0;
|
|
235
|
+
const report = await runPiStatusInteractive({
|
|
236
|
+
check: fakeCheck([{ current: "0.82.1", latest: "0.83.0", upToDate: false }]),
|
|
237
|
+
confirm: async (q) => {
|
|
238
|
+
expect(q).toContain("0.82.1");
|
|
239
|
+
expect(q).toContain("0.83.0");
|
|
240
|
+
return false;
|
|
241
|
+
},
|
|
242
|
+
runUpdate: async () => {
|
|
243
|
+
updateCalls++;
|
|
244
|
+
return { ok: true };
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
expect(updateCalls).toBe(0);
|
|
248
|
+
expect(report).toEqual({ current: "0.82.1", latest: "0.83.0", upToDate: false });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("updates and re-checks exactly once after the human confirms and the update succeeds", async () => {
|
|
252
|
+
const check = fakeCheck([
|
|
253
|
+
{ current: "0.82.1", latest: "0.83.0", upToDate: false },
|
|
254
|
+
{ current: "0.83.0", latest: "0.83.0", upToDate: true },
|
|
255
|
+
]);
|
|
256
|
+
let checkCalls = 0;
|
|
257
|
+
const report = await runPiStatusInteractive({
|
|
258
|
+
check: async () => {
|
|
259
|
+
checkCalls++;
|
|
260
|
+
return check();
|
|
261
|
+
},
|
|
262
|
+
confirm: async () => true,
|
|
263
|
+
runUpdate: async () => ({ ok: true }),
|
|
264
|
+
});
|
|
265
|
+
expect(checkCalls).toBe(2);
|
|
266
|
+
expect(report).toEqual({ current: "0.83.0", latest: "0.83.0", upToDate: true });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("reports the original stale status, never a fabricated success, when the update subprocess fails", async () => {
|
|
270
|
+
const check = fakeCheck([{ current: "0.82.1", latest: "0.83.0", upToDate: false }]);
|
|
271
|
+
let checkCalls = 0;
|
|
272
|
+
const report = await runPiStatusInteractive({
|
|
273
|
+
check: async () => {
|
|
274
|
+
checkCalls++;
|
|
275
|
+
return check();
|
|
276
|
+
},
|
|
277
|
+
confirm: async () => true,
|
|
278
|
+
runUpdate: async () => ({ ok: false }),
|
|
279
|
+
});
|
|
280
|
+
expect(checkCalls).toBe(1); // never re-checks after a failed update
|
|
281
|
+
expect(report).toEqual({ current: "0.82.1", latest: "0.83.0", upToDate: false });
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("pi.status operation", () => {
|
|
286
|
+
it("routes through the daemon's authenticated operation registry", async () => {
|
|
287
|
+
const app = createApp({
|
|
288
|
+
reg: new NoopRegistry(),
|
|
289
|
+
inst: new NoopInstaller(),
|
|
290
|
+
token: "test-token",
|
|
291
|
+
stateDir: mkdtempSync(join(tmpdir(), "packed-pi-version-state-")),
|
|
292
|
+
dataDir: mkdtempSync(join(tmpdir(), "packed-pi-version-data-")),
|
|
293
|
+
piVersion: { check: async () => ({ current: "0.82.1", latest: "0.83.0", upToDate: false }) },
|
|
294
|
+
});
|
|
295
|
+
const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
|
|
296
|
+
label: "Packed",
|
|
297
|
+
transport: (request) => app.fetch(request),
|
|
298
|
+
});
|
|
299
|
+
expect(await client.operations()).toContain("pi.status");
|
|
300
|
+
expect(await client.call("pi.status", {})).toEqual({ current: "0.82.1", latest: "0.83.0", upToDate: false });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("is unguarded read access -- never requires approval", async () => {
|
|
304
|
+
const app = createApp({
|
|
305
|
+
reg: new NoopRegistry(),
|
|
306
|
+
inst: new NoopInstaller(),
|
|
307
|
+
token: "test-token",
|
|
308
|
+
stateDir: mkdtempSync(join(tmpdir(), "packed-pi-version-state-")),
|
|
309
|
+
dataDir: mkdtempSync(join(tmpdir(), "packed-pi-version-data-")),
|
|
310
|
+
piVersion: { check: async () => ({}) },
|
|
311
|
+
});
|
|
312
|
+
const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
|
|
313
|
+
label: "Packed",
|
|
314
|
+
transport: (request) => app.fetch(request),
|
|
315
|
+
});
|
|
316
|
+
expect(await client.call("pi.status", {})).toEqual({});
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { OPERATION_NAMES } from "../src/daemon/service.ts";
|
|
5
|
+
import type {
|
|
6
|
+
ExtensionOperationName,
|
|
7
|
+
SetupApplyResult as PublicSetupApplyResult,
|
|
8
|
+
SetupPlan as PublicSetupPlan,
|
|
9
|
+
} from "../src/public/protocol.ts";
|
|
10
|
+
import type { SetupApplyResult as CoreSetupApplyResult, SetupPlan as CoreSetupPlan } from "../src/setup/setup.ts";
|
|
11
|
+
|
|
12
|
+
const publicPlan: PublicSetupPlan = {} as CoreSetupPlan;
|
|
13
|
+
const publicApply: PublicSetupApplyResult = {} as CoreSetupApplyResult;
|
|
14
|
+
void [publicPlan, publicApply];
|
|
15
|
+
|
|
16
|
+
const root = new URL("../..", import.meta.url).pathname;
|
|
17
|
+
const extensionOperations: ExtensionOperationName[] = [
|
|
18
|
+
"package.search",
|
|
19
|
+
"package.info",
|
|
20
|
+
"package.installed",
|
|
21
|
+
"package.updates",
|
|
22
|
+
"package.security.get",
|
|
23
|
+
"package.security.set",
|
|
24
|
+
"package.install",
|
|
25
|
+
"package.remove",
|
|
26
|
+
"package.update",
|
|
27
|
+
"setup.plan",
|
|
28
|
+
"setup.apply",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
describe("compiled public boundary", () => {
|
|
32
|
+
it("keeps the extension protocol aligned with daemon operations", () => {
|
|
33
|
+
expect(extensionOperations.every((operation) => OPERATION_NAMES.includes(operation))).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("loads under Node and contains no Bun-native implementation edge", async () => {
|
|
37
|
+
const entry = join(root, "dist/client.js");
|
|
38
|
+
const process = Bun.spawn(
|
|
39
|
+
["node", "--input-type=module", "--eval", `import(${JSON.stringify(entry)}).then(m => console.log(typeof m.connectPackedClient))`],
|
|
40
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
41
|
+
);
|
|
42
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
43
|
+
new Response(process.stdout).text(),
|
|
44
|
+
new Response(process.stderr).text(),
|
|
45
|
+
process.exited,
|
|
46
|
+
]);
|
|
47
|
+
expect({ code, stderr }).toEqual({ code: 0, stderr: "" });
|
|
48
|
+
expect(stdout.trim()).toBe("function");
|
|
49
|
+
const publicFiles = ["client.js", "client.d.ts", "protocol.d.ts"]
|
|
50
|
+
.map((name) => readFileSync(join(root, "dist", name), "utf8"))
|
|
51
|
+
.join("\n");
|
|
52
|
+
expect(publicFiles).not.toMatch(/\bBun\b|bun:sqlite|\.\.\/(?:daemon|db|check)/);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { ensureClient } from "../src/public/client.ts";
|
|
3
|
+
|
|
4
|
+
describe("ensureClient (packed daemon auto-spawn-or-wait decision)", () => {
|
|
5
|
+
it("connects immediately without ever checking for a service or spawning, when already reachable", async () => {
|
|
6
|
+
let spawnCalls = 0;
|
|
7
|
+
let serviceChecks = 0;
|
|
8
|
+
const client = { fake: true } as never;
|
|
9
|
+
const result = await ensureClient({
|
|
10
|
+
connect: async () => client,
|
|
11
|
+
isServiceInstalled: () => {
|
|
12
|
+
serviceChecks++;
|
|
13
|
+
return false;
|
|
14
|
+
},
|
|
15
|
+
spawn: () => {
|
|
16
|
+
spawnCalls++;
|
|
17
|
+
},
|
|
18
|
+
sleep: async () => {},
|
|
19
|
+
});
|
|
20
|
+
expect(result).toBe(client);
|
|
21
|
+
expect(spawnCalls).toBe(0);
|
|
22
|
+
expect(serviceChecks).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("spawns a detached daemon when no service is installed and none is reachable", async () => {
|
|
26
|
+
let spawnCalls = 0;
|
|
27
|
+
let connectAttempts = 0;
|
|
28
|
+
const client = { fake: true } as never;
|
|
29
|
+
const result = await ensureClient({
|
|
30
|
+
connect: async () => {
|
|
31
|
+
connectAttempts++;
|
|
32
|
+
if (connectAttempts < 3) throw new Error("not yet");
|
|
33
|
+
return client;
|
|
34
|
+
},
|
|
35
|
+
isServiceInstalled: () => false,
|
|
36
|
+
spawn: () => {
|
|
37
|
+
spawnCalls++;
|
|
38
|
+
},
|
|
39
|
+
sleep: async () => {},
|
|
40
|
+
retryAttempts: 5,
|
|
41
|
+
retryDelayMs: 0,
|
|
42
|
+
});
|
|
43
|
+
expect(result).toBe(client);
|
|
44
|
+
expect(spawnCalls).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("never spawns a second daemon when a supervised service is installed -- the real fix for the confirmed stray-daemon hazard: an auto-spawned orphan can silently outlive and win the lock race against every later supervised restart", async () => {
|
|
48
|
+
let spawnCalls = 0;
|
|
49
|
+
let connectAttempts = 0;
|
|
50
|
+
const client = { fake: true } as never;
|
|
51
|
+
const result = await ensureClient({
|
|
52
|
+
connect: async () => {
|
|
53
|
+
connectAttempts++;
|
|
54
|
+
if (connectAttempts < 3) throw new Error("not yet");
|
|
55
|
+
return client;
|
|
56
|
+
},
|
|
57
|
+
isServiceInstalled: () => true,
|
|
58
|
+
spawn: () => {
|
|
59
|
+
spawnCalls++;
|
|
60
|
+
},
|
|
61
|
+
sleep: async () => {},
|
|
62
|
+
retryAttempts: 5,
|
|
63
|
+
retryDelayMs: 0,
|
|
64
|
+
});
|
|
65
|
+
expect(result).toBe(client);
|
|
66
|
+
expect(spawnCalls).toBe(0); // waited for the supervised service instead
|
|
67
|
+
expect(connectAttempts).toBeGreaterThan(1); // genuinely retried, not a single shot
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("fails with a message pointing at the service, not a generic timeout, when a service is installed but never becomes reachable", async () => {
|
|
71
|
+
await expect(
|
|
72
|
+
ensureClient({
|
|
73
|
+
connect: async () => {
|
|
74
|
+
throw new Error("never reachable");
|
|
75
|
+
},
|
|
76
|
+
isServiceInstalled: () => true,
|
|
77
|
+
spawn: () => {
|
|
78
|
+
throw new Error("must never be called");
|
|
79
|
+
},
|
|
80
|
+
sleep: async () => {},
|
|
81
|
+
retryAttempts: 2,
|
|
82
|
+
retryDelayMs: 0,
|
|
83
|
+
}),
|
|
84
|
+
).rejects.toThrow(/supervised service is installed/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("fails with a plain timeout message, and did spawn, when no service is installed and nothing ever becomes reachable", async () => {
|
|
88
|
+
let spawnCalls = 0;
|
|
89
|
+
await expect(
|
|
90
|
+
ensureClient({
|
|
91
|
+
connect: async () => {
|
|
92
|
+
throw new Error("never reachable");
|
|
93
|
+
},
|
|
94
|
+
isServiceInstalled: () => false,
|
|
95
|
+
spawn: () => {
|
|
96
|
+
spawnCalls++;
|
|
97
|
+
},
|
|
98
|
+
sleep: async () => {},
|
|
99
|
+
retryAttempts: 2,
|
|
100
|
+
retryDelayMs: 0,
|
|
101
|
+
}),
|
|
102
|
+
).rejects.toThrow(/did not become ready/);
|
|
103
|
+
expect(spawnCalls).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("checks for an installed service exactly once per ensureClient call, not once per retry", async () => {
|
|
107
|
+
let serviceChecks = 0;
|
|
108
|
+
let connectAttempts = 0;
|
|
109
|
+
const client = { fake: true } as never;
|
|
110
|
+
await ensureClient({
|
|
111
|
+
connect: async () => {
|
|
112
|
+
connectAttempts++;
|
|
113
|
+
if (connectAttempts < 4) throw new Error("not yet");
|
|
114
|
+
return client;
|
|
115
|
+
},
|
|
116
|
+
isServiceInstalled: () => {
|
|
117
|
+
serviceChecks++;
|
|
118
|
+
return true;
|
|
119
|
+
},
|
|
120
|
+
spawn: () => {},
|
|
121
|
+
sleep: async () => {},
|
|
122
|
+
retryAttempts: 10,
|
|
123
|
+
retryDelayMs: 0,
|
|
124
|
+
});
|
|
125
|
+
expect(serviceChecks).toBe(1);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { connectPackedClient, type PackedExtensionClient, resolvePackedClientPaths } from "@danypops/pi-packed/client";
|
|
2
|
+
import type { ExtensionOperationName, SetupPlan } from "@danypops/pi-packed/protocol";
|
|
3
|
+
|
|
4
|
+
const paths = resolvePackedClientPaths({ env: {} });
|
|
5
|
+
const operation: ExtensionOperationName = "setup.plan";
|
|
6
|
+
const plan: SetupPlan | undefined = undefined;
|
|
7
|
+
const connect: () => Promise<PackedExtensionClient> = () => connectPackedClient(paths);
|
|
8
|
+
void [operation, plan, connect];
|