From 9a4e58551e420d90db30c835f0f59ef66877e663 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:05:42 +0000 Subject: [PATCH 01/25] Port Telescope's end-to-end batch watcher test laravel/telescope #1658 updated the test harness for PHP 8.5. Hypervel already uses #[WithConfig], #[DataProvider] and defineEnvironment(), so the remaining difference was BatchWatcherTest, which had diverged into a mocked BatchDispatched event test. The test now runs upstream's batch through the database queue worker: a processed and a failed job, then the batch entry with its job counts. The worker runs each job in its own coroutine, so the test stores the dispatch-time entries first, as the dispatching request would, for the job updates to apply. It keeps Hypervel's batch UUID, connection and allowsFailures assertions, and the job fixtures are fully typed. Upstream reference: laravel/telescope 5.x at bedfc50a35. Validation: BatchWatcherTest and the Telescope suite pass. --- tests/Telescope/Watchers/BatchWatcherTest.php | 117 ++++++++++++++---- 1 file changed, 93 insertions(+), 24 deletions(-) diff --git a/tests/Telescope/Watchers/BatchWatcherTest.php b/tests/Telescope/Watchers/BatchWatcherTest.php index 8a61d72543..ee2ec26efa 100644 --- a/tests/Telescope/Watchers/BatchWatcherTest.php +++ b/tests/Telescope/Watchers/BatchWatcherTest.php @@ -4,48 +4,117 @@ namespace Hypervel\Tests\Telescope\Watchers; -use Hypervel\Bus\Batch; -use Hypervel\Bus\Events\BatchDispatched; -use Hypervel\Contracts\Events\Dispatcher; +use Exception; +use Hypervel\Bus\Batchable; +use Hypervel\Contracts\Bus\QueueingDispatcher; +use Hypervel\Contracts\Queue\ShouldQueue; use Hypervel\Telescope\EntryType; use Hypervel\Telescope\Watchers\BatchWatcher; use Hypervel\Telescope\Watchers\JobWatcher; use Hypervel\Testbench\Attributes\WithConfig; +use Hypervel\Testbench\Attributes\WithMigration; use Hypervel\Tests\Telescope\FeatureTestCase; -use Mockery as m; +#[WithMigration('queue')] +#[WithConfig('logging.default', 'null')] +#[WithConfig('queue.failed.database', 'testing')] #[WithConfig('telescope.watchers', [ JobWatcher::class => true, BatchWatcher::class => true, ])] class BatchWatcherTest extends FeatureTestCase { - public function testJobDispatchRegistersEntries() + public function testJobDispatchRegistersEntries(): void { - $batch = m::mock(Batch::class); - $batch->id = 'batch-id'; - $batch->options = [ - 'queue' => 'on-demand', - 'connection' => 'database', - ]; - $batch->shouldReceive('toArray') - ->once() - ->andReturn(['foo' => 'bar']); - $batch->shouldReceive('allowsFailures') - ->once() - ->andReturn(true); + $batch = $this->app->make(QueueingDispatcher::class)->batch([ + new BananaJob('First Banana'), + new FailedBananaJob('Second Banana'), + ])->onQueue('on-demand')->onConnection('database')->dispatch(); + + // The worker runs each job in its own coroutine, so store the dispatch-time + // entries first, as the dispatching request would, for its updates to apply. + $this->terminateTelescope(); - $this->app->make(Dispatcher::class) - ->dispatch(new BatchDispatched($batch)); + $this->artisan('queue:work', [ + 'connection' => 'database', + '--max-jobs' => 2, + '--queue' => 'on-demand', + ])->run(); $entries = $this->loadTelescopeEntries()->all(); - $this->assertSame(1, count($entries)); + $this->assertSame(3, count($entries)); - $this->assertSame(EntryType::BATCH, $entries[0]->type); - $this->assertSame($batch->id, $entries[0]->uuid); - $this->assertSame('on-demand', $entries[0]->content['queue']); + $this->assertSame(EntryType::JOB, $entries[0]->type); + $this->assertSame('processed', $entries[0]->content['status']); $this->assertSame('database', $entries[0]->content['connection']); - $this->assertTrue($entries[0]->content['allowsFailures']); + $this->assertSame($batch->id, $entries[0]->family_hash); + $this->assertSame(BananaJob::class, $entries[0]->content['name']); + $this->assertSame('on-demand', $entries[0]->content['queue']); + $this->assertSame('First Banana', $entries[0]->content['data']['payload']); + + $this->assertSame(EntryType::JOB, $entries[1]->type); + $this->assertSame('failed', $entries[1]->content['status']); + $this->assertSame('database', $entries[1]->content['connection']); + $this->assertSame($batch->id, $entries[1]->family_hash); + $this->assertSame(FailedBananaJob::class, $entries[1]->content['name']); + $this->assertSame('on-demand', $entries[1]->content['queue']); + $this->assertSame('Second Banana', $entries[1]->content['data']['payload']); + + $this->assertSame(EntryType::BATCH, $entries[2]->type); + $this->assertSame($batch->id, $entries[2]->uuid); + $this->assertSame(2, $entries[2]->content['totalJobs']); + $this->assertSame(1, $entries[2]->content['failedJobs']); + $this->assertSame($batch->id, $entries[2]->content['id']); + $this->assertSame('on-demand', $entries[2]->content['queue']); + $this->assertSame('database', $entries[2]->content['connection']); + $this->assertFalse($entries[2]->content['allowsFailures']); + } +} + +class BananaJob implements ShouldQueue +{ + use Batchable; + + private string $payload; + + /** + * Create a new job instance. + */ + public function __construct(string $payload) + { + $this->payload = $payload; + } + + /** + * Execute the job. + */ + public function handle(): void + { + } +} + +class FailedBananaJob implements ShouldQueue +{ + use Batchable; + + public int $tries = 1; + + private string $payload; + + /** + * Create a new job instance. + */ + public function __construct(string $payload) + { + $this->payload = $payload; + } + + /** + * Execute the job. + */ + public function handle(): void + { + throw new Exception($this->payload); } } From 1beb35718bfe4a49bc53cc73ec56ee8eb2c6104b Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:05:49 +0000 Subject: [PATCH 02/25] Disable npm install scripts for Telescope's assets laravel/telescope #1707 added an .npmrc with ignore-scripts=true, so installing the dashboard's build dependencies cannot run package install scripts. Telescope's .npmrc already set Hypervel's seven-day min-release-age; it now disables install scripts too, as Horizon and Workbench do. The dashboard build succeeds with scripts disabled. Upstream reference: laravel/telescope 5.x at bedfc50a35. --- src/telescope/.npmrc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/telescope/.npmrc b/src/telescope/.npmrc index abe47e0175..63d512ea37 100644 --- a/src/telescope/.npmrc +++ b/src/telescope/.npmrc @@ -3,3 +3,4 @@ # dramatically reduces the chance of pulling a malicious release. # npm measures this value in days. min-release-age=7 +ignore-scripts=true From ee5fb7d22c76b4e28a32aa981b9589a01453f260 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:05:56 +0000 Subject: [PATCH 03/25] Port Telescope's duplicate reservation job test laravel/telescope #1741 clears the exception and failed tag when a job that another worker already failed is later marked processed. Hypervel's JobWatcher already did this, and its own test covered the same case. That test is replaced by upstream's, under its name and position: a second reservation fails the job with MaxAttemptsExceededException while the original worker completes it, and the stored entry must end up processed with no exception or failed tag. Upstream reference: laravel/telescope 5.x at bedfc50a35. Validation: JobWatcherTest and the Telescope suite pass. --- tests/Telescope/Watchers/JobWatcherTest.php | 64 ++++++++++++++------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/tests/Telescope/Watchers/JobWatcherTest.php b/tests/Telescope/Watchers/JobWatcherTest.php index b7af636de2..f5560f8ae4 100644 --- a/tests/Telescope/Watchers/JobWatcherTest.php +++ b/tests/Telescope/Watchers/JobWatcherTest.php @@ -21,9 +21,7 @@ use Hypervel\Queue\QueueManager; use Hypervel\Queue\SerializesModels; use Hypervel\Support\Str; -use Hypervel\Telescope\Contracts\EntriesRepository; use Hypervel\Telescope\EntryType; -use Hypervel\Telescope\IncomingEntry; use Hypervel\Telescope\Telescope; use Hypervel\Telescope\Watchers\JobWatcher; use Hypervel\Testbench\Attributes\WithConfig; @@ -108,6 +106,49 @@ public function testFailedJobsRegisterEntry() $this->assertSame('handle', $entry->content['exception']['trace'][0]['function']); } + public function testProcessedJobClearsStaleFailureStateLeftByADuplicateReservation(): void + { + // A long-running job whose runtime exceeds the queue's retry_after can be + // reserved twice: a second worker fails it with MaxAttemptsExceededException + // (JobFailed) while the original worker eventually completes it (JobProcessed). + // Both events target the same telescope_uuid, so the processed update must not + // leave behind the failure's exception payload or "failed" tag. + $watcher = new JobWatcher; + + $entry = $watcher->recordJob('redis', 'default', [ + 'job' => 'Hypervel\Queue\CallQueuedHandler@call', + 'displayName' => MyDatabaseJob::class, + 'maxTries' => 1, + 'timeout' => 30, + 'data' => ['payload' => 'long-running'], + ]); + + $job = m::mock(Job::class); + $job->shouldReceive('payload')->andReturn(['telescope_uuid' => $entry->uuid]); + + $watcher->recordFailedJob(new QueueJobFailed( + 'redis', + $job, + new Exception(MyDatabaseJob::class . ' has been attempted too many times.') + )); + + $watcher->recordProcessedJob(new QueueJobProcessed('redis', $job)); + + $stored = $this->loadTelescopeEntries()->first(); + + $this->assertSame(EntryType::JOB, $stored->type); + $this->assertSame('processed', $stored->content['status']); + $this->assertNull($stored->content['exception']); + + $hasFailedTag = $this->app->make('db')->connection('testing') + ->table('telescope_entries_tags') + ->where('entry_uuid', $entry->uuid) + ->where('tag', 'failed') + ->exists(); + + $this->assertFalse($hasFailedTag, 'The "failed" tag must be removed once the job is processed.'); + } + public function testItHandlesPushedJobs() { $queueExceptions = []; @@ -272,25 +313,6 @@ public function testInvalidFailedPayloadIsIgnoredWithoutASecondRead(): void $this->assertCount(0, $this->loadTelescopeEntries()); } - public function testProcessedJobClearsPriorFailureState(): void - { - $entry = IncomingEntry::make(['status' => 'pending']); - Telescope::recordJob($entry); - - $job = new TelescopeQueueJob(['telescope_uuid' => $entry->uuid]); - $watcher = $this->app->make(JobWatcher::class); - - $watcher->recordFailedJob(new QueueJobFailed('database', $job, new Exception('failed'))); - $watcher->recordProcessedJob(new QueueJobProcessed('database', $job)); - - $stored = $this->loadTelescopeEntries()->first(); - $result = $this->app->make(EntriesRepository::class)->find($entry->uuid)->jsonSerialize(); - - $this->assertSame('processed', $stored->content['status']); - $this->assertNull($stored->content['exception']); - $this->assertNotContains('failed', $result['tags']); - } - public function testBatchIdFailureRestoresRecordingState(): void { $job = new TelescopeQueueJob([ From 318a3f85f05957d05c950140cea84402fc238846 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:06:05 +0000 Subject: [PATCH 04/25] Update Telescope's axios and moment build dependencies laravel/telescope #1746 bumped axios. Telescope was on axios 1.19.0 and moment 2.30.1, both inside ranges with published security advisories, as upstream's lock still is. They now require ^1.20.0 and ^2.31.0, matching Horizon. The dashboard bundle is rebuilt with them in the following asset commit. The remaining npm audit reports are for the vite and esbuild development servers and nanoid, which the production build does not use in an affected way, and Vue 2, which has no fixed release. Upstream reference: laravel/telescope 5.x at bedfc50a35. --- src/telescope/package-lock.json | 16 ++++++++-------- src/telescope/package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/telescope/package-lock.json b/src/telescope/package-lock.json index 1d19a3f647..3815f8d675 100644 --- a/src/telescope/package-lock.json +++ b/src/telescope/package-lock.json @@ -6,12 +6,12 @@ "": { "devDependencies": { "@vitejs/plugin-vue2": "^2.3.3", - "axios": "^1.18", + "axios": "^1.20.0", "bootstrap": "^4.5.0", "highlight.js": "^11.3.1", "jquery": "^3.5", "lodash": "^4.18.1", - "moment": "^2.30.1", + "moment": "^2.31.0", "moment-timezone": "^0.5.45", "popper.js": "^1.16", "sass": "^1.74.1", @@ -1182,9 +1182,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", - "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "dev": true, "license": "MIT", "dependencies": { @@ -1693,9 +1693,9 @@ } }, "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.31.0.tgz", + "integrity": "sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==", "dev": true, "license": "MIT", "engines": { diff --git a/src/telescope/package.json b/src/telescope/package.json index 7c2f979197..0ba1e0c21c 100644 --- a/src/telescope/package.json +++ b/src/telescope/package.json @@ -13,12 +13,12 @@ }, "devDependencies": { "@vitejs/plugin-vue2": "^2.3.3", - "axios": "^1.18", + "axios": "^1.20.0", "bootstrap": "^4.5.0", "highlight.js": "^11.3.1", "jquery": "^3.5", "lodash": "^4.18.1", - "moment": "^2.30.1", + "moment": "^2.31.0", "moment-timezone": "^0.5.45", "popper.js": "^1.16", "sass": "^1.74.1", From f9fbcfcac8d60ccf28feb85cc36374fcc5845c8e Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:06:12 +0000 Subject: [PATCH 05/25] Consolidate Telescope's CSP nonce tests under upstream's name laravel/telescope #1752 added Telescope::cspNonce(), with #1756 escaping the attribute value. Hypervel already had both, with the nonce held per coroutine, and CspTest covered them. The tests move to upstream's CspNonceTest: upstream's tests render the dashboard page, and keep Hypervel's assertions on each style and script tag, the escaped attribute value and the coroutine isolation of the nonce. Upstream's nonce documentation is added with the next documentation change to the Telescope page. Upstream reference: laravel/telescope 5.x at bedfc50a35. Validation: CspNonceTest and the Telescope suite pass. --- tests/Telescope/Http/CspNonceTest.php | 86 +++++++++++++++++++++++++++ tests/Telescope/Http/CspTest.php | 69 --------------------- 2 files changed, 86 insertions(+), 69 deletions(-) create mode 100644 tests/Telescope/Http/CspNonceTest.php delete mode 100644 tests/Telescope/Http/CspTest.php diff --git a/tests/Telescope/Http/CspNonceTest.php b/tests/Telescope/Http/CspNonceTest.php new file mode 100644 index 0000000000..14cf535668 --- /dev/null +++ b/tests/Telescope/Http/CspNonceTest.php @@ -0,0 +1,86 @@ +withoutMiddleware([Authorize::class]); + } + + public function testCspNonceIsNotRenderedInStyleAndScriptTagsIfNotSet(): void + { + $response = $this->get('/telescope'); + + $response->assertOk() + ->assertSeeHtml('