@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.9.0-dev.202606191950.a2e5513",
3
+ "version": "0.9.0-dev.202606192132.caa886f",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -372,9 +372,7 @@ describe("projectSkillTools", () => {
372
372
  previouslyActiveSkillIds: sessionState,
373
373
  });
374
374
 
375
- // Tool definitions are not sent to the LLM here — tools are invoked via
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 = ["anthropic", "openai", "gemini", "fireworks"] as const;
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("fw-key");
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
- `Git ref to fetch from (default: ${DEFAULT_PLUGIN_REF})`,
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 (name: string, opts: { force?: boolean; ref?: string }) => {
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 result = await installPlugin(
99
- {
100
- name,
101
- force: opts.force ?? false,
102
- ref: opts.ref ?? DEFAULT_PLUGIN_REF,
103
- },
104
- { fetch: globalThis.fetch.bind(globalThis) },
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
+ });