Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion scripts/validate-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,11 @@ function main() {
console.log(`Validated ${metadataFiles.length} metadata files.`);
}

main();
if (require.main === module) {
main();
}

module.exports = {
parseScalar,
validateMetadataFile,
};
151 changes: 151 additions & 0 deletions tests/validate-metadata.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
const test = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { validateMetadataFile } = require('../scripts/validate-metadata');

function withTempDir(testFn) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'validate-metadata-test-'));
try {
testFn(tempDir);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}

test('validateMetadataFile is imported correctly', () => {
assert.strictEqual(typeof validateMetadataFile, 'function');
});

const VALID_METADATA_YAML = `
id: "test-model"
name: "Test Model"
description: "A test model"
tags:
- test
- mock
category: "other"
compatibility:
bng2_compatible: true
uses_compartments: false
uses_energy: false
uses_functions: true
nfsim_compatible: false
simulation_methods: [ode, ssa]
source:
origin: "published"
original_repository: "https://github.com/example/repo"
playground:
visible: true
gallery_category: "Test"
featured: false
difficulty: "beginner"
`;

test('valid metadata file passes validation without errors', () => {
withTempDir((tempDir) => {
const metadataFile = path.join(tempDir, 'metadata.yaml');
fs.writeFileSync(metadataFile, VALID_METADATA_YAML);
fs.writeFileSync(path.join(tempDir, 'README.md'), '# Test Model');
fs.writeFileSync(path.join(tempDir, 'testmodel.bngl'), 'begin model\nend model');

const errors = [];
validateMetadataFile(metadataFile, errors);

assert.deepStrictEqual(errors, []);
});
});

test('missing README.md adds error', () => {
withTempDir((tempDir) => {
const metadataFile = path.join(tempDir, 'metadata.yaml');
fs.writeFileSync(metadataFile, VALID_METADATA_YAML);
fs.writeFileSync(path.join(tempDir, 'testmodel.bngl'), 'begin model\nend model');

const errors = [];
validateMetadataFile(metadataFile, errors);

assert.strictEqual(errors.length, 1);
assert.match(errors[0], /missing README\.md/);
});
});

test('missing .bngl file adds error', () => {
withTempDir((tempDir) => {
const metadataFile = path.join(tempDir, 'metadata.yaml');
fs.writeFileSync(metadataFile, VALID_METADATA_YAML);
fs.writeFileSync(path.join(tempDir, 'README.md'), '# Test Model');

const errors = [];
validateMetadataFile(metadataFile, errors);

assert.strictEqual(errors.length, 1);
assert.match(errors[0], /no \.bngl files found alongside metadata\.yaml/);
});
});

test('invalid metadata structure adds errors', () => {
withTempDir((tempDir) => {
const metadataFile = path.join(tempDir, 'metadata.yaml');
const invalidYaml = `
id: "test-model"
# missing name and description
category: "invalid-category"
# missing source, playground, and compatibility
`;
fs.writeFileSync(metadataFile, invalidYaml);
fs.writeFileSync(path.join(tempDir, 'README.md'), '# Test Model');
fs.writeFileSync(path.join(tempDir, 'testmodel.bngl'), 'begin model\nend model');

const errors = [];
validateMetadataFile(metadataFile, errors);

assert.ok(errors.length > 0);
assert.ok(errors.some(e => e.includes('missing or invalid name')));
assert.ok(errors.some(e => e.includes('missing or invalid description')));
assert.ok(errors.some(e => e.includes('invalid category ("invalid-category")')));
assert.ok(errors.some(e => e.includes('missing compatibility section')));
assert.ok(errors.some(e => e.includes('missing source section')));
assert.ok(errors.some(e => e.includes('missing playground section')));
});
});

test('multiple models without collection adds error if no primary model', () => {
withTempDir((tempDir) => {
const metadataFile = path.join(tempDir, 'metadata.yaml');
fs.writeFileSync(metadataFile, VALID_METADATA_YAML); // id: "test-model"
fs.writeFileSync(path.join(tempDir, 'README.md'), '# Test Model');
// none of these match "test-model"
fs.writeFileSync(path.join(tempDir, 'othermodel1.bngl'), 'begin model\nend model');
fs.writeFileSync(path.join(tempDir, 'othermodel2.bngl'), 'begin model\nend model');

const errors = [];
validateMetadataFile(metadataFile, errors);

assert.strictEqual(errors.length, 1);
assert.match(errors[0], /multiple \.bngl files require either a collection section or a primary model file/);
});
});

test('multiple models with collection valid count', () => {
withTempDir((tempDir) => {
const metadataFile = path.join(tempDir, 'metadata.yaml');
const collectionYaml = VALID_METADATA_YAML + `
collection:
type: "parameter-fit-variants"
parent_model: "test-model"
variant_key: "test"
count: 2
`;
fs.writeFileSync(metadataFile, collectionYaml);
fs.writeFileSync(path.join(tempDir, 'README.md'), '# Test Model');
fs.writeFileSync(path.join(tempDir, 'model1.bngl'), '');
fs.writeFileSync(path.join(tempDir, 'model2.bngl'), '');

const errors = [];
validateMetadataFile(metadataFile, errors);

assert.deepStrictEqual(errors, []);
});
});
Loading