@vellumai/assistant 0.8.11 → 0.8.12-staging.2
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/ARCHITECTURE.md +15 -17
- package/README.md +0 -6
- package/bun.lock +6 -122
- package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
- package/node_modules/@vellumai/gateway-client/package.json +3 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
- package/openapi.yaml +633 -4
- package/package.json +1 -3
- package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
- package/src/__tests__/anthropic-provider.test.ts +129 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
- package/src/__tests__/btw-routes.test.ts +7 -34
- package/src/__tests__/checker.test.ts +6 -12
- package/src/__tests__/config-loader-backfill.test.ts +4 -2
- package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +8 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/disk-pressure-tools.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +692 -0
- package/src/__tests__/filing-service.test.ts +8 -3
- package/src/__tests__/guardian-action-store.test.ts +0 -167
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
- package/src/__tests__/heartbeat-service.test.ts +5 -2
- package/src/__tests__/identity-intro-cache.test.ts +12 -5
- package/src/__tests__/identity-routes.test.ts +16 -57
- package/src/__tests__/injector-chain.test.ts +8 -3
- package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
- package/src/__tests__/llm-catalog-parity.test.ts +16 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/log-export-workspace.test.ts +468 -3
- package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +19 -8
- package/src/__tests__/openai-provider.test.ts +34 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
- package/src/__tests__/recurrence-engine.test.ts +45 -0
- package/src/__tests__/schedule-routes.test.ts +34 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
- package/src/__tests__/secret-fixtures.ts +20 -0
- package/src/__tests__/skill-tool-factory.test.ts +49 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -1
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +109 -11
- package/src/__tests__/tool-approval-handler.test.ts +85 -0
- package/src/__tests__/tool-audit-listener.test.ts +86 -0
- package/src/__tests__/tool-error-hook.test.ts +1 -0
- package/src/__tests__/tool-result-spool.test.ts +337 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/validate-input.test.ts +95 -1
- package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
- package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
- package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
- package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/acp/__tests__/agent-process.test.ts +315 -2
- package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
- package/src/acp/agent-process.ts +163 -34
- package/src/acp/prepare-agent-env.ts +55 -15
- package/src/agent/loop.ts +81 -24
- package/src/api/events/usage-progress.ts +28 -0
- package/src/api/index.ts +6 -0
- package/src/background-wake/wake-intent-hooks.test.ts +2 -0
- package/src/bundler/app-bundler.ts +25 -42
- package/src/bundler/app-compiler.ts +8 -0
- package/src/calls/call-controller.ts +1 -1
- package/src/cli/commands/plugins.ts +248 -15
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
- package/src/cli/lib/inspect-plugin.ts +252 -0
- package/src/cli/lib/install-from-github.ts +214 -21
- package/src/cli/lib/list-installed-plugins.ts +17 -6
- package/src/cli/lib/plugin-artifact.ts +103 -0
- package/src/cli/lib/plugin-details.ts +18 -1
- package/src/cli/lib/plugin-fingerprint.ts +197 -0
- package/src/cli/lib/upgrade-plugin.ts +225 -0
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
- package/src/config/call-site-defaults.ts +13 -2
- package/src/config/feature-flag-registry.json +8 -16
- package/src/config/loader.ts +52 -59
- package/src/config/schema.ts +0 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
- package/src/config/schemas/llm.ts +10 -0
- package/src/config/schemas/memory-v2.ts +13 -0
- package/src/config/schemas/memory-v3.ts +92 -0
- package/src/config/seed-inference-profiles.ts +4 -8
- package/src/context/post-turn-tool-result-truncation.ts +32 -18
- package/src/context/tool-result-spool.ts +104 -0
- package/src/credential-execution/feature-gates.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
- package/src/daemon/conversation-error.ts +6 -15
- package/src/daemon/conversation.ts +9 -0
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +1 -20
- package/src/daemon/message-types/conversations.ts +2 -15
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +40 -9
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/__tests__/home-content-refresh.test.ts +114 -0
- package/src/home/__tests__/suggested-prompts.test.ts +86 -5
- package/src/home/home-content-refresh.ts +43 -31
- package/src/home/home-greeting-cache.ts +8 -1
- package/src/home/home-greeting.ts +13 -9
- package/src/home/suggested-prompts.ts +77 -24
- package/src/ipc/routes/trust-rules.test.ts +66 -72
- package/src/media/image-credentials.ts +2 -2
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
- package/src/memory/conversation-attention-store.ts +1 -0
- package/src/memory/conversation-bootstrap.ts +18 -9
- package/src/memory/conversation-crud.ts +12 -2
- package/src/memory/conversation-title-service.ts +53 -9
- package/src/memory/delivery-channels.ts +0 -69
- package/src/memory/graph/extraction-job.ts +0 -15
- package/src/memory/guardian-action-store.ts +1 -376
- package/src/memory/llm-usage-store.ts +5 -1
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
- package/src/memory/v2/__tests__/injection.test.ts +70 -0
- package/src/memory/v2/__tests__/static-context.test.ts +12 -0
- package/src/memory/v2/consolidation-job.ts +93 -9
- package/src/memory/v2/injection.ts +53 -0
- package/src/memory/v2/prompts/consolidation.ts +1 -0
- package/src/memory/v2/static-context.ts +13 -1
- package/src/memory/v2/sweep-job.ts +1 -1
- package/src/memory/v2/types.ts +5 -0
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
- package/src/plugins/defaults/exploration-drift/package.json +15 -0
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
- package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
- package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
- package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
- package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
- package/src/prompts/cache-boundary.ts +17 -0
- package/src/prompts/sections.ts +50 -17
- package/src/prompts/system-prompt.ts +12 -4
- package/src/prompts/templates/system-sections.ts +22 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
- package/src/providers/anthropic/client.ts +74 -28
- package/src/providers/gemini/client.ts +5 -1
- package/src/providers/minimax/client.ts +9 -0
- package/src/providers/model-catalog.ts +28 -0
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/chat-completions-provider.ts +4 -2
- package/src/providers/openai/responses-provider.ts +7 -2
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/providers/unparseable-tool-args.ts +56 -0
- package/src/runtime/AGENTS.md +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +2 -2
- package/src/runtime/agent-wake.ts +5 -5
- package/src/runtime/background-job-runner.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
- package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
- package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
- package/src/runtime/migrations/vbundle-importer.ts +9 -4
- package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
- package/src/runtime/routes/btw-routes.ts +0 -14
- package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
- package/src/runtime/routes/conversation-list-routes.ts +77 -5
- package/src/runtime/routes/conversation-management-routes.ts +54 -0
- package/src/runtime/routes/conversation-query-routes.ts +79 -4
- package/src/runtime/routes/gateway-log-routes.ts +14 -64
- package/src/runtime/routes/home-feed-routes.ts +10 -0
- package/src/runtime/routes/identity-intro-cache.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +76 -20
- package/src/runtime/routes/inbound-message-handler.ts +0 -36
- package/src/runtime/routes/log-export-routes.ts +143 -96
- package/src/runtime/routes/plugins-routes.ts +380 -0
- package/src/runtime/routes/redact-staged-export.ts +259 -0
- package/src/runtime/routes/schedule-routes.ts +19 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -67
- package/src/schedule/recurrence-engine.ts +34 -0
- package/src/schedule/scheduler.ts +1 -0
- package/src/security/redact-json.ts +61 -0
- package/src/skills/validate-input.ts +41 -1
- package/src/subagent/types.ts +26 -1
- package/src/telemetry/types.ts +15 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/apps/executors.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +19 -8
- package/src/tools/tool-approval-handler.ts +31 -0
- package/src/usage/types.ts +8 -1
- package/src/util/platform.ts +16 -0
- package/src/watcher/engine.ts +1 -0
- package/src/workspace/adaptive-thinking-repair.ts +113 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
- package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
- package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
- package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
- package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
- package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
- package/src/__tests__/update-bulletin-job.test.ts +0 -292
- package/src/config/schemas/updates.ts +0 -14
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
- package/src/memory/conversation-starters-cadence.ts +0 -78
- package/src/prompts/update-bulletin-job.ts +0 -180
- package/src/runtime/guardian-action-followup-executor.ts +0 -306
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for {@link inspectPlugin}.
|
|
3
|
+
*
|
|
4
|
+
* The marketplace fetch is replaced with an in-memory fixture passed via the
|
|
5
|
+
* `fetch` dependency, and the installed-copy path is exercised against a real
|
|
6
|
+
* temp directory passed via `workspacePluginsDir` — no globals are monkeypatched.
|
|
7
|
+
* Drift is the exact comparison of the provenance sidecar's commit against the
|
|
8
|
+
* marketplace pin, so the fixtures vary those two SHAs.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
inspectPlugin,
|
|
18
|
+
PluginInspectNotFoundError,
|
|
19
|
+
} from "../inspect-plugin.js";
|
|
20
|
+
import type { FetchLike } from "../install-from-github.js";
|
|
21
|
+
import { computeFingerprint } from "../plugin-fingerprint.js";
|
|
22
|
+
|
|
23
|
+
const SHA_A = "a".repeat(40);
|
|
24
|
+
const SHA_B = "b".repeat(40);
|
|
25
|
+
|
|
26
|
+
/** A marketplace manifest object with a single entry for `name`. */
|
|
27
|
+
function manifestWith(name: string, ref: string): unknown {
|
|
28
|
+
return {
|
|
29
|
+
name: "vellum",
|
|
30
|
+
plugins: [
|
|
31
|
+
{
|
|
32
|
+
name,
|
|
33
|
+
source: { source: "github", repo: `example-org/${name}`, ref },
|
|
34
|
+
description: "A test plugin.",
|
|
35
|
+
category: "developer",
|
|
36
|
+
homepage: `https://github.com/example-org/${name}`,
|
|
37
|
+
license: "MIT",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build a `fetch` that serves the marketplace manifest. `marketplace:
|
|
45
|
+
* undefined` answers 404 (empty catalog); `fail: true` rejects with a network
|
|
46
|
+
* error. Anything else surfaces a 500 so test bugs are loud.
|
|
47
|
+
*/
|
|
48
|
+
function makeFetch(opts: { marketplace?: unknown; fail?: boolean }): FetchLike {
|
|
49
|
+
return (async (input: RequestInfo | URL) => {
|
|
50
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
51
|
+
if (url.includes("marketplace.json")) {
|
|
52
|
+
if (opts.fail) throw new Error("network down");
|
|
53
|
+
if (opts.marketplace === undefined) {
|
|
54
|
+
return new Response("not found", { status: 404 });
|
|
55
|
+
}
|
|
56
|
+
return new Response(JSON.stringify(opts.marketplace), { status: 200 });
|
|
57
|
+
}
|
|
58
|
+
return new Response("unexpected url: " + url, { status: 500 });
|
|
59
|
+
}) as FetchLike;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Materialize an installed plugin copy with an optional provenance sidecar. */
|
|
63
|
+
function installPlugin(
|
|
64
|
+
workspace: string,
|
|
65
|
+
name: string,
|
|
66
|
+
opts: {
|
|
67
|
+
version?: string;
|
|
68
|
+
sidecar?: { commit?: string | null; ref?: string } | null;
|
|
69
|
+
/** Embed a content fingerprint of the materialized tree in the sidecar. */
|
|
70
|
+
fingerprint?: boolean;
|
|
71
|
+
} = {},
|
|
72
|
+
): void {
|
|
73
|
+
const dir = join(workspace, name);
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
writeFileSync(
|
|
76
|
+
join(dir, "package.json"),
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
name,
|
|
79
|
+
version: opts.version ?? "0.1.0",
|
|
80
|
+
description: "Installed copy.",
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
if (opts.sidecar !== null) {
|
|
84
|
+
const sidecar = opts.sidecar ?? { commit: SHA_A };
|
|
85
|
+
// Mirror install: fingerprint the tree before the sidecar is written so it
|
|
86
|
+
// is not part of its own baseline.
|
|
87
|
+
const fingerprint = opts.fingerprint
|
|
88
|
+
? computeFingerprint(dir, ["install-meta.json"])
|
|
89
|
+
: undefined;
|
|
90
|
+
writeFileSync(
|
|
91
|
+
join(dir, "install-meta.json"),
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
origin: "vellum",
|
|
94
|
+
name,
|
|
95
|
+
source: {
|
|
96
|
+
kind: "github",
|
|
97
|
+
owner: "example-org",
|
|
98
|
+
repo: name,
|
|
99
|
+
ref: sidecar.ref ?? sidecar.commit ?? SHA_A,
|
|
100
|
+
},
|
|
101
|
+
commit: sidecar.commit ?? undefined,
|
|
102
|
+
installedAt: "2026-06-10T12:00:00.000Z",
|
|
103
|
+
fingerprint,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let workspace: string;
|
|
110
|
+
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
workspace = mkdtempSync(join(tmpdir(), "inspect-plugin-"));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
afterEach(() => {
|
|
116
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("inspectPlugin", () => {
|
|
120
|
+
test("reports up-to-date when the installed commit equals the marketplace pin", async () => {
|
|
121
|
+
// GIVEN an installed plugin pinned to the same commit the marketplace pins
|
|
122
|
+
installPlugin(workspace, "level-up", { sidecar: { commit: SHA_A } });
|
|
123
|
+
const fetch = makeFetch({ marketplace: manifestWith("level-up", SHA_A) });
|
|
124
|
+
|
|
125
|
+
// WHEN it is inspected
|
|
126
|
+
const result = await inspectPlugin(
|
|
127
|
+
{ name: "level-up" },
|
|
128
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// THEN the status is up-to-date and both sides resolve the same commit
|
|
132
|
+
expect(result.status).toBe("up-to-date");
|
|
133
|
+
expect(result.installed).toBe(true);
|
|
134
|
+
expect(result.local?.commit).toBe(SHA_A);
|
|
135
|
+
expect(result.remote?.commit).toBe(SHA_A);
|
|
136
|
+
// AND local + remote metadata are surfaced
|
|
137
|
+
expect(result.local?.version).toBe("0.1.0");
|
|
138
|
+
expect(result.remote?.repo).toBe("example-org/level-up");
|
|
139
|
+
expect(result.remote?.license).toBe("MIT");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("reports update-available when the marketplace pin has advanced", async () => {
|
|
143
|
+
// GIVEN an installed plugin whose commit differs from the marketplace pin
|
|
144
|
+
installPlugin(workspace, "level-up", { sidecar: { commit: SHA_A } });
|
|
145
|
+
const fetch = makeFetch({ marketplace: manifestWith("level-up", SHA_B) });
|
|
146
|
+
|
|
147
|
+
// WHEN it is inspected
|
|
148
|
+
const result = await inspectPlugin(
|
|
149
|
+
{ name: "level-up" },
|
|
150
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// THEN drift is reported with both commits
|
|
154
|
+
expect(result.status).toBe("update-available");
|
|
155
|
+
expect(result.local?.commit).toBe(SHA_A);
|
|
156
|
+
expect(result.remote?.commit).toBe(SHA_B);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("falls back to the sidecar ref when no commit was recorded", async () => {
|
|
160
|
+
// GIVEN a sidecar with no commit but a full-SHA ref equal to the pin
|
|
161
|
+
installPlugin(workspace, "level-up", {
|
|
162
|
+
sidecar: { commit: null, ref: SHA_A },
|
|
163
|
+
});
|
|
164
|
+
const fetch = makeFetch({ marketplace: manifestWith("level-up", SHA_A) });
|
|
165
|
+
|
|
166
|
+
// WHEN it is inspected
|
|
167
|
+
const result = await inspectPlugin(
|
|
168
|
+
{ name: "level-up" },
|
|
169
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// THEN the ref stands in for the commit and the status is up-to-date
|
|
173
|
+
expect(result.status).toBe("up-to-date");
|
|
174
|
+
expect(result.local?.commit).toBe(SHA_A);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("reports unknown-provenance when installed without a resolvable commit", async () => {
|
|
178
|
+
// GIVEN an installed copy with no provenance sidecar at all
|
|
179
|
+
installPlugin(workspace, "level-up", { sidecar: null });
|
|
180
|
+
const fetch = makeFetch({ marketplace: manifestWith("level-up", SHA_A) });
|
|
181
|
+
|
|
182
|
+
// WHEN it is inspected
|
|
183
|
+
const result = await inspectPlugin(
|
|
184
|
+
{ name: "level-up" },
|
|
185
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// THEN the commit cannot be compared
|
|
189
|
+
expect(result.status).toBe("unknown-provenance");
|
|
190
|
+
expect(result.local?.commit).toBeNull();
|
|
191
|
+
expect(result.remote?.commit).toBe(SHA_A);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("reports not-in-marketplace when installed but no entry claims the name", async () => {
|
|
195
|
+
// GIVEN an installed plugin and an empty catalog
|
|
196
|
+
installPlugin(workspace, "level-up", { sidecar: { commit: SHA_A } });
|
|
197
|
+
const fetch = makeFetch({ marketplace: undefined });
|
|
198
|
+
|
|
199
|
+
// WHEN it is inspected
|
|
200
|
+
const result = await inspectPlugin(
|
|
201
|
+
{ name: "level-up" },
|
|
202
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// THEN there is no remote to compare against
|
|
206
|
+
expect(result.status).toBe("not-in-marketplace");
|
|
207
|
+
expect(result.installed).toBe(true);
|
|
208
|
+
expect(result.remote).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("reports not-installed but previews the marketplace entry", async () => {
|
|
212
|
+
// GIVEN a marketplace entry with no local copy installed
|
|
213
|
+
const fetch = makeFetch({ marketplace: manifestWith("level-up", SHA_B) });
|
|
214
|
+
|
|
215
|
+
// WHEN it is inspected
|
|
216
|
+
const result = await inspectPlugin(
|
|
217
|
+
{ name: "level-up" },
|
|
218
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// THEN the remote metadata previews what would be installed
|
|
222
|
+
expect(result.status).toBe("not-installed");
|
|
223
|
+
expect(result.installed).toBe(false);
|
|
224
|
+
expect(result.local).toBeNull();
|
|
225
|
+
expect(result.remote?.commit).toBe(SHA_B);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("reports remote-unavailable when the marketplace cannot be reached", async () => {
|
|
229
|
+
// GIVEN an installed plugin and a failing marketplace fetch
|
|
230
|
+
installPlugin(workspace, "level-up", { sidecar: { commit: SHA_A } });
|
|
231
|
+
const fetch = makeFetch({ fail: true });
|
|
232
|
+
|
|
233
|
+
// WHEN it is inspected
|
|
234
|
+
const result = await inspectPlugin(
|
|
235
|
+
{ name: "level-up" },
|
|
236
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// THEN local info is still reported alongside the fetch error
|
|
240
|
+
expect(result.status).toBe("remote-unavailable");
|
|
241
|
+
expect(result.local?.commit).toBe(SHA_A);
|
|
242
|
+
expect(result.remote).toBeNull();
|
|
243
|
+
expect(result.remoteError).toContain("network down");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("reports no local changes when the on-disk tree matches the fingerprint", async () => {
|
|
247
|
+
// GIVEN an installed plugin with a recorded content fingerprint, untouched
|
|
248
|
+
installPlugin(workspace, "level-up", {
|
|
249
|
+
sidecar: { commit: SHA_A },
|
|
250
|
+
fingerprint: true,
|
|
251
|
+
});
|
|
252
|
+
const fetch = makeFetch({ marketplace: manifestWith("level-up", SHA_A) });
|
|
253
|
+
|
|
254
|
+
// WHEN it is inspected
|
|
255
|
+
const result = await inspectPlugin(
|
|
256
|
+
{ name: "level-up" },
|
|
257
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// THEN the local copy is reported clean against its baseline
|
|
261
|
+
expect(result.local?.localChanges?.clean).toBe(true);
|
|
262
|
+
expect(result.local?.localChanges?.modified).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("detects a locally modified file against the fingerprint", async () => {
|
|
266
|
+
// GIVEN an installed plugin with a recorded fingerprint
|
|
267
|
+
installPlugin(workspace, "level-up", {
|
|
268
|
+
sidecar: { commit: SHA_A },
|
|
269
|
+
fingerprint: true,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// AND a tracked file is edited after install
|
|
273
|
+
writeFileSync(
|
|
274
|
+
join(workspace, "level-up", "package.json"),
|
|
275
|
+
JSON.stringify({ name: "level-up", version: "9.9.9" }),
|
|
276
|
+
);
|
|
277
|
+
const fetch = makeFetch({ marketplace: manifestWith("level-up", SHA_A) });
|
|
278
|
+
|
|
279
|
+
// WHEN it is inspected
|
|
280
|
+
const result = await inspectPlugin(
|
|
281
|
+
{ name: "level-up" },
|
|
282
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// THEN the edited file surfaces as a local modification
|
|
286
|
+
expect(result.local?.localChanges?.clean).toBe(false);
|
|
287
|
+
expect(result.local?.localChanges?.modified).toEqual(["package.json"]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("reports unknown local changes when no fingerprint was recorded", async () => {
|
|
291
|
+
// GIVEN an install whose sidecar predates fingerprinting
|
|
292
|
+
installPlugin(workspace, "level-up", { sidecar: { commit: SHA_A } });
|
|
293
|
+
const fetch = makeFetch({ marketplace: manifestWith("level-up", SHA_A) });
|
|
294
|
+
|
|
295
|
+
// WHEN it is inspected
|
|
296
|
+
const result = await inspectPlugin(
|
|
297
|
+
{ name: "level-up" },
|
|
298
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// THEN modification cannot be determined
|
|
302
|
+
expect(result.local).not.toBeNull();
|
|
303
|
+
expect(result.local?.localChanges).toBeNull();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("throws when the plugin is neither installed nor in the marketplace", async () => {
|
|
307
|
+
// GIVEN no local copy and an empty catalog
|
|
308
|
+
const fetch = makeFetch({ marketplace: undefined });
|
|
309
|
+
|
|
310
|
+
// WHEN/THEN inspecting a missing plugin rejects with a not-found error
|
|
311
|
+
await expect(
|
|
312
|
+
inspectPlugin(
|
|
313
|
+
{ name: "ghost" },
|
|
314
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
315
|
+
),
|
|
316
|
+
).rejects.toBeInstanceOf(PluginInspectNotFoundError);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -371,16 +371,23 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
371
371
|
"https://github.com/JuliusBrussee/caveman.git",
|
|
372
372
|
);
|
|
373
373
|
|
|
374
|
-
// AND
|
|
375
|
-
const
|
|
376
|
-
readFileSync(join(target, "
|
|
374
|
+
// AND an install-meta sidecar records origin + github source + commit.
|
|
375
|
+
const meta = JSON.parse(
|
|
376
|
+
readFileSync(join(target, "install-meta.json"), "utf-8"),
|
|
377
377
|
);
|
|
378
|
-
expect(
|
|
379
|
-
expect(
|
|
380
|
-
expect(
|
|
381
|
-
|
|
382
|
-
);
|
|
383
|
-
expect(
|
|
378
|
+
expect(meta.origin).toBe("vellum");
|
|
379
|
+
expect(meta.sourceRepo).toBe("JuliusBrussee/caveman");
|
|
380
|
+
expect(meta.source.kind).toBe("github");
|
|
381
|
+
expect(meta.source.owner).toBe("JuliusBrussee");
|
|
382
|
+
expect(meta.source.ref).toBe("63a91ecadbf4c4719a4602a5abb00883f9966034");
|
|
383
|
+
expect(meta.commit).toBe("63a91ecadbf4c4719a4602a5abb00883f9966034");
|
|
384
|
+
|
|
385
|
+
// AND both content digests baseline the materialized tree (excluding the
|
|
386
|
+
// sidecar) so later local edits are detectable.
|
|
387
|
+
expect(meta.fingerprint.algorithm).toBe("sha256");
|
|
388
|
+
expect(meta.fingerprint.files["package.json"]).toMatch(/^[0-9a-f]{64}$/);
|
|
389
|
+
expect(meta.fingerprint.files["install-meta.json"]).toBeUndefined();
|
|
390
|
+
expect(meta.contentHash).toMatch(/^v2:[0-9a-f]{64}$/);
|
|
384
391
|
});
|
|
385
392
|
|
|
386
393
|
test("refuses to install when the checked-out commit differs from the pinned SHA", async () => {
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for {@link parsePluginArtifact} — the `vellum.artifact` descriptor
|
|
3
|
+
* reader. A descriptor is surfaced only when both fields are well-formed
|
|
4
|
+
* (https URL + 64-hex sha256); every malformed or partial shape is "no
|
|
5
|
+
* artifact yet" (`null`) so callers never offer an unverifiable download.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
import { parsePluginArtifact } from "../plugin-artifact.js";
|
|
11
|
+
|
|
12
|
+
const VALID_SHA = "a".repeat(64);
|
|
13
|
+
|
|
14
|
+
describe("parsePluginArtifact", () => {
|
|
15
|
+
test("returns the descriptor when url + sha256 are well-formed", () => {
|
|
16
|
+
// GIVEN a package.json with a complete vellum.artifact block
|
|
17
|
+
const pkg = {
|
|
18
|
+
vellum: {
|
|
19
|
+
artifact: {
|
|
20
|
+
url: "https://example.com/releases/v1.0.0/App.dmg",
|
|
21
|
+
sha256: VALID_SHA,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// WHEN we parse it
|
|
27
|
+
const artifact = parsePluginArtifact(pkg);
|
|
28
|
+
|
|
29
|
+
// THEN both fields come through unchanged
|
|
30
|
+
expect(artifact).toEqual({
|
|
31
|
+
url: "https://example.com/releases/v1.0.0/App.dmg",
|
|
32
|
+
sha256: VALID_SHA,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("surfaces an optional label when the plugin names the download", () => {
|
|
37
|
+
// GIVEN a block that labels its artifact (e.g. for multi-artifact plugins)
|
|
38
|
+
const pkg = {
|
|
39
|
+
vellum: {
|
|
40
|
+
artifact: {
|
|
41
|
+
url: "https://example.com/App.dmg",
|
|
42
|
+
sha256: VALID_SHA,
|
|
43
|
+
label: "Download for macOS",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// WHEN we parse it
|
|
49
|
+
const artifact = parsePluginArtifact(pkg);
|
|
50
|
+
|
|
51
|
+
// THEN the label comes through alongside url + sha256
|
|
52
|
+
expect(artifact).toEqual({
|
|
53
|
+
url: "https://example.com/App.dmg",
|
|
54
|
+
sha256: VALID_SHA,
|
|
55
|
+
label: "Download for macOS",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("trims a label and drops it when blank, keeping the descriptor", () => {
|
|
60
|
+
// GIVEN a descriptor whose label is whitespace-only
|
|
61
|
+
const pkg = {
|
|
62
|
+
vellum: {
|
|
63
|
+
artifact: {
|
|
64
|
+
url: "https://example.com/App.dmg",
|
|
65
|
+
sha256: VALID_SHA,
|
|
66
|
+
label: " ",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// WHEN we parse it
|
|
72
|
+
const artifact = parsePluginArtifact(pkg);
|
|
73
|
+
|
|
74
|
+
// THEN the blank label is dropped but the artifact still surfaces
|
|
75
|
+
expect(artifact).toEqual({
|
|
76
|
+
url: "https://example.com/App.dmg",
|
|
77
|
+
sha256: VALID_SHA,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("ignores a non-string label without dropping the descriptor", () => {
|
|
82
|
+
// GIVEN a label of the wrong type
|
|
83
|
+
const pkg = {
|
|
84
|
+
vellum: {
|
|
85
|
+
artifact: {
|
|
86
|
+
url: "https://example.com/App.dmg",
|
|
87
|
+
sha256: VALID_SHA,
|
|
88
|
+
label: 42,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// WHEN we parse it
|
|
94
|
+
const artifact = parsePluginArtifact(pkg);
|
|
95
|
+
|
|
96
|
+
// THEN the bad label is ignored but url + sha256 still come through
|
|
97
|
+
expect(artifact).toEqual({
|
|
98
|
+
url: "https://example.com/App.dmg",
|
|
99
|
+
sha256: VALID_SHA,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("ignores extra fields inside the artifact block", () => {
|
|
104
|
+
// GIVEN a block carrying fields beyond url + sha256
|
|
105
|
+
const pkg = {
|
|
106
|
+
vellum: {
|
|
107
|
+
artifact: {
|
|
108
|
+
url: "https://example.com/App.dmg",
|
|
109
|
+
sha256: VALID_SHA,
|
|
110
|
+
platform: "macos",
|
|
111
|
+
minOS: "13.0",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// WHEN we parse it
|
|
117
|
+
const artifact = parsePluginArtifact(pkg);
|
|
118
|
+
|
|
119
|
+
// THEN only the two recognised fields are surfaced
|
|
120
|
+
expect(artifact).toEqual({
|
|
121
|
+
url: "https://example.com/App.dmg",
|
|
122
|
+
sha256: VALID_SHA,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test.each([
|
|
127
|
+
["no vellum block", { name: "x" }],
|
|
128
|
+
["no artifact block", { vellum: {} }],
|
|
129
|
+
[
|
|
130
|
+
"empty sha256 (bootstrap placeholder)",
|
|
131
|
+
{
|
|
132
|
+
vellum: {
|
|
133
|
+
artifact: { url: "https://example.com/App.dmg", sha256: "" },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
"sha256 too short",
|
|
139
|
+
{
|
|
140
|
+
vellum: {
|
|
141
|
+
artifact: { url: "https://example.com/App.dmg", sha256: "abc" },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
"sha256 with uppercase hex",
|
|
147
|
+
{
|
|
148
|
+
vellum: {
|
|
149
|
+
artifact: {
|
|
150
|
+
url: "https://example.com/App.dmg",
|
|
151
|
+
sha256: "A".repeat(64),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
[
|
|
157
|
+
"non-https url",
|
|
158
|
+
{
|
|
159
|
+
vellum: {
|
|
160
|
+
artifact: { url: "http://example.com/App.dmg", sha256: VALID_SHA },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
["missing url", { vellum: { artifact: { sha256: VALID_SHA } } }],
|
|
165
|
+
["artifact is not an object", { vellum: { artifact: "App.dmg" } }],
|
|
166
|
+
["vellum is an array", { vellum: [] }],
|
|
167
|
+
])("returns null for %s", (_label, pkg) => {
|
|
168
|
+
// WHEN we parse a malformed / partial descriptor
|
|
169
|
+
// THEN nothing is surfaced
|
|
170
|
+
expect(parsePluginArtifact(pkg)).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test.each([
|
|
174
|
+
["null", null],
|
|
175
|
+
["a string", "not-a-package"],
|
|
176
|
+
["an array", []],
|
|
177
|
+
["a number", 42],
|
|
178
|
+
])("returns null when package.json is %s", (_label, value) => {
|
|
179
|
+
// WHEN we parse a non-object package.json value
|
|
180
|
+
// THEN nothing is surfaced
|
|
181
|
+
expect(parsePluginArtifact(value)).toBeNull();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -372,4 +372,162 @@ describe("getPluginDetails", () => {
|
|
|
372
372
|
),
|
|
373
373
|
).rejects.toThrow();
|
|
374
374
|
});
|
|
375
|
+
|
|
376
|
+
test("surfaces a well-formed vellum.artifact from the repo package.json", async () => {
|
|
377
|
+
// GIVEN an external plugin whose repo package.json declares a complete
|
|
378
|
+
// vellum.artifact (https url + 64-hex sha256)
|
|
379
|
+
const sha = "a".repeat(64);
|
|
380
|
+
const fetch = makeFetch({
|
|
381
|
+
marketplace: {
|
|
382
|
+
name: "vellum",
|
|
383
|
+
plugins: [
|
|
384
|
+
{
|
|
385
|
+
name: "dynamic-notch",
|
|
386
|
+
source: {
|
|
387
|
+
source: "github",
|
|
388
|
+
repo: "example-org/dynamic-notch",
|
|
389
|
+
ref: "1111111111111111111111111111111111111111",
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
listings: {
|
|
395
|
+
"example-org/dynamic-notch": [
|
|
396
|
+
fileEntry("package.json", "raw://notch/pkg"),
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
raw: {
|
|
400
|
+
"raw://notch/pkg": JSON.stringify({
|
|
401
|
+
version: "1.0.0",
|
|
402
|
+
vellum: {
|
|
403
|
+
artifact: {
|
|
404
|
+
url: "https://example.com/releases/v1.0.0/App.dmg",
|
|
405
|
+
sha256: sha,
|
|
406
|
+
label: "Download for macOS",
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
}),
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// WHEN we resolve the detail view
|
|
414
|
+
const details = await getPluginDetails(
|
|
415
|
+
{ name: "dynamic-notch" },
|
|
416
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// THEN the artifact descriptor is surfaced, including its optional label
|
|
420
|
+
expect(details.artifact).toEqual({
|
|
421
|
+
url: "https://example.com/releases/v1.0.0/App.dmg",
|
|
422
|
+
sha256: sha,
|
|
423
|
+
label: "Download for macOS",
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("an installed copy's artifact wins over the repo's", async () => {
|
|
428
|
+
// GIVEN an installed copy whose package.json declares its own artifact
|
|
429
|
+
const localSha = "b".repeat(64);
|
|
430
|
+
const remoteSha = "c".repeat(64);
|
|
431
|
+
const target = join(workspace, "dynamic-notch");
|
|
432
|
+
mkdirSync(target, { recursive: true });
|
|
433
|
+
writeFileSync(
|
|
434
|
+
join(target, "package.json"),
|
|
435
|
+
JSON.stringify({
|
|
436
|
+
vellum: {
|
|
437
|
+
artifact: {
|
|
438
|
+
url: "https://example.com/local/App.dmg",
|
|
439
|
+
sha256: localSha,
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// AND a marketplace entry + repo package.json declaring a different artifact
|
|
446
|
+
const fetch = makeFetch({
|
|
447
|
+
marketplace: {
|
|
448
|
+
name: "vellum",
|
|
449
|
+
plugins: [
|
|
450
|
+
{
|
|
451
|
+
name: "dynamic-notch",
|
|
452
|
+
source: {
|
|
453
|
+
source: "github",
|
|
454
|
+
repo: "example-org/dynamic-notch",
|
|
455
|
+
ref: "1111111111111111111111111111111111111111",
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
},
|
|
460
|
+
listings: {
|
|
461
|
+
"example-org/dynamic-notch": [
|
|
462
|
+
fileEntry("package.json", "raw://notch/pkg"),
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
raw: {
|
|
466
|
+
"raw://notch/pkg": JSON.stringify({
|
|
467
|
+
vellum: {
|
|
468
|
+
artifact: {
|
|
469
|
+
url: "https://example.com/remote/App.dmg",
|
|
470
|
+
sha256: remoteSha,
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
}),
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// WHEN we resolve the detail view
|
|
478
|
+
const details = await getPluginDetails(
|
|
479
|
+
{ name: "dynamic-notch" },
|
|
480
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// THEN the installed copy's artifact wins
|
|
484
|
+
expect(details.artifact).toEqual({
|
|
485
|
+
url: "https://example.com/local/App.dmg",
|
|
486
|
+
sha256: localSha,
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("treats a placeholder sha256 as no artifact yet", async () => {
|
|
491
|
+
// GIVEN a repo package.json with a url but an empty (placeholder) sha256 —
|
|
492
|
+
// the bootstrap state before a release workflow fills the digest in
|
|
493
|
+
const fetch = makeFetch({
|
|
494
|
+
marketplace: {
|
|
495
|
+
name: "vellum",
|
|
496
|
+
plugins: [
|
|
497
|
+
{
|
|
498
|
+
name: "dynamic-notch",
|
|
499
|
+
source: {
|
|
500
|
+
source: "github",
|
|
501
|
+
repo: "example-org/dynamic-notch",
|
|
502
|
+
ref: "1111111111111111111111111111111111111111",
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
},
|
|
507
|
+
listings: {
|
|
508
|
+
"example-org/dynamic-notch": [
|
|
509
|
+
fileEntry("package.json", "raw://notch/pkg"),
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
raw: {
|
|
513
|
+
"raw://notch/pkg": JSON.stringify({
|
|
514
|
+
vellum: {
|
|
515
|
+
artifact: {
|
|
516
|
+
url: "https://example.com/releases/v1.0.0/App.dmg",
|
|
517
|
+
sha256: "",
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
}),
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// WHEN we resolve the detail view
|
|
525
|
+
const details = await getPluginDetails(
|
|
526
|
+
{ name: "dynamic-notch" },
|
|
527
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
// THEN no artifact is surfaced (a client must not offer an unverifiable download)
|
|
531
|
+
expect(details.artifact).toBeNull();
|
|
532
|
+
});
|
|
375
533
|
});
|