468 lines
21 KiB
JavaScript
468 lines
21 KiB
JavaScript
|
|
import { test, describe, it } from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { Arbiter } from '../../../src/index.js';
|
||
|
|
import { setupChainRuleTestGraph } from './helpers.js';
|
||
|
|
|
||
|
|
describe('ChainRule Comprehensive Tests', () => {
|
||
|
|
|
||
|
|
describe('Debugging Aggregation Issues (RelationalComparatorRule Integration)', () => {
|
||
|
|
it('debugs the $9000 vs $4500 aggregation problem', () => {
|
||
|
|
const arbiter = new Arbiter({ embeddingDimensions: 256 });
|
||
|
|
|
||
|
|
// Replicate the exact scenario from RelationalComparatorRule test
|
||
|
|
arbiter.setRelationConfig('can_debit', { type: 'direct' });
|
||
|
|
arbiter.setRelationConfig('has_balance', { type: 'direct' });
|
||
|
|
|
||
|
|
// Create nodes
|
||
|
|
arbiter.addNode('user:test', 'user');
|
||
|
|
arbiter.addNode('account:business', 'account');
|
||
|
|
arbiter.addNode('account:personal', 'account');
|
||
|
|
arbiter.addNode('account:restricted', 'account');
|
||
|
|
arbiter.addNode('unit:usd', 'currency');
|
||
|
|
|
||
|
|
// Set up account balances
|
||
|
|
arbiter.addRelation('account:business', 'has_balance', 'unit:usd', { value: 3000 });
|
||
|
|
arbiter.addRelation('account:personal', 'has_balance', 'unit:usd', { value: 1500 });
|
||
|
|
arbiter.addRelation('account:restricted', 'has_balance', 'unit:usd', { value: 10000 });
|
||
|
|
|
||
|
|
// Set up debit rights (only to business and personal)
|
||
|
|
arbiter.addRelation('user:test', 'can_debit', 'account:business');
|
||
|
|
arbiter.addRelation('user:test', 'can_debit', 'account:personal');
|
||
|
|
|
||
|
|
// Configure the chain rule that's causing problems
|
||
|
|
arbiter.setRelationConfig('balance_chain', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'can_debit', direction: 'out' }, // user → accounts
|
||
|
|
{ relation: 'has_balance', direction: 'out' } // accounts → currency
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 1, // Extract from step 1 (accounts)
|
||
|
|
extractRelation: 'has_balance',
|
||
|
|
valueAggregation: 'sum', // Sum all authorized balances
|
||
|
|
evaluateFrom: 'user'
|
||
|
|
});
|
||
|
|
|
||
|
|
// Test the chain rule directly
|
||
|
|
const result = arbiter.check('user:test', 'balance_chain', 'unit:usd');
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\n=== CHAIN RULE AGGREGATION DEBUG ===');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Expected path: user:test → [account:business, account:personal] → unit:usd');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Expected balance sum: $3000 + $1500 = $4500');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Actual result:', JSON.stringify(result, null, 2));
|
||
|
|
|
||
|
|
// Check if the chain rule reaches the target
|
||
|
|
assert.equal(result.possibility, 1, 'Chain should successfully reach unit:usd');
|
||
|
|
assert.equal(result.reason, 'allow_rule_matched');
|
||
|
|
|
||
|
|
// The key question: what values are being collected?
|
||
|
|
if (result.collectedValues) {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\n=== COLLECTED VALUES ANALYSIS ===');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Number of collected values:', result.collectedValues.length);
|
||
|
|
result.collectedValues.forEach((cv, i) => {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log(`Value ${i + 1}:`, {
|
||
|
|
value: cv.value,
|
||
|
|
source: cv.source,
|
||
|
|
metadata: cv.metadata || cv.meta
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check what the RelationManager thinks about this path
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\n=== RELATION MANAGER DEBUG ===');
|
||
|
|
const userNodeId = arbiter.nodeManager.getNodeId('user:test');
|
||
|
|
const debitRels = arbiter.relationManager.getRelationsFromSrc(userNodeId, 'can_debit');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Debit relations:', debitRels.map(r => ({
|
||
|
|
target: arbiter.keyByNodeId.get(r.dst),
|
||
|
|
possibility: r.possibility
|
||
|
|
})));
|
||
|
|
|
||
|
|
debitRels.forEach(debitRel => {
|
||
|
|
const balanceRels = arbiter.relationManager.getRelationsFromSrc(debitRel.dst, 'has_balance');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log(`Balance relations from ${arbiter.keyByNodeId.get(debitRel.dst)}:`,
|
||
|
|
balanceRels.map(r => ({
|
||
|
|
target: arbiter.keyByNodeId.get(r.dst),
|
||
|
|
value: r.value,
|
||
|
|
possibility: r.possibility
|
||
|
|
}))
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
// This test documents the issue for investigation
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\n=== ISSUE SUMMARY ===');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('If chain rule aggregation is working correctly, the total should be $4500');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('If we get $9000, there is likely double counting in the aggregation logic');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('tests simpler two-account aggregation to isolate the issue', () => {
|
||
|
|
const arbiter = new Arbiter({ embeddingDimensions: 256 });
|
||
|
|
|
||
|
|
// Simpler test case
|
||
|
|
arbiter.setRelationConfig('owns', { type: 'direct' });
|
||
|
|
arbiter.setRelationConfig('worth', { type: 'direct' });
|
||
|
|
|
||
|
|
arbiter.addNode('user:simple', 'user');
|
||
|
|
arbiter.addNode('asset:a', 'asset');
|
||
|
|
arbiter.addNode('asset:b', 'asset');
|
||
|
|
arbiter.addNode('currency:usd', 'currency');
|
||
|
|
|
||
|
|
// User owns two assets
|
||
|
|
arbiter.addRelation('user:simple', 'owns', 'asset:a');
|
||
|
|
arbiter.addRelation('user:simple', 'owns', 'asset:b');
|
||
|
|
|
||
|
|
// Assets have known values
|
||
|
|
arbiter.addRelation('asset:a', 'worth', 'currency:usd', { value: 100 });
|
||
|
|
arbiter.addRelation('asset:b', 'worth', 'currency:usd', { value: 200 });
|
||
|
|
|
||
|
|
arbiter.setRelationConfig('total_worth_chain', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'owns', direction: 'out' },
|
||
|
|
{ relation: 'worth', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 1, // Extract from step 1 (assets)
|
||
|
|
extractRelation: 'worth',
|
||
|
|
valueAggregation: 'sum'
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = arbiter.check('user:simple', 'total_worth_chain', 'currency:usd');
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\n=== SIMPLE AGGREGATION TEST ===');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Expected: $100 + $200 = $300');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Result:', JSON.stringify(result, null, 2));
|
||
|
|
|
||
|
|
assert.equal(result.possibility, 1);
|
||
|
|
|
||
|
|
// This should help us see if the issue is specific to the can_debit scenario
|
||
|
|
// or a general problem with chain rule aggregation
|
||
|
|
if (result.collectedValues) {
|
||
|
|
const totalValue = result.collectedValues.reduce((sum, cv) => {
|
||
|
|
const val = typeof cv.value === 'number' ? cv.value :
|
||
|
|
(cv.value && typeof cv.value.value === 'number' ? cv.value.value : 0);
|
||
|
|
return sum + val;
|
||
|
|
}, 0);
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Manually calculated total from collected values:', totalValue);
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Expected total: 300');
|
||
|
|
|
||
|
|
if (totalValue !== 300) {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('🚨 AGGREGATION ISSUE CONFIRMED: Chain rule is not summing correctly');
|
||
|
|
} else {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('✅ Simple aggregation works correctly');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('tests chain rule value extraction vs RelationalComparatorRule value extraction', () => {
|
||
|
|
const arbiter = new Arbiter({ embeddingDimensions: 256 });
|
||
|
|
|
||
|
|
// Set up the same scenario but test chain rule in isolation
|
||
|
|
arbiter.setRelationConfig('can_debit', { type: 'direct' });
|
||
|
|
arbiter.setRelationConfig('has_balance', { type: 'direct' });
|
||
|
|
|
||
|
|
arbiter.addNode('user:compare', 'user');
|
||
|
|
arbiter.addNode('account:x', 'account');
|
||
|
|
arbiter.addNode('account:y', 'account');
|
||
|
|
arbiter.addNode('currency:usd', 'currency');
|
||
|
|
|
||
|
|
arbiter.addRelation('user:compare', 'can_debit', 'account:x');
|
||
|
|
arbiter.addRelation('user:compare', 'can_debit', 'account:y');
|
||
|
|
arbiter.addRelation('account:x', 'has_balance', 'currency:usd', { value: 1000 });
|
||
|
|
arbiter.addRelation('account:y', 'has_balance', 'currency:usd', { value: 2000 });
|
||
|
|
|
||
|
|
// Test 1: Direct ChainRule evaluation
|
||
|
|
arbiter.setRelationConfig('direct_chain', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'can_debit', direction: 'out' },
|
||
|
|
{ relation: 'has_balance', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 1,
|
||
|
|
extractRelation: 'has_balance',
|
||
|
|
valueAggregation: 'sum'
|
||
|
|
});
|
||
|
|
|
||
|
|
const chainResult = arbiter.check('user:compare', 'direct_chain', 'currency:usd');
|
||
|
|
|
||
|
|
// Test 2: RelationalComparatorRule using the same chain
|
||
|
|
arbiter.setRelationConfig('comparator_chain', {
|
||
|
|
type: 'relational_comparator',
|
||
|
|
leftOperand: {
|
||
|
|
rule: {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'can_debit', direction: 'out' },
|
||
|
|
{ relation: 'has_balance', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 1,
|
||
|
|
extractRelation: 'has_balance',
|
||
|
|
valueAggregation: 'sum',
|
||
|
|
evaluateFrom: 'user'
|
||
|
|
},
|
||
|
|
extractValue: true,
|
||
|
|
evaluateFrom: 'user'
|
||
|
|
},
|
||
|
|
rightOperand: {
|
||
|
|
rule: {
|
||
|
|
type: 'direct',
|
||
|
|
relation: 'has_balance' // dummy
|
||
|
|
},
|
||
|
|
extractValue: false // Use possibility (1.0) as value
|
||
|
|
},
|
||
|
|
comparator: '>='
|
||
|
|
});
|
||
|
|
|
||
|
|
const comparatorResult = arbiter.check('user:compare', 'comparator_chain', 'currency:usd');
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\n=== CHAIN VS RELATIONAL_COMPARATOR COMPARISON ===');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Expected total: $1000 + $2000 = $3000');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\nDirect chain result:', {
|
||
|
|
possibility: chainResult.possibility,
|
||
|
|
collectedValues: chainResult.collectedValues?.length || 'none',
|
||
|
|
reason: chainResult.reason
|
||
|
|
});
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\nRelationalComparatorRule result:', {
|
||
|
|
possibility: comparatorResult.possibility,
|
||
|
|
leftValue: comparatorResult.meta?.allow?.leftValue || comparatorResult.meta?.deny?.leftValue,
|
||
|
|
rightValue: comparatorResult.meta?.allow?.rightValue || comparatorResult.meta?.deny?.rightValue,
|
||
|
|
reason: comparatorResult.reason
|
||
|
|
});
|
||
|
|
|
||
|
|
// Both should reach the target
|
||
|
|
assert.equal(chainResult.possibility, 1);
|
||
|
|
|
||
|
|
if (comparatorResult.meta?.allow?.leftValue) {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\n🔍 Investigating value extraction:');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('RelationalComparatorRule extracted leftValue:', comparatorResult.meta.allow.leftValue);
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Expected: 3000');
|
||
|
|
|
||
|
|
if (comparatorResult.meta.allow.leftValue === 6000) {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('🚨 DOUBLE COUNTING CONFIRMED: 2x the expected value');
|
||
|
|
} else if (comparatorResult.meta.allow.leftValue === 3000) {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('✅ Correct aggregation');
|
||
|
|
} else {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('🤔 Unexpected value - needs investigation');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('isolates collectedValues disappearing issue', () => {
|
||
|
|
const arbiter = new Arbiter({ embeddingDimensions: 256 });
|
||
|
|
|
||
|
|
// Minimal test case
|
||
|
|
arbiter.setRelationConfig('owns', { type: 'direct' });
|
||
|
|
arbiter.setRelationConfig('worth', { type: 'direct' });
|
||
|
|
|
||
|
|
arbiter.addNode('user:minimal', 'user');
|
||
|
|
arbiter.addNode('item:test', 'item');
|
||
|
|
arbiter.addNode('currency:usd', 'currency');
|
||
|
|
|
||
|
|
arbiter.addRelation('user:minimal', 'owns', 'item:test');
|
||
|
|
arbiter.addRelation('item:test', 'worth', 'currency:usd', { value: 100 });
|
||
|
|
|
||
|
|
arbiter.setRelationConfig('minimal_chain', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'owns', direction: 'out' },
|
||
|
|
{ relation: 'worth', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 1,
|
||
|
|
extractRelation: 'worth',
|
||
|
|
valueAggregation: 'sum'
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = arbiter.check('user:minimal', 'minimal_chain', 'currency:usd');
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\n=== MINIMAL TEST: COLLECTED VALUES INVESTIGATION ===');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Full result structure:', JSON.stringify(result, null, 2));
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Has collectedValues field?', 'collectedValues' in result);
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('collectedValues type:', typeof result.collectedValues);
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('collectedValues value:', result.collectedValues);
|
||
|
|
|
||
|
|
if (result.collectedValues && Array.isArray(result.collectedValues)) {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('✅ collectedValues preserved:', result.collectedValues.length, 'values');
|
||
|
|
result.collectedValues.forEach((cv, i) => {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Value ${i + 1}:`, {
|
||
|
|
value: cv.value,
|
||
|
|
possibility: cv.possibility,
|
||
|
|
source: cv.source?.relation
|
||
|
|
});
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('🚨 collectedValues NOT preserved in final result');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('This explains why RelationalComparatorRule cannot extract values from ChainRule');
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.equal(result.possibility, 1, 'Chain should reach target');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('isolates why RelationalComparatorRule gets $9000 instead of $4500', () => {
|
||
|
|
const arbiter = new Arbiter({ embeddingDimensions: 256 });
|
||
|
|
|
||
|
|
// Set up the same scenario but test chain rule in isolation
|
||
|
|
arbiter.setRelationConfig('can_debit', { type: 'direct' });
|
||
|
|
arbiter.setRelationConfig('has_balance', { type: 'direct' });
|
||
|
|
|
||
|
|
arbiter.addNode('user:compare', 'user');
|
||
|
|
arbiter.addNode('account:x', 'account');
|
||
|
|
arbiter.addNode('account:y', 'account');
|
||
|
|
arbiter.addNode('currency:usd', 'currency');
|
||
|
|
|
||
|
|
arbiter.addRelation('user:compare', 'can_debit', 'account:x');
|
||
|
|
arbiter.addRelation('user:compare', 'can_debit', 'account:y');
|
||
|
|
arbiter.addRelation('account:x', 'has_balance', 'currency:usd', { value: 1000 });
|
||
|
|
arbiter.addRelation('account:y', 'has_balance', 'currency:usd', { value: 2000 });
|
||
|
|
|
||
|
|
// Test 1: Direct ChainRule evaluation
|
||
|
|
arbiter.setRelationConfig('direct_chain', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'can_debit', direction: 'out' },
|
||
|
|
{ relation: 'has_balance', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 1,
|
||
|
|
extractRelation: 'has_balance',
|
||
|
|
valueAggregation: 'sum'
|
||
|
|
});
|
||
|
|
|
||
|
|
const chainResult = arbiter.check('user:compare', 'direct_chain', 'currency:usd');
|
||
|
|
|
||
|
|
// Test 2: RelationalComparatorRule using the same chain
|
||
|
|
arbiter.setRelationConfig('comparator_chain', {
|
||
|
|
type: 'relational_comparator',
|
||
|
|
leftOperand: {
|
||
|
|
rule: {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'can_debit', direction: 'out' },
|
||
|
|
{ relation: 'has_balance', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 1,
|
||
|
|
extractRelation: 'has_balance',
|
||
|
|
valueAggregation: 'sum',
|
||
|
|
evaluateFrom: 'user'
|
||
|
|
},
|
||
|
|
extractValue: true,
|
||
|
|
evaluateFrom: 'user'
|
||
|
|
},
|
||
|
|
rightOperand: {
|
||
|
|
rule: {
|
||
|
|
type: 'direct',
|
||
|
|
relation: 'has_balance' // dummy
|
||
|
|
},
|
||
|
|
extractValue: false // Use possibility (1.0) as value
|
||
|
|
},
|
||
|
|
comparator: '>='
|
||
|
|
});
|
||
|
|
|
||
|
|
const comparatorResult = arbiter.check('user:compare', 'comparator_chain', 'currency:usd');
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\n=== CHAIN VS RELATIONAL_COMPARATOR COMPARISON ===');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Expected total: $1000 + $2000 = $3000');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\nDirect chain result:', {
|
||
|
|
possibility: chainResult.possibility,
|
||
|
|
collectedValues: chainResult.collectedValues?.length || 'none',
|
||
|
|
reason: chainResult.reason
|
||
|
|
});
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\nRelationalComparatorRule result:', {
|
||
|
|
possibility: comparatorResult.possibility,
|
||
|
|
leftValue: comparatorResult.meta?.allow?.leftValue || comparatorResult.meta?.deny?.leftValue,
|
||
|
|
rightValue: comparatorResult.meta?.allow?.rightValue || comparatorResult.meta?.deny?.rightValue,
|
||
|
|
reason: comparatorResult.reason
|
||
|
|
});
|
||
|
|
|
||
|
|
// Both should reach the target
|
||
|
|
assert.equal(chainResult.possibility, 1);
|
||
|
|
|
||
|
|
if (comparatorResult.meta?.allow?.leftValue) {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\n🔍 Investigating value extraction:');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('RelationalComparatorRule extracted leftValue:', comparatorResult.meta.allow.leftValue);
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Expected: 3000');
|
||
|
|
|
||
|
|
if (comparatorResult.meta.allow.leftValue === 6000) {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('🚨 DOUBLE COUNTING CONFIRMED: 2x the expected value');
|
||
|
|
} else if (comparatorResult.meta.allow.leftValue === 3000) {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('✅ Correct aggregation');
|
||
|
|
} else {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('🤔 Unexpected value - needs investigation');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('regression test: ensures collectedValues are preserved after AuthorizationChecker fix', () => {
|
||
|
|
const arbiter = new Arbiter({ embeddingDimensions: 256 });
|
||
|
|
|
||
|
|
// Set up a simple value collection scenario
|
||
|
|
arbiter.setRelationConfig('owns', { type: 'direct' });
|
||
|
|
arbiter.setRelationConfig('worth', { type: 'direct' });
|
||
|
|
|
||
|
|
arbiter.addNode('user:collector', 'user');
|
||
|
|
arbiter.addNode('asset:a', 'asset');
|
||
|
|
arbiter.addNode('asset:b', 'asset');
|
||
|
|
arbiter.addNode('currency:usd', 'currency');
|
||
|
|
|
||
|
|
// Set up asset ownership and values
|
||
|
|
arbiter.addRelation('user:collector', 'owns', 'asset:a');
|
||
|
|
arbiter.addRelation('user:collector', 'owns', 'asset:b');
|
||
|
|
arbiter.addRelation('asset:a', 'worth', 'currency:usd', { value: 500 });
|
||
|
|
arbiter.addRelation('asset:b', 'worth', 'currency:usd', { value: 750 });
|
||
|
|
|
||
|
|
// Configure chain rule with value collection
|
||
|
|
arbiter.setRelationConfig('asset_values', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'owns', direction: 'out' },
|
||
|
|
{ relation: 'worth', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 1,
|
||
|
|
extractRelation: 'worth',
|
||
|
|
valueAggregation: 'sum'
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = arbiter.check('user:collector', 'asset_values', 'currency:usd', { collectValues: true });
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('\n=== REGRESSION TEST: COLLECTED VALUES PRESERVATION ===');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Testing that collectedValues are preserved in final result...');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Result structure:', {
|
||
|
|
possibility: result.possibility,
|
||
|
|
hasCollectedValues: 'collectedValues' in result,
|
||
|
|
collectedValuesType: typeof result.collectedValues,
|
||
|
|
collectedValuesLength: result.collectedValues?.length || 'N/A',
|
||
|
|
reason: result.reason
|
||
|
|
});
|
||
|
|
|
||
|
|
// CRITICAL REGRESSION TEST: collectedValues must be preserved
|
||
|
|
assert.equal(result.possibility, 1, 'Chain should reach target successfully');
|
||
|
|
assert.ok('collectedValues' in result, '🚨 REGRESSION: collectedValues field missing from result');
|
||
|
|
assert.ok(Array.isArray(result.collectedValues), '🚨 REGRESSION: collectedValues is not an array');
|
||
|
|
assert.ok(result.collectedValues.length > 0, '🚨 REGRESSION: collectedValues array is empty');
|
||
|
|
|
||
|
|
// Verify the collected values have the expected structure
|
||
|
|
const firstValue = result.collectedValues[0];
|
||
|
|
assert.ok(firstValue.value !== undefined, 'Collected value should have a value field');
|
||
|
|
assert.ok(firstValue.possibility !== undefined, 'Collected value should have a possibility field');
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('✅ REGRESSION TEST PASSED: collectedValues properly preserved');
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log(' collectedValues count:', result.collectedValues.length);
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log(' First collected value:', {
|
||
|
|
value: firstValue.value,
|
||
|
|
possibility: firstValue.possibility,
|
||
|
|
source: firstValue.source
|
||
|
|
});
|
||
|
|
|
||
|
|
// Also verify it's in meta for backwards compatibility
|
||
|
|
if (result.meta?.collectedValues) {
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('✅ BONUS: collectedValues also available in meta');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|