From edd72169b57f10349b7c9fd6ce1e3467c6c71d44 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:22:32 +0100 Subject: [PATCH 1/9] 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..d9f7e67 --- /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({ + "add-requesthandler": { + name: "/replication", + class: "solr.ReplicationHandler", + customOptions: { leader: { replicateAfter: "commit" } } + } + }); + await follower.config({ + "add-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({ 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 d0885dc02837c3bd75fb9d557e0aa6af8e80f1d4 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:22:47 +0100 Subject: [PATCH 2/9] test(replication): document leader follower lifecycle coverage --- 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 7cd35c3..073971c 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 6793173dc37858588d40f9d5f5fb0f52378e7978 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:22:49 +0100 Subject: [PATCH 3/9] test(replication): document leader follower lifecycle coverage --- .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 d801c08b0b333216557ef67ba7d696e2c1ec36a8 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:22:52 +0100 Subject: [PATCH 4/9] test(replication): document leader follower lifecycle coverage --- 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 8ed4948..02fdac5 100644 --- a/docs/api-inventory.json +++ b/docs/api-inventory.json @@ -395,11 +395,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 2252b38701137c35b0b376fb63366b09f7470292 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:22:53 +0100 Subject: [PATCH 5/9] test(replication): document leader follower lifecycle coverage --- docs/replication.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/replication.md b/docs/replication.md index 375730c..b5961dc 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -102,5 +102,7 @@ Supported directory types are `file` (Lucene index files), `cf` (configuration f - 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. +- 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 c18ad8d39ce75e8f45df630892404a698ff6f3cf Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:22:56 +0100 Subject: [PATCH 6/9] test(replication): document leader follower lifecycle coverage --- 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 d410c54..c17dd41 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 88f1d0205924ea67e3ade7227f36d671749cc17d Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:22:58 +0100 Subject: [PATCH 7/9] test(replication): document leader follower lifecycle coverage --- docs/compatibility.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index b2cfcdc..aba7989 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 a97a0d9e7fd0fd8dc8066be8b6a35a2891393751 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:29:09 +0100 Subject: [PATCH 8/9] fix(replication): update existing handler and type query response --- 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 d9f7e67..466fbf0 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -23,7 +23,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const documentId = createId("replication_lifecycle"); await leader.config({ - "add-requesthandler": { + "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", customOptions: { leader: { replicateAfter: "commit" } } From 0e3c06345f882643bca6baa6cca3f64a4b02c05a Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 07:29:16 +0100 Subject: [PATCH 9/9] fix(replication): update existing follower handler --- 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 466fbf0..7b21fcb 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -30,7 +30,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso } }); await follower.config({ - "add-requesthandler": { + "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", customOptions: { follower: { leaderUrl } }