@wenathlan/getry 2.0.154 → 2.0.155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +182 -11
- package/tests/cli.test.ts +300 -0
- package/tests/debonairlane.test.ts +66 -21
- package/tests/gateway.test.ts +962 -0
- package/tests/gatewayserver.ts +99 -0
- package/tests/intelligence.test.ts +62 -0
- package/tests/jsonpolicy.test.ts +289 -0
- package/tests/llm.test.ts +696 -0
- package/tests/modelroute.test.ts +112 -0
- package/tests/promptlibrary.test.ts +116 -0
- package/tests/provider.constants.test.ts +134 -0
- package/tests/provider.fingerprint.test.ts +149 -0
- package/tests/provider.models.test.ts +250 -0
- package/tests/provider.oauth.test.ts +150 -0
- package/tests/providercli.test.ts +84 -0
- package/tests/providerfixtures.ts +224 -0
- package/tests/researchfixtures.ts +4572 -0
- package/tests/server.auth.test.ts +394 -0
- package/tests/server.engine.test.ts +1074 -0
- package/tests/server.property.test.ts +220 -0
- package/tests/server.utils.test.ts +546 -0
- package/tests/torture.llm.test.ts +807 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file models.test.ts
|
|
3
|
+
* @description
|
|
4
|
+
* Tests for the models module of maene (`../models.js`). Confirms the
|
|
5
|
+
* logical->physical routing helpers (`routeModelViaOmniRoute`,
|
|
6
|
+
* `mapModelFor9Router`) plus catalog smoke checks: catalog presence,
|
|
7
|
+
* lookup determinism, unknown-id fallback.
|
|
8
|
+
*
|
|
9
|
+
* Runs on the vitest runner with `node:assert/strict` assertions. Imports only leaf modules; the
|
|
10
|
+
* models module pulls only `node:crypto` + `node:os`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { it as test } from "vitest";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
ALL_MODELS_WITH_PREFIX,
|
|
18
|
+
ANTIGRAVITY_MODELS,
|
|
19
|
+
DEFAULT_MODEL,
|
|
20
|
+
FALLBACKS,
|
|
21
|
+
GEMINICLI_MODELS,
|
|
22
|
+
getModel,
|
|
23
|
+
hasModel,
|
|
24
|
+
isCLIOnly,
|
|
25
|
+
listBareModelIds,
|
|
26
|
+
listModelIds,
|
|
27
|
+
MODEL_ALIASES_2026_08_25,
|
|
28
|
+
MODEL_CATALOG_2026_08_25,
|
|
29
|
+
MODEL_CATALOG_GROUPED,
|
|
30
|
+
MODEL_MAP_2026_08_25,
|
|
31
|
+
MODELS_2026,
|
|
32
|
+
mapModelFor9Router,
|
|
33
|
+
normalizeModelId,
|
|
34
|
+
OMNIROUTE_FALLBACK_MODEL,
|
|
35
|
+
routeModelViaOmniRoute,
|
|
36
|
+
SEARCH_MODEL,
|
|
37
|
+
} from "../models.js";
|
|
38
|
+
|
|
39
|
+
test("MODEL_CATALOG_2026_08_25 is a frozen non-empty array", () => {
|
|
40
|
+
try {
|
|
41
|
+
assert.ok(Array.isArray(MODEL_CATALOG_2026_08_25), "must be an array");
|
|
42
|
+
assert.ok(MODEL_CATALOG_2026_08_25.length > 0, "must have entries");
|
|
43
|
+
assert.equal(Object.isFrozen(MODEL_CATALOG_2026_08_25), true, "must be frozen");
|
|
44
|
+
} catch (err) {
|
|
45
|
+
assert.fail(`MODEL_CATALOG check failed: ${(err as Error).message}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("MODEL_MAP_2026_08_25 is a Map that resolves every catalog id", () => {
|
|
50
|
+
try {
|
|
51
|
+
assert.ok(MODEL_MAP_2026_08_25 instanceof Map, "MODEL_MAP must be a Map");
|
|
52
|
+
for (const m of MODEL_CATALOG_2026_08_25) {
|
|
53
|
+
const got = MODEL_MAP_2026_08_25.get(m.id.toLowerCase());
|
|
54
|
+
assert.ok(got, `MODEL_MAP must contain id ${m.id}`);
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
assert.fail(`MODEL_MAP check failed: ${(err as Error).message}`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("getModel resolves known ids and returns undefined for unknown", () => {
|
|
62
|
+
try {
|
|
63
|
+
const a = getModel("gemini-3.7-flash");
|
|
64
|
+
assert.ok(a, "gemini-3.7-flash must resolve");
|
|
65
|
+
assert.equal(a?.id, "gemini-3.7-flash");
|
|
66
|
+
|
|
67
|
+
const b = getModel("claude-opus-4-6-thinking");
|
|
68
|
+
assert.ok(b, "claude-opus-4-6-thinking must resolve");
|
|
69
|
+
assert.equal(b?.id, "claude-opus-4-6-thinking");
|
|
70
|
+
|
|
71
|
+
// antigravity- prefixed variant of the same id must resolve too.
|
|
72
|
+
const c = getModel("antigravity-gemini-3.7-flash");
|
|
73
|
+
assert.ok(c, "antigravity- prefixed alias must resolve");
|
|
74
|
+
|
|
75
|
+
// unknown id returns undefined.
|
|
76
|
+
const z = getModel("not-a-real-model-id");
|
|
77
|
+
assert.equal(z, undefined, "unknown id must return undefined");
|
|
78
|
+
|
|
79
|
+
// empty/null must not throw.
|
|
80
|
+
assert.equal(getModel(""), undefined);
|
|
81
|
+
assert.equal(getModel(null as unknown as string), undefined);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
assert.fail(`getModel check failed: ${(err as Error).message}`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("hasModel mirrors getModel", () => {
|
|
88
|
+
try {
|
|
89
|
+
assert.equal(hasModel("gemini-3.7-flash"), true);
|
|
90
|
+
assert.equal(hasModel("claude-opus-4-6-thinking"), true);
|
|
91
|
+
assert.equal(hasModel("not-real"), false);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
assert.fail(`hasModel check failed: ${(err as Error).message}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("routeModelViaOmniRoute returns the physical id + group + endpoint list for known ids", () => {
|
|
98
|
+
try {
|
|
99
|
+
const r = routeModelViaOmniRoute("gemini-3.7-flash");
|
|
100
|
+
assert.ok(r, "must return a routing result");
|
|
101
|
+
assert.equal(r.physicalId, "gemini-3.7-flash", "physicalId must match the input id for a known model");
|
|
102
|
+
assert.ok(r.group === "antigravity" || r.group === "gemini-cli", "group must be a known ModelGroup");
|
|
103
|
+
assert.ok(Array.isArray(r.endpoints), "endpoints must be an array");
|
|
104
|
+
assert.ok(r.endpoints.length > 0, "endpoints must be non-empty");
|
|
105
|
+
assert.equal(typeof r.prodOnly, "boolean", "prodOnly must be boolean");
|
|
106
|
+
assert.equal(typeof r.fallbackAllowed, "boolean", "fallbackAllowed must be boolean");
|
|
107
|
+
} catch (err) {
|
|
108
|
+
assert.fail(`routeModelViaOmniRoute known-id check failed: ${(err as Error).message}`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("routeModelViaOmniRoute round-trip: same id twice -> same result", () => {
|
|
113
|
+
try {
|
|
114
|
+
const a = routeModelViaOmniRoute("gemini-3.7-flash");
|
|
115
|
+
const b = routeModelViaOmniRoute("gemini-3.7-flash");
|
|
116
|
+
assert.deepEqual(a, b, "routing must be deterministic across calls");
|
|
117
|
+
} catch (err) {
|
|
118
|
+
assert.fail(`routeModelViaOmniRoute round-trip check failed: ${(err as Error).message}`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("routeModelViaOmniRoute unknown id falls back to OMNIROUTE_FALLBACK_MODEL on gemini-cli pool", () => {
|
|
123
|
+
try {
|
|
124
|
+
const r = routeModelViaOmniRoute("does-not-exist-xyz");
|
|
125
|
+
assert.equal(r.physicalId, OMNIROUTE_FALLBACK_MODEL, "unknown id must fall back to OMNIROUTE_FALLBACK_MODEL");
|
|
126
|
+
assert.equal(r.group, "gemini-cli", "fallback group must be gemini-cli");
|
|
127
|
+
assert.equal(r.prodOnly, true, "fallback must be prod-only (skip sandbox)");
|
|
128
|
+
assert.equal(r.fallbackAllowed, false, "fallback must disallow project fallback (403 risk)");
|
|
129
|
+
} catch (err) {
|
|
130
|
+
assert.fail(`routeModelViaOmniRoute unknown-id check failed: ${(err as Error).message}`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("mapModelFor9Router returns provider + model + group for known ids", () => {
|
|
135
|
+
try {
|
|
136
|
+
const m = mapModelFor9Router("gemini-3.7-flash");
|
|
137
|
+
assert.ok(m, "must return a 9router mapping");
|
|
138
|
+
assert.equal(typeof m.provider, "string", "provider must be string");
|
|
139
|
+
assert.equal(typeof m.model, "string", "model must be string");
|
|
140
|
+
assert.ok(m.group === "antigravity" || m.group === "gemini-cli", "group must be a known ModelGroup");
|
|
141
|
+
// For a known id, .model should equal the canonical id.
|
|
142
|
+
assert.equal(m.model, "gemini-3.7-flash");
|
|
143
|
+
} catch (err) {
|
|
144
|
+
assert.fail(`mapModelFor9Router known-id check failed: ${(err as Error).message}`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("mapModelFor9Router unknown id falls back to normalized id + gemini-cli group", () => {
|
|
149
|
+
try {
|
|
150
|
+
const m = mapModelFor9Router("gpt-4-turbo");
|
|
151
|
+
assert.equal(m.provider, "google", "unknown id must default provider to google");
|
|
152
|
+
assert.equal(m.group, "gemini-cli", "unknown id must default group to gemini-cli");
|
|
153
|
+
assert.equal(m.model, "gpt-4-turbo", "unknown id model must be the normalized id");
|
|
154
|
+
} catch (err) {
|
|
155
|
+
assert.fail(`mapModelFor9Router unknown-id check failed: ${(err as Error).message}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("normalizeModelId strips antigravity- prefix and lowercases", () => {
|
|
160
|
+
try {
|
|
161
|
+
assert.equal(normalizeModelId("Antigravity-Gemini-3.7-Flash"), "gemini-3.7-flash");
|
|
162
|
+
assert.equal(normalizeModelId("gemini-3.7-flash"), "gemini-3.7-flash");
|
|
163
|
+
assert.equal(normalizeModelId(" gemini-3.7-flash "), "gemini-3.7-flash");
|
|
164
|
+
} catch (err) {
|
|
165
|
+
assert.fail(`normalizeModelId check failed: ${(err as Error).message}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("listModelIds and listBareModelIds return non-empty arrays of strings", () => {
|
|
170
|
+
try {
|
|
171
|
+
const all = listModelIds();
|
|
172
|
+
assert.ok(Array.isArray(all));
|
|
173
|
+
assert.ok(all.length > 0);
|
|
174
|
+
|
|
175
|
+
const bare = listBareModelIds();
|
|
176
|
+
assert.ok(Array.isArray(bare));
|
|
177
|
+
assert.ok(bare.length > 0);
|
|
178
|
+
|
|
179
|
+
// listBareModelIds must not contain antigravity- prefixed ids.
|
|
180
|
+
assert.ok(!bare.some((id) => id.startsWith("antigravity-")), "bare ids must not have antigravity- prefix");
|
|
181
|
+
} catch (err) {
|
|
182
|
+
assert.fail(`list ids check failed: ${(err as Error).message}`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("isCLIOnly flags gemini-3+ / preview / customtools models", () => {
|
|
187
|
+
try {
|
|
188
|
+
assert.equal(isCLIOnly("gemini-3.7-flash-preview"), true, "preview must be CLI-only");
|
|
189
|
+
assert.equal(isCLIOnly("gemini-3.6-flash"), true, "3.6 must be CLI-only");
|
|
190
|
+
assert.equal(isCLIOnly("gemini-3.1-pro-preview-customtools"), true, "customtools must be CLI-only");
|
|
191
|
+
// antigravity-pool bare ids (not preview, not 3+) should be NOT CLI-only.
|
|
192
|
+
assert.equal(isCLIOnly("gpt-oss-120b-medium"), false, "gpt-oss must NOT be CLI-only");
|
|
193
|
+
} catch (err) {
|
|
194
|
+
assert.fail(`isCLIOnly check failed: ${(err as Error).message}`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("MODEL_ALIASES_2026_08_25 is a Map", () => {
|
|
199
|
+
try {
|
|
200
|
+
assert.ok(MODEL_ALIASES_2026_08_25 instanceof Map, "MODEL_ALIASES must be a Map");
|
|
201
|
+
// antigravity- prefixed aliases are part of the alias map.
|
|
202
|
+
assert.ok(MODEL_ALIASES_2026_08_25.size >= 0, "size must be a number (may be 0 if no aliases)");
|
|
203
|
+
} catch (err) {
|
|
204
|
+
assert.fail(`MODEL_ALIASES check failed: ${(err as Error).message}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("MODEL_CATALOG_GROUPED exposes per-group / per-level slices", () => {
|
|
209
|
+
try {
|
|
210
|
+
assert.equal(typeof MODEL_CATALOG_GROUPED, "object");
|
|
211
|
+
assert.ok(Array.isArray(MODEL_CATALOG_GROUPED.antigravity), "antigravity slice must be array");
|
|
212
|
+
assert.ok(Array.isArray(MODEL_CATALOG_GROUPED["gemini-cli"]), "gemini-cli slice must be array");
|
|
213
|
+
assert.ok(Array.isArray(MODEL_CATALOG_GROUPED.preview), "preview slice must be array");
|
|
214
|
+
assert.ok(Array.isArray(MODEL_CATALOG_GROUPED.stable), "stable slice must be array");
|
|
215
|
+
} catch (err) {
|
|
216
|
+
assert.fail(`MODEL_CATALOG_GROUPED check failed: ${(err as Error).message}`);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("DEFAULT_MODEL + SEARCH_MODEL + FALLBACKS are sensible constant strings", () => {
|
|
221
|
+
try {
|
|
222
|
+
assert.equal(typeof DEFAULT_MODEL, "string");
|
|
223
|
+
assert.equal(typeof SEARCH_MODEL, "string");
|
|
224
|
+
assert.ok(DEFAULT_MODEL.startsWith("antigravity-"), "DEFAULT_MODEL must be antigravity- prefixed");
|
|
225
|
+
assert.ok(SEARCH_MODEL.startsWith("gemini-"), "SEARCH_MODEL must be a bare gemini id");
|
|
226
|
+
|
|
227
|
+
assert.equal(typeof FALLBACKS, "object");
|
|
228
|
+
assert.equal(FALLBACKS.project, "rising-fact-p41fc");
|
|
229
|
+
assert.equal(FALLBACKS.version, "1.19.2");
|
|
230
|
+
assert.ok(FALLBACKS.ua.startsWith("antigravity/"), "FALLBACKS.ua must start with antigravity/");
|
|
231
|
+
} catch (err) {
|
|
232
|
+
assert.fail(`DEFAULT_MODEL / SEARCH_MODEL / FALLBACKS check failed: ${(err as Error).message}`);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("MODELS_2026 + ANTIGRAVITY_MODELS + GEMINICLI_MODELS + ALL_MODELS_WITH_PREFIX arrays are non-empty", () => {
|
|
237
|
+
try {
|
|
238
|
+
assert.ok(Array.isArray(MODELS_2026) && MODELS_2026.length > 0);
|
|
239
|
+
assert.ok(Array.isArray(ANTIGRAVITY_MODELS) && ANTIGRAVITY_MODELS.length > 0);
|
|
240
|
+
assert.ok(Array.isArray(GEMINICLI_MODELS) && GEMINICLI_MODELS.length > 0);
|
|
241
|
+
assert.ok(Array.isArray(ALL_MODELS_WITH_PREFIX) && ALL_MODELS_WITH_PREFIX.length > 0);
|
|
242
|
+
|
|
243
|
+
// Every antigravity id must start with antigravity-.
|
|
244
|
+
for (const id of ANTIGRAVITY_MODELS) {
|
|
245
|
+
assert.ok(id.startsWith("antigravity-"), `id ${id} must be antigravity- prefixed`);
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
assert.fail(`flat catalog arrays check failed: ${(err as Error).message}`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file oauth.test.ts
|
|
3
|
+
* @description
|
|
4
|
+
* Tests for the pure PKCE / crypto helpers exported from the consolidated
|
|
5
|
+
* authentication module (`../auth.js`, v2.1.14 merge of oauth.ts +
|
|
6
|
+
* antigravity-cli.ts into auth.ts). Strictly tests ONLY the pure functions
|
|
7
|
+
* (`generatePKCE`, `buildAuthUrl`, `extractCodeFromUrl`,
|
|
8
|
+
* `isValidTokenResponse`) — does NOT trigger the OAuth server flow, does NOT
|
|
9
|
+
* spin up a callback server, does NOT call out to the network.
|
|
10
|
+
*
|
|
11
|
+
* Per RFC 7636: the verifier is a high-entropy base64url string of length
|
|
12
|
+
* 43..128; the challenge is BASE64URL(SHA256(verifier_ascii_bytes)) with
|
|
13
|
+
* no padding. maene's `generatePKCE` uses 64 random bytes -> 86 chars,
|
|
14
|
+
* method "S256".
|
|
15
|
+
*
|
|
16
|
+
* Runs on the vitest runner with `node:assert/strict` assertions. Imports only leaf modules; the
|
|
17
|
+
* consolidated auth module pulls only `node:*` builtins + global fetch.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { it as test } from "vitest";
|
|
23
|
+
|
|
24
|
+
import { buildAuthUrl, extractCodeFromUrl, generatePKCE, isValidTokenResponse } from "../oauth.js";
|
|
25
|
+
|
|
26
|
+
test("generatePKCE returns a non-empty verifier + base64url S256 challenge + method S256", () => {
|
|
27
|
+
try {
|
|
28
|
+
const pkce = generatePKCE();
|
|
29
|
+
assert.ok(pkce, "generatePKCE must return an object");
|
|
30
|
+
assert.equal(typeof pkce.verifier, "string", "verifier must be a string");
|
|
31
|
+
assert.ok(pkce.verifier.length >= 43, "verifier must be >= 43 chars per RFC 7636");
|
|
32
|
+
assert.ok(pkce.verifier.length <= 128, "verifier must be <= 128 chars per RFC 7636");
|
|
33
|
+
assert.equal(typeof pkce.challenge, "string", "challenge must be a string");
|
|
34
|
+
assert.ok(pkce.challenge.length > 0, "challenge must be non-empty");
|
|
35
|
+
assert.equal(pkce.method, "S256", "method must be S256");
|
|
36
|
+
|
|
37
|
+
// Verifier must be base64url: no '+', '/', or '='.
|
|
38
|
+
assert.ok(!/[+/=]/.test(pkce.verifier), "verifier must be base64url (no +, /, =)");
|
|
39
|
+
|
|
40
|
+
// Challenge must be base64url too.
|
|
41
|
+
assert.ok(!/[+/=]/.test(pkce.challenge), "challenge must be base64url (no +, /, =)");
|
|
42
|
+
} catch (err) {
|
|
43
|
+
assert.fail(`generatePKCE shape check failed: ${(err as Error).message}`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("generatePKCE challenge equals base64url(sha256(verifier_ascii))", () => {
|
|
48
|
+
try {
|
|
49
|
+
const pkce = generatePKCE();
|
|
50
|
+
// RFC 7636 S256: challenge = base64url(SHA256(verifier ASCII bytes)).
|
|
51
|
+
const expected = createHash("sha256")
|
|
52
|
+
.update(pkce.verifier) // ASCII bytes of the verifier string
|
|
53
|
+
.digest("base64url");
|
|
54
|
+
assert.equal(pkce.challenge, expected, "challenge must match RFC 7636 S256");
|
|
55
|
+
} catch (err) {
|
|
56
|
+
assert.fail(`generatePKCE S256 check failed: ${(err as Error).message}`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("generatePKCE verifier + challenge are unique across two calls", () => {
|
|
61
|
+
try {
|
|
62
|
+
const a = generatePKCE();
|
|
63
|
+
const b = generatePKCE();
|
|
64
|
+
assert.notEqual(a.verifier, b.verifier, "two verifiers must differ (cryptographic randomness)");
|
|
65
|
+
assert.notEqual(a.challenge, b.challenge, "two challenges must differ");
|
|
66
|
+
} catch (err) {
|
|
67
|
+
assert.fail(`generatePKCE uniqueness check failed: ${(err as Error).message}`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("buildAuthUrl builds a valid OAuth URL with state, client_id, and PKCE challenge", () => {
|
|
72
|
+
try {
|
|
73
|
+
const pkce = generatePKCE();
|
|
74
|
+
const url = buildAuthUrl(pkce, "http://127.0.0.1:12345/callback");
|
|
75
|
+
assert.ok(url.startsWith("https://accounts.google.com/"), "must target Google OAuth");
|
|
76
|
+
const parsed = new URL(url);
|
|
77
|
+
assert.ok(parsed.searchParams.get("client_id"), "client_id must be present and non-empty");
|
|
78
|
+
assert.equal(parsed.searchParams.get("response_type"), "code");
|
|
79
|
+
assert.equal(parsed.searchParams.get("redirect_uri"), "http://127.0.0.1:12345/callback");
|
|
80
|
+
assert.equal(parsed.searchParams.get("code_challenge"), pkce.challenge);
|
|
81
|
+
assert.equal(parsed.searchParams.get("code_challenge_method"), "S256");
|
|
82
|
+
assert.equal(parsed.searchParams.get("access_type"), "offline");
|
|
83
|
+
assert.ok(parsed.searchParams.get("state"), "state must be present and non-empty");
|
|
84
|
+
} catch (err) {
|
|
85
|
+
assert.fail(`buildAuthUrl check failed: ${(err as Error).message}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("buildAuthUrl works without a PKCE pair (state-only Gemini CLI parity)", () => {
|
|
90
|
+
try {
|
|
91
|
+
const url = buildAuthUrl(null, "http://127.0.0.1:9999/callback");
|
|
92
|
+
const parsed = new URL(url);
|
|
93
|
+
assert.equal(parsed.searchParams.get("code_challenge"), null, "code_challenge must be absent when no PKCE");
|
|
94
|
+
assert.equal(
|
|
95
|
+
parsed.searchParams.get("code_challenge_method"),
|
|
96
|
+
null,
|
|
97
|
+
"code_challenge_method must be absent when no PKCE",
|
|
98
|
+
);
|
|
99
|
+
assert.ok(parsed.searchParams.get("state"), "state must still be present");
|
|
100
|
+
} catch (err) {
|
|
101
|
+
assert.fail(`buildAuthUrl no-PKCE check failed: ${(err as Error).message}`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("extractCodeFromUrl extracts the authorization code from a callback URL", () => {
|
|
106
|
+
try {
|
|
107
|
+
const code = extractCodeFromUrl("http://127.0.0.1:12345/callback?code=4/0AX4-abcdef&state=st");
|
|
108
|
+
assert.equal(code, "4/0AX4-abcdef");
|
|
109
|
+
} catch (err) {
|
|
110
|
+
assert.fail(`extractCodeFromUrl valid check failed: ${(err as Error).message}`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("extractCodeFromUrl returns null when no code param is present", () => {
|
|
115
|
+
try {
|
|
116
|
+
const code = extractCodeFromUrl("http://127.0.0.1:12345/callback?error=access_denied");
|
|
117
|
+
assert.equal(code, null, "must return null when there is no code");
|
|
118
|
+
} catch (err) {
|
|
119
|
+
assert.fail(`extractCodeFromUrl no-code check failed: ${(err as Error).message}`);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("isValidTokenResponse accepts well-formed objects and rejects malformed ones", () => {
|
|
124
|
+
try {
|
|
125
|
+
// The actual validator requires access_token (string), expires_at (number),
|
|
126
|
+
// and expires_in (number). It does NOT require token_type.
|
|
127
|
+
assert.ok(
|
|
128
|
+
isValidTokenResponse({ access_token: "abc", expires_at: 1234, expires_in: 3600 }),
|
|
129
|
+
"well-formed token response (access_token + expires_at + expires_in) must be valid",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// null/empty/string — all falsy return values (the impl short-circuits
|
|
133
|
+
// via `obj && ...`, so null returns null, not strictly false).
|
|
134
|
+
assert.ok(!isValidTokenResponse(null), "null must be falsy/invalid");
|
|
135
|
+
assert.ok(!isValidTokenResponse({}), "empty object must be invalid (missing access_token)");
|
|
136
|
+
assert.ok(!isValidTokenResponse("string"), "string must be invalid");
|
|
137
|
+
|
|
138
|
+
// Missing one of the required number fields must also fail.
|
|
139
|
+
assert.ok(!isValidTokenResponse({ access_token: "abc", expires_in: 3600 }), "missing expires_at must be invalid");
|
|
140
|
+
} catch (err) {
|
|
141
|
+
assert.fail(`isValidTokenResponse check failed: ${(err as Error).message}`);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("oauth module import smoke (does not throw on side-effect-free import)", () => {
|
|
146
|
+
// The act of importing the module already happened at the top of this file.
|
|
147
|
+
// If the module had any module-eval-time error, this test file would never
|
|
148
|
+
// have loaded. So this test is a no-op smoke assertion.
|
|
149
|
+
assert.ok(true, "oauth module imported without throwing");
|
|
150
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* providercli — the provider account console smoke (the ownership move of
|
|
3
|
+
* the gateway ownership wave, step 5): the console family answers from the
|
|
4
|
+
* getry owner now, and this lane pins the load-bearing seams of the move —
|
|
5
|
+
* the help text renders, the accounts path resolver agrees with the
|
|
6
|
+
* accounts owner's own resolution, the opencode model definitions answer
|
|
7
|
+
* from the models owner with the catalog shape, and the twin deletion
|
|
8
|
+
* (getDefaultAccountManager behind the historical getAccountManager name)
|
|
9
|
+
* answers the accounts owner's real manager. No network, no browser.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it, vi } from "vitest";
|
|
13
|
+
import {
|
|
14
|
+
buildOpenCodeModelDefinitions,
|
|
15
|
+
getAccountManager,
|
|
16
|
+
printHelp,
|
|
17
|
+
providercli,
|
|
18
|
+
resolveAccountsPath,
|
|
19
|
+
runproviderfamily,
|
|
20
|
+
} from "../providercli.js";
|
|
21
|
+
import { getDefaultAccountManager, resolveAccountsPath as accountsResolveAccountsPath } from "../accounts.js";
|
|
22
|
+
import { MODELS_2026_CATALOG } from "../models.js";
|
|
23
|
+
|
|
24
|
+
describe("providercli smoke", () => {
|
|
25
|
+
it("renders the help text", () => {
|
|
26
|
+
const logged: string[] = [];
|
|
27
|
+
const spy = vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => {
|
|
28
|
+
logged.push(parts.join(" "));
|
|
29
|
+
});
|
|
30
|
+
try {
|
|
31
|
+
printHelp();
|
|
32
|
+
} finally {
|
|
33
|
+
spy.mockRestore();
|
|
34
|
+
}
|
|
35
|
+
const text = logged.join("\n");
|
|
36
|
+
expect(text).toContain("devthink provider CLI (unified merge)");
|
|
37
|
+
expect(text).toContain("Usage:");
|
|
38
|
+
expect(text).toContain("cli.ts <command> [subcommand] [flags]");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("resolves the accounts path the accounts owner resolves", () => {
|
|
42
|
+
expect(resolveAccountsPath()).toBe(accountsResolveAccountsPath());
|
|
43
|
+
expect(resolveAccountsPath()).toContain("antigravity-accounts.json");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("builds the opencode model definitions with the catalog shape", () => {
|
|
47
|
+
const defs = buildOpenCodeModelDefinitions();
|
|
48
|
+
expect(defs.pluginList).toEqual(["devthink"]);
|
|
49
|
+
for (const entry of MODELS_2026_CATALOG) {
|
|
50
|
+
expect(defs.providerModels[entry.id]).toBeDefined();
|
|
51
|
+
expect(defs.providerModels[entry.id].antigravity.endpoint).toContain("cloudcode-pa.googleapis.com");
|
|
52
|
+
expect(defs.topLevelModels[`antigravity/${entry.id}`]).toBeDefined();
|
|
53
|
+
}
|
|
54
|
+
expect(Object.keys(defs.topLevelModels).length).toBeGreaterThan(MODELS_2026_CATALOG.length);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("answers the account manager from the accounts owner (the twin deletion)", () => {
|
|
58
|
+
expect(typeof getAccountManager).toBe("function");
|
|
59
|
+
const mgr = getAccountManager({ disableBackgroundTimer: true });
|
|
60
|
+
// the twin raced the real manager on the same file; the owner answers
|
|
61
|
+
// the real class now — the singleton and the direct call agree
|
|
62
|
+
const direct = getDefaultAccountManager({ disableBackgroundTimer: true });
|
|
63
|
+
expect(mgr).toBe(direct);
|
|
64
|
+
expect(typeof mgr.listAccounts).toBe("function");
|
|
65
|
+
expect(typeof mgr.getValidAccessToken).toBe("function");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("exposes the family entry and the providercli compat const", async () => {
|
|
69
|
+
expect(typeof runproviderfamily).toBe("function");
|
|
70
|
+
expect(typeof providercli.runCLI).toBe("function");
|
|
71
|
+
expect(typeof providercli.interactiveMenu).toBe("function");
|
|
72
|
+
// the family entry answers --help without a tty, without network
|
|
73
|
+
const logged: string[] = [];
|
|
74
|
+
const spy = vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => {
|
|
75
|
+
logged.push(parts.join(" "));
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
await runproviderfamily(["--help"]);
|
|
79
|
+
} finally {
|
|
80
|
+
spy.mockRestore();
|
|
81
|
+
}
|
|
82
|
+
expect(logged.join("\n")).toContain("devthink provider CLI (unified merge)");
|
|
83
|
+
});
|
|
84
|
+
});
|