import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import assert from 'node:assert/strict'; import { fileURLToPath } from 'node:url'; import { login, root } from './role-permissions-live.mjs'; export { login }; export const work = path.resolve(root, '../.codex-tmp/scene-closure-20260906'); export const report = path.join(root, 'ute2e/reports/scene-editor-closure-20260906'); export const fixturePath = path.join(work, 'fixtures.json'); fs.mkdirSync(work, { recursive: true }); fs.mkdirSync(report, { recursive: true }); export const fixture = () => JSON.parse(fs.readFileSync(fixturePath, 'utf8')); export function writeFixture(value) { const lock = `${fixturePath}.lock`; let handle; for (let attempt=0; attempt<100; attempt++) { try { handle=fs.openSync(lock,'wx'); break; } catch (error) { if(error.code!=='EEXIST')throw error; Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,20); } } if(handle===undefined)throw new Error('Scene fixture registry is busy'); try { const existing=fs.existsSync(fixturePath)?fixture():{}; const merged={...existing,...value}; for(const key of ['projects','extraUsers','roleIds']) { const entries=[...(existing[key]||[]),...(value[key]||[])]; if(entries.length)merged[key]=[...new Map(entries.map(item=>[typeof item==='object'?String(item.id):String(item),item])).values()]; } fs.writeFileSync(fixturePath,JSON.stringify(merged,null,2)); } finally { fs.closeSync(handle); fs.unlinkSync(lock); } } export const hash = value => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex'); export async function call(endpoint, token, body, method = body === undefined ? 'GET' : 'POST') { const response = await fetch(`http://127.0.0.1:6180/api/${endpoint}`, { method, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(120000), }); const value = await response.json(); return { status: response.status, code: value.code, data: value.data, message: value.message }; } export async function teacherLogin() { const user = fixture().user; const response = await call('auth/v1/auth/login', null, { userId: user.id, password: user.password, roleCode: 'teacher', departmentId: user.departmentId, rememberMe: false }); assert.equal(response.status, 200, 'Scene review teacher signs in'); return response.data; } export function registerProject(project) { const f = fixture(); if (!f.projects.some(p => p.id === project.id)) f.projects.push({ id: project.id, name: project.name, code: project.code, ownerId: project.ownerId }); writeFixture(f); } export async function freshScene(token, key = 'fox', suffix = '') { const f = fixture(), source = f.originals[key]; const result = await call(`tran/v1/content/projects/${source.id}/copy`, token, { name: `场景闭环-${key}-${suffix}-${Date.now()}`, version: source.version }); assert.equal(result.status, 200, `Copy scene: ${result.message}`); registerProject(result.data.project); return result.data; } async function prepare() { if (fs.existsSync(fixturePath) && !fixture().cleanedAt && fixture().user) throw new Error('Scene fixtures already active; reuse existing fixtures'); const session = await login('root'), token = session.accessToken; const f = { createdAt: new Date().toISOString(), originals: {}, projects: [], user: null }; writeFixture(f); try { for (const [key, id] of [['fox', '160'], ['truck', '159'], ['foxModel', '157'], ['truckModel', '156']]) { const original = await call(`tran/v1/content/projects/${id}`, token); assert.equal(original.status, 200); f.originals[key] = { id, name: original.data.project.name, version: original.data.project.version, hash: hash(original.data) }; fs.writeFileSync(path.join(work, `original-${key}.json`), JSON.stringify(original.data, null, 2)); writeFixture(f); } const me = (await call('auth/v1/auth/me', token)).data; const roles = (await call('auth/v1/roles/all', token)).data; const teacher = (roles.records || roles).find(r => r.code === 'teacher'); const password = `Sc!${crypto.randomBytes(7).toString('hex')}`; const made = await call('auth/v1/users', token, { username: `rv_scene_${crypto.randomBytes(4).toString('hex')}`, displayName: '场景编辑器闭环验收', departmentId: me.departmentId, roleIds: [teacher.id], defaultRoleId: teacher.id, enabled: true, password, remark: 'UTE2E-SCENE-CLOSURE-20260906', }); assert.equal(made.status, 200, `Temporary teacher: ${made.message}`); f.user = { ...made.data, password }; writeFixture(f); console.log(JSON.stringify({ prepared: true, originals: Object.fromEntries(Object.entries(f.originals).map(([k,v])=>[k,v.id])) })); } finally { await call('auth/v1/auth/logout', token, {}).catch(()=>{}); } } if (process.argv[1] === fileURLToPath(import.meta.url)) prepare().catch(e => { console.error(e.message); process.exitCode = 1; });