@vellumai/assistant 0.11.4-dev.202608201011.f81ab8d → 0.11.4-dev.202608201321.78202a0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.yaml +41 -0
- package/package.json +1 -1
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/config/feature-flag-registry.json +8 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/web-presence.ts +90 -0
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/tool-policy.ts +67 -0
package/openapi.yaml
CHANGED
|
@@ -4651,6 +4651,47 @@ paths:
|
|
|
4651
4651
|
required:
|
|
4652
4652
|
- recorded
|
|
4653
4653
|
additionalProperties: false
|
|
4654
|
+
/v1/clients/web-presence:
|
|
4655
|
+
post:
|
|
4656
|
+
operationId: clients_webpresence_post
|
|
4657
|
+
summary: Report web tab visibility and focused conversation
|
|
4658
|
+
description:
|
|
4659
|
+
Record the visibility and focused conversation reported by the web client identified by the
|
|
4660
|
+
X-Vellum-Client-Id header.
|
|
4661
|
+
tags:
|
|
4662
|
+
- clients
|
|
4663
|
+
requestBody:
|
|
4664
|
+
required: true
|
|
4665
|
+
content:
|
|
4666
|
+
application/json:
|
|
4667
|
+
schema:
|
|
4668
|
+
type: object
|
|
4669
|
+
properties:
|
|
4670
|
+
visible:
|
|
4671
|
+
type: boolean
|
|
4672
|
+
description: Whether the reporting tab is currently visible.
|
|
4673
|
+
focusedConversationId:
|
|
4674
|
+
anyOf:
|
|
4675
|
+
- type: string
|
|
4676
|
+
- type: "null"
|
|
4677
|
+
description: The conversation currently focused (chat composer on screen) in the tab, or null.
|
|
4678
|
+
required:
|
|
4679
|
+
- visible
|
|
4680
|
+
- focusedConversationId
|
|
4681
|
+
responses:
|
|
4682
|
+
"200":
|
|
4683
|
+
description: Successful response
|
|
4684
|
+
content:
|
|
4685
|
+
application/json:
|
|
4686
|
+
schema:
|
|
4687
|
+
type: object
|
|
4688
|
+
properties:
|
|
4689
|
+
recorded:
|
|
4690
|
+
type: boolean
|
|
4691
|
+
description: Whether a connected client matched the reporting clientId.
|
|
4692
|
+
required:
|
|
4693
|
+
- recorded
|
|
4694
|
+
additionalProperties: false
|
|
4654
4695
|
/v1/config:
|
|
4655
4696
|
get:
|
|
4656
4697
|
operationId: config_get
|
package/package.json
CHANGED
|
@@ -201,6 +201,22 @@ describe("client OS per-row evidence marker", () => {
|
|
|
201
201
|
expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(true);
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
// Mirrors the Mac-originated case above: a plain browser tab reports
|
|
205
|
+
// `client.os: "web"`, while the Electron desktop renderer resolves its own
|
|
206
|
+
// host OS first and persists `macos`/`windows` instead.
|
|
207
|
+
test("marks an OS this row's own transport reported (web)", async () => {
|
|
208
|
+
const ctx = createWebTurnContext("web");
|
|
209
|
+
await persistQueuedMessageBody(ctx, {
|
|
210
|
+
content: "hello",
|
|
211
|
+
requestId: "req-transport-os-web",
|
|
212
|
+
requestClientOs: "web",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(lastUserMetadata().client).toEqual({ os: "web" });
|
|
216
|
+
expect(lastUserMetadata().clientOsFromRequest).toBe(true);
|
|
217
|
+
expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
204
220
|
test("marks an OS this row's own request headers reported", async () => {
|
|
205
221
|
const ctx = createWebTurnContext(undefined);
|
|
206
222
|
await persistQueuedMessageBody(ctx, {
|
|
@@ -44,8 +44,24 @@ import { setSecureKeyAsync } from "../security/secure-keys.js";
|
|
|
44
44
|
import { CredentialBroker } from "../tools/credentials/broker.js";
|
|
45
45
|
import {
|
|
46
46
|
_setMetadataPath,
|
|
47
|
+
type CredentialMetadata,
|
|
47
48
|
upsertCredentialMetadata,
|
|
48
49
|
} from "../tools/credentials/metadata-store.js";
|
|
50
|
+
import { serverUseDenialReason } from "../tools/credentials/tool-policy.js";
|
|
51
|
+
|
|
52
|
+
/** The shared policy's verdict for a state the broker must also deny. */
|
|
53
|
+
function sharedDenialReason(
|
|
54
|
+
metadata: CredentialMetadata,
|
|
55
|
+
toolName: string,
|
|
56
|
+
service: string,
|
|
57
|
+
field: string,
|
|
58
|
+
): string {
|
|
59
|
+
const reason = serverUseDenialReason(metadata, toolName, service, field);
|
|
60
|
+
if (!reason) {
|
|
61
|
+
throw new Error("expected a denial reason from the shared policy");
|
|
62
|
+
}
|
|
63
|
+
return reason;
|
|
64
|
+
}
|
|
49
65
|
|
|
50
66
|
// ---------------------------------------------------------------------------
|
|
51
67
|
// Tests — serverUse (publish_page / unpublish_page regression)
|
|
@@ -518,9 +534,10 @@ describe("CredentialBroker.serverUseById", () => {
|
|
|
518
534
|
if (result.success) {
|
|
519
535
|
throw new Error("expected denial");
|
|
520
536
|
}
|
|
521
|
-
|
|
522
|
-
expect(result.reason).
|
|
523
|
-
|
|
537
|
+
// The broker passes the shared server-use policy's verdict through verbatim.
|
|
538
|
+
expect(result.reason).toBe(
|
|
539
|
+
sharedDenialReason(meta, "unauthorized_tool", "fal", "api_key"),
|
|
540
|
+
);
|
|
524
541
|
});
|
|
525
542
|
|
|
526
543
|
test("returns not found for unknown credential ID", async () => {
|
|
@@ -553,8 +570,10 @@ describe("CredentialBroker.serverUseById", () => {
|
|
|
553
570
|
if (result.success) {
|
|
554
571
|
throw new Error("expected denial");
|
|
555
572
|
}
|
|
556
|
-
|
|
557
|
-
expect(result.reason).
|
|
573
|
+
// The broker passes the shared server-use policy's verdict through verbatim.
|
|
574
|
+
expect(result.reason).toBe(
|
|
575
|
+
sharedDenialReason(meta, "media_proxy", "github", "oauth_token"),
|
|
576
|
+
);
|
|
558
577
|
});
|
|
559
578
|
|
|
560
579
|
test("returns empty injection templates when credential has none", async () => {
|
|
@@ -32,7 +32,10 @@ import {
|
|
|
32
32
|
populateCacheAtBoot,
|
|
33
33
|
resetPluginCacheForTests,
|
|
34
34
|
} from "../plugins/mtime-cache.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
PRESERVED_ENTRIES,
|
|
37
|
+
USER_STATE_ENTRIES,
|
|
38
|
+
} from "../plugins/plugin-tree-walk.js";
|
|
36
39
|
|
|
37
40
|
const ROOT = join(
|
|
38
41
|
tmpdir(),
|
|
@@ -87,6 +90,10 @@ describe("PRESERVED_ENTRIES", () => {
|
|
|
87
90
|
expect(PRESERVED_ENTRIES).toContain("data");
|
|
88
91
|
expect(PRESERVED_ENTRIES).toContain(".disabled");
|
|
89
92
|
});
|
|
93
|
+
|
|
94
|
+
test("user-state entries are the preserved set minus the provenance sidecar", () => {
|
|
95
|
+
expect([...USER_STATE_ENTRIES]).toEqual(["config.json", "data", ".disabled"]);
|
|
96
|
+
});
|
|
90
97
|
});
|
|
91
98
|
|
|
92
99
|
describe("fingerprint exclusion of preserved entries", () => {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { CredentialMetadata } from "../tools/credentials/metadata-store.js";
|
|
4
|
+
import {
|
|
5
|
+
BROWSER_FILL_CAPABILITY,
|
|
6
|
+
isToolAllowed,
|
|
7
|
+
serverUseDenialReason,
|
|
8
|
+
} from "../tools/credentials/tool-policy.js";
|
|
4
9
|
|
|
5
10
|
describe("isToolAllowed", () => {
|
|
6
11
|
// ── Allow cases ─────────────────────────────────────────────────────
|
|
@@ -72,3 +77,94 @@ describe("isToolAllowed", () => {
|
|
|
72
77
|
).toBe(false);
|
|
73
78
|
});
|
|
74
79
|
});
|
|
80
|
+
|
|
81
|
+
describe("serverUseDenialReason", () => {
|
|
82
|
+
function metadata(
|
|
83
|
+
overrides: Partial<CredentialMetadata> = {},
|
|
84
|
+
): CredentialMetadata {
|
|
85
|
+
return {
|
|
86
|
+
credentialId: "cred-1",
|
|
87
|
+
service: "acp",
|
|
88
|
+
field: "claude_oauth_token",
|
|
89
|
+
allowedTools: ["acp_spawn"],
|
|
90
|
+
allowedDomains: [],
|
|
91
|
+
createdAt: 0,
|
|
92
|
+
updatedAt: 0,
|
|
93
|
+
...overrides,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test("denies when metadata is missing", () => {
|
|
98
|
+
expect(
|
|
99
|
+
serverUseDenialReason(
|
|
100
|
+
undefined,
|
|
101
|
+
"acp_spawn",
|
|
102
|
+
"acp",
|
|
103
|
+
"claude_oauth_token",
|
|
104
|
+
),
|
|
105
|
+
).toBe("No credential found for acp/claude_oauth_token");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("denies fail-closed when allowedTools is empty", () => {
|
|
109
|
+
const reason = serverUseDenialReason(
|
|
110
|
+
metadata({ allowedTools: [] }),
|
|
111
|
+
"acp_spawn",
|
|
112
|
+
"acp",
|
|
113
|
+
"claude_oauth_token",
|
|
114
|
+
);
|
|
115
|
+
expect(reason).toContain(
|
|
116
|
+
'Tool "acp_spawn" is not allowed to use credential acp/claude_oauth_token.',
|
|
117
|
+
);
|
|
118
|
+
expect(reason).toContain("No tools are currently allowed");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("denies when allowedTools omits the requesting tool", () => {
|
|
122
|
+
const reason = serverUseDenialReason(
|
|
123
|
+
metadata({ allowedTools: ["bash", "web_fetch"] }),
|
|
124
|
+
"acp_spawn",
|
|
125
|
+
"acp",
|
|
126
|
+
"claude_oauth_token",
|
|
127
|
+
);
|
|
128
|
+
expect(reason).toContain(
|
|
129
|
+
'Tool "acp_spawn" is not allowed to use credential acp/claude_oauth_token.',
|
|
130
|
+
);
|
|
131
|
+
expect(reason).toContain("Allowed tools: bash, web_fetch.");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("allows when a legacy alias canonicalizes to the requesting tool", () => {
|
|
135
|
+
expect(
|
|
136
|
+
serverUseDenialReason(
|
|
137
|
+
metadata({ allowedTools: ["browser_fill_credential"] }),
|
|
138
|
+
BROWSER_FILL_CAPABILITY,
|
|
139
|
+
"acp",
|
|
140
|
+
"claude_oauth_token",
|
|
141
|
+
),
|
|
142
|
+
).toBeUndefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("denies a domain-restricted credential", () => {
|
|
146
|
+
expect(
|
|
147
|
+
serverUseDenialReason(
|
|
148
|
+
metadata({ allowedDomains: ["example.com", "example.org"] }),
|
|
149
|
+
"acp_spawn",
|
|
150
|
+
"acp",
|
|
151
|
+
"claude_oauth_token",
|
|
152
|
+
),
|
|
153
|
+
).toBe(
|
|
154
|
+
"Credential acp/claude_oauth_token has domain restrictions " +
|
|
155
|
+
"(example.com, example.org) and cannot be used server-side. " +
|
|
156
|
+
"Remove domain restrictions or use a separate credential without domain policy.",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("allows fully-permitted metadata", () => {
|
|
161
|
+
expect(
|
|
162
|
+
serverUseDenialReason(
|
|
163
|
+
metadata(),
|
|
164
|
+
"acp_spawn",
|
|
165
|
+
"acp",
|
|
166
|
+
"claude_oauth_token",
|
|
167
|
+
),
|
|
168
|
+
).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for the Claude OAuth config + capture/store helpers.
|
|
3
3
|
*
|
|
4
|
-
* The store helper reaches into secure-keys
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The store helper reaches into secure-keys, so we mock that (wired BEFORE
|
|
5
|
+
* importing the module under test via dynamic import) and assert the vault
|
|
6
|
+
* write targets `credential/acp/claude_oauth_token` and throws when the backend
|
|
7
|
+
* rejects the write.
|
|
8
|
+
*
|
|
9
|
+
* The ACP credential policy is NOT mocked: `hasAcpClaudeToken` has to answer
|
|
10
|
+
* exactly what the spawn-time broker read would, so it is exercised against the
|
|
11
|
+
* real metadata store (pointed at a temp dir) and the real policy evaluation.
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
|
-
import {
|
|
14
|
+
import { randomBytes } from "node:crypto";
|
|
15
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
19
|
|
|
12
20
|
// ---------------------------------------------------------------------------
|
|
13
21
|
// Mocks — wired BEFORE importing the module via dynamic import.
|
|
@@ -19,20 +27,17 @@ const setSecureKeyAsync = mock(
|
|
|
19
27
|
async (_account: string, _value: string) => storeReturn,
|
|
20
28
|
);
|
|
21
29
|
const getSecureKeyAsync = mock(async (_account: string) => getReturn);
|
|
22
|
-
const grantAcpSpawnPolicy = mock(
|
|
23
|
-
(_field: string, _usageDescription: string) => {},
|
|
24
|
-
);
|
|
25
|
-
let spawnCanRead = true;
|
|
26
|
-
const acpSpawnCanReadCredential = mock((_field: string) => spawnCanRead);
|
|
27
30
|
|
|
28
31
|
mock.module("../../security/secure-keys.js", () => ({
|
|
29
32
|
setSecureKeyAsync,
|
|
30
33
|
getSecureKeyAsync,
|
|
31
34
|
}));
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
const { _setMetadataPath, getCredentialMetadata, upsertCredentialMetadata } =
|
|
37
|
+
await import("../../tools/credentials/metadata-store.js");
|
|
38
|
+
|
|
39
|
+
const { acpSpawnCredentialDenialReason } =
|
|
40
|
+
await import("../prepare-agent-env.js");
|
|
36
41
|
|
|
37
42
|
const {
|
|
38
43
|
CLAUDE_OAUTH_CONFIG,
|
|
@@ -43,14 +48,31 @@ const {
|
|
|
43
48
|
hasAcpClaudeToken,
|
|
44
49
|
} = await import("../acp-claude-oauth.js");
|
|
45
50
|
|
|
51
|
+
const ACP_SERVICE = "acp";
|
|
52
|
+
const OAUTH_FIELD = "claude_oauth_token";
|
|
53
|
+
const ACP_SPAWN_TOOL = "acp_spawn";
|
|
54
|
+
|
|
55
|
+
const TEST_DIR = join(
|
|
56
|
+
tmpdir(),
|
|
57
|
+
`vellum-acp-claude-oauth-test-${randomBytes(4).toString("hex")}`,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
function oauthMetadata() {
|
|
61
|
+
return getCredentialMetadata(ACP_SERVICE, OAUTH_FIELD);
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
beforeEach(() => {
|
|
65
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
66
|
+
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
47
67
|
storeReturn = true;
|
|
48
68
|
getReturn = undefined;
|
|
49
|
-
spawnCanRead = true;
|
|
50
69
|
setSecureKeyAsync.mockClear();
|
|
51
70
|
getSecureKeyAsync.mockClear();
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
_setMetadataPath(null);
|
|
75
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
54
76
|
});
|
|
55
77
|
|
|
56
78
|
// ---------------------------------------------------------------------------
|
|
@@ -149,7 +171,7 @@ describe("parseManualClaudeCode", () => {
|
|
|
149
171
|
// ---------------------------------------------------------------------------
|
|
150
172
|
|
|
151
173
|
describe("storeAcpClaudeToken", () => {
|
|
152
|
-
test("writes the token
|
|
174
|
+
test("writes the token to the acp/claude_oauth_token vault field", async () => {
|
|
153
175
|
await storeAcpClaudeToken("sk-ant-oat-token");
|
|
154
176
|
|
|
155
177
|
expect(setSecureKeyAsync).toHaveBeenCalledTimes(1);
|
|
@@ -157,10 +179,25 @@ describe("storeAcpClaudeToken", () => {
|
|
|
157
179
|
"credential/acp/claude_oauth_token",
|
|
158
180
|
"sk-ant-oat-token",
|
|
159
181
|
);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("takes a domain-restricted credential from not-connected to connected", async () => {
|
|
185
|
+
// Invariant: when the vault holds a usable token whose domain policy
|
|
186
|
+
// makes the spawn read fail, the status check keeps the Connect card
|
|
187
|
+
// offered, and completing Connect repairs the policy so the retry after
|
|
188
|
+
// a successful sign-in succeeds. The per-shape repair details are covered
|
|
189
|
+
// by the repairAcpSpawnPolicy suite in prepare-agent-env.test.ts.
|
|
190
|
+
upsertCredentialMetadata(ACP_SERVICE, OAUTH_FIELD, {
|
|
191
|
+
allowedTools: [ACP_SPAWN_TOOL],
|
|
192
|
+
allowedDomains: ["api.anthropic.com"],
|
|
193
|
+
});
|
|
194
|
+
getReturn = "sk-ant-oat-token";
|
|
195
|
+
expect(await hasAcpClaudeToken()).toBe(false);
|
|
196
|
+
|
|
197
|
+
await storeAcpClaudeToken("sk-ant-oat-token");
|
|
198
|
+
|
|
199
|
+
expect(await hasAcpClaudeToken()).toBe(true);
|
|
200
|
+
expect(acpSpawnCredentialDenialReason(OAUTH_FIELD)).toBeUndefined();
|
|
164
201
|
});
|
|
165
202
|
|
|
166
203
|
test("throws when the secure store rejects the write", async () => {
|
|
@@ -169,7 +206,7 @@ describe("storeAcpClaudeToken", () => {
|
|
|
169
206
|
await expect(storeAcpClaudeToken("sk-ant-oat-token")).rejects.toThrow(
|
|
170
207
|
/Failed to store/,
|
|
171
208
|
);
|
|
172
|
-
expect(
|
|
209
|
+
expect(oauthMetadata()).toBeUndefined();
|
|
173
210
|
});
|
|
174
211
|
});
|
|
175
212
|
|
|
@@ -202,13 +239,44 @@ describe("hasAcpClaudeToken", () => {
|
|
|
202
239
|
expect(await hasAcpClaudeToken()).toBe(false);
|
|
203
240
|
});
|
|
204
241
|
|
|
205
|
-
test("reports
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
// it stays not-connected to keep the repair CTA visible.
|
|
242
|
+
test("reports true for metadata with an empty allowedTools (the spawn would grant acp_spawn)", async () => {
|
|
243
|
+
// One allow-state case: the full policy matrix is guarded by the parity
|
|
244
|
+
// suite in prepare-agent-env.test.ts and the tool-policy unit tests.
|
|
245
|
+
upsertCredentialMetadata(ACP_SERVICE, OAUTH_FIELD, { allowedTools: [] });
|
|
210
246
|
getReturn = "sk-ant-oat-token";
|
|
211
|
-
|
|
247
|
+
|
|
248
|
+
expect(await hasAcpClaudeToken()).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("reports false for a domain-restricted credential the broker refuses server-side", async () => {
|
|
252
|
+
// acp_spawn is allowed, but a non-empty allowedDomains scopes the credential
|
|
253
|
+
// to browser fills, so every spawn read is denied. The status check has to
|
|
254
|
+
// see that too, otherwise the Connect card self-dismisses while acp_spawn
|
|
255
|
+
// keeps failing with the missing-token marker.
|
|
256
|
+
upsertCredentialMetadata(ACP_SERVICE, OAUTH_FIELD, {
|
|
257
|
+
allowedTools: [ACP_SPAWN_TOOL],
|
|
258
|
+
allowedDomains: ["api.anthropic.com"],
|
|
259
|
+
});
|
|
260
|
+
getReturn = "sk-ant-oat-token";
|
|
261
|
+
|
|
212
262
|
expect(await hasAcpClaudeToken()).toBe(false);
|
|
213
263
|
});
|
|
264
|
+
|
|
265
|
+
test("writes nothing to the metadata store (the status route is a GET)", async () => {
|
|
266
|
+
upsertCredentialMetadata(ACP_SERVICE, OAUTH_FIELD, { allowedTools: [] });
|
|
267
|
+
getReturn = "sk-ant-oat-token";
|
|
268
|
+
const before = oauthMetadata();
|
|
269
|
+
|
|
270
|
+
await hasAcpClaudeToken();
|
|
271
|
+
|
|
272
|
+
expect(oauthMetadata()).toEqual(before);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("creates no metadata when there is none to begin with", async () => {
|
|
276
|
+
getReturn = "sk-ant-oat-token";
|
|
277
|
+
|
|
278
|
+
await hasAcpClaudeToken();
|
|
279
|
+
|
|
280
|
+
expect(oauthMetadata()).toBeUndefined();
|
|
281
|
+
});
|
|
214
282
|
});
|