Bonapara/twenty codex plugin (#20857)
@martmull v2.0 ;) --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
committed by
GitHub
parent
cb744b2eeb
commit
1642be86f5
@@ -0,0 +1,255 @@
|
||||
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('; ')}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user