@vellumai/assistant 0.9.0-dev.202606191950.a2e5513 → 0.9.0-dev.202606192132.caa886f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.yaml +68 -0
- package/package.json +1 -1
- package/src/__tests__/conversation-skill-tools.test.ts +1 -3
- package/src/__tests__/secret-routes-platform-proxy.test.ts +16 -2
- package/src/__tests__/skill-tool-factory.test.ts +0 -18
- package/src/cli/commands/plugins.ts +203 -10
- package/src/cli/lib/__tests__/install-from-github.test.ts +31 -0
- package/src/cli/lib/__tests__/plugin-pin-history.test.ts +162 -0
- package/src/cli/lib/install-from-github.ts +25 -4
- package/src/cli/lib/plugin-marketplace.ts +11 -0
- package/src/cli/lib/plugin-pin-history.ts +257 -0
- package/src/daemon/conversation-skill-tools.ts +1 -4
- package/src/daemon/conversation-tool-setup.ts +1 -55
- package/src/daemon/memory-v2-startup.test.ts +131 -0
- package/src/daemon/memory-v2-startup.ts +100 -1
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +3 -223
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +143 -0
- package/src/runtime/routes/plugins-routes.ts +164 -8
- package/src/runtime/routes/secret-routes.ts +5 -0
- package/src/tools/skills/skill-tool-factory.ts +1 -6
- package/src/__tests__/exploration-drift-empty-input-escalation.test.ts +0 -320
- package/src/__tests__/first-class-skill-defs.test.ts +0 -99
package/openapi.yaml
CHANGED
|
@@ -20359,6 +20359,67 @@ paths:
|
|
|
20359
20359
|
install-time baseline cannot be reconstructed.
|
|
20360
20360
|
"503":
|
|
20361
20361
|
description: The plugin source (GitHub) was temporarily unavailable; the upgrade is retryable.
|
|
20362
|
+
/v1/plugins/{name}/versions:
|
|
20363
|
+
get:
|
|
20364
|
+
operationId: plugins_by_name_versions_get
|
|
20365
|
+
summary: List a plugin's reviewed marketplace pins
|
|
20366
|
+
description:
|
|
20367
|
+
Report the distinct marketplace pins a plugin has been promoted to over time, newest first (the first entry
|
|
20368
|
+
is the current pin), capped at `limit` (default 5). The curated `marketplace.json` stores only the current pin,
|
|
20369
|
+
so this is reconstructed from the manifest's own commit history on the default branch — every entry is therefore
|
|
20370
|
+
a reviewed, known-good revision. Pair with `POST /v1/plugins/install`'s `pin` field to roll a plugin back to an
|
|
20371
|
+
older reviewed version. An unknown name (never in the manifest) returns an empty array, not 404.
|
|
20372
|
+
tags:
|
|
20373
|
+
- plugins
|
|
20374
|
+
parameters:
|
|
20375
|
+
- name: name
|
|
20376
|
+
in: path
|
|
20377
|
+
required: true
|
|
20378
|
+
schema:
|
|
20379
|
+
type: string
|
|
20380
|
+
- name: limit
|
|
20381
|
+
in: query
|
|
20382
|
+
required: false
|
|
20383
|
+
schema:
|
|
20384
|
+
type: string
|
|
20385
|
+
description: Maximum number of pins to return (positive integer; default 5).
|
|
20386
|
+
responses:
|
|
20387
|
+
"200":
|
|
20388
|
+
description: Successful response
|
|
20389
|
+
content:
|
|
20390
|
+
application/json:
|
|
20391
|
+
schema:
|
|
20392
|
+
type: array
|
|
20393
|
+
items:
|
|
20394
|
+
type: object
|
|
20395
|
+
properties:
|
|
20396
|
+
pin:
|
|
20397
|
+
type: string
|
|
20398
|
+
description: Plugin commit SHA pinned at this point in marketplace history.
|
|
20399
|
+
marketplaceCommit:
|
|
20400
|
+
type: string
|
|
20401
|
+
description: Marketplace-manifest commit to install this pin from (the newest commit carrying it).
|
|
20402
|
+
promotedAt:
|
|
20403
|
+
anyOf:
|
|
20404
|
+
- type: string
|
|
20405
|
+
- type: "null"
|
|
20406
|
+
description: ISO-8601 committer date (UTC) of the marketplace commit that promoted this pin; null when unreadable.
|
|
20407
|
+
current:
|
|
20408
|
+
type: boolean
|
|
20409
|
+
description: True for the pin currently active on the default branch.
|
|
20410
|
+
required:
|
|
20411
|
+
- pin
|
|
20412
|
+
- marketplaceCommit
|
|
20413
|
+
- promotedAt
|
|
20414
|
+
- current
|
|
20415
|
+
additionalProperties: false
|
|
20416
|
+
description:
|
|
20417
|
+
Distinct marketplace pins a plugin has been promoted to, newest first; the first entry is the current pin.
|
|
20418
|
+
Empty when the plugin has no resolvable history.
|
|
20419
|
+
"400":
|
|
20420
|
+
description: The plugin name failed sanitization, or `limit` was not a positive integer.
|
|
20421
|
+
"503":
|
|
20422
|
+
description: The marketplace pin history could not be read from GitHub (rate-limited or upstream error); retryable.
|
|
20362
20423
|
/v1/plugins/install:
|
|
20363
20424
|
post:
|
|
20364
20425
|
operationId: plugins_install_post
|
|
@@ -20385,6 +20446,13 @@ paths:
|
|
|
20385
20446
|
force:
|
|
20386
20447
|
description: Overwrite an existing install in place. Defaults to false.
|
|
20387
20448
|
type: boolean
|
|
20449
|
+
pin:
|
|
20450
|
+
description:
|
|
20451
|
+
Install a specific reviewed marketplace pin (full commit SHA) instead of the current one. The pin is
|
|
20452
|
+
validated against the plugin's marketplace pin history (`GET /v1/plugins/:name/versions`) and
|
|
20453
|
+
installed from the marketplace revision that introduced it; an unreviewed SHA is rejected. Use this
|
|
20454
|
+
to roll a plugin back to an older reviewed version.
|
|
20455
|
+
type: string
|
|
20388
20456
|
required:
|
|
20389
20457
|
- name
|
|
20390
20458
|
responses:
|
package/package.json
CHANGED
|
@@ -372,9 +372,7 @@ describe("projectSkillTools", () => {
|
|
|
372
372
|
previouslyActiveSkillIds: sessionState,
|
|
373
373
|
});
|
|
374
374
|
|
|
375
|
-
// Tool definitions are
|
|
376
|
-
// skill_execute dispatch; weak-open-model first-class exposure resolves
|
|
377
|
-
// defs from the registry by name in createResolveToolsCallback.
|
|
375
|
+
// Tool definitions are no longer sent to the LLM — tools are invoked via skill_execute dispatch.
|
|
378
376
|
expect(result.toolDefinitions).toEqual([]);
|
|
379
377
|
expect(result.allowedToolNames).toEqual(
|
|
380
378
|
new Set(["deploy_run", "deploy_status"]),
|
|
@@ -12,7 +12,12 @@ let providerRefreshCalls = 0;
|
|
|
12
12
|
const PLATFORM_BASE_URL = "https://platform.example.com";
|
|
13
13
|
const ASSISTANT_API_KEY_PATH = credentialKey("vellum", "assistant_api_key");
|
|
14
14
|
const PLATFORM_BASE_URL_PATH = credentialKey("vellum", "platform_base_url");
|
|
15
|
-
const MANAGED_PROVIDERS = [
|
|
15
|
+
const MANAGED_PROVIDERS = [
|
|
16
|
+
"anthropic",
|
|
17
|
+
"openai",
|
|
18
|
+
"gemini",
|
|
19
|
+
"fireworks",
|
|
20
|
+
] as const;
|
|
16
21
|
|
|
17
22
|
let platformBaseUrlOverride: string | undefined;
|
|
18
23
|
|
|
@@ -116,6 +121,13 @@ mock.module("../util/logger.js", () => ({
|
|
|
116
121
|
}),
|
|
117
122
|
}));
|
|
118
123
|
|
|
124
|
+
// `handleAddSecret` fires this detached when a managed-proxy credential lands —
|
|
125
|
+
// a v2-memory side effect outside this suite's provider-registry scope. Stub it
|
|
126
|
+
// to a no-op; its behavior is covered by memory-v2-startup.test.ts.
|
|
127
|
+
mock.module("../daemon/memory-v2-startup.js", () => ({
|
|
128
|
+
maybeReseedCapabilitiesAfterManagedCredential: async () => {},
|
|
129
|
+
}));
|
|
130
|
+
|
|
119
131
|
import {
|
|
120
132
|
getProviderRoutingSource,
|
|
121
133
|
initializeProviders,
|
|
@@ -199,7 +211,9 @@ describe("secret routes managed proxy registry sync", () => {
|
|
|
199
211
|
test("provider API key writes notify live-conversation refresh listeners", async () => {
|
|
200
212
|
await addApiKey("fireworks", "fw-key");
|
|
201
213
|
|
|
202
|
-
expect(secureKeyStore[credentialKey("fireworks", "api_key")]).toBe(
|
|
214
|
+
expect(secureKeyStore[credentialKey("fireworks", "api_key")]).toBe(
|
|
215
|
+
"fw-key",
|
|
216
|
+
);
|
|
203
217
|
expect(providerRefreshCalls).toBe(1);
|
|
204
218
|
|
|
205
219
|
await deleteApiKey("fireworks");
|
|
@@ -255,24 +255,6 @@ describe("createSkillTool — unknown parameter validation", () => {
|
|
|
255
255
|
expect(result.isError).toBe(false);
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
-
test("strips the harness `activity` field before validation", async () => {
|
|
259
|
-
const hash = computeSkillVersionHash(tempDir);
|
|
260
|
-
const tool = createSkillTool(
|
|
261
|
-
makeEntry({ executor: "echo.ts" }),
|
|
262
|
-
tempDir,
|
|
263
|
-
hash,
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
// A first-class skill-tool call may carry `activity` (the harness progress
|
|
267
|
-
// field). It must not be rejected as an unknown parameter.
|
|
268
|
-
const result = await tool.execute(
|
|
269
|
-
{ query: "hello", activity: "Doing the thing" },
|
|
270
|
-
makeContext(),
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
expect(result.isError).toBe(false);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
258
|
test("allows empty input when schema has no required fields", async () => {
|
|
277
259
|
const hash = computeSkillVersionHash(tempDir);
|
|
278
260
|
const tool = createSkillTool(
|
|
@@ -24,12 +24,20 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
DEFAULT_PLUGIN_REF,
|
|
26
26
|
installPlugin,
|
|
27
|
+
type InstallPluginOptions,
|
|
27
28
|
InvalidPluginNameError,
|
|
28
29
|
PluginAlreadyInstalledError,
|
|
29
30
|
PluginNotFoundError,
|
|
30
31
|
} from "../lib/install-from-github.js";
|
|
31
32
|
import { listInstalledPlugins } from "../lib/list-installed-plugins.js";
|
|
32
33
|
import type { FingerprintComparison } from "../lib/plugin-fingerprint.js";
|
|
34
|
+
import {
|
|
35
|
+
DEFAULT_PIN_HISTORY_LIMIT,
|
|
36
|
+
listPinHistory,
|
|
37
|
+
type PluginPinHistoryEntry,
|
|
38
|
+
PluginPinHistoryError,
|
|
39
|
+
resolvePinToMarketplaceCommit,
|
|
40
|
+
} from "../lib/plugin-pin-history.js";
|
|
33
41
|
import { registerCommand } from "../lib/register-command.js";
|
|
34
42
|
import {
|
|
35
43
|
InvalidSearchPatternError,
|
|
@@ -65,6 +73,9 @@ Examples:
|
|
|
65
73
|
$ assistant plugins install example
|
|
66
74
|
$ assistant plugins install example --force
|
|
67
75
|
$ assistant plugins install example --ref my-feature-branch
|
|
76
|
+
$ assistant plugins versions example
|
|
77
|
+
$ assistant plugins versions example --json
|
|
78
|
+
$ assistant plugins install example --pin <sha> --force
|
|
68
79
|
$ assistant plugins list
|
|
69
80
|
$ assistant plugins list --json
|
|
70
81
|
$ assistant plugins inspect example
|
|
@@ -90,19 +101,35 @@ Examples:
|
|
|
90
101
|
.option("--force", "Overwrite an existing install")
|
|
91
102
|
.option(
|
|
92
103
|
"--ref <ref>",
|
|
93
|
-
`
|
|
104
|
+
`Marketplace manifest revision to read the pin from (default: ${DEFAULT_PLUGIN_REF})`,
|
|
105
|
+
)
|
|
106
|
+
.option(
|
|
107
|
+
"--pin <sha>",
|
|
108
|
+
"Install a specific reviewed marketplace pin (full commit SHA); run `plugins versions <name>` to list them",
|
|
109
|
+
)
|
|
110
|
+
.option(
|
|
111
|
+
"--allow-unreviewed",
|
|
112
|
+
"With --pin, install a SHA that is not in the reviewed marketplace history (advanced; the curated adapter may not match)",
|
|
94
113
|
)
|
|
95
114
|
.action(
|
|
96
|
-
async (
|
|
115
|
+
async (
|
|
116
|
+
name: string,
|
|
117
|
+
opts: {
|
|
118
|
+
force?: boolean;
|
|
119
|
+
ref?: string;
|
|
120
|
+
pin?: string;
|
|
121
|
+
allowUnreviewed?: boolean;
|
|
122
|
+
},
|
|
123
|
+
) => {
|
|
97
124
|
try {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
125
|
+
const installOpts = await resolveInstallOptions(name, opts);
|
|
126
|
+
if (installOpts === null) {
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const result = await installPlugin(installOpts, {
|
|
131
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
132
|
+
});
|
|
106
133
|
log.info(
|
|
107
134
|
{
|
|
108
135
|
name: result.name,
|
|
@@ -131,6 +158,14 @@ Examples:
|
|
|
131
158
|
process.exitCode = 1;
|
|
132
159
|
return;
|
|
133
160
|
}
|
|
161
|
+
if (
|
|
162
|
+
err instanceof InvalidPluginNameError ||
|
|
163
|
+
err instanceof PluginPinHistoryError
|
|
164
|
+
) {
|
|
165
|
+
console.error(err.message);
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
134
169
|
const message = err instanceof Error ? err.message : String(err);
|
|
135
170
|
console.error(`Plugin install failed: ${message}`);
|
|
136
171
|
process.exitCode = 1;
|
|
@@ -138,6 +173,61 @@ Examples:
|
|
|
138
173
|
},
|
|
139
174
|
);
|
|
140
175
|
|
|
176
|
+
plugins
|
|
177
|
+
.command("versions <name>")
|
|
178
|
+
.description(
|
|
179
|
+
"List the recent reviewed marketplace pins for a plugin, newest first. Install an older one with `plugins install <name> --pin <sha>`",
|
|
180
|
+
)
|
|
181
|
+
.option("--json", "Emit machine-readable JSON instead of a table")
|
|
182
|
+
.option(
|
|
183
|
+
"--limit <n>",
|
|
184
|
+
`Maximum number of pins to show (default: ${DEFAULT_PIN_HISTORY_LIMIT})`,
|
|
185
|
+
)
|
|
186
|
+
.action(
|
|
187
|
+
async (name: string, opts: { json?: boolean; limit?: string }) => {
|
|
188
|
+
let limit: number | undefined;
|
|
189
|
+
if (opts.limit !== undefined) {
|
|
190
|
+
limit = Number.parseInt(opts.limit, 10);
|
|
191
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
192
|
+
console.error("--limit must be a positive integer.");
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const history = await listPinHistory(
|
|
199
|
+
name,
|
|
200
|
+
{ fetch: globalThis.fetch.bind(globalThis) },
|
|
201
|
+
limit !== undefined ? { limit } : {},
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (opts.json) {
|
|
205
|
+
process.stdout.write(JSON.stringify(history, null, 2) + "\n");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Logged after the JSON early-return so the logger's stdout
|
|
210
|
+
// writes never corrupt --json output.
|
|
211
|
+
log.info({ name, count: history.length }, "plugin versions");
|
|
212
|
+
for (const line of formatVersions(name, history)) {
|
|
213
|
+
console.log(line);
|
|
214
|
+
}
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (
|
|
217
|
+
err instanceof InvalidPluginNameError ||
|
|
218
|
+
err instanceof PluginPinHistoryError
|
|
219
|
+
) {
|
|
220
|
+
console.error(err.message);
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
225
|
+
console.error(`Plugin versions failed: ${message}`);
|
|
226
|
+
process.exitCode = 1;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
|
|
141
231
|
plugins
|
|
142
232
|
.command("list")
|
|
143
233
|
.description("List plugins installed under <workspaceDir>/plugins/")
|
|
@@ -483,6 +573,109 @@ Examples:
|
|
|
483
573
|
}
|
|
484
574
|
|
|
485
575
|
/** Abbreviate a commit SHA for display; passes through non-SHA / null values. */
|
|
576
|
+
/** Full Git commit SHA — 40 hex (SHA-1) or 64 (SHA-256). */
|
|
577
|
+
const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Resolve `plugins install` flags into {@link InstallPluginOptions}, or `null`
|
|
581
|
+
* when the flag combination is invalid (a message is printed in that case, and
|
|
582
|
+
* the caller exits non-zero). Network / marketplace failures raised while
|
|
583
|
+
* resolving a `--pin` propagate to the caller's catch.
|
|
584
|
+
*
|
|
585
|
+
* `--pin <sha>` installs a specific reviewed marketplace pin: the SHA is looked
|
|
586
|
+
* up in the plugin's pin history and installed from the marketplace commit that
|
|
587
|
+
* introduced it, so the curated adapter stub of that era comes along. With
|
|
588
|
+
* `--allow-unreviewed` the SHA is materialized directly (adapter from `main`),
|
|
589
|
+
* bypassing the history check — the advanced escape hatch.
|
|
590
|
+
*/
|
|
591
|
+
async function resolveInstallOptions(
|
|
592
|
+
name: string,
|
|
593
|
+
opts: {
|
|
594
|
+
force?: boolean;
|
|
595
|
+
ref?: string;
|
|
596
|
+
pin?: string;
|
|
597
|
+
allowUnreviewed?: boolean;
|
|
598
|
+
},
|
|
599
|
+
): Promise<InstallPluginOptions | null> {
|
|
600
|
+
const force = opts.force ?? false;
|
|
601
|
+
|
|
602
|
+
if (!opts.pin) {
|
|
603
|
+
if (opts.allowUnreviewed) {
|
|
604
|
+
console.error("--allow-unreviewed only applies together with --pin.");
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
return { name, force, ref: opts.ref ?? DEFAULT_PLUGIN_REF };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const pin = opts.pin.trim();
|
|
611
|
+
if (!FULL_SHA_RE.test(pin)) {
|
|
612
|
+
console.error(
|
|
613
|
+
`--pin must be a full commit SHA (40 or 64 hex chars); got ${JSON.stringify(opts.pin)}.`,
|
|
614
|
+
);
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
if (opts.ref) {
|
|
618
|
+
console.error(
|
|
619
|
+
"--ref and --pin cannot be combined; --pin already selects the revision.",
|
|
620
|
+
);
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (opts.allowUnreviewed) {
|
|
625
|
+
// Materialize the exact SHA from the plugin's current-manifest repo,
|
|
626
|
+
// bypassing the reviewed-history check. The adapter stub still comes from
|
|
627
|
+
// the default ref, so an adapted plugin may not reproduce faithfully.
|
|
628
|
+
return { name, force, ref: DEFAULT_PLUGIN_REF, commitOverride: pin };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const entry = await resolvePinToMarketplaceCommit(name, pin, {
|
|
632
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
633
|
+
});
|
|
634
|
+
if (!entry) {
|
|
635
|
+
console.error(
|
|
636
|
+
`"${pin}" is not a reviewed marketplace pin for "${name}".\n` +
|
|
637
|
+
`Run \`assistant plugins versions ${name}\` to see available pins, ` +
|
|
638
|
+
"or pass --allow-unreviewed to install it anyway.",
|
|
639
|
+
);
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
return { name, force, ref: entry.marketplaceCommit };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Render a plugin's marketplace-pin history as a table: the pinned commit (short
|
|
647
|
+
* SHA), when it was promoted, and a marker for the pin currently active. An
|
|
648
|
+
* empty history reports that none was found.
|
|
649
|
+
*/
|
|
650
|
+
function formatVersions(
|
|
651
|
+
name: string,
|
|
652
|
+
history: readonly PluginPinHistoryEntry[],
|
|
653
|
+
): string[] {
|
|
654
|
+
if (history.length === 0) {
|
|
655
|
+
return [`No marketplace pin history found for "${name}".`];
|
|
656
|
+
}
|
|
657
|
+
const rows = history.map((entry) => ({
|
|
658
|
+
pin: shortSha(entry.pin),
|
|
659
|
+
promoted: formatTimestamp(entry.promotedAt),
|
|
660
|
+
marker: entry.current ? "(current)" : "",
|
|
661
|
+
}));
|
|
662
|
+
const pinW = Math.max(3, ...rows.map((r) => r.pin.length));
|
|
663
|
+
const promotedW = Math.max(8, ...rows.map((r) => r.promoted.length));
|
|
664
|
+
const pad = (s: string, w: number) => s + " ".repeat(w - s.length);
|
|
665
|
+
const lines = [
|
|
666
|
+
`${pad("PIN", pinW)} ${pad("PROMOTED", promotedW)} `.trimEnd(),
|
|
667
|
+
];
|
|
668
|
+
for (const r of rows) {
|
|
669
|
+
lines.push(
|
|
670
|
+
`${pad(r.pin, pinW)} ${pad(r.promoted, promotedW)} ${r.marker}`.trimEnd(),
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
lines.push("");
|
|
674
|
+
lines.push("Install an older pin with:");
|
|
675
|
+
lines.push(` assistant plugins install ${name} --pin <sha> --force`);
|
|
676
|
+
return lines;
|
|
677
|
+
}
|
|
678
|
+
|
|
486
679
|
function shortSha(sha: string | null): string {
|
|
487
680
|
return sha ? sha.slice(0, 7) : "—";
|
|
488
681
|
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
PluginPostinstallError,
|
|
33
33
|
PluginSourceUnavailableError,
|
|
34
34
|
type PostinstallRunner,
|
|
35
|
+
readInstallMeta,
|
|
35
36
|
sanitizePluginName,
|
|
36
37
|
} from "../install-from-github.js";
|
|
37
38
|
|
|
@@ -292,6 +293,36 @@ describe("installPlugin — install lifecycle", () => {
|
|
|
292
293
|
expect(existsSync(join(target, "README.md"))).toBe(true);
|
|
293
294
|
});
|
|
294
295
|
|
|
296
|
+
test("commitOverride materializes a specific commit, keeping repo from the manifest", async () => {
|
|
297
|
+
// GIVEN a manifest pinning the current SHA, but an override to an older one
|
|
298
|
+
const OLD_SHA = "1".repeat(40);
|
|
299
|
+
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
300
|
+
const calls: string[][] = [];
|
|
301
|
+
const runGit = fakeGitRunner({
|
|
302
|
+
tree: { "package.json": '{"name":"caveman"}' },
|
|
303
|
+
commit: OLD_SHA,
|
|
304
|
+
calls,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// WHEN we install with the commit override
|
|
308
|
+
const result = await installPlugin(
|
|
309
|
+
{ name: "caveman", force: false, ref: "main", commitOverride: OLD_SHA },
|
|
310
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// THEN the clone fetched the override commit, not the manifest's pin
|
|
314
|
+
expect(calls.find((c) => c[0] === "fetch")?.at(-1)).toBe(OLD_SHA);
|
|
315
|
+
expect(result.commit).toBe(OLD_SHA);
|
|
316
|
+
expect(result.ref).toBe(OLD_SHA);
|
|
317
|
+
|
|
318
|
+
// AND provenance records the override as the installed ref while still
|
|
319
|
+
// resolving owner/repo from the manifest entry
|
|
320
|
+
const meta = readInstallMeta(join(pluginsDir, "caveman"));
|
|
321
|
+
expect(meta?.source.ref).toBe(OLD_SHA);
|
|
322
|
+
expect(meta?.source.repo).toBe("caveman");
|
|
323
|
+
expect(meta?.source.owner).toBe("JuliusBrussee");
|
|
324
|
+
});
|
|
325
|
+
|
|
295
326
|
test("--force preserves the existing install when the clone fails", async () => {
|
|
296
327
|
// GIVEN a working copy already on disk
|
|
297
328
|
const target = join(pluginsDir, "caveman");
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for {@link listPinHistory} and {@link resolvePinToMarketplaceCommit}.
|
|
3
|
+
*
|
|
4
|
+
* The marketplace manifest stores only the current pin, so pin history is
|
|
5
|
+
* reconstructed by walking the manifest's commit history and reading the
|
|
6
|
+
* plugin's pin at each commit. The fixtures model that timeline with an
|
|
7
|
+
* in-memory `fetch`: a commits-list endpoint plus a manifest-at-ref endpoint
|
|
8
|
+
* keyed by commit SHA.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import type { FetchLike } from "../install-from-github.js";
|
|
14
|
+
import {
|
|
15
|
+
listPinHistory,
|
|
16
|
+
resolvePinToMarketplaceCommit,
|
|
17
|
+
} from "../plugin-pin-history.js";
|
|
18
|
+
|
|
19
|
+
const PIN_A = "a".repeat(40);
|
|
20
|
+
const PIN_B = "b".repeat(40);
|
|
21
|
+
const PIN_C = "c".repeat(40);
|
|
22
|
+
|
|
23
|
+
// Marketplace-manifest commits, newest → oldest. `C2B` touches the manifest
|
|
24
|
+
// without changing this plugin's pin (a bump for some other plugin), so it must
|
|
25
|
+
// collapse into the same distinct pin as `C3`.
|
|
26
|
+
const C3 = "3".repeat(40);
|
|
27
|
+
const C2B = "d".repeat(40);
|
|
28
|
+
const C2 = "2".repeat(40);
|
|
29
|
+
const C1 = "1".repeat(40);
|
|
30
|
+
|
|
31
|
+
const COMMIT_DATES: Record<string, string> = {
|
|
32
|
+
[C3]: "2026-06-10T00:00:00.000Z",
|
|
33
|
+
[C2B]: "2026-06-08T00:00:00.000Z",
|
|
34
|
+
[C2]: "2026-06-05T00:00:00.000Z",
|
|
35
|
+
[C1]: "2026-06-01T00:00:00.000Z",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Plugin pin resolved from the manifest at each ref (`main` = current tip). */
|
|
39
|
+
const PIN_BY_REF: Record<string, string> = {
|
|
40
|
+
main: PIN_C,
|
|
41
|
+
[C3]: PIN_C,
|
|
42
|
+
[C2B]: PIN_C,
|
|
43
|
+
[C2]: PIN_B,
|
|
44
|
+
[C1]: PIN_A,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function manifest(pin: string): string {
|
|
48
|
+
return JSON.stringify({
|
|
49
|
+
name: "vellum",
|
|
50
|
+
plugins: [
|
|
51
|
+
{
|
|
52
|
+
name: "level-up",
|
|
53
|
+
source: { source: "github", repo: "example-org/level-up", ref: pin },
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build a `fetch` serving the commits list and the manifest at each ref.
|
|
61
|
+
* `commits` is the newest-first commit list the API returns.
|
|
62
|
+
*/
|
|
63
|
+
function makeFetch(commits: string[] = [C3, C2B, C2, C1]): FetchLike {
|
|
64
|
+
return (async (input: RequestInfo | URL) => {
|
|
65
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
66
|
+
if (url.includes("/commits?")) {
|
|
67
|
+
const body = commits.map((sha) => ({
|
|
68
|
+
sha,
|
|
69
|
+
commit: { committer: { date: COMMIT_DATES[sha] } },
|
|
70
|
+
}));
|
|
71
|
+
return new Response(JSON.stringify(body), { status: 200 });
|
|
72
|
+
}
|
|
73
|
+
const m = url.match(/contents\/plugins\/marketplace\.json\?ref=([^&]+)/);
|
|
74
|
+
if (m) {
|
|
75
|
+
const ref = decodeURIComponent(m[1]!);
|
|
76
|
+
const pin = PIN_BY_REF[ref];
|
|
77
|
+
if (!pin) return new Response("not found", { status: 404 });
|
|
78
|
+
return new Response(manifest(pin), { status: 200 });
|
|
79
|
+
}
|
|
80
|
+
return new Response("unexpected url: " + url, { status: 500 });
|
|
81
|
+
}) as FetchLike;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("listPinHistory", () => {
|
|
85
|
+
test("lists distinct pins newest-first, collapsing unchanged revisions", async () => {
|
|
86
|
+
// GIVEN a manifest history with three distinct pins (one commit left the
|
|
87
|
+
// plugin's pin unchanged)
|
|
88
|
+
const history = await listPinHistory("level-up", { fetch: makeFetch() });
|
|
89
|
+
|
|
90
|
+
// THEN one entry per distinct pin, newest first, each tagged with the
|
|
91
|
+
// newest marketplace commit that carries it
|
|
92
|
+
expect(history).toEqual([
|
|
93
|
+
{
|
|
94
|
+
pin: PIN_C,
|
|
95
|
+
marketplaceCommit: C3,
|
|
96
|
+
promotedAt: COMMIT_DATES[C3]!,
|
|
97
|
+
current: true,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
pin: PIN_B,
|
|
101
|
+
marketplaceCommit: C2,
|
|
102
|
+
promotedAt: COMMIT_DATES[C2]!,
|
|
103
|
+
current: false,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
pin: PIN_A,
|
|
107
|
+
marketplaceCommit: C1,
|
|
108
|
+
promotedAt: COMMIT_DATES[C1]!,
|
|
109
|
+
current: false,
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("honors the limit (newest pins win)", async () => {
|
|
115
|
+
const history = await listPinHistory(
|
|
116
|
+
"level-up",
|
|
117
|
+
{ fetch: makeFetch() },
|
|
118
|
+
{ limit: 2 },
|
|
119
|
+
);
|
|
120
|
+
expect(history.map((e) => e.pin)).toEqual([PIN_C, PIN_B]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("returns an empty list for a plugin absent from every revision", async () => {
|
|
124
|
+
const history = await listPinHistory("ghost", { fetch: makeFetch() });
|
|
125
|
+
expect(history).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("returns an empty list when the manifest has no commit history", async () => {
|
|
129
|
+
const history = await listPinHistory("level-up", {
|
|
130
|
+
fetch: makeFetch([]),
|
|
131
|
+
});
|
|
132
|
+
expect(history).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("resolvePinToMarketplaceCommit", () => {
|
|
137
|
+
test("maps a reviewed pin to the marketplace commit that introduced it", async () => {
|
|
138
|
+
const entry = await resolvePinToMarketplaceCommit("level-up", PIN_B, {
|
|
139
|
+
fetch: makeFetch(),
|
|
140
|
+
});
|
|
141
|
+
expect(entry?.marketplaceCommit).toBe(C2);
|
|
142
|
+
expect(entry?.pin).toBe(PIN_B);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("is case-insensitive on the pin SHA", async () => {
|
|
146
|
+
const entry = await resolvePinToMarketplaceCommit(
|
|
147
|
+
"level-up",
|
|
148
|
+
PIN_B.toUpperCase(),
|
|
149
|
+
{ fetch: makeFetch() },
|
|
150
|
+
);
|
|
151
|
+
expect(entry?.marketplaceCommit).toBe(C2);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("returns null for a pin not in the reviewed history", async () => {
|
|
155
|
+
const entry = await resolvePinToMarketplaceCommit(
|
|
156
|
+
"level-up",
|
|
157
|
+
"f".repeat(40),
|
|
158
|
+
{ fetch: makeFetch() },
|
|
159
|
+
);
|
|
160
|
+
expect(entry).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
});
|