import fs from 'node:fs/promises'; import path from 'node:path'; import assert from 'node:assert/strict'; import crypto from 'node:crypto'; import { pathToFileURL } from 'node:url'; import { buildModel } from '../src/core/kfs/build.js'; import { seedMemberGroups } from '../src/core/member-groups.js'; import { CORE_DELIVERABLES, DELIVERY_KINDS, DELIVERY_SCHEMA } from '../skills/ksf-workshop-runtime/scripts/report-contract.mjs'; const out = path.resolve('docs/workshop-report-parity-20261111'); const origin = 'http://127.0.0.1:4181'; const raw = { artifact: [], session: [], project: [], insight: [], opportunity: [{ objectId: 'separate-opportunity', title: '浏览器隔离夹具机会' }], source: [] }; const at = new Date().toISOString(); const htmlFile = Buffer.from('

浏览器隔离夹具提案

'); const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a5YkAAAAASUVORK5CYII=', 'base64'); const publicFiles = new Map(); for (let i = 0; i < 35; i++) { const participant = 'browser-participant-' + i, sessionId = 'browser-session-' + i; raw.session.push({ objectId: sessionId, studentName: '浏览器夹具组员' + i, workshopKey: 'browser-topic', groupId: 'G' + (i % 5 + 1), meta: { participantKey: participant }, createdAt: at }); for (const output of i === 0 ? CORE_DELIVERABLES : CORE_DELIVERABLES.slice(0, 1)) { const id = 'browser-report-' + i + '-' + output.key; const artifact = { objectId: id, title: '浏览器隔离夹具:' + output.name, ownerName: '浏览器夹具组员' + i, ownerSession: sessionId, uploadedByKey: participant, agentName: '隔离测试共用智能体', artifactType: DELIVERY_KINDS[output.key].types[0], stageCode: 'S5', dataMode: 'sample', workshopKey: 'browser-topic', version: 1, reportSeriesKey: 'browser-topic:D2-08:' + output.key, createdAt: at, rawText: '浏览器隔离测试正文,不代表真实研究成果。', report: { title: '浏览器隔离夹具:' + output.name, executive: ['浏览器隔离测试,不代表真实研究成果。'], delivery: { schema: DELIVERY_SCHEMA, moduleId: 'D2-08', kind: output.key, contentStatus: 'ready', outputs: [output.key] }, chapters: [{ id: output.anchor, title: output.name, sections: [{ kind: 'para', text: '浏览器隔离测试正文:核对同一人的分类、正文、文件与报告跳转。' }] }] } }; if (output.key === 'proposal') { artifact.report.attachments = [['报告.html','text/html',htmlFile],['示意图.png','image/png',png]].map(([name,contentType,bytes], index) => { const file = { id: 'browser-file-' + index, name, contentType, sizeBytes: bytes.length, sha256: crypto.createHash('sha256').update(bytes).digest('hex') }; file.url = origin + '/api/functions?path=%2Fksf%2Ffile&fileId=' + file.id + '&artifactId=' + id; publicFiles.set(file.id, { ...file, bytes });return file; }); } raw.artifact.push(artifact); } } const db = buildModel(raw), groups = seedMemberGroups(db.members); const { chromium } = await import(pathToFileURL(path.resolve('work/workshop-manual-tools/node_modules/playwright/index.mjs')).href); const browser = await chromium.launch({ headless: true, executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' }); const checks = [], errors = [], requests = []; try { const context = await browser.newContext({ viewport: { width: 1280, height: 900 } }); await context.route('**/*', route => { const request = route.request(), url = new URL(request.url()); if (url.pathname === '/api/functions' && url.searchParams.get('path') === '/ksf/file') { const file = publicFiles.get(url.searchParams.get('fileId')); if (!file) return route.abort(); return route.fulfill({ contentType: file.contentType, body: file.bytes, headers: { 'Content-Security-Policy': "default-src 'none'; sandbox", 'Content-Disposition': url.searchParams.has('preview') ? 'inline' : 'attachment' } }); } if (url.pathname === '/api/functions' || url.pathname === '/__dev/member-groups') { const body = request.postDataJSON();requests.push(body?.path); const response = body?.path === '/ksfGetPublicBoard' ? raw : body?.path === '/ksfGetPublicMemberGroups' ? groups : null; if (!response) { errors.push('Unexpected action ' + body?.path);return route.abort(); } return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ result: { ok: true, data: response } }) }); } if (url.origin === origin && request.method() === 'GET') return route.continue(); errors.push('Unexpected network request ' + url.origin + url.pathname);return route.abort(); }); const page = await context.newPage();page.on('pageerror', error => errors.push(error.message)); await page.goto(origin + '/workshop?mode=sample'); await page.locator('[data-output-member]').first().waitFor(); assert.equal(await page.locator('[data-output-member]').count(), 35); assert.equal(await page.locator('[data-output-group]').count(), 6); // Includes empty, explicitly ungrouped section. const memberId = db.artifactById['browser-report-0-market'].memberId; const member = page.locator(`[data-output-member="${memberId}"]`); await member.locator(':scope > summary').click(); assert.equal(await member.locator('[data-delivery-state="saved"]').count(), 7); assert.equal(await member.locator('[data-workshop-artifact]').count(), 7); checks.push('35名隔离夹具组员、5组、个人七类交付状态与报告链接'); for (const width of [1280,390]) { await page.setViewportSize({ width, height: 900 }); const size = await page.evaluate(() => ({ innerWidth, scrollWidth: document.documentElement.scrollWidth })); assert.ok(size.scrollWidth <= size.innerWidth, JSON.stringify(size)); await page.screenshot({ path: path.join(out, 'personal-reports-' + width + '.png'), fullPage: false }); checks.push(width + 'px个人报告页无横向溢出'); } for (const output of CORE_DELIVERABLES) { await page.goto(origin + '/workshop?mode=sample&reportType=' + DELIVERY_KINDS[output.key].category); await page.locator(`[data-output-member="${memberId}"] > summary`).click(); const row = page.locator('[data-workshop-artifact="browser-report-0-' + output.key + '"]'); await row.locator('h3 a').click(); await page.locator('view-report [data-chapter="' + output.anchor + '"]').waitFor(); assert.ok((await page.locator('view-report [data-chapter="' + output.anchor + '"]').textContent()).includes('浏览器隔离测试正文')); checks.push(output.name + ':个人分类报告点击进入同编号正文'); } assert.equal(await page.locator('[data-report-attachments] img').count(), 1); assert.equal(await page.locator('[data-report-attachments] a').filter({ hasText: '下载文件' }).count(), 2); await page.evaluate(() => { window.__reportScriptExecuted = false;window.addEventListener('message', e => { if (e.data === 'unexpected-report-script') window.__reportScriptExecuted = true; }); }); await page.locator('[data-report-attachments] details summary').click(); await page.frameLocator('[data-report-attachments] iframe').locator('h1').waitFor(); assert.equal(await page.locator('[data-report-attachments] iframe').getAttribute('sandbox'), ''); assert.equal(await page.evaluate(() => window.__reportScriptExecuted), false); assert.ok(await page.locator('[data-report-attachments] img').evaluate(img => img.complete && img.naturalWidth > 0)); await page.screenshot({ path: path.join(out, 'report-files-mobile.png'), fullPage: true }); checks.push('图片实际加载、HTML受限预览和文件下载入口;HTML脚本未执行'); await page.goto(origin + '/reports');await page.locator('[data-types]').waitFor(); assert.equal(await page.locator('[data-type-card]').count(), 11); checks.push('报告中心11类入口可渲染,独立机会不混入报告数'); await page.goto(origin + '/docs/workshop-quality-r13-20261110/' + encodeURIComponent('final-康师傅工作坊操作手册.html') + '#topic'); await page.locator('#prompt-topic-1').waitFor(); assert.match(await page.locator('#prompt-topic-1').textContent(), /按当前登录员工个人保存/); checks.push('手册课题收尾的复制正文包含后续个人保存、分类和文件关联约定'); await context.close(); } finally { await browser.close(); } assert.deepEqual(errors, []); await fs.writeFile(path.join(out, 'browser-check.json'), JSON.stringify({ checkedAt: new Date().toISOString(), productionWrites: 0, method: 'Local real components, isolated browser-only fixtures, all API requests intercepted; not real classroom results', checks, errors, requestFunctions: [...new Set(requests)] }, null, 2) + '\n'); console.log(JSON.stringify({ checks: checks.length, errors, productionWrites: 0 }));