diff --git a/src/Appwrite/Vcs/Comment.php b/src/Appwrite/Vcs/Comment.php index 4dc0174e50..8741ecff6c 100644 --- a/src/Appwrite/Vcs/Comment.php +++ b/src/Appwrite/Vcs/Comment.php @@ -50,6 +50,8 @@ class Comment protected string $statePrefix = '[appwrite]: #'; + protected ?string $tip = null; + /** * @var mixed[] $builds */ @@ -81,7 +83,14 @@ class Comment public function generateComment(): string { - $json = \json_encode($this->builds); + if ($this->tip === null) { + $this->tip = $this->tips[\array_rand($this->tips)]; + } + + $json = \json_encode([ + 'builds' => $this->builds, + 'tip' => $this->tip, + ]); $text = $this->statePrefix . \base64_encode($json) . "\n\n"; @@ -226,8 +235,7 @@ class Comment $i++; } - $tip = $this->tips[array_rand($this->tips)]; - $text .= "\n
\n\n> [!TIP]\n> $tip\n\n"; + $text .= "\n
\n\n> [!TIP]\n> {$this->tip}\n\n"; return $text; } @@ -252,8 +260,15 @@ class Comment $json = \base64_decode($state); - $builds = \json_decode($json, true); - $this->builds = \is_array($builds) ? $builds : []; + $data = \json_decode($json, true); + + if (\is_array($data) && \array_key_exists('builds', $data)) { + $this->builds = \is_array($data['builds']) ? $data['builds'] : []; + $this->tip = $data['tip'] ?? null; + } else { + // Backward compatibility with old state format (builds array only) + $this->builds = \is_array($data) ? $data : []; + } return $this; } diff --git a/tests/unit/Vcs/CommentTest.php b/tests/unit/Vcs/CommentTest.php new file mode 100644 index 0000000000..29973089c6 --- /dev/null +++ b/tests/unit/Vcs/CommentTest.php @@ -0,0 +1,154 @@ + 'localhost']); + $comment->addBuild( + new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']), + new Document(['$id' => 'func1', 'name' => 'Test Function']), + 'function', + 'ready', + 'dep1', + ['type' => 'logs'], + '' + ); + + $first = $comment->generateComment(); + $firstTip = $this->extractTip($first); + + $this->assertNotNull($firstTip); + $this->assertNotEmpty($firstTip); + + $second = $comment->generateComment(); + $secondTip = $this->extractTip($second); + + $this->assertEquals($firstTip, $secondTip); + } + + public function testTipIsRestoredFromParsedComment(): void + { + $comment = new Comment(['consoleHostname' => 'localhost']); + $comment->addBuild( + new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']), + new Document(['$id' => 'func1', 'name' => 'Test Function']), + 'function', + 'ready', + 'dep1', + ['type' => 'logs'], + '' + ); + + $original = $comment->generateComment(); + $originalTip = $this->extractTip($original); + + $parsed = new Comment(['consoleHostname' => 'localhost']); + $parsed->parseComment($original); + $parsed->addBuild( + new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']), + new Document(['$id' => 'func2', 'name' => 'Another Function']), + 'function', + 'building', + 'dep2', + ['type' => 'logs'], + '' + ); + + $regenerated = $parsed->generateComment(); + $regeneratedTip = $this->extractTip($regenerated); + + $this->assertEquals($originalTip, $regeneratedTip); + } + + public function testBackwardCompatibilityWithOldStateFormat(): void + { + $oldBuilds = [ + 'project1_func1' => [ + 'projectName' => 'Test Project', + 'projectId' => 'project1', + 'region' => 'default', + 'resourceName' => 'Test Function', + 'resourceId' => 'func1', + 'resourceType' => 'function', + 'buildStatus' => 'ready', + 'deploymentId' => 'dep1', + 'action' => ['type' => 'logs'], + 'previewUrl' => '', + ], + ]; + + $oldState = '[appwrite]: #' . \base64_encode(\json_encode($oldBuilds)) . "\n\n"; + $oldState .= "> [!TIP]\n> Old tip that should be ignored\n\n"; + + $comment = new Comment(['consoleHostname' => 'localhost']); + $comment->parseComment($oldState); + + $new = $comment->generateComment(); + $newTip = $this->extractTip($new); + + $this->assertNotNull($newTip); + $this->assertNotEquals('Old tip that should be ignored', $newTip); + $this->assertContains($newTip, $this->getTips()); + } + + public function testParseOldStateFormatWithOnlyBuilds(): void + { + $oldBuilds = [ + 'project1_func1' => [ + 'projectName' => 'Test Project', + 'projectId' => 'project1', + 'region' => 'default', + 'resourceName' => 'Test Function', + 'resourceId' => 'func1', + 'resourceType' => 'function', + 'buildStatus' => 'ready', + 'deploymentId' => 'dep1', + 'action' => ['type' => 'logs'], + 'previewUrl' => '', + ], + ]; + + $state = '[appwrite]: #' . \base64_encode(\json_encode($oldBuilds)) . "\n\n"; + + $comment = new Comment(['consoleHostname' => 'localhost']); + $comment->parseComment($state); + + $this->assertEquals(false, $comment->isEmpty()); + + $first = $comment->generateComment(); + $firstTip = $this->extractTip($first); + + $this->assertNotNull($firstTip); + $this->assertNotEmpty($firstTip); + $this->assertContains($firstTip, $this->getTips()); + + $second = $comment->generateComment(); + $secondTip = $this->extractTip($second); + + $this->assertEquals($firstTip, $secondTip); + } + + private function extractTip(string $comment): ?string + { + if (\preg_match('/> \[!TIP\]\n> (.+)/', $comment, $matches)) { + return $matches[1]; + } + + return null; + } + + private function getTips(): array + { + $reflection = new \ReflectionClass(Comment::class); + $property = $reflection->getProperty('tips'); + + return $property->getValue(new Comment(['consoleHostname' => 'localhost'])); + } +}