Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
886 lines
41 KiB
JavaScript
886 lines
41 KiB
JavaScript
'use strict';
|
||
/**
|
||
* capability-precedence-parity.test.cjs — DEFECT.GENERATIVE-FIX parity gate
|
||
*
|
||
* Proves that the config-key four-level precedence walk has a single owner
|
||
* (capability-activation.cjs) and that loop-resolver.cjs re-exports the same
|
||
* functions rather than maintaining local copies.
|
||
*
|
||
* Two contracts enforced:
|
||
* (a) Identity: loop-resolver's _resolveActivationValue, _getNestedConfigValue,
|
||
* and _readRawConfigKey are the SAME function objects as
|
||
* capability-activation's exports. Fails the instant a local copy is
|
||
* re-introduced.
|
||
* (b) Behavioral matrix: across a fixture matrix hitting each precedence level
|
||
* (loadConfig result, workstream config.json, root config.json, registry
|
||
* default, absent, falsy values, prototype-pollution key), asserts that
|
||
* _resolveActivationValue(key,config,cwd,registry) ===
|
||
* (resolveConfigKey(key,{config,cwd,registry}).found ?
|
||
* Boolean(resolveConfigKey(key,{config,cwd,registry}).value) : false).
|
||
* Level-2/3 cases use real tmpdir .planning/config.json fixtures.
|
||
*
|
||
* RULESET: RULESET.TESTS.no-source-grep — no readFileSync + .includes() on source.
|
||
* RULESET: RULESET.TESTS.boundary-coverage — covers limit-1/limit/limit+1 (falsy boundary).
|
||
*/
|
||
|
||
process.env.MSD_TEST_MODE = '1';
|
||
|
||
const { describe, test, before, after } = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const os = require('node:os');
|
||
const path = require('node:path');
|
||
const { cleanup } = require('./helpers.cjs');
|
||
|
||
// ── Module paths ──────────────────────────────────────────────────────────────
|
||
|
||
const LIB = path.join(__dirname, '..', 'msd-core', 'bin', 'lib');
|
||
|
||
const capabilityActivation = require(path.join(LIB, 'capability-activation.cjs'));
|
||
const loopResolver = require(path.join(LIB, 'loop-resolver.cjs'));
|
||
const capabilityStateMod = require(path.join(LIB, 'capability-state.cjs'));
|
||
|
||
// ─── (a) Identity guard ───────────────────────────────────────────────────────
|
||
|
||
describe('capability-precedence-parity: identity guard — loop-resolver re-exports same function objects', () => {
|
||
test('_resolveActivationValue is the same function in both modules', () => {
|
||
assert.strictEqual(
|
||
loopResolver._resolveActivationValue,
|
||
capabilityActivation._resolveActivationValue,
|
||
'_resolveActivationValue: loop-resolver must re-export the capability-activation.cjs function, not a local copy',
|
||
);
|
||
});
|
||
|
||
test('_getNestedConfigValue is the same function in both modules', () => {
|
||
assert.strictEqual(
|
||
loopResolver._getNestedConfigValue,
|
||
capabilityActivation._getNestedConfigValue,
|
||
'_getNestedConfigValue: loop-resolver must re-export the capability-activation.cjs function, not a local copy',
|
||
);
|
||
});
|
||
|
||
test('_readRawConfigKey is the same function in both modules', () => {
|
||
assert.strictEqual(
|
||
loopResolver._readRawConfigKey,
|
||
capabilityActivation._readRawConfigKey,
|
||
'_readRawConfigKey: loop-resolver must re-export the capability-activation.cjs function, not a local copy',
|
||
);
|
||
});
|
||
|
||
// #3661
|
||
test('_resolvePointGate is the same function in both modules', () => {
|
||
assert.strictEqual(
|
||
loopResolver._resolvePointGate,
|
||
capabilityActivation._resolvePointGate,
|
||
'_resolvePointGate: loop-resolver must re-export the capability-activation.cjs function, not a local copy',
|
||
);
|
||
});
|
||
});
|
||
|
||
// ─── (a2) #3661 — _resolvePointGate pure-function matrix (50-test-matrix.md Section A) ─────
|
||
//
|
||
// A1-A10: pure, no-I/O behavioral coverage of _resolvePointGate directly (happy /
|
||
// boundary / negative / hostile), mirroring the style of the _resolveActivationValue
|
||
// matrix below but scoped to the new pointFrom gate.
|
||
|
||
describe('capability-precedence-parity: _resolvePointGate matrix (#3661, matrix Section A)', () => {
|
||
const { _resolvePointGate } = capabilityActivation;
|
||
|
||
test('A1: resolvePointGateTrueWhenPointFromAbsent — pointFrom undefined → true', () => {
|
||
const registry = makeRegistry('workflow.point_key', undefined);
|
||
assert.strictEqual(
|
||
_resolvePointGate(undefined, 'execute:post', {}, undefined, registry),
|
||
true,
|
||
);
|
||
});
|
||
|
||
test('A2: resolvePointGateTrueWhenPointFromNull — pointFrom null → true (mirrors undefined)', () => {
|
||
const registry = makeRegistry('workflow.point_key', undefined);
|
||
assert.strictEqual(
|
||
_resolvePointGate(null, 'execute:post', {}, undefined, registry),
|
||
true,
|
||
);
|
||
});
|
||
|
||
test('A3: resolvePointGateTrueOnExactMatch — config resolves to the SAME value as point → true', () => {
|
||
const key = 'workflow.point_key';
|
||
const config = { workflow: { point_key: 'execute:wave:post' } };
|
||
const registry = makeRegistry(key, undefined);
|
||
assert.strictEqual(
|
||
_resolvePointGate(key, 'execute:wave:post', config, undefined, registry),
|
||
true,
|
||
);
|
||
});
|
||
|
||
test('A4: resolvePointGateFalseOnMismatch — config resolves to a DIFFERENT value than point → false', () => {
|
||
const key = 'workflow.point_key';
|
||
const config = { workflow: { point_key: 'execute:post' } };
|
||
const registry = makeRegistry(key, undefined);
|
||
assert.strictEqual(
|
||
_resolvePointGate(key, 'execute:wave:post', config, undefined, registry),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test('A5: resolvePointGateFalseWhenKeyUnresolvable — key absent from config and no schema default → false', () => {
|
||
const key = 'workflow.nonexistent_point_key';
|
||
const registry = makeRegistry(key, undefined);
|
||
assert.strictEqual(
|
||
_resolvePointGate(key, 'execute:post', {}, undefined, registry),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test('A6: resolvePointGateFalseOnEmptyString — pointFrom="" → false (malformed, mirrors when)', () => {
|
||
const registry = makeRegistry('workflow.point_key', undefined);
|
||
assert.strictEqual(
|
||
_resolvePointGate('', 'execute:post', {}, undefined, registry),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test('A7: resolvePointGateFalseOnNonStringType — pointFrom is a number/object/array → false', () => {
|
||
const registry = makeRegistry('workflow.point_key', undefined);
|
||
for (const malformed of [42, { key: 'workflow.point_key' }, ['workflow.point_key']]) {
|
||
assert.strictEqual(
|
||
_resolvePointGate(malformed, 'execute:post', {}, undefined, registry),
|
||
false,
|
||
`pointFrom=${JSON.stringify(malformed)} must resolve to false`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('A8: resolvePointGateUsesSchemaDefaultPrecedence — resolves via registry.configSchema default', () => {
|
||
const key = 'workflow.point_key';
|
||
// No explicit config value — only the schema default, matching point.
|
||
const registryMatch = makeRegistry(key, 'execute:post');
|
||
assert.strictEqual(
|
||
_resolvePointGate(key, 'execute:post', {}, undefined, registryMatch),
|
||
true,
|
||
'schema default equal to point must match',
|
||
);
|
||
// Schema default present but NOT equal to point → false.
|
||
const registryMismatch = makeRegistry(key, 'execute:wave:post');
|
||
assert.strictEqual(
|
||
_resolvePointGate(key, 'execute:post', {}, undefined, registryMismatch),
|
||
false,
|
||
'schema default not equal to point must not match',
|
||
);
|
||
});
|
||
|
||
test('A9: resolvePointGateDoesNotMatchOnDoubleEmptyUnlessFound — point="" and unresolved key must not spuriously match', () => {
|
||
const key = 'workflow.nonexistent_point_key';
|
||
const registry = makeRegistry(key, undefined); // no schema default → found:false
|
||
assert.strictEqual(
|
||
_resolvePointGate(key, '', {}, undefined, registry),
|
||
false,
|
||
'an unresolved (found:false) key must never match, even against an empty point string',
|
||
);
|
||
});
|
||
|
||
test('A10: resolvePointGateRejectsPrototypePollutionKey — pointFrom colliding with a prototype key segment → false', () => {
|
||
const key = 'a.__proto__.polluted';
|
||
const config = { a: { real: 1 } };
|
||
const registry = makeRegistry(key, undefined); // no schema default for this exact dotted key
|
||
assert.strictEqual(
|
||
_resolvePointGate(key, 'execute:post', config, undefined, registry),
|
||
false,
|
||
'__proto__ path segment must be rejected by the shared nested-traversal guard, yielding found:false',
|
||
);
|
||
});
|
||
});
|
||
|
||
// ─── (b) Behavioral matrix ────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Build a minimal registry with a configSchema entry for a dotted key.
|
||
* def=undefined means no default (absent). def=<value> provides a default.
|
||
*/
|
||
function makeRegistry(dotKey, def) {
|
||
const registry = {};
|
||
if (def !== undefined) {
|
||
registry.configSchema = {
|
||
[dotKey]: { default: def },
|
||
};
|
||
} else {
|
||
registry.configSchema = {};
|
||
}
|
||
return registry;
|
||
}
|
||
|
||
/**
|
||
* Helper: call resolveConfigKey and assert it equals
|
||
* _resolveActivationValue(key,config,cwd,registry) on the boolean coercion.
|
||
*/
|
||
function assertParity(key, config, cwd, registry, label) {
|
||
const { _resolveActivationValue, resolveConfigKey } = capabilityActivation;
|
||
const boolResult = _resolveActivationValue(key, config, cwd, registry);
|
||
const rawResult = resolveConfigKey(key, { config, cwd, registry });
|
||
const expectedBool = rawResult.found ? Boolean(rawResult.value) : false;
|
||
assert.strictEqual(
|
||
boolResult,
|
||
expectedBool,
|
||
`parity check failed for ${label}: _resolveActivationValue=${boolResult} but resolveConfigKey gives found=${rawResult.found} value=${JSON.stringify(rawResult.value)} → expectedBool=${expectedBool}`,
|
||
);
|
||
return { boolResult, rawResult };
|
||
}
|
||
|
||
describe('capability-precedence-parity: behavioral matrix', () => {
|
||
let tmpDir;
|
||
|
||
before(() => {
|
||
// Create a hermetic tmpdir for file-based fixtures (levels 2+3).
|
||
// Clear env vars that redirect planningDir/planningRoot.
|
||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'parity-test-'));
|
||
});
|
||
|
||
after(() => {
|
||
// Remove the tmpdir (helpers.cleanup carries the Windows-EBUSY retry budget).
|
||
cleanup(tmpDir);
|
||
});
|
||
|
||
// Env vars that planningDir/planningRoot read — save/restore around each test.
|
||
function withCleanEnv(fn) {
|
||
const saved = {
|
||
MSD_WORKSTREAM: process.env.MSD_WORKSTREAM,
|
||
MSD_PROJECT: process.env.MSD_PROJECT,
|
||
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
|
||
};
|
||
delete process.env.MSD_WORKSTREAM;
|
||
delete process.env.MSD_PROJECT;
|
||
delete process.env.CLAUDE_CONFIG_DIR;
|
||
try {
|
||
return fn();
|
||
} finally {
|
||
if (saved.MSD_WORKSTREAM !== undefined) process.env.MSD_WORKSTREAM = saved.MSD_WORKSTREAM;
|
||
else delete process.env.MSD_WORKSTREAM;
|
||
if (saved.MSD_PROJECT !== undefined) process.env.MSD_PROJECT = saved.MSD_PROJECT;
|
||
else delete process.env.MSD_PROJECT;
|
||
if (saved.CLAUDE_CONFIG_DIR !== undefined) process.env.CLAUDE_CONFIG_DIR = saved.CLAUDE_CONFIG_DIR;
|
||
else delete process.env.CLAUDE_CONFIG_DIR;
|
||
}
|
||
}
|
||
|
||
// ── Level 1: loadConfig result (config arg has the key) ─────────────────────
|
||
|
||
test('level-1 truthy: config arg has key=true → resolved true, parity holds', () => {
|
||
const key = 'feature.enabled';
|
||
const config = { feature: { enabled: true } };
|
||
const registry = makeRegistry(key, undefined);
|
||
const { boolResult, rawResult } = assertParity(key, config, undefined, registry, 'level-1 truthy');
|
||
assert.strictEqual(boolResult, true);
|
||
assert.strictEqual(rawResult.found, true);
|
||
assert.strictEqual(rawResult.value, true);
|
||
});
|
||
|
||
test('level-1 falsy (false): config arg has key=false → resolved false, parity holds', () => {
|
||
// BVA: falsy value false — boundary case
|
||
const key = 'feature.enabled';
|
||
const config = { feature: { enabled: false } };
|
||
const registry = makeRegistry(key, true); // default true but level-1 wins
|
||
const { boolResult, rawResult } = assertParity(key, config, undefined, registry, 'level-1 falsy=false');
|
||
assert.strictEqual(boolResult, false);
|
||
assert.strictEqual(rawResult.found, true);
|
||
assert.strictEqual(rawResult.value, false); // raw value preserved
|
||
});
|
||
|
||
test('level-1 falsy (0): config arg has key=0 → resolved false, parity holds', () => {
|
||
// BVA: falsy value 0 — boundary case
|
||
const key = 'feature.level';
|
||
const config = { feature: { level: 0 } };
|
||
const registry = makeRegistry(key, 2); // default 2 but level-1 wins
|
||
const { boolResult, rawResult } = assertParity(key, config, undefined, registry, 'level-1 falsy=0');
|
||
assert.strictEqual(boolResult, false);
|
||
assert.strictEqual(rawResult.found, true);
|
||
assert.strictEqual(rawResult.value, 0); // raw value 0 preserved
|
||
});
|
||
|
||
test('level-1 truthy (nonzero): config arg has key=2 → resolved true, parity holds', () => {
|
||
// BVA: truthy numeric — boundary-adjacent to 0
|
||
const key = 'feature.level';
|
||
const config = { feature: { level: 2 } };
|
||
const registry = makeRegistry(key, undefined);
|
||
const { boolResult, rawResult } = assertParity(key, config, undefined, registry, 'level-1 truthy=2');
|
||
assert.strictEqual(boolResult, true);
|
||
assert.strictEqual(rawResult.found, true);
|
||
assert.strictEqual(rawResult.value, 2);
|
||
});
|
||
|
||
// ── Level 2: workstream config.json ─────────────────────────────────────────
|
||
|
||
test('level-2: workstream config.json has key → resolved, level-1 empty, parity holds', () => {
|
||
withCleanEnv(() => {
|
||
// Set up a .planning/config.json in tmpDir
|
||
const planningDir = path.join(tmpDir, 'ws-l2', '.planning');
|
||
fs.mkdirSync(planningDir, { recursive: true });
|
||
fs.writeFileSync(
|
||
path.join(planningDir, 'config.json'),
|
||
JSON.stringify({ graphify: { enabled: true } }),
|
||
'utf8',
|
||
);
|
||
const key = 'graphify.enabled';
|
||
const config = {}; // level-1 miss
|
||
const registry = makeRegistry(key, undefined);
|
||
const cwd = path.join(tmpDir, 'ws-l2');
|
||
const { boolResult, rawResult } = assertParity(key, config, cwd, registry, 'level-2 workstream');
|
||
assert.strictEqual(boolResult, true);
|
||
assert.strictEqual(rawResult.found, true);
|
||
assert.strictEqual(rawResult.value, true);
|
||
});
|
||
});
|
||
|
||
test('level-2 falsy: workstream config.json has key=false → false, parity holds', () => {
|
||
withCleanEnv(() => {
|
||
const planningDir = path.join(tmpDir, 'ws-l2-false', '.planning');
|
||
fs.mkdirSync(planningDir, { recursive: true });
|
||
fs.writeFileSync(
|
||
path.join(planningDir, 'config.json'),
|
||
JSON.stringify({ graphify: { enabled: false } }),
|
||
'utf8',
|
||
);
|
||
const key = 'graphify.enabled';
|
||
const config = {};
|
||
const registry = makeRegistry(key, true); // default true, but level-2 wins
|
||
const cwd = path.join(tmpDir, 'ws-l2-false');
|
||
const { boolResult, rawResult } = assertParity(key, config, cwd, registry, 'level-2 falsy');
|
||
assert.strictEqual(boolResult, false);
|
||
assert.strictEqual(rawResult.found, true);
|
||
assert.strictEqual(rawResult.value, false);
|
||
});
|
||
});
|
||
|
||
// ── Level 3: root config.json (only when paths differ) ──────────────────────
|
||
|
||
// Without MSD_WORKSTREAM, planningDir === planningRoot, so levels 2 and 3 point
|
||
// to the same file. Level-3 distinction is only exercisable by setting MSD_WORKSTREAM.
|
||
// We test the "paths same → no double-read" path (covered by level-2 test above)
|
||
// and the "paths differ" path via MSD_WORKSTREAM.
|
||
test('level-3: root config.json read when workstream path differs, parity holds', () => {
|
||
// Use MSD_WORKSTREAM to force planningDir to differ from planningRoot.
|
||
withCleanEnv(() => {
|
||
const projectRoot = path.join(tmpDir, 'ws-l3-project');
|
||
// Root planning dir = projectRoot/.planning
|
||
const rootPlanningDir = path.join(projectRoot, '.planning');
|
||
// Workstream planning dir = projectRoot/.planning/workstreams/mystream
|
||
const wsName = 'mystream';
|
||
const wsPlanningDir = path.join(rootPlanningDir, 'workstreams', wsName);
|
||
fs.mkdirSync(rootPlanningDir, { recursive: true });
|
||
fs.mkdirSync(wsPlanningDir, { recursive: true });
|
||
|
||
// Write ONLY the root config.json (no ws config.json)
|
||
fs.writeFileSync(
|
||
path.join(rootPlanningDir, 'config.json'),
|
||
JSON.stringify({ intel: { enabled: true } }),
|
||
'utf8',
|
||
);
|
||
|
||
// Activate workstream so planningDir → wsPlanningDir, planningRoot → rootPlanningDir
|
||
process.env.MSD_WORKSTREAM = wsName;
|
||
|
||
const key = 'intel.enabled';
|
||
const config = {};
|
||
const registry = makeRegistry(key, undefined);
|
||
const { boolResult, rawResult } = assertParity(key, config, projectRoot, registry, 'level-3 root config');
|
||
// ws config.json absent → level-2 miss; root config.json present → level-3 hit
|
||
assert.strictEqual(boolResult, true);
|
||
assert.strictEqual(rawResult.found, true);
|
||
assert.strictEqual(rawResult.value, true);
|
||
});
|
||
});
|
||
|
||
// ── Level 4: registry configSchema default ───────────────────────────────────
|
||
|
||
test('level-4: registry default=true, no config or file → true, parity holds', () => {
|
||
withCleanEnv(() => {
|
||
const key = 'workflow.some_feature';
|
||
const config = {};
|
||
const registry = makeRegistry(key, true);
|
||
// cwd points to an empty dir — no .planning/config.json files
|
||
const cwd = path.join(tmpDir, 'l4-default-true');
|
||
fs.mkdirSync(cwd, { recursive: true });
|
||
const { boolResult, rawResult } = assertParity(key, config, cwd, registry, 'level-4 default=true');
|
||
assert.strictEqual(boolResult, true);
|
||
assert.strictEqual(rawResult.found, true);
|
||
assert.strictEqual(rawResult.value, true);
|
||
});
|
||
});
|
||
|
||
test('level-4: registry default=false → false (BVA: falsy default), parity holds', () => {
|
||
withCleanEnv(() => {
|
||
const key = 'workflow.some_feature';
|
||
const config = {};
|
||
const registry = makeRegistry(key, false);
|
||
const cwd = path.join(tmpDir, 'l4-default-false');
|
||
fs.mkdirSync(cwd, { recursive: true });
|
||
const { boolResult, rawResult } = assertParity(key, config, cwd, registry, 'level-4 default=false');
|
||
assert.strictEqual(boolResult, false);
|
||
assert.strictEqual(rawResult.found, true);
|
||
assert.strictEqual(rawResult.value, false); // raw false preserved
|
||
});
|
||
});
|
||
|
||
test('level-4: registry default=0 → false (BVA: numeric zero default), parity holds', () => {
|
||
withCleanEnv(() => {
|
||
const key = 'feature.level';
|
||
const config = {};
|
||
const registry = makeRegistry(key, 0);
|
||
const cwd = path.join(tmpDir, 'l4-default-0');
|
||
fs.mkdirSync(cwd, { recursive: true });
|
||
const { boolResult, rawResult } = assertParity(key, config, cwd, registry, 'level-4 default=0');
|
||
assert.strictEqual(boolResult, false);
|
||
assert.strictEqual(rawResult.found, true);
|
||
assert.strictEqual(rawResult.value, 0); // raw 0 preserved
|
||
});
|
||
});
|
||
|
||
// ── Level 5: absent ──────────────────────────────────────────────────────────
|
||
|
||
test('level-5: key absent everywhere → false, found=false, parity holds', () => {
|
||
withCleanEnv(() => {
|
||
const key = 'nonexistent.key';
|
||
const config = {};
|
||
const registry = makeRegistry(key, undefined); // no default
|
||
const cwd = path.join(tmpDir, 'l5-absent');
|
||
fs.mkdirSync(cwd, { recursive: true });
|
||
const { boolResult, rawResult } = assertParity(key, config, cwd, registry, 'level-5 absent');
|
||
assert.strictEqual(boolResult, false);
|
||
assert.strictEqual(rawResult.found, false);
|
||
assert.strictEqual(rawResult.value, undefined);
|
||
});
|
||
});
|
||
|
||
test('level-5: cwd=undefined, absent key → false, found=false, parity holds', () => {
|
||
const key = 'nonexistent.key';
|
||
const config = {};
|
||
const registry = makeRegistry(key, undefined);
|
||
const { boolResult, rawResult } = assertParity(key, config, undefined, registry, 'level-5 absent cwd=undefined');
|
||
assert.strictEqual(boolResult, false);
|
||
assert.strictEqual(rawResult.found, false);
|
||
});
|
||
|
||
// ── Prototype-pollution guard (nested-traversal sink, levels 1–3) ──────────────
|
||
//
|
||
// The guard protects the NESTED config-object traversal (_getNestedConfigValue) —
|
||
// the only prototype-pollution sink. A `__proto__`/`constructor`/`prototype`
|
||
// SEGMENT encountered mid-path is rejected → found=false. Level 4 is a flat
|
||
// single-key lookup of the whole dotted string against registry.configSchema (no
|
||
// nested traversal, not a pollution sink), so we deliberately seed NO level-4
|
||
// default here — otherwise that flat lookup would legitimately match the literal
|
||
// dotted key and the segment guard would not be the thing under test.
|
||
|
||
test('prototype-pollution: __proto__ mid-path segment → found=false, boolResult=false, parity holds', () => {
|
||
const key = 'a.__proto__.polluted';
|
||
const config = { a: { real: 1 } }; // 'a' resolves, then the '__proto__' segment must be rejected
|
||
const registry = makeRegistry(key, undefined); // no level-4 default
|
||
const { boolResult, rawResult } = assertParity(key, config, undefined, registry, 'proto-pollution __proto__ mid-path');
|
||
assert.strictEqual(rawResult.found, false, '__proto__ path segment must be rejected by the nested-traversal guard');
|
||
assert.strictEqual(boolResult, false);
|
||
});
|
||
|
||
test('prototype-pollution: constructor mid-path segment → found=false, boolResult=false, parity holds', () => {
|
||
const key = 'a.constructor.polluted';
|
||
const config = { a: { real: 1 } };
|
||
const registry = makeRegistry(key, undefined); // no level-4 default
|
||
const { boolResult, rawResult } = assertParity(key, config, undefined, registry, 'proto-pollution constructor mid-path');
|
||
assert.strictEqual(rawResult.found, false, 'constructor path segment must be rejected by the nested-traversal guard');
|
||
assert.strictEqual(boolResult, false);
|
||
});
|
||
|
||
// ── Level-1 precedence over level-4 ─────────────────────────────────────────
|
||
|
||
test('level-1 wins over level-4 default: config=false beats registry default=true', () => {
|
||
// BVA: this is the critical boundary — level-1 falsy beats level-4 truthy
|
||
const key = 'graphify.enabled';
|
||
const config = { graphify: { enabled: false } };
|
||
const registry = makeRegistry(key, true);
|
||
const { boolResult, rawResult } = assertParity(key, config, undefined, registry, 'level-1 false beats level-4 true default');
|
||
assert.strictEqual(boolResult, false, 'config value false must win over registry default true');
|
||
assert.strictEqual(rawResult.found, true);
|
||
assert.strictEqual(rawResult.value, false);
|
||
});
|
||
});
|
||
|
||
// ─── (c) resolveConfigKey identity guard (FIX 2 — loop-resolver re-exports resolveConfigKey) ────
|
||
//
|
||
// resolveLoopHooks.resolveConfigValues uses resolveConfigKey internally to build
|
||
// hook configValues. Exporting resolveConfigKey from loop-resolver and asserting
|
||
// identity here ensures that if someone ever introduces a local copy, this test
|
||
// will catch it immediately (same mechanism as the _resolveActivationValue guard
|
||
// above, applied to the raw-value consumer).
|
||
|
||
describe('capability-precedence-parity: identity guard — loop-resolver.resolveConfigKey is same fn as capability-activation.resolveConfigKey', () => {
|
||
test('resolveConfigKey is the same function in both modules', () => {
|
||
assert.ok(
|
||
typeof loopResolver.resolveConfigKey === 'function',
|
||
'loop-resolver must export resolveConfigKey',
|
||
);
|
||
assert.strictEqual(
|
||
loopResolver.resolveConfigKey,
|
||
capabilityActivation.resolveConfigKey,
|
||
'resolveConfigKey: loop-resolver must re-export the capability-activation.cjs function, not a local copy',
|
||
);
|
||
});
|
||
});
|
||
|
||
// ─── (d) Behavioral: loop-resolver resolveConfigValues uses the shared engine ────────────────────
|
||
//
|
||
// Proves that resolveLoopHooks's resolveConfigValues closure (the RAW-value consumer)
|
||
// produces the SAME result as calling capabilityActivation.resolveConfigKey directly
|
||
// for the same (dotKey, config, cwd, registry) arguments.
|
||
//
|
||
// This test FAILS if resolveConfigValues is ever replaced with a divergent copy that
|
||
// does not delegate to resolveConfigKey — fulfilling the DEFECT.GENERATIVE-FIX mandate.
|
||
//
|
||
// Coverage: three cases required:
|
||
// (1) Level-1 config hit — key present in config arg
|
||
// (2) Level-4 registry default — key absent from config, present in schema default
|
||
// (3) Absent key — key absent everywhere (omitted from resolved output)
|
||
|
||
describe('capability-precedence-parity: behavioral — loop-resolver resolveConfigValues uses shared resolveConfigKey engine', () => {
|
||
const { resolveLoopHooks } = loopResolver;
|
||
const { resolveConfigKey } = capabilityActivation;
|
||
const { CANONICAL_POINTS_FALLBACK } = loopResolver;
|
||
|
||
/**
|
||
* Build a minimal registry that has:
|
||
* - one contribution hook at the given point with configValues map
|
||
* - one capability that owns that hook (capId = 'test-cap', active=true implied)
|
||
* - configSchema entries for level-4 defaults
|
||
*/
|
||
function makeLoopRegistry({ point, configValues, schemaDefaults = {} }) {
|
||
const byLoopPoint = {};
|
||
for (const p of CANONICAL_POINTS_FALLBACK) {
|
||
byLoopPoint[p] = { steps: [], contributions: [], gates: [] };
|
||
}
|
||
byLoopPoint[point].contributions = [
|
||
{
|
||
capId: 'test-cap',
|
||
into: 'test-section',
|
||
fragment: { inline: 'test fragment' },
|
||
configValues,
|
||
},
|
||
];
|
||
const configSchema = {};
|
||
for (const [dotKey, def] of Object.entries(schemaDefaults)) {
|
||
configSchema[dotKey] = { default: def };
|
||
}
|
||
return { byLoopPoint, configSchema };
|
||
}
|
||
|
||
/** capabilityStatesById with test-cap active=true */
|
||
function activeCapMap() {
|
||
return new Map([['test-cap', { enabled: true, active: true }]]);
|
||
}
|
||
|
||
test('(1) level-1 config hit: resolveConfigValues matches resolveConfigKey for present key', () => {
|
||
// Hook declares configValues: { secLevel: 'security.asvs_level' }
|
||
// Config has security.asvs_level=2 → level-1 hit
|
||
const dotKey = 'security.asvs_level';
|
||
const alias = 'secLevel';
|
||
const config = { security: { asvs_level: 2 } };
|
||
const registry = makeLoopRegistry({
|
||
point: 'plan:pre',
|
||
configValues: { [alias]: dotKey },
|
||
});
|
||
|
||
const resolved = resolveLoopHooks({
|
||
point: 'plan:pre',
|
||
registry,
|
||
config,
|
||
cwd: undefined,
|
||
capabilityStatesById: activeCapMap(),
|
||
});
|
||
|
||
assert.strictEqual(resolved.activeHooks.length, 1, 'One active contribution hook expected');
|
||
const hook = resolved.activeHooks[0];
|
||
assert.ok(hook.configValues, 'configValues must be present on the resolved hook');
|
||
|
||
// resolveConfigKey direct result for the same args
|
||
const directResult = resolveConfigKey(dotKey, { config, cwd: undefined, registry });
|
||
assert.strictEqual(directResult.found, true, 'direct resolveConfigKey must find the level-1 key');
|
||
assert.strictEqual(
|
||
hook.configValues[alias],
|
||
directResult.value,
|
||
`resolved hook configValues[${alias}] must equal resolveConfigKey(...).value for level-1 hit (both=${JSON.stringify(directResult.value)})`,
|
||
);
|
||
assert.strictEqual(hook.configValues[alias], 2, 'level-1 numeric value 2 must be preserved (not coerced to boolean)');
|
||
});
|
||
|
||
test('(2) level-4 registry default hit: resolveConfigValues matches resolveConfigKey for schema default', () => {
|
||
// Hook declares configValues: { blockOn: 'security.block_on' }
|
||
// Config is empty; schema default = 'medium'
|
||
const dotKey = 'security.block_on';
|
||
const alias = 'blockOn';
|
||
const schemaDefault = 'medium';
|
||
const config = {};
|
||
const registry = makeLoopRegistry({
|
||
point: 'execute:pre',
|
||
configValues: { [alias]: dotKey },
|
||
schemaDefaults: { [dotKey]: schemaDefault },
|
||
});
|
||
|
||
const resolved = resolveLoopHooks({
|
||
point: 'execute:pre',
|
||
registry,
|
||
config,
|
||
cwd: undefined,
|
||
capabilityStatesById: activeCapMap(),
|
||
});
|
||
|
||
assert.strictEqual(resolved.activeHooks.length, 1, 'One active contribution hook expected');
|
||
const hook = resolved.activeHooks[0];
|
||
assert.ok(hook.configValues, 'configValues must be present on the resolved hook (schema default found)');
|
||
|
||
const directResult = resolveConfigKey(dotKey, { config, cwd: undefined, registry });
|
||
assert.strictEqual(directResult.found, true, 'direct resolveConfigKey must find the level-4 schema default');
|
||
assert.strictEqual(
|
||
hook.configValues[alias],
|
||
directResult.value,
|
||
`resolved hook configValues[${alias}] must equal resolveConfigKey(...).value for level-4 hit (both=${JSON.stringify(directResult.value)})`,
|
||
);
|
||
assert.strictEqual(hook.configValues[alias], 'medium', 'level-4 string default must be preserved as raw string');
|
||
});
|
||
|
||
test('(3a) mixed hook: one resolvable alias + one absent alias → configValues present, contains only the resolvable alias', () => {
|
||
// Hook declares configValues: { present: 'feature.on', absent: 'nope.missing' }
|
||
// Config has feature.on=true (level-1 hit); no registry default for nope.missing.
|
||
// Expected: resolved hook.configValues is defined, has 'present'=true, does NOT have 'absent'.
|
||
const presentDotKey = 'feature.on';
|
||
const absentDotKey = 'nope.missing';
|
||
const config = { feature: { on: true } };
|
||
const registry = makeLoopRegistry({
|
||
point: 'verify:post',
|
||
configValues: { present: presentDotKey, absent: absentDotKey },
|
||
// No schema default for absentDotKey — it must be absent everywhere
|
||
});
|
||
|
||
const resolved = resolveLoopHooks({
|
||
point: 'verify:post',
|
||
registry,
|
||
config,
|
||
cwd: undefined,
|
||
capabilityStatesById: activeCapMap(),
|
||
});
|
||
|
||
assert.strictEqual(resolved.activeHooks.length, 1, 'One active contribution hook expected');
|
||
const hook = resolved.activeHooks[0];
|
||
|
||
// The 'present' alias must resolve via level-1 config hit
|
||
assert.ok(hook.configValues !== undefined, 'configValues must be defined (at least one alias resolved)');
|
||
assert.strictEqual(
|
||
hook.configValues['present'],
|
||
true,
|
||
"hook.configValues['present'] must equal true (level-1 hit for feature.on)",
|
||
);
|
||
|
||
// The 'absent' alias must NOT appear in configValues at all (omit-when-absent contract)
|
||
assert.ok(
|
||
!Object.prototype.hasOwnProperty.call(hook.configValues, 'absent'),
|
||
"Absent key 'nope.missing' must not appear in hook.configValues (found=false → omit, not include as undefined)",
|
||
);
|
||
});
|
||
|
||
test('(3b) all-absent hook: all configValues aliases absent everywhere → hook.configValues === undefined', () => {
|
||
// Hook declares configValues: { missingAlias: 'does.not.exist' }
|
||
// Key absent from config, no schema default, no config files.
|
||
// resolveConfigValues must return undefined (empty resolved map → omitted entirely).
|
||
const dotKey = 'does.not.exist';
|
||
const alias = 'missingAlias';
|
||
const config = {};
|
||
const registry = makeLoopRegistry({
|
||
point: 'ship:pre',
|
||
configValues: { [alias]: dotKey },
|
||
// No schema default for this key
|
||
});
|
||
|
||
const resolved = resolveLoopHooks({
|
||
point: 'ship:pre',
|
||
registry,
|
||
config,
|
||
cwd: undefined,
|
||
capabilityStatesById: activeCapMap(),
|
||
});
|
||
|
||
assert.strictEqual(resolved.activeHooks.length, 1, 'One active contribution hook expected');
|
||
const hook = resolved.activeHooks[0];
|
||
|
||
// resolveConfigKey direct result must confirm absent
|
||
const directResult = resolveConfigKey(dotKey, { config, cwd: undefined, registry });
|
||
assert.strictEqual(directResult.found, false, 'resolveConfigKey must return found=false for absent key');
|
||
assert.strictEqual(directResult.value, undefined);
|
||
|
||
// When ALL aliases are absent, resolveConfigValues returns undefined → hook.configValues must be undefined
|
||
assert.strictEqual(
|
||
hook.configValues,
|
||
undefined,
|
||
'hook.configValues must be undefined when all aliases are absent (omit-when-empty contract)',
|
||
);
|
||
});
|
||
});
|
||
|
||
// ─── (e) #3661 — loop-resolver and capability-state agree on pointFrom selection ──
|
||
// (50-test-matrix.md Section D)
|
||
//
|
||
// A single synthetic two-point capability fixture is run through BOTH resolvers
|
||
// (resolveLoopHooks / resolveCapabilityState) sharing the same enum config key,
|
||
// proving the two consumers of _resolvePointGate can never diverge.
|
||
|
||
describe('capability-precedence-parity: loop-resolver + capability-state agree on pointFrom selection (#3661, matrix Section D)', () => {
|
||
const { resolveCapabilityState } = capabilityStateMod;
|
||
const { resolveLoopHooks, CANONICAL_POINTS_FALLBACK } = loopResolver;
|
||
|
||
/**
|
||
* Build one synthetic capability declared twice (point A / point B) sharing one
|
||
* enum `pointFrom` key, plus a third control step at point A with NO `pointFrom`
|
||
* (governed by `when` alone). Returns both a loop-resolver-shaped registry
|
||
* (byLoopPoint) and a capability-state-shaped registry (capabilities), built from
|
||
* the SAME step objects, so both resolvers see byte-identical hook declarations.
|
||
*/
|
||
function makeTwoPointFixture() {
|
||
const capId = 'test-two-point-cap';
|
||
const pointA = 'execute:post';
|
||
const pointB = 'execute:wave:post';
|
||
const enumKey = 'workflow.test_point_select';
|
||
const whenAKey = 'workflow.test_step_a_enabled';
|
||
const whenBKey = 'workflow.test_step_b_enabled';
|
||
const whenControlKey = 'workflow.test_control_enabled';
|
||
|
||
const stepA = {
|
||
capId, point: pointA, ref: { skill: 'test-skill' },
|
||
produces: [], consumes: [], when: whenAKey, pointFrom: enumKey, onError: 'skip',
|
||
};
|
||
const stepB = {
|
||
capId, point: pointB, ref: { skill: 'test-skill' },
|
||
produces: [], consumes: [], when: whenBKey, pointFrom: enumKey, onError: 'skip',
|
||
};
|
||
// Control: no pointFrom at all — must be unaffected by the enum's value (D4).
|
||
const controlStep = {
|
||
capId, point: pointA, ref: { skill: 'control-skill' },
|
||
produces: [], consumes: [], when: whenControlKey, onError: 'skip',
|
||
};
|
||
|
||
const configSchema = {
|
||
[enumKey]: { default: pointA },
|
||
[whenAKey]: { default: true },
|
||
[whenBKey]: { default: true },
|
||
[whenControlKey]: { default: true },
|
||
};
|
||
|
||
const byLoopPoint = {};
|
||
for (const p of CANONICAL_POINTS_FALLBACK) {
|
||
byLoopPoint[p] = { steps: [], contributions: [], gates: [] };
|
||
}
|
||
byLoopPoint[pointA].steps = [stepA, controlStep];
|
||
byLoopPoint[pointB].steps = [stepB];
|
||
const loopRegistry = { byLoopPoint, configSchema };
|
||
|
||
const capStateRegistry = {
|
||
capabilities: {
|
||
[capId]: {
|
||
id: capId, tier: 'standard', skills: [], steps: [stepA, stepB, controlStep],
|
||
gates: [], contributions: [], config: {},
|
||
},
|
||
},
|
||
configSchema,
|
||
};
|
||
|
||
const capabilityStatesById = new Map([[capId, { enabled: true, active: true }]]);
|
||
|
||
return {
|
||
capId, pointA, pointB, enumKey, whenAKey, whenBKey, whenControlKey,
|
||
loopRegistry, capStateRegistry, capabilityStatesById,
|
||
};
|
||
}
|
||
|
||
function capStateHooks(fixture, config) {
|
||
const result = resolveCapabilityState({
|
||
registry: fixture.capStateRegistry,
|
||
installedSkills: '*',
|
||
surfacedSkills: new Set(),
|
||
config,
|
||
});
|
||
assert.strictEqual(result.capabilities.length, 1, 'fixture declares exactly one capability');
|
||
return result.capabilities[0].hooks;
|
||
}
|
||
|
||
test('D1: loopResolverAndCapabilityStateAgreeOnDefaultPointFromSelection', () => {
|
||
const f = makeTwoPointFixture();
|
||
const config = {}; // everything resolves via schema default: enumKey -> pointA
|
||
|
||
const resultA = resolveLoopHooks({ point: f.pointA, registry: f.loopRegistry, config, capabilityStatesById: f.capabilityStatesById });
|
||
const resultB = resolveLoopHooks({ point: f.pointB, registry: f.loopRegistry, config, capabilityStatesById: f.capabilityStatesById });
|
||
const loopStepAActive = resultA.activeHooks.some((h) => h.when === f.whenAKey);
|
||
const loopStepBActive = resultB.activeHooks.some((h) => h.when === f.whenBKey);
|
||
assert.strictEqual(loopStepAActive, true, 'loop-resolver: point A step must be active by default');
|
||
assert.strictEqual(loopStepBActive, false, 'loop-resolver: point B step must be inactive by default');
|
||
|
||
const hooks = capStateHooks(f, config);
|
||
const stateStepA = hooks.find((h) => h.when === f.whenAKey);
|
||
const stateStepB = hooks.find((h) => h.when === f.whenBKey);
|
||
assert.strictEqual(stateStepA.active, true, 'capability-state: point A step must be active by default');
|
||
assert.strictEqual(stateStepB.active, false, 'capability-state: point B step must be inactive by default');
|
||
|
||
assert.strictEqual(loopStepAActive, stateStepA.active, 'resolvers must agree on point A');
|
||
assert.strictEqual(loopStepBActive, stateStepB.active, 'resolvers must agree on point B');
|
||
});
|
||
|
||
test('D2: loopResolverAndCapabilityStateAgreeOnFlippedPointFromSelection', () => {
|
||
const f = makeTwoPointFixture();
|
||
const config = { workflow: { test_point_select: f.pointB } };
|
||
|
||
const resultA = resolveLoopHooks({ point: f.pointA, registry: f.loopRegistry, config, capabilityStatesById: f.capabilityStatesById });
|
||
const resultB = resolveLoopHooks({ point: f.pointB, registry: f.loopRegistry, config, capabilityStatesById: f.capabilityStatesById });
|
||
const loopStepAActive = resultA.activeHooks.some((h) => h.when === f.whenAKey);
|
||
const loopStepBActive = resultB.activeHooks.some((h) => h.when === f.whenBKey);
|
||
assert.strictEqual(loopStepAActive, false, 'loop-resolver: point A step must flip to inactive');
|
||
assert.strictEqual(loopStepBActive, true, 'loop-resolver: point B step must flip to active');
|
||
|
||
const hooks = capStateHooks(f, config);
|
||
const stateStepA = hooks.find((h) => h.when === f.whenAKey);
|
||
const stateStepB = hooks.find((h) => h.when === f.whenBKey);
|
||
assert.strictEqual(stateStepA.active, false, 'capability-state: point A step must flip to inactive');
|
||
assert.strictEqual(stateStepB.active, true, 'capability-state: point B step must flip to active');
|
||
|
||
assert.strictEqual(loopStepAActive, stateStepA.active, 'resolvers must agree on point A after flip');
|
||
assert.strictEqual(loopStepBActive, stateStepB.active, 'resolvers must agree on point B after flip');
|
||
});
|
||
|
||
test('D3: loopResolverAndCapabilityStateAgreeOnOutOfEnumPointFromValue', () => {
|
||
const f = makeTwoPointFixture();
|
||
const config = { workflow: { test_point_select: 'bogus' } };
|
||
|
||
const resultA = resolveLoopHooks({ point: f.pointA, registry: f.loopRegistry, config, capabilityStatesById: f.capabilityStatesById });
|
||
const resultB = resolveLoopHooks({ point: f.pointB, registry: f.loopRegistry, config, capabilityStatesById: f.capabilityStatesById });
|
||
const loopStepAActive = resultA.activeHooks.some((h) => h.when === f.whenAKey);
|
||
const loopStepBActive = resultB.activeHooks.some((h) => h.when === f.whenBKey);
|
||
assert.strictEqual(loopStepAActive, false, 'loop-resolver: point A step must be inactive on an out-of-enum value');
|
||
assert.strictEqual(loopStepBActive, false, 'loop-resolver: point B step must be inactive on an out-of-enum value');
|
||
|
||
const hooks = capStateHooks(f, config);
|
||
const stateStepA = hooks.find((h) => h.when === f.whenAKey);
|
||
const stateStepB = hooks.find((h) => h.when === f.whenBKey);
|
||
assert.strictEqual(stateStepA.active, false, 'capability-state: point A step must be inactive on an out-of-enum value');
|
||
assert.strictEqual(stateStepB.active, false, 'capability-state: point B step must be inactive on an out-of-enum value');
|
||
|
||
assert.strictEqual(loopStepAActive, stateStepA.active, 'resolvers must agree: both points inactive');
|
||
assert.strictEqual(loopStepBActive, stateStepB.active, 'resolvers must agree: both points inactive');
|
||
});
|
||
|
||
test('D4: loopResolverAndCapabilityStateAgreeWhenPointFromAbsent', () => {
|
||
// Reuse D3's out-of-enum config — the strongest proof that the control step
|
||
// (no pointFrom at all) is UNAFFECTED by the enum's value in either resolver.
|
||
const f = makeTwoPointFixture();
|
||
const config = { workflow: { test_point_select: 'bogus' } };
|
||
|
||
const resultA = resolveLoopHooks({ point: f.pointA, registry: f.loopRegistry, config, capabilityStatesById: f.capabilityStatesById });
|
||
const loopControlActive = resultA.activeHooks.some((h) => h.when === f.whenControlKey);
|
||
assert.strictEqual(loopControlActive, true, 'loop-resolver: control step (no pointFrom) must remain active, governed by when alone');
|
||
|
||
const hooks = capStateHooks(f, config);
|
||
const stateControl = hooks.find((h) => h.when === f.whenControlKey);
|
||
assert.strictEqual(stateControl.active, true, 'capability-state: control step (no pointFrom) must remain active, governed by when alone');
|
||
|
||
assert.strictEqual(loopControlActive, stateControl.active, 'resolvers must agree the control step is unaffected');
|
||
});
|
||
});
|