Skip to content

Conversation

@Snider
Copy link
Owner

@Snider Snider commented Nov 3, 2025

No description provided.

Snider added 17 commits November 3, 2025 16:49
…ltidimensional points (performance weighted route discovery)
@codecov
Copy link

codecov bot commented Nov 3, 2025

Codecov Report

❌ Patch coverage is 71.80451% with 150 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
kdtree.go 70.37% 47 Missing and 25 partials ⚠️
kdtree_helpers.go 74.91% 35 Missing and 35 partials ⚠️
kdtree_gonum_stub.go 11.11% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

coderabbitai[bot]

This comment was marked as off-topic.

coderabbitai[bot]

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as outdated.

google-labs-jules bot and others added 2 commits November 4, 2025 10:37
This commit introduces a comprehensive test suite for the `gonum` backend, which was previously untested. It also adds tests for the `kdtree_helpers` package, specifically for the `ComputeNormStats3D` and `BuildND` functions.

The new tests cover a wide range of scenarios, including:
- Basic functionality of `Nearest`, `KNearest`, and `Radius`
- Edge cases such as empty trees, zero/negative inputs, and mismatched dimensions
- Various data configurations, including collinear points and negative coordinates

This commit also includes minor fixes to the existing tests to improve their robustness and accuracy.

As a result of these changes, the overall test coverage of the project has been increased from 80% to over 90%.
feat: Increase test coverage to over 90%
Repository owner deleted a comment from coderabbitai bot Nov 4, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 4, 2025

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":404,"request":{"method":"PATCH","url":"https://api.github.com/repos/Snider/Poindexter/issues/comments/3482443961","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.5 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n<!-- This is an auto-generated comment: release notes by coderabbit.ai -->\n\n## Summary by CodeRabbit\n\n## Release Notes\n\n* **New Features**\n  * Added KDTree nearest-neighbour search with support for multiple distance metrics (Euclidean, Manhattan, Chebyshev, Cosine)\n  * Multi-dimensional point support (2D, 3D, 4D, N-dimensional) with per-axis weighting and normalisation\n  * WebAssembly build for browser environments with TypeScript types and ESM loader\n  * Point insertion, deletion, and dynamic tree updates\n  * Radius and k-nearest-neighbour query operations\n\n* **Documentation**\n  * Comprehensive API documentation with usage examples\n  * Multiple runnable examples (DHT routing, multi-dimensional queries)\n  * Browser/WASM integration guide\n  * Performance benchmarking guidance\n\n* **Chores**\n  * Version bumped to 0.3.0\n  * Added CI/release automation workflows\n  * Added development tooling and build system\n\n<!-- end of auto-generated comment: release notes by coderabbit.ai -->\n## Walkthrough\n\nAdds a generic KDTree implementation with multiple distance metrics, normalization builders (ND/2D/3D/4D), Gonum backend (with stub), extensive tests (unit, fuzz, benchmarks), WASM bindings and npm package (JS loaders, types), examples/docs, CI/release workflows, maintainer Makefile, and version bump to 0.3.0.\n\n## Changes\n\n| Cohort / File(s) | Summary |\n|---|---|\n| **CI / Release workflows** <br> `\\.github/workflows/ci.yml`, `\\.github/workflows/release.yml` | New CI workflow with build-test-wasm and build-test-gonum jobs; updated release job renamed to release with Go 1.23, tidy check, build, race tests and adjusted goreleaser args. |\n| **Lint & Release config** <br> `\\.golangci\\.yml`, `\\.goreleaser\\.yaml`, `\\.goreleaser\\.yml` | Added golangci-lint config and library-focused GoReleaser configurations (no binary builds, checksums, GitHub changelog, snapshot settings). |\n| **Makefile & ignores** <br> `Makefile`, `\\.gitignore` | New maintainer Makefile with CI-parity targets (wasm-build, npm-pack, test, fuzz, bench, docs, release, etc.); `.gitignore` adds `bench.txt` and `coverage.html`. |\n| **Community & governance** <br> `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `SECURITY.md` | Added Contributor Covenant Code of Conduct, contributing guidelines (build/test/lint/fuzz/release), and a security policy with vulnerability reporting instructions. |\n| **Core library — KDTree** <br> `kdtree.go`, `doc.go`, `sort.go`, `poindexter.go`, `poindexter_test.go` | New generic KDTree[T], KDPoint type, DistanceMetric interface and concrete metrics; constructors NewKDTree/NewKDTreeFromDim; methods Dim/Len/Nearest/KNearest/Radius/Insert/DeleteByID; package docs; Version() bumped to 0.3.0 and test updated. |\n| **Normalization & builders** <br> `kdtree_helpers.go` | AxisStats/NormStats and ComputeNormStats helpers; BuildND and Build2D/3D/4D (and WithStats/NoErr variants) implementing normalization, weighting and inversion with validations. |\n| **Gonum backend / parity** <br> `kdtree_gonum.go`, `kdtree_gonum_stub.go`, `kdtree_backend_parity_test.go` | Gonum-backed backend (build tag `gonum`) implementing nearest/KNN/radius; non-gonum stub fallback; parity tests comparing backends. |\n| **Tests, benchmarks & fuzzing** <br> `kdtree_test.go`, `kdtree_*_test.go`, `bench_kdtree_*.go`, `bench_kdtree_dual*.go`, `fuzz_kdtree_test.go` | Extensive unit tests (construction, edge cases, metrics), fuzz tests, and benchmark suites for linear and gonum backends across sizes/distributions. |\n| **Examples & demos** <br> `examples/*` (dht_ping_1d, kdtree_2d_*, kdtree_3d_*, kdtree_4d_*, wasm-browser-ts, wasm-browser, dht_helpers, wasm examples) | New runnable examples and tests demonstrating 1D/2D/3D/4D builds, weighting/inversion, DHT ping examples, WASM browser demos and helper wrappers. |\n| **WASM / JS loader / npm package** <br> `wasm/main.go`, `npm/poindexter-wasm/*` | Go wasm main exposing px* functions; ESM loader (`loader.js`), CJS placeholder (`loader.cjs`), TypeScript declarations (`index.d.ts`), package.json, README, LICENSE, PROJECT_README, smoke script and npm packaging files. |\n| **Docs & site content** <br> `docs/*`, `README.md`, `CHANGELOG.md`, `mkdocs.yml` | Large documentation additions (API reference, KDTree helpers, perf, wasm, examples), README updates and badges, comprehensive CHANGELOG including v0.3.0, and MkDocs navigation updates. |\n| **Module / toolchain** <br> `go.mod` | Go toolchain version set to 1.23 in `go.mod`. |\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n    participant Client as Client code\n    participant KD as KDTree\n    participant Metric as DistanceMetric\n\n    Client->>KD: NewKDTree(points, WithMetric(m))\n    KD->>KD: validate points, dimension, unique IDs\n    KD-->>Client: KDTree instance\n\n    Client->>KD: Nearest(query)\n    KD->>KD: verify query dim\n    loop compute distances\n        KD->>Metric: Distance(point.coords, query)\n        Metric-->>KD: distance\n    end\n    KD-->>Client: nearest point, distance, found\n```\n\n```mermaid\nsequenceDiagram\n    participant Browser as Web app\n    participant Loader as loader.js\n    participant Runtime as wasm_exec.js (Go)\n    participant Wasm as poindexter.wasm\n\n    Browser->>Loader: init({wasmURL, wasmExecURL})\n    Loader->>Runtime: load wasm_exec.js\n    Loader->>Wasm: fetch & instantiate (or instantiateWasm)\n    Wasm->>Runtime: start Go runtime\n    Runtime->>Loader: register px* functions\n    Loader-->>Browser: API {version, hello, newTree}\n    Browser->>Loader: api.newTree(dim) => PxTree\n    Browser->>PxTree: nearest(query)\n    PxTree->>Runtime: call pxNearest\n    Runtime->>Wasm: execute nearest\n    Wasm-->>PxTree: result\n    PxTree-->>Browser: result (point, dist, found)\n```\n\n## Poem\n\n> 🐰 I hopped through points both near and far,  \n> I learned each metric, weight and bar.  \n> From Go to wasm I bounded clear,  \n> Nearest neighbours now appear.  \n> Docs, tests and builds — the path is here.\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n## Pre-merge checks and finishing touches\n<details>\n<summary>❌ Failed checks (1 warning, 1 inconclusive)</summary>\n\n|     Check name    | Status         | Explanation                                                                                                                                    | Resolution                                                                                                                                                                                                                    |\n| :---------------: | :------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Description check | ⚠️ Warning     | No pull request description was provided by the author, making it impossible to verify that the objectives and scope are clearly communicated. | Add a comprehensive pull request description outlining the changes, motivation, key features added (KDTree implementation, examples, CI workflows, documentation), and any breaking changes or migration notes.               |\n|    Title check    | ❓ Inconclusive | The title 'Kd tree peer finding' is generic and lacks clarity about the specific changes introduced in this comprehensive PR.                  | Consider a more descriptive title that reflects the main additions, such as 'Add KDTree implementation with peer-finding examples and CI/release workflows' or 'Implement KDTree with multi-dimensional support and tooling'. |\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `kd-tree-peer-finding`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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= -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/Snider/Poindexter/issues/comments/3482443961","status":404,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","content-encoding":"gzip","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Tue, 04 Nov 2025 10:51:00 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","transfer-encoding":"chunked","vary":"Accept-Encoding, Accept, X-Requested-With","x-accepted-github-permissions":"issues=write; pull_requests=write","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-api-version-selected":"2022-11-28","x-github-media-type":"github.v3; format=json","x-github-request-id":"80E7:870AD:FF3ACB:4597BAC:6909DA94","x-ratelimit-limit":"5000","x-ratelimit-remaining":"4911","x-ratelimit-reset":"1762256666","x-ratelimit-resource":"core","x-ratelimit-used":"89","x-xss-protection":"0"},"data":{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#update-an-issue-comment","status":"404"}}}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38a6c6a and fa42914.

📒 Files selected for processing (2)
  • kdtree_gonum_test.go (1 hunks)
  • kdtree_helpers_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
kdtree_gonum_test.go (1)
kdtree.go (8)
  • KDPoint (25-29)
  • NewKDTree (225-270)
  • WithBackend (203-203)
  • BackendGonum (194-194)
  • WithMetric (198-198)
  • BackendLinear (193-193)
  • ErrEmptyPoints (11-11)
  • NewKDTreeFromDim (274-294)
kdtree_helpers_test.go (2)
kdtree_helpers.go (12)
  • Build2D (225-258)
  • Build3D (288-329)
  • Build4D (363-412)
  • ComputeNormStats2D (173-183)
  • Build2DWithStats (261-285)
  • NormStats (29-31)
  • AxisStats (22-25)
  • Build3DWithStats (332-360)
  • ComputeNormStats4D (202-218)
  • Build4DWithStats (415-447)
  • ComputeNormStats3D (186-199)
  • BuildND (71-89)
kdtree.go (1)
  • NewKDTree (225-270)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: build-test-gonum
  • GitHub Check: build-test-wasm
  • GitHub Check: build-test-wasm
  • GitHub Check: build-test-gonum
🔇 Additional comments (24)
kdtree_helpers_test.go (8)

8-33: LGTM!

The test correctly validates normalization, inversion, and weighting logic. The inline comments clearly explain the expected coordinate transformations.


35-62: LGTM!

The test correctly validates the min==max edge case where scale01 returns 0. The constant ID is acceptable since this test doesn't construct a KDTree that would enforce uniqueness.


65-112: LGTM!

Excellent end-to-end test that validates the complete workflow from 4D building through normalization, weighting, inversion, to nearest-neighbour queries. The comment on line 108 helpfully documents the expected outcome.


114-154: LGTM!

This parity test correctly validates that Build2D and Build2DWithStats produce identical coordinates, ensuring consistency between the automatic and explicit stats paths.


156-178: LGTM!

Correctly validates that Build3DWithStats handles min==max axes by producing zero coordinates, consistent with the scale01 function behaviour.


227-245: LGTM!

The test correctly validates ComputeNormStats3D. The manual field comparison is clear and appropriate for this simple structure.


247-267: LGTM!

The test correctly validates BuildND's happy path with 3 dimensions, checking both point count and dimensionality.


269-283: LGTM!

The test correctly validates that BuildND returns an error when the weights slice length doesn't match the number of extractors.

kdtree_gonum_test.go (16)

1-8: LGTM!

The build tag, package declaration, and imports are correct. The build tag ensures these tests only run when the Gonum backend is available.


10-20: LGTM!

The equalish helper correctly implements tolerance-based floating-point comparison.


53-69: LGTM!

Basic smoke test for Nearest with the Gonum backend is correct.


71-92: LGTM!

Basic smoke test for KNearest with the Gonum backend is correct.


94-115: LGTM!

Basic smoke test for Radius with the Gonum backend is correct.


117-128: LGTM!

Correctly verifies that unsupported metrics cause a fallback to the linear backend.


137-142: Testing internal implementation details.

This test directly calls the internal axisStd function. If axisStd is not part of the public API, this test couples your test suite to internal implementation details. Consider whether this behaviour is adequately exercised through public API tests, or whether axisStd should be exported if it requires direct testing.


152-165: LGTM!

Correctly verifies that dimension mismatches are handled gracefully.


181-193: LGTM!

Correctly verifies that zero-K queries return empty results.


195-207: LGTM!

Correctly verifies that negative radius queries return empty results.


209-221: LGTM!

Correctly verifies behaviour with a single-point tree.


238-254: LGTM!

Correctly verifies KNearest with k=1.


255-264: LGTM!

Correctly tests empty tree behaviour using the public API.


265-275: Testing internal implementation details.

This test directly calls the internal buildKDRecursive function. Consider whether this behaviour is adequately covered by public API tests, such as constructing trees with single points and querying them.


277-291: LGTM!

Correctly verifies that KNearest returns all available points when k exceeds the tree size.


292-625: Comprehensive test coverage.

The remaining test functions provide excellent coverage of various edge cases and scenarios:

  • Identical and overlapping points
  • Various distance scenarios
  • Collinear points
  • Negative coordinates
  • Extreme float values
  • Multi-dimensional inputs
  • Ordering guarantees

This thorough testing will help ensure the Gonum backend behaves correctly.

Comment on lines +22 to +51
func TestGonumKnnHeap(t *testing.T) {
h := knnHeap{}

h.push(knnItem{idx: 1, dist: 1.0})
h.push(knnItem{idx: 2, dist: 2.0})
h.push(knnItem{idx: 3, dist: 0.5})

if h.Len() != 3 {
t.Errorf("expected heap length 3, got %d", h.Len())
}

item := h.pop()
if item.idx != 2 || item.dist != 2.0 {
t.Errorf("expected item with index 2 and dist 2.0, got idx %d dist %f", item.idx, item.dist)
}

item = h.pop()
if item.idx != 1 || item.dist != 1.0 {
t.Errorf("expected item with index 1 and dist 1.0, got idx %d dist %f", item.idx, item.dist)
}

item = h.pop()
if item.idx != 3 || item.dist != 0.5 {
t.Errorf("expected item with index 3 and dist 0.5, got idx %d dist %f", item.idx, item.dist)
}

if h.Len() != 0 {
t.Errorf("expected heap length 0, got %d", h.Len())
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Testing internal implementation details.

This test directly accesses the internal knnHeap type. Whilst thorough unit testing of internal structures can be valuable, consider whether this behaviour is sufficiently covered by the public API tests. If knnHeap is meant to remain internal, ensure changes to its implementation won't break these tests unnecessarily.

Additionally, this test is largely duplicated by TestGonumKnnHeapPop (lines 222-236). Consider consolidating or removing the duplicate.

🤖 Prompt for AI Agents
In kdtree_gonum_test.go around lines 22-51, the test inspects the internal
knnHeap type and duplicates behavior already covered by TestGonumKnnHeapPop;
update the suite by either (A) removing these lines and relying on the existing
TestGonumKnnHeapPop to cover heap behavior, or (B) rewriting this test to
exercise the public API (call the exported KNN/search functions) instead of
directly using knnHeap so internal implementation changes won't break tests; if
you keep both, consolidate assertions into a single test to avoid duplication
and ensure the remaining test asserts identical expected ordering and length
checks.

Comment on lines +130 to +179
func TestGonumNearestWithEmptyTree(t *testing.T) {
_, err := NewKDTree([]KDPoint[int]{}, WithBackend(BackendGonum))
if err != ErrEmptyPoints {
t.Fatalf("expected ErrEmptyPoints, got %v", err)
}
}

func TestAxisStdWithNoPoints(t *testing.T) {
stds := axisStd(nil, nil, 2)
if len(stds) != 2 || stds[0] != 0 || stds[1] != 0 {
t.Errorf("expected [0, 0], got %v", stds)
}
}

func TestGonumNearestWithNilRoot(t *testing.T) {
backend := &kdBackend{root: nil, dim: 2}
_, _, ok := gonumNearest[int](backend, []float64{1, 1})
if ok {
t.Error("expected no point found, but got one")
}
}

func TestGonumNearestWithMismatchedDimensions(t *testing.T) {
points := []KDPoint[int]{
{ID: "1", Coords: []float64{1, 1}},
}
tree, err := NewKDTree(points, WithBackend(BackendGonum))
if err != nil {
t.Fatal(err)
}

_, _, ok := tree.Nearest([]float64{1, 1, 1})
if ok {
t.Error("expected no point found, but got one")
}
}

func TestGonumKNearestWithEmptyTree(t *testing.T) {
_, err := NewKDTree([]KDPoint[int]{}, WithBackend(BackendGonum))
if err != ErrEmptyPoints {
t.Fatalf("expected ErrEmptyPoints, got %v", err)
}
}

func TestGonumRadiusWithEmptyTree(t *testing.T) {
_, err := NewKDTree([]KDPoint[int]{}, WithBackend(BackendGonum))
if err != ErrEmptyPoints {
t.Fatalf("expected ErrEmptyPoints, got %v", err)
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider consolidating duplicate empty tree tests.

Three separate tests (TestGonumNearestWithEmptyTree, TestGonumKNearestWithEmptyTree, TestGonumRadiusWithEmptyTree) verify the same behaviour: that constructing a KD-tree with an empty point slice returns ErrEmptyPoints. Consider consolidating these into a single test, as the error occurs during construction, not during the query methods.

Comment on lines +144 to +150
func TestGonumNearestWithNilRoot(t *testing.T) {
backend := &kdBackend{root: nil, dim: 2}
_, _, ok := gonumNearest[int](backend, []float64{1, 1})
if ok {
t.Error("expected no point found, but got one")
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Testing internal implementation details.

This test directly constructs the internal kdBackend struct and calls gonumNearest. Consider using the public API (NewKDTreeFromDim followed by Nearest) to achieve the same test coverage without coupling to internal structures. For example, lines 255-264 demonstrate a similar test using the public API.

🤖 Prompt for AI Agents
In kdtree_gonum_test.go around lines 144 to 150, the test constructs the
internal kdBackend and calls gonumNearest directly, coupling the test to
internal implementation; replace this with the public API by creating a KDTree
with NewKDTreeFromDim(2) (or equivalent constructor) to get an empty tree, then
call tree.Nearest with the query point and assert that no point is found (ok ==
false); ensure the test imports/uses the public types and methods and remove
direct references to kdBackend and gonumNearest so the test verifies behavior
through the public API only.

Comment on lines +571 to +593
func TestGonumKNearestWithMorePoints(t *testing.T) {
points := []KDPoint[int]{
{ID: "1", Coords: []float64{0, 0}},
{ID: "2", Coords: []float64{1, 1}},
{ID: "3", Coords: []float64{2, 2}},
{ID: "4", Coords: []float64{3, 3}},
{ID: "5", Coords: []float64{4, 4}},
}
tree, err := NewKDTree(points, WithBackend(BackendGonum))
if err != nil {
t.Fatal(err)
}
ps, _ := tree.KNearest([]float64{0.5, 0.5}, 3)
if len(ps) != 3 {
t.Fatalf("expected 3 points, got %d", len(ps))
}
if !((ps[0].ID == "1" && ps[1].ID == "2") || (ps[0].ID == "2" && ps[1].ID == "1")) {
t.Errorf("expected first two points to be 1 and 2, got %s and %s", ps[0].ID, ps[1].ID)
}
if ps[2].ID != "3" {
t.Errorf("expected third point to be 3, got %s", ps[2].ID)
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Simplify complex assertion logic.

The assertion on lines 587-589 is difficult to read:

if !((ps[0].ID == "1" && ps[1].ID == "2") || (ps[0].ID == "2" && ps[1].ID == "1")) {

Consider using a helper function or restructuring the check for clarity:

-	if !((ps[0].ID == "1" && ps[1].ID == "2") || (ps[0].ID == "2" && ps[1].ID == "1")) {
-		t.Errorf("expected first two points to be 1 and 2, got %s and %s", ps[0].ID, ps[1].ID)
-	}
+	ids := []string{ps[0].ID, ps[1].ID}
+	if !((ids[0] == "1" && ids[1] == "2") || (ids[0] == "2" && ids[1] == "1")) {
+		t.Errorf("expected first two points to be 1 and 2 in any order, got %s and %s", ids[0], ids[1])
+	}

Or even better, sort and compare:

ids := []string{ps[0].ID, ps[1].ID}
sort.Strings(ids)
if ids[0] != "1" || ids[1] != "2" {
	t.Errorf("expected first two points to be 1 and 2 in any order, got %v", ids)
}
🤖 Prompt for AI Agents
In kdtree_gonum_test.go around lines 571 to 593, the assertion that the first
two returned IDs are "1" and "2" in any order is written as a nested boolean
expression which is hard to read; replace that check with a clearer approach:
collect the two IDs into a slice, sort the slice, and assert the sorted result
equals ["1","2"] (update the error message to print the sorted ids on failure);
ensure you add the necessary import for sort if not present.

Comment on lines +211 to +218
newPts, _ := Build4DWithStats([]Peer{newPeer},
func(p Peer) string { return p.ID },
func(p Peer) float64 { return p.Ping },
func(p Peer) float64 { return p.Hops },
func(p Peer) float64 { return p.Geo },
func(p Peer) float64 { return p.Score },
weights, invert, stats,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Check the error from Build4DWithStats.

The error return is silently ignored, which could hide failures and lead to panics if newPts is nil or empty when accessed on line 219.

Apply this diff to check the error:

-	newPts, _ := Build4DWithStats([]Peer{newPeer},
+	newPts, err := Build4DWithStats([]Peer{newPeer},
 		func(p Peer) string { return p.ID },
 		func(p Peer) float64 { return p.Ping },
 		func(p Peer) float64 { return p.Hops },
 		func(p Peer) float64 { return p.Geo },
 		func(p Peer) float64 { return p.Score },
 		weights, invert, stats,
 	)
+	if err != nil {
+		t.Fatalf("build new peer err: %v", err)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
newPts, _ := Build4DWithStats([]Peer{newPeer},
func(p Peer) string { return p.ID },
func(p Peer) float64 { return p.Ping },
func(p Peer) float64 { return p.Hops },
func(p Peer) float64 { return p.Geo },
func(p Peer) float64 { return p.Score },
weights, invert, stats,
)
newPts, err := Build4DWithStats([]Peer{newPeer},
func(p Peer) string { return p.ID },
func(p Peer) float64 { return p.Ping },
func(p Peer) float64 { return p.Hops },
func(p Peer) float64 { return p.Geo },
func(p Peer) float64 { return p.Score },
weights, invert, stats,
)
if err != nil {
t.Fatalf("build new peer err: %v", err)
}
🤖 Prompt for AI Agents
In kdtree_helpers_test.go around lines 211 to 218, the error returned by
Build4DWithStats is being ignored which can mask failures and cause panics when
newPts is used; update the call to capture the error value, check it
immediately, and fail the test if non-nil (e.g., call t.Fatalf or t.Fatal with
the error) so failures are reported and the test stops before dereferencing
newPts.

@Snider Snider merged commit 1a6ab5b into main Nov 4, 2025
4 of 7 checks passed
@Snider Snider deleted the kd-tree-peer-finding branch November 4, 2025 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants