@testchimp/cli 0.1.84 → 0.1.86
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/README.md +87 -1
- package/dist/agentwatch/connect.d.ts +37 -0
- package/dist/agentwatch/connect.js +250 -0
- package/dist/agentwatch/connect.test.d.ts +1 -0
- package/dist/agentwatch/connect.test.js +166 -0
- package/dist/agentwatch/credentialsFile.d.ts +40 -0
- package/dist/agentwatch/credentialsFile.js +76 -0
- package/dist/agentwatch/pairing.d.ts +36 -0
- package/dist/agentwatch/pairing.js +161 -0
- package/dist/agentwatch/pairing.test.d.ts +1 -0
- package/dist/agentwatch/pairing.test.js +90 -0
- package/dist/cli/program.d.ts +2 -0
- package/dist/cli/program.js +318 -4
- package/dist/core/client.d.ts +32 -0
- package/dist/core/client.js +133 -9
- package/dist/core/client.test.js +130 -2
- package/dist/core/schemas.d.ts +94 -23
- package/dist/core/schemas.js +56 -0
- package/dist/core/tools.d.ts +5 -1
- package/dist/core/tools.js +125 -0
- package/dist/core/tools.test.js +102 -0
- package/dist/mcp/httpServer.d.ts +14 -0
- package/dist/mcp/httpServer.js +169 -0
- package/dist/mcp/httpServer.test.d.ts +1 -0
- package/dist/mcp/httpServer.test.js +108 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +8 -2
- package/dist/workspace/gitRepo.d.ts +18 -0
- package/dist/workspace/gitRepo.js +103 -0
- package/dist/workspace/projectsRegistry.d.ts +82 -0
- package/dist/workspace/projectsRegistry.js +246 -0
- package/dist/workspace/projectsRegistry.test.d.ts +1 -0
- package/dist/workspace/projectsRegistry.test.js +249 -0
- package/package.json +2 -2
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local AgentWatch credentials in `~/.testchimp/agentwatch/credentials.json` (TESTCHIMP_HOME honoured).
|
|
3
|
+
*
|
|
4
|
+
* Written by `testchimp bot connect`, read by headless AgentWatch (desktop
|
|
5
|
+
* `src/main/auth/agentwatchCredentials.ts`) so it runs without TestChimp Studio sign-in. Keyed by
|
|
6
|
+
* project because the API key is per project. Holds the user's PAT: directory 0700, file 0600.
|
|
7
|
+
*/
|
|
8
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { getTestchimpHome } from "../workspace/projectsRegistry.js";
|
|
12
|
+
export const AgentwatchProjectCredentialsSchema = z.looseObject({
|
|
13
|
+
userId: z.string().min(1),
|
|
14
|
+
email: z.string().optional(),
|
|
15
|
+
userAuthKey: z.string().min(8),
|
|
16
|
+
projectApiKey: z.string().min(8),
|
|
17
|
+
botId: z.string().optional(),
|
|
18
|
+
backendUrl: z.string().url(),
|
|
19
|
+
ingressUrl: z.string().url().optional(),
|
|
20
|
+
savedAtMillis: z.number().int().nonnegative(),
|
|
21
|
+
});
|
|
22
|
+
export const AgentwatchCredentialsFileSchema = z.looseObject({
|
|
23
|
+
schemaVersion: z.literal(1),
|
|
24
|
+
projects: z.record(z.string(), AgentwatchProjectCredentialsSchema),
|
|
25
|
+
});
|
|
26
|
+
export function agentwatchCredentialsPath(env = {}) {
|
|
27
|
+
return join(getTestchimpHome(env), "agentwatch", "credentials.json");
|
|
28
|
+
}
|
|
29
|
+
/** Parsed file, or null when missing / unreadable / another schema. */
|
|
30
|
+
export function readAgentwatchCredentials(env = {}) {
|
|
31
|
+
const path = agentwatchCredentialsPath(env);
|
|
32
|
+
if (!existsSync(path))
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = AgentwatchCredentialsFileSchema.safeParse(JSON.parse(readFileSync(path, "utf8")));
|
|
36
|
+
return parsed.success ? parsed.data : null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function writeAtomic(file, env) {
|
|
43
|
+
const path = agentwatchCredentialsPath(env);
|
|
44
|
+
const dir = join(getTestchimpHome(env), "agentwatch");
|
|
45
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
46
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
47
|
+
writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
|
|
48
|
+
renameSync(tmp, path);
|
|
49
|
+
try {
|
|
50
|
+
chmodSync(path, 0o600);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Windows ignores POSIX modes.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function saveProjectCredentials(projectId, creds, env = {}) {
|
|
57
|
+
const entry = AgentwatchProjectCredentialsSchema.parse(creds);
|
|
58
|
+
const file = readAgentwatchCredentials(env) ?? { schemaVersion: 1, projects: {} };
|
|
59
|
+
file.projects[projectId] = entry;
|
|
60
|
+
writeAtomic(file, env);
|
|
61
|
+
return agentwatchCredentialsPath(env);
|
|
62
|
+
}
|
|
63
|
+
/** True when an entry was removed. Deletes the file once no project is left. */
|
|
64
|
+
export function removeProjectCredentials(projectId, env = {}) {
|
|
65
|
+
const file = readAgentwatchCredentials(env);
|
|
66
|
+
if (!file || !(projectId in file.projects))
|
|
67
|
+
return false;
|
|
68
|
+
delete file.projects[projectId];
|
|
69
|
+
if (Object.keys(file.projects).length === 0) {
|
|
70
|
+
unlinkSync(agentwatchCredentialsPath(env));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
writeAtomic(file, env);
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `testchimp bot connect --pair` / `--finish-pair`: AgentWatch credentials without a second browser consent.
|
|
3
|
+
*
|
|
4
|
+
* Start keeps a random verifier in `~/.testchimp/agentwatch/pairing.json` (0600) and prints the pairing code
|
|
5
|
+
* BASE64URL(SHA-256(verifier)). The QA bot approves that code with its own OAuth token
|
|
6
|
+
* (`approve-agentwatch-pairing`, needs the consent-page "set up AgentWatch" opt-in). Finish redeems with the
|
|
7
|
+
* verifier at `/bots/redeem_agentwatch_pairing` and stores the keys like `bot connect`. The bot only ever
|
|
8
|
+
* sees the pairing code, which is useless without the verifier on this computer.
|
|
9
|
+
*/
|
|
10
|
+
import { type RegistryEnv } from "../workspace/projectsRegistry.js";
|
|
11
|
+
import type { ConnectResult } from "./connect.js";
|
|
12
|
+
/** Matches the server's pairing TTL. */
|
|
13
|
+
export declare const PAIRING_TTL_MS: number;
|
|
14
|
+
export type StartPairingOptions = {
|
|
15
|
+
backendUrl: string;
|
|
16
|
+
ingressUrl?: string;
|
|
17
|
+
expectedProjectId?: string;
|
|
18
|
+
env?: RegistryEnv;
|
|
19
|
+
};
|
|
20
|
+
export type StartPairingResult = {
|
|
21
|
+
pairingCode: string;
|
|
22
|
+
expiresAtMillis: number;
|
|
23
|
+
};
|
|
24
|
+
export type FinishPairingOptions = {
|
|
25
|
+
/** When set, must match the backend the pairing was started against. */
|
|
26
|
+
backendUrl?: string;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
pollIntervalMs?: number;
|
|
29
|
+
env?: RegistryEnv;
|
|
30
|
+
};
|
|
31
|
+
export declare function pendingPairingPath(env?: RegistryEnv): string;
|
|
32
|
+
export declare function pairingCodeFor(verifier: string): string;
|
|
33
|
+
/** Replaces any earlier pending pairing on this computer. */
|
|
34
|
+
export declare function startAgentWatchPairing(opts: StartPairingOptions): StartPairingResult;
|
|
35
|
+
/** Polls until the bot has approved the pending pairing, then stores the credentials and removes the pending file. */
|
|
36
|
+
export declare function finishAgentWatchPairing(opts?: FinishPairingOptions): Promise<ConnectResult>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `testchimp bot connect --pair` / `--finish-pair`: AgentWatch credentials without a second browser consent.
|
|
3
|
+
*
|
|
4
|
+
* Start keeps a random verifier in `~/.testchimp/agentwatch/pairing.json` (0600) and prints the pairing code
|
|
5
|
+
* BASE64URL(SHA-256(verifier)). The QA bot approves that code with its own OAuth token
|
|
6
|
+
* (`approve-agentwatch-pairing`, needs the consent-page "set up AgentWatch" opt-in). Finish redeems with the
|
|
7
|
+
* verifier at `/bots/redeem_agentwatch_pairing` and stores the keys like `bot connect`. The bot only ever
|
|
8
|
+
* sees the pairing code, which is useless without the verifier on this computer.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { getTestchimpHome } from "../workspace/projectsRegistry.js";
|
|
15
|
+
import { saveProjectCredentials } from "./credentialsFile.js";
|
|
16
|
+
/** Matches the server's pairing TTL. */
|
|
17
|
+
export const PAIRING_TTL_MS = 10 * 60_000;
|
|
18
|
+
const DEFAULT_FINISH_TIMEOUT_MS = 60_000;
|
|
19
|
+
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
20
|
+
const PendingPairingSchema = z.looseObject({
|
|
21
|
+
verifier: z.string().min(43),
|
|
22
|
+
pairingCode: z.string().length(43),
|
|
23
|
+
backendUrl: z.string().url(),
|
|
24
|
+
ingressUrl: z.string().url().optional(),
|
|
25
|
+
expectedProjectId: z.string().optional(),
|
|
26
|
+
createdAtMillis: z.number().int().nonnegative(),
|
|
27
|
+
});
|
|
28
|
+
export function pendingPairingPath(env = {}) {
|
|
29
|
+
return join(getTestchimpHome(env), "agentwatch", "pairing.json");
|
|
30
|
+
}
|
|
31
|
+
export function pairingCodeFor(verifier) {
|
|
32
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
33
|
+
}
|
|
34
|
+
function writePending(pending, env) {
|
|
35
|
+
const path = pendingPairingPath(env);
|
|
36
|
+
mkdirSync(join(getTestchimpHome(env), "agentwatch"), { recursive: true, mode: 0o700 });
|
|
37
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
38
|
+
writeFileSync(tmp, `${JSON.stringify(pending, null, 2)}\n`, { mode: 0o600 });
|
|
39
|
+
renameSync(tmp, path);
|
|
40
|
+
try {
|
|
41
|
+
chmodSync(path, 0o600);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Windows ignores POSIX modes.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function readPending(env) {
|
|
48
|
+
const path = pendingPairingPath(env);
|
|
49
|
+
if (!existsSync(path))
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const parsed = PendingPairingSchema.safeParse(JSON.parse(readFileSync(path, "utf8")));
|
|
53
|
+
return parsed.success ? parsed.data : null;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function clearPending(env) {
|
|
60
|
+
try {
|
|
61
|
+
unlinkSync(pendingPairingPath(env));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* already gone */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Replaces any earlier pending pairing on this computer. */
|
|
68
|
+
export function startAgentWatchPairing(opts) {
|
|
69
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
70
|
+
const pairingCode = pairingCodeFor(verifier);
|
|
71
|
+
const createdAtMillis = Date.now();
|
|
72
|
+
writePending({
|
|
73
|
+
verifier,
|
|
74
|
+
pairingCode,
|
|
75
|
+
backendUrl: opts.backendUrl,
|
|
76
|
+
...(opts.ingressUrl ? { ingressUrl: opts.ingressUrl } : {}),
|
|
77
|
+
...(opts.expectedProjectId ? { expectedProjectId: opts.expectedProjectId } : {}),
|
|
78
|
+
createdAtMillis,
|
|
79
|
+
}, opts.env ?? {});
|
|
80
|
+
return { pairingCode, expiresAtMillis: createdAtMillis + PAIRING_TTL_MS };
|
|
81
|
+
}
|
|
82
|
+
function stringField(obj, key) {
|
|
83
|
+
const v = obj[key];
|
|
84
|
+
return typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
85
|
+
}
|
|
86
|
+
async function redeem(backendUrl, verifier) {
|
|
87
|
+
const res = await fetch(`${backendUrl}/bots/redeem_agentwatch_pairing`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: { "Content-Type": "application/json" },
|
|
90
|
+
body: JSON.stringify({ codeVerifier: verifier }),
|
|
91
|
+
});
|
|
92
|
+
const text = await res.text();
|
|
93
|
+
let body = {};
|
|
94
|
+
try {
|
|
95
|
+
body = text ? JSON.parse(text) : {};
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* non-JSON error body */
|
|
99
|
+
}
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
const detail = stringField(body.responseHeader ?? {}, "errorMessage") ?? text.slice(0, 200);
|
|
102
|
+
if (res.status === 404) {
|
|
103
|
+
throw new Error("This TestChimp deployment does not support AgentWatch pairing yet; use testchimp bot connect (browser)");
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Redeeming the AgentWatch pairing failed (${res.status})${detail ? `: ${detail}` : ""}`);
|
|
106
|
+
}
|
|
107
|
+
const creds = body.credentials;
|
|
108
|
+
return creds && typeof creds === "object" ? creds : null;
|
|
109
|
+
}
|
|
110
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
111
|
+
/** Polls until the bot has approved the pending pairing, then stores the credentials and removes the pending file. */
|
|
112
|
+
export async function finishAgentWatchPairing(opts = {}) {
|
|
113
|
+
const env = opts.env ?? {};
|
|
114
|
+
const pending = readPending(env);
|
|
115
|
+
if (!pending) {
|
|
116
|
+
throw new Error("No pending AgentWatch pairing on this computer. Run: testchimp bot connect --pair");
|
|
117
|
+
}
|
|
118
|
+
if (Date.now() > pending.createdAtMillis + PAIRING_TTL_MS) {
|
|
119
|
+
clearPending(env);
|
|
120
|
+
throw new Error("The AgentWatch pairing expired. Run: testchimp bot connect --pair");
|
|
121
|
+
}
|
|
122
|
+
if (opts.backendUrl && opts.backendUrl !== pending.backendUrl) {
|
|
123
|
+
throw new Error(`The pending pairing was started against ${pending.backendUrl}, not ${opts.backendUrl}. Run testchimp bot connect --pair again`);
|
|
124
|
+
}
|
|
125
|
+
const deadline = Date.now() + (opts.timeoutMs ?? DEFAULT_FINISH_TIMEOUT_MS);
|
|
126
|
+
const interval = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
127
|
+
let creds = null;
|
|
128
|
+
for (;;) {
|
|
129
|
+
creds = await redeem(pending.backendUrl, pending.verifier);
|
|
130
|
+
if (creds || Date.now() + interval > deadline)
|
|
131
|
+
break;
|
|
132
|
+
await sleep(interval);
|
|
133
|
+
}
|
|
134
|
+
if (!creds) {
|
|
135
|
+
throw new Error(`The pairing code ${pending.pairingCode} has not been approved yet. Ask your QA bot to approve it, then run testchimp bot connect --finish-pair again`);
|
|
136
|
+
}
|
|
137
|
+
const projectId = stringField(creds, "projectId");
|
|
138
|
+
const userId = stringField(creds, "userId");
|
|
139
|
+
const userAuthKey = stringField(creds, "userAuthKey");
|
|
140
|
+
const projectApiKey = stringField(creds, "projectApiKey");
|
|
141
|
+
if (!projectId || !userId || !userAuthKey || !projectApiKey) {
|
|
142
|
+
throw new Error("AgentWatch credentials response was incomplete");
|
|
143
|
+
}
|
|
144
|
+
clearPending(env);
|
|
145
|
+
if (pending.expectedProjectId && pending.expectedProjectId !== projectId) {
|
|
146
|
+
throw new Error(`The bot approved project ${projectId}, not ${pending.expectedProjectId}; nothing was stored. Connect the bot to the right project and pair again`);
|
|
147
|
+
}
|
|
148
|
+
const email = stringField(creds, "email");
|
|
149
|
+
const botId = stringField(creds, "botId");
|
|
150
|
+
const credentialsPath = saveProjectCredentials(projectId, {
|
|
151
|
+
userId,
|
|
152
|
+
...(email ? { email } : {}),
|
|
153
|
+
userAuthKey,
|
|
154
|
+
projectApiKey,
|
|
155
|
+
...(botId ? { botId } : {}),
|
|
156
|
+
backendUrl: pending.backendUrl,
|
|
157
|
+
...(pending.ingressUrl ? { ingressUrl: pending.ingressUrl } : {}),
|
|
158
|
+
savedAtMillis: Date.now(),
|
|
159
|
+
}, env);
|
|
160
|
+
return { projectId, userId, ...(email ? { email } : {}), ...(botId ? { botId } : {}), credentialsPath };
|
|
161
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
7
|
+
import { readAgentwatchCredentials } from "./credentialsFile.js";
|
|
8
|
+
import { finishAgentWatchPairing, pairingCodeFor, pendingPairingPath, startAgentWatchPairing } from "./pairing.js";
|
|
9
|
+
async function startFakeBackend(projectId = "proj-1") {
|
|
10
|
+
const approved = new Set();
|
|
11
|
+
const redeemed = new Set();
|
|
12
|
+
const fake = {
|
|
13
|
+
url: "",
|
|
14
|
+
server: createServer(),
|
|
15
|
+
approve: (code) => approved.add(code),
|
|
16
|
+
redeemCalls: 0,
|
|
17
|
+
};
|
|
18
|
+
fake.server.on("request", (req, res) => {
|
|
19
|
+
let data = "";
|
|
20
|
+
req.on("data", (c) => (data += c));
|
|
21
|
+
req.on("end", () => {
|
|
22
|
+
if (req.url !== "/bots/redeem_agentwatch_pairing") {
|
|
23
|
+
res.writeHead(404).end();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
27
|
+
fake.redeemCalls++;
|
|
28
|
+
const code = pairingCodeFor(JSON.parse(data).codeVerifier);
|
|
29
|
+
if (!approved.has(code) || redeemed.has(code)) {
|
|
30
|
+
res.end(JSON.stringify({ pending: true }));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
redeemed.add(code);
|
|
34
|
+
res.end(JSON.stringify({
|
|
35
|
+
credentials: {
|
|
36
|
+
projectId,
|
|
37
|
+
userId: "user-1",
|
|
38
|
+
email: "a@b.c",
|
|
39
|
+
userAuthKey: "pat-123456789",
|
|
40
|
+
projectApiKey: "key-123456789",
|
|
41
|
+
botId: "bot-1",
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
await new Promise((r) => fake.server.listen(0, "127.0.0.1", r));
|
|
47
|
+
fake.url = `http://127.0.0.1:${fake.server.address().port}`;
|
|
48
|
+
return fake;
|
|
49
|
+
}
|
|
50
|
+
describe("AgentWatch pairing", () => {
|
|
51
|
+
let home;
|
|
52
|
+
let fake;
|
|
53
|
+
let env;
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
home = mkdtempSync(join(tmpdir(), "aw-pair-"));
|
|
56
|
+
env = { home };
|
|
57
|
+
fake = await startFakeBackend();
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
fake.server.close();
|
|
61
|
+
rmSync(home, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
it("stores credentials once the bot approves, and keeps the verifier private", async () => {
|
|
64
|
+
const started = startAgentWatchPairing({ backendUrl: fake.url, expectedProjectId: "proj-1", env });
|
|
65
|
+
assert.match(started.pairingCode, /^[A-Za-z0-9_-]{43}$/);
|
|
66
|
+
if (process.platform !== "win32") {
|
|
67
|
+
assert.equal(statSync(pendingPairingPath(env)).mode & 0o777, 0o600);
|
|
68
|
+
}
|
|
69
|
+
await assert.rejects(finishAgentWatchPairing({ backendUrl: fake.url, timeoutMs: 50, pollIntervalMs: 10, env }), /has not been approved yet/);
|
|
70
|
+
fake.approve(started.pairingCode);
|
|
71
|
+
const result = await finishAgentWatchPairing({ backendUrl: fake.url, timeoutMs: 1000, pollIntervalMs: 10, env });
|
|
72
|
+
assert.deepEqual({ projectId: result.projectId, userId: result.userId, botId: result.botId }, { projectId: "proj-1", userId: "user-1", botId: "bot-1" });
|
|
73
|
+
const stored = readAgentwatchCredentials(env).projects["proj-1"];
|
|
74
|
+
assert.equal(stored.userAuthKey, "pat-123456789");
|
|
75
|
+
assert.equal(stored.backendUrl, fake.url);
|
|
76
|
+
assert.equal(existsSync(pendingPairingPath(env)), false);
|
|
77
|
+
await assert.rejects(finishAgentWatchPairing({ backendUrl: fake.url, env }), /No pending AgentWatch pairing/);
|
|
78
|
+
});
|
|
79
|
+
it("stores nothing when the bot approved another project", async () => {
|
|
80
|
+
const started = startAgentWatchPairing({ backendUrl: fake.url, expectedProjectId: "proj-2", env });
|
|
81
|
+
fake.approve(started.pairingCode);
|
|
82
|
+
await assert.rejects(finishAgentWatchPairing({ backendUrl: fake.url, timeoutMs: 1000, pollIntervalMs: 10, env }), /approved project proj-1, not proj-2/);
|
|
83
|
+
assert.equal(readAgentwatchCredentials(env), null);
|
|
84
|
+
});
|
|
85
|
+
it("refuses to finish against a different backend", async () => {
|
|
86
|
+
startAgentWatchPairing({ backendUrl: fake.url, env });
|
|
87
|
+
await assert.rejects(finishAgentWatchPairing({ backendUrl: "https://featureservice.testchimp.io", env }), /was started against/);
|
|
88
|
+
assert.equal(fake.redeemCalls, 0);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/dist/cli/program.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { PACKAGE_VERSION } from "../core/version.js";
|
|
3
3
|
export { PACKAGE_VERSION };
|
|
4
|
+
/** Numeric x.y.z comparison; prerelease / build suffixes are ignored. */
|
|
5
|
+
export declare function compareSemver(a: string, b: string): number;
|
|
4
6
|
export declare function buildCliProgram(): Command;
|