Merge pull request 'Walk the live password-change gate in k6 load setup' (#442) from feature/u7-k6-password-gate into main
ci / test-pg (push) Successful in 1m34s
ci / test (push) Successful in 3m24s

Reviewed-on: https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/442
This commit was merged in pull request #442.
This commit is contained in:
2026-10-12 06:27:44 +00:00
2 changed files with 260 additions and 12 deletions
File diff suppressed because one or more lines are too long
+55 -12
View File
@@ -81,30 +81,63 @@ function otherBase(mine) {
return BASE_URLS[0] === mine ? BASE_URLS[1] : BASE_URLS[0];
}
const jsonHeaders = { headers: { 'Content-Type': 'application/json' } };
function parseJSON(res) {
try {
return res.json();
} catch (e) {
return null;
}
}
// 与现网一致:管理员建号 → 首登 must_change → POST /api/auth/password → 再登录。
// 压测口令保持不变(next=同一密码),只走完设密门,不另造后门清标志。
function loginReady(base, owner, password) {
const login = http.post(`${base}/api/auth/login`, JSON.stringify({ owner, password }), jsonHeaders);
if (login.status !== 200) return login;
const body = parseJSON(login);
if (body && body.mustChangePassword) {
const ch = http.post(`${base}/api/auth/password`, JSON.stringify({ next: password }), jsonHeaders);
if (ch.status !== 200) return ch;
return http.post(`${base}/api/auth/login`, JSON.stringify({ owner, password }), jsonHeaders);
}
return login;
}
// ---- setup:管理员建压测用户;双实例时建共享热点会话 ----
export function setup() {
const adminBase = BASE_URLS[0];
const login = http.post(`${adminBase}/api/auth/login`, JSON.stringify({ owner: ADMIN_OWNER, password: ADMIN_PASS }), { headers: { 'Content-Type': 'application/json' } });
if (login.status !== 200) throw new Error(`admin login: ${login.status} ${login.body}`);
const adminLogin = loginReady(adminBase, ADMIN_OWNER, ADMIN_PASS);
if (adminLogin.status !== 200) throw new Error(`admin login: ${adminLogin.status} ${adminLogin.body}`);
const users = [];
for (let i = 0; i < USERS; i++) {
const owner = `load_u${i}`;
const password = `load-user-pass-${i}-Aa9`;
const created = http.post(`${adminBase}/api/admin/users`, JSON.stringify({ owner, name: `压测用户${i}`, password, isAdmin: false }), { headers: { 'Content-Type': 'application/json' } });
const created = http.post(`${adminBase}/api/admin/users`, JSON.stringify({ owner, name: `压测用户${i}`, password, isAdmin: false }), jsonHeaders);
if (created.status !== 200 && created.status !== 409) throw new Error(`create user ${owner}: ${created.status} ${created.body}`);
users.push({ owner, password });
}
// 建号必须保持管理员会话;设密会换 cookie,只能在全员建完后逐个走门。
for (const u of users) {
const gated = loginReady(adminBase, u.owner, u.password);
if (gated.status !== 200) throw new Error(`activate ${u.owner}: ${gated.status} ${gated.body}`);
}
// 热点会话:多 VU 跨实例争抢同一会话(C2「互相不拖累」的争用源)。
// 建用户时 cookie 落在最后一位 load_u* 上,热点资源须用管理员会话。
const adminAgain = loginReady(adminBase, ADMIN_OWNER, ADMIN_PASS);
if (adminAgain.status !== 200) throw new Error(`admin re-login: ${adminAgain.status} ${adminAgain.body}`);
let hotspot = null;
if (BASE_URLS.length >= 2) {
const proj = http.post(`${adminBase}/api/projects`, JSON.stringify({ name: 'hotspot-shared' }), { headers: { 'Content-Type': 'application/json' } });
const proj = http.post(`${adminBase}/api/projects`, JSON.stringify({ name: 'hotspot-shared' }), jsonHeaders);
if (proj.status !== 200) throw new Error(`hotspot project: ${proj.status} ${proj.body}`);
const conv = http.post(`${adminBase}/api/projects/${proj.json().id}/conversations`, JSON.stringify({ title: '热点会话' }), { headers: { 'Content-Type': 'application/json' } });
const projBody = parseJSON(proj);
const conv = http.post(`${adminBase}/api/projects/${projBody.id}/conversations`, JSON.stringify({ title: '热点会话' }), jsonHeaders);
if (conv.status !== 200) throw new Error(`hotspot conv: ${conv.status} ${conv.body}`);
hotspot = { projectID: proj.json().id, convID: conv.json().id, owner: ADMIN_OWNER, password: ADMIN_PASS };
hotspot = { projectID: projBody.id, convID: parseJSON(conv).id, owner: ADMIN_OWNER, password: ADMIN_PASS };
}
return { users, hotspot, bases: BASE_URLS };
@@ -115,16 +148,26 @@ export function setup() {
export default function (data) {
const base = baseForVU();
const u = data.users[exec.scenario.iterationInTest % data.users.length];
const login = http.post(`${base}/api/auth/login`, JSON.stringify({ owner: u.owner, password: u.password }), { headers: { 'Content-Type': 'application/json' } });
const login = loginReady(base, u.owner, u.password);
if (login.status !== 200) {
check(null, { 'user login': () => false });
return;
}
if (!u.__ctx) {
const proj = http.post(`${base}/api/projects`, JSON.stringify({ name: `压测-${u.owner}` }), { headers: { 'Content-Type': 'application/json' } });
const conv = http.post(`${base}/api/projects/${proj.json().id}/conversations`, JSON.stringify({ title: '压测会话' }), { headers: { 'Content-Type': 'application/json' } });
u.__ctx = { projectID: proj.json().id, convID: conv.json().id };
const proj = http.post(`${base}/api/projects`, JSON.stringify({ name: `压测-${u.owner}` }), jsonHeaders);
const projBody = parseJSON(proj);
if (proj.status !== 200 || !projBody || !projBody.id) {
check(proj, { 'create project 2xx': () => false });
return;
}
const conv = http.post(`${base}/api/projects/${projBody.id}/conversations`, JSON.stringify({ title: '压测会话' }), jsonHeaders);
const convBody = parseJSON(conv);
if (conv.status !== 200 || !convBody || !convBody.id) {
check(conv, { 'create conversation 2xx': () => false });
return;
}
u.__ctx = { projectID: projBody.id, convID: convBody.id };
}
const { projectID, convID } = u.__ctx;
@@ -143,7 +186,7 @@ export default function (data) {
if (BASE_URLS.length >= 2 && exec.scenario.iterationInTest % 5 === 0) {
const other = otherBase(base);
// 另一实例需同用户登录(cookie jar 按 VU;换 host 需重新登录)。
const loginB = http.post(`${other}/api/auth/login`, JSON.stringify({ owner: u.owner, password: u.password }), { headers: { 'Content-Type': 'application/json' } });
const loginB = loginReady(other, u.owner, u.password);
if (loginB.status === 200) {
const cross = http.get(`${other}/api/conversations/${convID}`);
if (cross.status === 200) crossReadOk.add(1);
@@ -158,7 +201,7 @@ export default function (data) {
if (data.hotspot && exec.scenario.iterationInTest % 5 === 0) {
targetConv = data.hotspot.convID;
// 热点会话属 admin:先以 admin 登录再写。
const hl = http.post(`${writeBase}/api/auth/login`, JSON.stringify({ owner: data.hotspot.owner, password: data.hotspot.password }), { headers: { 'Content-Type': 'application/json' } });
const hl = loginReady(writeBase, data.hotspot.owner, data.hotspot.password);
if (hl.status !== 200) {
check(null, { 'hotspot admin login': () => false });
return;