102 lines
4.1 KiB
JavaScript
102 lines
4.1 KiB
JavaScript
|
|
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']);
|
||
|
|
});
|