@velum-labs/routekit-gateway 0.9.0
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/LICENSE +201 -0
- package/README.md +28 -0
- package/dist/acp-agent.d.ts +38 -0
- package/dist/acp-agent.js +142 -0
- package/dist/acp-registry.d.ts +36 -0
- package/dist/acp-registry.js +85 -0
- package/dist/adapters/anthropic.d.ts +131 -0
- package/dist/adapters/anthropic.js +1195 -0
- package/dist/adapters/chat.d.ts +14 -0
- package/dist/adapters/chat.js +34 -0
- package/dist/adapters/cursor.d.ts +34 -0
- package/dist/adapters/cursor.js +305 -0
- package/dist/adapters/dropped.d.ts +10 -0
- package/dist/adapters/dropped.js +24 -0
- package/dist/adapters/openai-chat-wire.d.ts +93 -0
- package/dist/adapters/openai-chat-wire.js +143 -0
- package/dist/adapters/responses-stream.d.ts +7 -0
- package/dist/adapters/responses-stream.js +597 -0
- package/dist/adapters/responses.d.ts +174 -0
- package/dist/adapters/responses.js +778 -0
- package/dist/adapters/server-tool-loop.d.ts +94 -0
- package/dist/adapters/server-tool-loop.js +477 -0
- package/dist/adapters/upstream-error.d.ts +14 -0
- package/dist/adapters/upstream-error.js +25 -0
- package/dist/adapters/validate.d.ts +27 -0
- package/dist/adapters/validate.js +180 -0
- package/dist/adapters/web-search.d.ts +46 -0
- package/dist/adapters/web-search.js +151 -0
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +28 -0
- package/dist/backend.d.ts +151 -0
- package/dist/backend.js +143 -0
- package/dist/capacity-pool.d.ts +31 -0
- package/dist/capacity-pool.js +99 -0
- package/dist/cost.d.ts +49 -0
- package/dist/cost.js +112 -0
- package/dist/endpoint-health.d.ts +54 -0
- package/dist/endpoint-health.js +123 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +23 -0
- package/dist/provenance.d.ts +31 -0
- package/dist/provenance.js +191 -0
- package/dist/provider-backends.d.ts +40 -0
- package/dist/provider-backends.js +1050 -0
- package/dist/provider-source.d.ts +40 -0
- package/dist/provider-source.js +293 -0
- package/dist/router.d.ts +168 -0
- package/dist/router.js +474 -0
- package/dist/server.d.ts +67 -0
- package/dist/server.js +930 -0
- package/dist/sse/chat-assembler.d.ts +45 -0
- package/dist/sse/chat-assembler.js +190 -0
- package/dist/sse/parse.d.ts +50 -0
- package/dist/sse/parse.js +149 -0
- package/dist/sse-wire.d.ts +10 -0
- package/dist/sse-wire.js +31 -0
- package/dist/switching-proxy.d.ts +15 -0
- package/dist/switching-proxy.js +232 -0
- package/dist/test/acp-agent.test.d.ts +1 -0
- package/dist/test/acp-agent.test.js +66 -0
- package/dist/test/acp-registry.test.d.ts +1 -0
- package/dist/test/acp-registry.test.js +70 -0
- package/dist/test/anthropic.test.d.ts +1 -0
- package/dist/test/anthropic.test.js +793 -0
- package/dist/test/auth.test.d.ts +1 -0
- package/dist/test/auth.test.js +25 -0
- package/dist/test/boundary.test.d.ts +1 -0
- package/dist/test/boundary.test.js +32 -0
- package/dist/test/chat.test.d.ts +1 -0
- package/dist/test/chat.test.js +418 -0
- package/dist/test/cost.test.d.ts +1 -0
- package/dist/test/cost.test.js +60 -0
- package/dist/test/cursor.test.d.ts +1 -0
- package/dist/test/cursor.test.js +100 -0
- package/dist/test/drain.test.d.ts +1 -0
- package/dist/test/drain.test.js +116 -0
- package/dist/test/dropped.test.d.ts +1 -0
- package/dist/test/dropped.test.js +80 -0
- package/dist/test/endpoint-health.test.d.ts +1 -0
- package/dist/test/endpoint-health.test.js +73 -0
- package/dist/test/provenance.test.d.ts +1 -0
- package/dist/test/provenance.test.js +176 -0
- package/dist/test/provider-backends.test.d.ts +1 -0
- package/dist/test/provider-backends.test.js +699 -0
- package/dist/test/responses.test.d.ts +1 -0
- package/dist/test/responses.test.js +813 -0
- package/dist/test/routed-backend.test.d.ts +1 -0
- package/dist/test/routed-backend.test.js +39 -0
- package/dist/test/router.test.d.ts +1 -0
- package/dist/test/router.test.js +297 -0
- package/dist/test/server-resilience.test.d.ts +1 -0
- package/dist/test/server-resilience.test.js +169 -0
- package/dist/test/sse-codec.test.d.ts +1 -0
- package/dist/test/sse-codec.test.js +186 -0
- package/dist/test/web-search-loop.test.d.ts +1 -0
- package/dist/test/web-search-loop.test.js +469 -0
- package/dist/test/wire-validation.test.d.ts +1 -0
- package/dist/test/wire-validation.test.js +140 -0
- package/package.json +48 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable data-plane front door whose internal router generation can be
|
|
3
|
+
* replaced atomically.
|
|
4
|
+
*
|
|
5
|
+
* A singleton daemon keeps this listener for its whole lifetime. Reload builds
|
|
6
|
+
* a complete router on an ephemeral loopback port, calls `swapTarget`, then
|
|
7
|
+
* drains the old router. Requests accepted before the swap stay attached to
|
|
8
|
+
* the old generation; later requests immediately use the new one.
|
|
9
|
+
*/
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
import { assertAuthenticatedBind, trimTrailingSlashes } from "@velum-labs/routekit-runtime";
|
|
12
|
+
import { authorizedRequest } from "./auth.js";
|
|
13
|
+
const MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
const HOP_BY_HOP = new Set([
|
|
15
|
+
"connection",
|
|
16
|
+
"keep-alive",
|
|
17
|
+
"proxy-authenticate",
|
|
18
|
+
"proxy-authorization",
|
|
19
|
+
"te",
|
|
20
|
+
"trailer",
|
|
21
|
+
"transfer-encoding",
|
|
22
|
+
"upgrade"
|
|
23
|
+
]);
|
|
24
|
+
function requestHeaders(headers) {
|
|
25
|
+
const result = new Headers();
|
|
26
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
27
|
+
if (value === undefined || HOP_BY_HOP.has(name.toLowerCase()) || name === "host")
|
|
28
|
+
continue;
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
for (const entry of value)
|
|
31
|
+
result.append(name, entry);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
result.set(name, value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
async function requestBody(req) {
|
|
40
|
+
if (req.method === "GET" || req.method === "HEAD")
|
|
41
|
+
return undefined;
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let total = 0;
|
|
44
|
+
for await (const value of req) {
|
|
45
|
+
const chunk = value;
|
|
46
|
+
total += chunk.length;
|
|
47
|
+
if (total > MAX_BODY_BYTES)
|
|
48
|
+
throw new Error("proxy request body too large");
|
|
49
|
+
chunks.push(chunk);
|
|
50
|
+
}
|
|
51
|
+
return Buffer.concat(chunks);
|
|
52
|
+
}
|
|
53
|
+
function writeJson(res, status, value) {
|
|
54
|
+
const payload = Buffer.from(JSON.stringify(value));
|
|
55
|
+
res.statusCode = status;
|
|
56
|
+
res.setHeader("content-type", "application/json");
|
|
57
|
+
res.setHeader("content-length", String(payload.length));
|
|
58
|
+
res.end(payload);
|
|
59
|
+
}
|
|
60
|
+
async function pipe(res, upstream) {
|
|
61
|
+
res.statusCode = upstream.status;
|
|
62
|
+
for (const [name, value] of upstream.headers) {
|
|
63
|
+
if (!HOP_BY_HOP.has(name.toLowerCase()) && name.toLowerCase() !== "content-length") {
|
|
64
|
+
res.setHeader(name, value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (upstream.body === null) {
|
|
68
|
+
res.end();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const reader = upstream.body.getReader();
|
|
72
|
+
try {
|
|
73
|
+
for (;;) {
|
|
74
|
+
const { done, value } = await reader.read();
|
|
75
|
+
if (done)
|
|
76
|
+
break;
|
|
77
|
+
if (value !== undefined && !res.write(Buffer.from(value))) {
|
|
78
|
+
await Promise.race([
|
|
79
|
+
new Promise((resolve) => res.once("drain", resolve)),
|
|
80
|
+
new Promise((resolve) => res.once("close", resolve))
|
|
81
|
+
]);
|
|
82
|
+
if (res.destroyed)
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
reader.releaseLock();
|
|
89
|
+
}
|
|
90
|
+
res.end();
|
|
91
|
+
}
|
|
92
|
+
export async function startSwitchingGatewayProxy(input) {
|
|
93
|
+
const host = input.host ?? "127.0.0.1";
|
|
94
|
+
assertAuthenticatedBind(host, input.authToken);
|
|
95
|
+
let active = {
|
|
96
|
+
url: trimTrailingSlashes(input.target),
|
|
97
|
+
leases: 0,
|
|
98
|
+
waiters: new Set()
|
|
99
|
+
};
|
|
100
|
+
const generations = new Map([[active.url, active]]);
|
|
101
|
+
let draining = false;
|
|
102
|
+
let inflight = 0;
|
|
103
|
+
const server = createServer((req, res) => {
|
|
104
|
+
inflight += 1;
|
|
105
|
+
res.once("close", () => {
|
|
106
|
+
inflight -= 1;
|
|
107
|
+
});
|
|
108
|
+
void (async () => {
|
|
109
|
+
const path = req.url ?? "/";
|
|
110
|
+
if (path.split("?")[0] === "/health") {
|
|
111
|
+
writeJson(res, draining ? 503 : 200, {
|
|
112
|
+
status: draining ? "draining" : "ok"
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (draining) {
|
|
117
|
+
writeJson(res, 503, {
|
|
118
|
+
error: { message: "gateway is draining", type: "unavailable" }
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (input.authToken !== undefined && !authorizedRequest(req, input.authToken)) {
|
|
123
|
+
writeJson(res, 401, {
|
|
124
|
+
error: { message: "unauthorized", type: "auth_error" }
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const selected = active;
|
|
129
|
+
selected.leases += 1;
|
|
130
|
+
const aborter = new AbortController();
|
|
131
|
+
const onClose = () => {
|
|
132
|
+
if (!res.writableEnded)
|
|
133
|
+
aborter.abort(new Error("gateway client disconnected"));
|
|
134
|
+
};
|
|
135
|
+
res.once("close", onClose);
|
|
136
|
+
try {
|
|
137
|
+
const body = await requestBody(req);
|
|
138
|
+
const upstream = await fetch(`${selected.url}${path}`, {
|
|
139
|
+
method: req.method ?? "GET",
|
|
140
|
+
headers: requestHeaders(req.headers),
|
|
141
|
+
...(body !== undefined ? { body } : {}),
|
|
142
|
+
signal: AbortSignal.any([
|
|
143
|
+
aborter.signal,
|
|
144
|
+
AbortSignal.timeout(10 * 60 * 1000)
|
|
145
|
+
])
|
|
146
|
+
});
|
|
147
|
+
await pipe(res, upstream);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
if (!res.destroyed && !res.headersSent) {
|
|
151
|
+
writeJson(res, 502, {
|
|
152
|
+
error: { message: "router generation unavailable", type: "upstream_error" }
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else if (!res.writableEnded) {
|
|
156
|
+
res.destroy();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
res.off("close", onClose);
|
|
161
|
+
selected.leases -= 1;
|
|
162
|
+
if (selected.leases === 0) {
|
|
163
|
+
for (const resolve of selected.waiters)
|
|
164
|
+
resolve();
|
|
165
|
+
selected.waiters.clear();
|
|
166
|
+
if (selected !== active)
|
|
167
|
+
generations.delete(selected.url);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
})();
|
|
171
|
+
});
|
|
172
|
+
await new Promise((resolve, reject) => {
|
|
173
|
+
server.once("error", reject);
|
|
174
|
+
server.listen(input.port ?? 0, host, () => {
|
|
175
|
+
server.off("error", reject);
|
|
176
|
+
resolve();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
const address = server.address();
|
|
180
|
+
const port = typeof address === "object" && address !== null ? address.port : input.port ?? 0;
|
|
181
|
+
let drainRun;
|
|
182
|
+
const drain = (graceMs = 0) => {
|
|
183
|
+
drainRun ??= (async () => {
|
|
184
|
+
draining = true;
|
|
185
|
+
server.closeIdleConnections();
|
|
186
|
+
const deadline = Date.now() + graceMs;
|
|
187
|
+
while (inflight > 0 && Date.now() < deadline) {
|
|
188
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
189
|
+
}
|
|
190
|
+
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
191
|
+
server.closeAllConnections();
|
|
192
|
+
await closed;
|
|
193
|
+
})();
|
|
194
|
+
return drainRun;
|
|
195
|
+
};
|
|
196
|
+
return {
|
|
197
|
+
url: () => `http://${host.includes(":") ? `[${host}]` : host}:${port}`,
|
|
198
|
+
port: () => port,
|
|
199
|
+
target: () => active.url,
|
|
200
|
+
swapTarget(next) {
|
|
201
|
+
const previous = active.url;
|
|
202
|
+
const url = trimTrailingSlashes(next);
|
|
203
|
+
active = generations.get(url) ?? { url, leases: 0, waiters: new Set() };
|
|
204
|
+
generations.set(url, active);
|
|
205
|
+
return previous;
|
|
206
|
+
},
|
|
207
|
+
async waitForTargetIdle(url, graceMs) {
|
|
208
|
+
const generation = generations.get(trimTrailingSlashes(url));
|
|
209
|
+
if (generation === undefined || generation.leases === 0)
|
|
210
|
+
return true;
|
|
211
|
+
let timer;
|
|
212
|
+
const idle = new Promise((resolve) => {
|
|
213
|
+
const done = () => {
|
|
214
|
+
if (timer !== undefined)
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
resolve(true);
|
|
217
|
+
};
|
|
218
|
+
generation.waiters.add(done);
|
|
219
|
+
timer = setTimeout(() => {
|
|
220
|
+
generation.waiters.delete(done);
|
|
221
|
+
resolve(false);
|
|
222
|
+
}, graceMs);
|
|
223
|
+
});
|
|
224
|
+
const result = await idle;
|
|
225
|
+
if (generation !== active && generation.leases === 0)
|
|
226
|
+
generations.delete(generation.url);
|
|
227
|
+
return result;
|
|
228
|
+
},
|
|
229
|
+
drain,
|
|
230
|
+
close: async () => await drain(0)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import { runAcpAgent } from "../acp-agent.js";
|
|
5
|
+
async function driveAgent(runner, requests) {
|
|
6
|
+
const input = new PassThrough();
|
|
7
|
+
const output = new PassThrough();
|
|
8
|
+
let raw = "";
|
|
9
|
+
output.on("data", (chunk) => {
|
|
10
|
+
raw += chunk.toString("utf8");
|
|
11
|
+
});
|
|
12
|
+
const done = runAcpAgent({ runner, input, output });
|
|
13
|
+
for (const request of requests) {
|
|
14
|
+
input.write(`${JSON.stringify(request)}\n`);
|
|
15
|
+
}
|
|
16
|
+
input.end();
|
|
17
|
+
await done;
|
|
18
|
+
return raw
|
|
19
|
+
.split("\n")
|
|
20
|
+
.filter((line) => line.trim().length > 0)
|
|
21
|
+
.map((line) => JSON.parse(line));
|
|
22
|
+
}
|
|
23
|
+
test("acp agent completes initialize, session/new, and session/prompt", async () => {
|
|
24
|
+
const runner = async (input) => ({
|
|
25
|
+
finalOutput: `ROUTE_OK:${input.prompt}`,
|
|
26
|
+
runId: "run_acp_1",
|
|
27
|
+
status: "succeeded",
|
|
28
|
+
evidence: ["patch_artifact", "test_report"]
|
|
29
|
+
});
|
|
30
|
+
const messages = await driveAgent(runner, [
|
|
31
|
+
{ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: 1 } },
|
|
32
|
+
{ jsonrpc: "2.0", id: 2, method: "session/new", params: { cwd: "/tmp", mcpServers: [] } },
|
|
33
|
+
{
|
|
34
|
+
jsonrpc: "2.0",
|
|
35
|
+
id: 3,
|
|
36
|
+
method: "session/prompt",
|
|
37
|
+
params: { sessionId: "sess_1", prompt: [{ type: "text", text: "patch the bug" }] }
|
|
38
|
+
}
|
|
39
|
+
]);
|
|
40
|
+
const initialize = messages.find((message) => message.id === 1);
|
|
41
|
+
assert.ok(initialize?.result);
|
|
42
|
+
assert.equal(initialize.result.protocolVersion, 1);
|
|
43
|
+
const sessionNew = messages.find((message) => message.id === 2);
|
|
44
|
+
assert.equal((sessionNew?.result).sessionId, "sess_1");
|
|
45
|
+
const update = messages.find((message) => message.method === "session/update");
|
|
46
|
+
assert.ok(update);
|
|
47
|
+
const updateParams = update.params;
|
|
48
|
+
assert.match(updateParams.update.content.text, /ROUTE_OK:patch the bug/);
|
|
49
|
+
const promptResult = messages.find((message) => message.id === 3);
|
|
50
|
+
const result = promptResult?.result;
|
|
51
|
+
assert.equal(result.stopReason, "end_turn");
|
|
52
|
+
assert.equal(result._meta.runId, "run_acp_1");
|
|
53
|
+
});
|
|
54
|
+
test("acp agent returns method-not-found for unknown methods", async () => {
|
|
55
|
+
const runner = async () => ({
|
|
56
|
+
finalOutput: "unused",
|
|
57
|
+
runId: "run",
|
|
58
|
+
status: "succeeded",
|
|
59
|
+
evidence: []
|
|
60
|
+
});
|
|
61
|
+
const messages = await driveAgent(runner, [
|
|
62
|
+
{ jsonrpc: "2.0", id: 9, method: "nonsense/method", params: {} }
|
|
63
|
+
]);
|
|
64
|
+
const error = messages.find((message) => message.id === 9);
|
|
65
|
+
assert.equal(error?.error?.code, -32601);
|
|
66
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { fetchAcpRegistry, installAcpAdapters } from "../acp-registry.js";
|
|
7
|
+
const FAKE_REGISTRY = {
|
|
8
|
+
agents: [
|
|
9
|
+
{
|
|
10
|
+
id: "codex-cli",
|
|
11
|
+
name: "Codex CLI",
|
|
12
|
+
version: "0.16.0",
|
|
13
|
+
description: "ACP adapter for OpenAI's coding assistant",
|
|
14
|
+
distribution: { type: "npm", package: "@zed-industries/codex-acp" }
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "claude-agent",
|
|
18
|
+
name: "Claude Agent",
|
|
19
|
+
version: "0.46.0",
|
|
20
|
+
description: "ACP wrapper for Anthropic's Claude",
|
|
21
|
+
distribution: { type: "npm", package: "@agentclientprotocol/claude-agent-acp" }
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "no-distribution",
|
|
25
|
+
name: "Broken",
|
|
26
|
+
version: "1.0.0"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
};
|
|
30
|
+
function fakeFetcher(payload = FAKE_REGISTRY) {
|
|
31
|
+
return async () => payload;
|
|
32
|
+
}
|
|
33
|
+
test("fetchAcpRegistry normalizes agent metadata", async () => {
|
|
34
|
+
const registry = await fetchAcpRegistry(fakeFetcher());
|
|
35
|
+
const ids = registry.agents.map((agent) => agent.id);
|
|
36
|
+
assert.ok(ids.includes("codex-cli"));
|
|
37
|
+
assert.ok(ids.includes("claude-agent"));
|
|
38
|
+
});
|
|
39
|
+
test("installAcpAdapters writes metadata for known agents", async () => {
|
|
40
|
+
const dir = mkdtempSync(join(tmpdir(), "acp-registry-"));
|
|
41
|
+
try {
|
|
42
|
+
const installed = await installAcpAdapters({
|
|
43
|
+
agentIds: ["codex-cli", "claude-agent"],
|
|
44
|
+
installDir: dir,
|
|
45
|
+
fetcher: fakeFetcher()
|
|
46
|
+
});
|
|
47
|
+
assert.equal(installed.length, 2);
|
|
48
|
+
const codex = JSON.parse(readFileSync(join(dir, "codex-cli.json"), "utf8"));
|
|
49
|
+
assert.equal(codex.id, "codex-cli");
|
|
50
|
+
assert.equal(codex.version, "0.16.0");
|
|
51
|
+
assert.equal(codex.distribution.package, "@zed-industries/codex-acp");
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
rmSync(dir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
test("installAcpAdapters rejects unknown ids and missing distribution", async () => {
|
|
58
|
+
const dir = mkdtempSync(join(tmpdir(), "acp-registry-"));
|
|
59
|
+
try {
|
|
60
|
+
await assert.rejects(() => installAcpAdapters({ agentIds: ["missing"], installDir: dir, fetcher: fakeFetcher() }), /no agent with id "missing"/);
|
|
61
|
+
await assert.rejects(() => installAcpAdapters({
|
|
62
|
+
agentIds: ["no-distribution"],
|
|
63
|
+
installDir: dir,
|
|
64
|
+
fetcher: fakeFetcher()
|
|
65
|
+
}), /no distribution metadata/);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
rmSync(dir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|