test: characterize runtime settings hook merge
This commit is contained in:
parent
90401b41a1
commit
b2edfe5d2a
2 changed files with 136 additions and 0 deletions
|
|
@ -1,5 +1,7 @@
|
|||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
type JsonArray = unknown[];
|
||||
|
||||
function isJsonObject(value: unknown): value is JsonObject {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
|
@ -13,10 +15,77 @@ function parseJsonSettingsObject(raw: string): JsonObject | null {
|
|||
}
|
||||
}
|
||||
|
||||
function isHookEntry(value: unknown): value is JsonObject {
|
||||
return isJsonObject(value) && Array.isArray(value.hooks);
|
||||
}
|
||||
|
||||
function getHookEntryDedupeKey(value: unknown): string | null {
|
||||
if (!isHookEntry(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commands = value.hooks
|
||||
.map((hook) => (isJsonObject(hook) && typeof hook.command === 'string' ? hook.command : null))
|
||||
.filter((command): command is string => Boolean(command));
|
||||
if (commands.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
matcher: typeof value.matcher === 'string' ? value.matcher : '',
|
||||
commands,
|
||||
});
|
||||
}
|
||||
|
||||
function mergeHookEntryArrays(target: JsonArray, source: JsonArray): JsonArray {
|
||||
const merged = [...target];
|
||||
const seen = new Set<string>();
|
||||
for (const entry of merged) {
|
||||
const key = getHookEntryDedupeKey(entry);
|
||||
if (key) {
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of source) {
|
||||
const key = getHookEntryDedupeKey(entry);
|
||||
if (key && seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
merged.push(entry);
|
||||
if (key) {
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
function mergeHooksObject(target: JsonObject, source: JsonObject): JsonObject {
|
||||
const merged: JsonObject = { ...target };
|
||||
for (const [hookName, sourceValue] of Object.entries(source)) {
|
||||
const currentValue = merged[hookName];
|
||||
if (Array.isArray(currentValue) && Array.isArray(sourceValue)) {
|
||||
merged[hookName] = mergeHookEntryArrays(currentValue, sourceValue);
|
||||
continue;
|
||||
}
|
||||
if (isJsonObject(currentValue) && isJsonObject(sourceValue)) {
|
||||
merged[hookName] = deepMergeJsonObjects(currentValue, sourceValue);
|
||||
continue;
|
||||
}
|
||||
merged[hookName] = sourceValue;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function deepMergeJsonObjects(target: JsonObject, source: JsonObject): JsonObject {
|
||||
const merged: JsonObject = { ...target };
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
const current = merged[key];
|
||||
if (key === 'hooks' && isJsonObject(current) && isJsonObject(value)) {
|
||||
merged[key] = mergeHooksObject(current, value);
|
||||
continue;
|
||||
}
|
||||
if (isJsonObject(current) && isJsonObject(value)) {
|
||||
merged[key] = deepMergeJsonObjects(current, value);
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -64,4 +64,71 @@ describe('mergeJsonSettingsArgs', () => {
|
|||
'{"fastMode":false,"codex":{"forced_login_method":"chatgpt"}}',
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves multiple hook entries for the same hook event', () => {
|
||||
const merged = mergeJsonSettingsArgs([
|
||||
'--settings',
|
||||
JSON.stringify({
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
matcher: '',
|
||||
hooks: [{ type: 'command', command: '/bin/sh user-stop.sh' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
'--settings',
|
||||
JSON.stringify({
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
matcher: '',
|
||||
hooks: [{ type: 'command', command: '/bin/sh app-stop.sh' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(JSON.parse(getSettingsValues(merged)[0] ?? '{}')).toEqual({
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
matcher: '',
|
||||
hooks: [{ type: 'command', command: '/bin/sh user-stop.sh' }],
|
||||
},
|
||||
{
|
||||
matcher: '',
|
||||
hooks: [{ type: 'command', command: '/bin/sh app-stop.sh' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('dedupes identical hook entries while preserving unrelated array replacement semantics', () => {
|
||||
const appHook = {
|
||||
matcher: '',
|
||||
hooks: [{ type: 'command', command: '/bin/sh app-stop.sh' }],
|
||||
};
|
||||
|
||||
const merged = mergeJsonSettingsArgs([
|
||||
'--settings',
|
||||
JSON.stringify({
|
||||
permissions: { allow: ['Read'] },
|
||||
hooks: { Stop: [appHook] },
|
||||
}),
|
||||
'--settings',
|
||||
JSON.stringify({
|
||||
permissions: { allow: ['Bash'] },
|
||||
hooks: { Stop: [appHook] },
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(JSON.parse(getSettingsValues(merged)[0] ?? '{}')).toEqual({
|
||||
permissions: { allow: ['Bash'] },
|
||||
hooks: { Stop: [appHook] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue