Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"start": "node index.js",
"run": "node index.js run",
"solidify": "node index.js solidify",
"test": "node --test test/*.test.js",
"a2a:export": "node scripts/a2a_export.js",
"a2a:ingest": "node scripts/a2a_ingest.js",
"a2a:promote": "node scripts/a2a_promote.js"
Expand Down
134 changes: 134 additions & 0 deletions test/a2aProtocol.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const {
PROTOCOL_NAME,
PROTOCOL_VERSION,
VALID_MESSAGE_TYPES,
buildMessage,
buildHello,
buildPublish,
buildFetch,
buildReport,
buildDecision,
buildRevoke,
isValidProtocolMessage,
unwrapAssetFromMessage,
} = require('../src/gep/a2aProtocol');

describe('protocol constants', () => {
it('has expected protocol name', () => {
assert.equal(PROTOCOL_NAME, 'gep-a2a');
});

it('has 6 valid message types', () => {
assert.equal(VALID_MESSAGE_TYPES.length, 6);
for (const t of ['hello', 'publish', 'fetch', 'report', 'decision', 'revoke']) {
assert.ok(VALID_MESSAGE_TYPES.includes(t), `missing type: ${t}`);
}
});
});

describe('buildMessage', () => {
it('builds a valid protocol message', () => {
const msg = buildMessage({ messageType: 'hello', payload: { test: true } });
assert.equal(msg.protocol, PROTOCOL_NAME);
assert.equal(msg.message_type, 'hello');
assert.ok(msg.message_id.startsWith('msg_'));
assert.ok(msg.timestamp);
assert.deepEqual(msg.payload, { test: true });
});

it('rejects invalid message type', () => {
assert.throws(() => buildMessage({ messageType: 'invalid' }), /Invalid message type/);
});
});

describe('typed message builders', () => {
it('buildHello includes env_fingerprint', () => {
const msg = buildHello({});
assert.equal(msg.message_type, 'hello');
assert.ok(msg.payload.env_fingerprint);
});

it('buildPublish requires asset with type and id', () => {
assert.throws(() => buildPublish({}), /asset must have type and id/);
assert.throws(() => buildPublish({ asset: { type: 'Gene' } }), /asset must have type and id/);

const msg = buildPublish({ asset: { type: 'Gene', id: 'g1' } });
assert.equal(msg.message_type, 'publish');
assert.equal(msg.payload.asset_type, 'Gene');
assert.equal(msg.payload.local_id, 'g1');
assert.ok(msg.payload.signature);
});

it('buildFetch creates a fetch message', () => {
const msg = buildFetch({ assetType: 'Capsule', localId: 'c1' });
assert.equal(msg.message_type, 'fetch');
assert.equal(msg.payload.asset_type, 'Capsule');
});

it('buildReport creates a report message', () => {
const msg = buildReport({ assetId: 'sha256:abc', validationReport: { ok: true } });
assert.equal(msg.message_type, 'report');
assert.equal(msg.payload.target_asset_id, 'sha256:abc');
});

it('buildDecision validates decision values', () => {
assert.throws(() => buildDecision({ decision: 'maybe' }), /decision must be/);

for (const d of ['accept', 'reject', 'quarantine']) {
const msg = buildDecision({ decision: d, assetId: 'test' });
assert.equal(msg.payload.decision, d);
}
});

it('buildRevoke creates a revoke message', () => {
const msg = buildRevoke({ assetId: 'sha256:abc', reason: 'outdated' });
assert.equal(msg.message_type, 'revoke');
assert.equal(msg.payload.reason, 'outdated');
});
});

describe('isValidProtocolMessage', () => {
it('returns true for well-formed messages', () => {
const msg = buildHello({});
assert.ok(isValidProtocolMessage(msg));
});

it('returns false for null/undefined', () => {
assert.ok(!isValidProtocolMessage(null));
assert.ok(!isValidProtocolMessage(undefined));
});

it('returns false for wrong protocol', () => {
assert.ok(!isValidProtocolMessage({ protocol: 'other', message_type: 'hello', message_id: 'x', timestamp: 'y' }));
});

it('returns false for missing fields', () => {
assert.ok(!isValidProtocolMessage({ protocol: PROTOCOL_NAME }));
});
});

describe('unwrapAssetFromMessage', () => {
it('extracts asset from publish message', () => {
const asset = { type: 'Gene', id: 'g1', strategy: ['test'] };
const msg = buildPublish({ asset });
const result = unwrapAssetFromMessage(msg);
assert.equal(result.type, 'Gene');
assert.equal(result.id, 'g1');
});

it('returns plain asset objects as-is', () => {
const gene = { type: 'Gene', id: 'g1' };
assert.deepEqual(unwrapAssetFromMessage(gene), gene);

const capsule = { type: 'Capsule', id: 'c1' };
assert.deepEqual(unwrapAssetFromMessage(capsule), capsule);
});

it('returns null for unrecognized input', () => {
assert.equal(unwrapAssetFromMessage(null), null);
assert.equal(unwrapAssetFromMessage({ random: true }), null);
assert.equal(unwrapAssetFromMessage('string'), null);
});
});
106 changes: 106 additions & 0 deletions test/contentHash.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { canonicalize, computeAssetId, verifyAssetId, SCHEMA_VERSION } = require('../src/gep/contentHash');

describe('canonicalize', () => {
it('serializes null and undefined as "null"', () => {
assert.equal(canonicalize(null), 'null');
assert.equal(canonicalize(undefined), 'null');
});

it('serializes primitives', () => {
assert.equal(canonicalize(true), 'true');
assert.equal(canonicalize(false), 'false');
assert.equal(canonicalize(42), '42');
assert.equal(canonicalize('hello'), '"hello"');
});

it('serializes non-finite numbers as null', () => {
assert.equal(canonicalize(Infinity), 'null');
assert.equal(canonicalize(-Infinity), 'null');
assert.equal(canonicalize(NaN), 'null');
});

it('serializes arrays preserving order', () => {
assert.equal(canonicalize([1, 2, 3]), '[1,2,3]');
assert.equal(canonicalize([]), '[]');
});

it('serializes objects with sorted keys', () => {
assert.equal(canonicalize({ b: 2, a: 1 }), '{"a":1,"b":2}');
assert.equal(canonicalize({ z: 'last', a: 'first' }), '{"a":"first","z":"last"}');
});

it('produces deterministic output regardless of key insertion order', () => {
const obj1 = { c: 3, a: 1, b: 2 };
const obj2 = { a: 1, b: 2, c: 3 };
assert.equal(canonicalize(obj1), canonicalize(obj2));
});

it('handles nested objects and arrays', () => {
const nested = { arr: [{ b: 2, a: 1 }], val: null };
const result = canonicalize(nested);
assert.equal(result, '{"arr":[{"a":1,"b":2}],"val":null}');
});
});

describe('computeAssetId', () => {
it('returns a sha256-prefixed hash string', () => {
const id = computeAssetId({ type: 'Gene', id: 'test_gene' });
assert.ok(id.startsWith('sha256:'));
assert.equal(id.length, 7 + 64); // "sha256:" + 64 hex chars
});

it('excludes asset_id field from hash by default', () => {
const obj = { type: 'Gene', id: 'g1', data: 'x' };
const withoutField = computeAssetId(obj);
const withField = computeAssetId({ ...obj, asset_id: 'sha256:something' });
assert.equal(withoutField, withField);
});

it('produces identical hashes for identical content', () => {
const a = computeAssetId({ type: 'Capsule', id: 'c1', value: 42 });
const b = computeAssetId({ type: 'Capsule', id: 'c1', value: 42 });
assert.equal(a, b);
});

it('produces different hashes for different content', () => {
const a = computeAssetId({ type: 'Gene', id: 'g1' });
const b = computeAssetId({ type: 'Gene', id: 'g2' });
assert.notEqual(a, b);
});

it('returns null for non-object input', () => {
assert.equal(computeAssetId(null), null);
assert.equal(computeAssetId('string'), null);
});
});

describe('verifyAssetId', () => {
it('returns true for correct asset_id', () => {
const obj = { type: 'Gene', id: 'g1', data: 'test' };
obj.asset_id = computeAssetId(obj);
assert.ok(verifyAssetId(obj));
});

it('returns false for tampered content', () => {
const obj = { type: 'Gene', id: 'g1', data: 'test' };
obj.asset_id = computeAssetId(obj);
obj.data = 'tampered';
assert.ok(!verifyAssetId(obj));
});

it('returns false for missing asset_id', () => {
assert.ok(!verifyAssetId({ type: 'Gene', id: 'g1' }));
});

it('returns false for null input', () => {
assert.ok(!verifyAssetId(null));
});
});

describe('SCHEMA_VERSION', () => {
it('is a semver string', () => {
assert.match(SCHEMA_VERSION, /^\d+\.\d+\.\d+$/);
});
});
142 changes: 142 additions & 0 deletions test/mutation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const {
buildMutation,
isValidMutation,
normalizeMutation,
isHighRiskMutationAllowed,
isHighRiskPersonality,
clamp01,
} = require('../src/gep/mutation');

describe('clamp01', () => {
it('clamps values to [0, 1]', () => {
assert.equal(clamp01(0.5), 0.5);
assert.equal(clamp01(0), 0);
assert.equal(clamp01(1), 1);
assert.equal(clamp01(-0.5), 0);
assert.equal(clamp01(1.5), 1);
});

it('returns 0 for non-finite input', () => {
assert.equal(clamp01(NaN), 0);
assert.equal(clamp01(undefined), 0);
// Note: clamp01(Infinity) returns 0 because the implementation checks
// Number.isFinite() before clamping. Mathematically clamp(Inf, 0, 1) = 1,
// but the current behavior treats all non-finite values uniformly as 0.
assert.equal(clamp01(Infinity), 0);
});
});

describe('buildMutation', () => {
it('returns a valid Mutation object', () => {
const m = buildMutation({ signals: ['log_error'], selectedGene: { id: 'gene_repair' } });
assert.ok(isValidMutation(m));
assert.equal(m.type, 'Mutation');
assert.ok(m.id.startsWith('mut_'));
});

it('selects repair category when error signals present', () => {
const m = buildMutation({ signals: ['log_error', 'errsig:something'] });
assert.equal(m.category, 'repair');
});

it('selects innovate category when drift enabled', () => {
const m = buildMutation({ signals: ['stable_success_plateau'], driftEnabled: true });
assert.equal(m.category, 'innovate');
});

it('selects innovate for opportunity signals without errors', () => {
const m = buildMutation({ signals: ['user_feature_request'] });
assert.equal(m.category, 'innovate');
});

it('downgrades innovate to optimize for high-risk personality', () => {
const highRiskPersonality = { rigor: 0.3, risk_tolerance: 0.8, creativity: 0.5 };
const m = buildMutation({
signals: ['user_feature_request'],
personalityState: highRiskPersonality,
});
assert.equal(m.category, 'optimize');
assert.ok(m.trigger_signals.some(s => s.includes('safety')));
});

it('caps risk_level to medium when personality disallows high risk', () => {
const conservativePersonality = { rigor: 0.5, risk_tolerance: 0.6, creativity: 0.5 };
const m = buildMutation({
signals: ['stable_success_plateau'],
driftEnabled: true,
allowHighRisk: true,
personalityState: conservativePersonality,
});
assert.notEqual(m.risk_level, 'high');
});
});

describe('isValidMutation', () => {
it('returns true for valid mutation', () => {
const m = buildMutation({ signals: ['log_error'] });
assert.ok(isValidMutation(m));
});

it('returns false for missing fields', () => {
assert.ok(!isValidMutation(null));
assert.ok(!isValidMutation({}));
assert.ok(!isValidMutation({ type: 'Mutation' }));
});

it('returns false for invalid category', () => {
assert.ok(!isValidMutation({
type: 'Mutation', id: 'x', category: 'destroy',
trigger_signals: [], target: 't', expected_effect: 'e', risk_level: 'low',
}));
});
});

describe('normalizeMutation', () => {
it('fills defaults for empty object', () => {
const m = normalizeMutation({});
assert.ok(isValidMutation(m));
assert.equal(m.category, 'optimize');
assert.equal(m.risk_level, 'low');
});

it('preserves valid fields', () => {
const m = normalizeMutation({
id: 'mut_custom', category: 'repair',
trigger_signals: ['log_error'], target: 'file.js',
expected_effect: 'fix bug', risk_level: 'medium',
});
assert.equal(m.id, 'mut_custom');
assert.equal(m.category, 'repair');
assert.equal(m.risk_level, 'medium');
});
});

describe('isHighRiskPersonality', () => {
it('detects low rigor as high risk', () => {
assert.ok(isHighRiskPersonality({ rigor: 0.3 }));
});

it('detects high risk_tolerance as high risk', () => {
assert.ok(isHighRiskPersonality({ risk_tolerance: 0.7 }));
});

it('returns false for conservative personality', () => {
assert.ok(!isHighRiskPersonality({ rigor: 0.8, risk_tolerance: 0.2 }));
});
});

describe('isHighRiskMutationAllowed', () => {
it('allows when rigor >= 0.6 and risk_tolerance <= 0.5', () => {
assert.ok(isHighRiskMutationAllowed({ rigor: 0.8, risk_tolerance: 0.3 }));
});

it('disallows when rigor too low', () => {
assert.ok(!isHighRiskMutationAllowed({ rigor: 0.4, risk_tolerance: 0.3 }));
});

it('disallows when risk_tolerance too high', () => {
assert.ok(!isHighRiskMutationAllowed({ rigor: 0.8, risk_tolerance: 0.6 }));
});
});
Loading