2025-09-12 14:37:39 -07:00
|
|
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
|
|
|
import {
|
|
|
|
|
validateFindParams,
|
|
|
|
|
validateAddParams,
|
|
|
|
|
validateUpdateParams,
|
|
|
|
|
validateRelateParams,
|
|
|
|
|
getValidationConfig,
|
|
|
|
|
recordQueryPerformance
|
|
|
|
|
} from '../../../src/utils/paramValidation.js'
|
|
|
|
|
import { NounType, VerbType } from '../../../src/types/graphTypes.js'
|
|
|
|
|
import { FindParams, AddParams, UpdateParams, RelateParams } from '../../../src/types/brainy.types.js'
|
|
|
|
|
|
|
|
|
|
describe('Zero-Config Parameter Validation', () => {
|
|
|
|
|
|
|
|
|
|
describe('validateFindParams', () => {
|
|
|
|
|
|
|
|
|
|
it('should accept valid parameters', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
query: 'test query',
|
|
|
|
|
limit: 10,
|
|
|
|
|
offset: 0
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
type: NounType.Document,
|
|
|
|
|
where: { status: 'active' }
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should reject negative limit', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
limit: -1
|
|
|
|
|
})).toThrow('limit must be non-negative')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should reject negative offset', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
offset: -1
|
|
|
|
|
})).toThrow('offset must be non-negative')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should reject threshold outside 0-1 range', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
near: { id: 'test', threshold: -0.1 }
|
|
|
|
|
})).toThrow('threshold must be between 0 and 1')
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
near: { id: 'test', threshold: 1.1 }
|
|
|
|
|
})).toThrow('threshold must be between 0 and 1')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should reject both query and vector', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
query: 'test',
|
|
|
|
|
vector: new Array(384).fill(0)
|
|
|
|
|
})).toThrow('cannot specify both query and vector')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should reject both cursor and offset', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
cursor: 'abc123',
|
|
|
|
|
offset: 10
|
|
|
|
|
})).toThrow('cannot use both cursor and offset pagination')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate vector dimensions', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
vector: new Array(100).fill(0) // Wrong dimensions
|
|
|
|
|
})).toThrow('vector must have exactly 384 dimensions')
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
vector: new Array(384).fill(0) // Correct dimensions
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate NounType enum', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
type: 'InvalidType' as any
|
|
|
|
|
})).toThrow('invalid NounType: InvalidType')
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
type: NounType.Document
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate array of NounTypes', () => {
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
type: [NounType.Document, NounType.Person]
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
type: [NounType.Document, 'InvalidType' as any]
|
|
|
|
|
})).toThrow('invalid NounType: InvalidType')
|
|
|
|
|
})
|
|
|
|
|
|
fix: recalibrate find({ limit }) cap + two-tier enforcement + caller location
Brainy 7.30.0 introduced a memory-derived synchronous cap on `find({ limit })`
to prevent OOM. The cap was sound in intent but ~4x too conservative in
calibration: assumed 100 KB per result while typical entity footprint is 7-10 KB
(384-dim float32 vector ≈ 1.5 KB + standard fields + metadata). On a 900 MB
free-memory box the cap derived to 9000 — breaking common safety-cap patterns
like `find({ type, where, limit: 10_000 })` that typically return 10-500
entities. Surfaced as a runtime regression with cascading 500s degrading
production dashboards.
Three concurrent fixes:
A. RECALIBRATE THE FORMULA
- src/utils/paramValidation.ts:175,196,212 — the three memory-derived priorities
(reservedQueryMemory / containerMemory / freeMemory) all divided by
100 * 1024 * 1024 (100 KB per result, ~10-15x over conservative). Replaced
with a new MAX_LIMIT_KB_PER_RESULT = 25 constant that matches observed
entity size.
- Result: 4 GB container cap goes 10_000 → 40_000; 2 GB cap goes 5_000 →
20_000; 900 MB free-memory cap goes 9_000 → ~36_000. 100k hard ceiling
unchanged. `maxQueryLimit` / `reservedQueryMemory` constructor overrides
unchanged in behavior.
B. TWO-TIER ENFORCEMENT (warn-then-throw)
- Below cap (limit <= maxLimit): silent pass, unchanged.
- Soft tier (maxLimit < limit <= 2 * maxLimit): NEW — one-time warning per
call site (dedup keyed on caller stack frame + limit value), query
proceeds. Pre-7.30.2 code that relied on the cap silently allowing typical
safety-cap limits keeps working; the warning teaches the recipe so consumers
can fix it intentionally.
- Hard tier (limit > 2 * maxLimit): throw with the same teaching message
format. Real OOM territory; the cap stops being a recommendation and becomes
a guardrail.
- The 2x soft margin absorbs typical safety-cap patterns (limit: 10_000
against a 9 K-cap box) without disabling OOM protection. Real OOM territory
on a JS in-memory brain is hundreds of thousands of results, not 10x the
safety cap.
C. IMPROVED ERROR / WARNING MESSAGE
- Same shape as the 7.30.1 enforcement-error messages: state the problem,
name the three escape valves (maxQueryLimit / reservedQueryMemory /
pagination), include caller location, link to docs.
- Extracted findCallerLocation() helper from brainy.ts to a new
src/utils/callerLocation.ts so both the subtype enforcement (7.30.1) and
the limit enforcement (7.30.2) share one implementation without circular
imports.
DOCS
- New docs/guides/find-limits.md (public: true) — full reference: why the cap
exists, the four memory sources the auto-config considers, the three escape
valves with when-to-use-which guidance, and an explicit "pagination is the
future-proof pattern" callout (8.0 may tighten the cap further; pagination
keeps working unchanged).
- docs/api/README.md find() entry gets a one-paragraph `limit` tip + pointer
to the new guide.
- RELEASES.md v7.30.2 entry.
TESTS
- New tests/integration/find-limits.test.ts (9 tests): below-cap silent pass;
soft-tier warns once per call site (dedup verified by exercising same vs.
different source lines via wrapper closures); soft-tier message format
(names all three escape valves + docs link); soft-tier message includes
caller location; hard-tier throws; hard-tier message format same as
soft-tier; consumer maxQueryLimit override raises the cap and shifts both
tiers accordingly; pre-7.30.2 regression scenario explicitly covered.
- tests/unit/utils/memoryLimits.test.ts — 4 tests updated for the recalibrated
cap values (hardcoded expected numbers bumped 4x to match new 25 KB/result
assumption).
- tests/unit/utils/paramValidation.test.ts — auto-limit test extended to cover
the three-tier semantics (below-cap pass / soft-tier silent / hard-tier
throw).
- Existing suites unchanged: subtype-and-facets 26/26, verb-subtype-and-
enforcement 30/30, strict-mode-self-test 13/13. Unit 1468/1468.
CORTEX COMPATIBILITY
- Zero Cortex changes required. Every change is JS-side: formula recalibration
runs in ValidationConfig.constructor(), two-tier enforcement runs in
validateFindParams(), both fire before any storage / index / Cortex call.
- The new guide notes that Brainy 8.0's Datomic-style Db.find() may tighten
per-call limits to keep snapshot semantics cheap; pagination remains the
pattern that's guaranteed to keep working.
REPO-WIDE CLEANUP
Brainy is the only Soulcraft project that is open source. This commit also
scrubs closed-source product names and product-specific class/field references
from every tracked file in the repo (src/, docs/, tests/, RELEASES.md,
CHANGELOG.md). Consumer-reported bugs, regression scenarios, and release
notes now refer to "a consumer", "a downstream application", "a production
deployment", or "an internal report" — never to the named product. Two
product-named test files renamed to neutral diagnostic names. CLAUDE.md gains
a project-level guard rule documenting the policy and an example list of the
identifiers that may not appear in tracked code.
Verification
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- All four integration subtype + verb + strict + find-limits suites: 78/78
- npm run build: clean
- Closed-source product reference audit: clean
2026-06-08 12:34:05 -07:00
|
|
|
it('should auto-limit based on system memory (two-tier enforcement)', () => {
|
2025-09-12 14:37:39 -07:00
|
|
|
const config = getValidationConfig()
|
fix: recalibrate find({ limit }) cap + two-tier enforcement + caller location
Brainy 7.30.0 introduced a memory-derived synchronous cap on `find({ limit })`
to prevent OOM. The cap was sound in intent but ~4x too conservative in
calibration: assumed 100 KB per result while typical entity footprint is 7-10 KB
(384-dim float32 vector ≈ 1.5 KB + standard fields + metadata). On a 900 MB
free-memory box the cap derived to 9000 — breaking common safety-cap patterns
like `find({ type, where, limit: 10_000 })` that typically return 10-500
entities. Surfaced as a runtime regression with cascading 500s degrading
production dashboards.
Three concurrent fixes:
A. RECALIBRATE THE FORMULA
- src/utils/paramValidation.ts:175,196,212 — the three memory-derived priorities
(reservedQueryMemory / containerMemory / freeMemory) all divided by
100 * 1024 * 1024 (100 KB per result, ~10-15x over conservative). Replaced
with a new MAX_LIMIT_KB_PER_RESULT = 25 constant that matches observed
entity size.
- Result: 4 GB container cap goes 10_000 → 40_000; 2 GB cap goes 5_000 →
20_000; 900 MB free-memory cap goes 9_000 → ~36_000. 100k hard ceiling
unchanged. `maxQueryLimit` / `reservedQueryMemory` constructor overrides
unchanged in behavior.
B. TWO-TIER ENFORCEMENT (warn-then-throw)
- Below cap (limit <= maxLimit): silent pass, unchanged.
- Soft tier (maxLimit < limit <= 2 * maxLimit): NEW — one-time warning per
call site (dedup keyed on caller stack frame + limit value), query
proceeds. Pre-7.30.2 code that relied on the cap silently allowing typical
safety-cap limits keeps working; the warning teaches the recipe so consumers
can fix it intentionally.
- Hard tier (limit > 2 * maxLimit): throw with the same teaching message
format. Real OOM territory; the cap stops being a recommendation and becomes
a guardrail.
- The 2x soft margin absorbs typical safety-cap patterns (limit: 10_000
against a 9 K-cap box) without disabling OOM protection. Real OOM territory
on a JS in-memory brain is hundreds of thousands of results, not 10x the
safety cap.
C. IMPROVED ERROR / WARNING MESSAGE
- Same shape as the 7.30.1 enforcement-error messages: state the problem,
name the three escape valves (maxQueryLimit / reservedQueryMemory /
pagination), include caller location, link to docs.
- Extracted findCallerLocation() helper from brainy.ts to a new
src/utils/callerLocation.ts so both the subtype enforcement (7.30.1) and
the limit enforcement (7.30.2) share one implementation without circular
imports.
DOCS
- New docs/guides/find-limits.md (public: true) — full reference: why the cap
exists, the four memory sources the auto-config considers, the three escape
valves with when-to-use-which guidance, and an explicit "pagination is the
future-proof pattern" callout (8.0 may tighten the cap further; pagination
keeps working unchanged).
- docs/api/README.md find() entry gets a one-paragraph `limit` tip + pointer
to the new guide.
- RELEASES.md v7.30.2 entry.
TESTS
- New tests/integration/find-limits.test.ts (9 tests): below-cap silent pass;
soft-tier warns once per call site (dedup verified by exercising same vs.
different source lines via wrapper closures); soft-tier message format
(names all three escape valves + docs link); soft-tier message includes
caller location; hard-tier throws; hard-tier message format same as
soft-tier; consumer maxQueryLimit override raises the cap and shifts both
tiers accordingly; pre-7.30.2 regression scenario explicitly covered.
- tests/unit/utils/memoryLimits.test.ts — 4 tests updated for the recalibrated
cap values (hardcoded expected numbers bumped 4x to match new 25 KB/result
assumption).
- tests/unit/utils/paramValidation.test.ts — auto-limit test extended to cover
the three-tier semantics (below-cap pass / soft-tier silent / hard-tier
throw).
- Existing suites unchanged: subtype-and-facets 26/26, verb-subtype-and-
enforcement 30/30, strict-mode-self-test 13/13. Unit 1468/1468.
CORTEX COMPATIBILITY
- Zero Cortex changes required. Every change is JS-side: formula recalibration
runs in ValidationConfig.constructor(), two-tier enforcement runs in
validateFindParams(), both fire before any storage / index / Cortex call.
- The new guide notes that Brainy 8.0's Datomic-style Db.find() may tighten
per-call limits to keep snapshot semantics cheap; pagination remains the
pattern that's guaranteed to keep working.
REPO-WIDE CLEANUP
Brainy is the only Soulcraft project that is open source. This commit also
scrubs closed-source product names and product-specific class/field references
from every tracked file in the repo (src/, docs/, tests/, RELEASES.md,
CHANGELOG.md). Consumer-reported bugs, regression scenarios, and release
notes now refer to "a consumer", "a downstream application", "a production
deployment", or "an internal report" — never to the named product. Two
product-named test files renamed to neutral diagnostic names. CLAUDE.md gains
a project-level guard rule documenting the policy and an example list of the
identifiers that may not appear in tracked code.
Verification
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- All four integration subtype + verb + strict + find-limits suites: 78/78
- npm run build: clean
- Closed-source product reference audit: clean
2026-06-08 12:34:05 -07:00
|
|
|
|
|
|
|
|
// 7.30.2+ design: the cap fires in two tiers.
|
|
|
|
|
// - Below cap (`limit <= maxLimit`): silent pass, no signal.
|
|
|
|
|
// - Soft tier (`maxLimit < limit <= 2 * maxLimit`): one-time warning
|
|
|
|
|
// per call site, query proceeds. No throw.
|
|
|
|
|
// - Hard tier (`limit > 2 * maxLimit`): real OOM danger zone, throw.
|
|
|
|
|
|
|
|
|
|
// Below cap → pass
|
|
|
|
|
expect(() => validateFindParams({ limit: config.maxLimit })).not.toThrow()
|
|
|
|
|
|
|
|
|
|
// Soft tier → no throw (just a warn we don't assert here — proper coverage
|
|
|
|
|
// lives in the find-limits integration suite which can intercept the log).
|
|
|
|
|
expect(() => validateFindParams({ limit: config.maxLimit + 1 })).not.toThrow()
|
|
|
|
|
expect(() => validateFindParams({ limit: config.maxLimit * 2 })).not.toThrow()
|
|
|
|
|
|
|
|
|
|
// Hard tier → throw with the new message format
|
2025-09-12 14:37:39 -07:00
|
|
|
expect(() => validateFindParams({
|
fix: recalibrate find({ limit }) cap + two-tier enforcement + caller location
Brainy 7.30.0 introduced a memory-derived synchronous cap on `find({ limit })`
to prevent OOM. The cap was sound in intent but ~4x too conservative in
calibration: assumed 100 KB per result while typical entity footprint is 7-10 KB
(384-dim float32 vector ≈ 1.5 KB + standard fields + metadata). On a 900 MB
free-memory box the cap derived to 9000 — breaking common safety-cap patterns
like `find({ type, where, limit: 10_000 })` that typically return 10-500
entities. Surfaced as a runtime regression with cascading 500s degrading
production dashboards.
Three concurrent fixes:
A. RECALIBRATE THE FORMULA
- src/utils/paramValidation.ts:175,196,212 — the three memory-derived priorities
(reservedQueryMemory / containerMemory / freeMemory) all divided by
100 * 1024 * 1024 (100 KB per result, ~10-15x over conservative). Replaced
with a new MAX_LIMIT_KB_PER_RESULT = 25 constant that matches observed
entity size.
- Result: 4 GB container cap goes 10_000 → 40_000; 2 GB cap goes 5_000 →
20_000; 900 MB free-memory cap goes 9_000 → ~36_000. 100k hard ceiling
unchanged. `maxQueryLimit` / `reservedQueryMemory` constructor overrides
unchanged in behavior.
B. TWO-TIER ENFORCEMENT (warn-then-throw)
- Below cap (limit <= maxLimit): silent pass, unchanged.
- Soft tier (maxLimit < limit <= 2 * maxLimit): NEW — one-time warning per
call site (dedup keyed on caller stack frame + limit value), query
proceeds. Pre-7.30.2 code that relied on the cap silently allowing typical
safety-cap limits keeps working; the warning teaches the recipe so consumers
can fix it intentionally.
- Hard tier (limit > 2 * maxLimit): throw with the same teaching message
format. Real OOM territory; the cap stops being a recommendation and becomes
a guardrail.
- The 2x soft margin absorbs typical safety-cap patterns (limit: 10_000
against a 9 K-cap box) without disabling OOM protection. Real OOM territory
on a JS in-memory brain is hundreds of thousands of results, not 10x the
safety cap.
C. IMPROVED ERROR / WARNING MESSAGE
- Same shape as the 7.30.1 enforcement-error messages: state the problem,
name the three escape valves (maxQueryLimit / reservedQueryMemory /
pagination), include caller location, link to docs.
- Extracted findCallerLocation() helper from brainy.ts to a new
src/utils/callerLocation.ts so both the subtype enforcement (7.30.1) and
the limit enforcement (7.30.2) share one implementation without circular
imports.
DOCS
- New docs/guides/find-limits.md (public: true) — full reference: why the cap
exists, the four memory sources the auto-config considers, the three escape
valves with when-to-use-which guidance, and an explicit "pagination is the
future-proof pattern" callout (8.0 may tighten the cap further; pagination
keeps working unchanged).
- docs/api/README.md find() entry gets a one-paragraph `limit` tip + pointer
to the new guide.
- RELEASES.md v7.30.2 entry.
TESTS
- New tests/integration/find-limits.test.ts (9 tests): below-cap silent pass;
soft-tier warns once per call site (dedup verified by exercising same vs.
different source lines via wrapper closures); soft-tier message format
(names all three escape valves + docs link); soft-tier message includes
caller location; hard-tier throws; hard-tier message format same as
soft-tier; consumer maxQueryLimit override raises the cap and shifts both
tiers accordingly; pre-7.30.2 regression scenario explicitly covered.
- tests/unit/utils/memoryLimits.test.ts — 4 tests updated for the recalibrated
cap values (hardcoded expected numbers bumped 4x to match new 25 KB/result
assumption).
- tests/unit/utils/paramValidation.test.ts — auto-limit test extended to cover
the three-tier semantics (below-cap pass / soft-tier silent / hard-tier
throw).
- Existing suites unchanged: subtype-and-facets 26/26, verb-subtype-and-
enforcement 30/30, strict-mode-self-test 13/13. Unit 1468/1468.
CORTEX COMPATIBILITY
- Zero Cortex changes required. Every change is JS-side: formula recalibration
runs in ValidationConfig.constructor(), two-tier enforcement runs in
validateFindParams(), both fire before any storage / index / Cortex call.
- The new guide notes that Brainy 8.0's Datomic-style Db.find() may tighten
per-call limits to keep snapshot semantics cheap; pagination remains the
pattern that's guaranteed to keep working.
REPO-WIDE CLEANUP
Brainy is the only Soulcraft project that is open source. This commit also
scrubs closed-source product names and product-specific class/field references
from every tracked file in the repo (src/, docs/, tests/, RELEASES.md,
CHANGELOG.md). Consumer-reported bugs, regression scenarios, and release
notes now refer to "a consumer", "a downstream application", "a production
deployment", or "an internal report" — never to the named product. Two
product-named test files renamed to neutral diagnostic names. CLAUDE.md gains
a project-level guard rule documenting the policy and an example list of the
identifiers that may not appear in tracked code.
Verification
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- All four integration subtype + verb + strict + find-limits suites: 78/78
- npm run build: clean
- Closed-source product reference audit: clean
2026-06-08 12:34:05 -07:00
|
|
|
limit: config.maxLimit * 2 + 1
|
|
|
|
|
})).toThrow(/exceeds the auto-configured query limit/)
|
2025-09-12 14:37:39 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should auto-limit query length', () => {
|
|
|
|
|
const config = getValidationConfig()
|
|
|
|
|
const longQuery = 'a'.repeat(config.maxQueryLength + 1)
|
|
|
|
|
|
|
|
|
|
expect(() => validateFindParams({
|
|
|
|
|
query: longQuery
|
|
|
|
|
})).toThrow(`query exceeds auto-configured maximum length of ${config.maxQueryLength}`)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('validateAddParams', () => {
|
|
|
|
|
|
|
|
|
|
it('should accept valid add parameters', () => {
|
|
|
|
|
expect(() => validateAddParams({
|
|
|
|
|
data: 'test content',
|
|
|
|
|
type: NounType.Document
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
|
|
|
|
|
expect(() => validateAddParams({
|
|
|
|
|
vector: new Array(384).fill(0),
|
|
|
|
|
type: NounType.Person
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should require either data or vector', () => {
|
|
|
|
|
expect(() => validateAddParams({
|
|
|
|
|
type: NounType.Document
|
2025-09-22 15:45:35 -07:00
|
|
|
} as AddParams)).toThrow('Invalid add() parameters: Missing required field \'data\'')
|
2025-09-12 14:37:39 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate NounType', () => {
|
|
|
|
|
expect(() => validateAddParams({
|
|
|
|
|
data: 'test',
|
|
|
|
|
type: 'InvalidType' as any
|
2025-09-22 15:45:35 -07:00
|
|
|
})).toThrow('Invalid NounType: \'InvalidType\'')
|
2025-09-12 14:37:39 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate vector dimensions', () => {
|
|
|
|
|
expect(() => validateAddParams({
|
|
|
|
|
vector: new Array(100).fill(0),
|
|
|
|
|
type: NounType.Document
|
|
|
|
|
})).toThrow('vector must have exactly 384 dimensions')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('validateUpdateParams', () => {
|
|
|
|
|
|
|
|
|
|
it('should accept valid update parameters', () => {
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
id: 'test-id',
|
|
|
|
|
data: 'new content'
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
id: 'test-id',
|
|
|
|
|
metadata: { status: 'updated' }
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should require an ID', () => {
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
data: 'new content'
|
|
|
|
|
} as UpdateParams)).toThrow('id is required for update')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should require at least one field to update', () => {
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
id: 'test-id'
|
|
|
|
|
})).toThrow('must specify at least one field to update')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate NounType if changing', () => {
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
id: 'test-id',
|
|
|
|
|
type: 'InvalidType' as any
|
|
|
|
|
})).toThrow('invalid NounType: InvalidType')
|
|
|
|
|
|
|
|
|
|
expect(() => validateUpdateParams({
|
|
|
|
|
id: 'test-id',
|
|
|
|
|
type: NounType.Event
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('validateRelateParams', () => {
|
|
|
|
|
|
|
|
|
|
it('should accept valid relate parameters', () => {
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: VerbType.RelatedTo
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: VerbType.Creates,
|
|
|
|
|
weight: 0.8
|
|
|
|
|
})).not.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should require from and to', () => {
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: VerbType.RelatedTo
|
|
|
|
|
} as RelateParams)).toThrow('from entity ID is required')
|
|
|
|
|
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
type: VerbType.RelatedTo
|
|
|
|
|
} as RelateParams)).toThrow('to entity ID is required')
|
|
|
|
|
})
|
|
|
|
|
|
2025-10-09 16:33:08 -07:00
|
|
|
// Self-referential relationships are now allowed (valid in graph systems)
|
|
|
|
|
// Previous test "should reject self-referential relationships" removed
|
|
|
|
|
|
2025-09-12 14:37:39 -07:00
|
|
|
it('should validate VerbType', () => {
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: 'InvalidVerb' as any
|
|
|
|
|
})).toThrow('invalid VerbType: InvalidVerb')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate weight range', () => {
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: VerbType.RelatedTo,
|
|
|
|
|
weight: -0.1
|
|
|
|
|
})).toThrow('weight must be between 0 and 1')
|
|
|
|
|
|
|
|
|
|
expect(() => validateRelateParams({
|
|
|
|
|
from: 'entity1',
|
|
|
|
|
to: 'entity2',
|
|
|
|
|
type: VerbType.RelatedTo,
|
|
|
|
|
weight: 1.1
|
|
|
|
|
})).toThrow('weight must be between 0 and 1')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('Auto-configuration', () => {
|
|
|
|
|
|
|
|
|
|
it('should provide configuration based on system resources', () => {
|
|
|
|
|
const config = getValidationConfig()
|
|
|
|
|
|
|
|
|
|
expect(config.maxLimit).toBeGreaterThan(0)
|
|
|
|
|
expect(config.maxLimit).toBeLessThanOrEqual(100000)
|
|
|
|
|
expect(config.maxQueryLength).toBeGreaterThan(0)
|
|
|
|
|
expect(config.maxVectorDimensions).toBe(384)
|
|
|
|
|
expect(config.systemMemory).toBeGreaterThan(0)
|
|
|
|
|
expect(config.availableMemory).toBeGreaterThan(0)
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-17 09:20:09 -07:00
|
|
|
it('never mutates the cap from query timing (telemetry only)', () => {
|
|
|
|
|
const initialLimit = getValidationConfig().maxLimit
|
|
|
|
|
|
|
|
|
|
// Fast queries with large results: no silent growth.
|
2025-09-12 14:37:39 -07:00
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
recordQueryPerformance(50, initialLimit * 0.9)
|
|
|
|
|
}
|
2026-07-17 09:20:09 -07:00
|
|
|
expect(getValidationConfig().maxLimit).toBe(initialLimit)
|
|
|
|
|
|
|
|
|
|
// A burst of catastrophically slow queries must not strangle the cap.
|
|
|
|
|
// The removed "learning" ratchet shrank it 20% per recorded query down
|
|
|
|
|
// to a floor of 1000 — below the documented MIN_AUTO_QUERY_LIMIT — and
|
|
|
|
|
// the error message blamed "available free memory" (a production
|
|
|
|
|
// incident: every find({ limit: 5000 }) failed on an idle 23GB-free box).
|
|
|
|
|
for (let i = 0; i < 50; i++) {
|
|
|
|
|
recordQueryPerformance(90_000, 100)
|
2025-09-12 14:37:39 -07:00
|
|
|
}
|
2026-07-17 09:20:09 -07:00
|
|
|
expect(getValidationConfig().maxLimit).toBe(initialLimit)
|
2025-09-12 14:37:39 -07:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|