import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { buildPageTemplateMandatoryInstruction, collectPageTemplateViolations, extractStyleSelectors, resolveSelectedPageTemplateSkill, validatePublishedPageAgainstTemplate, } from './page-template-enforcement.mjs'; test('resolveSelectedPageTemplateSkill reads latest user selectedChatSkill', () => { const skill = resolveSelectedPageTemplateSkill([ { role: 'user', metadata: { memindRun: { selectedChatSkill: 'page-template-travel' } } }, { role: 'assistant', content: [{ type: 'text', text: 'done' }] }, ]); assert.equal(skill, 'page-template-travel'); }); test('validatePublishedPageAgainstTemplate rejects extra css selectors', () => { const templatePath = path.join(process.cwd(), 'skills/page-template-travel/templates/travel-hero.html'); const templateHtml = fs.readFileSync(templatePath, 'utf8'); const driftedHtml = templateHtml .replace('{{PAGE_TITLE}}', '标题') .replace('{{PAGE_DESCRIPTION}}', '摘要') .replace('{{MINDSPACE_COVER_JSON}}', '{}') .replace('{{ACCENT}}', '#0ea5e9') .replace('{{ACCENT2}}', '#0369a1') .replace('{{HERO_EYEBROW}}', '标签') .replace('{{HERO_HEADLINE}}', '主标题') .replace('{{HERO_SUBTITLE}}', '副标题') .replace('{{HERO_IMAGE}}', 'assets/hero.webp') .replace('{{MAIN_CONTENT_HTML}}', '

正文

') .replace( '.content h2 { color: #fff; margin: 2.5rem 0 1rem; font-size: clamp(22px, 4vw, 28px); letter-spacing: -.01em; }', '.content h2 { color: #fff; margin: 2.5rem 0 1rem; font-size: clamp(22px, 4vw, 28px); letter-spacing: -.01em; }\n .spot-card { padding: 12px; }', ); const result = validatePublishedPageAgainstTemplate({ skillName: 'page-template-travel', html: driftedHtml, h5Root: process.cwd(), }); assert.equal(result.ok, false); assert.equal(result.reason, 'style_drift'); }); test('validatePublishedPageAgainstTemplate accepts filled travel template', () => { const templatePath = path.join(process.cwd(), 'skills/page-template-travel/templates/travel-hero.html'); let html = fs.readFileSync(templatePath, 'utf8'); html = html .replaceAll('{{PAGE_TITLE}}', '标题') .replaceAll('{{PAGE_DESCRIPTION}}', '摘要') .replaceAll('{{MINDSPACE_COVER_JSON}}', '{}') .replaceAll('{{ACCENT}}', '#0ea5e9') .replaceAll('{{ACCENT2}}', '#0369a1') .replaceAll('{{HERO_EYEBROW}}', '标签') .replaceAll('{{HERO_HEADLINE}}', '主标题') .replaceAll('{{HERO_SUBTITLE}}', '副标题') .replaceAll('{{HERO_IMAGE}}', 'assets/hero.webp') .replaceAll('{{MAIN_CONTENT_HTML}}', '

章节

正文

'); const result = validatePublishedPageAgainstTemplate({ skillName: 'page-template-travel', html, h5Root: process.cwd(), }); assert.equal(result.ok, true); }); test('collectPageTemplateViolations scans touched public html files', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'page-template-guard-')); const publishDir = path.join(root, 'public'); fs.mkdirSync(publishDir, { recursive: true }); const xianjuPath = path.join(process.cwd(), 'MindSpace/5331128a-810e-42ef-b4f6-95a075d12787/public/xianju-water-guide.html'); if (fs.existsSync(xianjuPath)) { fs.copyFileSync(xianjuPath, path.join(publishDir, 'xianju-water-guide.html')); const violations = collectPageTemplateViolations({ messages: [ { role: 'user', metadata: { memindRun: { selectedChatSkill: 'page-template-travel' } }, }, ], publishDir: root, syncResult: { publicHtmlRelativePaths: ['public/xianju-water-guide.html'] }, h5Root: process.cwd(), }); assert.ok(violations.length >= 1); assert.equal(violations[0].reason, 'style_drift'); } else { assert.match(buildPageTemplateMandatoryInstruction('page-template-travel'), /禁止从零手写/); } }); test('extractStyleSelectors ignores at-rules', () => { const selectors = extractStyleSelectors('@media (max-width: 600px) { .hero { color: red; } } .hero { color: blue; }'); assert.deepEqual([...selectors], ['.hero']); });