admin-pages-flow.test.js
5.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// End-to-end check of the exact sequence admin-characters.html /
// admin-garments.html / admin-dance-refs.html perform in the browser:
// login -> upload a real file -> create the record with the returned URL
// -> list -> confirm the shape has everything the page renders. Doesn't
// render actual HTML/JS (no browser tooling available here), but exercises
// the same API calls in the same order with real files, which is where a
// field-name mismatch between a page's JS and the backend would surface.
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");
const { spawn } = require("node:child_process");
const PORT = 21934 + Math.floor(Math.random() * 1000);
const BASE = `http://localhost:${PORT}`;
const ADMIN_PASSWORD = "test-password-only-for-this-run";
let cookie;
let child;
let cwd;
const TINY_VALID_PNG = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEklEQVR4nGPkEpFjYGBgYgADAALmAEAUQs4PAAAAAElFTkSuQmCC",
"base64"
);
function waitForServer(url, timeoutMs = 10000) {
const deadline = Date.now() + timeoutMs;
return (async function poll() {
while (Date.now() < deadline) {
try {
await fetch(url);
return;
} catch {
await new Promise((r) => setTimeout(r, 100));
}
}
throw new Error("server did not come up in time");
})();
}
test.before(async () => {
cwd = fs.mkdtempSync(path.join(os.tmpdir(), "admin-pages-flow-"));
for (const f of ["server.js", "ingest.js", "uploads.js", "store.js", "dressup-store.js", "prompt-template.js", "db.js", "pricing.js"]) {
fs.copyFileSync(path.join(__dirname, f), path.join(cwd, f));
}
child = spawn(process.execPath, ["server.js"], {
cwd,
env: { ...process.env, PORT: String(PORT), H3_CONSOLE_BASE: "", H3_CONSOLE_TOKEN: "", ADMIN_PASSWORD },
stdio: "pipe",
});
await waitForServer(BASE);
const loginRes = await fetch(`${BASE}/api/admin/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: ADMIN_PASSWORD }),
});
cookie = (loginRes.headers.get("set-cookie") || "").split(";")[0];
});
test.after(() => {
child.kill();
fs.rmSync(cwd, { recursive: true, force: true });
});
test("admin-characters.html flow: upload image then create, list shows imageUrl reachable via GET /uploads/*", async () => {
const fd = new FormData();
fd.append("file", new Blob([TINY_VALID_PNG], { type: "image/png" }), "person.png");
const uploadRes = await fetch(`${BASE}/api/admin/upload`, { method: "POST", headers: { Cookie: cookie }, body: fd });
assert.equal(uploadRes.status, 200);
const { url: imageUrl } = await uploadRes.json();
const createRes = await fetch(`${BASE}/api/admin/characters`, {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: cookie },
body: JSON.stringify({ name: "小美", imageUrl, appearanceDescriptor: "black hair", tags: ["seed"], isDefault: true }),
});
assert.equal(createRes.status, 200);
const created = await createRes.json();
const list = await fetch(`${BASE}/api/admin/characters`, { headers: { Cookie: cookie } }).then((r) => r.json());
const row = list.find((c) => c.id === created.id);
assert.ok(row);
assert.equal(row.imageUrl, imageUrl);
const served = await fetch(`${BASE}${row.imageUrl}`);
assert.equal(served.status, 200);
});
test("admin-garments.html flow: upload image then create, thumbnailUrl falls back to imageUrl for the table's <img>", async () => {
const fd = new FormData();
fd.append("file", new Blob([TINY_VALID_PNG], { type: "image/png" }), "garment.png");
const uploadRes = await fetch(`${BASE}/api/admin/upload`, { method: "POST", headers: { Cookie: cookie }, body: fd });
const { url: imageUrl } = await uploadRes.json();
const createRes = await fetch(`${BASE}/api/admin/garments`, {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: cookie },
body: JSON.stringify({ name: "红旗袍", category: "古风", imageUrl, appearanceDescriptor: "red qipao" }),
});
const created = await createRes.json();
assert.equal(created.thumbnailUrl, imageUrl);
const served = await fetch(`${BASE}${created.thumbnailUrl}`);
assert.equal(served.status, 200);
});
test("admin-dance-refs.html flow: upload real mp4 then create, videoUrl reachable and motionDescriptor round-trips", async () => {
const realMp4 = fs.readFileSync(path.join(__dirname, "spike", "s1-s2-s4-2026-09-02", "danceref.mp4"));
const fd = new FormData();
fd.append("file", new Blob([realMp4], { type: "video/mp4" }), "dance.mp4");
const uploadRes = await fetch(`${BASE}/api/admin/upload-video`, { method: "POST", headers: { Cookie: cookie }, body: fd });
assert.equal(uploadRes.status, 200);
const { url: videoUrl } = await uploadRes.json();
const createRes = await fetch(`${BASE}/api/admin/dance-refs`, {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: cookie },
body: JSON.stringify({ name: "拉丁舞", videoUrl, motionDescriptor: "hip sways and raised-arm turns", styleTags: ["latin"] }),
});
assert.equal(createRes.status, 200);
const created = await createRes.json();
assert.equal(created.videoUrl, videoUrl);
assert.equal(created.motionDescriptor, "hip sways and raised-arm turns");
const served = await fetch(`${BASE}${created.videoUrl}`);
assert.equal(served.status, 200);
assert.equal(served.headers.get("content-type"), "video/mp4");
});