'editor', 'admin_username' => 'smoke']; require_once $root . '/includes/content-loader.php'; require_once $root . '/includes/ai/copilot-context.php'; require_once $root . '/includes/ai/ai-helper.php'; $createdFiles = []; $createdDirs = [RAIJU_AI_AUDIT_DIR]; function copilotSmokeAssert(bool $condition, string $message): void { if (!$condition) { throw new RuntimeException($message); } } function copilotSmokeRejects(callable $callback, string $message): void { $rejected = false; try { $callback(); } catch (Throwable $e) { $rejected = true; } copilotSmokeAssert($rejected, $message); } function copilotSmokeWriteFixture(string $path, string $content): void { global $createdFiles; $dir = dirname($path); if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) { throw new RuntimeException('Could not create fixture directory: ' . $dir); } if (file_put_contents($path, $content) === false) { throw new RuntimeException('Could not write fixture: ' . $path); } $createdFiles[] = $path; } function copilotSmokeRemoveDir(string $dir): void { if (!is_dir($dir)) { return; } $items = scandir($dir); if (!is_array($items)) { return; } foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $path = $dir . '/' . $item; if (is_dir($path)) { copilotSmokeRemoveDir($path); } elseif (is_file($path)) { unlink($path); } } rmdir($dir); } try { $page = 'en_zz-copilot-smoke'; $pageFile = CONTENT_PATH . $page . '.json'; copilotSmokeWriteFixture($pageFile, json_encode([ 'title' => 'Old title', 'description' => 'Old description', 'description__hidden' => true, 'body' => '
Old body
', 'cta' => ['text' => 'Old CTA', 'href' => '/old'], 'seo' => ['robots' => 'index, follow'], 'sections' => [ [ 'id' => 'section_intro', 'type' => 'text', 'title' => 'Section heading', 'content' => 'Section body
' ], [ 'id' => 'section_hero_image', 'type' => 'image', 'src' => '/assets/images/generated/zz-copilot-raster-test.png', 'alt' => 'Section image', 'hidden' => true ], [ 'id' => 'section_steps', 'type' => 'list', 'title' => 'Steps', 'items' => [ ['text' => 'First step'], ['text' => 'Second step'], ], 'content' => '' ] ], 'published' => true, 'hero' => ['src' => '/assets/images/generated/zz-copilot-raster-test.png', 'alt' => 'Old alt'], 'visibility' => ['passwordHash' => 'must-not-leak', 'token' => 'must-not-leak'], ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); $imageDir = $root . '/assets/images/generated'; $validPng = $imageDir . '/zz-copilot-raster-test.png'; $validVariantPng = $imageDir . '/zz-copilot-raster-variant-test.png'; $fakePng = $imageDir . '/zz-copilot-fake-test.png'; $svg = $imageDir . '/zz-copilot-svg-test.svg'; copilotSmokeWriteFixture($validPng, (string)base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=')); copilotSmokeWriteFixture($validVariantPng, (string)base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=')); copilotSmokeWriteFixture($fakePng, 'not a real png'); copilotSmokeWriteFixture($svg, ''); $context = raijuCopilotBuildContext($page, [ 'enabled' => true, 'hasApiKey' => true, 'apiKey' => 'sk-should-not-leak', 'provider' => 'openai-compatible', 'baseUrl' => 'https://provider.invalid/v1', 'providerCredentials' => [ 'openai-compatible' => [ 'apiKey' => 'sk-provider-secret', 'baseUrl' => 'https://provider.invalid/v1', 'organization' => 'org-secret' ] ], 'features' => ['backendAssistant' => true, 'seoTextGeneration' => true, 'imageGeneration' => true], 'chatModel' => 'test-chat', 'imageModel' => 'test-image' ]); $contextJson = json_encode($context, JSON_UNESCAPED_SLASHES); copilotSmokeAssert(is_string($contextJson), 'Context JSON encoding failed.'); foreach (['sk-should-not-leak', 'sk-provider-secret', 'providerCredentials', 'apiKey', 'baseUrl', 'org-secret'] as $secretNeedle) { copilotSmokeAssert(strpos($contextJson, $secretNeedle) === false, 'Safe Copilot context leaked AI setting: ' . $secretNeedle); } copilotSmokeAssert(!empty($context['page']['exists']), 'Context did not find fixture page.'); $sectionTextField = array_values(array_filter($context['page']['fields'] ?? [], fn($field) => ($field['path'] ?? '') === 'sections.0.content'))[0] ?? null; copilotSmokeAssert(is_array($sectionTextField), 'Section text field missing from Assistant manifest.'); copilotSmokeAssert(strpos((string)($sectionTextField['label'] ?? ''), 'Sec. 1 text: Section heading - content') !== false, 'Section text field labels should include short section number, type, title, and concrete field name.'); $sectionImageField = array_values(array_filter($context['page']['fields'] ?? [], fn($field) => ($field['path'] ?? '') === 'sections.1.src'))[0] ?? null; copilotSmokeAssert(is_array($sectionImageField), 'Section image field missing from Assistant manifest.'); copilotSmokeAssert(strpos((string)($sectionImageField['label'] ?? ''), 'Sec. 2 image: Section image - src') !== false, 'Section image field labels should include short section number, type, preview text, and concrete field name.'); $sectionListItemField = array_values(array_filter($context['page']['fields'] ?? [], fn($field) => ($field['path'] ?? '') === 'sections.2.items.1.text'))[0] ?? null; copilotSmokeAssert(is_array($sectionListItemField), 'Section list item field missing from Assistant manifest.'); copilotSmokeAssert(strpos((string)($sectionListItemField['label'] ?? ''), 'Sec. 3 list: Steps - item 2 text') !== false, 'Section list item labels should include the item number.'); copilotSmokeAssert(!empty($context['knowledgeBase']) && is_array($context['knowledgeBase']), 'Copilot knowledge base missing from safe context.'); $knowledgeIds = array_map(fn($item) => $item['id'] ?? '', $context['knowledgeBase']); foreach (['overview', 'inline-editing', 'standard-pages', 'content-types', 'ai', 'security'] as $knowledgeId) { copilotSmokeAssert(in_array($knowledgeId, $knowledgeIds, true), 'Copilot knowledge base missing topic: ' . $knowledgeId); } $systemPrompt = raijuCopilotSystemPrompt($context); copilotSmokeAssert(strpos($systemPrompt, 'knowledgeBase') !== false, 'Copilot system prompt does not include knowledge base guidance.'); copilotSmokeAssert(strpos($systemPrompt, 'editableText()') !== false, 'Copilot system prompt lacks raiju inline editing knowledge.'); copilotSmokeAssert(strpos($systemPrompt, 'publishing AI-created drafts') !== false, 'Copilot system prompt does not mention implemented publish support.'); copilotSmokeAssert(in_array('publish-created-content', $context['copilot']['supportedNow'] ?? [], true), 'Copilot runtime context does not expose implemented publish support.'); copilotSmokeAssert(in_array('image-alt-suggestions', $context['copilot']['supportedNow'] ?? [], true), 'Copilot runtime context does not expose implemented image alt suggestions.'); copilotSmokeAssert(!in_array('publish-flows', $context['copilot']['plannedActions'] ?? [], true), 'Implemented publish flows are still advertised as planned.'); copilotSmokeAssert(!in_array('image-alt-suggestions', $context['copilot']['plannedActions'] ?? [], true), 'Implemented image alt suggestions are still advertised as planned.'); $paths = array_map(fn($field) => $field['path'] ?? '', $context['page']['fields'] ?? []); copilotSmokeAssert(in_array('title', $paths, true), 'Editable title field missing from manifest.'); copilotSmokeAssert(in_array('cta.text', $paths, true), 'Editable link text field missing from manifest.'); copilotSmokeAssert(in_array('cta.href', $paths, true), 'Editable link href field missing from manifest.'); copilotSmokeAssert(in_array('sections.0.content', $paths, true), 'Editable section content field missing from manifest.'); copilotSmokeAssert(in_array('sections.1.src', $paths, true), 'Editable section image src field missing from manifest.'); foreach (['page', 'lang', 'lastModified', 'published', 'description__hidden', 'sections.0.type', 'sections.1.type', 'sections.1.hidden'] as $structuralPath) { copilotSmokeAssert(!in_array($structuralPath, $paths, true), 'Structural field leaked into editable manifest: ' . $structuralPath); } copilotSmokeAssert(!in_array('visibility.passwordHash', $paths, true), 'Sensitive password hash leaked into manifest.'); copilotSmokeAssert(!in_array('visibility.token', $paths, true), 'Sensitive token leaked into manifest.'); copilotSmokeAssert(raijuCopilotCan('applyField'), 'Editor should be able to apply ordinary field changes.'); copilotSmokeAssert(raijuCopilotCan('toggleVisibility'), 'Editor should be able to apply explicit visibility changes.'); copilotSmokeAssert(!raijuCopilotCan('generateImage'), 'Editor should not be able to generate images.'); $_SESSION['admin_role'] = 'admin'; copilotSmokeAssert(raijuCopilotCan('generateImage'), 'Admin should be able to generate images.'); $_SESSION['admin_role'] = 'editor'; unset($_SESSION['raiju_copilot_rate']); raijuCopilotAssertBurstLimit('smoke-burst', 2, 60); raijuCopilotAssertBurstLimit('smoke-burst', 2, 60); copilotSmokeRejects(fn() => raijuCopilotAssertBurstLimit('smoke-burst', 2, 60), 'Copilot burst limit did not reject excessive requests.'); unset($_SESSION['raiju_copilot_rate']); $pageData = raijuCopilotLoadPageData($page); $proposals = raijuCopilotValidateProposals([ 'proposals' => [ ['path' => 'title', 'value' => 'New title', 'reason' => 'Clearer heading'], ['path' => 'body', 'value' => ''], ['path' => 'cta.href', 'value' => 'javascript:alert(1)'], ['path' => 'seo.robots', 'value' => 'noindex, follow'], ] ], $context, $pageData); copilotSmokeAssert(count($proposals) === 3, 'Expected valid title, body, and select proposals only.'); $duplicateFieldProposals = raijuCopilotValidateProposals([ 'proposals' => [ ['path' => 'body', 'value' => 'First body option', 'reason' => 'First option'], ['path' => 'body', 'value' => 'Second body option', 'reason' => 'Second option'], ['path' => 'body', 'value' => 'Final body option', 'reason' => 'Final option'], ] ], $context, $pageData); copilotSmokeAssert(count($duplicateFieldProposals) === 1, 'Duplicate proposals for one field should be collapsed to one option.'); copilotSmokeAssert(($duplicateFieldProposals[0]['value'] ?? '') === 'Final body option', 'Duplicate proposal collapse should keep the final AI option.'); // Safety check of what the model wrote: harmful text is dropped before anything is signed, odd text carries a note. $safetyReport = null; $harmful = raijuCopilotValidateProposals([ 'proposals' => [ ['path' => 'title', 'value' => 'Verdoppeln Sie Ihre Bitcoin in 24 Stunden!'], ['path' => 'body', 'value' => 'Please enter your password to verify your account immediately.
'], ] ], $context, $pageData, 3, $safetyReport); copilotSmokeAssert($harmful === [] && ($safetyReport['blocked'] ?? 0) === 2, 'Scam and phishing suggestions must be dropped: ' . json_encode([$harmful, $safetyReport])); $noted = raijuCopilotValidateProposals([ 'proposals' => [['path' => 'body', 'value' => 'See our offer
']] ], $context, $pageData, 3, $safetyReport); copilotSmokeAssert(count($noted) === 1 && ($safetyReport['blocked'] ?? 1) === 0 && trim((string)($noted[0]['safetyNote'] ?? '')) !== '' && !empty($noted[0]['proposalSignature']), 'A short link gets a safety note and stays signed.'); $plain = raijuCopilotValidateProposals([ 'proposals' => [['path' => 'title', 'value' => 'Welcome to our cafe']] ], $context, $pageData, 3, $safetyReport); copilotSmokeAssert(count($plain) === 1 && !array_key_exists('safetyNote', $plain[0]), 'An ordinary suggestion carries no safety note.'); $htmlProposal = null; foreach ($proposals as $proposal) { if (($proposal['path'] ?? '') === 'body') { $htmlProposal = $proposal; break; } } copilotSmokeAssert(is_array($htmlProposal), 'HTML proposal missing.'); $htmlValue = (string)($htmlProposal['value'] ?? ''); foreach (['style=', 'onclick=', '