← Files JUJIES ArbeitsblätterARCHIVED FILE
tests/check-package.mjs
8.8 KB · Nov 11, 2026 · 13:26 UTC
import { readFile, readdir, mkdtemp, mkdir, cp, rm, writeFile, stat } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createHash } from 'node:crypto';
import assert from 'node:assert/strict';
import { checkInteractiveMaterial } from '../skills/arbeitsblatt-html/scripts/check-interactive-material.mjs';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const examplesRoot = path.join(root, 'skills/arbeitsblatt-html/assets/beispiele');
// Authoring/test caches never belong in a distributed plugin.
async function checkCleanTree(directory) {
for (const item of await readdir(directory,{withFileTypes:true})) {
assert(item.name !== '__pycache__' && !item.name.endsWith('.pyc'), `Generated Python cache in package: ${item.name}`);
if(item.isDirectory()) await checkCleanTree(path.join(directory,item.name));
}
}
await checkCleanTree(root);
const manifest = JSON.parse(await readFile(path.join(root, 'plugin.json'), 'utf8'));
const nativeManifest = JSON.parse(await readFile(path.join(root, '.codex-plugin/plugin.json'), 'utf8'));
assert.equal(manifest.name, 'jujies-arbeitsblaetter');
assert.match(manifest.version, /^\d+\.\d+\.\d+$/);
assert.equal(nativeManifest.version, manifest.version);
assert.deepEqual(nativeManifest.interface, manifest.extensions['com.openai'].interface, 'Compatibility interface must match the canonical manifest before host normalization');
assert(!('interface' in nativeManifest.extensions['com.openai']), 'Compatibility interface must occur only in its native top-level location');
assert.deepEqual(nativeManifest.extensions['com.openai'].publication, manifest.extensions['com.openai'].publication, 'Publication metadata must survive host normalization unchanged');
const references = JSON.parse(await readFile(path.join(root, 'tests/originalreferenzen.json'), 'utf8'));
assert.equal(references.version, manifest.version);
const examples = (await readdir(examplesRoot)).sort();
const additions = JSON.parse(await readFile(path.join(root, 'tests/ergaenzungen-0.1.8.json'), 'utf8'));
const addedIds = additions.map(x => x.id);
const expected = ['bio-opioide-einstieg', 'hamburg-city-bikes-listening', 'immer-richtung-sueden-lesebegleitung', 'ottomotor-bauteile-funktionen', 'ottomotor-vier-takte', 'transport-announcements-listening', ...addedIds].sort();
assert.deepEqual(examples, expected);
const results = [];
for (const id of examples) {
const sourceDirectory = path.join(examplesRoot, id);
const result = await checkInteractiveMaterial({ sourceDirectory, requirePrint: true });
// The active revision uses shared pages; historical entry files remain for saved instances.
assert.equal(result.print.layout, 'shared-pages');
assert.equal(result.entry, 'pages.html');
assert.equal(result.print.entry, result.entry);
const html = await readFile(path.join(sourceDirectory, result.entry), 'utf8');
if (!addedIds.includes(id)) {
const migration = JSON.parse(await readFile(path.join(root, 'tests/shared-pages-migration.json'), 'utf8')).find(x => x.id === id);
assert.equal((html.match(/\bdata-tafelraum-page=/g) ?? []).length, migration.expectedPages);
assert.equal(createHash('sha256').update(await readFile(path.join(sourceDirectory, 'index.html'))).digest('hex'), migration.legacyHtmlSha256);
assert.equal(createHash('sha256').update(await readFile(path.join(sourceDirectory, 'pages.html'))).digest('hex'), migration.newHtmlSha256);
assert.equal(createHash('sha256').update(await readFile(path.join(sourceDirectory, 'pages.css'))).digest('hex'), migration.newCssSha256);
} else {
assert.equal((html.match(/\bdata-tafelraum-page(?:=|>)/g) ?? []).length, 1);
assert.doesNotMatch(html, /contenteditable=/);
const added = additions.find(x => x.id === id);
for (const file of added.files) assert.equal(createHash('sha256').update(await readFile(path.join(root, file.path))).digest('hex'), file.sha256, file.path);
}
const fields = [...html.matchAll(/\bid="([^"]+)"/g)].map(x => x[1]);
assert.equal(new Set(fields).size, fields.length, `${id}: doppelte IDs`);
assert.doesNotMatch(html, /<script\b|<iframe\b|\bchecked(?:\s|=|>)/i);
for (const match of html.matchAll(/\b(?:src|href)=["']([^"']+)["']/gi)) {
const value = match[1];
if (/^(?:https?:|#|mailto:)/i.test(value)) continue; // Source-attribution links are not runtime dependencies.
assert.doesNotMatch(value, /^(?:\/\/|file:)/i);
const resolved = path.resolve(sourceDirectory, value.split('#')[0]);
assert.ok(resolved.startsWith(sourceDirectory + path.sep));
assert.ok((await stat(resolved)).isFile(), `${id}: missing ${value}`);
}
assert.doesNotMatch(html, /\bsrc=["'](?:https?:|\/\/|file:)/i);
async function checkCssTree(directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const filename = path.join(directory, entry.name);
if (entry.isDirectory()) await checkCssTree(filename);
else if (entry.name.endsWith('.css')) assert.doesNotMatch(await readFile(filename, 'utf8'), /@import|url\(\s*["']?(?:https?:|\/\/|file:)/i);
assert.ok(!/^(?:LEHRKRAFT|UI_BRIEF)\.md$/.test(entry.name), `${id}: teacher file in student package`);
}
}
await checkCssTree(sourceDirectory);
results.push({ id, print: result.print, fileCount: result.fileCount });
}
for (const original of references.files) {
const contents = await readFile(path.join(root, original.pluginRelativePath));
assert.equal(createHash('sha256').update(contents).digest('hex'), original.pluginSha256 ?? original.sha256, original.pluginRelativePath);
}
// Independently exercise the current new-sheet contract; historical examples do not weaken it.
const scratchRoot = path.resolve(process.cwd(), 'work');
await mkdir(scratchRoot, { recursive: true });
const tmp = await mkdtemp(path.join(scratchRoot, 'plugin-tests-'));
const target = path.join(tmp, 'contract-probe');
await mkdir(target);
const probeManifest = { format: 'tafelraum-interactive-html', schemaVersion: 1, id: 'contract-probe', title: 'Paketvertragsprobe', entry: 'index.html', print: { entry: 'index.html', page: 'A4', orientation: 'portrait', mode: 'template', layout: 'shared-pages' }, defaultViewport: { width: 794, height: 1123 } };
const probeHtml = '<!doctype html><html lang="de"><head><meta charset="utf-8"><title>Probe</title></head><body><section data-tafelraum-page="1"><label for="answer">Antwort</label><textarea id="answer"></textarea></section></body></html>';
try {
async function reset() {
await writeFile(path.join(target, 'manifest.json'), JSON.stringify(probeManifest));
await writeFile(path.join(target, 'index.html'), probeHtml);
}
async function alterManifest(change) {
const value = structuredClone(probeManifest); change(value);
await writeFile(path.join(target, 'manifest.json'), JSON.stringify(value));
}
await reset();
const valid = await checkInteractiveMaterial({ sourceDirectory: target, requirePrint: true });
assert.equal(valid.print.layout, 'shared-pages');
await alterManifest(v => { delete v.print; });
await assert.rejects(checkInteractiveMaterial({ sourceDirectory: target, requirePrint: true }), /Druckvorlage/);
await reset(); await alterManifest(v => { v.print.entry = 'print.html'; });
await assert.rejects(checkInteractiveMaterial({ sourceDirectory: target, requirePrint: true }), /denselben/);
await reset(); await writeFile(path.join(target, 'index.html'), probeHtml.replaceAll('data-tafelraum-page', 'data-other-page'));
await assert.rejects(checkInteractiveMaterial({ sourceDirectory: target, requirePrint: true }), /data-tafelraum-page/);
await reset(); await writeFile(path.join(target, 'index.html'), probeHtml.replace('</body>', '<script>let phase=0;</script></body>'));
await assert.rejects(checkInteractiveMaterial({ sourceDirectory: target, requirePrint: true }), /getState/);
await reset(); await alterManifest(v => { v.entry = '../outside.html'; });
await assert.rejects(checkInteractiveMaterial({ sourceDirectory: target, requirePrint: true }), /ungültig/);
// Regression: delivery documentation must not break an otherwise valid import.
await reset();
await writeFile(path.join(target, 'README.txt'), 'Öffnungsanleitung');
await assert.rejects(checkInteractiveMaterial({ sourceDirectory: target, requirePrint: true }), /Dateityp.*nicht unterstützt/);
await rm(path.join(target, 'README.txt'));
await writeFile(path.join(tmp, 'ANLEITUNG.md'), 'Diesen Text nicht als Material importieren.');
const withoutDeliveryDocs = await checkInteractiveMaterial({ sourceDirectory: target, requirePrint: true });
assert.equal(withoutDeliveryDocs.id, 'contract-probe');
} finally { await rm(tmp, { recursive: true, force: true }); }
console.log(JSON.stringify({ ok: true, originalCases: 9, examples: results, verifiedSourceFiles: references.files.length, currentSharedPagesContract: true, rejectionCases: 6, version: manifest.version }, null, 2));
SHA-256: e11b5b008eb282b3cbe716dd2f6e46110443081e965d80523dcf4b550c4d3c68