Files
memind/page-template-enforcement.test.mjs
T

102 lines
4.1 KiB
JavaScript
Raw Normal View History

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}}', '<p>正文</p>')
.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}}', '<h2>章节</h2><p>正文</p>');
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']);
});