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' ] }));