File size: 5,604 Bytes
2b64d42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { createHash } from 'node:crypto';
import { copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
const ROOT = resolve(process.cwd());
function makeWorkspace({ includeHuggingFaceModel = false } = {}) {
const workspace = mkdtempSync(join(tmpdir(), 'wa-gen-docs-'));
const srcDir = join(workspace, 'src');
const docsDir = join(workspace, 'docs');
const scriptsDir = join(workspace, 'scripts');
const dashboardDir = join(srcDir, 'dashboard');
mkdirSync(srcDir, { recursive: true });
mkdirSync(docsDir, { recursive: true });
mkdirSync(scriptsDir, { recursive: true });
mkdirSync(dashboardDir, { recursive: true });
copyFileSync(join(ROOT, 'package.json'), join(workspace, 'package.json'));
copyFileSync(join(ROOT, 'src/models.js'), join(srcDir, 'models.js'));
copyFileSync(join(ROOT, 'scripts/gen-docs-models.js'), join(scriptsDir, 'gen-docs-models.js'));
copyFileSync(join(ROOT, 'docs/index.html'), join(docsDir, 'index.html'));
if (!includeHuggingFaceModel) {
return workspace;
}
const modelsPath = join(srcDir, 'models.js');
let source = readFileSync(modelsPath, 'utf8');
const marker = source.indexOf('// Build reverse lookup');
if (marker < 0) {
throw new Error('unexpected models.js structure: missing // Build reverse lookup marker');
}
const objectEndMatch = source.match(/\r?\n\};\r?\n\r?\n\/\/ Build reverse lookup/);
const objectEnd = objectEndMatch ? objectEndMatch.index : -1;
if (objectEnd < 0) {
throw new Error('unexpected models.js structure: missing end of MODELS object');
}
const patch = "\n 'model-huggingface-custom': { name: 'model-huggingface-custom', provider: 'huggingface', enumValue: 999999, modelUid: 'model-huggingface-custom', credit: 1 },\n";
source = `${source.slice(0, objectEnd + 1)}${patch}${source.slice(objectEnd + 1)}`;
writeFileSync(modelsPath, source, 'utf8');
return workspace;
}
function runGenerator(workspace) {
const result = spawnSync(process.execPath, ['scripts/gen-docs-models.js'], {
cwd: workspace,
encoding: 'utf8',
timeout: 20000,
});
return result;
}
function parseModels(htmlText) {
const start = htmlText.indexOf('const MODELS = [');
assert.ok(start >= 0, 'MODELS array anchor not found');
const open = htmlText.indexOf('[', start);
const close = htmlText.indexOf(' ];', start);
assert.ok(close > open, 'MODELS array end marker not found');
const literal = htmlText.slice(open, close + 3);
const models = new Function(`return ${literal}`)();
assert.ok(Array.isArray(models), 'MODELS should be an array');
return models;
}
function readDocsHtml(workspace) {
return readFileSync(join(workspace, 'docs', 'index.html'), 'utf8');
}
function sha256(content) {
return createHash('sha256').update(content).digest('hex');
}
describe('scripts/gen-docs-models.js', () => {
it('regenerates a parseable MODELS block and keeps HTML shape', () => {
const workspace = makeWorkspace();
try {
const result = runGenerator(workspace);
assert.equal(result.status, 0, `generator exit: ${result.status}, stderr=${result.stderr}`);
const html = readDocsHtml(workspace);
const models = parseModels(html);
assert.ok(html.includes('<html'), 'generated docs should include html root');
assert.ok(html.includes('</html>'), 'generated docs should include html close tag');
assert.equal(typeof html, 'string');
assert.ok(models.length >= 100, `expected at least 100 models, got ${models.length}`);
assert.ok(models.every(model => model?.k && model?.p && typeof model.c === 'number'));
// Pick a stable, non-deprecated marker. `adaptive` was removed from
// /v1/models in v2.0.51 (#109) — upstream rejects its UID — so it
// can't be used as a presence anchor. claude-sonnet-4.6 is the
// current default and is unlikely to be deprecated near-term.
assert.ok(models.some(model => model.k === 'claude-sonnet-4.6'));
} finally {
rmSync(workspace, { recursive: true, force: true });
}
});
it('is idempotent when run repeatedly on the same input', () => {
const workspace = makeWorkspace();
try {
const first = runGenerator(workspace);
assert.equal(first.status, 0, `first run failed: ${first.stderr}`);
const firstHtml = readDocsHtml(workspace);
const second = runGenerator(workspace);
assert.equal(second.status, 0, `second run failed: ${second.stderr}`);
const secondHtml = readDocsHtml(workspace);
assert.equal(sha256(firstHtml), sha256(secondHtml), 'generator output changed between runs');
assert.deepEqual(parseModels(firstHtml), parseModels(secondHtml));
} finally {
rmSync(workspace, { recursive: true, force: true });
}
});
it('supports unknown provider values when models are merged in', () => {
const workspace = makeWorkspace({ includeHuggingFaceModel: true });
try {
const result = runGenerator(workspace);
assert.equal(result.status, 0, `generator failed on unknown provider: ${result.stderr}`);
const html = readDocsHtml(workspace);
const models = parseModels(html);
assert.ok(models.some(model => model.k === 'model-huggingface-custom' && model.p === 'huggingface'));
} finally {
rmSync(workspace, { recursive: true, force: true });
}
});
});
|