@danypops/pi-packed 0.19.12 → 0.20.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 +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/protocol.d.ts +0 -1
- package/dist/protocol.d.ts.map +1 -1
- package/extension/src/menu-theme.ts +17 -12
- package/extension/src/model.ts +11 -4
- package/extension/src/tui.ts +2 -2
- package/package.json +19 -6
- package/service/src/adoption/doctor.ts +5 -1
- package/service/src/adoption/service-doctor.ts +67 -94
- package/service/src/cli/cli.ts +32 -60
- package/service/src/daemon/client.ts +20 -26
- package/service/src/daemon/daemon-service.ts +127 -37
- package/service/src/daemon/daemon.ts +24 -2
- package/service/src/daemon/service.ts +76 -11
- package/service/src/daemon/vehicle-registration.ts +173 -0
- package/service/src/public/client.ts +7 -19
- package/service/src/public/protocol.ts +0 -1
- package/service/src/security/security.ts +2 -0
- package/service/src/shared/constants.ts +2 -0
- package/service/test/cli.test.ts +121 -137
- package/service/test/daemon-kit-migration.test.ts +1 -0
- package/service/test/daemon-maintenance.test.ts +127 -0
- package/service/test/daemon-service.test.ts +93 -40
- package/service/test/public-client.test.ts +1 -1
- package/service/test/security.test.ts +1 -0
- package/service/test/service-doctor.test.ts +53 -95
- package/service/test/service.test.ts +89 -13
- package/service/test/vehicle-registration.test.ts +121 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { 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 { OPERATION_NAMES } from "../src/daemon/service.ts";
|
|
6
|
+
import { createApp, type Deps } from "../src/daemon/service.ts";
|
|
7
|
+
import type { Installer, Pkg, PkgInfo, Registry, SearchPage, UpdateOutcome } from "../src/packages/package.ts";
|
|
8
|
+
|
|
9
|
+
class FakeRegistry implements Registry {
|
|
10
|
+
constructor(
|
|
11
|
+
private results: Pkg[] = [],
|
|
12
|
+
private total = 0,
|
|
13
|
+
) {}
|
|
14
|
+
async search(query: string, limit: number): Promise<SearchPage> {
|
|
15
|
+
return { results: this.results, total: this.total };
|
|
16
|
+
}
|
|
17
|
+
async searchPage(): Promise<SearchPage> {
|
|
18
|
+
return { results: this.results, total: this.total };
|
|
19
|
+
}
|
|
20
|
+
async searchAll(): Promise<Pkg[]> {
|
|
21
|
+
return this.results;
|
|
22
|
+
}
|
|
23
|
+
async info(name: string): Promise<PkgInfo> {
|
|
24
|
+
return { name, version: "1.0.0" };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class FakeInstaller implements Installer {
|
|
29
|
+
async install(source: string): Promise<string> {
|
|
30
|
+
return `installed ${source}`;
|
|
31
|
+
}
|
|
32
|
+
async remove(source: string): Promise<string> {
|
|
33
|
+
return `removed ${source}`;
|
|
34
|
+
}
|
|
35
|
+
async update(source: string): Promise<UpdateOutcome> {
|
|
36
|
+
return { output: `updated ${source}`, reloadRequired: true, alreadyUpToDate: false, pinned: false };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function deps(over: Partial<Deps> = {}): Deps {
|
|
41
|
+
return {
|
|
42
|
+
reg: new FakeRegistry([{ name: "pi-lsp", version: "1.0.0", description: "An LSP package" }], 1),
|
|
43
|
+
inst: new FakeInstaller(),
|
|
44
|
+
token: "test-token",
|
|
45
|
+
stateDir: mkdtempSync(join(tmpdir(), "packed-vehicle-")),
|
|
46
|
+
...over,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function invoke(app: { fetch(request: Request): Promise<Response> }, name: string, input: Record<string, unknown>, token = "test-token") {
|
|
51
|
+
const response = await app.fetch(
|
|
52
|
+
new Request("http://packed.internal/vehicle/invoke", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
55
|
+
body: JSON.stringify({ name, version: 1, input, permissions: ["packed:read", "packed:write"] }),
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
return { status: response.status, body: (await response.json()) as { output?: unknown; error?: { category: string; message: string } } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("packed's daemon operation surface, through the real Vehicle wire protocol", () => {
|
|
62
|
+
it("GET /vehicle/manifest lists all 29 operations, authenticated the same as /api/v1/ops", async () => {
|
|
63
|
+
const app = createApp(deps());
|
|
64
|
+
const unauthorized = await app.fetch(new Request("http://packed.internal/vehicle/manifest"));
|
|
65
|
+
expect(unauthorized.status).toBe(401);
|
|
66
|
+
const response = await app.fetch(new Request("http://packed.internal/vehicle/manifest", { headers: { authorization: "Bearer test-token" } }));
|
|
67
|
+
expect(response.status).toBe(200);
|
|
68
|
+
const body = (await response.json()) as { operations: Array<{ name: string }> };
|
|
69
|
+
expect(body.operations.map((o) => o.name).sort()).toEqual([...OPERATION_NAMES].sort());
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("package.search round-trips through /vehicle/invoke, matching /api/v1/ops's own shape", async () => {
|
|
73
|
+
const app = createApp(deps());
|
|
74
|
+
const result = await invoke(app, "package.search", { query: "lsp", limit: 10 });
|
|
75
|
+
expect(result.status).toBe(200);
|
|
76
|
+
expect((result.body.output as { total: number }).total).toBe(1);
|
|
77
|
+
expect((result.body.output as { results: Array<{ name: string }> }).results[0]?.name).toBe("pi-lsp");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("package.installed and pi.status (daemon-only, never a Pi tool) are still reachable through /vehicle/invoke", async () => {
|
|
81
|
+
const app = createApp(deps({ piHome: mkdtempSync(join(tmpdir(), "packed-vehicle-pihome-")) }));
|
|
82
|
+
const installed = await invoke(app, "package.installed", {});
|
|
83
|
+
expect(installed.status).toBe(200);
|
|
84
|
+
expect(installed.body.output).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("a denied mutation (default mutationApproval: always, no approved flag) surfaces Vehicle's authorization category -> HTTP 403, same as /api/v1/ops's own 403", async () => {
|
|
88
|
+
const app = createApp(deps());
|
|
89
|
+
const result = await invoke(app, "package.remove", { name: "pi-lsp" });
|
|
90
|
+
expect(result.status).toBe(403);
|
|
91
|
+
expect(result.body.error?.category).toBe("authorization");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("an approved mutation succeeds through /vehicle/invoke exactly like an approved /api/v1/ops call", async () => {
|
|
95
|
+
const app = createApp(deps());
|
|
96
|
+
const result = await invoke(app, "package.remove", { name: "pi-lsp", approved: true });
|
|
97
|
+
expect(result.status).toBe(200);
|
|
98
|
+
expect((result.body.output as { ok: boolean }).ok).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("a handler-thrown validation error keeps its own real message, mapped to Vehicle's validation category (not a generic internal 500)", async () => {
|
|
102
|
+
const app = createApp(deps());
|
|
103
|
+
const result = await invoke(app, "package.check", { path: "" });
|
|
104
|
+
expect(result.status).toBe(400);
|
|
105
|
+
expect(result.body.error?.category).toBe("validation");
|
|
106
|
+
expect(result.body.error?.message).toContain("path must be a non-empty string");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("the old /api/v1/ops route keeps working unchanged, alongside /vehicle/invoke", async () => {
|
|
110
|
+
const app = createApp(deps());
|
|
111
|
+
const legacy = await app.fetch(
|
|
112
|
+
new Request("http://packed.internal/api/v1/ops", {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { authorization: "Bearer test-token", "content-type": "application/json" },
|
|
115
|
+
body: JSON.stringify({ op: "package.search", input: { query: "lsp", limit: 10 } }),
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
expect(legacy.status).toBe(200);
|
|
119
|
+
expect(((await legacy.json()) as { result: { total: number } }).result.total).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
});
|