Compare commits

...
3 changed files with 50 additions and 8 deletions

No files matched your search

@@ -133,15 +133,15 @@ it('automatic: a map that is ready while the seat is mid-turn waits for idle ins
expect(f.compactIndex()).toBeGreaterThan(-1);
});
it('automatic: a seat that never goes idle is disarmed by the existing preparation deadline, never sent to', async () => {
it('automatic: a completed map that never reaches idle expires at the bounded idle deadline', async () => {
const f = fixture();
await f.e.maybeAutoCompact(input);
f.stageMap();
f.activity('running');
await f.e.maybeAutoCompact(input);
f.advance(AUTO_PREP_WAIT_MS_DEFAULT + 1);
f.advance(AUTO_PREP_WAIT_MS_DEFAULT + AUTO_RETRY_AFTER_STOP_MS_DEFAULT + 1);
await f.e.maybeAutoCompact(input);
expect(f.e.getPreparationState(seat)).toMatchObject({status: 'stopped', reason: 'preparation_incomplete', compact: 'not_sent'});
expect(f.e.getPreparationState(seat)).toMatchObject({status: 'stopped', reason: 'map_complete_seat_never_idle', compact: 'not_sent'});
expect(f.compactIndex()).toBe(-1);
});
@@ -696,3 +696,18 @@ for (const completedStages of [1, 2]) {
expect(f.events).toEqual(before);
});
}
it('automatic: completed map survives the prep deadline and sends once at the next idle boundary without re-preparing', async () => {
const f = fixture(); await f.e.maybeAutoCompact(input); f.stageMap();
const first = f.e.getPreparationState(seat)!;
f.activity('running'); f.advance(AUTO_PREP_WAIT_MS_DEFAULT + 1);
f.e.reconcilePreparations();
expect(f.e.getPreparationState(seat)).toMatchObject({attemptId: first.attemptId, status: 'waiting',
deadlineAt: first.deadlineAt, idleDeadlineAt: first.deadlineAt! + AUTO_RETRY_AFTER_STOP_MS_DEFAULT});
expect(await f.e.maybeAutoCompact(input)).toEqual({triggered: false, reason: 'waiting_for_idle'});
expect(f.texts()).toHaveLength(1);
f.activity('idle'); expect(await f.e.maybeAutoCompact(input)).toEqual({triggered: true});
expect(f.texts().filter(t => t.startsWith('/compact'))).toHaveLength(1);
expect(f.e.getPreparationState(seat)).toMatchObject({attemptId: first.attemptId, status: 'compact-sent'});
});
@@ -298,7 +298,7 @@ it('automatic preparation gives the post-delivery ceiling without a fabricated p
expect(prompt).toContain(f.e.getPreparationState(seat)!.marker);
});
for(const expiry of ['missing map','idle after completed map'])it(`route reports incomplete preparation on ${expiry} expiry`,async()=>{
for(const expiry of ['missing map','idle after completed map'])it(`route distinguishes ${expiry} expiry`,async()=>{
const f=fixture();
if(expiry==='idle after completed map')f.onSleep(async()=>{stageMap(f);f.activity('busy');});
vi.spyOn(f.transport,'resolveSessions').mockResolvedValue({ok:true,sessions:[seat]} as any);
@@ -317,9 +317,9 @@ for(const expiry of ['missing map','idle after completed map'])it(`route reports
});
const body=await response.json();
expect(response.status).toBe(409);
expect(body.reason).toBe('preparation_incomplete');
expect(body.error).toContain('Preparation (restore map and idle wait) did not finish in time');
expect(body.error).not.toContain('restore map was not completed');
expect(body.reason).toBe(expiry==='missing map'?'preparation_incomplete':'map_complete_seat_never_idle');
expect(body.error).toContain(expiry==='missing map'?'restore map was not completed':'map complete, seat never idle');
expect(f.clock()).toBeLessThanOrEqual(11000);
expect(body.error).toContain('managed compaction is disarmed');
expect(f.e.getPreparationState(seat)?.status).toBe('stopped');
expect(compacts(f)).toHaveLength(0);
@@ -736,3 +736,17 @@ it('manual uncertain compact receipt never retries input',async()=>{
await f.e.maybeAutoCompact(input);await f.e.maybeAutoCompact(input);
expect(compacts(f)).toHaveLength(1);expect(f.e.getPreparationState(seat)?.compact).toBe('uncertain');
});
for (const end of ['cancel', 'disable', 'replacement'] as const) it(`automatic: completed map still stops on ${end} during idle grace`, async () => {
const f=fixture();await f.e.maybeAutoCompact(input);stageMap(f);
f.advance(AUTO_PREP_WAIT_MS_DEFAULT+1);f.e.reconcilePreparations();
expect(f.e.getPreparationState(seat)?.status).toBe('waiting');
if(end==='cancel')f.e.cancelPreparation(seat);
if(end==='disable')f.policy.enabled=false;
if(end==='replacement')f.generation('generation-two');
f.e.reconcilePreparations();
expect(f.e.getPreparationState(seat)).toMatchObject({status:'stopped',reason:
{cancel:'preparation_cancelled',disable:'disabled',replacement:'stale_generation'}[end]});
expect(compacts(f)).toHaveLength(0);
});
@@ -6,7 +6,7 @@ import {tmpdir} from 'node:os';
import {join, dirname} from 'node:path';
import {randomUUID} from 'node:crypto';
import {Hono} from 'hono';
import {ClaudeCompactionEnforcer, RESTART_RECOVERY_WINDOW_MS_DEFAULT, buildPostCompactTurnBoundaryPrompt} from '../src/domain/claude-compaction-enforcer.js';
import {ClaudeCompactionEnforcer, AUTO_PREP_WAIT_MS_DEFAULT, RESTART_RECOVERY_WINDOW_MS_DEFAULT, buildPostCompactTurnBoundaryPrompt} from '../src/domain/claude-compaction-enforcer.js';
import {SessionTransport} from '../src/domain/session-transport.js';
import {SeatDeliveryGuard} from '../src/domain/seat-delivery-guard.js';
import {compactionRoutes} from '../src/routes/compaction.js';
@@ -343,3 +343,16 @@ it.each(['unresolved','rejected'])('owner notice starts after the lifecycle leas
if(mode==='rejected')expect(e.getOwedRestore(seat)?.lastDelivery?.noticeErrorClass).toBe('RangeError');
expect(readFileSync(recordFile(e),'utf8')).not.toContain('PRIVATE NOTICE ERROR');
});
it('a restart retains a completed automatic map after its prep deadline and the delayed compact still owes restore', async () => {
const f=fixture(),first=f.daemon();await first.maybeAutoCompact(high);const a=completeMap(first);
f.hooks.busy=true;f.advance(AUTO_PREP_WAIT_MS_DEFAULT+1);
const restarted=f.daemon();await restarted.maybeAutoCompact(high);
expect(restarted.getPreparationState(seat)).toMatchObject({attemptId:a.attemptId,status:'waiting'});
expect(prepPrompts(f)).toHaveLength(1);expect(compacts(f)).toHaveLength(0);
f.hooks.busy=false;await restarted.maybeAutoCompact(high);
expect(compacts(f)).toHaveLength(1);expect(prepPrompts(f)).toHaveLength(1);
for(let i=0;i<4;i++)await staleTick(restarted);
expect(boundaries(f)).toHaveLength(1);expect(restores(f)).toHaveLength(1);expect(audits(f)).toHaveLength(1);
});