update a plugin, or all of them, from the marketplace
There was no update path. fetchPlugin REFUSES an existing directory — cloning over one is how uncommitted work disappears — so install was a one-way door, and the answer to "my plugin changed" was uninstall and reinstall, which drops its tables on the way past. An update is a git pull and then the ORDINARY install path. installPlugin already skips the clone when the directory is present and re-runs everything else: os dependencies, schema push, assets, the ecosystem entry, the sidecar, the remount and the frontend rebuild. So there is no second implementation of "apply a plugin" here to drift from the first. Two refusals, both about not destroying work. A dirty working tree is refused rather than stashed or force-pulled — every plugin directory on a developer's machine is a live checkout, and this repository's own are edited in place. Non-fast-forward is refused for the same reason: a diverged local commit is somebody's work and merging it automatically is not the platform's decision. The per-plugin button replaces one that was quietly broken: it appeared only when `outdated` and called `install`, which cannot fetch, so it re-applied stale code. Nothing bumps a version today either, so with everything pinned at 1.0.0 that flag never fires and the button never appeared. Now always offered, and it pulls. Update-all is a plain POST rather than the streaming door: twelve interleaved step streams is not a readable log, and the useful answer is the summary. It runs sequentially — each update pushes schema, republishes assets, rewrites the ecosystem file and rebuilds the frontend, and two of those at once is a race over one file each. One failure does not stop the rest, and failures are named with their reason, because "2 failed" sends someone to the terminal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, test, afterAll } from 'bun:test';
|
||||
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { PLUGINS_DIR } from './discover';
|
||||
import { pullPlugin } from './update';
|
||||
|
||||
// The REFUSALS are what this test is for. A pull that works is easy to believe; a pull that quietly
|
||||
// discarded somebody's uncommitted work is the failure that matters, and every plugin directory on a
|
||||
// developer's machine is a live checkout — this repository's own plugins are edited in place.
|
||||
|
||||
const tmp = (name: string) => join(PLUGINS_DIR, name);
|
||||
const made: string[] = [];
|
||||
const scratch = (name: string) => {
|
||||
const dir = tmp(name);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
made.push(dir);
|
||||
return dir;
|
||||
};
|
||||
|
||||
afterAll(() => made.forEach((d) => rmSync(d, { recursive: true, force: true })));
|
||||
|
||||
describe('pullPlugin refuses rather than guesses', () => {
|
||||
test('a plugin that is not on this machine', async () => {
|
||||
const r = await pullPlugin('definitely-not-installed-xyz');
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.error).toContain('not on this machine');
|
||||
});
|
||||
|
||||
test('a directory that is not a git checkout — copied in by hand, or bundled', async () => {
|
||||
scratch('test-update-nogit');
|
||||
const r = await pullPlugin('test-update-nogit');
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.error).toContain('not a git checkout');
|
||||
// Not phrased as a git error: there is nothing wrong here, there is simply nothing to pull.
|
||||
expect(r.updated).toBe(false);
|
||||
});
|
||||
|
||||
test('a checkout with uncommitted work — the one that would destroy something', async () => {
|
||||
const dir = scratch('test-update-dirty');
|
||||
Bun.spawnSync(['git', 'init', '-q'], { cwd: dir });
|
||||
writeFileSync(join(dir, 'manifest.ts'), 'export const manifest = {};\n');
|
||||
const r = await pullPlugin('test-update-dirty');
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.error).toContain('uncommitted');
|
||||
expect(r.updated).toBe(false);
|
||||
});
|
||||
|
||||
test('a refusal never reports having updated anything', async () => {
|
||||
for (const name of ['definitely-not-installed-xyz', 'test-update-nogit', 'test-update-dirty']) {
|
||||
const r = await pullPlugin(name);
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.updated).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user