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 JsonObject = Record<string, unknown>;
|
||||||
|
|
||||||
|
type JsonArray = unknown[];
|
||||||
|
|
||||||
function isJsonObject(value: unknown): value is JsonObject {
|
function isJsonObject(value: unknown): value is JsonObject {
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
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 {
|
function deepMergeJsonObjects(target: JsonObject, source: JsonObject): JsonObject {
|
||||||
const merged: JsonObject = { ...target };
|
const merged: JsonObject = { ...target };
|
||||||
for (const [key, value] of Object.entries(source)) {
|
for (const [key, value] of Object.entries(source)) {
|
||||||
const current = merged[key];
|
const current = merged[key];
|
||||||
|
if (key === 'hooks' && isJsonObject(current) && isJsonObject(value)) {
|
||||||
|
merged[key] = mergeHooksObject(current, value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (isJsonObject(current) && isJsonObject(value)) {
|
if (isJsonObject(current) && isJsonObject(value)) {
|
||||||
merged[key] = deepMergeJsonObjects(current, value);
|
merged[key] = deepMergeJsonObjects(current, value);
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -64,4 +64,71 @@ describe('mergeJsonSettingsArgs', () => {
|
||||||
'{"fastMode":false,"codex":{"forced_login_method":"chatgpt"}}',
|
'{"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