gentle-pi 2.0.0 → 2.1.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 +3 -12
- package/extensions/gentle-ai.ts +362 -123
- package/lib/gentle-ai-binary.ts +64 -17
- package/lib/native-review-cli.ts +207 -27
- package/lib/review-candidate-view.ts +9 -5
- package/lib/review-integration-v2.ts +5 -1
- package/package.json +1 -1
- package/runtime/gentle-ai-binary.mjs +64 -17
- package/runtime/native-review-cli.mjs +206 -26
- package/runtime/review-integration-v2.mjs +5 -1
- package/scripts/gentle-ai-installer.mjs +454 -71
- package/scripts/test-packed-runner.mjs +1 -1
- package/skills/_shared/review-ledger-contract.md +3 -1
- package/tests/gentle-ai-binary.test.ts +70 -3
- package/tests/gentle-ai-installer.test.ts +543 -7
- package/tests/gentle-ai.test.ts +2 -2
- package/tests/native-review-consent.test.ts +205 -0
- package/tests/native-review-parity-runtime.test.ts +15 -6
- package/tests/native-review-parity.test.ts +228 -53
- package/tests/package-manifest.test.ts +10 -3
- package/tests/review-candidate-view.test.ts +43 -0
- package/tests/review-controller-lock-status.test.ts +25 -7
- package/tests/review-controller-native-routing.test.ts +419 -20
- package/tests/review-controller-workspace-root.test.ts +40 -10
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-integration-v2.test.ts +10 -1
- package/tests/runtime-harness.mjs +24 -12
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
5
5
|
import { PassThrough } from "node:stream";
|
|
6
|
-
import { chmod, mkdtemp, mkdir, readFile, readdir, stat, symlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import { chmod, copyFile, mkdtemp, mkdir, readFile, readdir, rename, stat, symlink, writeFile } from "node:fs/promises";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import test from "node:test";
|
|
@@ -11,6 +11,7 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
import {
|
|
12
12
|
GENTLE_AI_PENDING_DIGEST,
|
|
13
13
|
GENTLE_AI_RELEASE_ASSETS,
|
|
14
|
+
GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
14
15
|
downloadGentleAiAsset,
|
|
15
16
|
installGentleAi,
|
|
16
17
|
resolveGentleAiInstallerPackageRoot,
|
|
@@ -69,13 +70,548 @@ test("release digests are all-or-none and install fails closed while any digest
|
|
|
69
70
|
}
|
|
70
71
|
});
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
interface WindowsGoCall {
|
|
74
|
+
file: string;
|
|
75
|
+
arguments_: string[];
|
|
76
|
+
options: {
|
|
77
|
+
env?: NodeJS.ProcessEnv;
|
|
78
|
+
shell?: boolean;
|
|
79
|
+
timeout?: number;
|
|
80
|
+
maxBuffer?: number;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface WindowsGoFixtureOptions {
|
|
85
|
+
goVersion?: string;
|
|
86
|
+
goVersionError?: Error;
|
|
87
|
+
reportedVersion?: string;
|
|
88
|
+
installError?: Error;
|
|
89
|
+
goArchitecture?: "amd64" | "arm64";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
|
|
93
|
+
const calls: WindowsGoCall[] = [];
|
|
94
|
+
let goExecutable = "go";
|
|
95
|
+
const metadata = [
|
|
96
|
+
"gentle-ai.exe: go1.25.10",
|
|
97
|
+
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
|
|
98
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.2.2\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
99
|
+
"\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
|
|
100
|
+
].join("\n");
|
|
101
|
+
const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
|
|
102
|
+
calls.push({ file, arguments_, options });
|
|
103
|
+
if (file === goExecutable && arguments_.length === 1 && arguments_[0] === "version") {
|
|
104
|
+
if (fixtureOptions.goVersionError) throw fixtureOptions.goVersionError;
|
|
105
|
+
return { stdout: fixtureOptions.goVersion ?? "go version go1.25.10 windows/amd64\n", stderr: "" };
|
|
106
|
+
}
|
|
107
|
+
if (file === goExecutable && arguments_[0] === "install") {
|
|
108
|
+
if (fixtureOptions.installError) throw fixtureOptions.installError;
|
|
109
|
+
const gobin = options.env?.GOBIN;
|
|
110
|
+
assert.ok(gobin, "go install must receive an explicit staging GOBIN");
|
|
111
|
+
await mkdir(gobin, { recursive: true });
|
|
112
|
+
await writeFile(join(gobin, "gentle-ai.exe"), "trusted Windows source build");
|
|
113
|
+
return { stdout: "", stderr: "" };
|
|
114
|
+
}
|
|
115
|
+
if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
116
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 2.2.2\n", stderr: "" };
|
|
117
|
+
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
118
|
+
};
|
|
119
|
+
return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test("win32 x64 and arm64 install the exact Go SumDB source tag without archive lookup", async () => {
|
|
123
|
+
for (const [arch, goArchitecture] of [["x64", "amd64"], ["arm64", "arm64"]] as const) {
|
|
124
|
+
const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-windows-${arch}-`));
|
|
125
|
+
const fixture = windowsGoFixture({ goArchitecture });
|
|
126
|
+
const goPath = join(packageRoot, "go.exe");
|
|
127
|
+
await writeFile(goPath, "trusted local Go executable");
|
|
128
|
+
fixture.setGoExecutable(goPath);
|
|
129
|
+
const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
|
|
130
|
+
assert.equal(result.installed, true);
|
|
131
|
+
assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [
|
|
132
|
+
["version"], ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.2.2"], ["version", "-m"],
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("Windows source install reports missing or too-old Go without publishing a binary", async () => {
|
|
138
|
+
for (const fixtureOptions of [
|
|
139
|
+
{ goVersion: "go version go1.25.9 windows/amd64\n", expectedCode: "GENTLE_AI_GO_TOOLCHAIN_TOO_OLD" },
|
|
140
|
+
{ goVersionError: Object.assign(new Error("go was not found"), { code: "ENOENT" }), expectedCode: "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE" },
|
|
141
|
+
] as const) {
|
|
142
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-go-"));
|
|
143
|
+
const fixture = windowsGoFixture(fixtureOptions);
|
|
144
|
+
const goPath = join(packageRoot, "go.exe");
|
|
145
|
+
await writeFile(goPath, "trusted local Go executable");
|
|
146
|
+
fixture.setGoExecutable(goPath);
|
|
147
|
+
await assert.rejects(
|
|
148
|
+
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
149
|
+
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
150
|
+
);
|
|
151
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai.exe")), false);
|
|
152
|
+
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("Windows source install cleans staging after Go failure or wrong built version", async () => {
|
|
157
|
+
for (const fixtureOptions of [
|
|
158
|
+
{ installError: Object.assign(new Error("go install timed out"), { code: "ETIMEDOUT" }), expectedCode: "GENTLE_AI_GO_INSTALL_FAILED" },
|
|
159
|
+
{ reportedVersion: "gentle-ai 2.2.1\n", expectedCode: "GENTLE_AI_VERSION_MISMATCH" },
|
|
160
|
+
] as const) {
|
|
161
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-cleanup-"));
|
|
162
|
+
const fixture = windowsGoFixture(fixtureOptions);
|
|
163
|
+
const goPath = join(packageRoot, "go.exe");
|
|
164
|
+
await writeFile(goPath, "trusted local Go executable");
|
|
165
|
+
fixture.setGoExecutable(goPath);
|
|
166
|
+
await assert.rejects(
|
|
167
|
+
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
168
|
+
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
169
|
+
);
|
|
170
|
+
const runtimeDirectory = join(packageRoot, ".gentle-ai", "v2.2.2");
|
|
171
|
+
assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
|
|
172
|
+
assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
|
|
173
|
+
assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("Windows source installs reuse only a fully verified package-local binary", async () => {
|
|
178
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-reuse-"));
|
|
179
|
+
const goPath = join(packageRoot, "go.exe");
|
|
180
|
+
await writeFile(goPath, "trusted local Go executable");
|
|
181
|
+
const initial = windowsGoFixture();
|
|
182
|
+
initial.setGoExecutable(goPath);
|
|
183
|
+
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: async () => goPath });
|
|
184
|
+
|
|
185
|
+
const reuse = windowsGoFixture();
|
|
186
|
+
reuse.setGoExecutable(goPath);
|
|
187
|
+
const reused = await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: async () => goPath });
|
|
188
|
+
assert.equal(reused.installed, false);
|
|
189
|
+
assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
|
|
190
|
+
|
|
191
|
+
await writeFile(join(packageRoot, ".gentle-ai", "v2.2.2", "integrity.json"), "{}\n");
|
|
192
|
+
const repaired = windowsGoFixture();
|
|
193
|
+
repaired.setGoExecutable(goPath);
|
|
194
|
+
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
|
|
195
|
+
assert.ok(repaired.calls.some((call) => call.arguments_[0] === "install"));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
interface HardenedGoCall {
|
|
199
|
+
file: string;
|
|
200
|
+
arguments_: string[];
|
|
201
|
+
options: WindowsGoCall["options"] & { cwd?: string };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const GO_ARCHITECTURE = {
|
|
205
|
+
AMD64: "amd64",
|
|
206
|
+
ARM64: "arm64",
|
|
207
|
+
} as const;
|
|
208
|
+
|
|
209
|
+
type GoArchitecture = (typeof GO_ARCHITECTURE)[keyof typeof GO_ARCHITECTURE];
|
|
210
|
+
|
|
211
|
+
interface HardenedGoFixtureOptions {
|
|
212
|
+
architecture?: GoArchitecture;
|
|
213
|
+
blockInstall?: boolean;
|
|
214
|
+
metadataOverride?: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: HardenedGoFixtureOptions = {}) {
|
|
218
|
+
const goDirectory = join(packageRoot, "trusted-go");
|
|
219
|
+
const goPath = join(goDirectory, "go.exe");
|
|
220
|
+
await mkdir(goDirectory, { recursive: true });
|
|
221
|
+
await writeFile(goPath, "trusted local Go executable");
|
|
222
|
+
const calls: HardenedGoCall[] = [];
|
|
223
|
+
let resolveCalls = 0;
|
|
224
|
+
let signalInstallStarted: (() => void) | undefined;
|
|
225
|
+
const installStarted = new Promise<void>((resolve) => { signalInstallStarted = resolve; });
|
|
226
|
+
let releaseInstall: (() => void) | undefined;
|
|
227
|
+
const installGate = fixtureOptions.blockInstall
|
|
228
|
+
? new Promise<void>((resolve) => { releaseInstall = resolve; })
|
|
229
|
+
: undefined;
|
|
230
|
+
const metadata = fixtureOptions.metadataOverride ?? [
|
|
231
|
+
"gentle-ai.exe: go1.25.10",
|
|
232
|
+
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
|
|
233
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.2.2\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
234
|
+
"\tbuild\t-buildmode=exe",
|
|
235
|
+
"\tbuild\t-compiler=gc",
|
|
236
|
+
"\tbuild\tCGO_ENABLED=0",
|
|
237
|
+
`\tbuild\tGOARCH=${fixtureOptions.architecture ?? "amd64"}`,
|
|
238
|
+
"\tbuild\tGOOS=windows",
|
|
239
|
+
].join("\n");
|
|
240
|
+
const run = async (file: string, arguments_: string[], options: HardenedGoCall["options"]) => {
|
|
241
|
+
calls.push({ file, arguments_, options });
|
|
242
|
+
if (file === goPath && arguments_.length === 1 && arguments_[0] === "version") return { stdout: "go version go1.25.10 windows/amd64\n", stderr: "" };
|
|
243
|
+
if (file === goPath && arguments_[0] === "install") {
|
|
244
|
+
signalInstallStarted?.();
|
|
245
|
+
await installGate;
|
|
246
|
+
const gobin = options.env?.GOBIN;
|
|
247
|
+
assert.ok(gobin);
|
|
248
|
+
await mkdir(gobin, { recursive: true });
|
|
249
|
+
await writeFile(join(gobin, "gentle-ai.exe"), "trusted Windows source build");
|
|
250
|
+
return { stdout: "", stderr: "" };
|
|
251
|
+
}
|
|
252
|
+
if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
253
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 2.2.2\n", stderr: "" };
|
|
254
|
+
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
255
|
+
};
|
|
256
|
+
return {
|
|
257
|
+
calls,
|
|
258
|
+
goPath,
|
|
259
|
+
resolveGoExecutable: async () => { resolveCalls += 1; return goPath; },
|
|
260
|
+
resolveCalls: () => resolveCalls,
|
|
261
|
+
waitForInstall: () => installStarted,
|
|
262
|
+
releaseInstall: () => releaseInstall?.(),
|
|
263
|
+
run,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
test("Windows source installation resolves one validated Go executable and seals hostile Go environment", async () => {
|
|
268
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-hardened-go-"));
|
|
269
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
270
|
+
await installGentleAi({
|
|
271
|
+
packageRoot,
|
|
272
|
+
platform: "win32",
|
|
273
|
+
arch: "x64",
|
|
274
|
+
execFile: fixture.run,
|
|
275
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
276
|
+
env: {
|
|
277
|
+
GOENV: "C:\\attacker\\goenv",
|
|
278
|
+
GOFLAGS: "-mod=mod",
|
|
279
|
+
GOWORK: "C:\\attacker\\go.work",
|
|
280
|
+
GOPROXY: "http://attacker.invalid",
|
|
281
|
+
GOBIN: "C:\\attacker\\bin",
|
|
282
|
+
GOPATH: "C:\\attacker\\path",
|
|
283
|
+
GOMODCACHE: "C:\\attacker\\mods",
|
|
284
|
+
GOCACHE: "C:\\attacker\\cache",
|
|
285
|
+
GOOS: "linux",
|
|
286
|
+
GOARCH: "386",
|
|
287
|
+
PATH: "C:\\attacker",
|
|
288
|
+
UNRELATED_SECRET: "must-not-reach-go",
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
assert.equal(fixture.resolveCalls(), 1);
|
|
292
|
+
const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
|
|
293
|
+
assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
|
|
294
|
+
["version"],
|
|
295
|
+
["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.2.2"],
|
|
296
|
+
["version", "-m"],
|
|
297
|
+
]);
|
|
298
|
+
for (const call of goCalls) {
|
|
299
|
+
assert.equal(call.options.shell, false);
|
|
300
|
+
assert.equal(call.options.env?.GOENV, "off");
|
|
301
|
+
assert.equal(call.options.env?.GOFLAGS, "");
|
|
302
|
+
assert.equal(call.options.env?.GOWORK, "off");
|
|
303
|
+
assert.equal(call.options.env?.GOTOOLCHAIN, "local");
|
|
304
|
+
assert.equal(call.options.env?.GOSUMDB, "sum.golang.org");
|
|
305
|
+
assert.equal(call.options.env?.GONOSUMDB, "");
|
|
306
|
+
assert.equal(call.options.env?.GOPRIVATE, "");
|
|
307
|
+
assert.equal(call.options.env?.GONOPROXY, "");
|
|
308
|
+
assert.equal(call.options.env?.GOINSECURE, "");
|
|
309
|
+
assert.equal(call.options.env?.GOPROXY, "https://proxy.golang.org");
|
|
310
|
+
assert.equal(call.options.env?.GOOS, "windows");
|
|
311
|
+
assert.equal(call.options.env?.GOARCH, "amd64");
|
|
312
|
+
assert.equal(call.options.env?.UNRELATED_SECRET, undefined);
|
|
313
|
+
assert.doesNotMatch(call.options.env?.PATH ?? "", /attacker/i);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("Windows source manifest binds verified Go metadata and architecture", async () => {
|
|
318
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
|
|
319
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
|
|
320
|
+
const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
321
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v2.2.2", "integrity.json"), "utf8")) as Record<string, string>;
|
|
322
|
+
assert.equal(result.installed, true);
|
|
323
|
+
assert.deepEqual(manifest, {
|
|
324
|
+
version: "2.2.2",
|
|
325
|
+
method: "go-sumdb-source-build",
|
|
326
|
+
package: "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
|
|
327
|
+
module: "github.com/gentleman-programming/gentle-ai/v2",
|
|
328
|
+
tag: "v2.2.2",
|
|
329
|
+
architecture: "arm64",
|
|
330
|
+
binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
|
|
331
|
+
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
332
|
+
goVersion: "go1.25.10",
|
|
333
|
+
goos: "windows",
|
|
334
|
+
goarch: "arm64",
|
|
335
|
+
buildMode: "exe",
|
|
336
|
+
compiler: "gc",
|
|
337
|
+
cgoEnabled: "0",
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("Windows source installation rejects Go metadata for a different architecture", async () => {
|
|
342
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-architecture-"));
|
|
343
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
|
|
344
|
+
await assert.rejects(
|
|
345
|
+
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
346
|
+
(error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
|
|
347
|
+
);
|
|
348
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai.exe")), false);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("Windows source installation treats a fresh ownerless lock as active", async () => {
|
|
352
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
|
|
353
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
|
|
354
|
+
await mkdir(lockPath, { recursive: true });
|
|
355
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
356
|
+
await assertManualLockRecoveryRequired(packageRoot, fixture);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
|
|
360
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
|
|
361
|
+
const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock", "owner.json");
|
|
362
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
363
|
+
let replacedOwner = false;
|
|
364
|
+
const run = async (...arguments_: Parameters<typeof fixture.run>) => {
|
|
365
|
+
const result = await fixture.run(...arguments_);
|
|
366
|
+
if (!replacedOwner && arguments_[0] !== fixture.goPath && arguments_[1].length === 1 && arguments_[1][0] === "version") {
|
|
367
|
+
replacedOwner = true;
|
|
368
|
+
await writeFile(lockOwnerPath, `${JSON.stringify({ createdAt: Date.now(), nonce: "1".repeat(64) })}\n`);
|
|
369
|
+
}
|
|
370
|
+
return result;
|
|
371
|
+
};
|
|
372
|
+
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
373
|
+
assert.equal(replacedOwner, true);
|
|
374
|
+
assert.equal((await readFile(lockOwnerPath, "utf8")).includes("1".repeat(64)), true);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
|
|
378
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
|
|
379
|
+
await assert.rejects(
|
|
380
|
+
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
|
|
381
|
+
(error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
|
|
382
|
+
);
|
|
383
|
+
assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
|
|
384
|
+
assert.equal(existsSync(lockPath), true);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function tombstonePath(packageRoot: string, nonce: string): string {
|
|
388
|
+
return join(packageRoot, ".gentle-ai", `.v2.2.2.install.tombstone-${nonce}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function tombstones(packageRoot: string): Promise<string[]> {
|
|
392
|
+
const runtimeRoot = join(packageRoot, ".gentle-ai");
|
|
393
|
+
return existsSync(runtimeRoot)
|
|
394
|
+
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v2.2.2.install.tombstone-"))
|
|
395
|
+
: [];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function backupBundlePath(packageRoot: string, nonce: string): string {
|
|
399
|
+
return join(packageRoot, ".gentle-ai", `.v2.2.2.backup-${nonce}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function copyWindowsBundle(source: string, destination: string): Promise<void> {
|
|
403
|
+
await mkdir(destination, { recursive: true });
|
|
404
|
+
await copyFile(join(source, "gentle-ai.exe"), join(destination, "gentle-ai.exe"));
|
|
405
|
+
await copyFile(join(source, "integrity.json"), join(destination, "integrity.json"));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
|
|
409
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
|
|
410
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
|
|
411
|
+
await mkdir(lockPath, { recursive: true });
|
|
412
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
413
|
+
await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("Windows source installation fails closed for a stale owner lock", async () => {
|
|
417
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
|
|
418
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
|
|
419
|
+
await mkdir(lockPath, { recursive: true });
|
|
420
|
+
await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
|
|
421
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
422
|
+
await assertManualLockRecoveryRequired(packageRoot, fixture);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("Windows source release moves a replacement lock to a preserved tombstone", async () => {
|
|
426
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-release-tombstone-"));
|
|
427
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
428
|
+
const replacementNonce = "2".repeat(64);
|
|
429
|
+
await assert.rejects(
|
|
430
|
+
() => installGentleAi({
|
|
431
|
+
packageRoot,
|
|
432
|
+
platform: "win32",
|
|
433
|
+
arch: "x64",
|
|
434
|
+
execFile: fixture.run,
|
|
435
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
436
|
+
beforeInstallLockReleaseRename: async (lockPath: string) => writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: Date.now(), nonce: replacementNonce })}\n`),
|
|
437
|
+
}),
|
|
438
|
+
/package-private installation lock or tombstone exists/,
|
|
439
|
+
);
|
|
440
|
+
const preserved = await tombstones(packageRoot);
|
|
441
|
+
assert.equal(preserved.length, 1);
|
|
442
|
+
const preservedPath = join(packageRoot, ".gentle-ai", preserved[0]);
|
|
443
|
+
assert.equal((await readFile(join(preservedPath, "owner.json"), "utf8")).includes(replacementNonce), true);
|
|
444
|
+
const retry = await hardenedWindowsGoFixture(packageRoot);
|
|
445
|
+
await assert.rejects(
|
|
446
|
+
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: retry.run, resolveGoExecutable: retry.resolveGoExecutable }),
|
|
447
|
+
(error: unknown) => error instanceof Error && error.message.includes(preservedPath),
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("Windows source acquisition fails closed when a tombstone appears after its first scan", async () => {
|
|
452
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-acquire-tombstone-"));
|
|
453
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
454
|
+
const foreignTombstone = tombstonePath(packageRoot, "3".repeat(64));
|
|
455
|
+
await assert.rejects(
|
|
456
|
+
() => installGentleAi({
|
|
457
|
+
packageRoot,
|
|
458
|
+
platform: "win32",
|
|
459
|
+
arch: "x64",
|
|
460
|
+
execFile: fixture.run,
|
|
461
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
462
|
+
afterInstallLockFirstTombstoneScan: async () => {
|
|
463
|
+
await mkdir(foreignTombstone, { recursive: true });
|
|
464
|
+
await writeFile(join(foreignTombstone, "owner.json"), `${JSON.stringify({ createdAt: Date.now(), nonce: "3".repeat(64) })}\n`);
|
|
465
|
+
},
|
|
466
|
+
}),
|
|
467
|
+
(error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
|
|
468
|
+
);
|
|
469
|
+
assert.equal(existsSync(foreignTombstone), true);
|
|
470
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock")), false);
|
|
471
|
+
assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("Windows source release deletes its matching tombstone and allows reuse", async () => {
|
|
475
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
|
|
476
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
477
|
+
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
478
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock")), false);
|
|
479
|
+
assert.deepEqual(await tombstones(packageRoot), []);
|
|
480
|
+
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
481
|
+
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("Windows source recovers a valid backup after a crash between publication renames before any build", async () => {
|
|
485
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
|
|
486
|
+
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
487
|
+
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
488
|
+
const live = join(packageRoot, ".gentle-ai", "v2.2.2");
|
|
489
|
+
const backup = backupBundlePath(packageRoot, "crash");
|
|
490
|
+
await rename(live, backup);
|
|
491
|
+
const recovery = await hardenedWindowsGoFixture(packageRoot);
|
|
492
|
+
let buildAttempted = false;
|
|
493
|
+
const run = async (...arguments_: Parameters<typeof recovery.run>) => {
|
|
494
|
+
if (arguments_[1][0] === "install") { buildAttempted = true; throw new Error("recovery must precede a new build"); }
|
|
495
|
+
return recovery.run(...arguments_);
|
|
496
|
+
};
|
|
497
|
+
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: run, resolveGoExecutable: recovery.resolveGoExecutable })).installed, false);
|
|
498
|
+
assert.equal(existsSync(live), true);
|
|
499
|
+
assert.equal(existsSync(backup), false);
|
|
500
|
+
assert.equal(buildAttempted, false);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("Windows source fails closed for multiple or invalid backup candidates", async () => {
|
|
504
|
+
for (const setup of ["multiple", "invalid"] as const) {
|
|
505
|
+
const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-backup-${setup}-`));
|
|
506
|
+
const first = backupBundlePath(packageRoot, "first");
|
|
507
|
+
await mkdir(first, { recursive: true });
|
|
508
|
+
if (setup === "multiple") await mkdir(backupBundlePath(packageRoot, "second"), { recursive: true });
|
|
509
|
+
else await writeFile(join(first, "not-a-bundle"), "invalid");
|
|
510
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
511
|
+
await assert.rejects(
|
|
512
|
+
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
513
|
+
/bundle recovery.*manual/i,
|
|
514
|
+
);
|
|
515
|
+
assert.equal(existsSync(first), true);
|
|
516
|
+
assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("Windows source cleans one validated backup only when the live bundle is valid", async () => {
|
|
521
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
|
|
522
|
+
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
523
|
+
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
524
|
+
const live = join(packageRoot, ".gentle-ai", "v2.2.2");
|
|
525
|
+
const backup = backupBundlePath(packageRoot, "valid");
|
|
526
|
+
await copyWindowsBundle(live, backup);
|
|
527
|
+
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
528
|
+
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
|
|
529
|
+
assert.equal(existsSync(live), true);
|
|
530
|
+
assert.equal(existsSync(backup), false);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("Windows source preserves a valid backup when the live bundle is invalid", async () => {
|
|
534
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
|
|
535
|
+
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
536
|
+
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
537
|
+
const live = join(packageRoot, ".gentle-ai", "v2.2.2");
|
|
538
|
+
const backup = backupBundlePath(packageRoot, "valid");
|
|
539
|
+
await copyWindowsBundle(live, backup);
|
|
540
|
+
await writeFile(join(live, "integrity.json"), "{}\n");
|
|
541
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
542
|
+
await assert.rejects(
|
|
543
|
+
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
544
|
+
/bundle recovery.*manual/i,
|
|
545
|
+
);
|
|
546
|
+
assert.equal(existsSync(backup), true);
|
|
547
|
+
assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test("Windows concurrent installs fail closed until normal release, then reuse the published bundle", async () => {
|
|
551
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-concurrent-"));
|
|
552
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
|
|
553
|
+
const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
554
|
+
await fixture.waitForInstall();
|
|
555
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
|
|
556
|
+
await assert.rejects(
|
|
557
|
+
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
558
|
+
(error: unknown) => error instanceof Error && error.message.includes(lockPath),
|
|
559
|
+
);
|
|
560
|
+
assert.equal(fixture.calls.filter((call) => call.file === fixture.goPath && call.arguments_[0] === "install").length, 1);
|
|
561
|
+
fixture.releaseInstall();
|
|
562
|
+
assert.equal((await first).installed, true);
|
|
563
|
+
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable })).installed, false);
|
|
564
|
+
assert.equal(fixture.calls.filter((call) => call.file === fixture.goPath && call.arguments_[0] === "install").length, 1);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
|
|
568
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
|
|
569
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", "v2.2.2");
|
|
570
|
+
await mkdir(versionDirectory, { recursive: true });
|
|
571
|
+
await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
|
|
572
|
+
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
573
|
+
await assert.rejects(
|
|
574
|
+
() => installGentleAi({
|
|
575
|
+
packageRoot,
|
|
576
|
+
platform: "win32",
|
|
577
|
+
arch: "x64",
|
|
578
|
+
execFile: fixture.run,
|
|
579
|
+
resolveGoExecutable: fixture.resolveGoExecutable,
|
|
580
|
+
rename: async (from: string, to: string) => {
|
|
581
|
+
if (to === versionDirectory && from.includes(".staging-")) throw new Error("simulated final swap failure");
|
|
582
|
+
await rename(from, to);
|
|
583
|
+
},
|
|
584
|
+
}),
|
|
585
|
+
/simulated final swap failure/,
|
|
586
|
+
);
|
|
587
|
+
assert.equal(await readFile(join(versionDirectory, "old.txt"), "utf8"), "previous bundle");
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("Darwin/Linux signed bundles retain their four-field manifest and reusable compatibility", async () => {
|
|
591
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-signed-compatibility-"));
|
|
592
|
+
const payload = Buffer.from("signed archive fixture");
|
|
593
|
+
const asset = { name: "gentle-ai_2.2.2_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai" };
|
|
594
|
+
const options = {
|
|
595
|
+
packageRoot,
|
|
596
|
+
platform: "linux",
|
|
597
|
+
arch: "x64",
|
|
598
|
+
releaseAssets: { "linux/amd64": asset },
|
|
599
|
+
download: async (_url: string, destination: string) => writeFile(destination, payload),
|
|
600
|
+
extractArchive: async (_archive: string, destination: string) => {
|
|
601
|
+
await mkdir(destination, { recursive: true });
|
|
602
|
+
await writeFile(join(destination, "gentle-ai"), "signed binary");
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
await installGentleAi(options);
|
|
606
|
+
const manifestPath = join(packageRoot, ".gentle-ai", "v2.2.2", "integrity.json");
|
|
607
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
|
|
608
|
+
assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
|
|
609
|
+
assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("Windows archive lookup remains unsupported and names Go SumDB source installation", () => {
|
|
76
613
|
for (const arch of ["x64", "arm64"]) {
|
|
77
|
-
assert.throws(() => resolveGentleAiReleaseAsset("win32", arch), /
|
|
78
|
-
assert.throws(() => resolveGentleAiReleaseAsset("windows", arch), /unsupported Gentle AI platform\/architecture/);
|
|
614
|
+
assert.throws(() => resolveGentleAiReleaseAsset("win32", arch), /Go SumDB source installation/);
|
|
79
615
|
}
|
|
80
616
|
});
|
|
81
617
|
|
package/tests/gentle-ai.test.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
Theme,
|
|
11
11
|
ToolCallEventResult,
|
|
12
12
|
} from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import
|
|
13
|
+
import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
14
14
|
import { GATE_TARGET_KIND } from "../lib/review-publication-gate.ts";
|
|
15
15
|
import {
|
|
16
16
|
REVIEW_MODE,
|
|
@@ -242,7 +242,7 @@ test("runtime lifecycle gates reject fabricated metadata while compound and wrap
|
|
|
242
242
|
registerCommand() {},
|
|
243
243
|
registerTool() {},
|
|
244
244
|
} as unknown as ExtensionAPI;
|
|
245
|
-
|
|
245
|
+
createGentleAiExtension({ nativeReviewCli: null })(pi);
|
|
246
246
|
const toolCall = handlers.get("tool_call");
|
|
247
247
|
assert.equal(typeof toolCall, "function");
|
|
248
248
|
const authority = runtimeAuthority(t);
|