Raiju/tests/copilot-api-smoke.js
2026-10-25 06:05:42 +03:00

132 lines
9.5 KiB
JavaScript

const { apiSource, dashboardSource } = require('./source-helpers');
const { readFileSync } = require('fs');
const { dirname, resolve } = require('path');
const root = dirname(__dirname);
const source = apiSource();
const helper = readFileSync(resolve(root, 'includes/ai/ai-helper.php'), 'utf8');
const context = readFileSync(resolve(root, 'includes/ai/copilot-context.php'), 'utf8');
function blockFor(action) {
const needle = "case '" + action + "':";
const start = source.indexOf(needle);
if (start === -1) {
throw new Error('Missing API action: ' + action);
}
const rest = source.slice(start + needle.length);
const next = rest.search(/\n\s*case '/);
return next === -1 ? rest : rest.slice(0, next);
}
function assertContains(block, needle, message) {
if (!block.includes(needle)) {
throw new Error(message);
}
}
const actions = [
'ai-copilot-context',
'ai-copilot-chat-stream',
'ai-copilot-translate',
'ai-content-audit',
'ai-content-audit-suggest',
'ai-content-audit-apply',
'ai-copilot-history-list',
'ai-copilot-history-load',
'ai-copilot-history-save',
'ai-copilot-history-delete',
'ai-copilot-chat',
'ai-copilot-suggest',
'ai-copilot-format-html',
'ai-copilot-visibility',
'ai-copilot-apply',
'ai-copilot-apply-visibility',
'ai-copilot-undo',
'ai-copilot-draft-content',
'ai-copilot-create-content',
'ai-copilot-publish-content',
'ai-copilot-generate-image'
];
const blocks = Object.fromEntries(actions.map(action => [action, blockFor(action)]));
const keepaliveBlock = blockFor('keepalive');
for (const [action, block] of Object.entries(blocks)) {
assertContains(block, 'dashboardAiModuleEnabled()', action + ' should respect the global AI module gate.');
assertContains(block, 'validateCsrfToken()', action + ' should require CSRF validation.');
}
assertContains(source, 'return raijuSessionValidate();', 'Authenticated API requests should use shared session validation and activity refresh.');
assertContains(keepaliveBlock, 'validateCsrfToken()', 'Keepalive should require CSRF validation.');
assertContains(keepaliveBlock, "jsonResponse(true, ['time' => time()]", 'Keepalive should return a successful timestamp response.');
for (const action of ['ai-copilot-history-list', 'ai-copilot-history-load', 'ai-copilot-history-save', 'ai-copilot-history-delete', 'ai-copilot-chat', 'ai-copilot-suggest', 'ai-copilot-format-html', 'ai-copilot-visibility', 'ai-copilot-apply', 'ai-copilot-apply-visibility', 'ai-copilot-undo', 'ai-copilot-draft-content', 'ai-copilot-create-content', 'ai-copilot-publish-content', 'ai-copilot-generate-image']) {
assertContains(blocks[action], 'raijuCopilotAssertBurstLimit(', action + ' should enforce a Copilot burst limit.');
}
for (const action of ['ai-copilot-apply', 'ai-copilot-apply-visibility', 'ai-copilot-undo', 'ai-copilot-create-content', 'ai-copilot-publish-content']) {
assertContains(blocks[action], 'dashboardCopilotConfirmed()', action + ' should require explicit confirmation.');
}
assertContains(blocks['ai-copilot-chat'], "raijuCopilotCan('chat')", 'Copilot chat should require chat permission.');
assertContains(blocks['ai-copilot-chat-stream'], "raijuCopilotCan('chat')", 'Copilot chat streaming should require chat permission.');
assertContains(blocks['ai-copilot-chat-stream'], 'raijuCopilotAssertBurstLimit(', 'Copilot chat streaming should enforce a burst limit.');
assertContains(blocks['ai-copilot-translate'], "raijuCopilotCan('suggestField')", 'Copilot translate should require suggest permission.');
assertContains(blocks['ai-copilot-translate'], 'raijuCopilotAssertBurstLimit(', 'Copilot translate should enforce a burst limit.');
assertContains(blocks['ai-copilot-translate'], 'raijuCopilotValidateProposals(', 'Copilot translate should validate and sign proposals.');
assertContains(blocks['ai-content-audit'], 'isAdmin()', 'Content audit should be admin-only.');
assertContains(blocks['ai-content-audit-suggest'], 'isAdmin()', 'Content audit suggestions should be admin-only.');
assertContains(blocks['ai-content-audit-apply'], 'isAdmin()', 'Content audit apply should be admin-only.');
assertContains(blocks['ai-content-audit-apply'], 'dashboardCopilotConfirmed()', 'Content audit apply should require explicit confirmation.');
assertContains(blocks['ai-content-audit-apply'], 'dashboardCopilotCreatePageBackup(', 'Content audit apply should create a page backup first.');
assertContains(blocks['ai-copilot-context'], "dashboardCopilotUiLanguage()", 'Copilot context should include the active dashboard language.');
assertContains(blocks['ai-copilot-chat'], "dashboardCopilotUiLanguage()", 'Copilot chat should include the active dashboard language.');
assertContains(helper, "'assistantForceEnglish' => false", 'AI defaults should include the Assistant force-English setting.');
assertContains(helper, "'assistantForceEnglish' => !empty($input['assistantForceEnglish'])", 'AI settings save should persist the Assistant force-English setting.');
assertContains(helper, "'assistantSurfaces' => [", 'AI defaults should include Assistant surface visibility settings.');
assertContains(helper, "'visualEditor' => !array_key_exists('assistantSurfaces', $input) || !empty($input['assistantSurfaces']['visualEditor'])", 'AI settings save should persist the Visual Editor Assistant visibility setting.');
assertContains(helper, "'dashboard' => !array_key_exists('assistantSurfaces', $input) || !empty($input['assistantSurfaces']['dashboard'])", 'AI settings save should persist the Dashboard Assistant visibility setting.');
assertContains(context, 'assistantLanguage', 'Copilot context should expose the resolved Assistant language.');
assertContains(context, 'function raijuCopilotContentPath(string $contentPage): string', 'Copilot context should resolve non-page content targets.');
assertContains(context, "preg_match('/^news:", 'Copilot context should support news post targets.');
assertContains(context, 'Always answer in English', 'Copilot system prompt should force English when configured.');
assertContains(context, 'Answer in the dashboard UI language', 'Copilot system prompt should follow the dashboard language by default.');
for (const action of ['ai-copilot-history-list', 'ai-copilot-history-load', 'ai-copilot-history-save', 'ai-copilot-history-delete']) {
assertContains(blocks[action], "raijuCopilotCan('chat')", action + ' should require chat permission.');
}
assertContains(blocks['ai-copilot-history-save'], 'dashboardCopilotCleanHistoryMessages', 'Copilot history save should sanitize archived messages.');
assertContains(blocks['ai-copilot-history-load'], 'dashboardCopilotLoadOwnedHistory', 'Copilot history load should enforce per-user ownership.');
assertContains(blocks['ai-copilot-history-delete'], 'dashboardCopilotLoadOwnedHistory', 'Copilot history delete should enforce per-user ownership.');
assertContains(blocks['ai-copilot-suggest'], "raijuCopilotCan('suggestField')", 'Copilot suggestions should require suggest permission.');
assertContains(blocks['ai-copilot-format-html'], "raijuCopilotCan('suggestField')", 'Copilot HTML formatting should require suggest permission.');
assertContains(blocks['ai-copilot-visibility'], "raijuCopilotCan('toggleVisibility')", 'Copilot visibility drafts should require visibility permission.');
assertContains(blocks['ai-copilot-apply'], "raijuCopilotCan('applyField')", 'Copilot apply should require apply permission.');
assertContains(blocks['ai-copilot-apply-visibility'], "raijuCopilotCan('toggleVisibility')", 'Copilot visibility apply should require visibility permission.');
assertContains(blocks['ai-copilot-undo'], "raijuCopilotCan('undoField')", 'Copilot undo should require undo permission.');
assertContains(blocks['ai-copilot-generate-image'], "raijuCopilotCan('generateImage')", 'Copilot image generation should require image permission.');
assertContains(blocks['ai-copilot-generate-image'], 'raijuAiEnsureImageReady($settings, $imageService)', 'Copilot image generation should require the imageGeneration feature and a usable image service.');
assertContains(blocks['ai-copilot-generate-image'], "['auto', '1024x1024', '1536x1024', '1024x1536']", 'Copilot image generation should allowlist image sizes.');
assertContains(blocks['ai-copilot-generate-image'], 'max(1, min(4', 'Copilot image generation should clamp variant count.');
assertContains(blocks['ai-copilot-generate-image'], "['auto', 'low', 'medium', 'high']", 'Copilot image generation should allowlist image quality.');
assertContains(blocks['ai-copilot-create-content'], "'page' => 'createPage'", 'Copilot create should map page create permission.');
assertContains(blocks['ai-copilot-create-content'], "'news' => 'createNews'", 'Copilot create should map news create permission.');
assertContains(blocks['ai-copilot-create-content'], "'event' => 'createEvent'", 'Copilot create should map event create permission.');
assertContains(blocks['ai-copilot-publish-content'], "'page' => 'publishPage'", 'Copilot publish should map page publish permission.');
assertContains(blocks['ai-copilot-publish-content'], "'news' => 'publishNews'", 'Copilot publish should map news publish permission.');
assertContains(blocks['ai-copilot-publish-content'], "'event' => 'publishEvent'", 'Copilot publish should map event publish permission.');
console.log(JSON.stringify({
ok: true,
checks: [
'copilotActionsPresent',
'moduleGates',
'csrfGates',
'burstLimits',
'confirmationGates',
'permissionGates',
'visibilityPermissionGates',
'imageFeatureGate',
'imageOptionValidation',
'contentPermissionMaps'
]
}));