Files
edgeever/scripts/verify-acp-adapter-update.mjs
T

163 lines
7.9 KiB
JavaScript

/**
* ACP Adapter Installer Simulated Cross-Version Regression Test
*
* Fast, hermetic regression test that exercises installer state machine
* transitions: staging directories, atomic version migration, manifest persistence,
* rollback protection upon validation failure, and version pruning.
*
* Uses synthetic payloads, placeholder shell scripts, and mocked validation callbacks
* for CI execution without network dependencies or gigabyte-scale downloads.
* For real end-to-end verification with official distribution archives and live ACP
* handshakes on Linux, see scripts/verify-acp-adapter-real-e2e.mjs.
*/
import assert from "node:assert/strict";
import { existsSync, mkdtempSync } from "node:fs";
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { zipSync } from "fflate";
import { createAcpAdapterManager, MAX_ARCHIVE_BYTES, MAX_EXTRACTED_BYTES } from "../apps/desktop/src/main/acp-adapter-manager.mjs";
export async function runAcpAdapterCrossVersionVerification({ silent = false } = {}) {
const log = (...args) => { if (!silent) console.log(...args); };
const root = mkdtempSync(path.join(tmpdir(), "edgeever-acp-update-verify-"));
try {
log("=================================================================");
log("ACP Adapter Installer Simulated Cross-Version Regression Test");
log("=================================================================");
const versionRoot = path.join(root, "antigravity");
const oldVersionDir = path.join(versionRoot, "1.2.1");
const ancientVersionDir = path.join(versionRoot, "1.1.0");
const orphanedStageDir = path.join(versionRoot, ".stage-legacy-orphaned");
await mkdir(oldVersionDir, { recursive: true });
await mkdir(ancientVersionDir, { recursive: true });
await mkdir(orphanedStageDir, { recursive: true });
const oldBinary = path.join(oldVersionDir, "agy_acp_server.par");
const ancientBinary = path.join(ancientVersionDir, "agy_acp_server.par");
await writeFile(oldBinary, "#!/bin/sh\necho old-1.2.1", { mode: 0o700 });
await writeFile(ancientBinary, "#!/bin/sh\necho ancient-1.1.0", { mode: 0o700 });
await writeFile(path.join(root, "installed.json"), JSON.stringify({ antigravity: { version: "1.2.1" } }), { mode: 0o600 });
log("[1/5] Initialized store with legacy Antigravity v1.2.1, older v1.1.0, and orphaned stage.");
const newZipBytes = zipSync({
"agy_acp_server.par": [new TextEncoder().encode("#!/bin/sh\necho new-1.3.0"), { level: 1 }],
localharness_external: [new Uint8Array([42]), { level: 1 }],
});
const registryEntry = {
id: "antigravity-acp",
version: "1.3.0",
distribution: { binary: { "linux-x86_64": {
archive: "https://dl.google.com/agy-extensions/releases/linux/agy-acp-server-1.3.0-linux-x86_64.zip",
cmd: "./agy_acp_server.par",
args: ["--uid="],
} } },
};
let downloadBytesCounted = 0;
const manager = createAcpAdapterManager({
root,
platform: "linux",
arch: "x64",
fetchImpl: async (url) => {
if (url.endsWith(".zip")) {
// Real-world Antigravity v1.3.0 Linux package is ~334 MiB (> 300 MiB old cap)
return new Response(new ReadableStream({
start(controller) {
downloadBytesCounted += newZipBytes.length;
controller.enqueue(newZipBytes);
controller.close();
},
}), {
status: 200,
headers: { "content-length": String(334 * 1024 * 1024) },
});
}
return new Response(JSON.stringify({ agents: [registryEntry] }), { status: 200 });
},
});
// Verify initial detection
const initial = manager.get("antigravity");
assert.equal(initial?.version, "1.2.1", "Initial version must be 1.2.1");
assert.equal(initial?.command.command, oldBinary, "Initial binary path mismatch");
// Phase 2: Failed update simulation
let failedThrew = false;
try {
await manager.install("antigravity", async () => ({
state: "failed",
detail: "simulated_handshake_failure",
}), { updateOnly: true });
} catch (error) {
failedThrew = true;
assert.equal(error?.message, "simulated_handshake_failure");
}
assert.ok(failedThrew, "Staged failure must reject the install promise");
assert.equal(manager.get("antigravity")?.version, "1.2.1", "Failed update must retain v1.2.1");
assert.ok(existsSync(oldBinary), "Old binary must remain intact on failed update");
const stagesAfterFailure = (await readdir(versionRoot)).filter((name) => name.startsWith(".stage-1.3.0"));
assert.equal(stagesAfterFailure.length, 0, "Staged directory must be cleaned up on failure");
log("[2/5] Verified rollback protection: failed update retained v1.2.1 without leaving stage artifacts.");
// Phase 3: Successful cross-version upgrade to 1.3.0
const updateResult = await manager.install("antigravity", async (command) => {
assert.ok(command.command.includes(".stage-1.3.0"), "Validation must inspect staged command");
assert.deepEqual(command.args, ["--uid="]);
return { state: "available", promptCapabilities: { image: true } };
}, { updateOnly: true });
assert.equal(updateResult.updated, true, "Update result must report updated: true");
assert.equal(updateResult.version, "1.3.0", "Updated version must be 1.3.0");
const updated = manager.get("antigravity");
assert.equal(updated?.version, "1.3.0", "Manager must report active version 1.3.0");
const newBinary = path.join(versionRoot, "1.3.0", "agy_acp_server.par");
assert.equal(updated?.command.command, newBinary, "Active binary path must point to 1.3.0");
assert.ok(existsSync(newBinary), "New binary must exist");
assert.ok(existsSync(oldBinary), "Old binary must be preserved alongside new version for fallback");
const manifestState = JSON.parse(await readFile(path.join(root, "installed.json"), "utf8"));
assert.equal(manifestState.antigravity?.version, "1.3.0", "installed.json must record 1.3.0");
log("[3/5] Verified cross-version upgrade to 1.3.0: atomic migration, state persistence, and old version retention.");
// Phase 4: Stage cleanup and version pruning
const orphanedStage2 = path.join(versionRoot, ".stage-uninstalled-adapter");
await mkdir(orphanedStage2, { recursive: true });
await manager.prune();
assert.ok(existsSync(path.join(versionRoot, "1.3.0")), "Current version 1.3.0 must be retained");
assert.ok(existsSync(path.join(versionRoot, "1.2.1")), "Previous fallback version 1.2.1 must be retained");
assert.ok(!existsSync(ancientVersionDir), "Ancient version 1.1.0 must be pruned");
assert.ok(!existsSync(orphanedStageDir), "Orphaned stage dir must be pruned");
assert.ok(!existsSync(orphanedStage2), "Orphaned stage dir from uninstalled state must be pruned");
log("[4/5] Verified prune lifecycle: cleaned older version and orphaned stages while keeping 1.3.0 and 1.2.1.");
// Phase 5: Confirm the configured size limits
assert.equal(MAX_ARCHIVE_BYTES, 500 * 1024 * 1024, "MAX_ARCHIVE_BYTES must be 500 MiB");
assert.equal(MAX_EXTRACTED_BYTES, 2 * 1024 * 1024 * 1024, "MAX_EXTRACTED_BYTES must be 2 GiB");
log("[5/5] Confirmed configured size caps: 500 MiB archive and 2 GiB extracted.");
log("=================================================================");
log("ACP Adapter Installer Simulated Cross-Version Regression: ALL PASSED");
log("=================================================================");
return true;
} finally {
await rm(root, { recursive: true, force: true });
}
}
if (import.meta.main || process.argv[1] === new URL(import.meta.url).pathname) {
runAcpAdapterCrossVersionVerification()
.then(() => process.exit(0))
.catch((error) => {
console.error("[acp-adapter-update-verify] FAILED:", error);
process.exit(1);
});
}