Skip to content

Commit 0bb308e

Browse files
test: prove generated installer lifecycle
1 parent 67b4641 commit 0bb308e

4 files changed

Lines changed: 104 additions & 4 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ node artifact/scripts/plugin-library.mjs stop
9898

9999
The bundled skill also tells agents how to answer skill questions from the read-only API without opening a window, and forbids them from calling `POST /api/apply` — applying to a bot stays a user click.
100100

101-
`npm test` runs the `node --test` suite (front matter, cache dedupe, path containment, apply-status mapping, and an HTTP smoke test against a throwaway cache).
101+
`npm test` builds the generated artifact, then runs the `node --test` suite
102+
(installer lifecycle, front matter, cache dedupe, path containment,
103+
apply-status mapping, and an HTTP smoke test against a throwaway cache).
102104

103105
## Browse API (read-only)
104106

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
"type": "module",
1313
"scripts": {
1414
"build": "agent-bundle build",
15-
"check": "npm run validate && npm run build && npm run validate:artifact && npm run typecheck && npm test && npm run test:packed",
15+
"check": "npm run validate && npm run typecheck && npm test && npm run validate:artifact && npm run test:packed",
1616
"start": "node server.js",
17-
"test": "node --test \"test/**/*.test.js\"",
17+
"test": "npm run build && node --test \"test/**/*.test.js\"",
1818
"test:packed": "node scripts/packed-smoke.mjs",
1919
"typecheck": "tsc -p tsconfig.json --noEmit",
2020
"validate": "agent-bundle validate",

‎scripts/packed-smoke.mjs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ try {
3232
const tarball = path.join(packages, packed[0].filename);
3333
runNpm(["install", "--global", "--prefix", prefix, "--ignore-scripts", tarball]);
3434

35-
const env = { ...process.env, HOME: home };
35+
const env = {
36+
...process.env,
37+
HOME: home,
38+
XDG_STATE_HOME: path.join(home, ".local", "state"),
39+
};
40+
delete env.AGENT_BUNDLE_STATE_ROOT;
3641
const installBin = path.join(prefix, "bin", "plugin-library-install");
3742
const libraryBin = path.join(prefix, "bin", "plugin-library");
3843
execFileSync(installBin, ["install", "cursor"], { env, stdio: "inherit" });

‎test/install.test.js‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"use strict";
2+
3+
const test = require("node:test");
4+
const assert = require("node:assert/strict");
5+
const fs = require("node:fs");
6+
const os = require("node:os");
7+
const path = require("node:path");
8+
const { spawnSync } = require("node:child_process");
9+
10+
const artifactRoot = path.join(__dirname, "..", "artifact");
11+
const installer = path.join(artifactRoot, "install.mjs");
12+
13+
function makeHome() {
14+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "plugin-library-install-"));
15+
fs.mkdirSync(path.join(home, ".cursor"));
16+
return home;
17+
}
18+
19+
function runInstaller(home, args = []) {
20+
const env = {
21+
...process.env,
22+
HOME: home,
23+
XDG_STATE_HOME: path.join(home, ".local", "state"),
24+
};
25+
delete env.AGENT_BUNDLE_STATE_ROOT;
26+
return spawnSync(process.execPath, [installer, ...args], {
27+
encoding: "utf8",
28+
env,
29+
});
30+
}
31+
32+
test("generated installer copies only the artifact and writes a lifecycle receipt", (t) => {
33+
const home = makeHome();
34+
t.after(() => fs.rmSync(home, { force: true, recursive: true }));
35+
36+
const result = runInstaller(home);
37+
assert.equal(result.status, 0, result.stderr);
38+
assert.match(result.stdout, /Installed plugin-library@0\.3\.0/);
39+
40+
const installed = path.join(home, ".cursor", "plugins", "local", "plugin-library");
41+
assert.equal(fs.lstatSync(installed).isSymbolicLink(), false);
42+
assert.equal(fs.existsSync(path.join(installed, ".cursor-plugin", "plugin.json")), true);
43+
assert.equal(fs.existsSync(path.join(installed, "commands", "plugin-library.md")), true);
44+
assert.equal(fs.existsSync(path.join(installed, "skills", "plugin-library", "SKILL.md")), true);
45+
assert.equal(fs.existsSync(path.join(installed, "assets", "data", "unified-catalog.json")), true);
46+
assert.equal(fs.existsSync(path.join(installed, "scripts", "plugin-library.mjs")), true);
47+
48+
const receipt = JSON.parse(
49+
fs.readFileSync(path.join(installed, ".agent-bundle-install.json"), "utf8"),
50+
);
51+
const manifest = JSON.parse(
52+
fs.readFileSync(path.join(artifactRoot, "agent-bundle.manifest.json"), "utf8"),
53+
);
54+
assert.equal(receipt.format, "agent-bundle-install-receipt/2");
55+
assert.equal(receipt.plugin, "plugin-library");
56+
assert.equal(receipt.version, "0.3.0");
57+
assert.equal(receipt.host, "cursor");
58+
assert.equal(receipt.mode, "local");
59+
assert.equal(receipt.scope, "user");
60+
assert.deepEqual(receipt.registrations, [{ kind: "cursor-local-plugin" }]);
61+
assert.deepEqual(
62+
[...receipt.files].sort(),
63+
["agent-bundle.manifest.json", ...manifest.files.map((file) => file.path)].sort(),
64+
);
65+
assert.match(receipt.contentHash, /^[a-f0-9]{64}$/);
66+
});
67+
68+
test("generated installer is idempotent for identical content", (t) => {
69+
const home = makeHome();
70+
t.after(() => fs.rmSync(home, { force: true, recursive: true }));
71+
72+
assert.equal(runInstaller(home).status, 0);
73+
const second = runInstaller(home);
74+
assert.equal(second.status, 0, second.stderr);
75+
assert.match(second.stdout, /Already installed plugin-library@0\.3\.0/);
76+
});
77+
78+
test("generated installer plans and performs receipt-owned uninstall", (t) => {
79+
const home = makeHome();
80+
t.after(() => fs.rmSync(home, { force: true, recursive: true }));
81+
const installed = path.join(home, ".cursor", "plugins", "local", "plugin-library");
82+
83+
assert.equal(runInstaller(home).status, 0);
84+
const plan = runInstaller(home, ["--uninstall", "--plan"]);
85+
assert.equal(plan.status, 0, plan.stderr);
86+
assert.match(plan.stdout, /Would uninstall plugin-library@0\.3\.0/);
87+
assert.equal(fs.existsSync(installed), true);
88+
89+
const uninstall = runInstaller(home, ["--uninstall"]);
90+
assert.equal(uninstall.status, 0, uninstall.stderr);
91+
assert.match(uninstall.stdout, /Uninstalled plugin-library@0\.3\.0/);
92+
assert.equal(fs.existsSync(installed), false);
93+
});

0 commit comments

Comments
 (0)