nounCountsByType (read by stats().entitiesByType / counts.byTypeEnum) was incremented in saveNoun_internal(), which runs on every noun write — including the HNSW index re-saving a node whenever its neighbor links change. So per-type counts grew with graph connectivity instead of entity count (an internal report saw 8 documents read as 44). Moved the increment into saveNounMetadata_internal(), gated on isNew + visibility, exactly parallel to the authoritative total; the visibility-flip path adjusts it in lockstep too. Tests: tests/unit/storage/stats-count-accuracy.test.ts (30 dense-type entities -> exactly 30 across stats/counts/getNounCount) + de-theatricalized the >=2 assertion in brainy-core.unit.test.ts to exact equality. Build green; 1455 unit green. Part of the 8.0 readiness audit Phase 1; cold-reopen count rehydration is still WIP.
58 lines
2.6 KiB
TypeScript
58 lines
2.6 KiB
TypeScript
/**
|
|
* @module tests/unit/storage/stats-count-accuracy
|
|
* @description Regression for per-type count inflation. `nounCountsByType` (read by
|
|
* `brain.stats().entitiesByType` and `counts.byTypeEnum`) used to be incremented in
|
|
* `saveNoun_internal()`, which runs on EVERY noun write — including the HNSW index
|
|
* re-saving a node whenever its neighbor links change. So the per-type counts grew
|
|
* with graph connectivity rather than entity count (an internal report saw 8
|
|
* documents read as 44). The counter is now maintained in `saveNounMetadata_internal()`
|
|
* gated on `isNew` + visibility, exactly parallel to the authoritative total — so the
|
|
* per-type counts must equal the entity count EXACTLY, no matter how dense the graph.
|
|
*
|
|
* These assert exact equality on purpose: the prior `toBeGreaterThanOrEqual` style
|
|
* passed for any inflated value and masked the bug.
|
|
*/
|
|
import { describe, it, expect } from 'vitest'
|
|
import { Brainy } from '../../../src/brainy.js'
|
|
import { NounType } from '../../../src/types/graphTypes.js'
|
|
|
|
describe('per-type count accuracy (HNSW re-save inflation)', () => {
|
|
it('stats()/counts report the EXACT entity count for a single dense type', async () => {
|
|
const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
|
await brain.init()
|
|
try {
|
|
// Enough nodes that HNSW links each new node to existing neighbors and
|
|
// re-saves them — the exact path that used to double-count.
|
|
const N = 30
|
|
for (let i = 0; i < N; i++) {
|
|
await brain.add({ data: { name: `person ${i}` }, type: NounType.Person })
|
|
}
|
|
|
|
const stats = await brain.stats()
|
|
expect(stats.entityCount).toBe(N)
|
|
expect(stats.entitiesByType[NounType.Person]).toBe(N)
|
|
expect(brain.counts.byTypeEnum(NounType.Person)).toBe(N)
|
|
expect(await brain.getNounCount()).toBe(N)
|
|
} finally {
|
|
await brain.close()
|
|
}
|
|
})
|
|
|
|
it('keeps per-type counts exact across multiple types', async () => {
|
|
const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
|
await brain.init()
|
|
try {
|
|
for (let i = 0; i < 12; i++) await brain.add({ data: { n: `p${i}` }, type: NounType.Person })
|
|
for (let i = 0; i < 7; i++) await brain.add({ data: { n: `d${i}` }, type: NounType.Document })
|
|
|
|
const stats = await brain.stats()
|
|
expect(stats.entitiesByType[NounType.Person]).toBe(12)
|
|
expect(stats.entitiesByType[NounType.Document]).toBe(7)
|
|
expect(stats.entityCount).toBe(19)
|
|
expect(brain.counts.byTypeEnum(NounType.Person)).toBe(12)
|
|
expect(brain.counts.byTypeEnum(NounType.Document)).toBe(7)
|
|
} finally {
|
|
await brain.close()
|
|
}
|
|
})
|
|
})
|