fix(coding-agent): refreshed github plugin lockfile pin on re-install

bun install <spec> respects the existing bun.lock pin when the spec is
unchanged and never re-resolves the remote ref, so re-running
`omp plugin install github:owner/repo` on an already-installed plugin
reported success while silently keeping the user on the original
resolved commit (1ms no-op, no network).

PluginManager.install now follows a git re-install with
`bun update <name>` to force re-resolution of the ref against the
upstream. First-time installs (no prior dep entry) skip the update —
the initial bun install already fetches HEAD. bun update failures
trigger the same rollback path as validation failures.

Fixes #3063
This commit is contained in:
roboomp
2026-06-19 18:22:57 +00:00
parent 259bb004e1
commit 0ada5fe168
4 changed files with 177 additions and 26 deletions
@@ -188,30 +188,42 @@ describe("PluginManager.install load validation", () => {
});
vi.spyOn(Bun, "spawn").mockImplementation(((cmd: string[]) => {
expect(cmd).toEqual(["bun", "install", "github:org/plugin#v2"]);
if (cmd[1] === "install") {
expect(cmd).toEqual(["bun", "install", "github:org/plugin#v2"]);
const prepare = (async () => {
await Bun.write(
pluginsPkgJson,
JSON.stringify(
{ name: "omp-plugins", private: true, dependencies: { "git-plugin": "github:org/plugin#v2" } },
null,
2,
),
);
await writePluginPackage(pluginsNodeModules, "git-plugin", {
version: "2.0.0",
peerDependencies: { "missing-peer": "^1.0.0" },
source:
'import { missing } from "missing-peer";\nexport default function(pi) { pi.registerCommand(String(missing), { handler: async () => {} }); }\n',
});
})();
const prepare = (async () => {
await Bun.write(
pluginsPkgJson,
JSON.stringify(
{ name: "omp-plugins", private: true, dependencies: { "git-plugin": "github:org/plugin#v2" } },
null,
2,
),
);
await writePluginPackage(pluginsNodeModules, "git-plugin", {
version: "2.0.0",
peerDependencies: { "missing-peer": "^1.0.0" },
source:
'import { missing } from "missing-peer";\nexport default function(pi) { pi.registerCommand(String(missing), { handler: async () => {} }); }\n',
});
})();
return {
pid: 1,
stdout: emptyStream(),
stderr: emptyStream(),
exited: prepare.then(() => 0),
} as Subprocess;
}
// The manager follows a git re-install with `bun update <name>` to refresh
// the lockfile pin (#3063). The mock treats it as a no-op exit-0 — the
// on-disk state already reflects the v2 install above.
expect(cmd).toEqual(["bun", "update", "git-plugin"]);
return {
pid: 1,
pid: 2,
stdout: emptyStream(),
stderr: emptyStream(),
exited: prepare.then(() => 0),
exited: Promise.resolve(0),
} as Subprocess;
}) as typeof Bun.spawn);