@vellumai/assistant 0.12.4-staging.2 → 0.12.4-staging.4
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/package.json +1 -1
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +32 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +24 -2
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/feature-flag-registry.json +9 -1
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/model-catalog.ts +234 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +2 -26
- package/src/tools/skills/skill-tool-factory.ts +2 -1
package/package.json
CHANGED
|
@@ -446,7 +446,7 @@ describe("OpenAIResponsesProvider explicit prompt caching (GPT-5.6+)", () => {
|
|
|
446
446
|
expect(JSON.stringify(messages)).not.toContain("prompt_cache_breakpoint");
|
|
447
447
|
});
|
|
448
448
|
|
|
449
|
-
test("catalog flags GPT-5.6 and GPT-6
|
|
449
|
+
test("catalog flags GPT-5.6 and GPT-6 direct-openai rows", () => {
|
|
450
450
|
const openai = PROVIDER_CATALOG.find((p) => p.id === "openai");
|
|
451
451
|
const flagged = (openai?.models ?? [])
|
|
452
452
|
.filter((m) => m.supportsPromptCacheBreakpoints)
|
|
@@ -457,6 +457,8 @@ describe("OpenAIResponsesProvider explicit prompt caching (GPT-5.6+)", () => {
|
|
|
457
457
|
"gpt-5.6-sol",
|
|
458
458
|
"gpt-5.6-terra",
|
|
459
459
|
"gpt-6-astra",
|
|
460
|
+
"gpt-6-luna",
|
|
461
|
+
"gpt-6-sol",
|
|
460
462
|
]);
|
|
461
463
|
});
|
|
462
464
|
});
|
|
@@ -135,6 +135,39 @@ describe("resolvePricing", () => {
|
|
|
135
135
|
expect(result.estimatedCostUsd).toBeCloseTo(0.5 + 0.625 + 0.05, 10);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
test("bills GPT-6 Sol at published short-context rates", () => {
|
|
139
|
+
const result = resolvePricingForUsage("openai", "gpt-6-sol", {
|
|
140
|
+
directInputTokens: 50_000,
|
|
141
|
+
outputTokens: 50_000,
|
|
142
|
+
cacheCreationInputTokens: 50_000,
|
|
143
|
+
cacheReadInputTokens: 50_000,
|
|
144
|
+
anthropicCacheCreation: null,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(result.pricingStatus).toBe("priced");
|
|
148
|
+
// 0.05M x $2 direct + 0.05M x $2.5 write + 0.05M x $0.2 read
|
|
149
|
+
// + 0.05M x $10 output.
|
|
150
|
+
expect(result.estimatedCostUsd).toBeCloseTo(0.1 + 0.125 + 0.01 + 0.5, 10);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("bills GPT-6 Luna at the long-context tier above 272k", () => {
|
|
154
|
+
const result = resolvePricingForUsage("openai", "gpt-6-luna", {
|
|
155
|
+
directInputTokens: 500_000,
|
|
156
|
+
outputTokens: 1_000_000,
|
|
157
|
+
cacheCreationInputTokens: 500_000,
|
|
158
|
+
cacheReadInputTokens: 1_000_000,
|
|
159
|
+
anthropicCacheCreation: null,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result.pricingStatus).toBe("priced");
|
|
163
|
+
// Tier rates: 0.5M x $0.2 direct + 0.5M x $0.25 write + 1M x $0.02 read
|
|
164
|
+
// + 1M x $0.75 output.
|
|
165
|
+
expect(result.estimatedCostUsd).toBeCloseTo(
|
|
166
|
+
0.1 + 0.125 + 0.02 + 0.75,
|
|
167
|
+
10,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
138
171
|
test("bills GPT-6 Astra at the long-context tier above 272k", () => {
|
|
139
172
|
const result = resolvePricingForUsage("openai", "gpt-6-astra", {
|
|
140
173
|
directInputTokens: 500_000,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, symlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
4
|
|
|
@@ -781,16 +781,48 @@ describe("skill_load tool", () => {
|
|
|
781
781
|
expect(result.content).toContain("Deploy the application to production");
|
|
782
782
|
expect(result.content).toContain("Rollback to previous version");
|
|
783
783
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
784
|
+
const schemas = [
|
|
785
|
+
...result.content.matchAll(/```json\n([\s\S]*?)\n```/g),
|
|
786
|
+
].map((match) => JSON.parse(match[1]));
|
|
787
|
+
expect(schemas[0].required).toEqual(["environment"]);
|
|
788
|
+
expect(schemas[0].properties.environment).toEqual({
|
|
789
|
+
type: "string",
|
|
790
|
+
description: "Target environment",
|
|
791
|
+
});
|
|
792
|
+
expect(schemas[0].properties.force.type).toBe("boolean");
|
|
793
|
+
expect(schemas[1].required).toEqual(["version"]);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test("discloses the complete annotation mark schema before execution", async () => {
|
|
797
|
+
const manifest = JSON.parse(
|
|
798
|
+
readFileSync(
|
|
799
|
+
join(
|
|
800
|
+
import.meta.dir,
|
|
801
|
+
"../config/bundled-skills/screen-annotation/TOOLS.json",
|
|
802
|
+
),
|
|
803
|
+
"utf-8",
|
|
804
|
+
),
|
|
787
805
|
);
|
|
788
|
-
|
|
789
|
-
"-
|
|
806
|
+
writeSkill(
|
|
807
|
+
"annotation-schema",
|
|
808
|
+
"Annotation Schema",
|
|
809
|
+
"Point at a control",
|
|
810
|
+
"Name the control.",
|
|
790
811
|
);
|
|
791
|
-
|
|
792
|
-
|
|
812
|
+
writeToolsJson("annotation-schema", manifest.tools);
|
|
813
|
+
|
|
814
|
+
const result = await executeSkillLoad(
|
|
815
|
+
{ skill: "annotation-schema" },
|
|
816
|
+
"macos",
|
|
793
817
|
);
|
|
818
|
+
expect(result.isError).toBe(false);
|
|
819
|
+
const schemaMatch = result.content.match(/```json\n([\s\S]*?)\n```/);
|
|
820
|
+
expect(schemaMatch).not.toBeNull();
|
|
821
|
+
const schema = JSON.parse(schemaMatch![1]);
|
|
822
|
+
expect(schema).toEqual(manifest.tools[0].input_schema);
|
|
823
|
+
expect(schema.properties.marks.items.oneOf).toHaveLength(2);
|
|
824
|
+
expect(schema.properties.marks.items.properties.target.minLength).toBe(1);
|
|
825
|
+
expect(schema.properties.marks.items.properties.caption.maxLength).toBe(80);
|
|
794
826
|
});
|
|
795
827
|
|
|
796
828
|
test("skill without TOOLS.json does not include tool schemas section", async () => {
|
|
@@ -862,9 +894,8 @@ describe("skill_load tool", () => {
|
|
|
862
894
|
// The child skill's tool schemas should appear (#### level under ### Tools from …)
|
|
863
895
|
expect(result.content).toContain("#### child_action");
|
|
864
896
|
expect(result.content).toContain("A child tool action");
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
);
|
|
897
|
+
const schemaMatch = result.content.match(/```json\n([\s\S]*?)\n```/);
|
|
898
|
+
expect(JSON.parse(schemaMatch![1]).required).toEqual(["target"]);
|
|
868
899
|
expect(result.content).toContain(
|
|
869
900
|
"Use `skill_execute` to call these tools.",
|
|
870
901
|
);
|
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
6
6
|
|
|
7
|
-
import type
|
|
7
|
+
import { getBundledSkillsDir, type SkillToolEntry } from "../config/skills.js";
|
|
8
8
|
import { RiskLevel } from "../permissions/types.js";
|
|
9
9
|
import { computeSkillVersionHash } from "../skills/version-hash.js";
|
|
10
10
|
import { resolveSubagentRole } from "../subagent/role-resolution.js";
|
|
@@ -261,6 +261,54 @@ describe("createSkillTool — unknown parameter validation", () => {
|
|
|
261
261
|
expect(result.content).toContain('Unknown parameter "bar"');
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
+
test.each([
|
|
265
|
+
{ description: "Filters" },
|
|
266
|
+
{ marks: [{ description: "Filters" }] },
|
|
267
|
+
{ marks: [{}] },
|
|
268
|
+
{ marks: [{ target: 42 }] },
|
|
269
|
+
{ marks: [{ x: 0.1, y: 0.2 }] },
|
|
270
|
+
{ marks: [{ target: "Filters", x: 0.1, y: 0.2, width: 0.3, height: 0.4 }] },
|
|
271
|
+
])(
|
|
272
|
+
"an annotation validation error provides enough schema for one corrected retry: %j",
|
|
273
|
+
async (invalid) => {
|
|
274
|
+
const skillDir = join(getBundledSkillsDir(), "screen-annotation");
|
|
275
|
+
const manifest = JSON.parse(
|
|
276
|
+
readFileSync(join(skillDir, "TOOLS.json"), "utf-8"),
|
|
277
|
+
);
|
|
278
|
+
const entry = manifest.tools.find(
|
|
279
|
+
(tool: SkillToolEntry) => tool.name === "screen_point_at",
|
|
280
|
+
);
|
|
281
|
+
const tool = createSkillTool(
|
|
282
|
+
entry,
|
|
283
|
+
skillDir,
|
|
284
|
+
computeSkillVersionHash(skillDir),
|
|
285
|
+
BUNDLED,
|
|
286
|
+
);
|
|
287
|
+
const calls: unknown[] = [];
|
|
288
|
+
const context = makeContext({
|
|
289
|
+
proxyToolResolver: async (name, input) => {
|
|
290
|
+
calls.push({ name, input });
|
|
291
|
+
return { content: "Drew 1 mark", isError: false };
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const rejected = await tool.execute(invalid, context);
|
|
296
|
+
expect(rejected.isError).toBe(true);
|
|
297
|
+
expect(calls).toHaveLength(0);
|
|
298
|
+
expect(rejected.content).toContain("skill_load");
|
|
299
|
+
const schemaMatch = rejected.content.match(/```json\n([\s\S]*?)\n```/);
|
|
300
|
+
expect(schemaMatch).not.toBeNull();
|
|
301
|
+
expect(JSON.parse(schemaMatch![1])).toEqual(entry.input_schema);
|
|
302
|
+
|
|
303
|
+
const input = {
|
|
304
|
+
marks: [{ target: "Filters", caption: "Click Filters" }],
|
|
305
|
+
};
|
|
306
|
+
const corrected = await tool.execute(input, context);
|
|
307
|
+
expect(corrected.isError).toBe(false);
|
|
308
|
+
expect(calls).toEqual([{ name: "computer_use_point_at", input }]);
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
|
|
264
312
|
test("allows input with only known parameters", async () => {
|
|
265
313
|
const hash = computeSkillVersionHash(tempDir);
|
|
266
314
|
const tool = createSkillTool(
|
|
@@ -471,7 +519,7 @@ describe("createSkillTool — required/type/enum validation", () => {
|
|
|
471
519
|
|
|
472
520
|
expect(result.isError).toBe(true);
|
|
473
521
|
expect(result.content).toContain(
|
|
474
|
-
"auto_open must be a boolean
|
|
522
|
+
"auto_open must be a boolean: pass true or false as a JSON boolean, not a string",
|
|
475
523
|
);
|
|
476
524
|
});
|
|
477
525
|
|
|
@@ -6,6 +6,79 @@ import {
|
|
|
6
6
|
validateInputAgainstSchema,
|
|
7
7
|
} from "../skills/validate-input.js";
|
|
8
8
|
|
|
9
|
+
describe("nested input validation", () => {
|
|
10
|
+
const schema = {
|
|
11
|
+
properties: {
|
|
12
|
+
options: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: { label: { type: "string" } },
|
|
15
|
+
required: ["label"],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
test("reports nested required fields and types with their path", () => {
|
|
21
|
+
expect(
|
|
22
|
+
validateInputAgainstSchema("example", { options: {} }, schema),
|
|
23
|
+
).toEqual({
|
|
24
|
+
ok: false,
|
|
25
|
+
errors: ["options.label is required"],
|
|
26
|
+
});
|
|
27
|
+
expect(
|
|
28
|
+
validateInputAgainstSchema("example", { options: { label: 42 } }, schema),
|
|
29
|
+
).toEqual({
|
|
30
|
+
ok: false,
|
|
31
|
+
errors: ["options.label must be a string"],
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("preserves open nested objects and only rejects explicitly closed extra fields", () => {
|
|
36
|
+
const input = { options: { label: "Example", extra: true } };
|
|
37
|
+
expect(validateInputAgainstSchema("example", input, schema)).toEqual({
|
|
38
|
+
ok: true,
|
|
39
|
+
});
|
|
40
|
+
expect(
|
|
41
|
+
validateInputAgainstSchema("example", input, {
|
|
42
|
+
properties: {
|
|
43
|
+
options: {
|
|
44
|
+
...schema.properties.options,
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
).toEqual({
|
|
50
|
+
ok: false,
|
|
51
|
+
errors: ['Unknown parameter "options.extra". Supported: "label"'],
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("does not treat a richer oneOf as a presence-only union", () => {
|
|
56
|
+
expect(
|
|
57
|
+
validateInputAgainstSchema(
|
|
58
|
+
"example",
|
|
59
|
+
{ options: { label: "Example" } },
|
|
60
|
+
{
|
|
61
|
+
properties: {
|
|
62
|
+
options: {
|
|
63
|
+
...schema.properties.options,
|
|
64
|
+
oneOf: [
|
|
65
|
+
{
|
|
66
|
+
required: ["label"],
|
|
67
|
+
properties: { label: { const: "Example" } },
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
required: ["label"],
|
|
71
|
+
properties: { label: { const: "Other" } },
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
).toEqual({ ok: true });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
9
82
|
// ---------------------------------------------------------------------------
|
|
10
83
|
// required
|
|
11
84
|
// ---------------------------------------------------------------------------
|
|
@@ -66,6 +66,20 @@ let platformInstallCalls: Array<{ name: string; force?: boolean }> = [];
|
|
|
66
66
|
let upgradePluginCalls: UpgradePluginOptions[] = [];
|
|
67
67
|
let catalogMatches: PluginSearchMatch[] = [];
|
|
68
68
|
let catalogError: Error | null = null;
|
|
69
|
+
const CATALOG_COMMIT = "a".repeat(40);
|
|
70
|
+
|
|
71
|
+
function githubCatalogMatch(name: string): PluginSearchMatch {
|
|
72
|
+
return {
|
|
73
|
+
name,
|
|
74
|
+
path: `github:example-org/${name}@${CATALOG_COMMIT}`,
|
|
75
|
+
category: null,
|
|
76
|
+
source: {
|
|
77
|
+
kind: "github",
|
|
78
|
+
repo: `example-org/${name}`,
|
|
79
|
+
ref: CATALOG_COMMIT,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
69
83
|
|
|
70
84
|
/**
|
|
71
85
|
* Queued daemon IPC responses. The default (empty queue) is a transport
|
|
@@ -269,7 +283,10 @@ beforeEach(() => {
|
|
|
269
283
|
installPluginCalls = [];
|
|
270
284
|
platformInstallCalls = [];
|
|
271
285
|
upgradePluginCalls = [];
|
|
272
|
-
catalogMatches = [
|
|
286
|
+
catalogMatches = [
|
|
287
|
+
githubCatalogMatch("example"),
|
|
288
|
+
githubCatalogMatch("imessage"),
|
|
289
|
+
];
|
|
273
290
|
catalogError = null;
|
|
274
291
|
ipcResults = [];
|
|
275
292
|
inspectResult = null;
|
|
@@ -485,6 +502,17 @@ describe("plugins install - declared-schedules consent", () => {
|
|
|
485
502
|
]);
|
|
486
503
|
expect(r.exitCode).toBe(0);
|
|
487
504
|
});
|
|
505
|
+
|
|
506
|
+
test("does not use the outage fallback for a gated integration", async () => {
|
|
507
|
+
catalogError = new Error("catalog unavailable");
|
|
508
|
+
|
|
509
|
+
const r = await runCommand(["plugins", "install", "gamma"]);
|
|
510
|
+
|
|
511
|
+
expect(installPluginCalls).toHaveLength(0);
|
|
512
|
+
expect(platformInstallCalls).toHaveLength(0);
|
|
513
|
+
expect(r.stderr).toContain("not in the marketplace catalog");
|
|
514
|
+
expect(r.exitCode).toBe(1);
|
|
515
|
+
});
|
|
488
516
|
});
|
|
489
517
|
|
|
490
518
|
describe("plugins upgrade - declared-schedules consent (local fallback)", () => {
|
|
@@ -238,7 +238,15 @@ export function registerPluginsCommand(program: Command): void {
|
|
|
238
238
|
const platformEnabled =
|
|
239
239
|
libs.catalogLocal.arePlatformFeaturesEnabled();
|
|
240
240
|
if (platformEnabled) {
|
|
241
|
+
if (!libs.catalogLocal.isPluginCatalogEntryVisible(nameOrUrl)) {
|
|
242
|
+
console.error(
|
|
243
|
+
`Plugin "${nameOrUrl}" is not in the marketplace catalog.`,
|
|
244
|
+
);
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
241
248
|
let match;
|
|
249
|
+
let catalogResolved = false;
|
|
242
250
|
try {
|
|
243
251
|
match = (
|
|
244
252
|
await libs.catalogCache.getPluginCatalog(
|
|
@@ -246,10 +254,18 @@ export function registerPluginsCommand(program: Command): void {
|
|
|
246
254
|
{ fetch: globalThis.fetch.bind(globalThis) },
|
|
247
255
|
)
|
|
248
256
|
).matches.find((candidate) => candidate.name === nameOrUrl);
|
|
257
|
+
catalogResolved = true;
|
|
249
258
|
} catch {
|
|
250
259
|
// The install endpoint remains authoritative and can succeed
|
|
251
260
|
// independently when catalog discovery is unavailable.
|
|
252
261
|
}
|
|
262
|
+
if (!match && catalogResolved) {
|
|
263
|
+
console.error(
|
|
264
|
+
`Plugin "${nameOrUrl}" is not in the marketplace catalog.`,
|
|
265
|
+
);
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
253
269
|
if (match?.source.kind === "local") {
|
|
254
270
|
result = await libs.installGitHub.installPlugin(
|
|
255
271
|
{
|
|
@@ -306,6 +306,35 @@ describe("inspectPlugin", () => {
|
|
|
306
306
|
expect(result.remote?.commit).toBe(SHA_B);
|
|
307
307
|
});
|
|
308
308
|
|
|
309
|
+
test("hides an uninstalled gated integration without fetching the marketplace", async () => {
|
|
310
|
+
let fetchCalls = 0;
|
|
311
|
+
const fetch = (async () => {
|
|
312
|
+
fetchCalls += 1;
|
|
313
|
+
return new Response("unexpected request", { status: 500 });
|
|
314
|
+
}) as FetchLike;
|
|
315
|
+
|
|
316
|
+
await expect(
|
|
317
|
+
inspectPlugin(
|
|
318
|
+
{ name: "gamma" },
|
|
319
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
320
|
+
),
|
|
321
|
+
).rejects.toBeInstanceOf(PluginInspectNotFoundError);
|
|
322
|
+
expect(fetchCalls).toBe(0);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("keeps an installed gated integration inspectable", async () => {
|
|
326
|
+
installPlugin(workspace, "gamma", { sidecar: null });
|
|
327
|
+
const fetch = makeFetch({ marketplace: manifestWith("gamma", SHA_B) });
|
|
328
|
+
|
|
329
|
+
const result = await inspectPlugin(
|
|
330
|
+
{ name: "gamma" },
|
|
331
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
expect(result.installed).toBe(true);
|
|
335
|
+
expect(result.local?.description).toBe("Installed copy.");
|
|
336
|
+
});
|
|
337
|
+
|
|
309
338
|
test("previews an uninstalled bundled provider offline without fetching GitHub", async () => {
|
|
310
339
|
const savedDisablePlatform = process.env.VELLUM_DISABLE_PLATFORM;
|
|
311
340
|
const savedIsPlatform = process.env.IS_PLATFORM;
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import type { FetchLike } from "../fetch-like.js";
|
|
20
20
|
import {
|
|
21
21
|
getPluginCatalog,
|
|
22
|
+
getPluginCatalogForInstalledMetadata,
|
|
22
23
|
invalidatePluginCatalogCache,
|
|
23
24
|
mergePlatformCatalogWithBundledLocals,
|
|
24
25
|
PLUGIN_CATALOG_CACHE_TTL_MS,
|
|
@@ -102,7 +103,7 @@ describe("getPluginCatalog", () => {
|
|
|
102
103
|
|
|
103
104
|
// THEN the platform is fetched exactly once — the second call is cached
|
|
104
105
|
expect(calls()).toBe(1);
|
|
105
|
-
expect(first).
|
|
106
|
+
expect(first).toEqual(second);
|
|
106
107
|
expect(githubNames(first)).toEqual(["a"]);
|
|
107
108
|
expect(first.ref).toBe("main");
|
|
108
109
|
});
|
|
@@ -161,7 +162,10 @@ describe("getPluginCatalog", () => {
|
|
|
161
162
|
delete process.env.IS_PLATFORM;
|
|
162
163
|
|
|
163
164
|
const { fetch, calls } = platformFetch(["ignored"]);
|
|
164
|
-
const deps: SearchPluginsDeps = {
|
|
165
|
+
const deps: SearchPluginsDeps = {
|
|
166
|
+
fetch,
|
|
167
|
+
featureFlagEnabled: () => true,
|
|
168
|
+
};
|
|
165
169
|
|
|
166
170
|
// WHEN we request the catalog
|
|
167
171
|
const result = await getPluginCatalog("main", deps);
|
|
@@ -173,6 +177,32 @@ describe("getPluginCatalog", () => {
|
|
|
173
177
|
// The requested ref is echoed onto the wire contract.
|
|
174
178
|
expect(result.ref).toBe("main");
|
|
175
179
|
});
|
|
180
|
+
|
|
181
|
+
test("applies feature-flag visibility to bundled catalog entries", async () => {
|
|
182
|
+
process.env.VELLUM_DISABLE_PLATFORM = "true";
|
|
183
|
+
delete process.env.IS_PLATFORM;
|
|
184
|
+
|
|
185
|
+
const { fetch } = platformFetch(["ignored"]);
|
|
186
|
+
const result = await getPluginCatalog("main", {
|
|
187
|
+
fetch,
|
|
188
|
+
featureFlagEnabled: () => false,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(result.matches.map((match) => match.name)).not.toContain("gamma");
|
|
192
|
+
expect(result.matches.map((match) => match.name)).toContain("fathom");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("preserves hidden entries for installed-plugin metadata", async () => {
|
|
196
|
+
process.env.VELLUM_DISABLE_PLATFORM = "true";
|
|
197
|
+
delete process.env.IS_PLATFORM;
|
|
198
|
+
|
|
199
|
+
const { fetch } = platformFetch(["ignored"]);
|
|
200
|
+
const result = await getPluginCatalogForInstalledMetadata("main", {
|
|
201
|
+
fetch,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(result.matches.map((match) => match.name)).toContain("gamma");
|
|
205
|
+
});
|
|
176
206
|
});
|
|
177
207
|
|
|
178
208
|
describe("mergePlatformCatalogWithBundledLocals", () => {
|
|
@@ -13,6 +13,7 @@ import bundledManifest from "../bundled-marketplace.json" with { type: "json" };
|
|
|
13
13
|
import {
|
|
14
14
|
buildBundledPluginCatalog,
|
|
15
15
|
readBundledPluginCatalog,
|
|
16
|
+
resolveBundledPluginSource,
|
|
16
17
|
} from "../plugin-catalog-local.js";
|
|
17
18
|
import { marketplaceManifestSchema } from "../plugin-marketplace.js";
|
|
18
19
|
import { marketplaceMatch } from "../search-plugins.js";
|
|
@@ -96,3 +97,17 @@ describe("readBundledPluginCatalog", () => {
|
|
|
96
97
|
expect(readBundledPluginCatalog()).toBe(readBundledPluginCatalog());
|
|
97
98
|
});
|
|
98
99
|
});
|
|
100
|
+
|
|
101
|
+
describe("resolveBundledPluginSource", () => {
|
|
102
|
+
test("hides a QA integration while its feature flag is off", () => {
|
|
103
|
+
expect(resolveBundledPluginSource("gamma", () => false)).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("resolves a QA integration while its feature flag is on", () => {
|
|
107
|
+
expect(resolveBundledPluginSource("gamma", () => true)).toEqual({
|
|
108
|
+
kind: "local",
|
|
109
|
+
path: "plugins/mcp-catalog/gamma",
|
|
110
|
+
version: "1.0.2",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -195,6 +195,27 @@ describe("catalog-backed resolvers (bundled, offline)", () => {
|
|
|
195
195
|
await resolvePluginSourceFromCatalog("no-such-plugin", rejectingDeps),
|
|
196
196
|
).toBeNull();
|
|
197
197
|
});
|
|
198
|
+
|
|
199
|
+
test("does not resolve a QA integration while its feature flag is off", async () => {
|
|
200
|
+
expect(
|
|
201
|
+
await resolvePluginSourceFromCatalog("gamma", {
|
|
202
|
+
...rejectingDeps,
|
|
203
|
+
featureFlagEnabled: () => false,
|
|
204
|
+
}),
|
|
205
|
+
).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("resolves a QA integration while its feature flag is on", async () => {
|
|
209
|
+
expect(
|
|
210
|
+
await resolvePluginSourceFromCatalog("gamma", {
|
|
211
|
+
...rejectingDeps,
|
|
212
|
+
featureFlagEnabled: () => true,
|
|
213
|
+
}),
|
|
214
|
+
).toMatchObject({
|
|
215
|
+
kind: "local",
|
|
216
|
+
path: "plugins/mcp-catalog/gamma",
|
|
217
|
+
});
|
|
218
|
+
});
|
|
198
219
|
});
|
|
199
220
|
|
|
200
221
|
describe("catalog-backed resolvers (platform enabled)", () => {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
filterPluginCatalogByFeatureFlags,
|
|
5
|
+
MCP_CATALOG_QA_INTEGRATIONS_FLAG,
|
|
6
|
+
} from "../plugin-catalog-visibility.js";
|
|
7
|
+
import type { PluginCatalog, PluginSearchMatch } from "../search-plugins.js";
|
|
8
|
+
|
|
9
|
+
function match(name: string): PluginSearchMatch {
|
|
10
|
+
return {
|
|
11
|
+
name,
|
|
12
|
+
path: `local:plugins/mcp-catalog/${name}@1.0.0`,
|
|
13
|
+
category: null,
|
|
14
|
+
source: {
|
|
15
|
+
kind: "local",
|
|
16
|
+
path: `plugins/mcp-catalog/${name}`,
|
|
17
|
+
version: "1.0.0",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function catalog(names: string[]): PluginCatalog {
|
|
23
|
+
return { ref: "main", matches: names.map(match) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("filterPluginCatalogByFeatureFlags", () => {
|
|
27
|
+
test("hides QA integrations while the flag is off", () => {
|
|
28
|
+
const input = catalog([
|
|
29
|
+
"fathom",
|
|
30
|
+
"gamma",
|
|
31
|
+
"guru",
|
|
32
|
+
"intercom",
|
|
33
|
+
"ramp",
|
|
34
|
+
"semrush",
|
|
35
|
+
"typeform",
|
|
36
|
+
"notion",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const result = filterPluginCatalogByFeatureFlags(input, (key) => {
|
|
40
|
+
expect(key).toBe(MCP_CATALOG_QA_INTEGRATIONS_FLAG);
|
|
41
|
+
return false;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(result.matches.map((entry) => entry.name)).toEqual([
|
|
45
|
+
"fathom",
|
|
46
|
+
"notion",
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("returns the full catalog while the flag is on", () => {
|
|
51
|
+
const input = catalog([
|
|
52
|
+
"fathom",
|
|
53
|
+
"gamma",
|
|
54
|
+
"guru",
|
|
55
|
+
"intercom",
|
|
56
|
+
"ramp",
|
|
57
|
+
"semrush",
|
|
58
|
+
"typeform",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const result = filterPluginCatalogByFeatureFlags(input, () => true);
|
|
62
|
+
|
|
63
|
+
expect(result).toBe(input);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -513,6 +513,49 @@ describe("getPluginDetails (bundled catalog, offline)", () => {
|
|
|
513
513
|
expect(details.ref).toBe(historicalRef);
|
|
514
514
|
});
|
|
515
515
|
|
|
516
|
+
test("hides a gated integration at an explicit historical ref", async () => {
|
|
517
|
+
const historicalRef = "a".repeat(40);
|
|
518
|
+
let fetchCalls = 0;
|
|
519
|
+
const fetch = (async () => {
|
|
520
|
+
fetchCalls += 1;
|
|
521
|
+
return new Response("unexpected request", { status: 500 });
|
|
522
|
+
}) as FetchLike;
|
|
523
|
+
|
|
524
|
+
await expect(
|
|
525
|
+
getPluginDetails(
|
|
526
|
+
{ name: "gamma", ref: historicalRef },
|
|
527
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
528
|
+
),
|
|
529
|
+
).rejects.toBeInstanceOf(PluginDetailsNotFoundError);
|
|
530
|
+
expect(fetchCalls).toBe(0);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("keeps an installed gated integration visible at an explicit historical ref", async () => {
|
|
534
|
+
const target = join(workspace, "gamma");
|
|
535
|
+
mkdirSync(target, { recursive: true });
|
|
536
|
+
writeFileSync(join(target, "README.md"), "# Installed Gamma");
|
|
537
|
+
writeFileSync(
|
|
538
|
+
join(target, "package.json"),
|
|
539
|
+
JSON.stringify({ version: "1.0.0", description: "local gamma" }),
|
|
540
|
+
);
|
|
541
|
+
let fetchCalls = 0;
|
|
542
|
+
const fetch = (async () => {
|
|
543
|
+
fetchCalls += 1;
|
|
544
|
+
return new Response("unexpected request", { status: 500 });
|
|
545
|
+
}) as FetchLike;
|
|
546
|
+
|
|
547
|
+
const details = await getPluginDetails(
|
|
548
|
+
{ name: "gamma", ref: "a".repeat(40) },
|
|
549
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
expect(details.installed).toBe(true);
|
|
553
|
+
expect(details.readme).toBe("# Installed Gamma");
|
|
554
|
+
expect(details.description).toBe("local gamma");
|
|
555
|
+
expect(details.source).toBeNull();
|
|
556
|
+
expect(fetchCalls).toBe(0);
|
|
557
|
+
});
|
|
558
|
+
|
|
516
559
|
test("degrades gracefully when the historical marketplace fetch fails", async () => {
|
|
517
560
|
// GIVEN an installed copy and a historical ref whose marketplace fetch errors
|
|
518
561
|
const historicalRef = "a".repeat(40);
|