mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-11 09:26:53 -04:00
@martmull v2.0 ;) --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
256 lines
9.9 KiB
JavaScript
256 lines
9.9 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const PLUGIN_ROOT = path.resolve(__dirname, '..', '..');
|
|
const PLUGIN_JSON_PATH = path.join(PLUGIN_ROOT, '.codex-plugin', 'plugin.json');
|
|
const PACKAGE_JSON_PATH = path.join(PLUGIN_ROOT, 'package.json');
|
|
const MCP_JSON_PATH = path.join(PLUGIN_ROOT, '.mcp.json');
|
|
const MARKETPLACE_TEMPLATE_PATH = path.join(PLUGIN_ROOT, 'templates', 'marketplace.example.json');
|
|
|
|
const metadata = require('../validators/metadata');
|
|
const assets = require('../validators/assets');
|
|
const skills = require('../validators/skills');
|
|
const references = require('../validators/references');
|
|
const crossDocContracts = require('../validators/cross-doc-contracts');
|
|
const setupHelper = require('../validators/setup-helper');
|
|
|
|
const collectFailures = (assertion) => {
|
|
const failures = [];
|
|
assertion((message) => failures.push(message));
|
|
return failures;
|
|
};
|
|
|
|
const withFileMutation = (filePath, mutator, body) => {
|
|
const original = fs.readFileSync(filePath, 'utf8');
|
|
|
|
try {
|
|
fs.writeFileSync(filePath, mutator(original));
|
|
body();
|
|
} finally {
|
|
fs.writeFileSync(filePath, original);
|
|
}
|
|
};
|
|
|
|
const withJsonMutation = (filePath, mutator, body) =>
|
|
withFileMutation(
|
|
filePath,
|
|
(original) => {
|
|
const data = JSON.parse(original);
|
|
mutator(data);
|
|
return `${JSON.stringify(data, null, 2)}\n`;
|
|
},
|
|
body,
|
|
);
|
|
|
|
const withExtraFile = (filePath, contents, body) => {
|
|
try {
|
|
fs.writeFileSync(filePath, contents);
|
|
body();
|
|
} finally {
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Smoke tests — every assertion should pass on the current plugin state.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('assertJsonMetadata passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(metadata.assertJsonMetadata), []);
|
|
});
|
|
|
|
test('assertNoBundledMcpConfig passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(metadata.assertNoBundledMcpConfig), []);
|
|
});
|
|
|
|
test('assertInterfaceFields passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(metadata.assertInterfaceFields), []);
|
|
});
|
|
|
|
test('assertMarketplaceTemplate passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(metadata.assertMarketplaceTemplate), []);
|
|
});
|
|
|
|
test('assertAssets passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(assets.assertAssets), []);
|
|
});
|
|
|
|
test('assertSkills passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(skills.assertSkills), []);
|
|
});
|
|
|
|
test('assertSkillTriggerPhrases passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(skills.assertSkillTriggerPhrases), []);
|
|
});
|
|
|
|
test('assertNoLegacySkillReferences passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(skills.assertNoLegacySkillReferences), []);
|
|
});
|
|
|
|
test('assertReferences passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(references.assertReferences), []);
|
|
});
|
|
|
|
test('assertHowAppsWork passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(references.assertHowAppsWork), []);
|
|
});
|
|
|
|
test('assertTwentyMcpFormattingContract passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(crossDocContracts.assertTwentyMcpFormattingContract), []);
|
|
});
|
|
|
|
test('assertFrontComponentGuidance passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(crossDocContracts.assertFrontComponentGuidance), []);
|
|
});
|
|
|
|
test('assertCliGuidanceSplit passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(crossDocContracts.assertCliGuidanceSplit), []);
|
|
});
|
|
|
|
test('assertTestingGuidance passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(crossDocContracts.assertTestingGuidance), []);
|
|
});
|
|
|
|
test('assertSetupHelper passes on current state', () => {
|
|
assert.deepStrictEqual(collectFailures(setupHelper.assertSetupHelper), []);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Negative cases — each assertion catches its targeted failure.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('assertJsonMetadata catches version mismatch between package.json and plugin.json', () => {
|
|
withJsonMutation(PACKAGE_JSON_PATH, (pkg) => { pkg.version = '99.99.99'; }, () => {
|
|
const failures = collectFailures(metadata.assertJsonMetadata);
|
|
assert.ok(
|
|
failures.some((f) => f.includes('version must match')),
|
|
`expected version-mismatch failure, got: ${failures.join('; ')}`,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('assertJsonMetadata catches missing .mcp.json from package.json files', () => {
|
|
withJsonMutation(PACKAGE_JSON_PATH, (pkg) => {
|
|
pkg.files = pkg.files.filter((f) => f !== '.mcp.json');
|
|
}, () => {
|
|
const failures = collectFailures(metadata.assertJsonMetadata);
|
|
assert.ok(failures.some((f) => f.includes('.mcp.json')));
|
|
});
|
|
});
|
|
|
|
test('assertJsonMetadata catches non-canonical MCP server', () => {
|
|
withJsonMutation(MCP_JSON_PATH, (mcp) => {
|
|
mcp.mcpServers['rogue-server'] = { url: 'https://example.com/mcp' };
|
|
}, () => {
|
|
const failures = collectFailures(metadata.assertJsonMetadata);
|
|
assert.ok(failures.some((f) => f.includes('twenty-docs')));
|
|
});
|
|
});
|
|
|
|
test('assertNoBundledMcpConfig catches a bundled .app.json', () => {
|
|
const stub = path.join(PLUGIN_ROOT, '.app.json');
|
|
withExtraFile(stub, '{}', () => {
|
|
const failures = collectFailures(metadata.assertNoBundledMcpConfig);
|
|
assert.ok(failures.some((f) => f.includes('app declarations must not be shipped')));
|
|
});
|
|
});
|
|
|
|
test('assertNoBundledMcpConfig catches a non-placeholder URL', () => {
|
|
const stub = path.join(PLUGIN_ROOT, 'scratch-url-check.md');
|
|
withExtraFile(stub, 'see https://internal.private-domain.test/secret for details', () => {
|
|
const failures = collectFailures(metadata.assertNoBundledMcpConfig);
|
|
assert.ok(failures.some((f) => f.includes('non-placeholder URL')));
|
|
});
|
|
});
|
|
|
|
test('assertNoBundledMcpConfig catches a bearer token', () => {
|
|
const stub = path.join(PLUGIN_ROOT, 'scratch-bearer.md');
|
|
withExtraFile(stub, 'Authorization: Bearer abc123def456ghi789jkl012mno', () => {
|
|
const failures = collectFailures(metadata.assertNoBundledMcpConfig);
|
|
assert.ok(failures.some((f) => f.includes('bearer token')));
|
|
});
|
|
});
|
|
|
|
test('assertInterfaceFields catches invalid brandColor', () => {
|
|
withJsonMutation(PLUGIN_JSON_PATH, (j) => { j.interface.brandColor = 'red'; }, () => {
|
|
const failures = collectFailures(metadata.assertInterfaceFields);
|
|
assert.ok(failures.some((f) => f.includes('brandColor must match')));
|
|
});
|
|
});
|
|
|
|
test('assertInterfaceFields catches too-long shortDescription', () => {
|
|
withJsonMutation(PLUGIN_JSON_PATH, (j) => { j.interface.shortDescription = 'x'.repeat(100); }, () => {
|
|
const failures = collectFailures(metadata.assertInterfaceFields);
|
|
assert.ok(failures.some((f) => f.includes('shortDescription must be 64')));
|
|
});
|
|
});
|
|
|
|
test('assertInterfaceFields catches unknown category', () => {
|
|
withJsonMutation(PLUGIN_JSON_PATH, (j) => { j.interface.category = 'Photography'; }, () => {
|
|
const failures = collectFailures(metadata.assertInterfaceFields);
|
|
assert.ok(failures.some((f) => f.includes('category must be one of')));
|
|
});
|
|
});
|
|
|
|
test('assertInterfaceFields catches invalid capability', () => {
|
|
withJsonMutation(PLUGIN_JSON_PATH, (j) => { j.interface.capabilities = ['Magic']; }, () => {
|
|
const failures = collectFailures(metadata.assertInterfaceFields);
|
|
assert.ok(failures.some((f) => f.includes('capabilities contains invalid value')));
|
|
});
|
|
});
|
|
|
|
test('assertInterfaceFields catches empty defaultPrompt', () => {
|
|
withJsonMutation(PLUGIN_JSON_PATH, (j) => { j.interface.defaultPrompt = []; }, () => {
|
|
const failures = collectFailures(metadata.assertInterfaceFields);
|
|
assert.ok(failures.some((f) => f.includes('defaultPrompt')));
|
|
});
|
|
});
|
|
|
|
test('assertAssets catches a missing screenshot reference', () => {
|
|
withJsonMutation(PLUGIN_JSON_PATH, (j) => {
|
|
j.interface.screenshots = ['./assets/screenshots/nonexistent.png'];
|
|
}, () => {
|
|
const failures = collectFailures(assets.assertAssets);
|
|
assert.ok(failures.some((f) => f.includes('screenshots entry is missing')));
|
|
});
|
|
});
|
|
|
|
test('assertAssets catches a non-PNG logo', () => {
|
|
withJsonMutation(PLUGIN_JSON_PATH, (j) => { j.interface.logo = './assets/twenty-logo.svg'; }, () => {
|
|
const failures = collectFailures(assets.assertAssets);
|
|
assert.ok(failures.some((f) => f.includes('logo must be a PNG')));
|
|
});
|
|
});
|
|
|
|
test('assertMarketplaceTemplate catches version drift', () => {
|
|
withJsonMutation(MARKETPLACE_TEMPLATE_PATH, (t) => {
|
|
t.plugins[0].version = '0.0.0';
|
|
}, () => {
|
|
const failures = collectFailures(metadata.assertMarketplaceTemplate);
|
|
assert.ok(failures.some((f) => f.includes('version must match')));
|
|
});
|
|
});
|
|
|
|
test('assertSkillTriggerPhrases catches a SKILL.md missing the When To Use section', () => {
|
|
const skillPath = path.join(PLUGIN_ROOT, 'skills', 'create-app', 'SKILL.md');
|
|
withFileMutation(skillPath, (original) => original.replace(/^#+\s+When To Use[\s\S]*?(?=\n#\s)/m, ''), () => {
|
|
const failures = collectFailures(skills.assertSkillTriggerPhrases);
|
|
assert.ok(failures.some((f) => f.includes('create-app') && f.includes('When To Use')));
|
|
});
|
|
});
|
|
|
|
test('assertTestingGuidance catches missing manage-app test target instructions', () => {
|
|
const skillPath = path.join(PLUGIN_ROOT, 'skills', 'manage-app', 'SKILL.md');
|
|
withFileMutation(skillPath, (original) => original.replace('TWENTY_API_URL=http://localhost:2021 yarn test', 'yarn test'), () => {
|
|
const failures = collectFailures(crossDocContracts.assertTestingGuidance);
|
|
assert.ok(
|
|
failures.some((f) => f.includes('manage-app/SKILL.md') && f.includes('TWENTY_API_URL')),
|
|
`expected manage-app test target failure, got: ${failures.join('; ')}`,
|
|
);
|
|
});
|
|
});
|