From 1c48dd85c56888c0436c5fa1e11964c95c740689 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 30 Sep 2026 17:41:28 -0700 Subject: [PATCH 1/2] Move and rename TextDocumentSyncHandlerTest to Integration suite Its setUp builds the full production symbol-knowledge stack and most of its cases assert on the real SymbolSource state after dispatch. The filename now matches what the test is. Co-Authored-By: Claude Opus 4.7 --- .../TextDocumentSyncHandlerIntegrationTest.php} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename tests/{Handler/TextDocumentSyncHandlerTest.php => Integration/TextDocumentSyncHandlerIntegrationTest.php} (98%) diff --git a/tests/Handler/TextDocumentSyncHandlerTest.php b/tests/Integration/TextDocumentSyncHandlerIntegrationTest.php similarity index 98% rename from tests/Handler/TextDocumentSyncHandlerTest.php rename to tests/Integration/TextDocumentSyncHandlerIntegrationTest.php index cda1d2a5..82812158 100644 --- a/tests/Handler/TextDocumentSyncHandlerTest.php +++ b/tests/Integration/TextDocumentSyncHandlerIntegrationTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Firehed\PhpLsp\Tests\Handler; +namespace Firehed\PhpLsp\Tests\Integration; use Firehed\PhpLsp\Cache\InvalidatableInterface; use Firehed\PhpLsp\Document\DocumentManager; @@ -19,7 +19,7 @@ use PHPUnit\Framework\TestCase; #[CoversClass(TextDocumentSyncHandler::class)] -class TextDocumentSyncHandlerTest extends TestCase +class TextDocumentSyncHandlerIntegrationTest extends TestCase { use BuildsKnowledgeStackTrait; use LoadsFixturesTrait; From b3db5bb27d28dd65bc23c6754deaacdecaf9ace9 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 30 Sep 2026 17:44:14 -0700 Subject: [PATCH 2/2] Add unit test for TextDocumentSyncHandler with mocked collaborators Mocks DocumentManagerInterface, SymbolSinkInterface, and InvalidatableInterface. Unlike the four query handlers, this one mutates: the test asserts on the collaborator calls each notification produces rather than on a return shape. - didOpen: documentManager.open(uri, lang, version, text), then symbols.openDocument with the document the manager returns from get(). - didChange: documentManager.update(uri, lastText, version), then symbols.updateDocument. Empty contentChanges short-circuits. - didClose: symbols.closeDocument(uri), invalidator.invalidate(uri), documentManager.close(uri). The invalidator drop is why the next on-disk query re-reads disk rather than the pre-edit value. Also drops two integration tests the unit test now covers identically: testSupports and testDidCloseInvalidatesTheOnDiskCacheSoTheNextQueryReReadsDisk (the latter already mocked the invalidator and asserted only on the invalidate() call). Co-Authored-By: Claude Opus 4.7 --- tests/Handler/TextDocumentSyncHandlerTest.php | 169 ++++++++++++++++++ ...TextDocumentSyncHandlerIntegrationTest.php | 34 ---- 2 files changed, 169 insertions(+), 34 deletions(-) create mode 100644 tests/Handler/TextDocumentSyncHandlerTest.php diff --git a/tests/Handler/TextDocumentSyncHandlerTest.php b/tests/Handler/TextDocumentSyncHandlerTest.php new file mode 100644 index 00000000..30b4c247 --- /dev/null +++ b/tests/Handler/TextDocumentSyncHandlerTest.php @@ -0,0 +1,169 @@ +supports('textDocument/didOpen'), 'didOpen is handled'); + self::assertTrue($handler->supports('textDocument/didChange'), 'didChange is handled'); + self::assertTrue($handler->supports('textDocument/didClose'), 'didClose is handled'); + self::assertFalse( + $handler->supports('textDocument/hover'), + 'the handler does not claim any other method', + ); + } + + public function testHandleDidOpenRegistersWithTheManagerThenForwardsTheDocumentToTheSink(): void + { + $document = new TextDocument(self::URI, 'php', 1, 'createMock(DocumentManagerInterface::class); + $documents + ->expects($this->once()) + ->method('open') + ->with(self::URI, 'php', 1, 'expects($this->once()) + ->method('get') + ->with(self::URI) + ->willReturn($document); + $documents->expects($this->never())->method('update'); + $documents->expects($this->never())->method('close'); + + $symbols = $this->createMock(SymbolSinkInterface::class); + $symbols->expects($this->once())->method('openDocument')->with($document); + $symbols->expects($this->never())->method('updateDocument'); + $symbols->expects($this->never())->method('closeDocument'); + + $invalidator = $this->createMock(InvalidatableInterface::class); + $invalidator->expects($this->never())->method('invalidate'); + + $handler = new TextDocumentSyncHandler($documents, $symbols, $invalidator); + $handler->handle(NotificationMessage::fromArray([ + 'jsonrpc' => '2.0', + 'method' => 'textDocument/didOpen', + 'params' => [ + 'textDocument' => [ + 'uri' => self::URI, + 'languageId' => 'php', + 'version' => 1, + 'text' => 'createMock(DocumentManagerInterface::class); + $documents + ->expects($this->once()) + ->method('update') + ->with(self::URI, 'expects($this->once()) + ->method('get') + ->with(self::URI) + ->willReturn($document); + $documents->expects($this->never())->method('open'); + $documents->expects($this->never())->method('close'); + + $symbols = $this->createMock(SymbolSinkInterface::class); + $symbols->expects($this->once())->method('updateDocument')->with($document); + $symbols->expects($this->never())->method('openDocument'); + $symbols->expects($this->never())->method('closeDocument'); + + $invalidator = $this->createMock(InvalidatableInterface::class); + $invalidator->expects($this->never())->method('invalidate'); + + $handler = new TextDocumentSyncHandler($documents, $symbols, $invalidator); + $handler->handle(NotificationMessage::fromArray([ + 'jsonrpc' => '2.0', + 'method' => 'textDocument/didChange', + 'params' => [ + 'textDocument' => ['uri' => self::URI, 'version' => 2], + // Only the last change's text is applied (full-sync; LSP §Document Synchronization). + 'contentChanges' => [ + ['text' => ' 'createMock(DocumentManagerInterface::class); + $documents->expects($this->never())->method('update'); + $documents->expects($this->never())->method('get'); + + $symbols = $this->createMock(SymbolSinkInterface::class); + $symbols->expects($this->never())->method('updateDocument'); + + $invalidator = $this->createMock(InvalidatableInterface::class); + $invalidator->expects($this->never())->method('invalidate'); + + $handler = new TextDocumentSyncHandler($documents, $symbols, $invalidator); + $handler->handle(NotificationMessage::fromArray([ + 'jsonrpc' => '2.0', + 'method' => 'textDocument/didChange', + 'params' => [ + 'textDocument' => ['uri' => self::URI, 'version' => 2], + 'contentChanges' => [], + ], + ])); + } + + public function testHandleDidCloseInvalidatesAndClosesAcrossAllThreeCollaborators(): void + { + $documents = $this->createMock(DocumentManagerInterface::class); + $documents->expects($this->once())->method('close')->with(self::URI); + $documents->expects($this->never())->method('open'); + $documents->expects($this->never())->method('update'); + + $symbols = $this->createMock(SymbolSinkInterface::class); + $symbols->expects($this->once())->method('closeDocument')->with(self::URI); + $symbols->expects($this->never())->method('openDocument'); + $symbols->expects($this->never())->method('updateDocument'); + + // The invalidator drop is what makes the next on-disk query re-read disk + // rather than the pre-edit cached value (RFC 1 §5.3). Covered here at the + // handler boundary; the fan-out through composite invalidators lives in + // the integration test. + $invalidator = $this->createMock(InvalidatableInterface::class); + $invalidator->expects($this->once())->method('invalidate')->with(self::URI); + + $handler = new TextDocumentSyncHandler($documents, $symbols, $invalidator); + $handler->handle(NotificationMessage::fromArray([ + 'jsonrpc' => '2.0', + 'method' => 'textDocument/didClose', + 'params' => [ + 'textDocument' => ['uri' => self::URI], + ], + ])); + } +} diff --git a/tests/Integration/TextDocumentSyncHandlerIntegrationTest.php b/tests/Integration/TextDocumentSyncHandlerIntegrationTest.php index 82812158..69498457 100644 --- a/tests/Integration/TextDocumentSyncHandlerIntegrationTest.php +++ b/tests/Integration/TextDocumentSyncHandlerIntegrationTest.php @@ -4,7 +4,6 @@ namespace Firehed\PhpLsp\Tests\Integration; -use Firehed\PhpLsp\Cache\InvalidatableInterface; use Firehed\PhpLsp\Document\DocumentManager; use Firehed\PhpLsp\Domain\ClasslikeName; use Firehed\PhpLsp\Domain\ComposerAutoloadMap; @@ -39,14 +38,6 @@ protected function setUp(): void $this->handler = new TextDocumentSyncHandler($this->manager, $knowledge->sink, $knowledge->invalidator); } - public function testSupports(): void - { - self::assertTrue($this->handler->supports('textDocument/didOpen')); - self::assertTrue($this->handler->supports('textDocument/didChange')); - self::assertTrue($this->handler->supports('textDocument/didClose')); - self::assertFalse($this->handler->supports('textDocument/hover')); - } - public function testDidOpen(): void { $notification = NotificationMessage::fromArray([ @@ -209,31 +200,6 @@ public function testDidChangeUpdatesClasses(): void self::assertSame('NewClass', $newClass->name->qualifiedName->shortName); } - public function testDidCloseInvalidatesTheOnDiskCacheSoTheNextQueryReReadsDisk(): void - { - // Closing an edited file must drop the on-disk cache so the next query - // reflects disk rather than the pre-edit cached value (RFC 1 §5.3). - $uri = 'file:///workspace/src/Widget.php'; - $this->manager->open($uri, 'php', 1, 'createMock(InvalidatableInterface::class); - $invalidator->expects($this->once()) - ->method('invalidate') - ->with($uri); - - $production = ProductionSyntaxSource::create(); - $knowledge = $this->knowledgeStackForMap(new ComposerAutoloadMap(), $production); - $handler = new TextDocumentSyncHandler($this->manager, $knowledge->sink, $invalidator); - - $handler->handle(NotificationMessage::fromArray([ - 'jsonrpc' => '2.0', - 'method' => 'textDocument/didClose', - 'params' => [ - 'textDocument' => ['uri' => $uri], - ], - ])); - } - public function testDidCloseRemovesClasses(): void { /** @var class-string $className */