diff --git a/lib/CompositionManager.php b/lib/CompositionManager.php index 46e6070..e01600d 100644 --- a/lib/CompositionManager.php +++ b/lib/CompositionManager.php @@ -46,6 +46,11 @@ class CompositionManager { 'sender' => $sender, 'message' => $message, 'attachments' => [], + 'remote' => [ + 'status' => 'dirty', + 'entity' => null, + 'error' => null, + ], ]; // Construct the snapshot mutation that will run while the composition is locked. @@ -111,7 +116,9 @@ class CompositionManager { $saveComposition, ) ?? $snapshot; - return $snapshot; + $response = $snapshot; + unset($response['remote']); + return $response; } public function patch(string $tenantId, string $userId, string $identifier, array $data): array { @@ -140,6 +147,7 @@ class CompositionManager { $composed['revision'] = $data['revision']; $composed['sender'] = $data['sender'] ?? []; $composed['message'] = $data['message'] ?? []; + $composed = $this->markRemoteDirty($composed); $result = [ 'identifier' => $identifier, 'disposition' => 'patched', @@ -175,7 +183,7 @@ class CompositionManager { $composed['sender'] = $sender; $composed['message'] = $message; - return $composed; + return $this->markRemoteDirty($composed); }; // Execute the mutation and atomically persist the snapshot it returns. @@ -367,6 +375,12 @@ class CompositionManager { $failed = $this->attachmentAddFromDocuments($tenantId, $userId, $composition, $documents, $composed); } + $deviceAttachmentsChanged = $uploads !== []; + $documentAttachmentsChanged = count($failed) < count($documents); + if ($deviceAttachmentsChanged || $documentAttachmentsChanged) { + $composed = $this->markRemoteDirty($composed); + } + return $composed; }; @@ -551,6 +565,7 @@ class CompositionManager { } unset($composed['attachments'][$identifier]); + $composed = $this->markRemoteDirty($composed); $result['disposition'] = 'removed'; return $composed; }; @@ -566,6 +581,19 @@ class CompositionManager { return $result; } + private function markRemoteDirty(array $composed): array { + $remote = isset($composed['remote']) && is_array($composed['remote']) + ? $composed['remote'] + : []; + + $remote['status'] = 'dirty'; + $remote['entity'] ??= null; + $remote['error'] = null; + $composed['remote'] = $remote; + + return $composed; + } + /** * @param array $entries * @return array diff --git a/tests/php/Unit/CompositionManagerTest.php b/tests/php/Unit/CompositionManagerTest.php index e802740..88f97b4 100644 --- a/tests/php/Unit/CompositionManagerTest.php +++ b/tests/php/Unit/CompositionManagerTest.php @@ -52,6 +52,61 @@ final class CompositionManagerTest extends TestCase { $this->deleteDir($this->rootDir); } + public function testStageInitializesDirtyRemoteState(): void { + $documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class); + $manager = $this->makeManager($documentsManager); + + $response = $manager->stage( + self::TENANT_ID, + self::USER_ID, + 'draft-new', + 'new', + ['address' => 'sender@example.com'], + ['subject' => 'Draft'], + ); + + $snapshot = $this->compositionStore->compositionFetch(self::TENANT_ID, self::USER_ID, 'draft-new'); + $this->assertArrayNotHasKey('remote', $response); + $this->assertSame([ + 'status' => 'dirty', + 'entity' => null, + 'error' => null, + ], $snapshot['remote']); + } + + public function testPatchMarksExistingRemoteStateDirty(): void { + $compositionId = $this->stageEmptyComposition(); + $setSynced = static function (?array $snapshot): array { + $snapshot['revision'] = 1; + $snapshot['remote'] = [ + 'status' => 'synced', + 'entity' => 'provider:service:drafts:123', + 'error' => 'old error', + ]; + return $snapshot; + }; + $this->compositionStore->compositionSave( + self::TENANT_ID, + self::USER_ID, + $compositionId, + $setSynced, + ); + + $documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class); + $manager = $this->makeManager($documentsManager); + $result = $manager->patch(self::TENANT_ID, self::USER_ID, $compositionId, [ + 'revision' => 2, + 'sender' => ['address' => 'sender@example.com'], + 'message' => ['subject' => 'Changed'], + ]); + + $snapshot = $this->compositionStore->compositionFetch(self::TENANT_ID, self::USER_ID, $compositionId); + $this->assertSame('patched', $result['disposition']); + $this->assertSame('dirty', $snapshot['remote']['status']); + $this->assertSame('provider:service:drafts:123', $snapshot['remote']['entity']); + $this->assertNull($snapshot['remote']['error']); + } + public function testAttachmentAddFromDocumentsHappyPath(): void { $compositionId = $this->stageEmptyComposition(); $documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class); @@ -78,6 +133,11 @@ final class CompositionManagerTest extends TestCase { $this->assertSame('hello.txt', $attachment['name']); $this->assertSame('text/plain', $attachment['type']); $this->assertSame(strlen('hello world'), $attachment['size']); + + $snapshot = $this->compositionStore->compositionFetch(self::TENANT_ID, self::USER_ID, $compositionId); + $this->assertSame('dirty', $snapshot['remote']['status']); + $this->assertNull($snapshot['remote']['entity']); + $this->assertNull($snapshot['remote']['error']); } public function testAttachmentAddFromDocumentsPartialFailure(): void {