Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
InvestigateArticleCommand
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 2
156
0.00% covered (danger)
0.00%
0 / 1
 buildOptionParser
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 1
132
1<?php
2declare(strict_types=1);
3
4namespace App\Command;
5
6use Cake\Command\Command;
7use Cake\Console\Arguments;
8use Cake\Console\ConsoleIo;
9use Cake\Console\ConsoleOptionParser;
10use Cake\ORM\TableRegistry;
11use Exception;
12
13/**
14 * InvestigateArticle command to debug translation and SEO issues.
15 *
16 * This command provides comprehensive debugging information for articles that may have
17 * translation or SEO generation issues. It checks multiple data sources to help identify
18 * why AI jobs might have failed or not been triggered.
19 *
20 * ## Usage Examples:
21 *
22 * ```bash
23 * # Basic usage - investigate a specific article
24 * bin/cake investigate_article my-article-slug
25 *
26 * # Using Docker (recommended for development)
27 * docker compose exec willowcms bin/cake investigate_article my-article-slug
28 * ```
29 *
30 * ## What This Command Checks:
31 *
32 * 1. **Article Basic Info**: Verifies the article exists and shows core metadata
33 * 2. **Translation Status**: Checks if translations exist in articles_translations table
34 * 3. **Translation Logs**: Searches system_logs for translation job activities and errors
35 * 4. **SEO Logs**: Searches system_logs for SEO generation job activities and errors
36 * 5. **Queue Jobs**: Checks for pending or failed queue jobs related to the article
37 *
38 * ## Common Issues This Helps Diagnose:
39 *
40 * - Articles not appearing in other languages (translation missing)
41 * - Empty SEO fields (meta_title, meta_description, etc.)
42 * - AI jobs that were queued but never completed
43 * - Failed API calls to Anthropic or Google Translate
44 * - Queue worker not running when article was published
45 *
46 * ## Prerequisites:
47 *
48 * - Queue worker should be running: `bin/cake queue worker --verbose`
49 * - AI settings must be enabled in admin area
50 * - Valid API keys for Anthropic and/or Google Translate
51 *
52 * @since 1.0.0
53 */
54class InvestigateArticleCommand extends Command
55{
56    /**
57     * Hook method for defining this command's option parser.
58     *
59     * Configures the command to accept a required 'slug' argument which identifies
60     * the article to investigate. The slug is the URL-friendly identifier used
61     * in article URLs (e.g., 'my-article-title' from '/en/articles/my-article-title').
62     *
63     * @param \Cake\Console\ConsoleOptionParser $parser The parser to be defined
64     * @return \Cake\Console\ConsoleOptionParser The built parser.
65     */
66    public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
67    {
68        $parser
69            ->setDescription('Investigate article translation and SEO generation issues')
70            ->addArgument('slug', [
71                'help' => 'The slug of the article to investigate',
72                'required' => true,
73            ]);
74
75        return $parser;
76    }
77
78    /**
79     * Execute the investigation command.
80     *
81     * Performs a comprehensive analysis of an article's AI processing status by:
82     * 1. Locating the article by slug
83     * 2. Checking for existing translations in all configured locales
84     * 3. Reviewing system logs for translation job activity and errors
85     * 4. Reviewing system logs for SEO generation job activity and errors
86     * 5. Checking queue_jobs table for pending/failed jobs
87     *
88     * ## Output Sections:
89     * - **ARTICLE FOUND**: Basic article metadata and verification
90     * - **EXISTING TRANSLATIONS**: Shows translations in articles_translations table
91     * - **SYSTEM LOGS (Translation related)**: Recent logs containing translation keywords
92     * - **SYSTEM LOGS (SEO related)**: Recent logs containing SEO generation keywords
93     * - **PENDING QUEUE JOBS**: Active/failed jobs in the queue system
94     *
95     * ## Return Codes:
96     * - `Command::CODE_SUCCESS` (0): Investigation completed successfully
97     * - `Command::CODE_ERROR` (1): Article not found or exception occurred
98     *
99     * @param \Cake\Console\Arguments $args The command arguments containing the article slug
100     * @param \Cake\Console\ConsoleIo $io Console I/O for output formatting and display
101     * @return int The exit code indicating success or failure
102     */
103    public function execute(Arguments $args, ConsoleIo $io): ?int
104    {
105        $slug = $args->getArgument('slug');
106
107        $io->out("Investigating article with slug: $slug");
108        $io->hr();
109
110        try {
111            // 1. Find the article by slug
112            $articles = TableRegistry::getTableLocator()->get('Articles');
113            $article = $articles->find()->where(['slug' => $slug])->first();
114
115            if (!$article) {
116                $io->error("Article with slug '$slug' not found");
117
118                return static::CODE_ERROR;
119            }
120
121            $io->out('=== ARTICLE FOUND ===');
122            $io->out('ID: ' . $article->id);
123            $io->out('Title: ' . $article->title);
124            $io->out('Slug: ' . $article->slug);
125            $io->out('Locale: ' . $article->locale);
126            $io->out('Created: ' . $article->created);
127            $io->out('Modified: ' . $article->modified);
128            $io->out('');
129
130            // 2. Check for translations in articles_translations table
131            // Note: Uses TranslateBehavior structure where translations are stored separately
132            $translations = TableRegistry::getTableLocator()->get('ArticlesTranslations');
133            $existingTranslations = $translations->find()
134                ->where(['id' => $article->id])
135                ->toArray();
136
137            $io->out('=== EXISTING TRANSLATIONS ===');
138            if (empty($existingTranslations)) {
139                $io->out('No translations found');
140            } else {
141                foreach ($existingTranslations as $translation) {
142                    $io->out('Locale: ' . $translation->locale);
143                    $io->out('Field: ' . $translation->field);
144                    $io->out('Content: ' . substr($translation->content, 0, 100) . '...');
145                    $io->out('---');
146                }
147            }
148            $io->out('');
149
150            // 3. Search system logs for translation-related activities and errors
151            // This includes job queuing, processing, completion, and failure logs
152            $systemLogs = TableRegistry::getTableLocator()->get('SystemLogs');
153            $translationErrors = $systemLogs->find()
154                ->where(function ($exp) use ($article) {
155                    return $exp->or([
156                        $exp->like('message', '%TranslateArticleJob%'),
157                        $exp->like('message', '%translation%'),
158                        $exp->like('message', '%' . $article->id . '%'),
159                    ]);
160                })
161                ->orderByDesc('created')
162                ->limit(10)
163                ->toArray();
164
165            $io->out('=== SYSTEM LOGS (Translation related) ===');
166            if (empty($translationErrors)) {
167                $io->out('No translation-related log entries found');
168            } else {
169                foreach ($translationErrors as $log) {
170                    $io->out('Time: ' . $log->created);
171                    $io->out('Level: ' . $log->level);
172                    $io->out('Message: ' . $log->message);
173                    $io->out('---');
174                }
175            }
176            $io->out('');
177
178            // 4. Search system logs for SEO generation activities and errors
179            // Includes ArticleSeoUpdateJob processing and AI-powered SEO content generation
180            $seoErrors = $systemLogs->find()
181                ->where(function ($exp) {
182                    return $exp->or([
183                        $exp->like('message', '%ArticleSeoUpdateJob%'),
184                        $exp->like('message', '%SEO%'),
185                        $exp->like('message', '%seo%'),
186                    ]);
187                })
188                ->orderByDesc('created')
189                ->limit(10)
190                ->toArray();
191
192            $io->out('=== SYSTEM LOGS (SEO related) ===');
193            if (empty($seoErrors)) {
194                $io->out('No SEO-related log entries found');
195            } else {
196                foreach ($seoErrors as $log) {
197                    $io->out('Time: ' . $log->created);
198                    $io->out('Level: ' . $log->level);
199                    $io->out('Message: ' . $log->message);
200                    $io->out('---');
201                }
202            }
203            $io->out('');
204
205            // 5. Check queue_jobs table for pending, processing, or failed jobs
206            // This helps identify if jobs are stuck in the queue or failed to process
207            $connection = $articles->getConnection();
208            $queueJobs = $connection->execute(
209                'SELECT * FROM queue_jobs WHERE payload LIKE ? OR payload LIKE ? ORDER BY created DESC LIMIT 10',
210                ['%' . $article->id . '%', '%TranslateArticleJob%'],
211            )->fetchAll();
212
213            $io->out('=== PENDING QUEUE JOBS ===');
214            if (empty($queueJobs)) {
215                $io->out('No pending queue jobs found for this article');
216            } else {
217                foreach ($queueJobs as $job) {
218                    $io->out('ID: ' . $job['id']);
219                    $io->out('Status: ' . $job['status']);
220                    $io->out('Queue: ' . $job['queue']);
221                    $io->out('Job Type: ' . $job['job_type']);
222                    $io->out('Created: ' . $job['created']);
223                    $io->out('Payload excerpt: ' . substr($job['payload'], 0, 200) . '...');
224                    $io->out('---');
225                }
226            }
227
228            return static::CODE_SUCCESS;
229        } catch (Exception $e) {
230            $io->error('Error: ' . $e->getMessage());
231            $io->error('Trace: ' . $e->getTraceAsString());
232
233            return static::CODE_ERROR;
234        }
235    }
236}