Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ node artifact/scripts/plugin-library.mjs stop

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.

`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).
`npm test` builds the generated artifact, then runs the `node --test` suite
(installer lifecycle, front matter, cache dedupe, path containment,
apply-status mapping, and an HTTP smoke test against a throwaway cache).

## Browse API (read-only)

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"type": "module",
"scripts": {
"build": "agent-bundle build",
"check": "npm run validate && npm run build && npm run validate:artifact && npm run typecheck && npm test && npm run test:packed",
"check": "npm run validate && npm run typecheck && npm test && npm run validate:artifact && npm run test:packed",
"start": "node server.js",
"test": "node --test \"test/**/*.test.js\"",
"test": "npm run build && node --test \"test/**/*.test.js\"",
"test:packed": "node scripts/packed-smoke.mjs",
"typecheck": "tsc -p tsconfig.json --noEmit",
"validate": "agent-bundle validate",
Expand Down
7 changes: 6 additions & 1 deletion scripts/packed-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ try {
const tarball = path.join(packages, packed[0].filename);
runNpm(["install", "--global", "--prefix", prefix, "--ignore-scripts", tarball]);

const env = { ...process.env, HOME: home };
const env = {
...process.env,
HOME: home,
XDG_STATE_HOME: path.join(home, ".local", "state"),
};
delete env.AGENT_BUNDLE_STATE_ROOT;
const installBin = path.join(prefix, "bin", "plugin-library-install");
const libraryBin = path.join(prefix, "bin", "plugin-library");
execFileSync(installBin, ["install", "cursor"], { env, stdio: "inherit" });
Expand Down
93 changes: 93 additions & 0 deletions test/install.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"use strict";

const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawnSync } = require("node:child_process");

const artifactRoot = path.join(__dirname, "..", "artifact");
const installer = path.join(artifactRoot, "install.mjs");

function makeHome() {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "plugin-library-install-"));
fs.mkdirSync(path.join(home, ".cursor"));
return home;
}

function runInstaller(home, args = []) {
const env = {
...process.env,
HOME: home,
XDG_STATE_HOME: path.join(home, ".local", "state"),
};
delete env.AGENT_BUNDLE_STATE_ROOT;
return spawnSync(process.execPath, [installer, ...args], {
encoding: "utf8",
env,
});
}

test("generated installer copies only the artifact and writes a lifecycle receipt", (t) => {
const home = makeHome();
t.after(() => fs.rmSync(home, { force: true, recursive: true }));

const result = runInstaller(home);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /Installed plugin-library@0\.3\.0/);

const installed = path.join(home, ".cursor", "plugins", "local", "plugin-library");
assert.equal(fs.lstatSync(installed).isSymbolicLink(), false);
assert.equal(fs.existsSync(path.join(installed, ".cursor-plugin", "plugin.json")), true);
assert.equal(fs.existsSync(path.join(installed, "commands", "plugin-library.md")), true);
assert.equal(fs.existsSync(path.join(installed, "skills", "plugin-library", "SKILL.md")), true);
assert.equal(fs.existsSync(path.join(installed, "assets", "data", "unified-catalog.json")), true);
assert.equal(fs.existsSync(path.join(installed, "scripts", "plugin-library.mjs")), true);

const receipt = JSON.parse(
fs.readFileSync(path.join(installed, ".agent-bundle-install.json"), "utf8"),
);
const manifest = JSON.parse(
fs.readFileSync(path.join(artifactRoot, "agent-bundle.manifest.json"), "utf8"),
);
assert.equal(receipt.format, "agent-bundle-install-receipt/2");
assert.equal(receipt.plugin, "plugin-library");
assert.equal(receipt.version, "0.3.0");
assert.equal(receipt.host, "cursor");
assert.equal(receipt.mode, "local");
assert.equal(receipt.scope, "user");
assert.deepEqual(receipt.registrations, [{ kind: "cursor-local-plugin" }]);
assert.deepEqual(
[...receipt.files].sort(),
["agent-bundle.manifest.json", ...manifest.files.map((file) => file.path)].sort(),
);
Comment on lines +61 to +64

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare the actual installed inventory to the manifest

When the generated installer copies an unexpected or stale file but still populates receipt.files from the manifest, this assertion passes because it compares two metadata lists rather than inspecting the installed directory. Since this test is intended to prove that only manifest files are installed, recursively enumerate the installation tree (excluding the receipt itself) and compare that inventory with the manifest.

Useful? React with 👍 / 👎.

assert.match(receipt.contentHash, /^[a-f0-9]{64}$/);
});

test("generated installer is idempotent for identical content", (t) => {
const home = makeHome();
t.after(() => fs.rmSync(home, { force: true, recursive: true }));

assert.equal(runInstaller(home).status, 0);
const second = runInstaller(home);
assert.equal(second.status, 0, second.stderr);
assert.match(second.stdout, /Already installed plugin-library@0\.3\.0/);
});

test("generated installer plans and performs receipt-owned uninstall", (t) => {
const home = makeHome();
t.after(() => fs.rmSync(home, { force: true, recursive: true }));
const installed = path.join(home, ".cursor", "plugins", "local", "plugin-library");

assert.equal(runInstaller(home).status, 0);
const plan = runInstaller(home, ["--uninstall", "--plan"]);
assert.equal(plan.status, 0, plan.stderr);
assert.match(plan.stdout, /Would uninstall plugin-library@0\.3\.0/);
assert.equal(fs.existsSync(installed), true);

const uninstall = runInstaller(home, ["--uninstall"]);
assert.equal(uninstall.status, 0, uninstall.stderr);
assert.match(uninstall.stdout, /Uninstalled plugin-library@0\.3\.0/);
assert.equal(fs.existsSync(installed), false);
});