This commit is contained in:
174
packages/hive-mind-hooks-openclaw/tests/json5-merger.test.ts
Normal file
174
packages/hive-mind-hooks-openclaw/tests/json5-merger.test.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import JSON5 from 'json5';
|
||||
import {
|
||||
HIVE_ENTRY_KEY,
|
||||
HOOKS_KEY,
|
||||
hasHiveEntries,
|
||||
jsonRegister,
|
||||
jsonUnregister,
|
||||
parseConfig,
|
||||
serializeConfig,
|
||||
} from '../src/json5-merger.js';
|
||||
|
||||
function internal(config: Record<string, unknown>): Record<string, unknown> {
|
||||
const hooks = config[HOOKS_KEY] as Record<string, unknown> | undefined;
|
||||
return (hooks?.['internal'] as Record<string, unknown> | undefined) ?? {};
|
||||
}
|
||||
|
||||
function entries(config: Record<string, unknown>): Record<string, unknown> {
|
||||
return (internal(config)['entries'] as Record<string, unknown> | undefined) ?? {};
|
||||
}
|
||||
|
||||
describe('parseConfig (openclaw JSON5 codec — strict JSON first, JSON5 fallback)', () => {
|
||||
it('returns {} for an empty / whitespace-only config (create-if-missing)', () => {
|
||||
expect(parseConfig('')).toEqual({});
|
||||
expect(parseConfig(' \n ')).toEqual({});
|
||||
});
|
||||
|
||||
it('parses strict JSON (the fast path)', () => {
|
||||
const parsed = parseConfig('{"model":"opus","hooks":{}}');
|
||||
expect(parsed['model']).toBe('opus');
|
||||
});
|
||||
|
||||
it('parses a COMMENTED openclaw.json with trailing commas via the JSON5 fallback', () => {
|
||||
const raw = [
|
||||
'{',
|
||||
' // OpenClaw gateway config — hand-edited, comments matter',
|
||||
' model: "claude-opus", /* the good one */',
|
||||
' hooks: {',
|
||||
' internal: {',
|
||||
' enabled: false,',
|
||||
' entries: {',
|
||||
" 'user-own': { enabled: true }, // trailing comma below",
|
||||
' },',
|
||||
' },',
|
||||
' },',
|
||||
'}',
|
||||
].join('\n');
|
||||
const parsed = parseConfig(raw);
|
||||
expect(parsed['model']).toBe('claude-opus');
|
||||
const userEntry = entries(parsed)['user-own'] as Record<string, unknown>;
|
||||
expect(userEntry['enabled']).toBe(true);
|
||||
});
|
||||
|
||||
it('throws when BOTH strict JSON and JSON5 fail (installer fails loudly, never clobbers)', () => {
|
||||
expect(() => parseConfig('{ this : : : is not valid }')).toThrow(/parse/i);
|
||||
});
|
||||
|
||||
it('treats a top-level scalar/array JSON5 doc as empty (not a crash)', () => {
|
||||
expect(parseConfig('"just a string"')).toEqual({});
|
||||
expect(parseConfig('[1, 2, 3]')).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeConfig (openclaw — strict JSON, valid for OpenClaw JSON5 reader)', () => {
|
||||
it('serializes to 2-space JSON with a trailing newline that re-parses', () => {
|
||||
const text = serializeConfig(jsonRegister({ model: 'x' }));
|
||||
expect(text.endsWith('\n')).toBe(true);
|
||||
const reparsed = JSON5.parse(text) as Record<string, unknown>;
|
||||
expect(reparsed['model']).toBe('x');
|
||||
expect(hasHiveEntries(reparsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('jsonRegister (openclaw minimal-touch — flip enabled + add hive entry)', () => {
|
||||
it('returns a NEW object — does not mutate input (immutability contract)', () => {
|
||||
const original: Record<string, unknown> = { hooks: { internal: { enabled: false } } };
|
||||
const merged = jsonRegister(original);
|
||||
expect(merged).not.toBe(original);
|
||||
// Input untouched — enabled is still false on the original.
|
||||
expect((original['hooks'] as Record<string, Record<string, unknown>>)['internal']['enabled']).toBe(false);
|
||||
});
|
||||
|
||||
it('flips hooks.internal.enabled=true and adds entries["hive-mind"]={enabled:true}', () => {
|
||||
const merged = jsonRegister({});
|
||||
expect(internal(merged)['enabled']).toBe(true);
|
||||
const hive = entries(merged)[HIVE_ENTRY_KEY] as Record<string, unknown>;
|
||||
expect(hive).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it('attaches env to the hive entry when supplied', () => {
|
||||
const merged = jsonRegister({}, { env: { WAGGLE_HIVE_MIND_CLI: '/abs/cli.js', WAGGLE_WORKSPACE_ID: 'ws1' } });
|
||||
const hive = entries(merged)[HIVE_ENTRY_KEY] as Record<string, unknown>;
|
||||
expect(hive['env']).toEqual({ WAGGLE_HIVE_MIND_CLI: '/abs/cli.js', WAGGLE_WORKSPACE_ID: 'ws1' });
|
||||
});
|
||||
|
||||
it('does NOT clobber other internal entries or other internal keys (minimal-touch)', () => {
|
||||
const existing: Record<string, unknown> = {
|
||||
model: 'opus',
|
||||
hooks: {
|
||||
internal: {
|
||||
enabled: false,
|
||||
throttleMs: 250,
|
||||
entries: { 'user-own': { enabled: true, foo: 'bar' } },
|
||||
},
|
||||
external: { whatever: 1 },
|
||||
},
|
||||
};
|
||||
const merged = jsonRegister(existing);
|
||||
// user entry preserved verbatim.
|
||||
expect(entries(merged)['user-own']).toEqual({ enabled: true, foo: 'bar' });
|
||||
// sibling internal key preserved.
|
||||
expect(internal(merged)['throttleMs']).toBe(250);
|
||||
// sibling hooks subtree preserved.
|
||||
expect((merged['hooks'] as Record<string, unknown>)['external']).toEqual({ whatever: 1 });
|
||||
// unrelated top-level key preserved.
|
||||
expect(merged['model']).toBe('opus');
|
||||
// and our entry was added + subsystem turned on.
|
||||
expect(internal(merged)['enabled']).toBe(true);
|
||||
expect(hasHiveEntries(merged)).toBe(true);
|
||||
});
|
||||
|
||||
it('replaces OUR entry in place on re-install (idempotent — never duplicated)', () => {
|
||||
const merged1 = jsonRegister({}, { env: { A: '1' } });
|
||||
const merged2 = jsonRegister(merged1, { env: { A: '2' } });
|
||||
const hive = entries(merged2)[HIVE_ENTRY_KEY] as Record<string, unknown>;
|
||||
expect(hive['env']).toEqual({ A: '2' });
|
||||
// Exactly one hive-mind entry key.
|
||||
expect(Object.keys(entries(merged2)).filter((k) => k === HIVE_ENTRY_KEY)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('jsonUnregister (openclaw — diagnostics / backup-less path)', () => {
|
||||
it('strips our hive-mind entry but preserves other entries + leaves enabled as-is', () => {
|
||||
const withUser: Record<string, unknown> = {
|
||||
hooks: { internal: { enabled: true, entries: { 'user-own': { enabled: true } } } },
|
||||
};
|
||||
const merged = jsonRegister(withUser);
|
||||
expect(hasHiveEntries(merged)).toBe(true);
|
||||
|
||||
const stripped = jsonUnregister(merged);
|
||||
expect(hasHiveEntries(stripped)).toBe(false);
|
||||
// User entry survives.
|
||||
expect(entries(stripped)['user-own']).toEqual({ enabled: true });
|
||||
// Minimal-touch: we do NOT flip enabled back off (other hooks may rely on it).
|
||||
expect(internal(stripped)['enabled']).toBe(true);
|
||||
});
|
||||
|
||||
it('returns a NEW object and leaves the input untouched (immutability)', () => {
|
||||
const merged = jsonRegister({});
|
||||
const stripped = jsonUnregister(merged);
|
||||
expect(stripped).not.toBe(merged);
|
||||
expect(hasHiveEntries(merged)).toBe(true); // original still has the entry
|
||||
});
|
||||
|
||||
it('is a no-op (new object) when there is no hooks/internal block', () => {
|
||||
const stripped = jsonUnregister({ model: 'x' });
|
||||
expect(stripped['model']).toBe('x');
|
||||
expect(hasHiveEntries(stripped)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasHiveEntries (openclaw structural marker)', () => {
|
||||
it('false on empty / hookless / hive-less configs', () => {
|
||||
expect(hasHiveEntries(undefined)).toBe(false);
|
||||
expect(hasHiveEntries({})).toBe(false);
|
||||
expect(hasHiveEntries({ hooks: {} })).toBe(false);
|
||||
expect(hasHiveEntries({ hooks: { internal: { enabled: true, entries: {} } } })).toBe(false);
|
||||
expect(hasHiveEntries({ hooks: { internal: { entries: { 'user-own': {} } } } })).toBe(false);
|
||||
});
|
||||
|
||||
it('true once our hive-mind entry is present', () => {
|
||||
expect(hasHiveEntries(jsonRegister({}))).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user