mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-11 17:32:22 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61452c2997 |
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);
|
||||
@@ -498,7 +498,7 @@ for(const end of ['expiry','cancel','disable','replacement'])it(`manual ${end} d
|
||||
if(end==='replacement')f.generation('generation-two');
|
||||
return {ok:true,activity:{state:'idle',reason:'fixture',evidenceSource:'fixture'},waitedMs:0,attempts:1} as any;
|
||||
});
|
||||
expect(await f.e.triggerManualCompact(input,{operatorInitiated:true})).toMatchObject({triggered:false,reason:{expiry:'preparation_incomplete',cancel:'preparation_cancelled',disable:'disabled',replacement:'stale_generation'}[end]});
|
||||
expect(await f.e.triggerManualCompact(input,{operatorInitiated:true})).toMatchObject({triggered:false,reason:{expiry:'map_complete_seat_never_idle',cancel:'preparation_cancelled',disable:'disabled',replacement:'stale_generation'}[end]});
|
||||
expect(idle).toHaveBeenCalledTimes(1);
|
||||
expect(compacts(f)).toHaveLength(0);expect(existsSync(f.e.getPreparationState(seat)!.mapPath)).toBe(false);
|
||||
});
|
||||
@@ -699,7 +699,7 @@ it('manual attention clears within the same attempt with one preparation and one
|
||||
});
|
||||
it('manual unresolved attention consumes the deadline and never revives',async()=>{
|
||||
const f=fixture();const start=f.clock();f.onSleep(async()=>{stageMap(f);f.activity('needs_input');});
|
||||
expect(await f.e.triggerManualCompact(input,{operatorInitiated:true})).toMatchObject({triggered:false,reason:'preparation_incomplete'});
|
||||
expect(await f.e.triggerManualCompact(input,{operatorInitiated:true})).toMatchObject({triggered:false,reason:'map_complete_seat_never_idle'});
|
||||
expect(f.clock()).toBe(start+1000);expect(compacts(f)).toHaveLength(0);
|
||||
f.activity('idle');stageMap(f);await f.e.maybeAutoCompact(input);expect(compacts(f)).toHaveLength(0);
|
||||
});
|
||||
@@ -736,3 +736,30 @@ 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);
|
||||
});
|
||||
|
||||
|
||||
for(const fresh of [false,null])it(`an armed completed map reaches the next idle boundary with usage freshness ${fresh}`,async()=>{
|
||||
const f=fixture();await f.e.maybeAutoCompact(input);stageMap(f);
|
||||
f.advance(AUTO_PREP_WAIT_MS_DEFAULT+1);f.activity('running');
|
||||
const row={node_id:'node-one',session_id:1,session_name:seat,runtime:'claude-code',cwd:null,startup_status:'ready'};
|
||||
const db={prepare:()=>({all:()=>[row]})};
|
||||
const store={readAndNormalize:()=>fresh===null?null:{availability:'known',fresh:false,usedPercentage:90},persist:()=>{}};
|
||||
const monitor=new ContextMonitor(db as any,store as any,undefined,f.e);
|
||||
await monitor.pollOnce();expect(compacts(f)).toHaveLength(0);
|
||||
f.activity('idle');await monitor.pollOnce();
|
||||
expect(compacts(f)).toHaveLength(1);expect(f.writes.filter(t=>!t.startsWith('/compact'))).toHaveLength(1);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user