c136f06823
Memind CI / Test, build, and release guards (push) Successful in 4m51s
Rules-first safety gate covering adult content, minor protection, legal compliance (stock tips, crypto promotion, gambling, drugs, source provenance for political news) and ethics (tragedy tone, discrimination, gossip). Filters without blocking publication: - SearXNG prefetch uses safesearch=2 and drops block-level results before they can be supplemented into the page - worker sanitizes the page after dedupe and after the image gate, with a single batched model review; review-level items are removed fail-safe when the reviewer is unavailable - sections the gate filtered are exempt from fill/structure audits so filtering never turns into a failed generation - push/preview recheck removes block-level items and masks title/digest - AI-assisted disclosure in page footer and WeChat draft, plus aigc and content-safety meta markers Co-authored-by: Cursor <cursoragent@cursor.com>
132 lines
6.1 KiB
JavaScript
132 lines
6.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
DEFAULT_NEWS_MORNING_TEMPLATE_ID,
|
|
NEWS001_TEMPLATE_SPEC,
|
|
NEWS001_VERSION,
|
|
NEWS002_CORE_SECTION_IDS,
|
|
NEWS002_FLEX_SECTION_IDS,
|
|
NEWS002_MAX_STORY_CARDS,
|
|
NEWS002_SECTIONS,
|
|
NEWS002_STYLE_BLOCK,
|
|
NEWS002_TEMPLATE_SPEC,
|
|
NEWS002_VERSION,
|
|
NEWS_MORNING_TEMPLATES,
|
|
NEWS_MORNING_TEMPLATE_IDS,
|
|
auditNews002SectionFill,
|
|
buildNewsMorningTemplateSpec,
|
|
countNews002SectionItems,
|
|
getNewsMorningTemplate,
|
|
NEWS002_FILL_REQUIREMENT_SPEC,
|
|
resolveNewsMorningTemplateId,
|
|
} from './news-morning-templates.mjs';
|
|
|
|
test('registry exposes news001 and news002', () => {
|
|
assert.deepEqual(NEWS_MORNING_TEMPLATE_IDS, ['news001', 'news002']);
|
|
assert.equal(NEWS_MORNING_TEMPLATES.news001.status, 'frozen');
|
|
assert.equal(NEWS_MORNING_TEMPLATES.news001.version, NEWS001_VERSION);
|
|
assert.equal(NEWS_MORNING_TEMPLATES.news002.version, NEWS002_VERSION);
|
|
});
|
|
|
|
test('news001 stays byte-identical to the frozen 0918 spec', () => {
|
|
// news001 已上生产,任何改动都必须走 news003,不允许就地改。
|
|
assert.equal(NEWS001_VERSION, '2026-09-18-refined');
|
|
assert.match(NEWS001_TEMPLATE_SPEC, /quick-links 固定顺序/);
|
|
assert.match(NEWS001_TEMPLATE_SPEC, /card\.highlight-box/);
|
|
assert.equal(NEWS_MORNING_TEMPLATES.news001.supportsCardImages, false);
|
|
});
|
|
|
|
test('resolveNewsMorningTemplateId falls back safely', () => {
|
|
assert.equal(resolveNewsMorningTemplateId(null, {}), DEFAULT_NEWS_MORNING_TEMPLATE_ID);
|
|
assert.equal(resolveNewsMorningTemplateId('news002', {}), 'news002');
|
|
assert.equal(resolveNewsMorningTemplateId('NEWS002', {}), 'news002');
|
|
assert.equal(resolveNewsMorningTemplateId('news999', {}), DEFAULT_NEWS_MORNING_TEMPLATE_ID);
|
|
assert.equal(
|
|
resolveNewsMorningTemplateId(null, { H5_WECHAT_NEWS_MORNING_TEMPLATE_ID: 'news002' }),
|
|
'news002',
|
|
);
|
|
assert.equal(
|
|
resolveNewsMorningTemplateId('news001', { H5_WECHAT_NEWS_MORNING_TEMPLATE_ID: 'news002' }),
|
|
'news001',
|
|
'显式配置优先于环境变量',
|
|
);
|
|
});
|
|
|
|
test('news002 section tiers split core and flex', () => {
|
|
assert.ok(NEWS002_CORE_SECTION_IDS.includes('headlines'));
|
|
assert.ok(NEWS002_CORE_SECTION_IDS.includes('brief'));
|
|
assert.ok(NEWS002_CORE_SECTION_IDS.includes('deepdive'));
|
|
assert.ok(NEWS002_FLEX_SECTION_IDS.includes('living'));
|
|
assert.equal(
|
|
NEWS002_CORE_SECTION_IDS.length + NEWS002_FLEX_SECTION_IDS.length,
|
|
NEWS002_SECTIONS.length,
|
|
);
|
|
for (const section of NEWS002_SECTIONS) {
|
|
assert.ok(section.min <= section.max, `${section.id} 条数区间非法`);
|
|
}
|
|
});
|
|
|
|
test('news002 caps total story cards well below news001 output', () => {
|
|
// 0918 生产页产出 49 条卡片、仅约 20 个独立事件。
|
|
assert.equal(NEWS002_MAX_STORY_CARDS, 30);
|
|
assert.match(NEWS002_TEMPLATE_SPEC, new RegExp(`全页独立事件卡上限 ${NEWS002_MAX_STORY_CARDS} 条`));
|
|
});
|
|
|
|
test('news002 spec mandates event keys, images and dark mode', () => {
|
|
assert.match(NEWS002_TEMPLATE_SPEC, /data-event-key/);
|
|
assert.match(NEWS002_TEMPLATE_SPEC, /loading="lazy"/);
|
|
assert.match(NEWS002_TEMPLATE_SPEC, /referrerpolicy="no-referrer"/);
|
|
assert.match(NEWS002_TEMPLATE_SPEC, /prefers-color-scheme/);
|
|
assert.equal(NEWS_MORNING_TEMPLATES.news002.supportsCardImages, true);
|
|
});
|
|
|
|
test('news002 style block is self-contained and carries the new palette', () => {
|
|
assert.ok(NEWS002_STYLE_BLOCK.startsWith('<style>'));
|
|
assert.ok(NEWS002_STYLE_BLOCK.endsWith('</style>'));
|
|
assert.match(NEWS002_STYLE_BLOCK, /--brand:#C8362F/);
|
|
assert.match(NEWS002_STYLE_BLOCK, /--accent:#1B3A6B/);
|
|
assert.match(NEWS002_STYLE_BLOCK, /--paper:#FAF7F2/);
|
|
assert.match(NEWS002_STYLE_BLOCK, /@media \(prefers-color-scheme:dark\)/);
|
|
assert.ok(!NEWS002_STYLE_BLOCK.includes('#b0191e'), 'news002 不得残留 news001 主色');
|
|
});
|
|
|
|
test('buildNewsMorningTemplateSpec assembles content + seo + layout', () => {
|
|
const spec001 = buildNewsMorningTemplateSpec('news001', {});
|
|
assert.match(spec001, /版式模板:news001/);
|
|
assert.match(spec001, /版式参照(只读结构):https:\/\//);
|
|
|
|
const spec002 = buildNewsMorningTemplateSpec('news002', {});
|
|
assert.match(spec002, /版式模板:news002/);
|
|
assert.match(spec002, /内容硬约束/);
|
|
assert.match(spec002, /SEO \/ GEO 必做/);
|
|
assert.ok(spec002.includes(NEWS002_STYLE_BLOCK));
|
|
assert.ok(spec002.includes(NEWS002_FILL_REQUIREMENT_SPEC));
|
|
assert.ok(!spec002.includes('版式参照(只读结构)'), 'news002 自带样式,不依赖外部参照页');
|
|
});
|
|
|
|
test('auditNews002SectionFill requires core max counts and flex coverage', () => {
|
|
const sparse = `<div class="section" id="brief"><ol class="brief"><li><span class="n">1</span><span>一</span></li></ol></div>
|
|
<div class="section" id="headlines"><a class="lead-card" data-event-key="a"><div class="body"><h3>头条</h3></div></a></div>`;
|
|
const sparseAudit = auditNews002SectionFill(sparse);
|
|
assert.equal(sparseAudit.ok, false);
|
|
assert.ok(sparseAudit.violations.some((item) => item.startsWith('underfilled:brief:')));
|
|
assert.ok(sparseAudit.violations.some((item) => item.startsWith('insufficient-flex-sections:')));
|
|
|
|
assert.equal(countNews002SectionItems(sparse, 'brief'), 1);
|
|
assert.equal(countNews002SectionItems(sparse, 'headlines'), 1);
|
|
});
|
|
|
|
test('auditNews002SectionFill exempts sections the content safety gate filtered', () => {
|
|
const sparse = `<div class="section" id="brief"><ol class="brief"><li><span class="n">1</span><span>一</span></li></ol></div>
|
|
<div class="section" id="headlines"><a class="lead-card" data-event-key="a"><div class="body"><h3>头条</h3></div></a></div>`;
|
|
const audit = auditNews002SectionFill(sparse, { exemptSectionIds: ['brief', 'deepdive'] });
|
|
assert.ok(!audit.violations.some((item) => item.startsWith('underfilled:brief:')));
|
|
assert.ok(!audit.violations.some((item) => item.startsWith('underfilled:deepdive:')));
|
|
assert.ok(audit.violations.some((item) => item.startsWith('underfilled:domestic:')));
|
|
});
|
|
|
|
test('getNewsMorningTemplate returns the resolved template object', () => {
|
|
assert.equal(getNewsMorningTemplate('news002', {}).id, 'news002');
|
|
assert.equal(getNewsMorningTemplate('bogus', {}).id, DEFAULT_NEWS_MORNING_TEMPLATE_ID);
|
|
});
|