robodev 0.35.0 → 0.37.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/package.json +2 -2
- package/src/dev-clients.ts +86 -10
- package/src/dev-inspect.test.ts +269 -7
- package/src/dev-inspect.ts +1064 -106
- package/src/dev-server.ts +117 -16
- package/src/dev.ts +11 -1
- package/templates/auth-chat/package.json +1 -1
- package/templates/backend/package.json +1 -1
- package/templates/empty/package.json +1 -1
- package/templates/marketplace/package.json +1 -1
- package/templates/space/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robodev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "CLI for Robodev Starbase — create, auth, link, deploy, and run a project locally with robodev dev",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"embedded-postgres": "17.10.0-beta.17",
|
|
27
27
|
"fastify": "^5.12.1",
|
|
28
28
|
"tsx": "^4.20.5",
|
|
29
|
-
"@robodev-ai/runtime": "0.4.
|
|
29
|
+
"@robodev-ai/runtime": "0.4.1",
|
|
30
30
|
"@robodev-ai/sdk": "0.11.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
package/src/dev-clients.ts
CHANGED
|
@@ -4,8 +4,18 @@ import {
|
|
|
4
4
|
type StorageQueryable,
|
|
5
5
|
} from "@robodev-ai/runtime";
|
|
6
6
|
import type { RouteClients } from "@robodev-ai/runtime";
|
|
7
|
-
import type {
|
|
8
|
-
|
|
7
|
+
import type {
|
|
8
|
+
EmailClient,
|
|
9
|
+
JobEnqueueInput,
|
|
10
|
+
LlmClient,
|
|
11
|
+
SocketsClient,
|
|
12
|
+
StorageClient,
|
|
13
|
+
} from "@robodev-ai/sdk";
|
|
14
|
+
import {
|
|
15
|
+
truncateInspectPreview,
|
|
16
|
+
type DevInspectStore,
|
|
17
|
+
type StorageInspectOp,
|
|
18
|
+
} from "./dev-inspect.js";
|
|
9
19
|
|
|
10
20
|
/**
|
|
11
21
|
* The `ctx.*` clients `robodev dev` can offer offline. `email` prints to the terminal;
|
|
@@ -21,6 +31,68 @@ function errorMessage(error: unknown): string {
|
|
|
21
31
|
return error instanceof Error ? error.message : String(error);
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
function inspectStorageClient(client: StorageClient, inspect: DevInspectStore): StorageClient {
|
|
35
|
+
async function tracked<T>(op: StorageInspectOp, key: string, run: () => Promise<T>): Promise<T> {
|
|
36
|
+
try {
|
|
37
|
+
const result = await run();
|
|
38
|
+
inspect.record({ kind: "storage", op, key, ok: true });
|
|
39
|
+
return result;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
inspect.record({
|
|
42
|
+
kind: "storage",
|
|
43
|
+
op,
|
|
44
|
+
key,
|
|
45
|
+
ok: false,
|
|
46
|
+
error: errorMessage(error),
|
|
47
|
+
});
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
upload: (key, data, options) => tracked("upload", key, () => client.upload(key, data, options)),
|
|
53
|
+
get: (key) => tracked("get", key, () => client.get(key)),
|
|
54
|
+
getUrl: (key, options) => tracked("getUrl", key, () => client.getUrl(key, options)),
|
|
55
|
+
delete: (key) => tracked("delete", key, () => client.delete(key)),
|
|
56
|
+
list: (options) => tracked("list", options?.prefix ?? "", () => client.list(options)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function inspectSocketsClient(client: SocketsClient, inspect: DevInspectStore): SocketsClient {
|
|
61
|
+
function tracked(
|
|
62
|
+
socketKind: "send" | "broadcast",
|
|
63
|
+
name: string,
|
|
64
|
+
payload: unknown,
|
|
65
|
+
run: () => void,
|
|
66
|
+
) {
|
|
67
|
+
try {
|
|
68
|
+
run();
|
|
69
|
+
inspect.record({
|
|
70
|
+
kind: "socket",
|
|
71
|
+
socketKind,
|
|
72
|
+
name,
|
|
73
|
+
preview: truncateInspectPreview(payload),
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
inspect.record({
|
|
77
|
+
kind: "socket",
|
|
78
|
+
socketKind,
|
|
79
|
+
name,
|
|
80
|
+
error: errorMessage(error),
|
|
81
|
+
preview: truncateInspectPreview(payload),
|
|
82
|
+
});
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
send(input) {
|
|
88
|
+
tracked("send", input.name, input.payload, () => client.send(input));
|
|
89
|
+
},
|
|
90
|
+
broadcast(input) {
|
|
91
|
+
tracked("broadcast", input.name, input.payload, () => client.broadcast(input));
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
24
96
|
/** Prints mail to stdout so local signup and reset flows are followable. */
|
|
25
97
|
export function consoleEmailClient(
|
|
26
98
|
log: (line: string) => void,
|
|
@@ -65,18 +137,22 @@ export function devRouteClients(input: {
|
|
|
65
137
|
subscribe: () => notConfigured("llm_not_configured"),
|
|
66
138
|
destroy: () => notConfigured("llm_not_configured"),
|
|
67
139
|
},
|
|
68
|
-
storage:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
140
|
+
storage: inspectStorageClient(
|
|
141
|
+
createLocalStorageClient({
|
|
142
|
+
query: input.query,
|
|
143
|
+
objectsDir: input.objectsDir,
|
|
144
|
+
publicBaseUrl: input.publicBaseUrl,
|
|
145
|
+
projectId: input.projectId,
|
|
146
|
+
jwtSecret: input.jwtSecret,
|
|
147
|
+
}),
|
|
148
|
+
input.inspect,
|
|
149
|
+
),
|
|
75
150
|
push: { send: () => notConfigured("push_not_configured") },
|
|
76
151
|
jobs: {
|
|
77
152
|
enqueue: async (job) => {
|
|
78
153
|
try {
|
|
79
154
|
const result = await input.enqueue(job);
|
|
155
|
+
// Enqueue is not a run; the dashboard Jobs page reads `job_run` from onJobFinished.
|
|
80
156
|
input.inspect.record({
|
|
81
157
|
kind: "job",
|
|
82
158
|
name: job.name,
|
|
@@ -95,7 +171,7 @@ export function devRouteClients(input: {
|
|
|
95
171
|
}
|
|
96
172
|
},
|
|
97
173
|
},
|
|
98
|
-
sockets: createProjectSocketsClient(input.projectId),
|
|
174
|
+
sockets: inspectSocketsClient(createProjectSocketsClient(input.projectId), input.inspect),
|
|
99
175
|
env: input.env,
|
|
100
176
|
};
|
|
101
177
|
}
|
package/src/dev-inspect.test.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import {
|
|
4
|
+
buildDashboardState,
|
|
5
|
+
callMatchesRoute,
|
|
4
6
|
createDevInspect,
|
|
5
7
|
inspectHtml,
|
|
8
|
+
isHistogramExcludedPath,
|
|
6
9
|
isInspectPath,
|
|
10
|
+
shouldCountRequestHistogram,
|
|
11
|
+
socketEventMatchesDef,
|
|
12
|
+
storageActivityMatchesKey,
|
|
13
|
+
truncateInspectPreview,
|
|
7
14
|
type DevInspectAuthCodeEvent,
|
|
8
15
|
type DevInspectEmailEvent,
|
|
9
16
|
type DevInspectJobEvent,
|
|
@@ -11,9 +18,21 @@ import {
|
|
|
11
18
|
type DevInspectRequestEvent,
|
|
12
19
|
} from "./dev-inspect.js";
|
|
13
20
|
|
|
14
|
-
|
|
21
|
+
const fileRuntime = {
|
|
22
|
+
routes: [{ method: "get", path: "/api/planets" }],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
test("isInspectPath covers dashboard pages and JSON only", () => {
|
|
15
26
|
assert.equal(isInspectPath("/_robodev"), true);
|
|
27
|
+
assert.equal(isInspectPath("/_robodev/apis"), true);
|
|
28
|
+
assert.equal(isInspectPath("/_robodev/jobs"), true);
|
|
29
|
+
assert.equal(isInspectPath("/_robodev/emails"), true);
|
|
30
|
+
assert.equal(isInspectPath("/_robodev/auth"), true);
|
|
31
|
+
assert.equal(isInspectPath("/_robodev/storage"), true);
|
|
32
|
+
assert.equal(isInspectPath("/_robodev/sockets"), true);
|
|
16
33
|
assert.equal(isInspectPath("/_robodev/logs"), true);
|
|
34
|
+
assert.equal(isInspectPath("/storage/objects/avatar.png"), false);
|
|
35
|
+
assert.equal(isInspectPath("/_robodev/state.json"), true);
|
|
17
36
|
assert.equal(isInspectPath("/_robodev/logs.json"), true);
|
|
18
37
|
assert.equal(isInspectPath("/_robodev/docs"), false);
|
|
19
38
|
assert.equal(isInspectPath("/_robodev/ready"), false);
|
|
@@ -22,6 +41,26 @@ test("isInspectPath excludes only inspect self-routes", () => {
|
|
|
22
41
|
assert.equal(isInspectPath("/api/planets"), false);
|
|
23
42
|
});
|
|
24
43
|
|
|
44
|
+
test("shouldCountRequestHistogram counts Auth and matched file routes only", () => {
|
|
45
|
+
assert.equal(shouldCountRequestHistogram("/api/user/me", fileRuntime, "GET"), true);
|
|
46
|
+
assert.equal(shouldCountRequestHistogram("/api/user/auth/google", fileRuntime, "GET"), true);
|
|
47
|
+
assert.equal(shouldCountRequestHistogram("/api/planets", fileRuntime, "GET"), true);
|
|
48
|
+
assert.equal(shouldCountRequestHistogram("/api/planets", fileRuntime, "POST"), false);
|
|
49
|
+
assert.equal(shouldCountRequestHistogram("/api/unknown", fileRuntime, "GET"), false);
|
|
50
|
+
assert.equal(shouldCountRequestHistogram("/_robodev", fileRuntime, "GET"), false);
|
|
51
|
+
assert.equal(shouldCountRequestHistogram("/_robodev/apis", fileRuntime, "GET"), false);
|
|
52
|
+
assert.equal(shouldCountRequestHistogram("/_robodev/storage", fileRuntime, "GET"), false);
|
|
53
|
+
assert.equal(shouldCountRequestHistogram("/_robodev/sockets", fileRuntime, "GET"), false);
|
|
54
|
+
assert.equal(shouldCountRequestHistogram("/storage/objects/a.png", fileRuntime, "GET"), false);
|
|
55
|
+
assert.equal(shouldCountRequestHistogram("/_robodev/docs", fileRuntime, "GET"), false);
|
|
56
|
+
assert.equal(shouldCountRequestHistogram("/_robodev/scalar", fileRuntime, "GET"), false);
|
|
57
|
+
assert.equal(shouldCountRequestHistogram("/_robodev/ready", fileRuntime, "GET"), false);
|
|
58
|
+
assert.equal(shouldCountRequestHistogram("/openapi.json", fileRuntime, "GET"), false);
|
|
59
|
+
assert.equal(shouldCountRequestHistogram("/docs", fileRuntime, "GET"), false);
|
|
60
|
+
assert.equal(shouldCountRequestHistogram("/swagger", fileRuntime, "GET"), false);
|
|
61
|
+
assert.equal(isHistogramExcludedPath("/scalar"), true);
|
|
62
|
+
});
|
|
63
|
+
|
|
25
64
|
test("ring buffer drops oldest events past max", () => {
|
|
26
65
|
const inspect = createDevInspect({ max: 3 });
|
|
27
66
|
inspect.record({ kind: "log", message: "a" });
|
|
@@ -37,6 +76,32 @@ test("ring buffer drops oldest events past max", () => {
|
|
|
37
76
|
);
|
|
38
77
|
});
|
|
39
78
|
|
|
79
|
+
test("request histogram survives 500-event ring rotation", () => {
|
|
80
|
+
const inspect = createDevInspect({ max: 2 });
|
|
81
|
+
inspect.incrementRequestHistogram(200);
|
|
82
|
+
inspect.incrementRequestHistogram(200);
|
|
83
|
+
inspect.incrementRequestHistogram(404);
|
|
84
|
+
inspect.incrementRequestHistogram(500);
|
|
85
|
+
inspect.record({ kind: "request", method: "GET", path: "/api/a", status: 200, durationMs: 1 });
|
|
86
|
+
inspect.record({ kind: "request", method: "GET", path: "/api/b", status: 200, durationMs: 1 });
|
|
87
|
+
inspect.record({ kind: "request", method: "GET", path: "/api/c", status: 200, durationMs: 1 });
|
|
88
|
+
assert.equal(inspect.list().length, 2);
|
|
89
|
+
|
|
90
|
+
const last = inspect.requestHistogram().at(-1);
|
|
91
|
+
assert.equal(last?.c2xx, 2);
|
|
92
|
+
assert.equal(last?.c4xx, 1);
|
|
93
|
+
assert.equal(last?.c5xx, 1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("3xx does not increment request histogram buckets", () => {
|
|
97
|
+
const inspect = createDevInspect();
|
|
98
|
+
inspect.incrementRequestHistogram(302);
|
|
99
|
+
const last = inspect.requestHistogram().at(-1);
|
|
100
|
+
assert.equal(last?.c2xx, 0);
|
|
101
|
+
assert.equal(last?.c4xx, 0);
|
|
102
|
+
assert.equal(last?.c5xx, 0);
|
|
103
|
+
});
|
|
104
|
+
|
|
40
105
|
test("list is newest last and does not leak the internal buffer", () => {
|
|
41
106
|
const inspect = createDevInspect({ max: 500 });
|
|
42
107
|
inspect.record({ kind: "log", message: "first" });
|
|
@@ -112,12 +177,209 @@ test("record assigns id, at, and the expected fields for each kind", () => {
|
|
|
112
177
|
assert.equal(inspect.list().length, 5);
|
|
113
178
|
});
|
|
114
179
|
|
|
115
|
-
test("
|
|
180
|
+
test("callMatchesRoute uses matchPathParams and requires method", () => {
|
|
181
|
+
const route = { method: "GET", path: "/api/planets/:id" };
|
|
182
|
+
assert.equal(callMatchesRoute({ method: "GET", path: "/api/planets/123" }, route), true);
|
|
183
|
+
assert.equal(callMatchesRoute({ method: "GET", path: "/api/planets" }, route), false);
|
|
184
|
+
assert.equal(callMatchesRoute({ method: "POST", path: "/api/planets/123" }, route), false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("dashboard snapshot lists reserved Auth, filters emails, and ignores enqueue jobs", async () => {
|
|
188
|
+
const inspect = createDevInspect();
|
|
189
|
+
inspect.record({
|
|
190
|
+
kind: "request",
|
|
191
|
+
method: "GET",
|
|
192
|
+
path: "/api/planets",
|
|
193
|
+
status: 200,
|
|
194
|
+
durationMs: 4,
|
|
195
|
+
});
|
|
196
|
+
inspect.record({
|
|
197
|
+
kind: "request",
|
|
198
|
+
method: "GET",
|
|
199
|
+
path: "/_robodev/docs",
|
|
200
|
+
status: 200,
|
|
201
|
+
durationMs: 1,
|
|
202
|
+
});
|
|
203
|
+
inspect.record({ kind: "email", to: "a@b.test", subject: "Hello", text: "Body" });
|
|
204
|
+
inspect.record({ kind: "auth_code", type: "magic", to: "a@b.test", code: "999111" });
|
|
205
|
+
inspect.record({ kind: "job", name: "hourly", payload: { n: 1 }, error: "" });
|
|
206
|
+
inspect.record({
|
|
207
|
+
kind: "job_run",
|
|
208
|
+
name: "hourly",
|
|
209
|
+
status: "failed",
|
|
210
|
+
payload: { n: 1 },
|
|
211
|
+
error: "boom",
|
|
212
|
+
});
|
|
213
|
+
inspect.incrementJobHistogram("failed");
|
|
214
|
+
|
|
215
|
+
const state = await buildDashboardState({
|
|
216
|
+
inspect,
|
|
217
|
+
runtime: {
|
|
218
|
+
routes: [{ method: "get", path: "/api/planets", file: "api/planets.ts" }],
|
|
219
|
+
jobs: [{ name: "hourly", file: "jobs/hourly.ts", def: { cron: "0 * * * *" } }],
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
assert.equal(state.overview.apiCount, 1 + 12);
|
|
224
|
+
assert.equal(state.overview.jobCount, 1);
|
|
225
|
+
assert.equal(state.overview.google, "broker");
|
|
226
|
+
assert.ok(state.routes.some((route) => route.path === "/api/user/auth/google/callback"));
|
|
227
|
+
assert.equal(state.calls.length, 1);
|
|
228
|
+
assert.equal(state.calls[0]?.path, "/api/planets");
|
|
229
|
+
assert.equal(state.emails.length, 1);
|
|
230
|
+
assert.equal(state.emails[0]?.subject, "Hello");
|
|
231
|
+
assert.equal(state.jobRuns.length, 1);
|
|
232
|
+
assert.equal(state.jobRuns[0]?.status, "failed");
|
|
233
|
+
assert.equal(state.overview.jobHistogram.at(-1)?.failed, 1);
|
|
234
|
+
assert.deepEqual(state.auth.users, []);
|
|
235
|
+
assert.equal(state.overview.storageObjectCount, 0);
|
|
236
|
+
assert.equal(state.overview.socketCount, 0);
|
|
237
|
+
assert.deepEqual(state.storage, { objects: [], activity: [] });
|
|
238
|
+
assert.deepEqual(state.sockets, { connected: 0, defs: [], events: [] });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("incomplete Google credentials are not labeled override", async () => {
|
|
242
|
+
const state = await buildDashboardState({
|
|
243
|
+
inspect: createDevInspect(),
|
|
244
|
+
runtime: { routes: [], jobs: [] },
|
|
245
|
+
googleClientId: "only-id",
|
|
246
|
+
});
|
|
247
|
+
assert.equal(state.overview.google, "incomplete");
|
|
248
|
+
assert.equal(state.auth.google, "incomplete");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("inspect HTML has nav labels and polls state.json", () => {
|
|
116
252
|
const html = inspectHtml();
|
|
117
|
-
assert.match(html,
|
|
118
|
-
assert.match(html,
|
|
119
|
-
assert.match(html,
|
|
120
|
-
assert.match(html,
|
|
121
|
-
assert.match(html,
|
|
253
|
+
assert.match(html, />Overview</);
|
|
254
|
+
assert.match(html, />APIs</);
|
|
255
|
+
assert.match(html, />Jobs</);
|
|
256
|
+
assert.match(html, />Emails</);
|
|
257
|
+
assert.match(html, />Auth</);
|
|
258
|
+
assert.match(html, />Storage</);
|
|
259
|
+
assert.match(html, />Sockets</);
|
|
260
|
+
assert.match(html, /\/_robodev\/storage/);
|
|
261
|
+
assert.match(html, /\/_robodev\/sockets/);
|
|
262
|
+
assert.doesNotMatch(html, /<h2>Requests<\/h2>/);
|
|
263
|
+
assert.doesNotMatch(html, /<h2>Emails<\/h2>/);
|
|
264
|
+
assert.doesNotMatch(html, /<h2>Jobs<\/h2>/);
|
|
265
|
+
assert.doesNotMatch(html, /<h2>Logs<\/h2>/);
|
|
266
|
+
assert.match(html, /\/_robodev\/state\.json/);
|
|
267
|
+
assert.doesNotMatch(html, /fetch\("\/_robodev\/logs\.json"\)/);
|
|
122
268
|
assert.match(html, /setInterval\(poll, 1000\)/);
|
|
123
269
|
});
|
|
270
|
+
|
|
271
|
+
test("storageActivityMatchesKey uses exact key except list prefixes", () => {
|
|
272
|
+
assert.equal(storageActivityMatchesKey({ op: "upload", key: "a/b" }, "a/b"), true);
|
|
273
|
+
assert.equal(storageActivityMatchesKey({ op: "get", key: "a/b" }, "a/c"), false);
|
|
274
|
+
assert.equal(storageActivityMatchesKey({ op: "getUrl", key: "a/b" }, "a/b"), true);
|
|
275
|
+
assert.equal(storageActivityMatchesKey({ op: "delete", key: "a/b" }, "a/b"), true);
|
|
276
|
+
assert.equal(storageActivityMatchesKey({ op: "serve", key: "a/b" }, "a/b"), true);
|
|
277
|
+
assert.equal(storageActivityMatchesKey({ op: "list", key: "" }, "a/b"), true);
|
|
278
|
+
assert.equal(storageActivityMatchesKey({ op: "list", key: "a/" }, "a/b"), true);
|
|
279
|
+
assert.equal(storageActivityMatchesKey({ op: "list", key: "a/" }, "a/"), true);
|
|
280
|
+
assert.equal(storageActivityMatchesKey({ op: "list", key: "b" }, "a/b"), false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("socketEventMatchesDef matches path params or name", () => {
|
|
284
|
+
const chat = { path: "/sockets/chat", name: "chat" };
|
|
285
|
+
const room = { path: "/sockets/room/:id", name: "room" };
|
|
286
|
+
assert.equal(socketEventMatchesDef({ path: "/sockets/chat", name: "chat" }, chat), true);
|
|
287
|
+
assert.equal(socketEventMatchesDef({ name: "chat" }, chat), true);
|
|
288
|
+
assert.equal(socketEventMatchesDef({ path: "/sockets/other" }, chat), false);
|
|
289
|
+
assert.equal(socketEventMatchesDef({ path: "/sockets/room/abc" }, room), true);
|
|
290
|
+
assert.equal(socketEventMatchesDef({ name: "room" }, room), true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("truncateInspectPreview caps text and JSON at 200", () => {
|
|
294
|
+
assert.equal(truncateInspectPreview("short"), "short");
|
|
295
|
+
assert.equal(truncateInspectPreview({ a: 1 }), '{"a":1}');
|
|
296
|
+
const long = "x".repeat(220);
|
|
297
|
+
const preview = truncateInspectPreview(long);
|
|
298
|
+
assert.equal(preview.length, 201);
|
|
299
|
+
assert.equal(preview.endsWith("…"), true);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("dashboard snapshot includes storage objects, activity, and socket defs", async () => {
|
|
303
|
+
const inspect = createDevInspect();
|
|
304
|
+
inspect.record({ kind: "storage", op: "upload", key: "a.png", ok: true });
|
|
305
|
+
inspect.record({ kind: "storage", op: "list", key: "", ok: true });
|
|
306
|
+
inspect.record({ kind: "storage", op: "serve", key: "missing", ok: false, error: "not_found" });
|
|
307
|
+
inspect.record({
|
|
308
|
+
kind: "socket",
|
|
309
|
+
socketKind: "connect",
|
|
310
|
+
path: "/sockets/chat",
|
|
311
|
+
name: "chat",
|
|
312
|
+
userId: "u1",
|
|
313
|
+
});
|
|
314
|
+
inspect.record({
|
|
315
|
+
kind: "socket",
|
|
316
|
+
socketKind: "send",
|
|
317
|
+
name: "chat",
|
|
318
|
+
preview: '{"hi":1}',
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const pool = {
|
|
322
|
+
query: async <R extends Record<string, unknown>>(sql: string) => {
|
|
323
|
+
if (sql.includes("robodev_auth.users")) return { rows: [] as unknown as R[] };
|
|
324
|
+
if (sql.includes("count(*)") && sql.includes("robodev_storage.objects")) {
|
|
325
|
+
return { rows: [{ count: "2" }] as unknown as R[] };
|
|
326
|
+
}
|
|
327
|
+
if (sql.includes("robodev_storage.objects")) {
|
|
328
|
+
return {
|
|
329
|
+
rows: [
|
|
330
|
+
{
|
|
331
|
+
key: "a.png",
|
|
332
|
+
public: true,
|
|
333
|
+
size_bytes: 12,
|
|
334
|
+
content_type: "image/png",
|
|
335
|
+
created_at: new Date("2026-01-01T00:00:00.000Z"),
|
|
336
|
+
},
|
|
337
|
+
] as unknown as R[],
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return { rows: [] as unknown as R[] };
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const state = await buildDashboardState({
|
|
345
|
+
inspect,
|
|
346
|
+
projectId: "local",
|
|
347
|
+
runtime: {
|
|
348
|
+
routes: [],
|
|
349
|
+
jobs: [],
|
|
350
|
+
sockets: [
|
|
351
|
+
{ name: "chat", path: "/sockets/chat", file: "sockets/chat.ts", def: { auth: false } },
|
|
352
|
+
],
|
|
353
|
+
pool,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
assert.equal(state.overview.storageObjectCount, 2);
|
|
358
|
+
assert.equal(state.overview.socketCount, 1);
|
|
359
|
+
assert.equal(state.storage.objects.length, 1);
|
|
360
|
+
assert.equal(state.storage.objects[0]?.url, "/storage/objects/a.png");
|
|
361
|
+
assert.equal(state.storage.objects[0]?.sizeBytes, 12);
|
|
362
|
+
assert.equal(state.storage.activity.length, 3);
|
|
363
|
+
assert.equal(state.storage.activity[0]?.op, "serve");
|
|
364
|
+
assert.equal(state.sockets.defs[0]?.auth, "public");
|
|
365
|
+
assert.equal(state.sockets.connected, 0);
|
|
366
|
+
assert.equal(state.sockets.events.length, 2);
|
|
367
|
+
assert.equal(state.sockets.events[0]?.socketKind, "send");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("storage object count is 0 when the query fails", async () => {
|
|
371
|
+
const state = await buildDashboardState({
|
|
372
|
+
inspect: createDevInspect(),
|
|
373
|
+
runtime: {
|
|
374
|
+
routes: [],
|
|
375
|
+
jobs: [],
|
|
376
|
+
pool: {
|
|
377
|
+
query: async () => {
|
|
378
|
+
throw new Error("missing schema");
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
assert.equal(state.overview.storageObjectCount, 0);
|
|
384
|
+
assert.deepEqual(state.storage.objects, []);
|
|
385
|
+
});
|