Files
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

450 lines
17 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as metadata from '../validators/metadata.js';
import * as assets from '../validators/assets.js';
import * as skills from '../validators/skills.js';
import * as references from '../validators/references.js';
import * as crossDocContracts from '../validators/cross-doc-contracts.js';
import * as setupHelper from '../validators/setup-helper.js';
const PLUGIN_ROOT = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
);
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 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('assertFrontComponentGuidance catches independent UI version guidance', () => {
const frontComponentsPath = path.join(
PLUGIN_ROOT,
'references/develop-app/front-components.md',
);
withFileMutation(
frontComponentsPath,
(contents) => contents.replace(
'at the same version as `twenty-sdk` and `twenty-client-sdk`',
'at an independent version',
),
() => {
const failures = collectFailures(crossDocContracts.assertFrontComponentGuidance);
assert.ok(failures.some((failure) => failure.includes('same version as')));
},
);
});
test('assertCliGuidanceSplit passes on current state', () => {
assert.deepStrictEqual(collectFailures(crossDocContracts.assertCliGuidanceSplit), []);
});
test('assertCliGuidanceSplit catches deprecated dev --once guidance', () => {
const cliAndSyncPath = path.join(
PLUGIN_ROOT,
'references',
'manage-app',
'cli-and-sync.md',
);
withFileMutation(
cliAndSyncPath,
(contents) => contents.replace('yarn twenty apply', 'yarn twenty dev --once'),
() => {
const failures = collectFailures(crossDocContracts.assertCliGuidanceSplit);
assert.ok(
failures.some((failure) => failure.includes('yarn twenty dev --once')),
);
},
);
});
test('assertCliGuidanceSplit catches deprecated guidance outside the CLI reference', () => {
const manageSkillPath = path.join(
PLUGIN_ROOT,
'skills',
'manage-app',
'SKILL.md',
);
withFileMutation(
manageSkillPath,
(contents) => `${contents}\nRun yarn twenty dev --once after editing.\n`,
() => {
const failures = collectFailures(crossDocContracts.assertCliGuidanceSplit);
assert.ok(
failures.some(
(failure) =>
failure.includes('skills/manage-app/SKILL.md') &&
failure.includes('yarn twenty dev --once'),
),
);
},
);
});
test('assertCliGuidanceSplit catches stale one-shot sync terminology', () => {
const standalonePagesPath = path.join(
PLUGIN_ROOT,
'references',
'develop-app',
'standalone-pages.md',
);
withFileMutation(
standalonePagesPath,
(contents) => `${contents}\nUse one-shot sync for verification.\n`,
() => {
const failures = collectFailures(crossDocContracts.assertCliGuidanceSplit);
assert.ok(
failures.some(
(failure) =>
failure.includes('references/develop-app/standalone-pages.md') &&
failure.includes('one-shot sync'),
),
);
},
);
});
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('source scans ignore generated distribution files', () => {
const distributionRoot = path.join(PLUGIN_ROOT, 'dist');
fs.mkdirSync(distributionRoot, { recursive: true });
const fixtureRoot = fs.mkdtempSync(path.join(distributionRoot, 'source-validation-'));
try {
fs.writeFileSync(path.join(fixtureRoot, '.mcp.json'), '{}');
fs.writeFileSync(path.join(fixtureRoot, 'SKILL.md'), 'name: create-an-app\n');
assert.deepStrictEqual(collectFailures(metadata.assertNoBundledMcpConfig), []);
assert.deepStrictEqual(collectFailures(skills.assertNoLegacySkillReferences), []);
} finally {
fs.rmSync(fixtureRoot, { recursive: true, force: true });
}
});
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 requires marketplace identity', () => {
withJsonMutation(MARKETPLACE_TEMPLATE_PATH, (template) => {
delete template.name;
delete template.interface;
}, () => {
const failures = collectFailures(metadata.assertMarketplaceTemplate);
assert.ok(failures.some((failure) => failure.includes('name must be a non-empty string')));
assert.ok(failures.some((failure) => failure.includes('interface.displayName must be a non-empty string')));
});
});
test('assertMarketplaceTemplate catches the legacy source descriptor', () => {
withJsonMutation(MARKETPLACE_TEMPLATE_PATH, (template) => {
template.plugins[0].source = {
type: 'local',
path: './packages/twenty-agent-skills/dist',
};
}, () => {
const failures = collectFailures(metadata.assertMarketplaceTemplate);
assert.ok(failures.some((failure) => failure.includes('source.source must be local')));
});
});
for (const [field, value] of [['installation', 'manual'], ['authentication', 'user-local']]) {
test(`assertMarketplaceTemplate catches legacy ${field} policy`, () => {
withJsonMutation(MARKETPLACE_TEMPLATE_PATH, (template) => {
template.plugins[0].policy[field] = value;
}, () => {
const failures = collectFailures(metadata.assertMarketplaceTemplate);
assert.ok(failures.some((failure) => failure.includes(`policy.${field} must be`)));
});
});
}
test('assertMarketplaceTemplate requires the installable distribution path', () => {
withJsonMutation(MARKETPLACE_TEMPLATE_PATH, (template) => {
template.plugins[0].source.path = './packages/twenty-agent-skills';
}, () => {
const failures = collectFailures(metadata.assertMarketplaceTemplate);
assert.ok(failures.some((failure) => failure.includes('source.path must be ./packages/twenty-agent-skills/dist')));
});
});
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('; ')}`,
);
});
});
test('assertOperatingRulesSingleSource passes on current state', () => {
assert.deepStrictEqual(collectFailures(crossDocContracts.assertOperatingRulesSingleSource), []);
});
test('assertOperatingRulesSingleSource catches AGENTS.md restating a rule', () => {
const agentsPath = path.join(PLUGIN_ROOT, 'AGENTS.md');
withFileMutation(agentsPath, (original) => `${original}\n1. **Confirm destructive operations.** Restated.\n`, () => {
const failures = collectFailures(crossDocContracts.assertOperatingRulesSingleSource);
assert.ok(
failures.some((f) => f.includes('AGENTS.md') && f.includes('restates an operating rule')),
`expected a restated-rule failure, got: ${failures.join('; ')}`,
);
});
});
test('assertOperatingRulesSingleSource catches AGENTS.md losing the pointer', () => {
const agentsPath = path.join(PLUGIN_ROOT, 'AGENTS.md');
withFileMutation(agentsPath, (original) => original.replaceAll('references/concepts/operating-rules.md', 'nowhere.md'), () => {
const failures = collectFailures(crossDocContracts.assertOperatingRulesSingleSource);
assert.ok(
failures.some((f) => f.includes('AGENTS.md must point at')),
`expected a missing-pointer failure, got: ${failures.join('; ')}`,
);
});
});
test('assertHowAppsWork catches a skill that does not reference operating-rules.md', () => {
const skillPath = path.join(PLUGIN_ROOT, 'skills', 'develop-app', 'SKILL.md');
withFileMutation(skillPath, (original) => original.replace('../../references/concepts/operating-rules.md', 'nowhere.md'), () => {
const failures = collectFailures(references.assertHowAppsWork);
assert.ok(
failures.some((f) => f.includes('develop-app/SKILL.md') && f.includes('operating-rules.md')),
`expected an operating-rules reference failure, got: ${failures.join('; ')}`,
);
});
});
test('assertOperatingRulesSingleSource catches prose copied from the rules file', () => {
const agentsPath = path.join(PLUGIN_ROOT, 'AGENTS.md');
const copiedSentence = 'Watch mode leaks file handles and produces ambiguous failure output in agent sandboxes.';
withFileMutation(agentsPath, (original) => `${original}\n${copiedSentence}\n`, () => {
const failures = collectFailures(crossDocContracts.assertOperatingRulesSingleSource);
assert.ok(
failures.some((f) => f.includes('AGENTS.md') && f.includes('copies a sentence')),
`expected a copied-prose failure, got: ${failures.join('; ')}`,
);
});
});