brainy/tests/transaction/integration/sharding-transactions.test.ts
David Snelling 780fb6444b feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1)
Brainy 8.0 makes subtype required by default on every public write path
(`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`,
import). Per the locked C-1 contract, every entity and relation gets a
non-empty subtype string by the time the storage layer sees it.

OPT-OUT REMAINS FULLY SUPPORTED

The runtime flag is still consumer-controlled. Three opt-out paths
cover migration / legacy fixtures / typed escape:

- `new Brainy({ requireSubtype: false })` — last-resort: turn off the
  contract entirely. Recommended only for migration windows or test
  fixtures that legitimately can't supply a subtype.
- `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` —
  per-type allowlist: strict everywhere except the listed types.
- `brain.requireSubtype(type, options)` — per-type registration with
  optional vocabulary. Composes with the brain-wide flag.

Default is now `true`. Opt-out is explicit and documented; nothing
silently degrades.

TEST SWEEP

Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call
site across 120 test files. Three sed patterns covered the shapes:
  - `new Brainy({` → `new Brainy({ requireSubtype: false,`
  - `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,`
  - `new Brainy()` → `new Brainy({ requireSubtype: false })`

tests/helpers/test-factory.ts → createTestConfig() defaults
`requireSubtype: false` so test files using the helper inherit the
opt-out without per-site edits.

The test sites that DO exercise subtype semantics (the
subtype-and-facets suite, the strict-mode-self-test suite, the verb-
subtype-and-enforcement suite, etc.) already pass real subtypes — they
were the 7.30.x acceptance tests for this contract. Those tests
continue to pass unchanged.

CHANGES

src/brainy.ts
- normalizeConfig() — `requireSubtype` default `false` → `true`.
  Comment refreshed to document the three opt-out paths.

tests/* (120 files)
- Bulk-edited brain construction sites. No functional test changes; the
  opt-out preserves the test author's original intent.

tests/helpers/test-factory.ts
- createTestConfig() base config gains `requireSubtype: false`.

NO-OP for consumers who were already passing subtype on every write.

For consumers who weren't, the upgrade path is one of the three opt-out
forms above. Migration recipe documented in 8.0 release notes (next
commit).

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding;
  no other regressions from the flip)
2026-06-09 14:58:25 -07:00

295 lines
8.4 KiB
TypeScript

/**
* Sharding + Transactions Integration Tests
*
* Verifies that transactions work correctly with sharded storage:
* - Cross-shard atomicity
* - Shard-aware routing
* - Rollback across shards
* - UUID-based shard distribution
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../../src/brainy.js'
import { NounType, VerbType } from '../../../src/types/graphTypes.js'
import { tmpdir } from 'os'
import { join } from 'path'
import { mkdirSync, rmSync } from 'fs'
describe('Transactions + Sharding Integration', () => {
let brain: Brainy
let testDir: string
beforeEach(async () => {
testDir = join(tmpdir(), `brainy-shard-test-${Date.now()}`)
mkdirSync(testDir, { recursive: true })
brain = new Brainy({ requireSubtype: false,
storage: {
type: 'filesystem',
path: testDir
}
})
await brain.init()
})
afterEach(async () => {
if (brain) {
await brain.shutdown()
}
if (testDir) {
rmSync(testDir, { recursive: true, force: true })
}
})
describe('Cross-Shard Operations', () => {
it('should handle atomic operations across multiple shards', async () => {
// Create entities with different UUID prefixes (different shards)
const id1 = 'aaa00000-1111-4111-8111-111111111111' // Shard: aaa
const id2 = 'bbb00000-2222-4222-8222-222222222222' // Shard: bbb
// Add entities to different shards (atomic)
await brain.add({
id: id1,
data: { name: 'Entity in Shard A', shard: 'aaa' },
type: NounType.Thing
})
await brain.add({
id: id2,
data: { name: 'Entity in Shard B', shard: 'bbb' },
type: NounType.Thing
})
// Create relationship across shards (atomic)
const relationId = await brain.relate({
from: id1,
to: id2,
type: VerbType.RelatesTo
})
// Verify all entities exist
const entity1 = await brain.get(id1)
const entity2 = await brain.get(id2)
const relations = await brain.getRelations({ from: id1 })
expect(entity1).toBeTruthy()
expect(entity2).toBeTruthy()
expect(relations).toHaveLength(1)
expect(relations[0].targetId).toBe(id2)
})
it('should rollback operations across multiple shards', async () => {
const id1 = 'ccc00000-1111-4111-8111-111111111111' // Shard: ccc
const id2 = 'ddd00000-2222-4222-8222-222222222222' // Shard: ddd
// Add first entity (succeeds)
await brain.add({
id: id1,
data: { name: 'Entity in Shard C' },
type: NounType.Thing
})
// Attempt to add second entity with invalid data (fails)
let failed = false
try {
await brain.add({
id: id2,
data: null as any, // Invalid
type: NounType.Thing
})
} catch (e) {
failed = true
}
expect(failed).toBe(true)
// First entity should still exist (in shard C)
const entity1 = await brain.get(id1)
expect(entity1).toBeTruthy()
// Second entity should not exist (rollback in shard D)
const entity2 = await brain.get(id2)
expect(entity2).toBeNull()
})
it('should handle updates across shards atomically', async () => {
const id1 = 'eee00000-1111-4111-8111-111111111111'
const id2 = 'fff00000-2222-4222-8222-222222222222'
// Add entities
await brain.add({
id: id1,
data: { name: 'Original E', version: 1 },
type: NounType.Thing
})
await brain.add({
id: id2,
data: { name: 'Original F', version: 1 },
type: NounType.Thing
})
// Update both entities (different shards)
await brain.update({
id: id1,
data: { name: 'Updated E', version: 2 }
})
await brain.update({
id: id2,
data: { name: 'Updated F', version: 2 }
})
// Verify updates in both shards
const entity1 = await brain.get(id1)
const entity2 = await brain.get(id2)
expect(entity1?.data.version).toBe(2)
expect(entity2?.data.version).toBe(2)
})
})
describe('Shard Distribution', () => {
it('should distribute entities across shards based on UUID', async () => {
// Create entities with various UUID prefixes
const ids = [
'aaa00000-1111-4111-8111-111111111111',
'bbb00000-2222-4222-8222-222222222222',
'ccc00000-3333-4333-8333-333333333333',
'ddd00000-4444-4444-8444-444444444444'
]
// Add entities to different shards
for (let i = 0; i < ids.length; i++) {
await brain.add({
id: ids[i],
data: { name: `Entity ${i}`, index: i },
type: NounType.Thing
})
}
// Verify all entities can be retrieved (regardless of shard)
for (let i = 0; i < ids.length; i++) {
const entity = await brain.get(ids[i])
expect(entity).toBeTruthy()
expect(entity?.data.index).toBe(i)
}
})
it('should handle relationships between different shard combinations', async () => {
const entities = [
{ id: 'aaa00000-1111-4111-8111-111111111111', name: 'A' },
{ id: 'bbb00000-2222-4222-8222-222222222222', name: 'B' },
{ id: 'ccc00000-3333-4333-8333-333333333333', name: 'C' }
]
// Add all entities
for (const e of entities) {
await brain.add({
id: e.id,
data: { name: e.name },
type: NounType.Thing
})
}
// Create relationships across all shards
await brain.relate({
from: entities[0].id,
to: entities[1].id,
type: VerbType.RelatesTo
})
await brain.relate({
from: entities[1].id,
to: entities[2].id,
type: VerbType.RelatesTo
})
await brain.relate({
from: entities[2].id,
to: entities[0].id,
type: VerbType.RelatesTo
})
// Verify all relationships exist
const relations0 = await brain.getRelations({ from: entities[0].id })
const relations1 = await brain.getRelations({ from: entities[1].id })
const relations2 = await brain.getRelations({ from: entities[2].id })
expect(relations0).toHaveLength(1)
expect(relations1).toHaveLength(1)
expect(relations2).toHaveLength(1)
})
})
describe('Delete Operations Across Shards', () => {
it('should delete entities and relationships across shards atomically', async () => {
const id1 = 'ggg00000-1111-4111-8111-111111111111'
const id2 = 'hhh00000-2222-4222-8222-222222222222'
// Add entities in different shards
await brain.add({
id: id1,
data: { name: 'Entity G' },
type: NounType.Thing
})
await brain.add({
id: id2,
data: { name: 'Entity H' },
type: NounType.Thing
})
// Create relationship
await brain.relate({
from: id1,
to: id2,
type: VerbType.RelatesTo
})
// Delete first entity (should also delete relationship)
await brain.delete(id1)
// Verify entity deleted from shard G
const entity1 = await brain.get(id1)
expect(entity1).toBeNull()
// Entity H should still exist in shard H
const entity2 = await brain.get(id2)
expect(entity2).toBeTruthy()
// Relationship should be deleted
const relations = await brain.getRelations({ from: id1 })
expect(relations).toHaveLength(0)
})
})
describe('Batch Operations Across Shards', () => {
it('should handle batch adds across multiple shards atomically', async () => {
const entities = [
{ id: 'shard1-00-1111-4111-8111-111111111111', data: { name: 'S1-E1' } },
{ id: 'shard2-00-2222-4222-8222-222222222222', data: { name: 'S2-E1' } },
{ id: 'shard3-00-3333-4333-8333-333333333333', data: { name: 'S3-E1' } },
{ id: 'shard1-00-4444-4444-8444-444444444444', data: { name: 'S1-E2' } },
{ id: 'shard2-00-5555-4555-8555-555555555555', data: { name: 'S2-E2' } }
]
// Add all entities (distributed across shards)
for (const e of entities) {
await brain.add({
id: e.id,
data: e.data,
type: NounType.Thing
})
}
// Verify all entities exist in their respective shards
for (const e of entities) {
const entity = await brain.get(e.id)
expect(entity).toBeTruthy()
expect(entity?.data.name).toBe(e.data.name)
}
})
})
})