fix(lease): restore uniform settings array replacement
AMD1213-B1: preserve the gated Claude hook composition explicitly in the lease overlay while restoring last-layer-wins arrays and null tombstones.
This commit is contained in:
@@ -94,7 +94,7 @@ describe('three-layer settings merge', () => {
|
||||
).toEqual({ nested: { keep: true } });
|
||||
});
|
||||
|
||||
it('concatenates hook event arrays so an overlay adds gating without erasing base hooks', () => {
|
||||
it('replaces a hook event array wholesale with the higher layer', () => {
|
||||
const qaStop = { hooks: [{ type: 'command', command: 'qa-stop.sh' }] };
|
||||
const leaseStop = { hooks: [{ type: 'command', command: 'receipt-observer.py' }] };
|
||||
const qaPre = { matcher: 'Write', hooks: [{ type: 'command', command: 'qa-pre.sh' }] };
|
||||
@@ -103,7 +103,140 @@ describe('three-layer settings merge', () => {
|
||||
{ hooks: { Stop: [qaStop], PreToolUse: [qaPre] } },
|
||||
{ hooks: { Stop: [leaseStop] } },
|
||||
),
|
||||
).toEqual({ hooks: { Stop: [qaStop, leaseStop], PreToolUse: [qaPre] } });
|
||||
).toEqual({ hooks: { Stop: [leaseStop], PreToolUse: [qaPre] } });
|
||||
});
|
||||
|
||||
it('reconstructs every gated hook event from the base and lease overlay', () => {
|
||||
const fx = fixture({ schema: 1, harness: 'claude', overlay: 'lease-overlay.json' });
|
||||
const frameworkRuntime = join(process.cwd(), 'framework', 'runtime', 'claude');
|
||||
writeFileSync(
|
||||
join(fx.systemHome, 'runtime', 'claude', 'settings.json'),
|
||||
readFileSync(join(frameworkRuntime, 'settings.json'), 'utf8'),
|
||||
);
|
||||
writeFileSync(
|
||||
join(fx.agentDir, 'lease-overlay.json'),
|
||||
readFileSync(join(frameworkRuntime, 'lease-overlay.json'), 'utf8'),
|
||||
);
|
||||
|
||||
const plan = resolveFleetLaunchComposition('fred', {
|
||||
systemHome: fx.systemHome,
|
||||
userHome: fx.userHome,
|
||||
});
|
||||
|
||||
expect(plan.settings.merged['hooks']).toEqual({
|
||||
PreToolUse: [
|
||||
{
|
||||
matcher: 'Write|Edit|MultiEdit',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: '~/.config/mosaic/tools/qa/prevent-memory-write.sh',
|
||||
timeout: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
matcher: '.*',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command:
|
||||
'python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude --recovery-command ~/.config/mosaic/tools/lease-broker/recover-context.py',
|
||||
timeout: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|MultiEdit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: '~/.config/mosaic/tools/qa/qa-hook-stdin.sh',
|
||||
timeout: 60,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
matcher: 'Edit|MultiEdit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: '~/.config/mosaic/tools/qa/typecheck-hook.sh',
|
||||
timeout: 30,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
Stop: [
|
||||
{
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: '~/.config/mosaic/tools/qa/reflect-stop-hook.sh',
|
||||
timeout: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command:
|
||||
'python3 ~/.config/mosaic/tools/lease-broker/receipt-observer-client.py --runtime claude --latest-entry; observer_status=$?; python3 ~/.config/mosaic/tools/lease-broker/promote-complete.py; exit $observer_status',
|
||||
timeout: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
PreCompact: [
|
||||
{
|
||||
matcher: '.*',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command:
|
||||
'python3 "$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py" --runtime claude --reason pre-compact',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
SessionStart: [
|
||||
{
|
||||
matcher: 'compact',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command:
|
||||
'python3 "$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py" --runtime claude --reason session-start-compact',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
matcher: 'resume|clear',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command:
|
||||
'python3 "$HOME/.config/mosaic/tools/lease-broker/revoke-lease.py" --runtime claude --reason session-start-rollover --bump-generation',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
matcher: '^/mosaic-promote$',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: 'python3 ~/.config/mosaic/tools/lease-broker/promote-begin.py',
|
||||
timeout: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('still deletes a whole hook event via the null tombstone', () => {
|
||||
@@ -115,13 +248,13 @@ describe('three-layer settings merge', () => {
|
||||
).toEqual({ hooks: {} });
|
||||
});
|
||||
|
||||
it('keeps replace semantics for arrays outside the top-level hooks object', () => {
|
||||
it('replaces an allowedCommands-shaped non-hook array wholesale', () => {
|
||||
expect(
|
||||
deepMergeSettings(
|
||||
{ plugins: ['base'], nested: { hooks: { Stop: ['base'] } } },
|
||||
{ plugins: ['user'], nested: { hooks: { Stop: ['user'] } } },
|
||||
{ allowedCommands: ['pnpm', 'git'], nested: { hooks: { Stop: ['base'] } } },
|
||||
{ allowedCommands: ['node'], nested: { hooks: { Stop: ['user'] } } },
|
||||
),
|
||||
).toEqual({ plugins: ['user'], nested: { hooks: { Stop: ['user'] } } });
|
||||
).toEqual({ allowedCommands: ['node'], nested: { hooks: { Stop: ['user'] } } });
|
||||
});
|
||||
|
||||
it('deep-merges all three layers in precedence order', () => {
|
||||
|
||||
Reference in New Issue
Block a user