@ai-devkit/agent-manager 0.38.0 → 0.40.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/dist/__tests__/capacity/devin.test.js +239 -0
- package/dist/__tests__/capacity/devin.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/DevinAdapter.test.js +597 -0
- package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/DevinAgentMapper.test.js +91 -0
- package/dist/__tests__/harnesses/devin/DevinAgentMapper.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/DevinSessionLocator.test.js +192 -0
- package/dist/__tests__/harnesses/devin/DevinSessionLocator.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js +407 -0
- package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/fixtures.js +95 -0
- package/dist/__tests__/harnesses/devin/fixtures.js.map +1 -0
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +64 -1
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
- package/dist/__tests__/terminal/GhosttyBackend.test.js +334 -0
- package/dist/__tests__/terminal/GhosttyBackend.test.js.map +1 -0
- package/dist/adapters/AgentAdapter.d.ts +1 -1
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js +2 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/capacity/index.d.ts +3 -0
- package/dist/capacity/index.d.ts.map +1 -1
- package/dist/capacity/index.js +8 -0
- package/dist/capacity/index.js.map +1 -1
- package/dist/capacity/providers/devin.d.ts +13 -0
- package/dist/capacity/providers/devin.d.ts.map +1 -0
- package/dist/capacity/providers/devin.js +92 -0
- package/dist/capacity/providers/devin.js.map +1 -0
- package/dist/capacity/sources/devin.d.ts +12 -0
- package/dist/capacity/sources/devin.d.ts.map +1 -0
- package/dist/capacity/sources/devin.js +29 -0
- package/dist/capacity/sources/devin.js.map +1 -0
- package/dist/harnesses/devin/DevinAdapter.d.ts +39 -0
- package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinAdapter.js +177 -0
- package/dist/harnesses/devin/DevinAdapter.js.map +1 -0
- package/dist/harnesses/devin/DevinAgentMapper.d.ts +11 -0
- package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinAgentMapper.js +35 -0
- package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -0
- package/dist/harnesses/devin/DevinSessionLocator.d.ts +46 -0
- package/dist/harnesses/devin/DevinSessionLocator.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinSessionLocator.js +176 -0
- package/dist/harnesses/devin/DevinSessionLocator.js.map +1 -0
- package/dist/harnesses/devin/DevinSessionParser.d.ts +29 -0
- package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinSessionParser.js +166 -0
- package/dist/harnesses/devin/DevinSessionParser.js.map +1 -0
- package/dist/harnesses/devin/credentials.d.ts +10 -0
- package/dist/harnesses/devin/credentials.d.ts.map +1 -0
- package/dist/harnesses/devin/credentials.js +66 -0
- package/dist/harnesses/devin/credentials.js.map +1 -0
- package/dist/harnesses/devin/readiness.d.ts +3 -0
- package/dist/harnesses/devin/readiness.d.ts.map +1 -0
- package/dist/harnesses/devin/readiness.js +39 -0
- package/dist/harnesses/devin/readiness.js.map +1 -0
- package/dist/harnesses/index.d.ts.map +1 -1
- package/dist/harnesses/index.js +3 -1
- package/dist/harnesses/index.js.map +1 -1
- package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -1
- package/dist/harnesses/readiness/AgentReadiness.js +3 -1
- package/dist/harnesses/readiness/AgentReadiness.js.map +1 -1
- package/dist/harnesses/readiness/types.d.ts +1 -1
- package/dist/harnesses/readiness/types.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.js +2 -1
- package/dist/harnesses/runtimeProfiles.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/terminal/TtyWriter.d.ts.map +1 -1
- package/dist/terminal/TtyWriter.js +4 -3
- package/dist/terminal/TtyWriter.js.map +1 -1
- package/dist/terminal/backends/ghostty.d.ts +3 -0
- package/dist/terminal/backends/ghostty.d.ts.map +1 -0
- package/dist/terminal/backends/ghostty.js +161 -0
- package/dist/terminal/backends/ghostty.js.map +1 -0
- package/dist/terminal/backends/index.d.ts.map +1 -1
- package/dist/terminal/backends/index.js +2 -0
- package/dist/terminal/backends/index.js.map +1 -1
- package/dist/terminal/backends/shared.d.ts +2 -0
- package/dist/terminal/backends/shared.d.ts.map +1 -1
- package/dist/terminal/backends/shared.js +4 -1
- package/dist/terminal/backends/shared.js.map +1 -1
- package/dist/terminal/types.d.ts +1 -0
- package/dist/terminal/types.d.ts.map +1 -1
- package/dist/terminal/types.js +1 -0
- package/dist/terminal/types.js.map +1 -1
- package/dist/utils/process.d.ts +2 -0
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +3 -0
- package/dist/utils/process.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/capacity/devin.test.ts +210 -0
- package/src/__tests__/capacity/fixtures/devin-config.json +6 -0
- package/src/__tests__/capacity/fixtures/devin-credentials.toml +4 -0
- package/src/__tests__/capacity/fixtures/devin-quota-nonplan.json +5 -0
- package/src/__tests__/capacity/fixtures/devin-quota.json +10 -0
- package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +292 -0
- package/src/__tests__/harnesses/devin/DevinAgentMapper.test.ts +88 -0
- package/src/__tests__/harnesses/devin/DevinSessionLocator.test.ts +139 -0
- package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +218 -0
- package/src/__tests__/harnesses/devin/fixtures.ts +144 -0
- package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +41 -0
- package/src/__tests__/terminal/GhosttyBackend.test.ts +271 -0
- package/src/adapters/AgentAdapter.ts +1 -0
- package/src/capacity/index.ts +12 -0
- package/src/capacity/providers/devin.ts +121 -0
- package/src/capacity/sources/devin.ts +49 -0
- package/src/harnesses/devin/DevinAdapter.ts +224 -0
- package/src/harnesses/devin/DevinAgentMapper.ts +43 -0
- package/src/harnesses/devin/DevinSessionLocator.ts +230 -0
- package/src/harnesses/devin/DevinSessionParser.ts +236 -0
- package/src/harnesses/devin/credentials.ts +88 -0
- package/src/harnesses/devin/readiness.ts +36 -0
- package/src/harnesses/index.ts +2 -0
- package/src/harnesses/readiness/AgentReadiness.ts +2 -0
- package/src/harnesses/runtimeProfiles.ts +1 -0
- package/src/index.ts +1 -0
- package/src/terminal/TtyWriter.ts +5 -5
- package/src/terminal/backends/ghostty.ts +180 -0
- package/src/terminal/backends/index.ts +2 -0
- package/src/terminal/backends/shared.ts +6 -1
- package/src/terminal/types.ts +1 -0
- package/src/utils/process.ts +5 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { parseDevinCapacity } from "../../capacity/providers/devin.js";
|
|
5
|
+
import { probeDevinCapacity } from "../../capacity/sources/devin.js";
|
|
6
|
+
import { resolveDevinCredential } from "../../harnesses/devin/credentials.js";
|
|
7
|
+
const checkedAt = "2026-10-05T10:00:00.000Z";
|
|
8
|
+
const fixture = (name)=>readFile(fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)), "utf8");
|
|
9
|
+
const credentialFiles = async (path)=>{
|
|
10
|
+
if (path === "/users/test/.local/share/devin/credentials.toml") {
|
|
11
|
+
return fixture("devin-credentials.toml");
|
|
12
|
+
}
|
|
13
|
+
if (path === "/users/test/.config/devin/config.json") {
|
|
14
|
+
return fixture("devin-config.json");
|
|
15
|
+
}
|
|
16
|
+
throw Object.assign(new Error("missing"), {
|
|
17
|
+
code: "ENOENT"
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
describe("devin credential resolution", ()=>{
|
|
21
|
+
it("prefers DEVIN_API_KEY and DEVIN_ORG_ID without reading files", async ()=>{
|
|
22
|
+
const reader = vi.fn(credentialFiles);
|
|
23
|
+
await expect(resolveDevinCredential({
|
|
24
|
+
env: {
|
|
25
|
+
DEVIN_API_KEY: "env-key",
|
|
26
|
+
DEVIN_ORG_ID: "org-env"
|
|
27
|
+
},
|
|
28
|
+
readFile: reader
|
|
29
|
+
})).resolves.toEqual({
|
|
30
|
+
key: "env-key",
|
|
31
|
+
orgId: "org-env"
|
|
32
|
+
});
|
|
33
|
+
expect(reader).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
it("reads the key from credentials.toml and org from config.json", async ()=>{
|
|
36
|
+
const reader = vi.fn(credentialFiles);
|
|
37
|
+
await expect(resolveDevinCredential({
|
|
38
|
+
env: {
|
|
39
|
+
HOME: "/users/test"
|
|
40
|
+
},
|
|
41
|
+
readFile: reader
|
|
42
|
+
})).resolves.toEqual({
|
|
43
|
+
key: "devin-test-key",
|
|
44
|
+
orgId: "org-test1234"
|
|
45
|
+
});
|
|
46
|
+
expect(reader).toHaveBeenCalledWith("/users/test/.local/share/devin/credentials.toml", "utf8");
|
|
47
|
+
expect(reader).toHaveBeenCalledWith("/users/test/.config/devin/config.json", "utf8");
|
|
48
|
+
});
|
|
49
|
+
it.each([
|
|
50
|
+
[
|
|
51
|
+
"missing credentials file",
|
|
52
|
+
async (path)=>{
|
|
53
|
+
if (path.endsWith("config.json")) return fixture("devin-config.json");
|
|
54
|
+
throw Object.assign(new Error("missing"), {
|
|
55
|
+
code: "ENOENT"
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
/Devin API key not found/
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"malformed credentials file",
|
|
62
|
+
async (path)=>{
|
|
63
|
+
if (path.endsWith("credentials.toml")) return "not = [valid";
|
|
64
|
+
return fixture("devin-config.json");
|
|
65
|
+
},
|
|
66
|
+
/credentials file is malformed/
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
"credentials without key",
|
|
70
|
+
async (path)=>{
|
|
71
|
+
if (path.endsWith("credentials.toml")) return 'api_server_url = "https://x"';
|
|
72
|
+
return fixture("devin-config.json");
|
|
73
|
+
},
|
|
74
|
+
/Devin API key not found/
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
"missing config file",
|
|
78
|
+
async (path)=>{
|
|
79
|
+
if (path.endsWith("credentials.toml")) return fixture("devin-credentials.toml");
|
|
80
|
+
throw Object.assign(new Error("missing"), {
|
|
81
|
+
code: "ENOENT"
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
/Devin organization id not found/
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"config without org_id",
|
|
88
|
+
async (path)=>{
|
|
89
|
+
if (path.endsWith("credentials.toml")) return fixture("devin-credentials.toml");
|
|
90
|
+
return JSON.stringify({
|
|
91
|
+
devin: {}
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
/Devin organization id not found/
|
|
95
|
+
]
|
|
96
|
+
])("fails clearly for %s without exposing file content", async (_name, readFileImpl, pattern)=>{
|
|
97
|
+
await expect(resolveDevinCredential({
|
|
98
|
+
env: {
|
|
99
|
+
HOME: "/users/test"
|
|
100
|
+
},
|
|
101
|
+
readFile: readFileImpl
|
|
102
|
+
})).rejects.toThrow(pattern);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe("devin quota mapping", ()=>{
|
|
106
|
+
it("maps daily and weekly windows with credits", async ()=>{
|
|
107
|
+
const report = parseDevinCapacity(JSON.parse(await fixture("devin-quota.json")));
|
|
108
|
+
expect(report).toMatchObject({
|
|
109
|
+
authenticated: true,
|
|
110
|
+
available: "yes",
|
|
111
|
+
creditsRemaining: 12.5
|
|
112
|
+
});
|
|
113
|
+
expect(report.windows).toEqual([
|
|
114
|
+
{
|
|
115
|
+
id: "devin:daily",
|
|
116
|
+
label: "Daily",
|
|
117
|
+
durationMinutes: 1440,
|
|
118
|
+
usedPercent: 25,
|
|
119
|
+
resetsAt: "2026-10-06T08:00:00.000Z"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "devin:weekly",
|
|
123
|
+
label: "Weekly",
|
|
124
|
+
durationMinutes: 10080,
|
|
125
|
+
usedPercent: 60,
|
|
126
|
+
resetsAt: "2026-10-11T08:00:00.000Z"
|
|
127
|
+
}
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
it("omits the daily window when hide_daily_quota is true", ()=>{
|
|
131
|
+
const report = parseDevinCapacity({
|
|
132
|
+
is_quota_plan: true,
|
|
133
|
+
has_quota_allocation: true,
|
|
134
|
+
daily_percentage: 90,
|
|
135
|
+
weekly_percentage: 10,
|
|
136
|
+
hide_daily_quota: true
|
|
137
|
+
});
|
|
138
|
+
expect(report.windows.map((window)=>window.id)).toEqual([
|
|
139
|
+
"devin:weekly"
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
it("reports authenticated unknown availability for non-quota plans", async ()=>{
|
|
143
|
+
const report = parseDevinCapacity(JSON.parse(await fixture("devin-quota-nonplan.json")));
|
|
144
|
+
expect(report).toEqual({
|
|
145
|
+
authenticated: true,
|
|
146
|
+
available: "unknown",
|
|
147
|
+
windows: [],
|
|
148
|
+
creditsRemaining: null
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it("clamps percentages and degrades missing fields honestly", ()=>{
|
|
152
|
+
const report = parseDevinCapacity({
|
|
153
|
+
is_quota_plan: true,
|
|
154
|
+
has_quota_allocation: true,
|
|
155
|
+
daily_percentage: 140,
|
|
156
|
+
weekly_reset_at: "not-a-date",
|
|
157
|
+
overage_balance: "unknown"
|
|
158
|
+
});
|
|
159
|
+
const [daily, weekly] = report.windows;
|
|
160
|
+
expect(daily.usedPercent).toBe(100);
|
|
161
|
+
expect(weekly.usedPercent).toBeNull();
|
|
162
|
+
expect(weekly.resetsAt).toBeNull();
|
|
163
|
+
expect(report.creditsRemaining).toBeNull();
|
|
164
|
+
expect(report.available).toBe("no");
|
|
165
|
+
});
|
|
166
|
+
it("rejects non-object responses", ()=>{
|
|
167
|
+
expect(()=>parseDevinCapacity("nope")).toThrow(/Devin quota/);
|
|
168
|
+
expect(()=>parseDevinCapacity(null)).toThrow(/Devin quota/);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe("devin request", ()=>{
|
|
172
|
+
it("GETs the org quota endpoint with only Bearer authentication", async ()=>{
|
|
173
|
+
const body = await fixture("devin-quota.json");
|
|
174
|
+
const fetch = vi.fn(async ()=>new Response(body, {
|
|
175
|
+
status: 200
|
|
176
|
+
}));
|
|
177
|
+
const report = await probeDevinCapacity({
|
|
178
|
+
checkedAt,
|
|
179
|
+
env: {
|
|
180
|
+
DEVIN_API_KEY: "request-test-key",
|
|
181
|
+
DEVIN_ORG_ID: "org-abc"
|
|
182
|
+
},
|
|
183
|
+
fetch
|
|
184
|
+
});
|
|
185
|
+
expect(fetch).toHaveBeenCalledWith("https://app.devin.ai/api/org-abc/billing/quota/usage", expect.objectContaining({
|
|
186
|
+
method: "GET",
|
|
187
|
+
headers: {
|
|
188
|
+
Authorization: "Bearer request-test-key"
|
|
189
|
+
}
|
|
190
|
+
}));
|
|
191
|
+
expect(report.harness).toBe("devin");
|
|
192
|
+
expect(report.provider).toBe("devin");
|
|
193
|
+
expect(JSON.stringify(report)).not.toContain("request-test-key");
|
|
194
|
+
});
|
|
195
|
+
it.each([
|
|
196
|
+
401,
|
|
197
|
+
403
|
|
198
|
+
])("reports HTTP %i as unauthenticated", async (status)=>{
|
|
199
|
+
const report = await probeDevinCapacity({
|
|
200
|
+
checkedAt,
|
|
201
|
+
env: {
|
|
202
|
+
DEVIN_API_KEY: "request-test-key",
|
|
203
|
+
DEVIN_ORG_ID: "org-abc"
|
|
204
|
+
},
|
|
205
|
+
fetch: vi.fn(async ()=>new Response("denied", {
|
|
206
|
+
status
|
|
207
|
+
}))
|
|
208
|
+
});
|
|
209
|
+
expect(report).toMatchObject({
|
|
210
|
+
authenticated: false,
|
|
211
|
+
available: "unknown",
|
|
212
|
+
windows: []
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
it("rejects other non-200 and malformed JSON responses with sanitized errors", async ()=>{
|
|
216
|
+
await expect(probeDevinCapacity({
|
|
217
|
+
checkedAt,
|
|
218
|
+
env: {
|
|
219
|
+
DEVIN_API_KEY: "never-expose-this-key",
|
|
220
|
+
DEVIN_ORG_ID: "org-abc"
|
|
221
|
+
},
|
|
222
|
+
fetch: vi.fn(async ()=>new Response("private body", {
|
|
223
|
+
status: 503
|
|
224
|
+
}))
|
|
225
|
+
})).rejects.toThrow("Devin quota request failed: HTTP 503");
|
|
226
|
+
await expect(probeDevinCapacity({
|
|
227
|
+
checkedAt,
|
|
228
|
+
env: {
|
|
229
|
+
DEVIN_API_KEY: "never-expose-this-key",
|
|
230
|
+
DEVIN_ORG_ID: "org-abc"
|
|
231
|
+
},
|
|
232
|
+
fetch: vi.fn(async ()=>new Response("not-json", {
|
|
233
|
+
status: 200
|
|
234
|
+
}))
|
|
235
|
+
})).rejects.toThrow("Devin quota response is not valid JSON");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
//# sourceMappingURL=devin.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/__tests__/capacity/devin.test.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { fileURLToPath } from \"node:url\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport { parseDevinCapacity } from \"../../capacity/providers/devin.js\";\nimport { probeDevinCapacity } from \"../../capacity/sources/devin.js\";\nimport { resolveDevinCredential } from \"../../harnesses/devin/credentials.js\";\n\nconst checkedAt = \"2026-10-05T10:00:00.000Z\";\nconst fixture = (name: string) =>\n readFile(fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)), \"utf8\");\n\nconst credentialFiles = async (path: string): Promise<string> => {\n if (path === \"/users/test/.local/share/devin/credentials.toml\") {\n return fixture(\"devin-credentials.toml\");\n }\n if (path === \"/users/test/.config/devin/config.json\") {\n return fixture(\"devin-config.json\");\n }\n throw Object.assign(new Error(\"missing\"), { code: \"ENOENT\" });\n};\n\ndescribe(\"devin credential resolution\", () => {\n it(\"prefers DEVIN_API_KEY and DEVIN_ORG_ID without reading files\", async () => {\n const reader = vi.fn(credentialFiles);\n await expect(\n resolveDevinCredential({\n env: { DEVIN_API_KEY: \"env-key\", DEVIN_ORG_ID: \"org-env\" },\n readFile: reader,\n }),\n ).resolves.toEqual({ key: \"env-key\", orgId: \"org-env\" });\n expect(reader).not.toHaveBeenCalled();\n });\n\n it(\"reads the key from credentials.toml and org from config.json\", async () => {\n const reader = vi.fn(credentialFiles);\n await expect(\n resolveDevinCredential({ env: { HOME: \"/users/test\" }, readFile: reader }),\n ).resolves.toEqual({ key: \"devin-test-key\", orgId: \"org-test1234\" });\n expect(reader).toHaveBeenCalledWith(\"/users/test/.local/share/devin/credentials.toml\", \"utf8\");\n expect(reader).toHaveBeenCalledWith(\"/users/test/.config/devin/config.json\", \"utf8\");\n });\n\n it.each([\n [\n \"missing credentials file\",\n async (path: string) => {\n if (path.endsWith(\"config.json\")) return fixture(\"devin-config.json\");\n throw Object.assign(new Error(\"missing\"), { code: \"ENOENT\" });\n },\n /Devin API key not found/,\n ],\n [\n \"malformed credentials file\",\n async (path: string) => {\n if (path.endsWith(\"credentials.toml\")) return \"not = [valid\";\n return fixture(\"devin-config.json\");\n },\n /credentials file is malformed/,\n ],\n [\n \"credentials without key\",\n async (path: string) => {\n if (path.endsWith(\"credentials.toml\")) return 'api_server_url = \"https://x\"';\n return fixture(\"devin-config.json\");\n },\n /Devin API key not found/,\n ],\n [\n \"missing config file\",\n async (path: string) => {\n if (path.endsWith(\"credentials.toml\")) return fixture(\"devin-credentials.toml\");\n throw Object.assign(new Error(\"missing\"), { code: \"ENOENT\" });\n },\n /Devin organization id not found/,\n ],\n [\n \"config without org_id\",\n async (path: string) => {\n if (path.endsWith(\"credentials.toml\")) return fixture(\"devin-credentials.toml\");\n return JSON.stringify({ devin: {} });\n },\n /Devin organization id not found/,\n ],\n ])(\"fails clearly for %s without exposing file content\", async (_name, readFileImpl, pattern) => {\n await expect(\n resolveDevinCredential({ env: { HOME: \"/users/test\" }, readFile: readFileImpl }),\n ).rejects.toThrow(pattern);\n });\n});\n\ndescribe(\"devin quota mapping\", () => {\n it(\"maps daily and weekly windows with credits\", async () => {\n const report = parseDevinCapacity(JSON.parse(await fixture(\"devin-quota.json\")));\n expect(report).toMatchObject({\n authenticated: true,\n available: \"yes\",\n creditsRemaining: 12.5,\n });\n expect(report.windows).toEqual([\n {\n id: \"devin:daily\",\n label: \"Daily\",\n durationMinutes: 1440,\n usedPercent: 25,\n resetsAt: \"2026-10-06T08:00:00.000Z\",\n },\n {\n id: \"devin:weekly\",\n label: \"Weekly\",\n durationMinutes: 10080,\n usedPercent: 60,\n resetsAt: \"2026-10-11T08:00:00.000Z\",\n },\n ]);\n });\n\n it(\"omits the daily window when hide_daily_quota is true\", () => {\n const report = parseDevinCapacity({\n is_quota_plan: true,\n has_quota_allocation: true,\n daily_percentage: 90,\n weekly_percentage: 10,\n hide_daily_quota: true,\n });\n expect(report.windows.map((window) => window.id)).toEqual([\"devin:weekly\"]);\n });\n\n it(\"reports authenticated unknown availability for non-quota plans\", async () => {\n const report = parseDevinCapacity(JSON.parse(await fixture(\"devin-quota-nonplan.json\")));\n expect(report).toEqual({\n authenticated: true,\n available: \"unknown\",\n windows: [],\n creditsRemaining: null,\n });\n });\n\n it(\"clamps percentages and degrades missing fields honestly\", () => {\n const report = parseDevinCapacity({\n is_quota_plan: true,\n has_quota_allocation: true,\n daily_percentage: 140,\n weekly_reset_at: \"not-a-date\",\n overage_balance: \"unknown\",\n });\n const [daily, weekly] = report.windows;\n expect(daily.usedPercent).toBe(100);\n expect(weekly.usedPercent).toBeNull();\n expect(weekly.resetsAt).toBeNull();\n expect(report.creditsRemaining).toBeNull();\n expect(report.available).toBe(\"no\");\n });\n\n it(\"rejects non-object responses\", () => {\n expect(() => parseDevinCapacity(\"nope\")).toThrow(/Devin quota/);\n expect(() => parseDevinCapacity(null)).toThrow(/Devin quota/);\n });\n});\n\ndescribe(\"devin request\", () => {\n it(\"GETs the org quota endpoint with only Bearer authentication\", async () => {\n const body = await fixture(\"devin-quota.json\");\n const fetch = vi.fn(async () => new Response(body, { status: 200 }));\n const report = await probeDevinCapacity({\n checkedAt,\n env: { DEVIN_API_KEY: \"request-test-key\", DEVIN_ORG_ID: \"org-abc\" },\n fetch,\n });\n expect(fetch).toHaveBeenCalledWith(\n \"https://app.devin.ai/api/org-abc/billing/quota/usage\",\n expect.objectContaining({\n method: \"GET\",\n headers: { Authorization: \"Bearer request-test-key\" },\n }),\n );\n expect(report.harness).toBe(\"devin\");\n expect(report.provider).toBe(\"devin\");\n expect(JSON.stringify(report)).not.toContain(\"request-test-key\");\n });\n\n it.each([401, 403])(\"reports HTTP %i as unauthenticated\", async (status) => {\n const report = await probeDevinCapacity({\n checkedAt,\n env: { DEVIN_API_KEY: \"request-test-key\", DEVIN_ORG_ID: \"org-abc\" },\n fetch: vi.fn(async () => new Response(\"denied\", { status })),\n });\n expect(report).toMatchObject({\n authenticated: false,\n available: \"unknown\",\n windows: [],\n });\n });\n\n it(\"rejects other non-200 and malformed JSON responses with sanitized errors\", async () => {\n await expect(\n probeDevinCapacity({\n checkedAt,\n env: { DEVIN_API_KEY: \"never-expose-this-key\", DEVIN_ORG_ID: \"org-abc\" },\n fetch: vi.fn(async () => new Response(\"private body\", { status: 503 })),\n }),\n ).rejects.toThrow(\"Devin quota request failed: HTTP 503\");\n await expect(\n probeDevinCapacity({\n checkedAt,\n env: { DEVIN_API_KEY: \"never-expose-this-key\", DEVIN_ORG_ID: \"org-abc\" },\n fetch: vi.fn(async () => new Response(\"not-json\", { status: 200 })),\n }),\n ).rejects.toThrow(\"Devin quota response is not valid JSON\");\n });\n});\n"],"names":["readFile","fileURLToPath","describe","expect","it","vi","parseDevinCapacity","probeDevinCapacity","resolveDevinCredential","checkedAt","fixture","name","URL","url","credentialFiles","path","Object","assign","Error","code","reader","fn","env","DEVIN_API_KEY","DEVIN_ORG_ID","resolves","toEqual","key","orgId","not","toHaveBeenCalled","HOME","toHaveBeenCalledWith","each","endsWith","JSON","stringify","devin","_name","readFileImpl","pattern","rejects","toThrow","report","parse","toMatchObject","authenticated","available","creditsRemaining","windows","id","label","durationMinutes","usedPercent","resetsAt","is_quota_plan","has_quota_allocation","daily_percentage","weekly_percentage","hide_daily_quota","map","window","weekly_reset_at","overage_balance","daily","weekly","toBe","toBeNull","body","fetch","Response","status","objectContaining","method","headers","Authorization","harness","provider","toContain"],"mappings":"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"}
|