Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
Remove the nested v3/ subdirectory by moving all active source code,
tests, configs, and assets directly to the project root. Archive v2/
(dead code, 847MB) to v2-archive.tar.gz.

Key changes:
- Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/
- Move v3/tsconfig.json, v3/vitest.config.ts to root
- Merge v3/package.json into root package.json (ESM, scripts, deps, exports)
- Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/
- Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes
- Fix all init installer path resolution (3-level-up → 2-level-up)
- Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe"
- Update settings.json ADR/DDD directories to docs/implementation/
- Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks)
- Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces
- Update 15 guidance shards: v3/src/ → src/ path references
- Fix sync interfaces: remove v3/ and v2/ path references
- Fix test path calculations (process.cwd() + '..' no longer needed)
- Fix flaky perf test threshold (500ms → 1000ms for CI environments)
- Fix complexity-analyzer test to match ADR-051 keyword-based eligibility

Build passes, 17,901 tests pass, CLI and MCP bundles verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:35:41 +00:00

247 lines
6.7 KiB
TypeScript

/**
* G-Counter Unit Tests
*
* Tests for the Grow-only Counter CRDT implementation.
* Verifies CRDT properties: convergence, commutativity, idempotency.
*/
import { describe, it, expect } from 'vitest';
import {
createGCounter,
createGCounterFromState,
isGCounterState,
getNodeCountFromState,
getContributingNodes,
} from '../../../src/memory/crdt/g-counter.js';
describe('G-Counter', () => {
describe('increment and value', () => {
it('should start at zero', () => {
// Arrange
const counter = createGCounter('node-a');
// Act
const value = counter.get();
// Assert
expect(value).toBe(0);
});
it('should increment by 1 by default', () => {
// Arrange
const counter = createGCounter('node-a');
// Act
counter.increment();
// Assert
expect(counter.get()).toBe(1);
});
it('should increment by arbitrary positive value', () => {
// Arrange
const counter = createGCounter('node-a');
// Act
counter.increment(42);
// Assert
expect(counter.get()).toBe(42);
});
it('should accumulate multiple increments', () => {
// Arrange
const counter = createGCounter('node-a');
// Act
counter.increment(3);
counter.increment(7);
counter.increment(5);
// Assert
expect(counter.get()).toBe(15);
expect(counter.getLocalCount()).toBe(15);
});
it('should reject negative increment values', () => {
// Arrange
const counter = createGCounter('node-a');
// Act & Assert
expect(() => counter.increment(-1)).toThrow('G-Counter can only increment by positive values');
});
it('should no-op on zero increment', () => {
// Arrange
const counter = createGCounter('node-a');
counter.increment(5);
const stateBefore = counter.getState();
// Act
counter.increment(0);
// Assert
expect(counter.get()).toBe(5);
expect(counter.getState().version).toBe(stateBefore.version);
});
});
describe('convergence after merge', () => {
it('should converge two replicas with independent increments to same value', () => {
// Arrange
const replicaA = createGCounter('node-a');
const replicaB = createGCounter('node-b');
replicaA.increment(10);
replicaB.increment(7);
// Act - merge in both directions
const mergedA = createGCounterFromState('node-a', replicaA.getState());
mergedA.applyState(replicaB.getState());
const mergedB = createGCounterFromState('node-b', replicaB.getState());
mergedB.applyState(replicaA.getState());
// Assert - both converge to 17
expect(mergedA.get()).toBe(17);
expect(mergedB.get()).toBe(17);
});
it('should converge three replicas after pairwise merges', () => {
// Arrange
const a = createGCounter('node-a');
const b = createGCounter('node-b');
const c = createGCounter('node-c');
a.increment(5);
b.increment(3);
c.increment(8);
// Act - merge all into each
a.merge(b);
a.merge(c);
b.merge(a); // b now has a's merged state (which includes c)
c.merge(a);
// Assert
expect(a.get()).toBe(16);
expect(b.get()).toBe(16);
expect(c.get()).toBe(16);
});
});
describe('commutativity', () => {
it('should produce same result regardless of merge order: merge(a,b) === merge(b,a)', () => {
// Arrange
const a = createGCounter('node-a');
const b = createGCounter('node-b');
a.increment(4);
b.increment(9);
// Act
const ab = createGCounterFromState('node-x', a.getState());
ab.applyState(b.getState());
const ba = createGCounterFromState('node-y', b.getState());
ba.applyState(a.getState());
// Assert
expect(ab.get()).toBe(ba.get());
expect(ab.get()).toBe(13);
});
});
describe('idempotency', () => {
it('should produce same value when merging with self: merge(a,a) === a', () => {
// Arrange
const counter = createGCounter('node-a');
counter.increment(7);
const valueBefore = counter.get();
// Act
counter.applyState(counter.getState());
// Assert
expect(counter.get()).toBe(valueBefore);
expect(counter.get()).toBe(7);
});
it('should produce same value when merging same state multiple times', () => {
// Arrange
const a = createGCounter('node-a');
const b = createGCounter('node-b');
a.increment(5);
b.increment(3);
const stateB = b.getState();
// Act - apply same state 3 times
a.applyState(stateB);
a.applyState(stateB);
a.applyState(stateB);
// Assert
expect(a.get()).toBe(8);
});
});
describe('factory from state', () => {
it('should restore counter from serialized state', () => {
// Arrange
const original = createGCounter('node-a');
original.increment(12);
const state = original.getState();
// Act
const restored = createGCounterFromState('node-a', state);
// Assert
expect(restored.get()).toBe(12);
expect(restored.getLocalCount()).toBe(12);
});
});
describe('type guards and utilities', () => {
it('should validate correct GCounterState', () => {
// Arrange
const counter = createGCounter('node-a');
counter.increment(5);
const state = counter.getState();
// Assert
expect(isGCounterState(state)).toBe(true);
});
it('should reject invalid state objects', () => {
expect(isGCounterState(null)).toBe(false);
expect(isGCounterState({})).toBe(false);
expect(isGCounterState({ counts: { a: -1 }, version: 0, lastUpdated: 0 })).toBe(false);
});
it('should get node count from state', () => {
// Arrange
const counter = createGCounter('node-a');
counter.increment(8);
// Act & Assert
expect(getNodeCountFromState(counter.getState(), 'node-a')).toBe(8);
expect(getNodeCountFromState(counter.getState(), 'node-b')).toBe(0);
});
it('should get contributing nodes', () => {
// Arrange
const a = createGCounter('node-a');
const b = createGCounter('node-b');
a.increment(3);
b.increment(5);
a.merge(b);
// Act
const contributors = getContributingNodes(a.getState());
// Assert
expect(contributors).toContain('node-a');
expect(contributors).toContain('node-b');
expect(contributors).toHaveLength(2);
});
});
});