Skip to content

Harden Scout search, queue, and engine lifecycles - #464

Merged
binaryfire merged 17 commits into
0.4from
audit/scout-current-parity-and-lifecycles
Aug 2, 2026
Merged

Harden Scout search, queue, and engine lifecycles#464
binaryfire merged 17 commits into
0.4from
audit/scout-current-parity-and-lifecycles

Conversation

@binaryfire

@binaryfire binaryfire commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR brings Scout up to the current Laravel-facing API while preserving Hypervel's coroutine-aware execution, shared search clients, bounded command concurrency, and existing engine extensions.

The main changes are:

  • structured comparison clauses across every engine, including exact escaping, backed enums, soft deletes, repeated fields, and literal string-zero values;
  • custom Scout builders, substitutable paginators, HasManyThrough indexing, nesting-safe search suppression, and listener-gated bulk events;
  • configurable and unique queue jobs, query-free queued removals, one after-commit setting, ordered range imports, and first-failure propagation from concurrent imports;
  • precise Algolia, Meilisearch, and Typesense target and error handling, including bounded Typesense collection-wrapper retention and fewer unnecessary remote calls;
  • shared Database fixes for invalid chunk sizes, named CollectedBy attributes, and descending traversal of string identifiers;
  • complete split-package dependencies and updated Scout documentation.

For more details, see: docs/plans/2026-08-02-1006-scout-current-parity-queue-and-search-lifecycles.md.

Details

Query and model behavior

Scout constraints now retain the field, operator, and value as ordered entries. This preserves multiple comparisons against the same field and gives every engine the information it needs to render the query correctly.

Searchable models can select a custom Builder through the container. Generic pagination paths honor paginator substitutions without caching mutable instances. Raw Database pagination now uses the engine's declared model-pagination capabilities instead of treating a model paginator as a transport response.

The model lifecycle changes also prevent duplicate force-delete removal, restore nested search-suppression state correctly, and add the current HasManyThrough searchable and unsearchable macros.

Queue and command behavior

Search jobs read configured attempts, backoff, and exception limits. The opt-in unique jobs derive stable identities from the model class and sorted Scout keys. Queued removals reconstruct only the serialized search identity, so deletion remains correct after the database row is gone and does not perform a restoration query.

Queue imports support ascending and descending integer or string keys. Integer input is validated exactly, range arithmetic stays within platform integer bounds, and string ordering is left to the database collation. Concurrent imports retain and rethrow the first child failure after draining active work, then clear their coroutine-local runner state.

Engine behavior

Algolia now builds one escaped filters expression for comparisons, inclusion, exclusion, booleans, enums, and empty sets. Meilisearch applies the same value normalization and only treats a real not-found response as an absent index.

Typesense resolves read and write targets explicitly, ignores stale schema names in favor of the selected model index, transforms records before service access, and creates a collection only after a missing-collection response. Delete and flush no longer create first. Engine-owned collection wrappers are removed from the SDK registry after each operation, retaining the shared client and transport while preventing collection names from accumulating for the worker lifetime.

Database ownership

The shared chunk owners now reject zero and negative sizes instead of returning false success or issuing an unbounded query. Descending cursor pagination accepts string identifiers, and CollectedBy attributes are instantiated so named arguments and constructor validation work as intended.

Validation

  • composer fix
  • Scout unit, feature, and configured search-engine integration coverage
  • focused Database query, Eloquent, and UUID traversal coverage
  • split-package manifest validation
  • git diff --check

Summary by CodeRabbit

  • New Features

    • Added comparison, enum, escaped-value, and null-aware search filters across supported engines.
    • Added descending queued imports, bounded concurrent processing, unique jobs, configurable retry settings, and custom Scout builders.
    • Added searchable operations for through relationships and improved custom-key queued removals.
  • Bug Fixes

    • Improved pagination cursor compatibility and validation for invalid chunk sizes.
    • Corrected soft-delete, collection restoration, and search index lifecycle behavior.
  • Documentation

    • Expanded Scout configuration, queue, engine, and compatibility guidance.

Reject zero and negative chunk sizes at the shared Database query owners before they can report false success or issue an unbounded query.

Widen descending cursor pagination to accept string identifiers, matching forward traversal, and cover both query generation and real UUID-backed multi-page traversal.
Instantiate CollectedBy attributes instead of reading positional reflection arguments.

This preserves constructor validation, supports named arguments and inherited overrides, and keeps the existing worker-lifetime resolution cache truthful.
Store Scout constraints as ordered field, operator, and value entries so repeated fields, comparisons, and soft-delete clauses survive every engine boundary.

Resolve generic paginators through fresh parameterized container construction, honor database model-pagination capabilities in raw paths, and use the model default when per-page input is zero.
Consume structured comparisons consistently, preserve literal string-zero queries, and select soft-delete constraints by field instead of relying on the old associative shape.

Instantiate search attributes so named and positional arguments behave identically, while retaining per-engine metadata caches and adding focused feature coverage.
Port configurable job attempts, backoff, exception limits, timeout behavior, and opt-in unique jobs while retaining Laravel-compatible subclass extension points.

Give after-commit one top-level configuration owner and restore exact serialized Scout identities for queued removals without querying models that may already be deleted.
Make search suppression nesting-safe and coroutine-local, keep force deletion under one removal owner, add through-relation macros, and expose the observer methods every searchable implementation must provide.

Resolve custom builders freshly, gate optional import events by listener presence, propagate the first concurrent child failure after draining active work, clear operation state in finally blocks, and use the unified after-commit setting.
Add validated ascending and descending queue imports for integer and string Scout keys while preserving configured queue and connection selection.

Reject malformed or overflowing integer bounds, let the database own string collation, calculate platform-extreme ranges without numeric overflow, and return truthful command statuses.
Treat only missing or empty keys as absent so a literal zero reaches the engine, and confine unsupported-create handling to the create operation.

Continue settings synchronization after unsupported creation, look up settings by logical key before physical index fallback, and allow operational failures to propagate unchanged.
Replace legacy numeric filter arrays with one escaped filters expression that preserves comparisons, booleans, grouped inclusion, exclusion, backed enums, and empty-set behavior.

Preserve literal string-zero index overrides, retain request-scoped identify behavior, and delete queued models by their serialized Scout key.
Escape comparison and set filters consistently, preserve backed-enum values, and delete queued models by the exact serialized Scout key.

Treat only HTTP 404 as an absent index, accept only the documented already-exists creation race, and propagate authentication, validation, transport, and other operational failures.
Resolve search and write targets explicitly, replace stale schema names with the selected operation target, and classify only documented missing and create-race errors as recoverable.

Transform before I/O, import optimistically with one missing-collection recovery, avoid create-before-delete and create-before-flush calls, reject unsupported operators, and detach engine-owned collection wrappers to prevent worker-lifetime registry growth.
Add explicit void return types to the remaining Scout test methods whose results are not consumed.

This completes the package typing pass without changing data providers, helper return contracts, or runtime behavior.
Declare the Guzzle and PSR HTTP message packages that Scout resolves directly instead of relying on root or optional engine dependencies to provide them transitively.

Add a split-package metadata regression that verifies every direct runtime dependency and pins the intended constraints.
Record Scout provenance and the actionable Hypervel differences around queue configuration, coroutine-deferred indexing, bounded imports, search clients, and prefix-safe index deletion.

Document comparisons, custom builders, queue job options, unique jobs, ordered imports, and Typesense collection naming in the existing Laravel-style user guide sections.
Capture the approved Scout design, upstream research, implementation boundaries, performance analysis, and validation plan in a package-specific record.

Mark Scout complete in the framework routing and checklist records, record every durable finding, and close the Database cross-package revalidation entries.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@binaryfire, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1cab7207-005f-4a08-8f92-52b163cac10f

📥 Commits

Reviewing files that changed from the base of the PR and between a4a053a and fb146a6.

📒 Files selected for processing (12)
  • docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md
  • docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md
  • docs/plans/2026-08-02-1006-scout-current-parity-queue-and-search-lifecycles.md
  • src/boost/docs/scout.md
  • src/foundation/src/Testing/Concerns/InteractsWithAlgolia.php
  • src/foundation/src/Testing/Concerns/InteractsWithMeilisearch.php
  • tests/Foundation/Testing/Concerns/ExternalServiceOptInTest.php
  • tests/Integration/Scout/Meilisearch/MeilisearchScoutIntegrationTestCase.php
  • tests/Scout/Models/CustomScoutKeyModel.php
  • tests/Scout/Unit/Jobs/RemoveFromSearchTest.php
  • tests/Scout/Unit/Jobs/RemoveableScoutCollectionTest.php
  • tests/Support/MeilisearchIntegrationTestCase.php
📝 Walkthrough

Walkthrough

Scout adds structured search filters, directional queue imports, configurable and unique jobs, engine lifecycle corrections, model-aware builders, relation macros, and coroutine-safe cleanup. Database pagination and collection contracts also receive validation and type fixes. Unit and integration coverage validates the changes.

Changes

Scout parity and lifecycle

Layer / File(s) Summary
Audit, plan, and documentation
docs/plans/*, src/boost/docs/scout.md, src/scout/README.md, src/scout/composer.json
Audit records and implementation plans were completed. Scout documentation and required HTTP dependencies were updated.
Database contracts
src/database/src/Concerns/BuildsQueries.php, src/database/src/Eloquent/HasCollection.php, src/database/src/Query/Builder.php
Chunk sizes must be positive. Collection attributes use instantiated metadata. ID pagination accepts string cursors.
Builder and queued jobs
src/scout/src/Builder.php, src/scout/src/Console/QueueImportCommand.php, src/scout/src/Jobs/*, src/scout/src/Traits/*, src/scout/config/scout.php
Builders retain operator-aware constraints and resolve paginators through the container. Queue imports support ascending and descending ranges. Jobs use configurable retry settings, timeout failure, restoration, and uniqueness.
Search engines
src/scout/src/Contracts/SearchableInterface.php, src/scout/src/Engines/*
Engines preserve comparison operators, serialize enums, escape filter values, and handle engine-specific collection and index lifecycles.
Import, observer, and scope flow
src/scout/src/Console/ConcurrentImportRunner.php, src/scout/src/Console/IndexCommand.php, src/scout/src/ModelObserver.php, src/scout/src/Searchable.php, src/scout/src/SearchableScope.php
Concurrent imports preserve the first failure and clear coroutine state. Observer, relation macro, event, index command, builder selection, and synchronization behavior were updated.
Validation and fixtures
tests/Database/*, tests/Integration/Scout/*, tests/Scout/*
Tests cover database contracts, filters, queue imports, job behavior, engine lifecycles, relation macros, coroutine isolation, configuration, dependencies, and migrations.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • hypervel/components#431: Both changes modify queued Eloquent model or collection serialization and restoration.
  • hypervel/components#461: Both changes update Scout model identity and custom-key handling in queued search-removal workflows.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main Scout search, queue, and engine lifecycle changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch audit/scout-current-parity-and-lifecycles

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR updates Scout’s search constraints, queue behavior, model lifecycle handling, and external-engine resource management while preserving coroutine-aware execution.

  • Represents comparison constraints as ordered structured clauses across Scout engines.
  • Adds configurable and unique queue jobs, range imports, and bounded concurrent import failure propagation.
  • Refines soft-delete behavior, custom builders, pagination, relationship indexing, and nested search suppression.
  • Hardens Algolia, Meilisearch, and Typesense filtering, target selection, error handling, and collection-wrapper cleanup.
  • Adds related Database query traversal, chunk validation, package metadata, documentation, and regression coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/scout/src/Builder.php Converts constraints to ordered structured clauses and correctly replaces the reserved soft-delete control entry through supported trash-selection APIs.
src/scout/src/Engines/DatabaseEngine.php Applies structured comparison clauses and interprets the reserved soft-delete control predicate separately from ordinary model constraints.
src/scout/src/Engines/CollectionEngine.php Mirrors DatabaseEngine’s structured filtering and soft-delete control behavior for in-memory searches.
src/scout/src/Console/ConcurrentImportRunner.php Adds bounded concurrent import execution, first-failure propagation, draining, and coroutine-local cleanup.
src/scout/src/Console/QueueImportCommand.php Extends queued range imports with validated ascending and descending traversal for integer and string identifiers.
src/scout/src/Engines/TypesenseEngine.php Separates read and write targets, narrows missing-collection handling, and bounds SDK collection-wrapper retention.
src/scout/src/ModelObserver.php Adjusts model lifecycle indexing behavior to avoid duplicate force-delete removal while retaining the intended delete path.
src/database/src/Concerns/BuildsQueries.php Rejects invalid chunk sizes and supports descending cursor traversal for string identifiers.

Reviews (3): Last reviewed commit: "fix(testing): await search service tasks" | Re-trigger Greptile

Comment thread src/scout/src/Engines/DatabaseEngine.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (6)
src/scout/src/Searchable.php (1)

104-124: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the shared listener-gated dispatch helper.

These two blocks repeat the logic of SearchableScope::dispatchEvent() in src/scout/src/SearchableScope.php Lines 75-82. Extract one helper and call it from both places. This keeps the listener gate and event construction in a single location.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/scout/src/Searchable.php` around lines 104 - 124, Extract a shared
listener-gated event dispatch helper matching SearchableScope::dispatchEvent(),
then replace the duplicated ModelsImported and ModelsFlushed listener checks in
the HasManyThrough macros with calls to that helper. Keep event construction and
dispatch inside the helper, and invoke it from both callback blocks.
tests/Scout/Unit/Engines/TypesenseEngineTest.php (2)

208-217: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add an explicit assertion to the mock-only tests.

This test states its expectation only through shouldNotReceive. PHPUnit marks a test risky when it records no assertion, unless the Mockery integration converts expectations into assertions. The neighbouring test at lines 196-206 adds $this->assertTrue(true) for this reason. Apply the same convention here and in the other new mock-only tests: lines 219-239, 241-271, 273-297, 343-368, 370-391, 467-483, 485-501, and 578-609.

♻️ Proposed change
         $this->createEngine($client)->update(new EloquentCollection([$model]));
+
+        $this->assertTrue(true);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Scout/Unit/Engines/TypesenseEngineTest.php` around lines 208 - 217, Add
an explicit $this->assertTrue(true) assertion to
testUpdateTransformsBeforeTouchingTypesense and each listed mock-only test
method, matching the neighboring test convention. Do not alter the existing
Mockery expectations or test behavior.

853-907: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Move file-scope helper models and enums into test-specific namespaces. Six test files declare generic helper models and enums directly in the test file namespace. Names such as TypesenseLifecycleModel, TypesenseStringCategory, and NamedSearchAttributesModel can collide as more Scout tests are added. The coding guidelines require a test-specific namespace whose final segment is the test class name.

  • tests/Scout/Unit/Engines/TypesenseEngineTest.php#L853-L907: move TypesenseLifecycleModel, TypesenseStringCategory, and TypesenseIntegerPriority into a namespace ending in TypesenseEngineTest.
  • tests/Scout/Unit/Engines/MeilisearchEngineTest.php#L807-L836: move MeilisearchTestChirpModel, MeilisearchStringStatus, and MeilisearchIntegerPriority into a namespace ending in MeilisearchEngineTest.
  • tests/Integration/Scout/Typesense/TypesenseEngineIntegrationTest.php#L227-L254: move TypesenseReadWriteSearchableModel and TypesenseCustomScoutKeyModel into a namespace ending in TypesenseEngineIntegrationTest.
  • tests/Integration/Scout/Typesense/TypesenseFilteringIntegrationTest.php#L160-L167: move TypesenseFilterRanking and TypesenseFilterTitle into a namespace ending in TypesenseFilteringIntegrationTest.
  • tests/Integration/Scout/Meilisearch/MeilisearchFilteringIntegrationTest.php#L182-L190: move MeilisearchFilterId and MeilisearchFilterTitle into a namespace ending in MeilisearchFilteringIntegrationTest.
  • tests/Scout/Feature/DatabaseEngineTest.php#L389-L406: move NamedSearchAttributesModel and MalformedSearchAttributeModel into a namespace ending in DatabaseEngineTest.

As per coding guidelines: "Use test-specific namespaces for collision-prone generic helper classes, with the test class name as the final namespace segment."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Scout/Unit/Engines/TypesenseEngineTest.php` around lines 853 - 907,
Move the file-scope helpers into test-specific namespaces ending with their
corresponding test class name, updating references and imports as needed:
TypesenseEngineTest.php:853-907 for TypesenseLifecycleModel,
TypesenseStringCategory, and TypesenseIntegerPriority;
MeilisearchEngineTest.php:807-836 for MeilisearchTestChirpModel,
MeilisearchStringStatus, and MeilisearchIntegerPriority;
TypesenseEngineIntegrationTest.php:227-254 for TypesenseReadWriteSearchableModel
and TypesenseCustomScoutKeyModel; TypesenseFilteringIntegrationTest.php:160-167
for TypesenseFilterRanking and TypesenseFilterTitle;
MeilisearchFilteringIntegrationTest.php:182-190 for MeilisearchFilterId and
MeilisearchFilterTitle; DatabaseEngineTest.php:389-406 for
NamedSearchAttributesModel and MalformedSearchAttributeModel.

Source: Coding guidelines

tests/Integration/Scout/Meilisearch/MeilisearchEngineIntegrationTest.php (1)

66-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both queued-removal tests can pass without indexing anything. Each test indexes a model, runs the serialized RemoveFromSearch job, and then asserts that the document is absent. If the indexing step silently stores nothing, the absence assertion still holds and the test reports success without exercising the removal path. Assert the indexed state before you run the job.

  • tests/Integration/Scout/Meilisearch/MeilisearchEngineIntegrationTest.php#L66-L84: after waitForMeilisearchTasks(), assert that the index contains one hit, then run the job.
  • tests/Integration/Scout/Typesense/TypesenseEngineIntegrationTest.php#L202-L224: after update(), retrieve the document keyed by (string) $model->getScoutKey() and assert it exists, then run the job and expect ObjectNotFound.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Integration/Scout/Meilisearch/MeilisearchEngineIntegrationTest.php`
around lines 66 - 84, Verify the document exists before exercising each
queued-removal job. In
tests/Integration/Scout/Meilisearch/MeilisearchEngineIntegrationTest.php:66-84,
after waitForMeilisearchTasks() and before RemoveFromSearch::handle(), assert
the search results contain exactly one hit. In
tests/Integration/Scout/Typesense/TypesenseEngineIntegrationTest.php:202-224,
after update() and before running the job, retrieve the document using (string)
$model->getScoutKey() and assert it exists; preserve the subsequent
ObjectNotFound expectation.
tests/Scout/Unit/Engines/MeilisearchEngineTest.php (1)

181-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the expected filter string so a mismatch reports a diff.

The assertion runs inside m::on(). If the produced filter differs, Mockery reports "no matching handler" and does not show the actual string. The expected value contains nested quote and backslash escaping, so a mismatch is hard to diagnose. Capture the parameter and assert it after the call.

♻️ Proposed refactor
+        $captured = null;
+
         $index->shouldReceive('rawSearch')
             ->once()
-            ->with('query', m::on(function ($params) {
-                return $params['filter'] === 'status="active" AND score>=10 AND label!="draft\"review\\\copy" AND archived_at IS NOT NULL AND tags IN ["a\"b\\\c", true] AND hidden NOT IN ["draft", false]';
+            ->with('query', m::on(function ($params) use (&$captured) {
+                $captured = $params['filter'];
+
+                return true;
             }))
             ->andReturn(['hits' => [], 'totalHits' => 0]);

Then assert after $engine->search($builder);:

$this->assertSame(
    'status="active" AND score>=10 AND label!="draft\"review\\\copy" AND archived_at IS NOT NULL AND tags IN ["a\"b\\\c", true] AND hidden NOT IN ["draft", false]',
    $captured
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Scout/Unit/Engines/MeilisearchEngineTest.php` around lines 181 - 213,
Update testSearchWithFilters to capture the rawSearch parameters in the m::on()
callback instead of comparing the filter inline, then assert the captured filter
with assertSame after $engine->search($builder). Keep the expected filter string
unchanged so assertion failures report a direct diff.
tests/Integration/Scout/Typesense/TypesenseEngineIntegrationTest.php (1)

186-200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the schema name is not used as a collection target.

The test proves that indexing and searching work against indexableAs(). It does not prove that the _stale name from typesenseCollectionSchema() is unused. The test passes even if the engine also creates the _stale collection. Add an assertion that the _stale collection does not exist.

💡 Proposed additional assertion
         $this->assertSame([$model->id], $results->pluck('id')->all());
+
+        $this->expectException(\Typesense\Exceptions\ObjectNotFound::class);
+        $this->typesense->collections[$model->searchableAs() . '_stale']->retrieve();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Integration/Scout/Typesense/TypesenseEngineIntegrationTest.php` around
lines 186 - 200, Extend testResolvedWriteTargetOverridesSchemaName to assert
that the Typesense collection named from typesenseCollectionSchema() with the
“_stale” suffix is absent after indexing and searching, while preserving the
existing indexableAs() result assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/scout/config/scout.php`:
- Around line 56-67: Cast the `after_commit` value in the Scout config using the
same boolean coercion as `soft_delete`, ensuring `SCOUT_AFTER_COMMIT` always
produces a consistent boolean for `Config::boolean(...)`. Also add an
upgrade/migration note documenting the replacement of `scout.queue.after_commit`
and `SCOUT_QUEUE_AFTER_COMMIT` with `scout.after_commit` and
`SCOUT_AFTER_COMMIT`.

In `@src/scout/src/Console/QueueImportCommand.php`:
- Around line 194-204: Update parseIntegerBound() to accept integral float
values and numeric strings returned by aggregate queries, converting valid
integer-valued inputs to int before returning. Preserve rejection of
non-integral, malformed, and out-of-range values, and keep
dispatchIntegerRange() receiving int|false bounds.

In `@src/scout/src/Contracts/SearchableInterface.php`:
- Around line 95-108: Add a Scout release note documenting the three new
SearchableInterface methods—searchIndexShouldBeUpdated(),
wasSearchableBeforeUpdate(), and wasSearchableBeforeDelete()—as a breaking
change for external implementations that do not use the Searchable trait, noting
that those implementations must add the methods.

In `@src/scout/src/Engines/AlgoliaEngine.php`:
- Around line 210-235: Update the where-clause mapping in AlgoliaEngine so
string values enter the formatting branch only for “=” and “!=” operators;
preserve other comparison operators such as “>”, “<”, “>=”, and “<=” for the
final filter expression or reject them explicitly instead of converting them to
“:”.

In `@src/scout/src/Engines/TypesenseEngine.php`:
- Around line 353-357: Update the TypesenseEngine where-filter mapping around
parseFilterValue() so null values are handled before being passed to its
non-nullable parameter. Reject null with a descriptive Scout-level exception
consistent with the MeilisearchEngine behavior, and apply the same handling to
the additional affected path.

In `@src/scout/src/Searchable.php`:
- Around line 236-247: Update Searchable::search to check whether the model
declares the static scoutBuilder property with property_exists() before reading
it; use the declared custom builder when present and Builder::class otherwise.
Preserve the existing makeWith arguments and search behavior.

In `@tests/Scout/Feature/SearchableModelTest.php`:
- Around line 33-51: Declare the static $scoutBuilder property on the Searchable
trait with the appropriate nullable builder-class type and default, so
Searchable::search() can safely evaluate static::$scoutBuilder ?? Builder::class
for models that do not override it.

In `@tests/Scout/Unit/BuilderTest.php`:
- Around line 587-627: Update
testGenericPaginationUsesFreshContainerSubstitutionsAndDefaultPerPageForZero to
restore or remove the Paginator and LengthAwarePaginator container bindings
after assertions, using a try/finally cleanup so it runs even when the test
fails and does not affect later tests.

In `@tests/Scout/Unit/Jobs/MakeSearchableTest.php`:
- Around line 70-84: Update UniqueByScoutKeys::uniqueId() to hash the existing
payload of model class and sorted Scout keys using seeded xxh128 instead of
sha256. Preserve the payload structure and seed, then update
testUniqueIdIsBasedOnTheClassAndScoutKeys() to compute the matching xxh128
expectation.

---

Nitpick comments:
In `@src/scout/src/Searchable.php`:
- Around line 104-124: Extract a shared listener-gated event dispatch helper
matching SearchableScope::dispatchEvent(), then replace the duplicated
ModelsImported and ModelsFlushed listener checks in the HasManyThrough macros
with calls to that helper. Keep event construction and dispatch inside the
helper, and invoke it from both callback blocks.

In `@tests/Integration/Scout/Meilisearch/MeilisearchEngineIntegrationTest.php`:
- Around line 66-84: Verify the document exists before exercising each
queued-removal job. In
tests/Integration/Scout/Meilisearch/MeilisearchEngineIntegrationTest.php:66-84,
after waitForMeilisearchTasks() and before RemoveFromSearch::handle(), assert
the search results contain exactly one hit. In
tests/Integration/Scout/Typesense/TypesenseEngineIntegrationTest.php:202-224,
after update() and before running the job, retrieve the document using (string)
$model->getScoutKey() and assert it exists; preserve the subsequent
ObjectNotFound expectation.

In `@tests/Integration/Scout/Typesense/TypesenseEngineIntegrationTest.php`:
- Around line 186-200: Extend testResolvedWriteTargetOverridesSchemaName to
assert that the Typesense collection named from typesenseCollectionSchema() with
the “_stale” suffix is absent after indexing and searching, while preserving the
existing indexableAs() result assertion.

In `@tests/Scout/Unit/Engines/MeilisearchEngineTest.php`:
- Around line 181-213: Update testSearchWithFilters to capture the rawSearch
parameters in the m::on() callback instead of comparing the filter inline, then
assert the captured filter with assertSame after $engine->search($builder). Keep
the expected filter string unchanged so assertion failures report a direct diff.

In `@tests/Scout/Unit/Engines/TypesenseEngineTest.php`:
- Around line 208-217: Add an explicit $this->assertTrue(true) assertion to
testUpdateTransformsBeforeTouchingTypesense and each listed mock-only test
method, matching the neighboring test convention. Do not alter the existing
Mockery expectations or test behavior.
- Around line 853-907: Move the file-scope helpers into test-specific namespaces
ending with their corresponding test class name, updating references and imports
as needed: TypesenseEngineTest.php:853-907 for TypesenseLifecycleModel,
TypesenseStringCategory, and TypesenseIntegerPriority;
MeilisearchEngineTest.php:807-836 for MeilisearchTestChirpModel,
MeilisearchStringStatus, and MeilisearchIntegerPriority;
TypesenseEngineIntegrationTest.php:227-254 for TypesenseReadWriteSearchableModel
and TypesenseCustomScoutKeyModel; TypesenseFilteringIntegrationTest.php:160-167
for TypesenseFilterRanking and TypesenseFilterTitle;
MeilisearchFilteringIntegrationTest.php:182-190 for MeilisearchFilterId and
MeilisearchFilterTitle; DatabaseEngineTest.php:389-406 for
NamedSearchAttributesModel and MalformedSearchAttributeModel.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b89c5e4-5f19-4b81-bd6e-1f8ca20d42d8

📥 Commits

Reviewing files that changed from the base of the PR and between cbcb631 and dc01c0a.

📒 Files selected for processing (70)
  • docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md
  • docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md
  • docs/plans/2026-08-02-1006-scout-current-parity-queue-and-search-lifecycles.md
  • src/boost/docs/scout.md
  • src/database/src/Concerns/BuildsQueries.php
  • src/database/src/Eloquent/HasCollection.php
  • src/database/src/Query/Builder.php
  • src/scout/README.md
  • src/scout/composer.json
  • src/scout/config/scout.php
  • src/scout/src/Builder.php
  • src/scout/src/Console/ConcurrentImportRunner.php
  • src/scout/src/Console/IndexCommand.php
  • src/scout/src/Console/QueueImportCommand.php
  • src/scout/src/Contracts/SearchableInterface.php
  • src/scout/src/Engines/AlgoliaEngine.php
  • src/scout/src/Engines/CollectionEngine.php
  • src/scout/src/Engines/DatabaseEngine.php
  • src/scout/src/Engines/MeilisearchEngine.php
  • src/scout/src/Engines/TypesenseEngine.php
  • src/scout/src/Jobs/MakeRangeSearchable.php
  • src/scout/src/Jobs/MakeSearchable.php
  • src/scout/src/Jobs/MakeSearchableUniquely.php
  • src/scout/src/Jobs/RemoveFromSearch.php
  • src/scout/src/Jobs/RemoveFromSearchUniquely.php
  • src/scout/src/ModelObserver.php
  • src/scout/src/Searchable.php
  • src/scout/src/SearchableScope.php
  • src/scout/src/Traits/ConfiguresJobOptions.php
  • src/scout/src/Traits/UniqueByScoutKeys.php
  • tests/Database/DatabaseEloquentBuilderTest.php
  • tests/Database/DatabaseEloquentModelTest.php
  • tests/Database/DatabaseQueryBuilderTest.php
  • tests/Database/Eloquent/Concerns/HasUuidsTest.php
  • tests/Integration/Scout/Algolia/AlgoliaEngineIntegrationTest.php
  • tests/Integration/Scout/Algolia/AlgoliaFilteringIntegrationTest.php
  • tests/Integration/Scout/Meilisearch/MeilisearchEngineIntegrationTest.php
  • tests/Integration/Scout/Meilisearch/MeilisearchFilteringIntegrationTest.php
  • tests/Integration/Scout/Typesense/TypesenseEngineIntegrationTest.php
  • tests/Integration/Scout/Typesense/TypesenseFilteringIntegrationTest.php
  • tests/Scout/Feature/CollectionEngineTest.php
  • tests/Scout/Feature/CoroutineSafetyTest.php
  • tests/Scout/Feature/DatabaseEngineTest.php
  • tests/Scout/Feature/SearchableModelTest.php
  • tests/Scout/Feature/SearchableScopeTest.php
  • tests/Scout/Models/TypesenseSearchableModel.php
  • tests/Scout/PackageMetadataTest.php
  • tests/Scout/ScoutTestCase.php
  • tests/Scout/Unit/BuilderTest.php
  • tests/Scout/Unit/ConfigFileTest.php
  • tests/Scout/Unit/ConfigTest.php
  • tests/Scout/Unit/Console/ConcurrentImportRunnerTest.php
  • tests/Scout/Unit/Console/ImportCommandTest.php
  • tests/Scout/Unit/Console/IndexCommandTest.php
  • tests/Scout/Unit/Console/QueueImportCommandTest.php
  • tests/Scout/Unit/EngineManagerTest.php
  • tests/Scout/Unit/Engines/AlgoliaEngineTest.php
  • tests/Scout/Unit/Engines/MeilisearchEngineTest.php
  • tests/Scout/Unit/Engines/MeilisearchRetryMiddlewareTest.php
  • tests/Scout/Unit/Engines/MeilisearchRetryPolicyTest.php
  • tests/Scout/Unit/Engines/NullEngineTest.php
  • tests/Scout/Unit/Engines/TypesenseEngineTest.php
  • tests/Scout/Unit/Jobs/MakeRangeSearchableTest.php
  • tests/Scout/Unit/Jobs/MakeSearchableTest.php
  • tests/Scout/Unit/Jobs/RemoveFromSearchTest.php
  • tests/Scout/Unit/MakeSearchableUsingTest.php
  • tests/Scout/Unit/ModelObserverTest.php
  • tests/Scout/Unit/QueueDispatchTest.php
  • tests/Scout/Unit/ScoutServiceProviderTest.php
  • tests/Scout/migrations/2025_01_01_000000_create_searchable_models_table.php

Comment thread src/scout/config/scout.php Outdated
Comment thread src/scout/src/Console/QueueImportCommand.php
Comment thread src/scout/src/Contracts/SearchableInterface.php
Comment thread src/scout/src/Engines/AlgoliaEngine.php
Comment thread src/scout/src/Engines/TypesenseEngine.php
Comment thread src/scout/src/Searchable.php
Comment thread tests/Scout/Feature/SearchableModelTest.php
Comment thread tests/Scout/Unit/BuilderTest.php
Comment thread tests/Scout/Unit/Jobs/MakeSearchableTest.php
Normalize SCOUT_AFTER_COMMIT at the shipped config boundary so conventional 1 and 0 environment values satisfy the observer's strict boolean getter. Share the environment regression with the existing soft-delete case while preserving complete process-environment restoration.

Validate Algolia's documented comparison operators before formatting and keep facet equality separate from numeric ordering. Reject null, non-finite, nonnumeric, and non-scalar ordering values locally; preserve native integer and finite-float rendering, boolean 1/0 semantics, and canonical numeric strings without precision loss.

Strengthen queued-removal integration coverage by proving the exact custom Scout key exists in Meilisearch and Typesense before deletion. Update the Scout plan and audit ledger to record the final scalar, operator, config, and test boundaries.
Make Foundation's Meilisearch integration waiter inspect the terminal results it actually awaits and propagate failed tasks, timeouts, and transport errors instead of reporting false success. Consolidate Scout on that single waiter and use a Meilisearch-valid custom Scout key with a positive publication precondition.

Wait for the exact Meilisearch and Algolia deletion tasks created during test cleanup so one test cannot race the next test's setup. Preserve loud setup failures, exception-safe teardown, and Algolia's global HTTP client restoration.

Cover successful, failed, timed-out, and exact cleanup task ownership deterministically, document Meilisearch's identifier constraint, and record the Foundation-owned corrections in the audit ledger and dependency index.
@binaryfire

Copy link
Copy Markdown
Collaborator Author

@greptileai @coderabbitai Follow-up is complete.

The accepted review items are now addressed:

  • SCOUT_AFTER_COMMIT is normalized to a boolean at the shipped config boundary, including conventional 1 and 0 environment values.
  • Algolia comparisons validate the documented operator set and keep facet equality separate from numeric ordering. Null, non-finite, nonnumeric, and non-scalar ordering values now fail locally, while canonical numeric strings and boolean ordering remain intact.
  • The queued-removal integrations now prove that the exact custom Scout key was published before testing deletion.

That positive precondition exposed two test-harness defects. The shared custom key used a character Meilisearch rejects, while the Meilisearch waiter discarded asynchronous task failures. The fixture now uses a valid identifier, Foundation owns the single truthful waiter, and both Meilisearch and Algolia cleanup wait for the exact deletion tasks they start. Setup fails when isolation cannot be established; teardown remains exception-safe.

The rejected comments did not result in changes where the existing behavior was already correct or the proposed machinery would have weakened the design. The full repository gate and the configured Meilisearch integration group are green.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@binaryfire Confirmed. The accepted items are addressed.

The config boundary now normalizes SCOUT_AFTER_COMMIT. Algolia validates comparison inputs locally. The queued-removal tests establish publication of the exact custom Scout key before deletion.

The cleanup and waiter changes also make the integration tests fail correctly when setup isolation fails.


✏️ Learnings added
Learnt from: binaryfire
URL: https://github.com/hypervel/components/pull/464

Timestamp: 2026-08-02T16:57:10.053Z
Learning: Scout queued-removal integration tests must use a Meilisearch-valid custom Scout key, establish that the exact key was indexed before deletion, and rely on the Foundation waiter as the single authoritative asynchronous-task failure path. Algolia and Meilisearch cleanup must wait for the exact deletion tasks that they start. Setup must fail when test isolation cannot be established, while teardown must remain exception-safe.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown

Thanks for the thorough follow-up summary.

The three accepted items are resolved in the right direction:

  • Boolean normalization at the config boundary is the correct place to handle it — downstream code can trust the type without defensive casts.
  • Local rejection of null, non-finite, and non-scalar ordering values is better than letting those reach Algolia and getting an opaque remote error.
  • Proving the custom key was published before asserting deletion is the correct precondition — it turns what was a vacuous pass into a meaningful integration test.

The two test-harness defects you found along the way (invalid Meilisearch key character, waiter discarding async task failures) are exactly the kind of defect that surfaces once a test actually exercises the real precondition rather than assuming it. Fixing them in Foundation so both engines share a single truthful waiter is the right ownership decision.

The reasoning on the rejected comments is clear and the gate being green is the confirmation that matters. This is in good shape.

@binaryfire
binaryfire merged commit 53ec375 into 0.4 Aug 2, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant