@danypops/pi-packed 0.28.14 → 0.28.17
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/adoption/check.d.ts.map +1 -1
- package/dist/adoption/service-doctor.d.ts +1 -1
- package/dist/adoption/service-doctor.d.ts.map +1 -1
- package/dist/daemon/daemon-service.d.ts.map +1 -1
- package/extension/src/doctor-overlay.ts +3 -1
- package/extension/src/vehicle-tools.ts +3 -1
- package/package.json +6 -5
- package/service/benchmarks/baseline.ts +431 -0
- package/service/src/adoption/check.ts +17 -15
- package/service/src/adoption/service-doctor.ts +32 -5
- package/service/src/daemon/daemon-service.ts +17 -6
- package/service/src/daemon/service.ts +12 -3
- package/service/test/benchmark-baseline.test.ts +98 -0
- package/service/test/check.test.ts +24 -0
- package/service/test/daemon-service.test.ts +33 -4
- package/service/test/service-doctor.test.ts +41 -0
- package/service/test/service.test.ts +15 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../service/src/adoption/check.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../service/src/adoption/check.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,oBAAoB,EAAqB,MAAM,YAAY,CAAC;AAE1E,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE9D,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE;QAAE,UAAU,EAAE,oBAAoB,EAAE,CAAA;KAAE,CAAC;CAC/C;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC9B,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACzE;AAED,qBAAa,oBAAqB,YAAW,cAAc;IAC1D,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;CAGxE;AAyBD,QAAA,MAAM,eAAe,wDAAyD,CAAC;AA0B/E,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAoCpH;AAsCD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAIrE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAOnE;AA4FD,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D;;;;0EAI0E;AAC1E,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,CAAC,CAYrI;AAweD,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CA6GxG;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAwB5E"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ServiceInstallDeps } from "@danypops/vehicle-server/service";
|
|
2
2
|
export interface ServiceUnitDiagnostic {
|
|
3
|
-
code: "SERVICE_EXEC_PATH_MISSING" | "SERVICE_NOT_RUNNING" | "SERVICE_STATUS_UNAVAILABLE" | "SERVICE_STALE_CODE";
|
|
3
|
+
code: "SERVICE_EXEC_PATH_MISSING" | "SERVICE_NOT_RUNNING" | "SERVICE_STATUS_UNAVAILABLE" | "SERVICE_STALE_CODE" | "SERVICE_VERSION_MISMATCH" | "SERVICE_NOT_REGISTERED" | "SERVICE_DESCRIPTOR_DRIFT" | "SERVICE_REQUIREMENT_UNRESOLVED";
|
|
4
4
|
severity: "error" | "warning";
|
|
5
5
|
package: string;
|
|
6
6
|
unitName: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-doctor.d.ts","sourceRoot":"","sources":["../../service/src/adoption/service-doctor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAK3E,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,2BAA2B,GAAG,qBAAqB,GAAG,4BAA4B,GAAG,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"service-doctor.d.ts","sourceRoot":"","sources":["../../service/src/adoption/service-doctor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAK3E,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,2BAA2B,GAAG,qBAAqB,GAAG,4BAA4B,GAAG,oBAAoB,GAC5G,0BAA0B,GAAG,wBAAwB,GAAG,0BAA0B,GAAG,gCAAgC,CAAC;IACzH,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,WAAW,EAAE,qBAAqB,EAAE,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IAC5D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,+EAA+E;IAC/E,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC7C;AAwBD,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,IAAI,EAAE,iBAAiB,GACrB,mBAAmB,CA+GrB;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,mBAAmB,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAO5F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon-service.d.ts","sourceRoot":"","sources":["../../service/src/daemon/daemon-service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"daemon-service.d.ts","sourceRoot":"","sources":["../../service/src/daemon/daemon-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -46,7 +46,9 @@ function defaultClient(): VehicleClient {
|
|
|
46
46
|
async () => {
|
|
47
47
|
const resolved = currentVehicleClientTarget();
|
|
48
48
|
if (!resolved) throw new Error("Packed daemon is not running");
|
|
49
|
-
|
|
49
|
+
const remote = new RemoteVehicleClient({ baseUrl: resolved.baseUrl, token: resolved.token });
|
|
50
|
+
await remote.negotiate({ minimumVersion: 1, maximumVersion: 1, requiredCapabilities: [], optionalCapabilities: [] });
|
|
51
|
+
return remote;
|
|
50
52
|
},
|
|
51
53
|
{ connectRetry: true },
|
|
52
54
|
);
|
|
@@ -86,7 +86,9 @@ export async function registerPackedVehicle(pi: ExtensionAPI): Promise<void> {
|
|
|
86
86
|
async () => {
|
|
87
87
|
const resolved = currentVehicleClientTarget();
|
|
88
88
|
if (!resolved) throw new Error("Packed daemon is not running");
|
|
89
|
-
|
|
89
|
+
const remote = new RemoteVehicleClient({ baseUrl: resolved.baseUrl, token: resolved.token });
|
|
90
|
+
await remote.negotiate({ minimumVersion: 1, maximumVersion: 1, requiredCapabilities: [], optionalCapabilities: [] });
|
|
91
|
+
return remote;
|
|
90
92
|
},
|
|
91
93
|
{ connectRetry: true },
|
|
92
94
|
),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-packed",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.17",
|
|
4
4
|
"description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,17 +42,18 @@
|
|
|
42
42
|
"jiti": "^2.7.0",
|
|
43
43
|
"malevich-tui-components": "^0.32.1",
|
|
44
44
|
"publint": "0.3.22",
|
|
45
|
-
"semver": "^7.8.5"
|
|
45
|
+
"semver": "^7.8.5",
|
|
46
|
+
"typescript": "^5.9.3"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@danypops/pi-tui-harness": "^0.0.1",
|
|
49
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
50
|
-
"@danypops/vehicle-conformance": "^0.4.
|
|
50
|
+
"@danypops/vehicle-client-pi": "^0.48.3",
|
|
51
|
+
"@danypops/vehicle-conformance": "^0.4.2",
|
|
51
52
|
"@types/semver": "^7.7.1",
|
|
52
53
|
"follow-redirects": "^1.16.0"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
55
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
56
|
+
"@danypops/vehicle-client-pi": "^0.48.3",
|
|
56
57
|
"@earendil-works/pi-coding-agent": "*",
|
|
57
58
|
"@earendil-works/pi-tui": "*",
|
|
58
59
|
"typebox": "*"
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { arch, cpus, platform, tmpdir } from "node:os";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { AuthenticatedRpcClient } from "@danypops/vehicle-client/rpc-client";
|
|
8
|
+
import { readDaemonHandle } from "@danypops/vehicle-server/paths";
|
|
9
|
+
import { daemonOptions } from "../src/daemon/daemon.ts";
|
|
10
|
+
import { probe } from "../src/daemon/client.ts";
|
|
11
|
+
import { runDaemonProcess } from "@danypops/vehicle-server/daemon";
|
|
12
|
+
import type { OperationInputs, OperationName, OperationOutputs } from "../src/daemon/service.ts";
|
|
13
|
+
import { writeIndex } from "../src/index/build-index.ts";
|
|
14
|
+
import { dbPath, openDb, replaceAll } from "../src/packages/db.ts";
|
|
15
|
+
import type { Pkg } from "../src/packages/package.ts";
|
|
16
|
+
import { HttpRegistry } from "../src/registry/registry.ts";
|
|
17
|
+
import { resolvePackedPaths } from "../src/shared/paths.ts";
|
|
18
|
+
|
|
19
|
+
const MAX_COMMAND_OUTPUT_BYTES = 256_000;
|
|
20
|
+
const MAX_ARTIFACT_BYTES = 256_000;
|
|
21
|
+
const SERVICE_READY_DEADLINE_MS = 15_000;
|
|
22
|
+
const FREQUENCY_WINDOW_DAYS = 14;
|
|
23
|
+
const DOCTOR_EXTENSION_COUNT = 24;
|
|
24
|
+
const BENCHMARK_FILE = ".artifacts/benchmarks/packed-baseline.json";
|
|
25
|
+
|
|
26
|
+
type BenchmarkMode =
|
|
27
|
+
| "idle-polling"
|
|
28
|
+
| "cached-reads-cold"
|
|
29
|
+
| "cached-reads-warm"
|
|
30
|
+
| "doctor-cold"
|
|
31
|
+
| "doctor-warm"
|
|
32
|
+
| "doctor-concurrent"
|
|
33
|
+
| "catalog-sync-cold"
|
|
34
|
+
| "catalog-sync-warm"
|
|
35
|
+
| "index-build-cold"
|
|
36
|
+
| "index-build-warm"
|
|
37
|
+
| "updates-project";
|
|
38
|
+
|
|
39
|
+
export interface BenchmarkScenario {
|
|
40
|
+
readonly name: BenchmarkMode;
|
|
41
|
+
readonly warmup: number;
|
|
42
|
+
readonly repetitions: number;
|
|
43
|
+
readonly concurrency: number;
|
|
44
|
+
readonly deadlineMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const PACKED_BENCHMARK_SCENARIOS: readonly BenchmarkScenario[] = Object.freeze([
|
|
48
|
+
{ name: "idle-polling", warmup: 0, repetitions: 1, concurrency: 1, deadlineMs: 30_000 },
|
|
49
|
+
{ name: "cached-reads-cold", warmup: 0, repetitions: 1, concurrency: 1, deadlineMs: 30_000 },
|
|
50
|
+
{ name: "cached-reads-warm", warmup: 1, repetitions: 5, concurrency: 1, deadlineMs: 30_000 },
|
|
51
|
+
{ name: "doctor-cold", warmup: 0, repetitions: 1, concurrency: 1, deadlineMs: 120_000 },
|
|
52
|
+
{ name: "doctor-warm", warmup: 1, repetitions: 3, concurrency: 1, deadlineMs: 120_000 },
|
|
53
|
+
{ name: "doctor-concurrent", warmup: 0, repetitions: 2, concurrency: 2, deadlineMs: 120_000 },
|
|
54
|
+
{ name: "catalog-sync-cold", warmup: 0, repetitions: 1, concurrency: 1, deadlineMs: 60_000 },
|
|
55
|
+
{ name: "catalog-sync-warm", warmup: 1, repetitions: 3, concurrency: 1, deadlineMs: 60_000 },
|
|
56
|
+
{ name: "index-build-cold", warmup: 0, repetitions: 1, concurrency: 1, deadlineMs: 120_000 },
|
|
57
|
+
{ name: "index-build-warm", warmup: 1, repetitions: 3, concurrency: 1, deadlineMs: 120_000 },
|
|
58
|
+
{ name: "updates-project", warmup: 1, repetitions: 5, concurrency: 1, deadlineMs: 30_000 },
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
function isBenchmarkMode(value: string | undefined): value is BenchmarkMode {
|
|
62
|
+
return PACKED_BENCHMARK_SCENARIOS.some((scenario) => scenario.name === value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface BenchmarkClient {
|
|
66
|
+
invoke(operation: string, input: Record<string, unknown>): Promise<unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function record(value: unknown): Record<string, unknown> {
|
|
70
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function digest(value: unknown): string {
|
|
74
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Runs one bounded workload and returns only a correctness digest. */
|
|
78
|
+
export async function runBenchmarkWorkload(mode: BenchmarkMode, client: BenchmarkClient, projectRoot: string): Promise<string> {
|
|
79
|
+
if (mode === "idle-polling") {
|
|
80
|
+
await Bun.sleep(1_000);
|
|
81
|
+
return digest({ elapsed: "bounded-idle-window" });
|
|
82
|
+
}
|
|
83
|
+
if (mode === "cached-reads-cold" || mode === "cached-reads-warm") {
|
|
84
|
+
const catalog = record(await client.invoke("package.catalog", {}));
|
|
85
|
+
const index = record(await client.invoke("package.index", {}));
|
|
86
|
+
const updates = record(await client.invoke("package.updates", {}));
|
|
87
|
+
return digest({
|
|
88
|
+
catalogPackages: Array.isArray(catalog.packages) ? catalog.packages.length : 0,
|
|
89
|
+
indexPackages: Array.isArray(index.packages) ? index.packages.length : 0,
|
|
90
|
+
updates: Array.isArray(updates.updates) ? updates.updates.length : 0,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (mode === "doctor-cold" || mode === "doctor-warm" || mode === "doctor-concurrent") {
|
|
94
|
+
const report = record(await client.invoke("doctor.run", { projectRoot }));
|
|
95
|
+
return digest({ ok: report.ok, scanned: report.scanned, conflicts: Array.isArray(report.conflicts) ? report.conflicts.length : 0 });
|
|
96
|
+
}
|
|
97
|
+
if (mode === "catalog-sync-cold" || mode === "catalog-sync-warm") {
|
|
98
|
+
const result = record(await client.invoke("package.catalog.sync", {}));
|
|
99
|
+
return digest({ synced: result.synced });
|
|
100
|
+
}
|
|
101
|
+
if (mode === "index-build-cold" || mode === "index-build-warm") {
|
|
102
|
+
const result = record(await client.invoke("package.index.build", {}));
|
|
103
|
+
return digest({ packages: Array.isArray(result.packages) ? result.packages.length : 0, truncated: result.truncated });
|
|
104
|
+
}
|
|
105
|
+
const result = record(await client.invoke("package.updates.project", { projectRoot }));
|
|
106
|
+
return digest({ updates: Array.isArray(result.updates) ? result.updates.length : 0 });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface FrequencyObservation {
|
|
110
|
+
readonly since: string;
|
|
111
|
+
readonly until: string;
|
|
112
|
+
readonly days: number;
|
|
113
|
+
readonly calls: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface AvailableProjection extends FrequencyObservation {
|
|
117
|
+
readonly status: "available";
|
|
118
|
+
readonly source: "packed metrics.query";
|
|
119
|
+
readonly callsPerDay: number;
|
|
120
|
+
readonly projectedDailyCpuMs: number;
|
|
121
|
+
readonly projectedDailyWallMs: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type FrequencyProjection = AvailableProjection | { readonly status: "unavailable" };
|
|
125
|
+
|
|
126
|
+
export function doctorDailyProjection(observation: FrequencyObservation, cpuMsPerCall: number, wallMsPerCall: number): AvailableProjection {
|
|
127
|
+
const callsPerDay = observation.calls / observation.days;
|
|
128
|
+
return {
|
|
129
|
+
status: "available",
|
|
130
|
+
source: "packed metrics.query",
|
|
131
|
+
...observation,
|
|
132
|
+
callsPerDay,
|
|
133
|
+
projectedDailyCpuMs: callsPerDay * cpuMsPerCall,
|
|
134
|
+
projectedDailyWallMs: callsPerDay * wallMsPerCall,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface ArtifactInput {
|
|
139
|
+
readonly generatedAt: string;
|
|
140
|
+
readonly environment: { readonly platform: string; readonly architecture: string; readonly runtime: string; readonly logicalCpuCount: number };
|
|
141
|
+
readonly frequency: FrequencyProjection;
|
|
142
|
+
readonly results: readonly { readonly name: string; readonly phase?: string; readonly benchmark: unknown }[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function benchmarkArtifact(input: ArtifactInput): ArtifactInput & { readonly schemaVersion: 1; readonly networkBytes: "unavailable" } {
|
|
146
|
+
const artifact = { schemaVersion: 1 as const, networkBytes: "unavailable" as const, ...input };
|
|
147
|
+
if (Buffer.byteLength(JSON.stringify(artifact)) > MAX_ARTIFACT_BYTES) throw new Error("benchmark artifact exceeds 256000 bytes");
|
|
148
|
+
return artifact;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const FIXTURE_PACKAGES: readonly Pkg[] = Object.freeze(
|
|
152
|
+
Array.from({ length: 24 }, (_, index) => ({
|
|
153
|
+
name: `bench-pi-${index}`,
|
|
154
|
+
version: "1.0.0",
|
|
155
|
+
description: `Synthetic Pi package ${index}`,
|
|
156
|
+
date: "2026-01-01T00:00:00.000Z",
|
|
157
|
+
packageEvidence: { shape: "manifest" as const, verified: false, evidence: ["synthetic benchmark fixture"] },
|
|
158
|
+
publication: { trustedPublisher: "unknown" as const },
|
|
159
|
+
})),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
function startRegistryFixture(): { readonly baseUrl: string; close(): void } {
|
|
163
|
+
const server = Bun.serve({
|
|
164
|
+
hostname: "127.0.0.1",
|
|
165
|
+
port: 0,
|
|
166
|
+
async fetch(request) {
|
|
167
|
+
await Bun.sleep(10);
|
|
168
|
+
const url = new URL(request.url);
|
|
169
|
+
if (url.pathname === "/-/v1/search") {
|
|
170
|
+
const from = Math.max(0, Number(url.searchParams.get("from") ?? 0));
|
|
171
|
+
const size = Math.min(20, Math.max(1, Number(url.searchParams.get("size") ?? 5)));
|
|
172
|
+
const objects = FIXTURE_PACKAGES.slice(from, from + size).map((item) => ({ package: item }));
|
|
173
|
+
return Response.json({ total: FIXTURE_PACKAGES.length, objects });
|
|
174
|
+
}
|
|
175
|
+
const encodedName = url.pathname.replace(/^\//, "").replace(/\/latest$/, "");
|
|
176
|
+
const name = decodeURIComponent(encodedName);
|
|
177
|
+
const pkg = FIXTURE_PACKAGES.find((item) => item.name === name);
|
|
178
|
+
if (!pkg) return new Response("missing", { status: 404 });
|
|
179
|
+
if (url.pathname.endsWith("/latest")) {
|
|
180
|
+
return Response.json({ ...pkg, license: "MIT", keywords: ["pi-package"], pi: { extensions: ["extension/index.ts"] } });
|
|
181
|
+
}
|
|
182
|
+
return Response.json({ modified: pkg.date, readme: "Synthetic benchmark package." });
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
return { baseUrl: `http://127.0.0.1:${server.port}`, close: () => server.stop(true) };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isolatedEnvironment(root: string, idlePolling: boolean): Record<string, string> {
|
|
189
|
+
return {
|
|
190
|
+
...(process.env as Record<string, string>),
|
|
191
|
+
HOME: root,
|
|
192
|
+
PI_PACKED_HOME: join(root, "state"),
|
|
193
|
+
PI_PACKED_PI_HOME: join(root, "pi-home"),
|
|
194
|
+
PI_PACKED_WATCH_SECS: idlePolling ? "0.1" : "0",
|
|
195
|
+
PI_PACKED_CATALOG_SECS: "0",
|
|
196
|
+
PI_PACKED_INDEX_SECS: "0",
|
|
197
|
+
PI_PACKED_RECONCILE_SECS: "0",
|
|
198
|
+
PI_PACKED_IDLE_SECS: "600",
|
|
199
|
+
PI_PACKED_PI_BIN: "/usr/bin/false",
|
|
200
|
+
PI_OFFLINE: "1",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function initializeSyntheticHome(root: string, includeIndex: boolean): Promise<void> {
|
|
205
|
+
const state = join(root, "state");
|
|
206
|
+
const piHome = join(root, "pi-home");
|
|
207
|
+
const projectHome = join(root, "project", ".pi");
|
|
208
|
+
await mkdir(state, { recursive: true });
|
|
209
|
+
await mkdir(piHome, { recursive: true });
|
|
210
|
+
await mkdir(projectHome, { recursive: true });
|
|
211
|
+
const configured = FIXTURE_PACKAGES.slice(0, DOCTOR_EXTENSION_COUNT).map((pkg) => `npm:${pkg.name}@0.1.0`);
|
|
212
|
+
await writeFile(join(piHome, "settings.json"), JSON.stringify({ packages: configured }));
|
|
213
|
+
await writeFile(join(projectHome, "settings.json"), JSON.stringify({ packages: [] }));
|
|
214
|
+
for (const [index, pkg] of FIXTURE_PACKAGES.slice(0, DOCTOR_EXTENSION_COUNT).entries()) {
|
|
215
|
+
const directory = join(piHome, "npm", "node_modules", pkg.name);
|
|
216
|
+
await mkdir(join(directory, "extension"), { recursive: true });
|
|
217
|
+
await writeFile(join(directory, "package.json"), JSON.stringify({ name: pkg.name, version: pkg.version, pi: { extensions: ["extension/index.ts"] } }));
|
|
218
|
+
await writeFile(join(directory, "extension", "index.ts"), `export default function (pi: any) { pi.registerTool({ name: "bench_tool_${index}" }); }\n`);
|
|
219
|
+
}
|
|
220
|
+
const database = openDb(dbPath(state));
|
|
221
|
+
try {
|
|
222
|
+
replaceAll(database, [...FIXTURE_PACKAGES], "synthetic:localhost");
|
|
223
|
+
} finally {
|
|
224
|
+
database.close();
|
|
225
|
+
}
|
|
226
|
+
await writeFile(join(state, "updates.json"), JSON.stringify({ checkedAt: "2026-01-01T00:00:00.000Z", updates: [] }));
|
|
227
|
+
if (includeIndex) await writeIndex(join(state, "index.json"), { generatedAt: new Date().toISOString(), packages: [], truncated: false });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function commandResult(
|
|
231
|
+
command: string,
|
|
232
|
+
arguments_: readonly string[],
|
|
233
|
+
environment: Record<string, string> = process.env as Record<string, string>,
|
|
234
|
+
): Promise<{ readonly ok: boolean; readonly stdout: string; readonly stderr: string }> {
|
|
235
|
+
return new Promise((resolveCommand) => {
|
|
236
|
+
execFile(
|
|
237
|
+
command,
|
|
238
|
+
[...arguments_],
|
|
239
|
+
{ env: environment, encoding: "utf8", maxBuffer: MAX_COMMAND_OUTPUT_BYTES, timeout: 240_000 },
|
|
240
|
+
(error, stdout, stderr) => {
|
|
241
|
+
resolveCommand({ ok: !error, stdout, stderr });
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function command(commandName: string, arguments_: readonly string[], environment?: Record<string, string>): Promise<string> {
|
|
248
|
+
const result = await commandResult(commandName, arguments_, environment);
|
|
249
|
+
if (!result.ok) throw new Error(`benchmark command failed: ${basename(commandName)}`);
|
|
250
|
+
return result.stdout;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function waitForService(root: string, environment: Record<string, string>): Promise<void> {
|
|
254
|
+
const paths = resolvePackedPaths({ env: environment, home: root, uid: process.getuid?.() ?? 0 });
|
|
255
|
+
const deadline = Date.now() + SERVICE_READY_DEADLINE_MS;
|
|
256
|
+
while (Date.now() < deadline) {
|
|
257
|
+
if (readDaemonHandle(paths.handle)) return;
|
|
258
|
+
await Bun.sleep(50);
|
|
259
|
+
}
|
|
260
|
+
throw new Error("benchmark service readiness deadline exceeded");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function stopUnit(unit: string): Promise<void> {
|
|
264
|
+
try {
|
|
265
|
+
await command("systemctl", ["--user", "stop", unit]);
|
|
266
|
+
} catch {
|
|
267
|
+
// Cleanup remains best-effort after a failed transient unit.
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function runScenario(scenario: BenchmarkScenario, registryBase: string, index: number): Promise<{ readonly name: string; readonly phase: string; readonly benchmark: unknown }> {
|
|
272
|
+
const root = await mkdtemp(join(tmpdir(), "packed-benchmark-"));
|
|
273
|
+
const includeIndex = !scenario.name.startsWith("index-build");
|
|
274
|
+
await initializeSyntheticHome(root, includeIndex);
|
|
275
|
+
const environment = isolatedEnvironment(root, scenario.name === "idle-polling");
|
|
276
|
+
const vehicle = `packed-bench-${index}`;
|
|
277
|
+
const unit = `armada-${vehicle}.service`;
|
|
278
|
+
const benchmarkFile = fileURLToPath(import.meta.url);
|
|
279
|
+
const manifestPath = join(root, "armada.json");
|
|
280
|
+
const paths = resolvePackedPaths({ env: environment, home: root, uid: process.getuid?.() ?? 0 });
|
|
281
|
+
await writeFile(
|
|
282
|
+
manifestPath,
|
|
283
|
+
JSON.stringify({ schemaVersion: 1, vehicles: [{ name: vehicle, version: "0.0.0", executable: process.execPath, arguments: [benchmarkFile, "serve", root, registryBase], handlePath: paths.handle, restart: { policy: "never" }, readiness: { timeoutMs: SERVICE_READY_DEADLINE_MS, pollIntervalMs: 50 } }] }),
|
|
284
|
+
);
|
|
285
|
+
try {
|
|
286
|
+
const names = ["HOME", "PI_PACKED_HOME", "PI_PACKED_PI_HOME", "PI_PACKED_WATCH_SECS", "PI_PACKED_CATALOG_SECS", "PI_PACKED_INDEX_SECS", "PI_PACKED_RECONCILE_SECS", "PI_PACKED_IDLE_SECS", "PI_PACKED_PI_BIN", "PI_OFFLINE"];
|
|
287
|
+
await command("systemd-run", [
|
|
288
|
+
"--user",
|
|
289
|
+
`--unit=${unit}`,
|
|
290
|
+
"--collect",
|
|
291
|
+
"--property=Type=exec",
|
|
292
|
+
"--property=Restart=no",
|
|
293
|
+
...names.map((name) => `--setenv=${name}=${environment[name] ?? ""}`),
|
|
294
|
+
process.execPath,
|
|
295
|
+
benchmarkFile,
|
|
296
|
+
"serve",
|
|
297
|
+
root,
|
|
298
|
+
registryBase,
|
|
299
|
+
]);
|
|
300
|
+
await waitForService(root, environment);
|
|
301
|
+
const armadaArguments = [
|
|
302
|
+
"benchmark", vehicle, "--manifest", manifestPath,
|
|
303
|
+
"--exec", process.execPath,
|
|
304
|
+
"--arg", benchmarkFile, "--arg", "workload", "--arg", scenario.name, "--arg", root,
|
|
305
|
+
"--warmup", String(scenario.warmup), "--repetitions", String(scenario.repetitions), "--concurrency", String(scenario.concurrency),
|
|
306
|
+
"--deadline-ms", String(scenario.deadlineMs), "--sample-ms", "25", "--max-output-bytes", "1024", "--json",
|
|
307
|
+
];
|
|
308
|
+
const armadaCli = process.env.ARMADA_CLI;
|
|
309
|
+
const armadaResult = armadaCli
|
|
310
|
+
? await commandResult(process.execPath, [armadaCli, ...armadaArguments])
|
|
311
|
+
: await commandResult(process.env.ARMADA_BIN ?? "armada", armadaArguments);
|
|
312
|
+
if (armadaResult.stdout.trim().length === 0) {
|
|
313
|
+
throw new Error(`${scenario.name} benchmark produced no JSON: ${armadaResult.stderr.trim().slice(0, 500) || "no diagnostic"}`);
|
|
314
|
+
}
|
|
315
|
+
const output = JSON.parse(armadaResult.stdout) as {
|
|
316
|
+
benchmark?: { workload?: { failureCodes?: Record<string, number> } };
|
|
317
|
+
diagnostics?: Array<{ code?: string }>;
|
|
318
|
+
};
|
|
319
|
+
if (!armadaResult.ok) {
|
|
320
|
+
const failureCode = Object.keys(output.benchmark?.workload?.failureCodes ?? {})[0] ?? output.diagnostics?.[0]?.code ?? "unknown";
|
|
321
|
+
throw new Error(`${scenario.name} benchmark failed: ${failureCode}`);
|
|
322
|
+
}
|
|
323
|
+
if (!output.benchmark) throw new Error("benchmark result missing");
|
|
324
|
+
const phase = scenario.name.startsWith("catalog-sync") || scenario.name.startsWith("index-build") ? "localhost-registry-wait-and-local-compute" : scenario.name === "doctor-cold" || scenario.name === "doctor-warm" || scenario.name === "doctor-concurrent" ? "sandboxed-subprocess-and-local-compute" : "local-compute";
|
|
325
|
+
return { name: scenario.name, phase, benchmark: output.benchmark };
|
|
326
|
+
} finally {
|
|
327
|
+
await stopUnit(unit);
|
|
328
|
+
await rm(root, { recursive: true, force: true });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function connectBenchmarkClient(root: string): Promise<BenchmarkClient> {
|
|
333
|
+
const environment = isolatedEnvironment(root, false);
|
|
334
|
+
const paths = resolvePackedPaths({ env: environment, home: root, uid: process.getuid?.() ?? 0 });
|
|
335
|
+
const handle = readDaemonHandle(paths.handle);
|
|
336
|
+
if (!handle) throw new Error("benchmark daemon handle unavailable");
|
|
337
|
+
const token = (await readFile(paths.token, "utf8")).trim();
|
|
338
|
+
const rpc = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>(`http://${handle.host}:${handle.port}`, token, { label: "Packed benchmark" });
|
|
339
|
+
return { invoke: (operation, input) => rpc.call(operation as OperationName, input as OperationInputs[OperationName]) };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function workloadMain(args: readonly string[]): Promise<void> {
|
|
343
|
+
const [mode, root] = args;
|
|
344
|
+
if (!isBenchmarkMode(mode) || !root) throw new Error("invalid benchmark workload input");
|
|
345
|
+
const client = await connectBenchmarkClient(root);
|
|
346
|
+
const correctnessDigest = await runBenchmarkWorkload(mode, client, join(root, "project"));
|
|
347
|
+
process.stdout.write(`${correctnessDigest}\n`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function serveMain(args: readonly string[]): Promise<void> {
|
|
351
|
+
const [root, registryBase] = args;
|
|
352
|
+
if (!root || !registryBase) throw new Error("invalid benchmark service input");
|
|
353
|
+
const environment = isolatedEnvironment(root, process.env.PI_PACKED_WATCH_SECS === "0.1");
|
|
354
|
+
const paths = resolvePackedPaths({ env: environment, home: root, uid: process.getuid?.() ?? 0 });
|
|
355
|
+
const registry = new HttpRegistry(registryBase, 5, 0, 1, registryBase);
|
|
356
|
+
runDaemonProcess({ ...daemonOptions({ paths, reg: registry, piHome: join(root, "pi-home"), env: environment, migrateLegacy: false }), onListen: () => {} });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function configuredDoctorFrequency(): FrequencyObservation | undefined {
|
|
360
|
+
const calls = Number(process.env.PACKED_BENCHMARK_DOCTOR_CALLS);
|
|
361
|
+
const days = Number(process.env.PACKED_BENCHMARK_OBSERVATION_DAYS ?? FREQUENCY_WINDOW_DAYS);
|
|
362
|
+
if (!Number.isSafeInteger(calls) || calls < 0 || !Number.isSafeInteger(days) || days < 1 || days > 31) return undefined;
|
|
363
|
+
const untilDate = new Date();
|
|
364
|
+
untilDate.setUTCHours(0, 0, 0, 0);
|
|
365
|
+
const sinceDate = new Date(untilDate.getTime() - days * 86_400_000);
|
|
366
|
+
return { since: sinceDate.toISOString().slice(0, 10), until: untilDate.toISOString().slice(0, 10), days, calls };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function observeDoctorFrequency(): Promise<FrequencyObservation | undefined> {
|
|
370
|
+
try {
|
|
371
|
+
const handle = await probe();
|
|
372
|
+
if (!handle) return configuredDoctorFrequency();
|
|
373
|
+
const untilDate = new Date();
|
|
374
|
+
untilDate.setUTCHours(0, 0, 0, 0);
|
|
375
|
+
const sinceDate = new Date(untilDate.getTime() - FREQUENCY_WINDOW_DAYS * 86_400_000);
|
|
376
|
+
type MetricsName = "metrics.query";
|
|
377
|
+
type MetricsInputs = { "metrics.query": { since: number; until: number; source: "server"; toolName: string; groupBy: string[] } };
|
|
378
|
+
type MetricsOutputs = { "metrics.query": Array<{ count?: number }> };
|
|
379
|
+
const rpc = new AuthenticatedRpcClient<MetricsName, MetricsInputs, MetricsOutputs>(handle.base, handle.token, { label: "Packed metrics" });
|
|
380
|
+
const rows = await rpc.call("metrics.query", { since: sinceDate.getTime(), until: untilDate.getTime(), source: "server", toolName: "doctor.run", groupBy: ["day"] });
|
|
381
|
+
const calls = rows.slice(0, 31).reduce((sum, row) => sum + (Number.isFinite(row.count) ? Math.max(0, Number(row.count)) : 0), 0);
|
|
382
|
+
return { since: sinceDate.toISOString().slice(0, 10), until: untilDate.toISOString().slice(0, 10), days: FREQUENCY_WINDOW_DAYS, calls };
|
|
383
|
+
} catch {
|
|
384
|
+
return configuredDoctorFrequency();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function workloadMetrics(result: { readonly name: string; readonly benchmark: unknown }, mode: string): { cpuMsPerCall: number; wallMsPerCall: number } | undefined {
|
|
389
|
+
if (result.name !== mode) return undefined;
|
|
390
|
+
const workload = record(record(result.benchmark).workload);
|
|
391
|
+
const invocations = Number(workload.invocations);
|
|
392
|
+
const cpuMs = Number(workload.idleAdjustedCpuMs ?? workload.cpuMs);
|
|
393
|
+
const wallMs = Number(workload.wallMs);
|
|
394
|
+
if (!(invocations > 0) || !Number.isFinite(cpuMs) || !Number.isFinite(wallMs)) return undefined;
|
|
395
|
+
return { cpuMsPerCall: cpuMs / invocations, wallMsPerCall: wallMs / invocations };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function baselineMain(): Promise<void> {
|
|
399
|
+
if (platform() !== "linux") throw new Error("Packed cgroup benchmarks require Linux");
|
|
400
|
+
const observation = await observeDoctorFrequency();
|
|
401
|
+
const fixture = startRegistryFixture();
|
|
402
|
+
try {
|
|
403
|
+
const results = [];
|
|
404
|
+
for (let index = 0; index < PACKED_BENCHMARK_SCENARIOS.length; index++) {
|
|
405
|
+
const scenario = PACKED_BENCHMARK_SCENARIOS[index];
|
|
406
|
+
if (scenario) results.push(await runScenario(scenario, fixture.baseUrl, index));
|
|
407
|
+
}
|
|
408
|
+
const doctor = results.map((result) => workloadMetrics(result, "doctor-warm")).find((value) => value !== undefined);
|
|
409
|
+
const frequency = observation && doctor ? doctorDailyProjection(observation, doctor.cpuMsPerCall, doctor.wallMsPerCall) : { status: "unavailable" as const };
|
|
410
|
+
const artifact = benchmarkArtifact({ generatedAt: new Date().toISOString(), environment: { platform: platform(), architecture: arch(), runtime: basename(process.execPath), logicalCpuCount: cpus().length }, frequency, results });
|
|
411
|
+
const destination = join(fileURLToPath(new URL("../../../../", import.meta.url)), BENCHMARK_FILE);
|
|
412
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
413
|
+
const temporary = `${destination}.tmp`;
|
|
414
|
+
await writeFile(temporary, `${JSON.stringify(artifact, null, 2)}\n`);
|
|
415
|
+
await rename(temporary, destination);
|
|
416
|
+
process.stdout.write(`Packed benchmark: ${results.length} workloads; artifact ${BENCHMARK_FILE}\n`);
|
|
417
|
+
} finally {
|
|
418
|
+
fixture.close();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (import.meta.main) {
|
|
423
|
+
try {
|
|
424
|
+
if (process.argv[2] === "workload") await workloadMain(process.argv.slice(3));
|
|
425
|
+
else if (process.argv[2] === "serve") await serveMain(process.argv.slice(3));
|
|
426
|
+
else await baselineMain();
|
|
427
|
+
} catch (error) {
|
|
428
|
+
process.stderr.write(`Packed benchmark failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
429
|
+
process.exitCode = 1;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, lstatSync, opendirSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import * as ts from "typescript";
|
|
3
4
|
import { publint } from "publint";
|
|
4
5
|
import { formatMessage } from "publint/utils";
|
|
5
6
|
import { type ExtensionSmokeResult, runExtensionSmoke } from "./smoke.ts";
|
|
@@ -510,27 +511,28 @@ function shippedSourceFiles(context: Context): string[] {
|
|
|
510
511
|
);
|
|
511
512
|
}
|
|
512
513
|
|
|
513
|
-
/**
|
|
514
|
-
* Strips line and block comments before the import-specifier regex below runs
|
|
515
|
-
* -- a comment can contain an arbitrary quoted "from ..." substring the regex
|
|
516
|
-
* would otherwise match as if it were real code (confirmed live against a
|
|
517
|
-
* JSDoc comment describing a bug fix in prose that happened to contain
|
|
518
|
-
* exactly that shape). Not a full lexer: a string literal that itself
|
|
519
|
-
* contains a comment delimiter could still confuse this, a pre-existing
|
|
520
|
-
* limit of a regex-based scan, not a regression this introduces.
|
|
521
|
-
*/
|
|
522
|
-
function stripComments(source: string): string {
|
|
523
|
-
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, "");
|
|
524
|
-
}
|
|
525
|
-
|
|
526
514
|
/** Every import/require specifier literal in one file, bounded by
|
|
527
515
|
* MAX_SOURCE_BYTES -- undefined when the file is too large to read (the
|
|
528
516
|
* caller's own bound, never silently truncated mid-scan). */
|
|
529
517
|
function importSpecifiersIn(context: Context, file: string): string[] | undefined {
|
|
530
518
|
const absolute = join(context.root, file);
|
|
531
519
|
if (lstatSync(absolute).size > MAX_SOURCE_BYTES) return undefined;
|
|
532
|
-
const source =
|
|
533
|
-
|
|
520
|
+
const source = ts.createSourceFile(file, readFileSync(absolute, "utf8"), ts.ScriptTarget.Latest, false);
|
|
521
|
+
const specifiers: string[] = [];
|
|
522
|
+
const add = (node: ts.Node | undefined): void => {
|
|
523
|
+
if (node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))) specifiers.push(node.text);
|
|
524
|
+
};
|
|
525
|
+
const visit = (node: ts.Node): void => {
|
|
526
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) add(node.moduleSpecifier);
|
|
527
|
+
else if (ts.isExternalModuleReference(node)) add(node.expression);
|
|
528
|
+
else if (ts.isCallExpression(node) && (
|
|
529
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
530
|
+
(ts.isIdentifier(node.expression) && node.expression.text === "require")
|
|
531
|
+
)) add(node.arguments[0]);
|
|
532
|
+
ts.forEachChild(node, visit);
|
|
533
|
+
};
|
|
534
|
+
visit(source);
|
|
535
|
+
return specifiers;
|
|
534
536
|
}
|
|
535
537
|
|
|
536
538
|
function dependencyCheck(context: Context): void {
|
|
@@ -5,7 +5,8 @@ import { resolveDaemonServiceSpec } from "../daemon/daemon-service.ts";
|
|
|
5
5
|
import { RECONCILE_INTERVAL_DEFAULT_MS } from "../shared/constants.ts";
|
|
6
6
|
|
|
7
7
|
export interface ServiceUnitDiagnostic {
|
|
8
|
-
code: "SERVICE_EXEC_PATH_MISSING" | "SERVICE_NOT_RUNNING" | "SERVICE_STATUS_UNAVAILABLE" | "SERVICE_STALE_CODE"
|
|
8
|
+
code: "SERVICE_EXEC_PATH_MISSING" | "SERVICE_NOT_RUNNING" | "SERVICE_STATUS_UNAVAILABLE" | "SERVICE_STALE_CODE"
|
|
9
|
+
| "SERVICE_VERSION_MISMATCH" | "SERVICE_NOT_REGISTERED" | "SERVICE_DESCRIPTOR_DRIFT" | "SERVICE_REQUIREMENT_UNRESOLVED";
|
|
9
10
|
severity: "error" | "warning";
|
|
10
11
|
package: string;
|
|
11
12
|
unitName: string;
|
|
@@ -25,7 +26,7 @@ export interface ServiceDoctorDeps extends ServiceInstallDeps {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
interface ArmadaStatus {
|
|
28
|
-
vehicles?: Array<{ name?: string; executable?: string; nativeStatus?: string; ready?: boolean; nativePid?: number }>;
|
|
29
|
+
vehicles?: Array<{ name?: string; version?: string; descriptorDrift?: boolean; executable?: string; nativeStatus?: string; ready?: boolean; nativePid?: number }>;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -51,11 +52,16 @@ export function checkServiceUnitPaths(
|
|
|
51
52
|
projectRoot: string | undefined,
|
|
52
53
|
deps: ServiceDoctorDeps,
|
|
53
54
|
): ServiceDoctorReport {
|
|
55
|
+
const resolutionDiagnostics: ServiceUnitDiagnostic[] = [];
|
|
54
56
|
const managedPackages = readInstalledPackagesAcrossScopes(piHome, projectRoot).flatMap((pkg) => {
|
|
55
57
|
const resolved = resolveDaemonServiceSpec(piHome, `npm:${pkg.name}`);
|
|
58
|
+
if (!resolved.ok && !resolved.notADaemon) resolutionDiagnostics.push({
|
|
59
|
+
code: "SERVICE_REQUIREMENT_UNRESOLVED", severity: "error", package: pkg.name,
|
|
60
|
+
unitName: pkg.name, message: resolved.reason,
|
|
61
|
+
});
|
|
56
62
|
return resolved.ok ? [{ packageName: pkg.name, spec: resolved.spec }] : [];
|
|
57
63
|
});
|
|
58
|
-
if (managedPackages.length === 0) return { ok:
|
|
64
|
+
if (managedPackages.length === 0) return { ok: resolutionDiagnostics.length === 0, diagnostics: resolutionDiagnostics, checked: 0 };
|
|
59
65
|
|
|
60
66
|
const statusResult = deps.runCommand(process.execPath, [deps.armadaCliPath, "status", "--json"]);
|
|
61
67
|
if (!statusResult.ok) {
|
|
@@ -94,12 +100,33 @@ export function checkServiceUnitPaths(
|
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
const vehicles = new Map((status.vehicles ?? []).flatMap((vehicle) => (vehicle.name ? [[vehicle.name, vehicle]] : [])));
|
|
97
|
-
const diagnostics: ServiceUnitDiagnostic[] = [];
|
|
103
|
+
const diagnostics: ServiceUnitDiagnostic[] = [...resolutionDiagnostics];
|
|
98
104
|
let checked = 0;
|
|
99
105
|
for (const managedPackage of managedPackages) {
|
|
100
106
|
const vehicle = vehicles.get(managedPackage.spec.name);
|
|
101
|
-
if (!vehicle) continue;
|
|
102
107
|
checked++;
|
|
108
|
+
if (!vehicle) {
|
|
109
|
+
diagnostics.push({
|
|
110
|
+
code: "SERVICE_NOT_REGISTERED", severity: "error", package: managedPackage.packageName,
|
|
111
|
+
unitName: managedPackage.spec.name,
|
|
112
|
+
message: `${managedPackage.spec.name} has no Armada registration; install its persistent service through Packed`,
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (vehicle.version !== undefined && vehicle.version !== managedPackage.spec.version) {
|
|
117
|
+
diagnostics.push({
|
|
118
|
+
code: "SERVICE_VERSION_MISMATCH", severity: "error", package: managedPackage.packageName,
|
|
119
|
+
unitName: managedPackage.spec.name,
|
|
120
|
+
message: `${managedPackage.spec.name} resolves to ${managedPackage.spec.version}, but Armada declares ${vehicle.version}; reconcile this package's service through Packed, then verify readiness; session reload alone cannot adopt a daemon`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (vehicle.descriptorDrift === true) {
|
|
124
|
+
diagnostics.push({
|
|
125
|
+
code: "SERVICE_DESCRIPTOR_DRIFT", severity: "error", package: managedPackage.packageName,
|
|
126
|
+
unitName: managedPackage.spec.name,
|
|
127
|
+
message: `${managedPackage.spec.name}'s native descriptor differs from Armada's declaration; reconcile the service through Armada`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
103
130
|
const executable = vehicle.executable ?? managedPackage.spec.binPath;
|
|
104
131
|
if (!deps.fileExists(executable)) {
|
|
105
132
|
diagnostics.push({
|
|
@@ -30,6 +30,7 @@ import { createHash } from "node:crypto";
|
|
|
30
30
|
import { readdirSync, readFileSync } from "node:fs";
|
|
31
31
|
import { join } from "node:path";
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
|
+
import { satisfies, validRange } from "semver";
|
|
33
34
|
import { createVehicleRegistrar, type VehicleRegistrar, type VehicleRegistrationOutcome, type VehicleSpec } from "@danypops/armada";
|
|
34
35
|
import { resolveDaemonPaths } from "@danypops/vehicle-server/paths";
|
|
35
36
|
import {
|
|
@@ -208,12 +209,18 @@ function resolveDependencyNameCandidate(
|
|
|
208
209
|
packageDir: string,
|
|
209
210
|
depName: string,
|
|
210
211
|
hoistedNodeModulesDir: string | undefined,
|
|
212
|
+
requiredRange: string,
|
|
211
213
|
): ResolvedDaemonEntrypoint | undefined {
|
|
212
214
|
const candidateDirs = [join(packageDir, "node_modules", depName), ...(hoistedNodeModulesDir ? [join(hoistedNodeModulesDir, depName)] : [])];
|
|
213
|
-
const
|
|
215
|
+
const installed = candidateDirs
|
|
214
216
|
.map((depDir) => resolveDependencyCandidate(depDir, depName))
|
|
215
217
|
.filter((entry): entry is ResolvedDaemonEntrypoint => entry !== undefined);
|
|
216
|
-
if (
|
|
218
|
+
if (installed.length === 0) return undefined;
|
|
219
|
+
if (!validRange(requiredRange)) throw new Error(`cannot verify daemon requirement ${depName}@${requiredRange}`);
|
|
220
|
+
const resolved = installed.filter((entry) => satisfies(entry.version, requiredRange));
|
|
221
|
+
if (resolved.length === 0) {
|
|
222
|
+
throw new Error(`daemon ${depName} requires ${requiredRange}; installed candidates: ${installed.map((entry) => entry.version).join(", ")}`);
|
|
223
|
+
}
|
|
217
224
|
if (resolved.length === 1) return resolved[0];
|
|
218
225
|
// Nested and hoisted both resolved for the same dependency (the real jittor incident:
|
|
219
226
|
// a stale nested copy always won just because it's checked first). Prefer the newer
|
|
@@ -255,8 +262,8 @@ export function detectVehicleDaemonService(
|
|
|
255
262
|
const byName = unscopedName(own.name ?? fallbackName);
|
|
256
263
|
const wantedNamesake = namesakeDaemonName(byName);
|
|
257
264
|
const candidates: ResolvedDaemonEntrypoint[] = [];
|
|
258
|
-
for (const depName of Object.
|
|
259
|
-
const resolved = resolveDependencyNameCandidate(packageDir, depName, hoistedNodeModulesDir);
|
|
265
|
+
for (const [depName, requiredRange] of Object.entries(own.dependencies ?? {})) {
|
|
266
|
+
const resolved = resolveDependencyNameCandidate(packageDir, depName, hoistedNodeModulesDir, requiredRange);
|
|
260
267
|
if (!resolved) continue;
|
|
261
268
|
if (wantedNamesake !== undefined && resolved.name === wantedNamesake) return resolved;
|
|
262
269
|
candidates.push(resolved);
|
|
@@ -335,8 +342,12 @@ export function resolveDaemonServiceSpec(piHome: string, source: string): Resolv
|
|
|
335
342
|
};
|
|
336
343
|
}
|
|
337
344
|
|
|
338
|
-
|
|
339
|
-
|
|
345
|
+
try {
|
|
346
|
+
const detected = detectVehicleDaemonService(packageDir, packageName, join(piHome, "npm", "node_modules"));
|
|
347
|
+
if (detected) return { ok: true, spec: buildSpec(detected) };
|
|
348
|
+
} catch (error) {
|
|
349
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
350
|
+
}
|
|
340
351
|
|
|
341
352
|
return {
|
|
342
353
|
ok: false,
|
|
@@ -164,7 +164,10 @@ interface MutationResponse {
|
|
|
164
164
|
ok: boolean;
|
|
165
165
|
output: string;
|
|
166
166
|
}
|
|
167
|
-
interface UpdateMutationResponse extends MutationResponse, Partial<Omit<UpdateOutcome, "output">> {
|
|
167
|
+
interface UpdateMutationResponse extends MutationResponse, Partial<Omit<UpdateOutcome, "output">> {
|
|
168
|
+
packageUpdated?: boolean;
|
|
169
|
+
serviceReconciled?: boolean;
|
|
170
|
+
}
|
|
168
171
|
interface UpdateAllMutationResponse extends MutationResponse {
|
|
169
172
|
results: Array<{ source: string; ok: boolean; output: string; serviceReconciled?: boolean } & Partial<Omit<UpdateOutcome, "output">>>;
|
|
170
173
|
reresolveError?: string;
|
|
@@ -598,9 +601,15 @@ export function createApp(deps: Deps): { fetch: (req: Request) => Promise<Respon
|
|
|
598
601
|
const service = await daemonServiceInstaller.restart(piHomeForServiceInstall, restartSource);
|
|
599
602
|
if (!service.ok) {
|
|
600
603
|
if (service.notADaemon) return json({ ok: true, source, ...outcome });
|
|
601
|
-
return json({ ok: false, source, ...outcome,
|
|
604
|
+
return json({ ok: false, source, ...outcome, packageUpdated: !outcome.alreadyUpToDate,
|
|
605
|
+
serviceReconciled: false,
|
|
606
|
+
output: `${outcome.output}\nDaemon adoption required: ${service.reason}. Session reload alone is insufficient.` });
|
|
602
607
|
}
|
|
603
|
-
|
|
608
|
+
if (!service.restarted) return json({
|
|
609
|
+
ok: false, source, ...outcome, packageUpdated: !outcome.alreadyUpToDate, serviceReconciled: false,
|
|
610
|
+
output: `${outcome.output}\nDaemon adoption required for ${service.spec.name}: ${service.reason ?? "reconciliation incomplete"}. Use Packed's targeted install-service operation; session reload alone is insufficient.`,
|
|
611
|
+
});
|
|
612
|
+
return json({ ok: true, source, ...outcome, serviceReconciled: true });
|
|
604
613
|
} catch (error) {
|
|
605
614
|
return json({ ok: false, source, output: error instanceof Error ? error.message : String(error), reloadRequired: false });
|
|
606
615
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
benchmarkArtifact,
|
|
4
|
+
doctorDailyProjection,
|
|
5
|
+
runBenchmarkWorkload,
|
|
6
|
+
PACKED_BENCHMARK_SCENARIOS,
|
|
7
|
+
} from "../benchmarks/baseline.ts";
|
|
8
|
+
|
|
9
|
+
class FakeClient {
|
|
10
|
+
readonly calls: Array<{ operation: string; input: Record<string, unknown> }> = [];
|
|
11
|
+
|
|
12
|
+
async invoke(operation: string, input: Record<string, unknown>): Promise<unknown> {
|
|
13
|
+
this.calls.push({ operation, input });
|
|
14
|
+
if (operation === "package.catalog") return { packages: [{ name: "pi-one" }], sha256: "catalog-digest" };
|
|
15
|
+
if (operation === "package.index") return { packages: [{ name: "pi-one" }], truncated: false };
|
|
16
|
+
if (operation === "package.updates") return { updates: [] };
|
|
17
|
+
if (operation === "package.catalog.sync") return { synced: 12 };
|
|
18
|
+
if (operation === "package.index.build") return { packages: [{ name: "pi-one" }], truncated: false };
|
|
19
|
+
if (operation === "package.updates.project") return { updates: [{ name: "pi-one" }] };
|
|
20
|
+
if (operation === "doctor.run") return { ok: true, scanned: 4, extensions: [], conflicts: [] };
|
|
21
|
+
throw new Error(`unexpected operation ${operation}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("Packed benchmark scenarios", () => {
|
|
26
|
+
it("covers all bounded workload classes", () => {
|
|
27
|
+
expect(PACKED_BENCHMARK_SCENARIOS.map((scenario) => scenario.name)).toEqual([
|
|
28
|
+
"idle-polling",
|
|
29
|
+
"cached-reads-cold",
|
|
30
|
+
"cached-reads-warm",
|
|
31
|
+
"doctor-cold",
|
|
32
|
+
"doctor-warm",
|
|
33
|
+
"doctor-concurrent",
|
|
34
|
+
"catalog-sync-cold",
|
|
35
|
+
"catalog-sync-warm",
|
|
36
|
+
"index-build-cold",
|
|
37
|
+
"index-build-warm",
|
|
38
|
+
"updates-project",
|
|
39
|
+
]);
|
|
40
|
+
for (const scenario of PACKED_BENCHMARK_SCENARIOS) {
|
|
41
|
+
expect(scenario.repetitions).toBeGreaterThan(0);
|
|
42
|
+
expect(scenario.repetitions).toBeLessThanOrEqual(5);
|
|
43
|
+
expect(scenario.concurrency).toBeLessThanOrEqual(2);
|
|
44
|
+
expect(scenario.deadlineMs).toBeLessThanOrEqual(120_000);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("uses bounded inputs and digest output", async () => {
|
|
49
|
+
const client = new FakeClient();
|
|
50
|
+
for (const mode of [
|
|
51
|
+
"cached-reads-warm",
|
|
52
|
+
"doctor-warm",
|
|
53
|
+
"catalog-sync-warm",
|
|
54
|
+
"index-build-warm",
|
|
55
|
+
"updates-project",
|
|
56
|
+
] as const) {
|
|
57
|
+
expect(await runBenchmarkWorkload(mode, client, "/synthetic/project")).toMatch(/^[a-f0-9]{64}$/);
|
|
58
|
+
}
|
|
59
|
+
expect(client.calls.map((call) => call.operation)).toEqual([
|
|
60
|
+
"package.catalog",
|
|
61
|
+
"package.index",
|
|
62
|
+
"package.updates",
|
|
63
|
+
"doctor.run",
|
|
64
|
+
"package.catalog.sync",
|
|
65
|
+
"package.index.build",
|
|
66
|
+
"package.updates.project",
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("projects Doctor cost from complete days", () => {
|
|
71
|
+
expect(doctorDailyProjection({ since: "2026-08-20", until: "2026-09-03", days: 14, calls: 28_000 }, 12, 20)).toEqual({
|
|
72
|
+
status: "available",
|
|
73
|
+
source: "packed metrics.query",
|
|
74
|
+
since: "2026-08-20",
|
|
75
|
+
until: "2026-09-03",
|
|
76
|
+
days: 14,
|
|
77
|
+
calls: 28_000,
|
|
78
|
+
callsPerDay: 2_000,
|
|
79
|
+
projectedDailyCpuMs: 24_000,
|
|
80
|
+
projectedDailyWallMs: 40_000,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("bounds artifacts and marks unavailable counters", () => {
|
|
85
|
+
const artifact = benchmarkArtifact({
|
|
86
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
87
|
+
environment: { platform: "linux", architecture: "x64", runtime: "bun", logicalCpuCount: 8 },
|
|
88
|
+
frequency: { status: "unavailable" },
|
|
89
|
+
results: [{ name: "doctor-cold", benchmark: { workload: { outputDigest: "a".repeat(64) } } }],
|
|
90
|
+
});
|
|
91
|
+
const serialized = JSON.stringify(artifact);
|
|
92
|
+
expect(serialized.length).toBeLessThan(256_000);
|
|
93
|
+
expect(serialized).not.toContain("/home/");
|
|
94
|
+
expect(serialized).not.toContain("settings.json");
|
|
95
|
+
expect(artifact.schemaVersion).toBe(1);
|
|
96
|
+
expect(artifact.networkBytes).toBe("unavailable");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -34,6 +34,30 @@ const base = {
|
|
|
34
34
|
peerDependencies: { "@earendil-works/pi-coding-agent": "*" },
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
describe("syntax-aware dependency discovery", () => {
|
|
38
|
+
it("ignores import-shaped strings while retaining real missing dependencies", async () => {
|
|
39
|
+
const root = fixture(base, { "extensions/index.ts": `
|
|
40
|
+
const from = Math.max(0, Number(url.searchParams.get("from") ?? 0));
|
|
41
|
+
const size = Math.min(20, Math.max(1, Number(url.searchParams.get("size") ?? 5)));
|
|
42
|
+
const text = "import('fake-string')";
|
|
43
|
+
const template = \`require('fake-template')\`;
|
|
44
|
+
// import 'fake-comment';
|
|
45
|
+
import 'missing-side-effect';
|
|
46
|
+
export { value } from 'missing-export';
|
|
47
|
+
import value from 'missing-static';
|
|
48
|
+
const dynamic = import('missing-dynamic');
|
|
49
|
+
const common = require('missing-common');
|
|
50
|
+
import legacy = require('missing-legacy');
|
|
51
|
+
const liveTemplate = \`value: \${require('missing-template-expression')}\`;
|
|
52
|
+
` });
|
|
53
|
+
const report = await checkPackage(root);
|
|
54
|
+
const missing = report.diagnostics.filter((item) => item.code === "RUNTIME_DEPENDENCY_MISSING");
|
|
55
|
+
expect(missing.map((item) => item.message.split(" is imported")[0]).sort()).toEqual([
|
|
56
|
+
"missing-common", "missing-dynamic", "missing-export", "missing-legacy", "missing-side-effect", "missing-static", "missing-template-expression",
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
37
61
|
function codes(diagnostics: Diagnostic[]): string[] {
|
|
38
62
|
return diagnostics.map((diagnostic) => diagnostic.code);
|
|
39
63
|
}
|
|
@@ -44,6 +44,37 @@ function writeRawPackage(dir: string, pkg: Record<string, unknown>): void {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
describe("resolveDaemonServiceSpec", () => {
|
|
47
|
+
it("selects a daemon satisfying the extension range rather than a newer incompatible hoist", () => {
|
|
48
|
+
const home = fakePiHome();
|
|
49
|
+
const extension = join(home, "npm/node_modules/@scope/pi-lector");
|
|
50
|
+
writeRawPackage(extension, { name: "@scope/pi-lector", version: "0.17.2", dependencies: { "@scope/lector": "^0.23.1" } });
|
|
51
|
+
writeRawPackage(join(extension, "node_modules/@scope/lector"), {
|
|
52
|
+
name: "@scope/lector", version: "0.23.1", packed: { daemonService: { binPath: "cli.ts" } },
|
|
53
|
+
});
|
|
54
|
+
writeRawPackage(join(home, "npm/node_modules/@scope/lector"), {
|
|
55
|
+
name: "@scope/lector", version: "0.24.0", packed: { daemonService: { binPath: "cli.ts" } },
|
|
56
|
+
});
|
|
57
|
+
const result = resolveDaemonServiceSpec(home, "npm:@scope/pi-lector");
|
|
58
|
+
expect(result.ok).toBe(true);
|
|
59
|
+
if (!result.ok) throw new Error(result.reason);
|
|
60
|
+
expect(result.spec.version).toBe("0.23.1");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("fails visibly when the only daemon installation violates the extension range", () => {
|
|
64
|
+
const home = fakePiHome();
|
|
65
|
+
writeRawPackage(join(home, "npm/node_modules/@scope/pi-lector"), {
|
|
66
|
+
name: "@scope/pi-lector", version: "0.17.2", dependencies: { "@scope/lector": "^0.23.1" },
|
|
67
|
+
});
|
|
68
|
+
writeRawPackage(join(home, "npm/node_modules/@scope/lector"), {
|
|
69
|
+
name: "@scope/lector", version: "0.21.0", packed: { daemonService: { binPath: "cli.ts" } },
|
|
70
|
+
});
|
|
71
|
+
const result = resolveDaemonServiceSpec(home, "npm:@scope/pi-lector");
|
|
72
|
+
expect(result.ok).toBe(false);
|
|
73
|
+
if (result.ok) throw new Error("incompatible daemon was accepted");
|
|
74
|
+
expect(result.notADaemon).not.toBe(true);
|
|
75
|
+
expect(result.reason).toContain("^0.23.1");
|
|
76
|
+
});
|
|
77
|
+
|
|
47
78
|
it("builds a ServiceSpec from a package's packed.daemonService manifest", () => {
|
|
48
79
|
const piHome = fakePiHome();
|
|
49
80
|
writePackage(piHome, "@danypops/web-spider-daemon", {
|
|
@@ -275,12 +306,10 @@ describe("resolveDaemonServiceSpec", () => {
|
|
|
275
306
|
expect(result.spec.restartSec).toBe(2);
|
|
276
307
|
});
|
|
277
308
|
|
|
278
|
-
it("prefers the higher-semver candidate when
|
|
309
|
+
it("prefers the higher-semver candidate when nested and hoisted copies both satisfy the declared range", () => {
|
|
279
310
|
const piHome = fakePiHome();
|
|
280
311
|
const extDir = join(piHome, "npm", "node_modules", "@danypops/pi-papyrus");
|
|
281
|
-
writeRawPackage(extDir, { name: "@danypops/pi-papyrus", version: "1.0.0", dependencies: { "@danypops/jittor": "
|
|
282
|
-
// A stale nested copy pi-papyrus's own outdated floor forced npm to keep private, instead of
|
|
283
|
-
// deduping to the shared, correctly-versioned, hoisted copy every other consumer already uses.
|
|
312
|
+
writeRawPackage(extDir, { name: "@danypops/pi-papyrus", version: "1.0.0", dependencies: { "@danypops/jittor": ">=0.14.0 <0.19.0" } });
|
|
284
313
|
const nestedDir = join(extDir, "node_modules", "@danypops/jittor");
|
|
285
314
|
writeRawPackage(nestedDir, {
|
|
286
315
|
name: "@danypops/jittor",
|
|
@@ -44,6 +44,47 @@ function fakeDeps(status: unknown, fileExists = true): ServiceDoctorDeps {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
describe("checkServiceUnitPaths", () => {
|
|
47
|
+
it("reports version drift even after the old daemon has restarted", () => {
|
|
48
|
+
const home = piHome(["npm:fakedaemon"]);
|
|
49
|
+
const dir = installDaemonPackage(home, "fakedaemon");
|
|
50
|
+
const report = checkServiceUnitPaths(home, undefined, fakeDeps({ vehicles: [{
|
|
51
|
+
name: "fakedaemon", executable: join(dir, "cli.ts"), version: "0.21.0",
|
|
52
|
+
nativeStatus: "running", ready: true,
|
|
53
|
+
}] }));
|
|
54
|
+
expect(report.ok).toBe(false);
|
|
55
|
+
expect(report.diagnostics).toContainEqual(expect.objectContaining({
|
|
56
|
+
code: "SERVICE_VERSION_MISMATCH", severity: "error",
|
|
57
|
+
message: expect.stringContaining("0.21.0"),
|
|
58
|
+
}));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("accepts an isolated service at the resolved version", () => {
|
|
62
|
+
const home = piHome(["npm:fakedaemon"]);
|
|
63
|
+
installDaemonPackage(home, "fakedaemon");
|
|
64
|
+
const report = checkServiceUnitPaths(home, undefined, fakeDeps({ vehicles: [{
|
|
65
|
+
name: "fakedaemon", executable: "/managed/isolated/cli.ts", version: "1.0.0",
|
|
66
|
+
nativeStatus: "running", ready: true,
|
|
67
|
+
}] }));
|
|
68
|
+
expect(report).toEqual({ ok: true, diagnostics: [], checked: 1 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("reports missing registration rather than claiming a healthy deployment", () => {
|
|
72
|
+
const home = piHome(["npm:fakedaemon"]);
|
|
73
|
+
installDaemonPackage(home, "fakedaemon");
|
|
74
|
+
const report = checkServiceUnitPaths(home, undefined, fakeDeps({ vehicles: [] }));
|
|
75
|
+
expect(report.ok).toBe(false);
|
|
76
|
+
expect(report.diagnostics[0]?.code).toBe("SERVICE_NOT_REGISTERED");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("reports native descriptor drift independently of version equality", () => {
|
|
80
|
+
const home = piHome(["npm:fakedaemon"]);
|
|
81
|
+
installDaemonPackage(home, "fakedaemon");
|
|
82
|
+
const report = checkServiceUnitPaths(home, undefined, fakeDeps({ vehicles: [{
|
|
83
|
+
name: "fakedaemon", executable: "/managed/cli.ts", version: "1.0.0", descriptorDrift: true,
|
|
84
|
+
}] }));
|
|
85
|
+
expect(report.diagnostics).toContainEqual(expect.objectContaining({ code: "SERVICE_DESCRIPTOR_DRIFT" }));
|
|
86
|
+
});
|
|
87
|
+
|
|
47
88
|
it("skips packages with no Armada-managed Vehicle", () => {
|
|
48
89
|
const home = piHome(["npm:not-a-daemon"]);
|
|
49
90
|
const dir = join(home, "npm", "node_modules", "not-a-daemon");
|
|
@@ -531,6 +531,21 @@ describe("service app", () => {
|
|
|
531
531
|
expect(inst.updated).toBe("npm:pi-lsp");
|
|
532
532
|
});
|
|
533
533
|
|
|
534
|
+
it("POST /update reports package success separately from pending daemon adoption", async () => {
|
|
535
|
+
const svc = new FakeDaemonServiceInstaller();
|
|
536
|
+
svc.restarted = false;
|
|
537
|
+
svc.restartReason = "no persistent service is registered for probe";
|
|
538
|
+
const app = createApp(deps({ daemonServiceInstaller: svc }));
|
|
539
|
+
const response = await app.fetch(new Request("http://x/update", {
|
|
540
|
+
method: "POST", headers: { ...auth, "content-type": "application/json" },
|
|
541
|
+
body: JSON.stringify({ source: "npm:probe", approved: true }),
|
|
542
|
+
}));
|
|
543
|
+
expect(await response.json()).toMatchObject({
|
|
544
|
+
ok: false, packageUpdated: true, serviceReconciled: false,
|
|
545
|
+
output: expect.stringContaining("Daemon adoption required"),
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
534
549
|
it("POST /update reconciles an installed Vehicle after a real package change", async () => {
|
|
535
550
|
const svc = new FakeDaemonServiceInstaller();
|
|
536
551
|
const piHome = track(mkdtempSync(join(tmpdir(), "packed-update-vehicle-")));
|