agent-dealer 1.2.0 → 1.2.2
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/bundle/server/dist/adapters/linear-inbox.js +9 -5
- package/bundle/server/dist/capacity/adapter.js +2 -0
- package/bundle/server/dist/capacity/claude-events.js +18 -0
- package/bundle/server/dist/capacity/claude-events.test.js +7 -0
- package/bundle/server/dist/capacity/codex-app-server.js +100 -10
- package/bundle/server/dist/capacity/codex-app-server.test.js +195 -2
- package/bundle/server/dist/capacity/cursor-individual-credentials.js +238 -0
- package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +185 -0
- package/bundle/server/dist/capacity/cursor-individual.js +970 -0
- package/bundle/server/dist/capacity/cursor-individual.test.js +815 -0
- package/bundle/server/dist/capacity/muse.js +94 -43
- package/bundle/server/dist/capacity/muse.test.js +95 -13
- package/bundle/server/dist/capacity/service.js +1 -0
- package/bundle/server/dist/coordinator/developer-effect.js +12 -7
- package/bundle/server/dist/coordinator/developer-effect.test.js +142 -5
- package/bundle/server/dist/db/index.js +30 -0
- package/bundle/server/dist/db/schema.sql +27 -0
- package/bundle/server/dist/index.js +2 -0
- package/bundle/server/dist/repository/cursor-individual-billing.js +84 -0
- package/bundle/server/dist/repository/repository-mappings.js +46 -0
- package/bundle/server/dist/repository/repository-mappings.test.js +83 -0
- package/bundle/server/dist/repository/runtime-capacity.js +8 -3
- package/bundle/server/dist/repository/runtime-capacity.test.js +4 -0
- package/bundle/server/dist/routes/cursor-individual-billing.test.js +80 -0
- package/bundle/server/dist/routes/index.js +32 -0
- package/bundle/server/dist/routes/intake-linear.test.js +55 -1
- package/bundle/server/dist/routes/repository-mappings.js +22 -0
- package/bundle/server/dist/routes/repository-mappings.test.js +110 -0
- package/bundle/server/dist/routes/runtime-capacity.test.js +72 -2
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-0GDYfbaM.js +60 -0
- package/bundle/server/static-ui/assets/index-DUYzboWh.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/index.d.ts +6 -6
- package/bundle/shared/dist/linear-intake.d.ts +71 -0
- package/bundle/shared/dist/linear-intake.js +90 -0
- package/bundle/shared/dist/linear-intake.test.js +80 -1
- package/bundle/shared/dist/runtime-capacity.d.ts +89 -0
- package/bundle/shared/dist/runtime-capacity.js +60 -0
- package/bundle/shared/dist/runtime-capacity.test.js +1 -0
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.js +6 -0
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-J1u7JDns.css +0 -1
- package/bundle/server/static-ui/assets/index-obJbiSaU.js +0 -60
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// NOT-260: GET/PUT /api/settings/repository-mappings — normalized round
|
|
2
|
+
// trip plus HTTP 400 with a readable error that preserves the last valid array.
|
|
3
|
+
import { test, before } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import Fastify from "fastify";
|
|
9
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-repo-mappings-routes-"));
|
|
10
|
+
const { migrate } = await import("../db/index.js");
|
|
11
|
+
const { registerRepositoryMappingsRoutes } = await import("./repository-mappings.js");
|
|
12
|
+
before(() => {
|
|
13
|
+
migrate();
|
|
14
|
+
});
|
|
15
|
+
async function buildApp() {
|
|
16
|
+
const app = Fastify();
|
|
17
|
+
await registerRepositoryMappingsRoutes(app);
|
|
18
|
+
return app;
|
|
19
|
+
}
|
|
20
|
+
test("GET returns { mappings: [] } on a fresh install", async () => {
|
|
21
|
+
const app = await buildApp();
|
|
22
|
+
const res = await app.inject({ method: "GET", url: "/api/settings/repository-mappings" });
|
|
23
|
+
assert.equal(res.statusCode, 200);
|
|
24
|
+
assert.deepEqual(res.json(), { mappings: [] });
|
|
25
|
+
});
|
|
26
|
+
test("PUT persists and returns normalized mappings", async () => {
|
|
27
|
+
const app = await buildApp();
|
|
28
|
+
const res = await app.inject({
|
|
29
|
+
method: "PUT",
|
|
30
|
+
url: "/api/settings/repository-mappings",
|
|
31
|
+
payload: { mappings: [{ label: "agent-dealer", repository: "not-so-fat/agent-dealer" }] },
|
|
32
|
+
});
|
|
33
|
+
assert.equal(res.statusCode, 200);
|
|
34
|
+
assert.deepEqual(res.json(), {
|
|
35
|
+
mappings: [{ label: "agent-dealer", repository: "github.com/not-so-fat/agent-dealer" }],
|
|
36
|
+
});
|
|
37
|
+
const get = await app.inject({ method: "GET", url: "/api/settings/repository-mappings" });
|
|
38
|
+
assert.deepEqual(get.json(), {
|
|
39
|
+
mappings: [{ label: "agent-dealer", repository: "github.com/not-so-fat/agent-dealer" }],
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
test("PUT editing the repository overwrites the label's value", async () => {
|
|
43
|
+
const app = await buildApp();
|
|
44
|
+
await app.inject({
|
|
45
|
+
method: "PUT",
|
|
46
|
+
url: "/api/settings/repository-mappings",
|
|
47
|
+
payload: { mappings: [{ label: "agent-dealer", repository: "not-so-fat/agent-dealer" }] },
|
|
48
|
+
});
|
|
49
|
+
const res = await app.inject({
|
|
50
|
+
method: "PUT",
|
|
51
|
+
url: "/api/settings/repository-mappings",
|
|
52
|
+
payload: { mappings: [{ label: "agent-dealer", repository: "not-so-fat/other" }] },
|
|
53
|
+
});
|
|
54
|
+
assert.equal(res.statusCode, 200);
|
|
55
|
+
assert.deepEqual(res.json(), {
|
|
56
|
+
mappings: [{ label: "agent-dealer", repository: "github.com/not-so-fat/other" }],
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
test("PUT with duplicate normalized labels returns 400 and preserves the array", async () => {
|
|
60
|
+
const app = await buildApp();
|
|
61
|
+
await app.inject({
|
|
62
|
+
method: "PUT",
|
|
63
|
+
url: "/api/settings/repository-mappings",
|
|
64
|
+
payload: { mappings: [{ label: "agent-dealer", repository: "not-so-fat/other" }] },
|
|
65
|
+
});
|
|
66
|
+
const res = await app.inject({
|
|
67
|
+
method: "PUT",
|
|
68
|
+
url: "/api/settings/repository-mappings",
|
|
69
|
+
payload: {
|
|
70
|
+
mappings: [
|
|
71
|
+
{ label: "agent-dealer", repository: "not-so-fat/agent-dealer" },
|
|
72
|
+
{ label: " AGENT-DEALER ", repository: "not-so-fat/other" },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
assert.equal(res.statusCode, 400);
|
|
77
|
+
const body = res.json();
|
|
78
|
+
assert.ok(body.error && body.error.length > 0, "readable error");
|
|
79
|
+
const get = await app.inject({ method: "GET", url: "/api/settings/repository-mappings" });
|
|
80
|
+
assert.deepEqual(get.json(), {
|
|
81
|
+
mappings: [{ label: "agent-dealer", repository: "github.com/not-so-fat/other" }],
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
test("PUT with empty labels, invalid repos, or 101 rows returns 400 atomically", async () => {
|
|
85
|
+
const app = await buildApp();
|
|
86
|
+
await app.inject({
|
|
87
|
+
method: "PUT",
|
|
88
|
+
url: "/api/settings/repository-mappings",
|
|
89
|
+
payload: { mappings: [{ label: "keep", repository: "a/b" }] },
|
|
90
|
+
});
|
|
91
|
+
const lastValid = { mappings: [{ label: "keep", repository: "github.com/a/b" }] };
|
|
92
|
+
for (const payload of [
|
|
93
|
+
{ mappings: [{ label: " ", repository: "a/b" }] },
|
|
94
|
+
{ mappings: [{ label: "ok", repository: "not a repo!!" }] },
|
|
95
|
+
{
|
|
96
|
+
mappings: Array.from({ length: 101 }, (_, i) => ({ label: `l${i}`, repository: "a/b" })),
|
|
97
|
+
},
|
|
98
|
+
{ mappings: "nope" },
|
|
99
|
+
]) {
|
|
100
|
+
const res = await app.inject({
|
|
101
|
+
method: "PUT",
|
|
102
|
+
url: "/api/settings/repository-mappings",
|
|
103
|
+
payload,
|
|
104
|
+
});
|
|
105
|
+
assert.equal(res.statusCode, 400);
|
|
106
|
+
assert.ok(res.json().error, "readable error");
|
|
107
|
+
}
|
|
108
|
+
const get = await app.inject({ method: "GET", url: "/api/settings/repository-mappings" });
|
|
109
|
+
assert.deepEqual(get.json(), lastValid);
|
|
110
|
+
});
|
|
@@ -17,12 +17,14 @@ process.env.AGENT_DEALER_SKIP_AGENT_HEALTH = "1";
|
|
|
17
17
|
process.env.AGENT_DEALER_CODEX_CAPACITY_REFRESH = "off";
|
|
18
18
|
const { migrate } = await import("../db/index.js");
|
|
19
19
|
const { createAgent } = await import("../repository/agents.js");
|
|
20
|
-
const { clearAllCapacitySnapshots, listCapacitySnapshots } = await import("../repository/runtime-capacity.js");
|
|
21
|
-
const { fixtureMultiWindowAdapter } = await import("../capacity/adapter.js");
|
|
20
|
+
const { clearAllCapacitySnapshots, listCapacitySnapshots, recordCapacitySnapshots } = await import("../repository/runtime-capacity.js");
|
|
21
|
+
const { fixtureMultiWindowAdapter, normalizeAdapterWindow } = await import("../capacity/adapter.js");
|
|
22
22
|
const { refreshCapacityFromAdapters } = await import("../capacity/service.js");
|
|
23
23
|
const { resetMuseCapacityRefreshState } = await import("../capacity/muse.js");
|
|
24
|
+
const { refreshCodexCapacityFromAppServer } = await import("../capacity/codex-app-server.js");
|
|
24
25
|
const { registerRoutes } = await import("./index.js");
|
|
25
26
|
const { RuntimeCapacityResponse } = await import("@agent-dealer/shared");
|
|
27
|
+
const FAKE_CODEX = new URL("../capacity/fixtures/fake-codex-app-server.mjs", import.meta.url).pathname;
|
|
26
28
|
migrate();
|
|
27
29
|
async function buildApp() {
|
|
28
30
|
const app = Fastify();
|
|
@@ -104,6 +106,74 @@ test("GET triggers the Muse refresh when muse_code is configured", async () => {
|
|
|
104
106
|
resetMuseCapacityRefreshState();
|
|
105
107
|
}
|
|
106
108
|
});
|
|
109
|
+
test("GET serves one logical Codex pair after a dedup refresh of legacy duplicates", async () => {
|
|
110
|
+
// NOT-263: databases written by older versions hold both the aggregate
|
|
111
|
+
// aliases (`codex_rate_limit_primary/secondary`) and the detailed rows for
|
|
112
|
+
// the same logical pair. One refresh against a mirroring payload must
|
|
113
|
+
// delete the aliases, and GET must serve each logical window once.
|
|
114
|
+
clearAllCapacitySnapshots();
|
|
115
|
+
createAgent({
|
|
116
|
+
name: "route-codex-dedup",
|
|
117
|
+
runtime: "codex_local",
|
|
118
|
+
deckId: "55555555-5555-4555-8555-555555555555",
|
|
119
|
+
});
|
|
120
|
+
const seedNow = Date.now();
|
|
121
|
+
const seedObserved = new Date(seedNow).toISOString();
|
|
122
|
+
const seed = (windowKey, providerBucket, durationMinutes) => normalizeAdapterWindow("codex_local", {
|
|
123
|
+
windowKey,
|
|
124
|
+
providerBucket,
|
|
125
|
+
durationMinutes,
|
|
126
|
+
providerLabel: providerBucket,
|
|
127
|
+
usedValue: 9,
|
|
128
|
+
usedUnit: "percent",
|
|
129
|
+
usedPercent: 9,
|
|
130
|
+
resetAt: new Date(seedNow + 2 * 3600_000).toISOString(),
|
|
131
|
+
observedAt: seedObserved,
|
|
132
|
+
source: "supported_protocol",
|
|
133
|
+
}, seedNow);
|
|
134
|
+
recordCapacitySnapshots("codex_local", [
|
|
135
|
+
seed("codex_rate_limit_primary", "primary", 300),
|
|
136
|
+
seed("codex_rate_limit_secondary", "secondary", 10080),
|
|
137
|
+
seed("codex_limit_main_primary", "main/primary", 300),
|
|
138
|
+
seed("codex_limit_main_secondary", "main/secondary", 10080),
|
|
139
|
+
]);
|
|
140
|
+
assert.equal(listCapacitySnapshots("codex_local").length, 4);
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
await refreshCodexCapacityFromAppServer({
|
|
143
|
+
command: process.execPath,
|
|
144
|
+
args: [FAKE_CODEX],
|
|
145
|
+
env: { FAKE_CODEX_MODE: "mirror", FAKE_CODEX_NOW_MS: String(now) },
|
|
146
|
+
timeoutMs: 10_000,
|
|
147
|
+
nowMs: now,
|
|
148
|
+
});
|
|
149
|
+
const app = await buildApp();
|
|
150
|
+
try {
|
|
151
|
+
const res = await app.inject({ method: "GET", url: "/api/runtime-capacity" });
|
|
152
|
+
assert.equal(res.statusCode, 200);
|
|
153
|
+
const body = RuntimeCapacityResponse.parse(res.json());
|
|
154
|
+
const codex = body.runtimes.find((r) => r.runtime === "codex_local");
|
|
155
|
+
assert.ok(codex, "codex_local entry served");
|
|
156
|
+
assert.equal(codex.unavailableReason, null);
|
|
157
|
+
const keys = codex.windows.map((w) => w.windowKey);
|
|
158
|
+
assert.ok(!keys.some((k) => k.startsWith("codex_rate_limit_")), `no aggregate aliases, got ${keys}`);
|
|
159
|
+
// Mirrored `main` pair renders once (5H + weekly); the genuinely
|
|
160
|
+
// distinct `extra` bucket renders as its own pair.
|
|
161
|
+
assert.deepEqual([...keys].sort(), [
|
|
162
|
+
"codex_limit_extra_primary",
|
|
163
|
+
"codex_limit_extra_secondary",
|
|
164
|
+
"codex_limit_main_primary",
|
|
165
|
+
"codex_limit_main_secondary",
|
|
166
|
+
]);
|
|
167
|
+
const mainPrimary = codex.windows.find((w) => w.windowKey === "codex_limit_main_primary");
|
|
168
|
+
assert.equal(mainPrimary.displayLabel, "5H");
|
|
169
|
+
assert.equal(mainPrimary.remainingPercent, 30);
|
|
170
|
+
const raw = JSON.stringify(res.json());
|
|
171
|
+
assert.ok(!raw.includes("evidence"), "no evidence pointers leak to the browser");
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await app.close();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
107
177
|
test("GET /api/runtime-capacity serves a configured Codex account without spawning", async () => {
|
|
108
178
|
clearAllCapacitySnapshots();
|
|
109
179
|
createAgent({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-dealer/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-dealer/shared": "1.2.
|
|
25
|
+
"@agent-dealer/shared": "1.2.2",
|
|
26
26
|
"@fastify/cors": "^11.0.1",
|
|
27
27
|
"@fastify/static": "^8.2.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|