From 6c785de6c14d0ac396f01ee61d6e46ef93788387 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:30:26 +0100 Subject: [PATCH 01/62] test(replication): add Solr version to lifecycle matrix --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6dcca1..73640b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,7 @@ jobs: env: SOLR_BASE_URL: http://localhost:8983/solr SOLR_CORE: integration + SOLR_VERSION: "${{ matrix.solr-version }}" SOLR_BACKUP_LOCATION: /var/solr/backups - name: Print Solr logs if: always() From 0c360b8fee1be80985e7cedc173704fefa08f8ea Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:30:28 +0100 Subject: [PATCH 02/62] test(replication): update docs/api-coverage.md --- docs/api-coverage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-coverage.md b/docs/api-coverage.md index 31ff5fb..e9c8a1a 100644 --- a/docs/api-coverage.md +++ b/docs/api-coverage.md @@ -26,7 +26,7 @@ A generic request method does not mean that every endpoint has a typed wrapper, | Security | `security(area, command)` | Security-enabled standalone matrix covers anonymous 401 responses for both authentication and authorization, authenticated reads, authentication property mutation/read-back, and malformed JSON rejection on Solr 8.11.4/9.11.0/10.1.0. Unit tests verify Basic/Bearer credentials and Authorization headers are not exposed through client HTTP errors. See Issues #86–#88. | | System | `systemInfo()`, `systemProperties()`, `systemThreads()`, `systemLogging()` | System info, non-empty JVM/process properties, a non-empty thread dump, and logger metadata are exercised on pinned Solr 8.11.4/9.11.0/10.1.0. Optional fields remain version-dependent; response models preserve unknown fields. See Issues #89 and #91. |\n| Metrics | `metrics()` with typed group/registry/prefix/regex filters; exported `SolrMetricsJsonResponse`, `SolrMetricsRegistry`, and open `SolrMetricValue`; JSON/Prometheus response union | JVM response-format coverage remains distinct (observed JSON on Solr 8/9 and Prometheus exposition on 10.1). A `node` group integration check runs on all pinned versions, asserting a non-empty JSON registry on Solr 8/9 and Prometheus samples on 10.1; registry, prefix, and regex filters are asserted against JSON responses on Solr 8/9 (Solr 10.1 supports Prometheus/OpenMetrics, not the JSON writer). Consumer fixtures verify the extensible registry/value envelope; plugin-specific metrics remain open and unenumerated. See Issues #90, #91, and #236. | | Backup and restore | `replication("backup" | "restore" | "restorestatus" | "deletebackup")`, `collections("BACKUP" | "RESTORE" | "LISTBACKUP" | "DELETEBACKUP" | "REQUESTSTATUS" | "DELETESTATUS")` | Typed core and SolrCloud backup/restore parameters and response models, including async status polling. Full core restore and SolrCloud collection backup/restore cycles are integration-tested on Solr 8.11.4/9.11.0/10.1.0 using local filesystem repositories and shared SolrCloud storage. See [backup and restore docs](backup-restore.md). | -| Replication | `replication(command, params)`, `replicationFile(params)`; exported command-specific parameter and response models | V1 ReplicationHandler commands for index version, details, file list, leader/follower control (Solr 10 fetchindex/abortfetch; Solr 8/9 snappull/abort), core backup/restore/status/delete, and raw file-content streaming. Integration coverage includes core backup/restore cycles on Solr 8.11.4/9.11.0/10.1.0. An end-to-end leader/follower fetch cycle is not yet certified. See Issue #95 and [replication docs](replication.md). | +| Replication | `replication(command, params)`, `replicationFile(params)`; exported command-specific parameter and response models | V1 ReplicationHandler commands for index version, details, file list, leader/follower control (Solr 10 fetchindex/abortfetch; Solr 8/9 snappull/abort), core backup/restore/status/delete, and raw file-content streaming. Integration coverage includes core backup/restore cycles and a disposable two-core leader/follower fetch/read-back cycle on Solr 8.11.4/9.11.0/10.1.0. The test uses `snappull` on Solr 8/9 and `fetchindex` on Solr 10; abort and polling lifecycle behavior remains unit-only. See Issue #95 and [replication docs](replication.md). | | Streaming Expressions | `stream()` | Sends `expr` with `wt=json` and parses the complete JSON response through the shared transport. It is not an incremental tuple-stream parser: no chunk-boundary protocol or memory-bound/backpressure guarantee is claimed. SolrCloud search is integration-tested on pinned Solr 8.11.4/9.11.0/10.1.0; unit tests cover JSON parsing, caller cancellation during body consumption, and cleanup after body-stream errors. See [Streaming Expressions](streaming.md) and Issue #93. | | Export | `export()`, `exportStream()`; exported `SolrExportParams`, `SolrExportSort`, and `SolrExportFieldType` | Export requests require typed `fl` and `sort` parameters, support repeated filter queries, structured or raw sort expressions, batch sizing, JSON/JavaBin writers, and an explicit custom-parameter escape hatch. The raw Response preserves backpressure-aware streaming; `exportStream()` exposes its byte body directly. Sort fields must be single-valued docValues fields (int, long, float, double, or string); field lists additionally support date and boolean and may be multi-valued. Solr 10.1 `includeStoredFields` support is exposed but requires version-specific verification. Existing raw Export response tests pass on pinned Solr 8.11.4/9.11.0/10.1.0; typed parameter serialization and streamed chunk consumption have unit coverage. See [Export docs](export.md) and Issues #92–#94. | | Advanced / Raw | `requestRaw(path, options)` with typed method/query/header/body/control options and a raw Fetch `Response` | Relative paths and same-origin absolute URLs use the shared transport. Cross-origin URLs, embedded credentials, and URL fragments are rejected before Fetch, preventing configured credentials from being sent to unrelated hosts. Generic request()/requestRaw() transport behavior has unit coverage only; arbitrary custom endpoint behavior remains unverified until endpoint-specific integration tests exist. See [raw requests](raw-requests.md) and Issue #98. | From da5026772c00cccd22708ec364989895a9a91217 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:30:31 +0100 Subject: [PATCH 03/62] test(replication): update docs/api-inventory.json --- docs/api-inventory.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-inventory.json b/docs/api-inventory.json index 6ef9b53..a321008 100644 --- a/docs/api-inventory.json +++ b/docs/api-inventory.json @@ -396,11 +396,12 @@ "test/replication.test.ts", "test/replication-api-types.ts", "test/integration/solr-replication.test.ts", + "test/integration/solr-replication-lifecycle.test.ts", "test/backup-restore.test.ts", "test/backup-restore-api-types.ts", "test/integration/solr-core-backup-restore.test.ts" ], - "notes": "Typed V1 ReplicationHandler commands for enablereplication, disablereplication, indexversion, fetchindex, abortfetch, snappull, abort, enablepoll, disablepoll, details, filelist, backup, restore, restorestatus, and deletebackup; replicationFile() returns raw filecontent streams. Live integration coverage on Solr 8.11.4, 9.11.0, and 10.1.0 covers indexversion, details, filelist, and filecontent. enablereplication/disablereplication are integration-tested on a configured leader; fetchindex/abortfetch (Solr 10), snappull/abort (Solr 8/9), enablepoll, and disablepoll have unit serialization coverage, but a multi-core leader/follower fetch cycle is not yet certified. Core-level backup, restore, restorestatus, and deletebackup are integration-tested on all three pinned versions. Filelist uses the generation returned by newer Solr indexversion responses and falls back to the legacy indexversion parameter when that field is absent. Additional operation evidence: Core-level ReplicationHandler backup, restore, restorestatus, and deletebackup are typed. Backup defaults to the core dataDir; restore status is polled with restorestatus. Full backup/restore/read-back/delete cycle is integration-tested on Solr 8.11.4, 9.11.0, and 10.1.0. Custom filesystem locations must be permitted by server allowPaths." + "notes": "Typed V1 ReplicationHandler commands for enablereplication, disablereplication, indexversion, fetchindex, abortfetch, snappull, abort, enablepoll, disablepoll, details, filelist, backup, restore, restorestatus, and deletebackup; replicationFile() returns raw filecontent streams. Live integration coverage on Solr 8.11.4, 9.11.0, and 10.1.0 covers indexversion, details, filelist, filecontent, leader enable/disable, and a disposable two-core leader/follower fetch with follower query read-back. The integration job supplies SOLR_VERSION so the test uses fetchindex on Solr 10 and snappull on Solr 8/9. abortfetch/abort, enablepoll, and disablepoll retain unit serialization coverage only; an in-progress abort lifecycle is not certified. Core-level backup, restore, restorestatus, and deletebackup are integration-tested on all three pinned versions. Filelist uses the generation returned by newer Solr indexversion responses and falls back to the legacy indexversion parameter when that field is absent. Additional operation evidence: Core-level ReplicationHandler backup, restore, restorestatus, and deletebackup are typed. Backup defaults to the core dataDir; restore status is polled with restorestatus. Full backup/restore/read-back/delete cycle is integration-tested on Solr 8.11.4, 9.11.0, and 10.1.0. Custom filesystem locations must be permitted by server allowPaths." }, { "id": "replication.file", From 770a1ec27ba1737784620270d25750b66a2a0677 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:30:34 +0100 Subject: [PATCH 04/62] test(replication): update docs/compatibility.md --- docs/compatibility.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index f255131..f12d501 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -38,7 +38,7 @@ The 8.11, 9.11, and 10.1 guides describe the API families below. The Replica Man | `security()` — authentication and authorization handlers | Documented | Documented | Documented | Tested: authenticated BasicAuth/RuleBasedAuthorization reads, anonymous 401 on both handlers, authentication `set-property` read-back, and malformed JSON rejection on pinned 8.11.4 / 9.11.0 / 10.1.0 images. Unit tests verify Basic/Bearer credentials are not exposed by client HTTP errors | | `stream()` — `/stream` | Documented | Documented | Documented | Tested: Streaming Expressions search against a disposable SolrCloud collection on pinned 8.11.4 / 9.11.0 / 10.1.0 images (CI #145) | | `export()` — `/export` | Documented | Documented | Documented | Tested: raw response with docValues sort field on pinned 8.11.4 / 9.11.0 / 10.1.0 images | -| `replication()` / `replicationFile()` — `/solr/{core}/replication` | Documented | Documented | Documented | Tested: indexversion, details, filelist (generation on newer versions / legacy indexversion on older versions), streamed filecontent, enablereplication/disablereplication, core backup, restorestatus, restore, and deletebackup on pinned 8.11.4 / 9.11.0 / 10.1.0 images. fetchindex/abortfetch (Solr 10), snappull/abort (Solr 8/9), enablepoll, and disablepoll have unit serialization coverage; an end-to-end multi-core leader/follower fetch cycle is not yet certified | +| `replication()` / `replicationFile()` — `/solr/{core}/replication` | Documented | Documented | Documented | Tested: indexversion, details, filelist (generation on newer versions / legacy indexversion on older versions), streamed filecontent, enablereplication/disablereplication, core backup, restorestatus, restore, deletebackup, and a disposable two-core leader/follower fetch with follower query read-back on pinned 8.11.4 / 9.11.0 / 10.1.0 images. The fetch uses snappull on Solr 8/9 and fetchindex on Solr 10. Abort and polling lifecycle behavior remains unit-only | | Core and collection backup/restore | `replication("backup" | "restore" | "restorestatus" | "deletebackup")`, `collections("BACKUP" | "RESTORE" | "LISTBACKUP" | "DELETEBACKUP" | "REQUESTSTATUS" | "DELETESTATUS")` | Documented | Documented | Documented | Tested: full core backup/restore/read-back and SolrCloud collection backup/list/restore/read-back/delete with asynchronous status polling on pinned 8.11.4 / 9.11.0 / 10.1.0 images using local filesystem repositories and shared SolrCloud storage | | `request()` / `requestRaw()` — generic caller-defined transport | Depends on the selected endpoint | Depends on the selected endpoint | Depends on the selected endpoint | Generic transport behavior has unit tests only; no endpoint-independent live Solr integration claim is made. Each custom endpoint remains unverified until tested individually | From fba03294e9568eba1b8f3f4bf397a5f1d7874ae0 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:31:00 +0100 Subject: [PATCH 05/62] docs(replication): record lifecycle verification boundary --- docs/replication.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/replication.md b/docs/replication.md index 375730c..8ebb459 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -103,4 +103,7 @@ Supported directory types are `file` (Lucene index files), `cf` (configuration f - The client currently exposes the V1 handler path. Solr 10.1 also documents V2 equivalents for selected file and index-version operations; no dedicated V2 wrapper is provided. - The handler may not be configured for user-managed leader/follower replication on every core. The server must have the appropriate ReplicationHandler configuration before control commands can be used. +- The pinned integration matrix creates disposable leader and follower cores, commits a unique leader document, triggers `snappull` on Solr 8/9 or `fetchindex` on Solr 10, and verifies that the document becomes searchable on the follower. This verifies a complete fetch/read-back cycle, not polling or abort of an in-progress transfer. +- `abort` (Solr 8/9) and `abortfetch` (Solr 10), plus `enablepoll`/`disablepoll`, have request-serialization unit coverage; cancellation of a live transfer remains unverified. + See the [Solr 8.11](https://solr.apache.org/__root/docs.solr.apache.org/guide/8_11/deployment-guide/user-managed-index-replication.html), [Solr 9.11](https://solr.apache.org/guide/solr/9_11/deployment-guide/user-managed-index-replication.html), and [Solr 10.1](https://solr.apache.org/guide/solr/10_1/deployment-guide/user-managed-index-replication.html) guides for server configuration and command semantics. From e3934e6b75b156118d51a60cdbc74945ce04f6f8 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:31:02 +0100 Subject: [PATCH 06/62] test(replication): register lifecycle integration test --- test/integration/all.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/integration/all.test.ts b/test/integration/all.test.ts index 36f2f2f..96775af 100644 --- a/test/integration/all.test.ts +++ b/test/integration/all.test.ts @@ -15,6 +15,7 @@ import "./solr-cursor.test.js"; import "./solr-schema.test.js"; import "./solr-security.test.js"; import "./solr-replication.test.js"; +import "./solr-replication-lifecycle.test.js"; import "./solr-core-backup-restore.test.js"; import "./solrcloud-backup-restore.test.js"; import "./solr-smoke.test.js"; From 154ba70030cf60daebbf66bc8d580af7cea326a6 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:31:04 +0100 Subject: [PATCH 07/62] test(replication): verify leader follower fetch lifecycle --- .../solr-replication-lifecycle.test.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 test/integration/solr-replication-lifecycle.test.ts diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts new file mode 100644 index 0000000..b07f115 --- /dev/null +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -0,0 +1,70 @@ +import assert from "node:assert/strict"; +import { SolrClient } from "../../src/index.js"; +import { integrationTest } from "./fixtures.js"; + +integrationTest("ReplicationHandler fetches a committed leader index into an isolated follower core", async ({ + createCore, + createId +}) => { + const leaderCore = await createCore("replication_lifecycle_leader"); + const followerCore = await createCore("replication_lifecycle_follower"); + const baseUrl = process.env.SOLR_BASE_URL!; + const username = process.env.SOLR_USERNAME; + const password = process.env.SOLR_PASSWORD; + const makeClient = (core: string) => new SolrClient({ + baseUrl, + core, + timeoutMs: 30_000, + ...(username && password ? { auth: { type: "basic" as const, username, password } } : {}) + }); + const leader = makeClient(leaderCore); + const follower = makeClient(followerCore); + const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}/replication`; + const documentId = createId("replication_lifecycle"); + + await leader.config({ + "update-requesthandler": { + name: "/replication", + class: "solr.ReplicationHandler", + customOptions: { leader: { replicateAfter: "commit" } } + } + }); + await follower.config({ + "update-requesthandler": { + name: "/replication", + class: "solr.ReplicationHandler", + customOptions: { follower: { leaderUrl } } + } + }); + + await leader.add([{ id: documentId, title: "leader follower lifecycle" }]); + await leader.commit({ waitSearcher: true }); + + const leaderVersion = await leader.replication("indexversion"); + assert.equal(leaderVersion.responseHeader?.status, 0); + const leaderGeneration = leaderVersion.generation ?? Number(leaderVersion.indexversion); + assert.ok(Number.isFinite(leaderGeneration) && leaderGeneration > 0, "leader must publish a replicable index"); + + const solrMajor = process.env.SOLR_VERSION?.split(".")[0]; + const fetchCommand = solrMajor === "10" ? "fetchindex" : "snappull"; + const fetchResponse = await follower.replication(fetchCommand, { leaderUrl }); + assert.equal(fetchResponse.responseHeader?.status, 0, `${fetchCommand} should complete successfully`); + + // Replication copies Lucene index files asynchronously; assert the resulting + // searchable state rather than treating the command's HTTP response as proof. + let found = false; + for (let attempt = 0; attempt < 60; attempt += 1) { + const result = await follower.query<{ response?: { numFound?: number } }>({ query: `id:"${documentId}"`, fields: ["id"], limit: 1 }); + if ((result.response?.numFound ?? 0) === 1) { + found = true; + break; + } + await new Promise(resolve => setTimeout(resolve, 500)); + } + assert.ok(found, "follower should expose the leader document after the fetch completes"); + + const followerVersion = await follower.replication("indexversion"); + assert.equal(followerVersion.responseHeader?.status, 0); + const followerGeneration = followerVersion.generation ?? Number(followerVersion.indexversion); + assert.ok(Number.isFinite(followerGeneration) && followerGeneration > 0, "follower should publish its fetched index"); +}); From 0350e6c0cc35636e575372387cc3eaf8f515dfde Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:33:17 +0100 Subject: [PATCH 08/62] test(replication): allow loopback leader URLs in isolated CI Solr --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73640b8..54a2d33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: --volume "$RUNNER_TEMP/solr-backups:/var/solr/backups" \ --publish 8983:8983 \ --env SOLR_JAVA_MEM="-Xms512m -Xmx512m" \ - --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups" \ + --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups -Dsolr.disable.shardsWhitelist=true" \ "solr:${{ matrix.solr-version }}" \ solr-precreate integration - name: Wait for Solr and integration core @@ -125,7 +125,7 @@ jobs: --volume "$RUNNER_TEMP/solr-backups:/var/solr/backups" \ --publish 8983:8983 \ --env SOLR_JAVA_MEM="-Xms512m -Xmx512m" \ - --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups" \ + --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups -Dsolr.disable.shardsWhitelist=true" \ --entrypoint /bin/bash \ "solr:${{ matrix.solr-version }}" \ -lc 'mkdir -p /var/solr/data && cp /tmp/security.json /var/solr/data/security.json && chmod 666 /var/solr/data/security.json && exec docker-entrypoint.sh solr-precreate integration' From 2e763c2628960c480fbbd00fc9ee210b4bbc7e5f Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:36:04 +0100 Subject: [PATCH 09/62] test(replication): configure BasicAuth for secure leader fetches --- test/integration/solr-replication-lifecycle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index b07f115..fee2bf0 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -33,7 +33,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", - customOptions: { follower: { leaderUrl } } + customOptions: { follower: { leaderUrl, ...(username && password ? { httpBasicAuthUser: username, httpBasicAuthPassword: password } : {}) } } } }); From 157021890d03c849b452c57ec9a8940623578196 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:37:42 +0100 Subject: [PATCH 10/62] test(replication): pass Solr version to secure integration matrix --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54a2d33..a3b54c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,6 +150,7 @@ jobs: env: SOLR_BASE_URL: http://localhost:8983/solr SOLR_CORE: integration + SOLR_VERSION: "${{ matrix.solr-version }}" SOLR_SECURITY: "true" SOLR_USERNAME: integration-admin SOLR_PASSWORD: integration-password From e062fea1fa998e739090dd117012d22379e510d9 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:38:32 +0100 Subject: [PATCH 11/62] test(replication): allow loopback leader URLs on Solr 10 CI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3b54c4..b9ab3bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: --volume "$RUNNER_TEMP/solr-backups:/var/solr/backups" \ --publish 8983:8983 \ --env SOLR_JAVA_MEM="-Xms512m -Xmx512m" \ - --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups -Dsolr.disable.shardsWhitelist=true" \ + --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups -Dsolr.disable.shardsWhitelist=true -Dsolr.security.allow.urls.enabled=false" \ "solr:${{ matrix.solr-version }}" \ solr-precreate integration - name: Wait for Solr and integration core @@ -125,7 +125,7 @@ jobs: --volume "$RUNNER_TEMP/solr-backups:/var/solr/backups" \ --publish 8983:8983 \ --env SOLR_JAVA_MEM="-Xms512m -Xmx512m" \ - --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups -Dsolr.disable.shardsWhitelist=true" \ + --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups -Dsolr.disable.shardsWhitelist=true -Dsolr.security.allow.urls.enabled=false" \ --entrypoint /bin/bash \ "solr:${{ matrix.solr-version }}" \ -lc 'mkdir -p /var/solr/data && cp /tmp/security.json /var/solr/data/security.json && chmod 666 /var/solr/data/security.json && exec docker-entrypoint.sh solr-precreate integration' From edddceb0e5ab9a756b2691170116191c96b9b362 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:39:38 +0100 Subject: [PATCH 12/62] test(solrcloud): add opt-in SPLITSHARD and MIGRATE verification --- .github/workflows/ci.yml | 11 ++ docs/administration.md | 15 +++ docs/api-inventory.json | 3 +- docs/collections-api-coverage.md | 7 +- docs/endpoint-inventory.md | 2 +- test/integration/all.test.ts | 1 + test/integration/fixtures.ts | 6 +- .../solrcloud-split-migrate-optin.test.ts | 103 ++++++++++++++++++ 8 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 test/integration/solrcloud-split-migrate-optin.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9ab3bb..e685bc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,15 @@ on: branches: [master] pull_request: branches: [master] + workflow_dispatch: + inputs: + enable_destructive_solrcloud_tests: + description: "Run opt-in SPLITSHARD and MIGRATE checks on the disposable two-node cluster" + required: false + default: false + type: boolean +jobs: + test: jobs: test: strategy: @@ -160,6 +169,8 @@ jobs: run: docker logs solr-secure || true cloud-integration: + env: + SOLR_DESTRUCTIVE_OPT_IN: "${{ github.event_name == 'workflow_dispatch' && inputs.enable_destructive_solrcloud_tests == true && 'true' || 'false' }}" strategy: fail-fast: false matrix: diff --git a/docs/administration.md b/docs/administration.md index 4647f56..2156c2e 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -161,3 +161,18 @@ The alias and cluster-status integration checks exercise alias read-back, collec The V1 parameter spellings are intentional: this client calls `/solr/admin/collections`, not the Solr 10 V2 `/api/collections` resources. The V2 API maps `split.key` to `splitKey` and `target.collection` to `target`; those V2 names must not be sent through this V1 client. Reference guides: [Solr 8.1 Collections API](https://archive.apache.org/dist/lucene/solr/ref-guide/apache-solr-ref-guide-8.1.pdf), [Solr 9.2 Collection Management](https://solr.apache.org/guide/solr/9_2/deployment-guide/collection-management.html), [Solr 9.10 Shard Management](https://solr.apache.org/guide/solr/9_10/deployment-guide/shard-management.html), and [Solr 10.0 Collections API](https://solr.apache.org/guide/solr/10_0/configuration-guide/collections-api.html). + +## Opt-in SPLITSHARD and MIGRATE verification + +These operations are intentionally excluded from normal push/pull-request CI. The opt-in suite uses only collections created by the fixture with unique `integration_*` names. It refuses to run unless all of the following are true: + +- `SOLR_CLOUD=true` and `SOLR_MULTINODE=true`. +- `SOLR_DESTRUCTIVE_OPT_IN=true` is explicitly set. +- `SOLR_BASE_URL` resolves to localhost / loopback; remote Solr endpoints are skipped. +- The disposable cluster reports at least two live nodes; the split test additionally confirms its new collection starts with one shard and the migration test confirms the target collection is empty. + +Run it from the GitHub Actions **CI** workflow using **Run workflow** and enable `enable_destructive_solrcloud_tests`. This provisions the workflow's ephemeral two-node SolrCloud cluster and runs the opt-in tests for the pinned Solr version matrix. Leaving the input disabled keeps both tests skipped, including on ordinary pull requests. + +The tests use asynchronous request IDs and poll `REQUESTSTATUS` for up to 180 seconds, then remove completed status records. They verify active child-shard state and post-split document visibility, and verify that every routed document is searchable in the migration target. They do not claim to test concurrent writes, rollback of a partially completed operation, or recovery after a cluster/node failure. + +If a request times out or fails, do not rerun against a reused cluster. Preserve the CI job logs, inspect `CLUSTERSTATUS` and `REQUESTSTATUS`, and discard the ephemeral cluster. Fixture cleanup deletes only the collections it created; if Solr still has an operation in flight and refuses deletion, destroy the ephemeral cluster rather than attempting cleanup against a shared server. The suite is an opt-in verification harness, not permission to point these destructive actions at a user-owned cluster. diff --git a/docs/api-inventory.json b/docs/api-inventory.json index a321008..55abdf1 100644 --- a/docs/api-inventory.json +++ b/docs/api-inventory.json @@ -84,12 +84,13 @@ "testFiles": [ "test/admin.test.ts", "test/integration/solr-collections.test.ts", + "test/integration/solrcloud-split-migrate-optin.test.ts", "test/collections-types.ts", "test/backup-restore.test.ts", "test/backup-restore-api-types.ts", "test/integration/solrcloud-backup-restore.test.ts" ], - "notes": "SolrCloud CREATE/LIST/DELETE, alias lifecycle, implicit-router shard lifecycle, named ADDREPLICA/DELETEREPLICA lifecycle, replica property add/delete and BALANCESHARDUNIQUE responses, plus multi-node MOVEREPLICA and REBALANCELEADERS leader reassignment are tested on pinned Solr 8.11.4, 9.11.0, and 10.1.0 images (CI #177, #192, #209, and PR #142 CI #224). MOVEREPLICA verifies the named replica becomes active on a different live node. REBALANCELEADERS assigns preferredLeader to an active non-leader and verifies it becomes shard leader. Snapshot, backup, and cluster-state response models remain unverified. Additional operation evidence: Typed SolrCloud collection backup/restore actions and asynchronous REQUESTSTATUS/DELETESTATUS tracking. Full backup, list, restore, query read-back, and delete cycle is integration-tested on Solr 8.11.4, 9.11.0, and 10.1.0 with a shared local filesystem volume mounted on both SolrCloud nodes. S3 and other external repositories are not certified. LISTALIASES defaults to SolrCollectionAliasesResponse; CREATEALIAS/DELETEALIAS default to SolrCollectionAliasResponse; CLUSTERSTATUS defaults to SolrClusterStatusResponse with extensible collection/shard/replica models. Filtered cluster status and unobserved version-specific fields remain optional/open. CREATESHARD/DELETESHARD now default to SolrCollectionShardOperationResponse and remain integration-tested on all pinned versions. SPLITSHARD has a typed V1 split.key contract (shard or routing-key mode), but remains runtime-unverified because it is long-running, hash-router-specific, and has disk/routing implications. MIGRATE is added with V1 collection/split.key/target.collection/forward.timeout/async parameters and the same response envelope; it remains runtime-unverified because it is destructive and requires a controlled compositeId source/target pair. Do not treat these typed requests as integration certification. RELOAD now has exact action-specific serialization and isolated collection integration coverage on pinned Solr 8.11.4/9.11.0/10.1.0. MODIFYCOLLECTION, ADDROLE, and REMOVEROLE now have dedicated serialization and consumer type fixtures; MODIFYCOLLECTION remains runtime-unverified because it mutates collection policy, while role actions remain runtime-unverified because they mutate cluster-wide node role assignments. Named response contracts now cover every action literal: LIST uses SolrCollectionListResponse; CREATE/DELETE/RELOAD/MODIFYCOLLECTION use SolrCollectionAdminResponse; replica lifecycle, movement, balancing, and property actions use SolrCollectionReplicaResponse; ADDROLE/REMOVEROLE use SolrCollectionRoleResponse. CollectionAdminResponse selects the default contract from the action literal while explicit caller generics remain supported. These are extensible response envelopes, not a claim that all action-specific payload fields or all actions are runtime-certified." + "notes": "SolrCloud CREATE/LIST/DELETE, alias lifecycle, implicit-router shard lifecycle, named ADDREPLICA/DELETEREPLICA lifecycle, replica property add/delete and BALANCESHARDUNIQUE responses, plus multi-node MOVEREPLICA and REBALANCELEADERS leader reassignment are tested on pinned Solr 8.11.4, 9.11.0, and 10.1.0 images (CI #177, #192, #209, and PR #142 CI #224). MOVEREPLICA verifies the named replica becomes active on a different live node. REBALANCELEADERS assigns preferredLeader to an active non-leader and verifies it becomes shard leader. Snapshot, backup, and cluster-state response models remain unverified. Additional operation evidence: Typed SolrCloud collection backup/restore actions and asynchronous REQUESTSTATUS/DELETESTATUS tracking. Full backup, list, restore, query read-back, and delete cycle is integration-tested on Solr 8.11.4, 9.11.0, and 10.1.0 with a shared local filesystem volume mounted on both SolrCloud nodes. S3 and other external repositories are not certified. LISTALIASES defaults to SolrCollectionAliasesResponse; CREATEALIAS/DELETEALIAS default to SolrCollectionAliasResponse; CLUSTERSTATUS defaults to SolrClusterStatusResponse with extensible collection/shard/replica models. Filtered cluster status and unobserved version-specific fields remain optional/open. CREATESHARD/DELETESHARD now default to SolrCollectionShardOperationResponse and remain integration-tested on all pinned versions. SPLITSHARD has a typed V1 split.key contract (shard or routing-key mode). MIGRATE uses V1 collection/split.key/target.collection/forward.timeout/async parameters and the same response envelope. Both now have opt-in asynchronous integration checks that require SOLR_DESTRUCTIVE_OPT_IN=true, a loopback endpoint, SOLR_CLOUD=true, SOLR_MULTINODE=true, and disposable collections; the manual workflow provisions its own two-node cluster. The tests verify child-shard state and document read-back after split, and target document read-back after migration. The opt-in workflow has not yet been executed, so both operations remain runtime-unverified. Abort, partial-failure recovery, and concurrent-write behavior are not certified. RELOAD now has exact action-specific serialization and isolated collection integration coverage on pinned Solr 8.11.4/9.11.0/10.1.0. MODIFYCOLLECTION, ADDROLE, and REMOVEROLE now have dedicated serialization and consumer type fixtures; MODIFYCOLLECTION remains runtime-unverified because it mutates collection policy, while role actions remain runtime-unverified because they mutate cluster-wide node role assignments. Named response contracts now cover every action literal: LIST uses SolrCollectionListResponse; CREATE/DELETE/RELOAD/MODIFYCOLLECTION use SolrCollectionAdminResponse; replica lifecycle, movement, balancing, and property actions use SolrCollectionReplicaResponse; ADDROLE/REMOVEROLE use SolrCollectionRoleResponse. CollectionAdminResponse selects the default contract from the action literal while explicit caller generics remain supported. These are extensible response envelopes, not a claim that all action-specific payload fields or all actions are runtime-certified." }, { "id": "update.commit", diff --git a/docs/collections-api-coverage.md b/docs/collections-api-coverage.md index 60e3b5c..fa27607 100644 --- a/docs/collections-api-coverage.md +++ b/docs/collections-api-coverage.md @@ -23,8 +23,8 @@ This matrix is deliberately action-specific. The public client infers a named re | `ADDREPLICAPROP` | `AddReplicaPropertyParams` | `SolrCollectionReplicaResponse` | Serialization and compile fixtures | Adds replica property; all three pinned versions | | `DELETEREPLICAPROP` | `DeleteReplicaPropertyParams` | `SolrCollectionReplicaResponse` | Serialization and compile fixtures | Removes replica property; all three pinned versions | | `CREATESHARD` | `CreateShardParams` | `SolrCollectionShardOperationResponse` | Exact serialization and consumer type fixture | Creates a shard on a disposable implicit collection and reads it through CLUSTERSTATUS; all three pinned versions | -| `DELETESHARD` | `DeleteShardParams` | `SolrCollectionShardOperationResponse` | Exact serialization and consumer type fixture | Deletes only the shard created by the test and verifies absence; all three pinned versions |\n| `MIGRATE` | `MigrateCollectionParams` (`collection`, V1 `split.key`, V1 `target.collection`, optional `forward.timeout`, `async`) | `SolrCollectionShardOperationResponse` | Exact serialization and consumer type fixture | Not integration-tested: data migration is destructive/long-running and requires a controlled compositeId source/target pair | -| `SPLITSHARD` | `SplitShardParams` (`shard` or V1 `split.key`; `ranges`, `numSubShards`, `splitMethod`, `async`) | `SolrCollectionShardOperationResponse` | Exact serialization and consumer type fixture | Not integration-tested: long-running, hash-router-only operation with disk and routing implications; unsuitable for shared CI collections | +| `DELETESHARD` | `DeleteShardParams` | `SolrCollectionShardOperationResponse` | Exact serialization and consumer type fixture | Deletes only the shard created by the test and verifies absence; all three pinned versions |\n| `MIGRATE` | `MigrateCollectionParams` (`collection`, V1 `split.key`, V1 `target.collection`, optional `forward.timeout`, `async`) | `SolrCollectionShardOperationResponse` | Exact serialization and consumer type fixture | Opt-in only: disposable compositeId source/empty target; async completion plus target document read-back. Not yet executed against pinned versions; remains runtime-unverified | +| `SPLITSHARD` | `SplitShardParams` (`shard` or V1 `split.key`; `ranges`, `numSubShards`, `splitMethod`, `async`) | `SolrCollectionShardOperationResponse` | Exact serialization and consumer type fixture | Opt-in only: disposable one-shard compositeId collection; async completion, active child-shard state, and document read-back. Not yet executed against pinned versions; remains runtime-unverified | | `ADDROLE` | `CollectionRoleParams` | `SolrCollectionRoleResponse` | Exact serialization and consumer type fixture | Not integration-tested: changes cluster-wide node role assignment; requires a dedicated isolated cluster policy fixture | | `REMOVEROLE` | `CollectionRoleParams` | `SolrCollectionRoleResponse` | Exact serialization and consumer type fixture | Not integration-tested: changes cluster-wide node role assignment; requires a dedicated isolated cluster policy fixture | | `BACKUP` | `CollectionBackupParams` | `SolrCollectionBackupResponse` | Named response type and compile-time API declarations | Async collection backup with shared storage; all three pinned versions | @@ -44,13 +44,14 @@ The public `CollectionAdminResponse` conditional type maps each action litera - Compile-time consumer examples: `test/admin-api-types.ts`, `test/collections-types.ts`. - Unit serialization: `test/admin.test.ts`. - Collection, alias, shard, replica, and cluster-status runtime checks: `test/integration/solr-collections.test.ts`. +- Destructive, opt-in SPLITSHARD and MIGRATE verification: `test/integration/solrcloud-split-migrate-optin.test.ts` (manual workflow dispatch only). - Backup/restore and async status runtime checks: `test/integration/solrcloud-backup-restore.test.ts`. - Pinned server matrix: `test/integration/fixtures.ts` and the SolrCloud jobs in the CI workflow. ## Gaps and scope boundaries 1. **Cluster status response typing:** `SolrClusterStatusResponse` now models the stable live-node, alias, collection, shard, and replica envelope. Unknown and version-specific fields remain open; it is not a closed schema for every SolrCloud response variation. -2. **Shard split and migration:** `SPLITSHARD` and `MIGRATE` have typed V1 parameter and response contracts plus serialization/type fixtures, but no shared-CI runtime test. Both remain explicitly unverified because they can be long-running and affect routing/data placement. `MIGRATE` additionally requires a compositeId router and a quiescent target collection. +2. **Shard split and migration:** `SPLITSHARD` and `MIGRATE` have typed V1 parameter and response contracts plus serialization/type fixtures, but no normal-CI runtime test. A manually dispatched, loopback-only two-node workflow now contains opt-in tests that poll asynchronous completion and verify cluster/data state; until that workflow passes for a pinned version, both actions remain explicitly runtime-unverified because they can be long-running and affect routing/data placement. `MIGRATE` additionally requires a compositeId router and a quiescent target collection. 3. **Collection role operations:** `ADDROLE` and `REMOVEROLE` share `CollectionRoleParams` and now have exact serialization and consumer type fixtures. They remain runtime-unverified because they mutate cluster-wide node role assignments. 4. **Collection `RELOAD` and `MODIFYCOLLECTION`:** `RELOAD` now has an isolated integration assertion on the pinned matrix. `MODIFYCOLLECTION` has serialization/type coverage but remains runtime-unverified because it mutates collection policy and behavior varies with SolrCloud configuration. 5. **Response contract boundaries:** `SolrCollectionAdminResponse`, `SolrCollectionReplicaResponse`, and `SolrCollectionRoleResponse` are intentionally extensible envelopes; they do not claim undocumented action-specific payload fields. `SolrCollectionListResponse` types the stable `collections` list. These contracts describe the client-side JSON shape, not blanket runtime certification for every action or Solr version. diff --git a/docs/endpoint-inventory.md b/docs/endpoint-inventory.md index 35c74ec..235df19 100644 --- a/docs/endpoint-inventory.md +++ b/docs/endpoint-inventory.md @@ -12,7 +12,7 @@ This inventory defines the boundary of the typed client API after review against | Schema API `/schema` | `schema()` | Typed schema command models; integration coverage includes field, field-type, dynamic-field, copy-field, and schema read operations. | | Config API `/config` | `config()`, `getConfig()`, `getConfigOverlay()` | Typed POST command models and typed GET effective-config/overlay responses. Integration coverage includes user-property mutation/read-back; broader component mutation coverage is tracked separately in [Issue #75](https://github.com/vsimko/node-solr-lambda/issues/75). | | CoreAdmin V1 `/solr/admin/cores` | `coreAdmin()` | Action-specific parameter models include STATUS, CREATE, RELOAD, UNLOAD, RENAME, SWAP, MERGEINDEXES, SPLIT, REQUESTSTATUS, and REQUESTRECOVERY. Integration coverage includes isolated CREATE, STATUS discovery, RELOAD, and cleanup UNLOAD; async status/recovery actions have serialization coverage only. UPGRADEINDEX is intentionally excluded from the cross-version union because it is not part of the pinned Solr 8.11.4 baseline and is an in-place, non-SolrCloud operation. | -| Collections V1 `/solr/admin/collections` | `collections()` | Action-specific input models cover the current action union. Backup/restore and async operations have named response models; other actions use caller-supplied generic response shapes. Per-action evidence and unverified operations are tracked in the [Collections action coverage matrix](collections-api-coverage.md). Other unmodelled actions include MIGRATE, FORCELEADER, and selected cluster/node management commands. | +| Collections V1 `/solr/admin/collections` | `collections()` | Action-specific input models cover the current action union. Backup/restore and async operations have named response models; other actions use caller-supplied generic response shapes. Per-action evidence and unverified operations are tracked in the [Collections action coverage matrix](collections-api-coverage.md). SPLITSHARD and MIGRATE have opt-in, loopback-only runtime checks in the manually dispatched workflow; they remain unverified until that workflow passes. Other unmodelled actions include FORCELEADER and selected cluster/node management commands. | | Security API `/solr/admin/authentication`, `/solr/admin/authorization` | `security()` | Authentication and authorization requests; dedicated security-enabled integration coverage is available on all three pinned versions. | | Ping `/admin/ping` | `ping()` | Typed client method; response status tested on all three pinned versions. | | System information `/solr/admin/info/system` | `systemInfo()` | Typed client method; representative response integration-tested on all three pinned versions. | diff --git a/test/integration/all.test.ts b/test/integration/all.test.ts index 96775af..be2a43a 100644 --- a/test/integration/all.test.ts +++ b/test/integration/all.test.ts @@ -1,4 +1,5 @@ import "./solr-collections.test.js"; +import "./solrcloud-split-migrate-optin.test.js"; import "./solr-config-security.test.js"; import "./solr-config-operations.test.js"; import "./solr-core-lifecycle.test.js"; diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index 0b8d7dd..2bceb86 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -234,9 +234,11 @@ export function integrationTest( export function cloudIntegrationTest( name: string, run: (fixture: SolrCloudIntegrationFixture) => Promise, - requiresMultipleNodes = false + requiresMultipleNodes = false, + requiresDestructiveOptIn = false ): void { - test(name, { skip: !baseUrl || !cloudEnabled || (requiresMultipleNodes && process.env.SOLR_MULTINODE !== "true") }, async () => { + const localEndpoint = baseUrl ? ["localhost", "127.0.0.1", "::1"].includes(new URL(baseUrl).hostname) : false; + test(name, { skip: !baseUrl || !cloudEnabled || (requiresMultipleNodes && process.env.SOLR_MULTINODE !== "true") || (requiresDestructiveOptIn && (process.env.SOLR_DESTRUCTIVE_OPT_IN !== "true" || !localEndpoint)) }, async () => { const client = new SolrClient({ baseUrl: baseUrl!, timeoutMs: 30_000 }); const createdCollections = new Set(); const createdAliases = new Set(); diff --git a/test/integration/solrcloud-split-migrate-optin.test.ts b/test/integration/solrcloud-split-migrate-optin.test.ts new file mode 100644 index 0000000..84c683b --- /dev/null +++ b/test/integration/solrcloud-split-migrate-optin.test.ts @@ -0,0 +1,103 @@ +import assert from "node:assert/strict"; +import { randomUUID } from "node:crypto"; +import { cloudIntegrationTest } from "./fixtures.js"; + +const routeKey = "tenant!"; + +async function waitForCollectionRequest(client: import("../../src/index.js").SolrClient, requestId: string): Promise { + for (let attempt = 0; attempt < 180; attempt += 1) { + const result = await client.collections("REQUESTSTATUS", { requestid: requestId }); + const state = result.status?.state; + if (state === "completed") { + await client.collections("DELETESTATUS", { requestid: requestId }); + return; + } + if (state === "failed" || state === "notfound") { + throw new Error(`Collection request ${requestId} ended in ${state}: ${JSON.stringify(result)}`); + } + await new Promise(resolve => setTimeout(resolve, 1_000)); + } + throw new Error(`Collection request ${requestId} did not complete within 180 seconds`); +} + +cloudIntegrationTest( + "SPLITSHARD splits an isolated compositeId collection and preserves routed documents", + async ({ client, createCollection, clientForCollection }) => { + const collection = await createCollection("integration_splitshard_optin"); + const statusBefore = await client.collections("CLUSTERSTATUS", { collection }); + const liveNodes = statusBefore.cluster?.live_nodes ?? []; + assert.ok(liveNodes.length >= 2, "opt-in verification requires at least two live SolrCloud nodes"); + const beforeShards = statusBefore.cluster?.collections?.[collection]?.shards ?? {}; + assert.equal(Object.keys(beforeShards).length, 1, "new disposable collection must start with exactly one shard"); + + const source = clientForCollection(collection); + const ids = [1, 2, 3].map(index => `${routeKey}split-${index}`); + await source.add(ids.map(id => ({ id, title: "splitshard opt-in verification" }))); + await source.commit({ waitSearcher: true }); + + const splitRequestId = `split-${randomUUID()}`; + const split = await client.collections("SPLITSHARD", { + collection, + "split.key": routeKey, + async: splitRequestId + }); + assert.equal(split.responseHeader?.status, 0, "SPLITSHARD should be accepted"); + await waitForCollectionRequest(client, splitRequestId); + + const statusAfter = await client.collections("CLUSTERSTATUS", { collection }); + const shardsAfter = statusAfter.cluster?.collections?.[collection]?.shards ?? {}; + assert.ok(Object.keys(shardsAfter).length >= 2, "cluster state should expose the split child shards"); + assert.ok( + Object.values(shardsAfter).filter(shard => shard.state === "active").length >= 2, + "at least two child shards should be active" + ); + const result = await source.query<{ response?: { numFound?: number } }>({ query: "*:*", fields: ["id"], limit: 10 }); + assert.equal(result.response?.numFound, ids.length, "all documents should remain searchable after the split"); + }, + true, + true +); + +cloudIntegrationTest( + "MIGRATE moves an isolated compositeId routing prefix into a disposable target collection", + async ({ client, createCollection, clientForCollection }) => { + const sourceName = await createCollection("integration_migrate_source_optin"); + const targetName = await createCollection("integration_migrate_target_optin"); + const status = await client.collections("CLUSTERSTATUS"); + assert.ok((status.cluster?.live_nodes?.length ?? 0) >= 2, "opt-in verification requires at least two live SolrCloud nodes"); + const source = clientForCollection(sourceName); + const target = clientForCollection(targetName); + const ids = [1, 2, 3].map(index => `${routeKey}migrate-${index}`); + + await source.add(ids.map(id => ({ id, title: "migration opt-in verification" }))); + await source.commit({ waitSearcher: true }); + + const targetBefore = await target.query<{ response?: { numFound?: number } }>({ query: "*:*", fields: ["id"], limit: 10 }); + assert.equal(targetBefore.response?.numFound, 0, "migration target must be empty before mutation"); + + const migrationRequestId = `migrate-${randomUUID()}`; + const migrated = await client.collections("MIGRATE", { + collection: sourceName, + "split.key": routeKey, + "target.collection": targetName, + "forward.timeout": 120, + async: migrationRequestId + }); + assert.equal(migrated.responseHeader?.status, 0, "MIGRATE should be accepted"); + await waitForCollectionRequest(client, migrationRequestId); + + let targetCount = 0; + for (let attempt = 0; attempt < 60; attempt += 1) { + const result = await target.query<{ response?: { numFound?: number } }>({ query: "*:*", fields: ["id"], limit: 10 }); + targetCount = result.response?.numFound ?? 0; + if (targetCount === ids.length) break; + await new Promise(resolve => setTimeout(resolve, 1_000)); + } + assert.equal(targetCount, ids.length, "target collection should contain every migrated routing-key document"); + const targetReadBack = await target.query<{ response?: { docs?: Array<{ id: string }> } }>({ query: "*:*", fields: ["id"], limit: 10 }); + const foundIds = new Set(targetReadBack.response?.docs?.map(document => String(document.id)) ?? []); + for (const id of ids) assert.ok(foundIds.has(id), `target collection should contain migrated document ${id}`); + }, + true, + true +); From f7914483957b0b2a083a3b4f2632570232e51890 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:40:55 +0100 Subject: [PATCH 13/62] docs(solrcloud): clarify opt-in runtime certification boundary --- docs/administration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/administration.md b/docs/administration.md index 2156c2e..a97f5f3 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -175,4 +175,4 @@ Run it from the GitHub Actions **CI** workflow using **Run workflow** and enable The tests use asynchronous request IDs and poll `REQUESTSTATUS` for up to 180 seconds, then remove completed status records. They verify active child-shard state and post-split document visibility, and verify that every routed document is searchable in the migration target. They do not claim to test concurrent writes, rollback of a partially completed operation, or recovery after a cluster/node failure. -If a request times out or fails, do not rerun against a reused cluster. Preserve the CI job logs, inspect `CLUSTERSTATUS` and `REQUESTSTATUS`, and discard the ephemeral cluster. Fixture cleanup deletes only the collections it created; if Solr still has an operation in flight and refuses deletion, destroy the ephemeral cluster rather than attempting cleanup against a shared server. The suite is an opt-in verification harness, not permission to point these destructive actions at a user-owned cluster. +If a request times out or fails, do not rerun against a reused cluster. Preserve the CI job logs, inspect `CLUSTERSTATUS` and `REQUESTSTATUS`, and discard the ephemeral cluster. Fixture cleanup deletes only the collections it created; if Solr still has an operation in flight and refuses deletion, destroy the ephemeral cluster rather than attempting cleanup against a shared server. The suite is an opt-in verification harness, not permission to point these destructive actions at a user-owned cluster. A workflow that is skipped or has not been manually dispatched is not runtime evidence; record successful Solr versions individually before updating compatibility status. From 483c41c18d662264f6930e34bdb6dfebdd3f688e Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:41:51 +0100 Subject: [PATCH 14/62] test(replication): allow local leader URLs across Solr versions --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e685bc4..270ccf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: --volume "$RUNNER_TEMP/solr-backups:/var/solr/backups" \ --publish 8983:8983 \ --env SOLR_JAVA_MEM="-Xms512m -Xmx512m" \ - --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups -Dsolr.disable.shardsWhitelist=true -Dsolr.security.allow.urls.enabled=false" \ + --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups -Dsolr.disable.shardsWhitelist=true -Dsolr.disable.allowUrls=true -Dsolr.security.allow.urls.enabled=false" \ "solr:${{ matrix.solr-version }}" \ solr-precreate integration - name: Wait for Solr and integration core @@ -134,7 +134,7 @@ jobs: --volume "$RUNNER_TEMP/solr-backups:/var/solr/backups" \ --publish 8983:8983 \ --env SOLR_JAVA_MEM="-Xms512m -Xmx512m" \ - --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups -Dsolr.disable.shardsWhitelist=true -Dsolr.security.allow.urls.enabled=false" \ + --env SOLR_OPTS="-Dsolr.allowPaths=/var/solr/backups -Dsolr.disable.shardsWhitelist=true -Dsolr.disable.allowUrls=true -Dsolr.security.allow.urls.enabled=false" \ --entrypoint /bin/bash \ "solr:${{ matrix.solr-version }}" \ -lc 'mkdir -p /var/solr/data && cp /tmp/security.json /var/solr/data/security.json && chmod 666 /var/solr/data/security.json && exec docker-entrypoint.sh solr-precreate integration' From 3e99948f32fa3fe73c4ecc9765b36ba77d5cbc9b Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:46:57 +0100 Subject: [PATCH 15/62] test(replication): enable qt dispatch for internal leader fetch --- test/integration/solr-replication-lifecycle.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index fee2bf0..a56f06b 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -22,20 +22,24 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}/replication`; const documentId = createId("replication_lifecycle"); - await leader.config({ + const leaderConfig = await leader.config({ "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", + handleSelect: true, customOptions: { leader: { replicateAfter: "commit" } } } }); - await follower.config({ + assert.equal(leaderConfig.responseHeader?.status, 0); + const followerConfig = await follower.config({ "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", + handleSelect: true, customOptions: { follower: { leaderUrl, ...(username && password ? { httpBasicAuthUser: username, httpBasicAuthPassword: password } : {}) } } } }); + assert.equal(followerConfig.responseHeader?.status, 0); await leader.add([{ id: documentId, title: "leader follower lifecycle" }]); await leader.commit({ waitSearcher: true }); From 769b5154e94fe999ebe7c274aa3b68389af5205d Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:47:52 +0100 Subject: [PATCH 16/62] test(replication): use version-specific leader URL form --- test/integration/solr-replication-lifecycle.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index a56f06b..23ae073 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -19,7 +19,9 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso }); const leader = makeClient(leaderCore); const follower = makeClient(followerCore); - const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}/replication`; + const solrMajor = Number(process.env.SOLR_VERSION?.split(".")[0] ?? "0"); + // Solr 8 expects the handler URL; Solr 9+ appends /replication internally. + const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}${solrMajor === 8 ? "/replication" : ""}`; const documentId = createId("replication_lifecycle"); const leaderConfig = await leader.config({ From b83f953fdd99d4396563b03e548b97597ba5a6ad Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:48:14 +0100 Subject: [PATCH 17/62] docs(replication): clarify version-specific leader URL form (docs/replication.md) --- docs/replication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/replication.md b/docs/replication.md index 8ebb459..b0d2a5b 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -46,7 +46,7 @@ The `indexversion` response includes the current `generation` on newer Solr vers // Solr 10 uses fetchindex/abortfetch. Solr 8 and 9 use the legacy snappull/abort names. // leaderUrl may be omitted when configured on the follower. await client.replication("fetchindex", { - leaderUrl: "http://leader:8983/solr/catalog/replication", + leaderUrl: "http://leader:8983/solr/catalog", compression: "internal", httpConnTimeout: 5_000, httpReadTimeout: 10_000 @@ -59,7 +59,7 @@ await client.replication("enablepoll"); await client.replication("abortfetch"); ``` -`fetchindex` accepts the documented follower connection and compression settings. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. +`fetchindex` accepts the documented follower connection and compression settings. Solr 9+ expects `leaderUrl` as the core URL without the `/replication` suffix; Solr 8.11 expects the full handler URL. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. ## Control a leader From 9f9333d5f7dfbb61542aac30852e4fd54b826304 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:48:17 +0100 Subject: [PATCH 18/62] docs(replication): clarify version-specific leader URL form (test/replication-api-types.ts) --- test/replication-api-types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/replication-api-types.ts b/test/replication-api-types.ts index d5fb558..138d1e6 100644 --- a/test/replication-api-types.ts +++ b/test/replication-api-types.ts @@ -6,13 +6,13 @@ void client.replication("indexversion"); void client.replication("details"); void client.replication("filelist", { generation: 12 }); void client.replication("filelist", { indexversion: 12 }); -void client.replication("fetchindex", { leaderUrl: "http://leader:8983/solr/catalog/replication", compression: "internal" }); +void client.replication("fetchindex", { leaderUrl: "http://leader:8983/solr/catalog", compression: "internal" }); void client.replication("enablereplication"); void client.replication("disablereplication"); void client.replication("enablepoll"); void client.replication("disablepoll"); void client.replication("abortfetch"); -void client.replication("snappull", { leaderUrl: "http://leader:8983/solr/catalog/replication" }); +void client.replication("snappull", { leaderUrl: "http://leader:8983/solr/catalog" }); void client.replication("abort"); void client.replicationFile({ directoryType: "cf", filePath: "managed-schema", checksum: true }); From 8cb3bb671ab8e31297e19821a57c4f68df569e8c Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:48:19 +0100 Subject: [PATCH 19/62] docs(replication): clarify version-specific leader URL form (test/replication.test.ts) --- test/replication.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/replication.test.ts b/test/replication.test.ts index 1165174..9046b8e 100644 --- a/test/replication.test.ts +++ b/test/replication.test.ts @@ -14,7 +14,7 @@ test("ReplicationHandler commands use the configured core and JSON response form await client.replication("indexversion"); await client.replication("fetchindex", { - leaderUrl: "http://leader:8983/solr/catalog/replication", + leaderUrl: "http://leader:8983/solr/catalog", compression: "internal", httpConnTimeout: 5000, customParams: { maxWriteMBPerSec: 20 } @@ -27,7 +27,7 @@ test("ReplicationHandler commands use the configured core and JSON response form assert.equal(calls[0]!.url.searchParams.get("command"), "indexversion"); assert.equal(calls[0]!.url.searchParams.get("wt"), "json"); assert.equal(calls[1]!.url.searchParams.get("command"), "fetchindex"); - assert.equal(calls[1]!.url.searchParams.get("leaderUrl"), "http://leader:8983/solr/catalog/replication"); + assert.equal(calls[1]!.url.searchParams.get("leaderUrl"), "http://leader:8983/solr/catalog"); assert.equal(calls[1]!.url.searchParams.get("compression"), "internal"); assert.equal(calls[1]!.url.searchParams.get("httpConnTimeout"), "5000"); assert.equal(calls[1]!.url.searchParams.get("maxWriteMBPerSec"), "20"); @@ -54,7 +54,7 @@ test("ReplicationHandler supports read, poll, and replication lifecycle commands await client.replication("enablepoll"); await client.replication("disablepoll"); await client.replication("abortfetch"); - await client.replication("snappull", { leaderUrl: "http://leader:8983/solr/catalog/replication" }); + await client.replication("snappull", { leaderUrl: "http://leader:8983/solr/catalog" }); await client.replication("abort"); assert.deepEqual(commands, [ From 3338c6bbf0348b50c62b2cb79e159d5889d92906 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:49:32 +0100 Subject: [PATCH 20/62] types(config): expose request-handler handleSelect (src/types.ts) --- src/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types.ts b/src/types.ts index 249a2fd..28807a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1102,6 +1102,8 @@ export interface SolrConfigPlugin { customOptions?: Readonly>; } export interface SolrRequestHandlerConfig extends SolrConfigPlugin { + /** Allow the handler to be selected through the legacy `qt` request parameter. */ + handleSelect?: boolean; defaults?: SolrConfigRequestHandlerParams; appends?: SolrConfigRequestHandlerParams; invariants?: SolrConfigRequestHandlerParams; From 09cf69b833f927fea103ee511a2030d104469e61 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:49:36 +0100 Subject: [PATCH 21/62] types(config): expose request-handler handleSelect (test/admin-api-types.ts) --- test/admin-api-types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/admin-api-types.ts b/test/admin-api-types.ts index 24cb2cb..ce7814c 100644 --- a/test/admin-api-types.ts +++ b/test/admin-api-types.ts @@ -192,6 +192,7 @@ const invalidConfigValue: SolrConfigCommand = { "set-property": { "query.maxBool // @ts-expect-error unset-property only accepts known common Config API property names. const invalidUnsetProperty: SolrConfigCommand = { "unset-property": "query.notARealSetting" }; void [invalidConfigProperty, invalidConfigValue, invalidUnsetProperty]; +const handleSelectHandler: SolrConfigCommand = { "add-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", handleSelect: true } }; const addHandler: SolrConfigCommand = { "add-requesthandler": { name: "/select", @@ -214,6 +215,7 @@ const addInitParams: SolrConfigCommand = { "add-initparams": { name: "shared-search-params", path: ["/select", "/query"], defaults: { rows: 20, wt: "json" } } }; void client.config(setProperty); +void client.config(handleSelectHandler); void client.config(addHandler); void client.config(addListener); void client.config(addInitParams); From cf7d70e9196317a3abf0082ae65b2cac401de2a4 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:49:38 +0100 Subject: [PATCH 22/62] types(config): expose request-handler handleSelect (docs/config.md) --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index ad65901..e1b53f6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -17,7 +17,7 @@ await solr.config({ }); ``` -Typed command families include set/unset properties, request handlers, search components, query response writers, init params, query parsers, value-source parsers, transformers, update processors, query converters, listeners, and expressible components. `SolrRequestHandlerConfig` exposes typed `defaults`, `appends`, and `invariants` through `SolrConfigRequestHandlerParams`; common request parameters such as `q`, `fq`, `fl`, `rows`, `start`, `sort`, `wt`, `echoParams`, and time limits receive value-aware autocomplete, while the parameter map remains open for Solr extensions. Read models distinguish request handlers, search components, response writers, listeners, and init-params sets; response writers expose `contentType`/`charset`, and listeners expose standard event names with custom plugin event names still accepted. Cache settings are configured through `set-property` (for example, `query.filterCache.size`), not through a separate cache command. Search-component option maps and other plugin-defined component settings remain open; `customOptions` is the explicit flattened escape hatch for plugin-specific command properties. The library does not claim to validate every plugin's schema. Check the documentation for the target Solr version before applying configuration changes. +Typed command families include set/unset properties, request handlers, search components, query response writers, init params, query parsers, value-source parsers, transformers, update processors, query converters, listeners, and expressible components. `SolrRequestHandlerConfig` exposes typed `handleSelect` plus `defaults`, `appends`, and `invariants` through `SolrConfigRequestHandlerParams`; common request parameters such as `q`, `fq`, `fl`, `rows`, `start`, `sort`, `wt`, `echoParams`, and time limits receive value-aware autocomplete, while the parameter map remains open for Solr extensions. Read models distinguish request handlers, search components, response writers, listeners, and init-params sets; response writers expose `contentType`/`charset`, and listeners expose standard event names with custom plugin event names still accepted. Cache settings are configured through `set-property` (for example, `query.filterCache.size`), not through a separate cache command. Search-component option maps and other plugin-defined component settings remain open; `customOptions` is the explicit flattened escape hatch for plugin-specific command properties. The library does not claim to validate every plugin's schema. Check the documentation for the target Solr version before applying configuration changes. ## Common properties and response types From 7b1415c6ed54a4613907b138eee476c3bc33403c Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:53:06 +0100 Subject: [PATCH 23/62] docs(solrcloud): require per-version workflow evidence --- docs/administration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/administration.md b/docs/administration.md index a97f5f3..a1c1a9a 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -175,4 +175,4 @@ Run it from the GitHub Actions **CI** workflow using **Run workflow** and enable The tests use asynchronous request IDs and poll `REQUESTSTATUS` for up to 180 seconds, then remove completed status records. They verify active child-shard state and post-split document visibility, and verify that every routed document is searchable in the migration target. They do not claim to test concurrent writes, rollback of a partially completed operation, or recovery after a cluster/node failure. -If a request times out or fails, do not rerun against a reused cluster. Preserve the CI job logs, inspect `CLUSTERSTATUS` and `REQUESTSTATUS`, and discard the ephemeral cluster. Fixture cleanup deletes only the collections it created; if Solr still has an operation in flight and refuses deletion, destroy the ephemeral cluster rather than attempting cleanup against a shared server. The suite is an opt-in verification harness, not permission to point these destructive actions at a user-owned cluster. A workflow that is skipped or has not been manually dispatched is not runtime evidence; record successful Solr versions individually before updating compatibility status. +If a request times out or fails, do not rerun against a reused cluster. Preserve the CI job logs, inspect `CLUSTERSTATUS` and `REQUESTSTATUS`, and discard the ephemeral cluster. Fixture cleanup deletes only the collections it created; if Solr still has an operation in flight and refuses deletion, destroy the ephemeral cluster rather than attempting cleanup against a shared server. The suite is an opt-in verification harness, not permission to point these destructive actions at a user-owned cluster. A workflow that is skipped or has not been manually dispatched is not runtime evidence; record successful Solr versions individually before updating compatibility status. Keep the workflow run URL and tested Solr version alongside the compatibility record. From b3db2975a7088ea3baf13bdfc5be6bf36b1254ac Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:54:09 +0100 Subject: [PATCH 24/62] ci: fix duplicate jobs key in opt-in workflow --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 270ccf5..483d52f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,6 @@ on: required: false default: false type: boolean -jobs: - test: jobs: test: strategy: From 150ceb7d411637eafd2eb751afc8b5111d76f3d0 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:54:56 +0100 Subject: [PATCH 25/62] test(replication): reuse numeric Solr major for fetch command --- test/integration/solr-replication-lifecycle.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 23ae073..13bc594 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -51,8 +51,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const leaderGeneration = leaderVersion.generation ?? Number(leaderVersion.indexversion); assert.ok(Number.isFinite(leaderGeneration) && leaderGeneration > 0, "leader must publish a replicable index"); - const solrMajor = process.env.SOLR_VERSION?.split(".")[0]; - const fetchCommand = solrMajor === "10" ? "fetchindex" : "snappull"; + const fetchCommand = solrMajor >= 10 ? "fetchindex" : "snappull"; const fetchResponse = await follower.replication(fetchCommand, { leaderUrl }); assert.equal(fetchResponse.responseHeader?.status, 0, `${fetchCommand} should complete successfully`); From 648617d06597f0ba29035675f146a77d4402de50 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:56:46 +0100 Subject: [PATCH 26/62] test(replication): configure leader commit replication trigger --- test/integration/solr-replication-lifecycle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 13bc594..4793309 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -29,7 +29,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso name: "/replication", class: "solr.ReplicationHandler", handleSelect: true, - customOptions: { leader: { replicateAfter: "commit" } } + customOptions: { leader: { replicateAfter: ["commit"] } } } }); assert.equal(leaderConfig.responseHeader?.status, 0); From 9c15350e703430bceeec246a3cf794fdf8b47153 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:57:39 +0100 Subject: [PATCH 27/62] test(replication): use persisted follower credentials for fetch --- test/integration/solr-replication-lifecycle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 4793309..9b45c6f 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -52,7 +52,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso assert.ok(Number.isFinite(leaderGeneration) && leaderGeneration > 0, "leader must publish a replicable index"); const fetchCommand = solrMajor >= 10 ? "fetchindex" : "snappull"; - const fetchResponse = await follower.replication(fetchCommand, { leaderUrl }); + const fetchResponse = await follower.replication(fetchCommand); assert.equal(fetchResponse.responseHeader?.status, 0, `${fetchCommand} should complete successfully`); // Replication copies Lucene index files asynchronously; assert the resulting From 7947995f0d8f58b9b157eb10feb9097c3b8d56ef Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 08:05:34 +0100 Subject: [PATCH 28/62] test(replication): enable qt routing on leader select handler --- test/integration/solr-replication-lifecycle.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 9b45c6f..67980b5 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -28,16 +28,18 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", - handleSelect: true, customOptions: { leader: { replicateAfter: ["commit"] } } } }); assert.equal(leaderConfig.responseHeader?.status, 0); + const selectConfig = await leader.config({ + "update-requesthandler": { name: "/select", handleSelect: true } + }); + assert.equal(selectConfig.responseHeader?.status, 0); const followerConfig = await follower.config({ "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", - handleSelect: true, customOptions: { follower: { leaderUrl, ...(username && password ? { httpBasicAuthUser: username, httpBasicAuthPassword: password } : {}) } } } }); From 74a083ad1d29dd03d12d8324f92529bb4c8ed392 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 08:05:52 +0100 Subject: [PATCH 29/62] docs(replication): explain qt routing prerequisite --- docs/replication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/replication.md b/docs/replication.md index b0d2a5b..c1e87d0 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -59,7 +59,7 @@ await client.replication("enablepoll"); await client.replication("abortfetch"); ``` -`fetchindex` accepts the documented follower connection and compression settings. Solr 9+ expects `leaderUrl` as the core URL without the `/replication` suffix; Solr 8.11 expects the full handler URL. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. +`fetchindex` accepts the documented follower connection and compression settings. Solr 9+ expects `leaderUrl` as the core URL without the `/replication` suffix; Solr 8.11 expects the full handler URL. The follower's IndexFetcher requests leader details through `qt=/replication`; if the leader's `/select` handler has `handleSelect` disabled, enable it so that request can be routed to the replication handler. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. ## Control a leader From 709ab96a01e87d661a6b12b3e7ce2740b004cf88 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 08:06:59 +0100 Subject: [PATCH 30/62] test(replication): retain SearchHandler class when enabling qt dispatch --- test/integration/solr-replication-lifecycle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 67980b5..19fbfc1 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -33,7 +33,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso }); assert.equal(leaderConfig.responseHeader?.status, 0); const selectConfig = await leader.config({ - "update-requesthandler": { name: "/select", handleSelect: true } + "update-requesthandler": { name: "/select", class: "solr.SearchHandler", handleSelect: true } }); assert.equal(selectConfig.responseHeader?.status, 0); const followerConfig = await follower.config({ From 9f9493c1cee9a96e51dab3207c0c9ac28e09a7b2 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 11:46:26 +0100 Subject: [PATCH 31/62] test(replication): register handlers on isolated cores --- test/integration/solr-replication-lifecycle.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 19fbfc1..5791158 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -25,7 +25,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const documentId = createId("replication_lifecycle"); const leaderConfig = await leader.config({ - "update-requesthandler": { + "add-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", customOptions: { leader: { replicateAfter: ["commit"] } } @@ -37,7 +37,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso }); assert.equal(selectConfig.responseHeader?.status, 0); const followerConfig = await follower.config({ - "update-requesthandler": { + "add-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", customOptions: { follower: { leaderUrl, ...(username && password ? { httpBasicAuthUser: username, httpBasicAuthPassword: password } : {}) } } From be1c37ff8cba340e93e3a9a6cc4c97cc5f4c7add Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 11:47:24 +0100 Subject: [PATCH 32/62] test(replication): enable legacy qt routing on replication handler --- test/integration/solr-replication-lifecycle.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 5791158..9b45c6f 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -25,21 +25,19 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const documentId = createId("replication_lifecycle"); const leaderConfig = await leader.config({ - "add-requesthandler": { + "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", + handleSelect: true, customOptions: { leader: { replicateAfter: ["commit"] } } } }); assert.equal(leaderConfig.responseHeader?.status, 0); - const selectConfig = await leader.config({ - "update-requesthandler": { name: "/select", class: "solr.SearchHandler", handleSelect: true } - }); - assert.equal(selectConfig.responseHeader?.status, 0); const followerConfig = await follower.config({ - "add-requesthandler": { + "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", + handleSelect: true, customOptions: { follower: { leaderUrl, ...(username && password ? { httpBasicAuthUser: username, httpBasicAuthPassword: password } : {}) } } } }); From 116a310b0ec28385ca50341491759865415bb7a3 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 11:57:05 +0100 Subject: [PATCH 33/62] docs: keep replication lifecycle evidence pending CI From bbd1c0b87a898b76be8bb451d22b1730956b6af8 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 11:57:08 +0100 Subject: [PATCH 34/62] docs: qualify pending replication lifecycle verification --- docs/compatibility.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index f12d501..d392369 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -38,7 +38,7 @@ The 8.11, 9.11, and 10.1 guides describe the API families below. The Replica Man | `security()` — authentication and authorization handlers | Documented | Documented | Documented | Tested: authenticated BasicAuth/RuleBasedAuthorization reads, anonymous 401 on both handlers, authentication `set-property` read-back, and malformed JSON rejection on pinned 8.11.4 / 9.11.0 / 10.1.0 images. Unit tests verify Basic/Bearer credentials are not exposed by client HTTP errors | | `stream()` — `/stream` | Documented | Documented | Documented | Tested: Streaming Expressions search against a disposable SolrCloud collection on pinned 8.11.4 / 9.11.0 / 10.1.0 images (CI #145) | | `export()` — `/export` | Documented | Documented | Documented | Tested: raw response with docValues sort field on pinned 8.11.4 / 9.11.0 / 10.1.0 images | -| `replication()` / `replicationFile()` — `/solr/{core}/replication` | Documented | Documented | Documented | Tested: indexversion, details, filelist (generation on newer versions / legacy indexversion on older versions), streamed filecontent, enablereplication/disablereplication, core backup, restorestatus, restore, deletebackup, and a disposable two-core leader/follower fetch with follower query read-back on pinned 8.11.4 / 9.11.0 / 10.1.0 images. The fetch uses snappull on Solr 8/9 and fetchindex on Solr 10. Abort and polling lifecycle behavior remains unit-only | +| `replication()` / `replicationFile()` — `/solr/{core}/replication` | Documented | Documented | Documented | Tested: indexversion, details, filelist (generation on newer versions / legacy indexversion on older versions), streamed filecontent, enablereplication/disablereplication, core backup, restorestatus, restore, and deletebackup on pinned 8.11.4 / 9.11.0 / 10.1.0 images. A disposable two-core leader/follower fetch with follower query read-back is added as an opt-in check and remains pending runtime verification until the manual workflow passes on each pinned version; it uses snappull on Solr 8/9 and fetchindex on Solr 10. Abort and polling lifecycle behavior remains unit-only | | Core and collection backup/restore | `replication("backup" | "restore" | "restorestatus" | "deletebackup")`, `collections("BACKUP" | "RESTORE" | "LISTBACKUP" | "DELETEBACKUP" | "REQUESTSTATUS" | "DELETESTATUS")` | Documented | Documented | Documented | Tested: full core backup/restore/read-back and SolrCloud collection backup/list/restore/read-back/delete with asynchronous status polling on pinned 8.11.4 / 9.11.0 / 10.1.0 images using local filesystem repositories and shared SolrCloud storage | | `request()` / `requestRaw()` — generic caller-defined transport | Depends on the selected endpoint | Depends on the selected endpoint | Depends on the selected endpoint | Generic transport behavior has unit tests only; no endpoint-independent live Solr integration claim is made. Each custom endpoint remains unverified until tested individually | From 53cd4a504b3e56e13b04ec8655dd3e3523a28894 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 11:57:10 +0100 Subject: [PATCH 35/62] docs: document replication lifecycle opt-in limits --- docs/replication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/replication.md b/docs/replication.md index c1e87d0..fe63ecb 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -103,7 +103,7 @@ Supported directory types are `file` (Lucene index files), `cf` (configuration f - The client currently exposes the V1 handler path. Solr 10.1 also documents V2 equivalents for selected file and index-version operations; no dedicated V2 wrapper is provided. - The handler may not be configured for user-managed leader/follower replication on every core. The server must have the appropriate ReplicationHandler configuration before control commands can be used. -- The pinned integration matrix creates disposable leader and follower cores, commits a unique leader document, triggers `snappull` on Solr 8/9 or `fetchindex` on Solr 10, and verifies that the document becomes searchable on the follower. This verifies a complete fetch/read-back cycle, not polling or abort of an in-progress transfer. -- `abort` (Solr 8/9) and `abortfetch` (Solr 10), plus `enablepoll`/`disablepoll`, have request-serialization unit coverage; cancellation of a live transfer remains unverified. +- An opt-in integration test creates disposable leader and follower cores, commits a unique leader document, triggers `snappull` on Solr 8/9 or `fetchindex` on Solr 10, and verifies that the document becomes searchable on the follower. Treat this behavior as pending runtime verification until the pinned Solr 8.11.4, 9.11.0, and 10.1.0 CI jobs pass. The test does not cover polling or abort of an in-progress transfer. +- `abort` (Solr 8/9) and `abortfetch` (Solr 10), plus `enablepoll`/`disablepoll`, have request-serialization unit coverage; cancellation of a live transfer remains unverified. The disposable-core fixture unloads both cores in its `finally` cleanup. If a CI job is cancelled or the fetch does not settle, the ephemeral Solr container is discarded rather than reusing the cores. See the [Solr 8.11](https://solr.apache.org/__root/docs.solr.apache.org/guide/8_11/deployment-guide/user-managed-index-replication.html), [Solr 9.11](https://solr.apache.org/guide/solr/9_11/deployment-guide/user-managed-index-replication.html), and [Solr 10.1](https://solr.apache.org/guide/solr/10_1/deployment-guide/user-managed-index-replication.html) guides for server configuration and command semantics. From 39d24e8aa4a47a963c7b356189f92193986a85b7 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 11:59:41 +0100 Subject: [PATCH 36/62] test(replication): pass explicit leader handler URL and abort stalled fetch --- .../solr-replication-lifecycle.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 9b45c6f..74339e4 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -21,7 +21,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const follower = makeClient(followerCore); const solrMajor = Number(process.env.SOLR_VERSION?.split(".")[0] ?? "0"); // Solr 8 expects the handler URL; Solr 9+ appends /replication internally. - const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}${solrMajor === 8 ? "/replication" : ""}`; + const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}/replication`; const documentId = createId("replication_lifecycle"); const leaderConfig = await leader.config({ @@ -52,21 +52,26 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso assert.ok(Number.isFinite(leaderGeneration) && leaderGeneration > 0, "leader must publish a replicable index"); const fetchCommand = solrMajor >= 10 ? "fetchindex" : "snappull"; - const fetchResponse = await follower.replication(fetchCommand); + const fetchResponse = await follower.replication(fetchCommand, { leaderUrl }); assert.equal(fetchResponse.responseHeader?.status, 0, `${fetchCommand} should complete successfully`); // Replication copies Lucene index files asynchronously; assert the resulting // searchable state rather than treating the command's HTTP response as proof. let found = false; - for (let attempt = 0; attempt < 60; attempt += 1) { + for (let attempt = 0; attempt < 120; attempt += 1) { const result = await follower.query<{ response?: { numFound?: number } }>({ query: `id:"${documentId}"`, fields: ["id"], limit: 1 }); if ((result.response?.numFound ?? 0) === 1) { found = true; break; } - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise(resolve => setTimeout(resolve, 1_000)); + } + if (!found) { + const details = await follower.replication("details"); + const abortCommand = solrMajor >= 10 ? "abortfetch" : "abort"; + await follower.replication(abortCommand).catch(() => undefined); + assert.fail(`follower did not expose the leader document after fetch; details: ${JSON.stringify(details)}`); } - assert.ok(found, "follower should expose the leader document after the fetch completes"); const followerVersion = await follower.replication("indexversion"); assert.equal(followerVersion.responseHeader?.status, 0); From 6669a962e175c370be3662c008875fc04190df49 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 11:59:43 +0100 Subject: [PATCH 37/62] docs(replication): use full leader handler URL --- docs/replication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/replication.md b/docs/replication.md index fe63ecb..30aa898 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -59,7 +59,7 @@ await client.replication("enablepoll"); await client.replication("abortfetch"); ``` -`fetchindex` accepts the documented follower connection and compression settings. Solr 9+ expects `leaderUrl` as the core URL without the `/replication` suffix; Solr 8.11 expects the full handler URL. The follower's IndexFetcher requests leader details through `qt=/replication`; if the leader's `/select` handler has `handleSelect` disabled, enable it so that request can be routed to the replication handler. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. +`fetchindex` accepts the documented follower connection and compression settings. `leaderUrl` is the fully qualified ReplicationHandler URL, including the `/replication` suffix, on the pinned Solr 8, 9, and 10 lines. The follower's IndexFetcher requests leader details through `qt=/replication`; if the leader's `/select` handler has `handleSelect` disabled, enable it so that request can be routed to the replication handler. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. ## Control a leader @@ -103,7 +103,7 @@ Supported directory types are `file` (Lucene index files), `cf` (configuration f - The client currently exposes the V1 handler path. Solr 10.1 also documents V2 equivalents for selected file and index-version operations; no dedicated V2 wrapper is provided. - The handler may not be configured for user-managed leader/follower replication on every core. The server must have the appropriate ReplicationHandler configuration before control commands can be used. -- An opt-in integration test creates disposable leader and follower cores, commits a unique leader document, triggers `snappull` on Solr 8/9 or `fetchindex` on Solr 10, and verifies that the document becomes searchable on the follower. Treat this behavior as pending runtime verification until the pinned Solr 8.11.4, 9.11.0, and 10.1.0 CI jobs pass. The test does not cover polling or abort of an in-progress transfer. +- An opt-in integration test creates disposable leader and follower cores, commits a unique leader document, triggers `snappull` on Solr 8/9 or `fetchindex` on Solr 10 with the fully qualified leader handler URL, and verifies that the document becomes searchable on the follower. Treat this behavior as pending runtime verification until the pinned Solr 8.11.4, 9.11.0, and 10.1.0 CI jobs pass. The test does not cover polling or abort of an in-progress transfer. - `abort` (Solr 8/9) and `abortfetch` (Solr 10), plus `enablepoll`/`disablepoll`, have request-serialization unit coverage; cancellation of a live transfer remains unverified. The disposable-core fixture unloads both cores in its `finally` cleanup. If a CI job is cancelled or the fetch does not settle, the ephemeral Solr container is discarded rather than reusing the cores. See the [Solr 8.11](https://solr.apache.org/__root/docs.solr.apache.org/guide/8_11/deployment-guide/user-managed-index-replication.html), [Solr 9.11](https://solr.apache.org/guide/solr/9_11/deployment-guide/user-managed-index-replication.html), and [Solr 10.1](https://solr.apache.org/guide/solr/10_1/deployment-guide/user-managed-index-replication.html) guides for server configuration and command semantics. From ac14a67a493dd66d7d6e2d0ce9e6d35eb6d2476c Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 11:59:48 +0100 Subject: [PATCH 38/62] test(types): use full replication leader URL --- test/replication-api-types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/replication-api-types.ts b/test/replication-api-types.ts index 138d1e6..d5fb558 100644 --- a/test/replication-api-types.ts +++ b/test/replication-api-types.ts @@ -6,13 +6,13 @@ void client.replication("indexversion"); void client.replication("details"); void client.replication("filelist", { generation: 12 }); void client.replication("filelist", { indexversion: 12 }); -void client.replication("fetchindex", { leaderUrl: "http://leader:8983/solr/catalog", compression: "internal" }); +void client.replication("fetchindex", { leaderUrl: "http://leader:8983/solr/catalog/replication", compression: "internal" }); void client.replication("enablereplication"); void client.replication("disablereplication"); void client.replication("enablepoll"); void client.replication("disablepoll"); void client.replication("abortfetch"); -void client.replication("snappull", { leaderUrl: "http://leader:8983/solr/catalog" }); +void client.replication("snappull", { leaderUrl: "http://leader:8983/solr/catalog/replication" }); void client.replication("abort"); void client.replicationFile({ directoryType: "cf", filePath: "managed-schema", checksum: true }); From 922693944fde49275bb0528c8e8a84e916087700 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 11:59:52 +0100 Subject: [PATCH 39/62] test(replication): assert full leader handler URL --- test/replication.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/replication.test.ts b/test/replication.test.ts index 9046b8e..1165174 100644 --- a/test/replication.test.ts +++ b/test/replication.test.ts @@ -14,7 +14,7 @@ test("ReplicationHandler commands use the configured core and JSON response form await client.replication("indexversion"); await client.replication("fetchindex", { - leaderUrl: "http://leader:8983/solr/catalog", + leaderUrl: "http://leader:8983/solr/catalog/replication", compression: "internal", httpConnTimeout: 5000, customParams: { maxWriteMBPerSec: 20 } @@ -27,7 +27,7 @@ test("ReplicationHandler commands use the configured core and JSON response form assert.equal(calls[0]!.url.searchParams.get("command"), "indexversion"); assert.equal(calls[0]!.url.searchParams.get("wt"), "json"); assert.equal(calls[1]!.url.searchParams.get("command"), "fetchindex"); - assert.equal(calls[1]!.url.searchParams.get("leaderUrl"), "http://leader:8983/solr/catalog"); + assert.equal(calls[1]!.url.searchParams.get("leaderUrl"), "http://leader:8983/solr/catalog/replication"); assert.equal(calls[1]!.url.searchParams.get("compression"), "internal"); assert.equal(calls[1]!.url.searchParams.get("httpConnTimeout"), "5000"); assert.equal(calls[1]!.url.searchParams.get("maxWriteMBPerSec"), "20"); @@ -54,7 +54,7 @@ test("ReplicationHandler supports read, poll, and replication lifecycle commands await client.replication("enablepoll"); await client.replication("disablepoll"); await client.replication("abortfetch"); - await client.replication("snappull", { leaderUrl: "http://leader:8983/solr/catalog" }); + await client.replication("snappull", { leaderUrl: "http://leader:8983/solr/catalog/replication" }); await client.replication("abort"); assert.deepEqual(commands, [ From 1b62693f0c641a271ea89411790e8f9817eb1076 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:00:40 +0100 Subject: [PATCH 40/62] docs: keep replication fetch runtime evidence pending --- docs/api-coverage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-coverage.md b/docs/api-coverage.md index e9c8a1a..7e991a9 100644 --- a/docs/api-coverage.md +++ b/docs/api-coverage.md @@ -26,7 +26,7 @@ A generic request method does not mean that every endpoint has a typed wrapper, | Security | `security(area, command)` | Security-enabled standalone matrix covers anonymous 401 responses for both authentication and authorization, authenticated reads, authentication property mutation/read-back, and malformed JSON rejection on Solr 8.11.4/9.11.0/10.1.0. Unit tests verify Basic/Bearer credentials and Authorization headers are not exposed through client HTTP errors. See Issues #86–#88. | | System | `systemInfo()`, `systemProperties()`, `systemThreads()`, `systemLogging()` | System info, non-empty JVM/process properties, a non-empty thread dump, and logger metadata are exercised on pinned Solr 8.11.4/9.11.0/10.1.0. Optional fields remain version-dependent; response models preserve unknown fields. See Issues #89 and #91. |\n| Metrics | `metrics()` with typed group/registry/prefix/regex filters; exported `SolrMetricsJsonResponse`, `SolrMetricsRegistry`, and open `SolrMetricValue`; JSON/Prometheus response union | JVM response-format coverage remains distinct (observed JSON on Solr 8/9 and Prometheus exposition on 10.1). A `node` group integration check runs on all pinned versions, asserting a non-empty JSON registry on Solr 8/9 and Prometheus samples on 10.1; registry, prefix, and regex filters are asserted against JSON responses on Solr 8/9 (Solr 10.1 supports Prometheus/OpenMetrics, not the JSON writer). Consumer fixtures verify the extensible registry/value envelope; plugin-specific metrics remain open and unenumerated. See Issues #90, #91, and #236. | | Backup and restore | `replication("backup" | "restore" | "restorestatus" | "deletebackup")`, `collections("BACKUP" | "RESTORE" | "LISTBACKUP" | "DELETEBACKUP" | "REQUESTSTATUS" | "DELETESTATUS")` | Typed core and SolrCloud backup/restore parameters and response models, including async status polling. Full core restore and SolrCloud collection backup/restore cycles are integration-tested on Solr 8.11.4/9.11.0/10.1.0 using local filesystem repositories and shared SolrCloud storage. See [backup and restore docs](backup-restore.md). | -| Replication | `replication(command, params)`, `replicationFile(params)`; exported command-specific parameter and response models | V1 ReplicationHandler commands for index version, details, file list, leader/follower control (Solr 10 fetchindex/abortfetch; Solr 8/9 snappull/abort), core backup/restore/status/delete, and raw file-content streaming. Integration coverage includes core backup/restore cycles and a disposable two-core leader/follower fetch/read-back cycle on Solr 8.11.4/9.11.0/10.1.0. The test uses `snappull` on Solr 8/9 and `fetchindex` on Solr 10; abort and polling lifecycle behavior remains unit-only. See Issue #95 and [replication docs](replication.md). | +| Replication | `replication(command, params)`, `replicationFile(params)`; exported command-specific parameter and response models | V1 ReplicationHandler commands for index version, details, file list, leader/follower control (Solr 10 fetchindex/abortfetch; Solr 8/9 snappull/abort), core backup/restore/status/delete, and raw file-content streaming. Integration coverage includes core backup/restore cycles on Solr 8.11.4/9.11.0/10.1.0. A disposable two-core leader/follower fetch/read-back check is implemented but remains pending runtime verification until its pinned-version CI jobs pass; it uses `snappull` on Solr 8/9 and `fetchindex` on Solr 10. Abort and polling lifecycle behavior remains unit-only. See Issue #95 and [replication docs](replication.md). | | Streaming Expressions | `stream()` | Sends `expr` with `wt=json` and parses the complete JSON response through the shared transport. It is not an incremental tuple-stream parser: no chunk-boundary protocol or memory-bound/backpressure guarantee is claimed. SolrCloud search is integration-tested on pinned Solr 8.11.4/9.11.0/10.1.0; unit tests cover JSON parsing, caller cancellation during body consumption, and cleanup after body-stream errors. See [Streaming Expressions](streaming.md) and Issue #93. | | Export | `export()`, `exportStream()`; exported `SolrExportParams`, `SolrExportSort`, and `SolrExportFieldType` | Export requests require typed `fl` and `sort` parameters, support repeated filter queries, structured or raw sort expressions, batch sizing, JSON/JavaBin writers, and an explicit custom-parameter escape hatch. The raw Response preserves backpressure-aware streaming; `exportStream()` exposes its byte body directly. Sort fields must be single-valued docValues fields (int, long, float, double, or string); field lists additionally support date and boolean and may be multi-valued. Solr 10.1 `includeStoredFields` support is exposed but requires version-specific verification. Existing raw Export response tests pass on pinned Solr 8.11.4/9.11.0/10.1.0; typed parameter serialization and streamed chunk consumption have unit coverage. See [Export docs](export.md) and Issues #92–#94. | | Advanced / Raw | `requestRaw(path, options)` with typed method/query/header/body/control options and a raw Fetch `Response` | Relative paths and same-origin absolute URLs use the shared transport. Cross-origin URLs, embedded credentials, and URL fragments are rejected before Fetch, preventing configured credentials from being sent to unrelated hosts. Generic request()/requestRaw() transport behavior has unit coverage only; arbitrary custom endpoint behavior remains unverified until endpoint-specific integration tests exist. See [raw requests](raw-requests.md) and Issue #98. | From 7a0f7a42a4d17e43c6d7b0fc91ed76724664f6ba Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:00:49 +0100 Subject: [PATCH 41/62] docs: keep replication fetch runtime evidence pending --- docs/api-inventory.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-inventory.json b/docs/api-inventory.json index 55abdf1..a255a0b 100644 --- a/docs/api-inventory.json +++ b/docs/api-inventory.json @@ -402,7 +402,7 @@ "test/backup-restore-api-types.ts", "test/integration/solr-core-backup-restore.test.ts" ], - "notes": "Typed V1 ReplicationHandler commands for enablereplication, disablereplication, indexversion, fetchindex, abortfetch, snappull, abort, enablepoll, disablepoll, details, filelist, backup, restore, restorestatus, and deletebackup; replicationFile() returns raw filecontent streams. Live integration coverage on Solr 8.11.4, 9.11.0, and 10.1.0 covers indexversion, details, filelist, filecontent, leader enable/disable, and a disposable two-core leader/follower fetch with follower query read-back. The integration job supplies SOLR_VERSION so the test uses fetchindex on Solr 10 and snappull on Solr 8/9. abortfetch/abort, enablepoll, and disablepoll retain unit serialization coverage only; an in-progress abort lifecycle is not certified. Core-level backup, restore, restorestatus, and deletebackup are integration-tested on all three pinned versions. Filelist uses the generation returned by newer Solr indexversion responses and falls back to the legacy indexversion parameter when that field is absent. Additional operation evidence: Core-level ReplicationHandler backup, restore, restorestatus, and deletebackup are typed. Backup defaults to the core dataDir; restore status is polled with restorestatus. Full backup/restore/read-back/delete cycle is integration-tested on Solr 8.11.4, 9.11.0, and 10.1.0. Custom filesystem locations must be permitted by server allowPaths." + "notes": "Typed V1 ReplicationHandler commands for enablereplication, disablereplication, indexversion, fetchindex, abortfetch, snappull, abort, enablepoll, disablepoll, details, filelist, backup, restore, restorestatus, and deletebackup; replicationFile() returns raw filecontent streams. Live integration coverage on Solr 8.11.4, 9.11.0, and 10.1.0 covers indexversion, details, filelist, filecontent, leader enable/disable, and a disposable two-core leader/follower fetch with follower query read-back is implemented but remains pending runtime verification until the pinned-version CI jobs pass. The integration job supplies SOLR_VERSION so the test uses fetchindex on Solr 10 and snappull on Solr 8/9. abortfetch/abort, enablepoll, and disablepoll retain unit serialization coverage only; an in-progress abort lifecycle is not certified. Core-level backup, restore, restorestatus, and deletebackup are integration-tested on all three pinned versions. Filelist uses the generation returned by newer Solr indexversion responses and falls back to the legacy indexversion parameter when that field is absent. Additional operation evidence: Core-level ReplicationHandler backup, restore, restorestatus, and deletebackup are typed. Backup defaults to the core dataDir; restore status is polled with restorestatus. Full backup/restore/read-back/delete cycle is integration-tested on Solr 8.11.4, 9.11.0, and 10.1.0. Custom filesystem locations must be permitted by server allowPaths." }, { "id": "replication.file", From 543e04bffc529ca1a570edb01997eef355f171d2 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:00:52 +0100 Subject: [PATCH 42/62] docs(replication): show fully qualified leader handler URL --- docs/replication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/replication.md b/docs/replication.md index 30aa898..12fd18f 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -46,7 +46,7 @@ The `indexversion` response includes the current `generation` on newer Solr vers // Solr 10 uses fetchindex/abortfetch. Solr 8 and 9 use the legacy snappull/abort names. // leaderUrl may be omitted when configured on the follower. await client.replication("fetchindex", { - leaderUrl: "http://leader:8983/solr/catalog", + leaderUrl: "http://leader:8983/solr/catalog/replication", compression: "internal", httpConnTimeout: 5_000, httpReadTimeout: 10_000 From 5244978014333c498b6da9a3e7de8572c79d3839 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:00:56 +0100 Subject: [PATCH 43/62] test(replication): clarify leader handler URL --- test/integration/solr-replication-lifecycle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 74339e4..453eb29 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -20,7 +20,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const leader = makeClient(leaderCore); const follower = makeClient(followerCore); const solrMajor = Number(process.env.SOLR_VERSION?.split(".")[0] ?? "0"); - // Solr 8 expects the handler URL; Solr 9+ appends /replication internally. + // `leaderUrl` is the fully qualified ReplicationHandler URL. const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}/replication`; const documentId = createId("replication_lifecycle"); From 4634895c5765f97d2b75fcb2a662442277eec812 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:01:44 +0100 Subject: [PATCH 44/62] docs: distinguish replication matrix from manual cloud workflow --- docs/compatibility.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index d392369..b604ca9 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -38,7 +38,7 @@ The 8.11, 9.11, and 10.1 guides describe the API families below. The Replica Man | `security()` — authentication and authorization handlers | Documented | Documented | Documented | Tested: authenticated BasicAuth/RuleBasedAuthorization reads, anonymous 401 on both handlers, authentication `set-property` read-back, and malformed JSON rejection on pinned 8.11.4 / 9.11.0 / 10.1.0 images. Unit tests verify Basic/Bearer credentials are not exposed by client HTTP errors | | `stream()` — `/stream` | Documented | Documented | Documented | Tested: Streaming Expressions search against a disposable SolrCloud collection on pinned 8.11.4 / 9.11.0 / 10.1.0 images (CI #145) | | `export()` — `/export` | Documented | Documented | Documented | Tested: raw response with docValues sort field on pinned 8.11.4 / 9.11.0 / 10.1.0 images | -| `replication()` / `replicationFile()` — `/solr/{core}/replication` | Documented | Documented | Documented | Tested: indexversion, details, filelist (generation on newer versions / legacy indexversion on older versions), streamed filecontent, enablereplication/disablereplication, core backup, restorestatus, restore, and deletebackup on pinned 8.11.4 / 9.11.0 / 10.1.0 images. A disposable two-core leader/follower fetch with follower query read-back is added as an opt-in check and remains pending runtime verification until the manual workflow passes on each pinned version; it uses snappull on Solr 8/9 and fetchindex on Solr 10. Abort and polling lifecycle behavior remains unit-only | +| `replication()` / `replicationFile()` — `/solr/{core}/replication` | Documented | Documented | Documented | Tested: indexversion, details, filelist (generation on newer versions / legacy indexversion on older versions), streamed filecontent, enablereplication/disablereplication, core backup, restorestatus, restore, and deletebackup on pinned 8.11.4 / 9.11.0 / 10.1.0 images. A disposable two-core leader/follower fetch with follower query read-back is covered by the regular standalone/security integration matrix; runtime verification remains pending until those CI jobs pass on each pinned version. It uses snappull on Solr 8/9 and fetchindex on Solr 10. Abort and polling lifecycle behavior remains unit-only | | Core and collection backup/restore | `replication("backup" | "restore" | "restorestatus" | "deletebackup")`, `collections("BACKUP" | "RESTORE" | "LISTBACKUP" | "DELETEBACKUP" | "REQUESTSTATUS" | "DELETESTATUS")` | Documented | Documented | Documented | Tested: full core backup/restore/read-back and SolrCloud collection backup/list/restore/read-back/delete with asynchronous status polling on pinned 8.11.4 / 9.11.0 / 10.1.0 images using local filesystem repositories and shared SolrCloud storage | | `request()` / `requestRaw()` — generic caller-defined transport | Depends on the selected endpoint | Depends on the selected endpoint | Depends on the selected endpoint | Generic transport behavior has unit tests only; no endpoint-independent live Solr integration claim is made. Each custom endpoint remains unverified until tested individually | From b538027ce75543d28035155f66fbad0f11056d2a Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:01:47 +0100 Subject: [PATCH 45/62] docs(replication): clarify standard CI coverage --- docs/replication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/replication.md b/docs/replication.md index 12fd18f..c132d32 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -103,7 +103,7 @@ Supported directory types are `file` (Lucene index files), `cf` (configuration f - The client currently exposes the V1 handler path. Solr 10.1 also documents V2 equivalents for selected file and index-version operations; no dedicated V2 wrapper is provided. - The handler may not be configured for user-managed leader/follower replication on every core. The server must have the appropriate ReplicationHandler configuration before control commands can be used. -- An opt-in integration test creates disposable leader and follower cores, commits a unique leader document, triggers `snappull` on Solr 8/9 or `fetchindex` on Solr 10 with the fully qualified leader handler URL, and verifies that the document becomes searchable on the follower. Treat this behavior as pending runtime verification until the pinned Solr 8.11.4, 9.11.0, and 10.1.0 CI jobs pass. The test does not cover polling or abort of an in-progress transfer. +- A regular standalone/security integration test creates disposable leader and follower cores, commits a unique leader document, triggers `snappull` on Solr 8/9 or `fetchindex` on Solr 10 with the fully qualified leader handler URL, and verifies that the document becomes searchable on the follower. Treat this behavior as pending runtime verification until the pinned Solr 8.11.4, 9.11.0, and 10.1.0 CI jobs pass. The test does not cover polling or abort of an in-progress transfer. - `abort` (Solr 8/9) and `abortfetch` (Solr 10), plus `enablepoll`/`disablepoll`, have request-serialization unit coverage; cancellation of a live transfer remains unverified. The disposable-core fixture unloads both cores in its `finally` cleanup. If a CI job is cancelled or the fetch does not settle, the ephemeral Solr container is discarded rather than reusing the cores. See the [Solr 8.11](https://solr.apache.org/__root/docs.solr.apache.org/guide/8_11/deployment-guide/user-managed-index-replication.html), [Solr 9.11](https://solr.apache.org/guide/solr/9_11/deployment-guide/user-managed-index-replication.html), and [Solr 10.1](https://solr.apache.org/guide/solr/10_1/deployment-guide/user-managed-index-replication.html) guides for server configuration and command semantics. From e40337a713c9573c52443ce3452e1c30d0e3f6c5 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:15:56 +0100 Subject: [PATCH 46/62] fix(test): route replication qt requests through select handler --- test/integration/solr-replication-lifecycle.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 453eb29..970997d 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -20,7 +20,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const leader = makeClient(leaderCore); const follower = makeClient(followerCore); const solrMajor = Number(process.env.SOLR_VERSION?.split(".")[0] ?? "0"); - // `leaderUrl` is the fully qualified ReplicationHandler URL. + // Solr routes qt=/replication through /select; handleSelect belongs on /select. const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}/replication`; const documentId = createId("replication_lifecycle"); @@ -28,16 +28,22 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", - handleSelect: true, customOptions: { leader: { replicateAfter: ["commit"] } } } }); assert.equal(leaderConfig.responseHeader?.status, 0); + const selectConfig = await leader.config({ + "update-requesthandler": { + name: "/select", + class: "solr.SearchHandler", + handleSelect: true + } + }); + assert.equal(selectConfig.responseHeader?.status, 0); const followerConfig = await follower.config({ "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", - handleSelect: true, customOptions: { follower: { leaderUrl, ...(username && password ? { httpBasicAuthUser: username, httpBasicAuthPassword: password } : {}) } } } }); From ecf547c64ef8bc8d427840bfefc2fcd53737bee7 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:15:59 +0100 Subject: [PATCH 47/62] docs(replication): clarify legacy qt handler routing --- docs/replication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/replication.md b/docs/replication.md index c132d32..7798d45 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -59,7 +59,7 @@ await client.replication("enablepoll"); await client.replication("abortfetch"); ``` -`fetchindex` accepts the documented follower connection and compression settings. `leaderUrl` is the fully qualified ReplicationHandler URL, including the `/replication` suffix, on the pinned Solr 8, 9, and 10 lines. The follower's IndexFetcher requests leader details through `qt=/replication`; if the leader's `/select` handler has `handleSelect` disabled, enable it so that request can be routed to the replication handler. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. +`fetchindex` accepts the documented follower connection and compression settings. `leaderUrl` is the fully qualified ReplicationHandler URL, including the `/replication` suffix, on the pinned Solr 8, 9, and 10 lines. The follower's IndexFetcher requests leader details through `qt=/replication`; if the leader's `/select` handler has `handleSelect` disabled, enable it on the `/select` handler (not on `/replication`) so that the legacy `qt` request can be routed to the replication handler. For example, use the Config API to update `/select` with `handleSelect: true`. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. ## Control a leader From f2dfb6776543f30c674092f6e61d4dd7e360d7cd Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:19:06 +0100 Subject: [PATCH 48/62] docs: fix API coverage table row separator --- docs/api-coverage.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-coverage.md b/docs/api-coverage.md index 7e991a9..5b244ee 100644 --- a/docs/api-coverage.md +++ b/docs/api-coverage.md @@ -24,7 +24,8 @@ A generic request method does not mean that every endpoint has a typed wrapper, | Core Admin | `coreAdmin(action, params)` with `CoreAdminParams` action-specific models for STATUS, CREATE, RELOAD, UNLOAD, RENAME, SWAP, MERGEINDEXES, and SPLIT; `CoreAdminStatusResponse` for STATUS and `CoreAdminCommandResponse` as the default non-STATUS response envelope | STATUS models per-core paths/config metadata, optional index statistics, initialization failures, and unknown fields. Other actions default to a named extensible response-header envelope; callers may supply a more specific generic. Unit/type fixtures cover minimal and detailed STATUS payloads plus CREATE/RELOAD response inference. Isolated CREATE, STATUS, RELOAD, and fixture-cleanup UNLOAD are integration-tested on Solr 8.11.4/9.11.0/10.1.0; remaining actions retain serialization/type coverage only and are not claimed as runtime-certified. See [Core Admin evidence](administration.md#core-admin-status-response) and Issue #229. | Collections | `collections(action, params)` with action-specific `CollectionAdminParams` and `CollectionAdminResponse` conditional response contracts | `LIST` uses `SolrCollectionListResponse`; collection lifecycle/policy, replica, and role actions use named extensible response envelopes; alias, cluster, shard, backup/restore, and async actions retain their named response models. Runtime evidence remains partial, not blanket certification of every action: exact Solr 8.11.4/9.11.0/10.1.0 evidence is listed in the [per-action Collections matrix](collections-api-coverage.md). `MODIFYCOLLECTION`, `SPLITSHARD`, `MIGRATE`, `ADDROLE`, and `REMOVEROLE` remain runtime-unverified. See [backup and restore docs](backup-restore.md), [Collections action matrix](collections-api-coverage.md), and Issues #80–#85, #121, and #230. | | Security | `security(area, command)` | Security-enabled standalone matrix covers anonymous 401 responses for both authentication and authorization, authenticated reads, authentication property mutation/read-back, and malformed JSON rejection on Solr 8.11.4/9.11.0/10.1.0. Unit tests verify Basic/Bearer credentials and Authorization headers are not exposed through client HTTP errors. See Issues #86–#88. | -| System | `systemInfo()`, `systemProperties()`, `systemThreads()`, `systemLogging()` | System info, non-empty JVM/process properties, a non-empty thread dump, and logger metadata are exercised on pinned Solr 8.11.4/9.11.0/10.1.0. Optional fields remain version-dependent; response models preserve unknown fields. See Issues #89 and #91. |\n| Metrics | `metrics()` with typed group/registry/prefix/regex filters; exported `SolrMetricsJsonResponse`, `SolrMetricsRegistry`, and open `SolrMetricValue`; JSON/Prometheus response union | JVM response-format coverage remains distinct (observed JSON on Solr 8/9 and Prometheus exposition on 10.1). A `node` group integration check runs on all pinned versions, asserting a non-empty JSON registry on Solr 8/9 and Prometheus samples on 10.1; registry, prefix, and regex filters are asserted against JSON responses on Solr 8/9 (Solr 10.1 supports Prometheus/OpenMetrics, not the JSON writer). Consumer fixtures verify the extensible registry/value envelope; plugin-specific metrics remain open and unenumerated. See Issues #90, #91, and #236. | +| System | `systemInfo()`, `systemProperties()`, `systemThreads()`, `systemLogging()` | System info, non-empty JVM/process properties, a non-empty thread dump, and logger metadata are exercised on pinned Solr 8.11.4/9.11.0/10.1.0. Optional fields remain version-dependent; response models preserve unknown fields. See Issues #89 and #91. | +| Metrics | `metrics()` with typed group/registry/prefix/regex filters; exported `SolrMetricsJsonResponse`, `SolrMetricsRegistry`, and open `SolrMetricValue`; JSON/Prometheus response union | JVM response-format coverage remains distinct (observed JSON on Solr 8/9 and Prometheus exposition on 10.1). A `node` group integration check runs on all pinned versions, asserting a non-empty JSON registry on Solr 8/9 and Prometheus samples on 10.1; registry, prefix, and regex filters are asserted against JSON responses on Solr 8/9 (Solr 10.1 supports Prometheus/OpenMetrics, not the JSON writer). Consumer fixtures verify the extensible registry/value envelope; plugin-specific metrics remain open and unenumerated. See Issues #90, #91, and #236. | | Backup and restore | `replication("backup" | "restore" | "restorestatus" | "deletebackup")`, `collections("BACKUP" | "RESTORE" | "LISTBACKUP" | "DELETEBACKUP" | "REQUESTSTATUS" | "DELETESTATUS")` | Typed core and SolrCloud backup/restore parameters and response models, including async status polling. Full core restore and SolrCloud collection backup/restore cycles are integration-tested on Solr 8.11.4/9.11.0/10.1.0 using local filesystem repositories and shared SolrCloud storage. See [backup and restore docs](backup-restore.md). | | Replication | `replication(command, params)`, `replicationFile(params)`; exported command-specific parameter and response models | V1 ReplicationHandler commands for index version, details, file list, leader/follower control (Solr 10 fetchindex/abortfetch; Solr 8/9 snappull/abort), core backup/restore/status/delete, and raw file-content streaming. Integration coverage includes core backup/restore cycles on Solr 8.11.4/9.11.0/10.1.0. A disposable two-core leader/follower fetch/read-back check is implemented but remains pending runtime verification until its pinned-version CI jobs pass; it uses `snappull` on Solr 8/9 and `fetchindex` on Solr 10. Abort and polling lifecycle behavior remains unit-only. See Issue #95 and [replication docs](replication.md). | | Streaming Expressions | `stream()` | Sends `expr` with `wt=json` and parses the complete JSON response through the shared transport. It is not an incremental tuple-stream parser: no chunk-boundary protocol or memory-bound/backpressure guarantee is claimed. SolrCloud search is integration-tested on pinned Solr 8.11.4/9.11.0/10.1.0; unit tests cover JSON parsing, caller cancellation during body consumption, and cleanup after body-stream errors. See [Streaming Expressions](streaming.md) and Issue #93. | From cc6b384133e488f30b2a5eb0ea532edb15e266cc Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:21:42 +0100 Subject: [PATCH 49/62] test(replication): enable legacy qt routing for Solr 8 --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 483d52f..1217141 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,10 @@ jobs: - name: Prepare a reusable configset for CoreAdmin lifecycle fixtures run: | docker exec solr bash -lc 'mkdir -p /var/solr/data/configsets && cp -a /opt/solr/server/solr/configsets/_default /var/solr/data/configsets/_default' + if [[ "${{ matrix.solr-version }}" == "8.11.4" ]]; then + docker exec solr sed -i 's///' /var/solr/data/configsets/_default/conf/solrconfig.xml + docker exec solr grep -q '' /var/solr/data/configsets/_default/conf/solrconfig.xml + fi - name: Run Solr integration smoke tests run: npm run test:integration env: @@ -152,6 +156,10 @@ jobs: - name: Prepare a reusable configset for CoreAdmin lifecycle fixtures run: | docker exec solr-secure bash -lc 'mkdir -p /var/solr/data/configsets && cp -a /opt/solr/server/solr/configsets/_default /var/solr/data/configsets/_default' + if [[ "${{ matrix.solr-version }}" == "8.11.4" ]]; then + docker exec solr-secure sed -i 's///' /var/solr/data/configsets/_default/conf/solrconfig.xml + docker exec solr-secure grep -q '' /var/solr/data/configsets/_default/conf/solrconfig.xml + fi - name: Run authenticated Solr integration tests run: npm run test:integration env: From 4d4dd8b1e1d6ce439c909ab7de0e887ad697ce56 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:21:49 +0100 Subject: [PATCH 50/62] test(replication): use Solr 9 fetch command --- test/integration/solr-replication-lifecycle.test.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 970997d..98d3531 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -20,7 +20,6 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const leader = makeClient(leaderCore); const follower = makeClient(followerCore); const solrMajor = Number(process.env.SOLR_VERSION?.split(".")[0] ?? "0"); - // Solr routes qt=/replication through /select; handleSelect belongs on /select. const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}/replication`; const documentId = createId("replication_lifecycle"); @@ -32,14 +31,6 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso } }); assert.equal(leaderConfig.responseHeader?.status, 0); - const selectConfig = await leader.config({ - "update-requesthandler": { - name: "/select", - class: "solr.SearchHandler", - handleSelect: true - } - }); - assert.equal(selectConfig.responseHeader?.status, 0); const followerConfig = await follower.config({ "update-requesthandler": { name: "/replication", @@ -57,7 +48,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const leaderGeneration = leaderVersion.generation ?? Number(leaderVersion.indexversion); assert.ok(Number.isFinite(leaderGeneration) && leaderGeneration > 0, "leader must publish a replicable index"); - const fetchCommand = solrMajor >= 10 ? "fetchindex" : "snappull"; + const fetchCommand = solrMajor >= 9 ? "fetchindex" : "snappull"; const fetchResponse = await follower.replication(fetchCommand, { leaderUrl }); assert.equal(fetchResponse.responseHeader?.status, 0, `${fetchCommand} should complete successfully`); @@ -74,7 +65,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso } if (!found) { const details = await follower.replication("details"); - const abortCommand = solrMajor >= 10 ? "abortfetch" : "abort"; + const abortCommand = solrMajor >= 9 ? "abortfetch" : "abort"; await follower.replication(abortCommand).catch(() => undefined); assert.fail(`follower did not expose the leader document after fetch; details: ${JSON.stringify(details)}`); } From 455f12d9f9c2c56e709fa42d204c510536e2c7a1 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:22:01 +0100 Subject: [PATCH 51/62] docs(replication): correct Solr 9 fetch command --- docs/replication.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/replication.md b/docs/replication.md index 7798d45..6962043 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -43,7 +43,7 @@ The `indexversion` response includes the current `generation` on newer Solr vers ## Control a follower ```ts -// Solr 10 uses fetchindex/abortfetch. Solr 8 and 9 use the legacy snappull/abort names. +// Solr 8 uses snappull/abort; Solr 9 and 10 use fetchindex/abortfetch. // leaderUrl may be omitted when configured on the follower. await client.replication("fetchindex", { leaderUrl: "http://leader:8983/solr/catalog/replication", @@ -52,14 +52,14 @@ await client.replication("fetchindex", { httpReadTimeout: 10_000 }); -// On Solr 8/9, use "snappull" to trigger a fetch and "abort" to stop it. +// On Solr 8, use "snappull" to trigger a fetch and "abort" to stop it; Solr 9/10 use "fetchindex"/"abortfetch". // Pause/resume automatic polling or abort an in-progress fetch. await client.replication("disablepoll"); await client.replication("enablepoll"); await client.replication("abortfetch"); ``` -`fetchindex` accepts the documented follower connection and compression settings. `leaderUrl` is the fully qualified ReplicationHandler URL, including the `/replication` suffix, on the pinned Solr 8, 9, and 10 lines. The follower's IndexFetcher requests leader details through `qt=/replication`; if the leader's `/select` handler has `handleSelect` disabled, enable it on the `/select` handler (not on `/replication`) so that the legacy `qt` request can be routed to the replication handler. For example, use the Config API to update `/select` with `handleSelect: true`. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. +`fetchindex` accepts the documented follower connection and compression settings. `leaderUrl` is the fully qualified ReplicationHandler URL, including the `/replication` suffix, on the pinned Solr 8, 9, and 10 lines. Solr 8.11 uses the legacy `/select?qt=/replication` route when the follower resolves leader details, so the leader configset must set `` in `solrconfig.xml`. `handleSelect` is a RequestDispatcher attribute, not a request-handler option; do not try to set it on `/select` through the Config API. Solr 9.11 and 10.1 use `fetchindex`/`abortfetch`. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. ## Control a leader @@ -97,13 +97,13 @@ Supported directory types are `file` (Lucene index files), `cf` (configuration f ## Scope and compatibility -- Typed commands: `enablereplication`, `disablereplication`, `indexversion`, `fetchindex`/`abortfetch` (Solr 10), `snappull`/`abort` (Solr 8/9), `enablepoll`, `disablepoll`, `details`, `filelist`, `backup`, `restore`, `restorestatus`, and `deletebackup`. +- Typed commands: `enablereplication`, `disablereplication`, `indexversion`, `fetchindex`/`abortfetch` (Solr 9/10), `snappull`/`abort` (Solr 8), `enablepoll`, `disablepoll`, `details`, `filelist`, `backup`, `restore`, `restorestatus`, and `deletebackup`. - `filecontent` is exposed separately through `replicationFile()` because it returns a raw stream. - Core-level `backup`, `restore`, `restorestatus`, and `deletebackup` are exposed through `replication()`; see [backup and restore docs](backup-restore.md). - The client currently exposes the V1 handler path. Solr 10.1 also documents V2 equivalents for selected file and index-version operations; no dedicated V2 wrapper is provided. - The handler may not be configured for user-managed leader/follower replication on every core. The server must have the appropriate ReplicationHandler configuration before control commands can be used. -- A regular standalone/security integration test creates disposable leader and follower cores, commits a unique leader document, triggers `snappull` on Solr 8/9 or `fetchindex` on Solr 10 with the fully qualified leader handler URL, and verifies that the document becomes searchable on the follower. Treat this behavior as pending runtime verification until the pinned Solr 8.11.4, 9.11.0, and 10.1.0 CI jobs pass. The test does not cover polling or abort of an in-progress transfer. -- `abort` (Solr 8/9) and `abortfetch` (Solr 10), plus `enablepoll`/`disablepoll`, have request-serialization unit coverage; cancellation of a live transfer remains unverified. The disposable-core fixture unloads both cores in its `finally` cleanup. If a CI job is cancelled or the fetch does not settle, the ephemeral Solr container is discarded rather than reusing the cores. +- A regular standalone/security integration test creates disposable leader and follower cores, commits a unique leader document, triggers `snappull` on Solr 8 or `fetchindex` on Solr 9/10 with the fully qualified leader handler URL, and verifies that the document becomes searchable on the follower. Treat this behavior as pending runtime verification until the pinned Solr 8.11.4, 9.11.0, and 10.1.0 CI jobs pass. The test does not cover polling or abort of an in-progress transfer. +- `abort` (Solr 8) and `abortfetch` (Solr 9/10), plus `enablepoll`/`disablepoll`, have request-serialization unit coverage; cancellation of a live transfer remains unverified. The disposable-core fixture unloads both cores in its `finally` cleanup. If a CI job is cancelled or the fetch does not settle, the ephemeral Solr container is discarded rather than reusing the cores. See the [Solr 8.11](https://solr.apache.org/__root/docs.solr.apache.org/guide/8_11/deployment-guide/user-managed-index-replication.html), [Solr 9.11](https://solr.apache.org/guide/solr/9_11/deployment-guide/user-managed-index-replication.html), and [Solr 10.1](https://solr.apache.org/guide/solr/10_1/deployment-guide/user-managed-index-replication.html) guides for server configuration and command semantics. From dba8b1e9db988c5facfd1d45d930d79e6aaf52e8 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:22:23 +0100 Subject: [PATCH 52/62] docs: correct replication command matrix --- docs/api-coverage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-coverage.md b/docs/api-coverage.md index 5b244ee..5efb439 100644 --- a/docs/api-coverage.md +++ b/docs/api-coverage.md @@ -27,7 +27,7 @@ A generic request method does not mean that every endpoint has a typed wrapper, | System | `systemInfo()`, `systemProperties()`, `systemThreads()`, `systemLogging()` | System info, non-empty JVM/process properties, a non-empty thread dump, and logger metadata are exercised on pinned Solr 8.11.4/9.11.0/10.1.0. Optional fields remain version-dependent; response models preserve unknown fields. See Issues #89 and #91. | | Metrics | `metrics()` with typed group/registry/prefix/regex filters; exported `SolrMetricsJsonResponse`, `SolrMetricsRegistry`, and open `SolrMetricValue`; JSON/Prometheus response union | JVM response-format coverage remains distinct (observed JSON on Solr 8/9 and Prometheus exposition on 10.1). A `node` group integration check runs on all pinned versions, asserting a non-empty JSON registry on Solr 8/9 and Prometheus samples on 10.1; registry, prefix, and regex filters are asserted against JSON responses on Solr 8/9 (Solr 10.1 supports Prometheus/OpenMetrics, not the JSON writer). Consumer fixtures verify the extensible registry/value envelope; plugin-specific metrics remain open and unenumerated. See Issues #90, #91, and #236. | | Backup and restore | `replication("backup" | "restore" | "restorestatus" | "deletebackup")`, `collections("BACKUP" | "RESTORE" | "LISTBACKUP" | "DELETEBACKUP" | "REQUESTSTATUS" | "DELETESTATUS")` | Typed core and SolrCloud backup/restore parameters and response models, including async status polling. Full core restore and SolrCloud collection backup/restore cycles are integration-tested on Solr 8.11.4/9.11.0/10.1.0 using local filesystem repositories and shared SolrCloud storage. See [backup and restore docs](backup-restore.md). | -| Replication | `replication(command, params)`, `replicationFile(params)`; exported command-specific parameter and response models | V1 ReplicationHandler commands for index version, details, file list, leader/follower control (Solr 10 fetchindex/abortfetch; Solr 8/9 snappull/abort), core backup/restore/status/delete, and raw file-content streaming. Integration coverage includes core backup/restore cycles on Solr 8.11.4/9.11.0/10.1.0. A disposable two-core leader/follower fetch/read-back check is implemented but remains pending runtime verification until its pinned-version CI jobs pass; it uses `snappull` on Solr 8/9 and `fetchindex` on Solr 10. Abort and polling lifecycle behavior remains unit-only. See Issue #95 and [replication docs](replication.md). | +| Replication | `replication(command, params)`, `replicationFile(params)`; exported command-specific parameter and response models | V1 ReplicationHandler commands for index version, details, file list, leader/follower control (Solr 9/10 fetchindex/abortfetch; Solr 8 snappull/abort), core backup/restore/status/delete, and raw file-content streaming. Integration coverage includes core backup/restore cycles on Solr 8.11.4/9.11.0/10.1.0. A disposable two-core leader/follower fetch/read-back check is implemented but remains pending runtime verification until its pinned-version CI jobs pass; it uses `snappull` on Solr 8 and `fetchindex` on Solr 9/10. Abort and polling lifecycle behavior remains unit-only. See Issue #95 and [replication docs](replication.md). | | Streaming Expressions | `stream()` | Sends `expr` with `wt=json` and parses the complete JSON response through the shared transport. It is not an incremental tuple-stream parser: no chunk-boundary protocol or memory-bound/backpressure guarantee is claimed. SolrCloud search is integration-tested on pinned Solr 8.11.4/9.11.0/10.1.0; unit tests cover JSON parsing, caller cancellation during body consumption, and cleanup after body-stream errors. See [Streaming Expressions](streaming.md) and Issue #93. | | Export | `export()`, `exportStream()`; exported `SolrExportParams`, `SolrExportSort`, and `SolrExportFieldType` | Export requests require typed `fl` and `sort` parameters, support repeated filter queries, structured or raw sort expressions, batch sizing, JSON/JavaBin writers, and an explicit custom-parameter escape hatch. The raw Response preserves backpressure-aware streaming; `exportStream()` exposes its byte body directly. Sort fields must be single-valued docValues fields (int, long, float, double, or string); field lists additionally support date and boolean and may be multi-valued. Solr 10.1 `includeStoredFields` support is exposed but requires version-specific verification. Existing raw Export response tests pass on pinned Solr 8.11.4/9.11.0/10.1.0; typed parameter serialization and streamed chunk consumption have unit coverage. See [Export docs](export.md) and Issues #92–#94. | | Advanced / Raw | `requestRaw(path, options)` with typed method/query/header/body/control options and a raw Fetch `Response` | Relative paths and same-origin absolute URLs use the shared transport. Cross-origin URLs, embedded credentials, and URL fragments are rejected before Fetch, preventing configured credentials from being sent to unrelated hosts. Generic request()/requestRaw() transport behavior has unit coverage only; arbitrary custom endpoint behavior remains unverified until endpoint-specific integration tests exist. See [raw requests](raw-requests.md) and Issue #98. | From 3845a2d6b5981fa3226b868d7e71745d72699e69 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:22:27 +0100 Subject: [PATCH 53/62] docs: correct replication command matrix --- docs/endpoint-inventory.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/endpoint-inventory.md b/docs/endpoint-inventory.md index 235df19..bd6950c 100644 --- a/docs/endpoint-inventory.md +++ b/docs/endpoint-inventory.md @@ -19,7 +19,7 @@ This inventory defines the boundary of the typed client API after review against | Metrics API `/solr/admin/metrics` | `metrics()` | Typed response models; JSON metrics on Solr 8/9 and Prometheus exposition text on Solr 10.1 are documented. | | Streaming Expressions `/stream` | `stream()` | Expression request support and JSON response parsing; tested against SolrCloud on all three pinned versions. | | Export handler `/export` | `export()`, `exportStream()` | Typed export parameters and raw backpressure-aware response/byte-stream access; docValues sort-field flow tested on all three pinned versions. | -| ReplicationHandler V1 `/solr/{core}/replication` | `replication()`, `replicationFile()` | Typed commands include indexversion, details, filelist, fetch/poll/enable/disable/abort, backup, restore, restorestatus, and deletebackup (including Solr 8/9 snappull/abort aliases), plus raw file-content streaming. Core backup/restore cycles are integration-tested on all three pinned versions. A complete leader/follower fetch cycle is not yet certified. | +| ReplicationHandler V1 `/solr/{core}/replication` | `replication()`, `replicationFile()` | Typed commands include indexversion, details, filelist, fetch/poll/enable/disable/abort, backup, restore, restorestatus, and deletebackup (including Solr 8 snappull/abort and Solr 9/10 fetchindex/abortfetch variants), plus raw file-content streaming. Core backup/restore cycles are integration-tested on all three pinned versions. A complete leader/follower fetch cycle is not yet certified. | | Generic/custom request handlers | `request()`, `requestRaw()` | Escape hatches for custom paths and parameters. These do not imply typed support or integration certification for arbitrary endpoints. | ## Explicitly deferred or excluded endpoint families From f193c2e81e268847b8a429e95f33350cdf7f995e Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:24:05 +0100 Subject: [PATCH 54/62] test: remove unnecessary Solr 8 dispatcher override --- .github/workflows/ci.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1217141..483d52f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,10 +69,6 @@ jobs: - name: Prepare a reusable configset for CoreAdmin lifecycle fixtures run: | docker exec solr bash -lc 'mkdir -p /var/solr/data/configsets && cp -a /opt/solr/server/solr/configsets/_default /var/solr/data/configsets/_default' - if [[ "${{ matrix.solr-version }}" == "8.11.4" ]]; then - docker exec solr sed -i 's///' /var/solr/data/configsets/_default/conf/solrconfig.xml - docker exec solr grep -q '' /var/solr/data/configsets/_default/conf/solrconfig.xml - fi - name: Run Solr integration smoke tests run: npm run test:integration env: @@ -156,10 +152,6 @@ jobs: - name: Prepare a reusable configset for CoreAdmin lifecycle fixtures run: | docker exec solr-secure bash -lc 'mkdir -p /var/solr/data/configsets && cp -a /opt/solr/server/solr/configsets/_default /var/solr/data/configsets/_default' - if [[ "${{ matrix.solr-version }}" == "8.11.4" ]]; then - docker exec solr-secure sed -i 's///' /var/solr/data/configsets/_default/conf/solrconfig.xml - docker exec solr-secure grep -q '' /var/solr/data/configsets/_default/conf/solrconfig.xml - fi - name: Run authenticated Solr integration tests run: npm run test:integration env: From fb8ae0bedc5b27e80375a03f2bd8e9a60ae479f2 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:24:10 +0100 Subject: [PATCH 55/62] test(replication): use fetchindex across pinned Solr versions --- test/integration/solr-replication-lifecycle.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 98d3531..eeb1baf 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -19,7 +19,6 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso }); const leader = makeClient(leaderCore); const follower = makeClient(followerCore); - const solrMajor = Number(process.env.SOLR_VERSION?.split(".")[0] ?? "0"); const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}/replication`; const documentId = createId("replication_lifecycle"); @@ -48,7 +47,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const leaderGeneration = leaderVersion.generation ?? Number(leaderVersion.indexversion); assert.ok(Number.isFinite(leaderGeneration) && leaderGeneration > 0, "leader must publish a replicable index"); - const fetchCommand = solrMajor >= 9 ? "fetchindex" : "snappull"; + const fetchCommand = "fetchindex"; const fetchResponse = await follower.replication(fetchCommand, { leaderUrl }); assert.equal(fetchResponse.responseHeader?.status, 0, `${fetchCommand} should complete successfully`); @@ -65,7 +64,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso } if (!found) { const details = await follower.replication("details"); - const abortCommand = solrMajor >= 9 ? "abortfetch" : "abort"; + const abortCommand = "abortfetch"; await follower.replication(abortCommand).catch(() => undefined); assert.fail(`follower did not expose the leader document after fetch; details: ${JSON.stringify(details)}`); } From 929d447509f1500dfe2756b71ae51c26f85d8eb4 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:24:19 +0100 Subject: [PATCH 56/62] docs(replication): standardize fetch commands across Solr versions --- docs/replication.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/replication.md b/docs/replication.md index 6962043..4220cd3 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -43,7 +43,7 @@ The `indexversion` response includes the current `generation` on newer Solr vers ## Control a follower ```ts -// Solr 8 uses snappull/abort; Solr 9 and 10 use fetchindex/abortfetch. +// Use fetchindex/abortfetch across the pinned Solr 8, 9, and 10 versions. // leaderUrl may be omitted when configured on the follower. await client.replication("fetchindex", { leaderUrl: "http://leader:8983/solr/catalog/replication", @@ -52,14 +52,14 @@ await client.replication("fetchindex", { httpReadTimeout: 10_000 }); -// On Solr 8, use "snappull" to trigger a fetch and "abort" to stop it; Solr 9/10 use "fetchindex"/"abortfetch". +// Use "fetchindex" to trigger a fetch and "abortfetch" to stop it. // Pause/resume automatic polling or abort an in-progress fetch. await client.replication("disablepoll"); await client.replication("enablepoll"); await client.replication("abortfetch"); ``` -`fetchindex` accepts the documented follower connection and compression settings. `leaderUrl` is the fully qualified ReplicationHandler URL, including the `/replication` suffix, on the pinned Solr 8, 9, and 10 lines. Solr 8.11 uses the legacy `/select?qt=/replication` route when the follower resolves leader details, so the leader configset must set `` in `solrconfig.xml`. `handleSelect` is a RequestDispatcher attribute, not a request-handler option; do not try to set it on `/select` through the Config API. Solr 9.11 and 10.1 use `fetchindex`/`abortfetch`. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. +`fetchindex`/`abortfetch` are the canonical fetch and cancellation commands across the pinned Solr 8.11.4, 9.11.0, and 10.1.0 versions. `leaderUrl` is the fully qualified ReplicationHandler URL, including the `/replication` suffix. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. ## Control a leader @@ -97,13 +97,13 @@ Supported directory types are `file` (Lucene index files), `cf` (configuration f ## Scope and compatibility -- Typed commands: `enablereplication`, `disablereplication`, `indexversion`, `fetchindex`/`abortfetch` (Solr 9/10), `snappull`/`abort` (Solr 8), `enablepoll`, `disablepoll`, `details`, `filelist`, `backup`, `restore`, `restorestatus`, and `deletebackup`. +- Typed commands: `enablereplication`, `disablereplication`, `indexversion`, `fetchindex`/`abortfetch` (Solr 8/9/10), `enablepoll`, `disablepoll`, `details`, `filelist`, `backup`, `restore`, `restorestatus`, and `deletebackup`. - `filecontent` is exposed separately through `replicationFile()` because it returns a raw stream. - Core-level `backup`, `restore`, `restorestatus`, and `deletebackup` are exposed through `replication()`; see [backup and restore docs](backup-restore.md). - The client currently exposes the V1 handler path. Solr 10.1 also documents V2 equivalents for selected file and index-version operations; no dedicated V2 wrapper is provided. - The handler may not be configured for user-managed leader/follower replication on every core. The server must have the appropriate ReplicationHandler configuration before control commands can be used. -- A regular standalone/security integration test creates disposable leader and follower cores, commits a unique leader document, triggers `snappull` on Solr 8 or `fetchindex` on Solr 9/10 with the fully qualified leader handler URL, and verifies that the document becomes searchable on the follower. Treat this behavior as pending runtime verification until the pinned Solr 8.11.4, 9.11.0, and 10.1.0 CI jobs pass. The test does not cover polling or abort of an in-progress transfer. -- `abort` (Solr 8) and `abortfetch` (Solr 9/10), plus `enablepoll`/`disablepoll`, have request-serialization unit coverage; cancellation of a live transfer remains unverified. The disposable-core fixture unloads both cores in its `finally` cleanup. If a CI job is cancelled or the fetch does not settle, the ephemeral Solr container is discarded rather than reusing the cores. +- A regular standalone/security integration test creates disposable leader and follower cores, commits a unique leader document, triggers `fetchindex` on Solr 8/9/10 with the fully qualified leader handler URL, and verifies that the document becomes searchable on the follower. Treat this behavior as pending runtime verification until the pinned Solr 8.11.4, 9.11.0, and 10.1.0 CI jobs pass. The test does not cover polling or abort of an in-progress transfer. +- `abortfetch` on Solr 8/9/10, plus `enablepoll`/`disablepoll`, have request-serialization unit coverage; cancellation of a live transfer remains unverified. The disposable-core fixture unloads both cores in its `finally` cleanup. If a CI job is cancelled or the fetch does not settle, the ephemeral Solr container is discarded rather than reusing the cores. See the [Solr 8.11](https://solr.apache.org/__root/docs.solr.apache.org/guide/8_11/deployment-guide/user-managed-index-replication.html), [Solr 9.11](https://solr.apache.org/guide/solr/9_11/deployment-guide/user-managed-index-replication.html), and [Solr 10.1](https://solr.apache.org/guide/solr/10_1/deployment-guide/user-managed-index-replication.html) guides for server configuration and command semantics. From 9f36532dac2bd8e87725a8ef0a5007739464ee4c Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:24:24 +0100 Subject: [PATCH 57/62] docs: align replication compatibility matrix --- docs/api-coverage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-coverage.md b/docs/api-coverage.md index 5efb439..5bc6354 100644 --- a/docs/api-coverage.md +++ b/docs/api-coverage.md @@ -27,7 +27,7 @@ A generic request method does not mean that every endpoint has a typed wrapper, | System | `systemInfo()`, `systemProperties()`, `systemThreads()`, `systemLogging()` | System info, non-empty JVM/process properties, a non-empty thread dump, and logger metadata are exercised on pinned Solr 8.11.4/9.11.0/10.1.0. Optional fields remain version-dependent; response models preserve unknown fields. See Issues #89 and #91. | | Metrics | `metrics()` with typed group/registry/prefix/regex filters; exported `SolrMetricsJsonResponse`, `SolrMetricsRegistry`, and open `SolrMetricValue`; JSON/Prometheus response union | JVM response-format coverage remains distinct (observed JSON on Solr 8/9 and Prometheus exposition on 10.1). A `node` group integration check runs on all pinned versions, asserting a non-empty JSON registry on Solr 8/9 and Prometheus samples on 10.1; registry, prefix, and regex filters are asserted against JSON responses on Solr 8/9 (Solr 10.1 supports Prometheus/OpenMetrics, not the JSON writer). Consumer fixtures verify the extensible registry/value envelope; plugin-specific metrics remain open and unenumerated. See Issues #90, #91, and #236. | | Backup and restore | `replication("backup" | "restore" | "restorestatus" | "deletebackup")`, `collections("BACKUP" | "RESTORE" | "LISTBACKUP" | "DELETEBACKUP" | "REQUESTSTATUS" | "DELETESTATUS")` | Typed core and SolrCloud backup/restore parameters and response models, including async status polling. Full core restore and SolrCloud collection backup/restore cycles are integration-tested on Solr 8.11.4/9.11.0/10.1.0 using local filesystem repositories and shared SolrCloud storage. See [backup and restore docs](backup-restore.md). | -| Replication | `replication(command, params)`, `replicationFile(params)`; exported command-specific parameter and response models | V1 ReplicationHandler commands for index version, details, file list, leader/follower control (Solr 9/10 fetchindex/abortfetch; Solr 8 snappull/abort), core backup/restore/status/delete, and raw file-content streaming. Integration coverage includes core backup/restore cycles on Solr 8.11.4/9.11.0/10.1.0. A disposable two-core leader/follower fetch/read-back check is implemented but remains pending runtime verification until its pinned-version CI jobs pass; it uses `snappull` on Solr 8 and `fetchindex` on Solr 9/10. Abort and polling lifecycle behavior remains unit-only. See Issue #95 and [replication docs](replication.md). | +| Replication | `replication(command, params)`, `replicationFile(params)`; exported command-specific parameter and response models | V1 ReplicationHandler commands for index version, details, file list, leader/follower control (fetchindex/abortfetch across Solr 8/9/10), core backup/restore/status/delete, and raw file-content streaming. Integration coverage includes core backup/restore cycles on Solr 8.11.4/9.11.0/10.1.0. A disposable two-core leader/follower fetch/read-back check is implemented but remains pending runtime verification until its pinned-version CI jobs pass; it uses `fetchindex`/`abortfetch` across Solr 8/9/10. Abort and polling lifecycle behavior remains unit-only. See Issue #95 and [replication docs](replication.md). | | Streaming Expressions | `stream()` | Sends `expr` with `wt=json` and parses the complete JSON response through the shared transport. It is not an incremental tuple-stream parser: no chunk-boundary protocol or memory-bound/backpressure guarantee is claimed. SolrCloud search is integration-tested on pinned Solr 8.11.4/9.11.0/10.1.0; unit tests cover JSON parsing, caller cancellation during body consumption, and cleanup after body-stream errors. See [Streaming Expressions](streaming.md) and Issue #93. | | Export | `export()`, `exportStream()`; exported `SolrExportParams`, `SolrExportSort`, and `SolrExportFieldType` | Export requests require typed `fl` and `sort` parameters, support repeated filter queries, structured or raw sort expressions, batch sizing, JSON/JavaBin writers, and an explicit custom-parameter escape hatch. The raw Response preserves backpressure-aware streaming; `exportStream()` exposes its byte body directly. Sort fields must be single-valued docValues fields (int, long, float, double, or string); field lists additionally support date and boolean and may be multi-valued. Solr 10.1 `includeStoredFields` support is exposed but requires version-specific verification. Existing raw Export response tests pass on pinned Solr 8.11.4/9.11.0/10.1.0; typed parameter serialization and streamed chunk consumption have unit coverage. See [Export docs](export.md) and Issues #92–#94. | | Advanced / Raw | `requestRaw(path, options)` with typed method/query/header/body/control options and a raw Fetch `Response` | Relative paths and same-origin absolute URLs use the shared transport. Cross-origin URLs, embedded credentials, and URL fragments are rejected before Fetch, preventing configured credentials from being sent to unrelated hosts. Generic request()/requestRaw() transport behavior has unit coverage only; arbitrary custom endpoint behavior remains unverified until endpoint-specific integration tests exist. See [raw requests](raw-requests.md) and Issue #98. | From 6ad447192057f10c37998da523c569e838d3b9e8 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:24:29 +0100 Subject: [PATCH 58/62] docs: align replication compatibility matrix --- docs/endpoint-inventory.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/endpoint-inventory.md b/docs/endpoint-inventory.md index bd6950c..7c40c31 100644 --- a/docs/endpoint-inventory.md +++ b/docs/endpoint-inventory.md @@ -19,7 +19,7 @@ This inventory defines the boundary of the typed client API after review against | Metrics API `/solr/admin/metrics` | `metrics()` | Typed response models; JSON metrics on Solr 8/9 and Prometheus exposition text on Solr 10.1 are documented. | | Streaming Expressions `/stream` | `stream()` | Expression request support and JSON response parsing; tested against SolrCloud on all three pinned versions. | | Export handler `/export` | `export()`, `exportStream()` | Typed export parameters and raw backpressure-aware response/byte-stream access; docValues sort-field flow tested on all three pinned versions. | -| ReplicationHandler V1 `/solr/{core}/replication` | `replication()`, `replicationFile()` | Typed commands include indexversion, details, filelist, fetch/poll/enable/disable/abort, backup, restore, restorestatus, and deletebackup (including Solr 8 snappull/abort and Solr 9/10 fetchindex/abortfetch variants), plus raw file-content streaming. Core backup/restore cycles are integration-tested on all three pinned versions. A complete leader/follower fetch cycle is not yet certified. | +| ReplicationHandler V1 `/solr/{core}/replication` | `replication()`, `replicationFile()` | Typed commands include indexversion, details, filelist, fetch/poll/enable/disable/abort, backup, restore, restorestatus, and deletebackup (using fetchindex/abortfetch across the pinned Solr 8/9/10 versions), plus raw file-content streaming. Core backup/restore cycles are integration-tested on all three pinned versions. A complete leader/follower fetch cycle is not yet certified. | | Generic/custom request handlers | `request()`, `requestRaw()` | Escape hatches for custom paths and parameters. These do not imply typed support or integration certification for arbitrary endpoints. | ## Explicitly deferred or excluded endpoint families From 9336c68b5ef7b013902bc2fe43aa2d02b34c3a07 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:55:36 +0100 Subject: [PATCH 59/62] test(solr): use core URL for replication leader --- test/integration/solr-replication-lifecycle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index eeb1baf..a3d9ada 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -19,7 +19,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso }); const leader = makeClient(leaderCore); const follower = makeClient(followerCore); - const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}/replication`; + const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}`; const documentId = createId("replication_lifecycle"); const leaderConfig = await leader.config({ From 5d9fb34893b87f8cd665ad12173abaf98625d329 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 12:55:38 +0100 Subject: [PATCH 60/62] docs(solr): clarify replication leader URL format --- docs/replication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/replication.md b/docs/replication.md index 4220cd3..68632b4 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -46,7 +46,7 @@ The `indexversion` response includes the current `generation` on newer Solr vers // Use fetchindex/abortfetch across the pinned Solr 8, 9, and 10 versions. // leaderUrl may be omitted when configured on the follower. await client.replication("fetchindex", { - leaderUrl: "http://leader:8983/solr/catalog/replication", + leaderUrl: "http://leader:8983/solr/catalog", compression: "internal", httpConnTimeout: 5_000, httpReadTimeout: 10_000 @@ -59,7 +59,7 @@ await client.replication("enablepoll"); await client.replication("abortfetch"); ``` -`fetchindex`/`abortfetch` are the canonical fetch and cancellation commands across the pinned Solr 8.11.4, 9.11.0, and 10.1.0 versions. `leaderUrl` is the fully qualified ReplicationHandler URL, including the `/replication` suffix. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. +`fetchindex`/`abortfetch` are the canonical fetch and cancellation commands across the pinned Solr 8.11.4, 9.11.0, and 10.1.0 versions. `leaderUrl` is the leader core URL without the `/replication` suffix; Solr appends the handler path when fetching files. For Basic authentication between the follower and its leader, configure the ReplicationHandler's server-side follower settings; do not put credentials into URLs or query parameters. ## Control a leader From 2071c907aacdd51c869ba34f6b0a71c314a505bd Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 15:55:10 +0100 Subject: [PATCH 61/62] test(solr): pass replication credentials to fetchindex --- test/integration/solr-replication-lifecycle.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index a3d9ada..e1367a7 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -48,7 +48,10 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso assert.ok(Number.isFinite(leaderGeneration) && leaderGeneration > 0, "leader must publish a replicable index"); const fetchCommand = "fetchindex"; - const fetchResponse = await follower.replication(fetchCommand, { leaderUrl }); + const fetchResponse = await follower.replication(fetchCommand, { + leaderUrl, + ...(username && password ? { httpBasicAuthUser: username, httpBasicAuthPassword: password } : {}) + }); assert.equal(fetchResponse.responseHeader?.status, 0, `${fetchCommand} should complete successfully`); // Replication copies Lucene index files asynchronously; assert the resulting From 18076cb6d4b1ee11fa115bc216777b4b0459996b Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 15:55:17 +0100 Subject: [PATCH 62/62] test(solr): type replication fetch auth parameters --- test/integration/solr-replication-lifecycle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index e1367a7..cc7eda8 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -50,7 +50,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const fetchCommand = "fetchindex"; const fetchResponse = await follower.replication(fetchCommand, { leaderUrl, - ...(username && password ? { httpBasicAuthUser: username, httpBasicAuthPassword: password } : {}) + ...(username && password ? { customParams: { httpBasicAuthUser: username, httpBasicAuthPassword: password } } : {}) }); assert.equal(fetchResponse.responseHeader?.status, 0, `${fetchCommand} should complete successfully`);