diff --git a/kiloclaw/src/durable-objects/kiloclaw-instance.test.ts b/kiloclaw/src/durable-objects/kiloclaw-instance.test.ts index d93c7911e..335b4eddd 100644 --- a/kiloclaw/src/durable-objects/kiloclaw-instance.test.ts +++ b/kiloclaw/src/durable-objects/kiloclaw-instance.test.ts @@ -322,6 +322,20 @@ afterEach(() => { }); describe('two-phase destroy', () => { + it('throws with status 404 when instance was never provisioned', async () => { + const { instance } = createInstance(); + + const err: Error & { status?: number } = await instance.destroy().then( + () => { + throw new Error('expected rejection'); + }, + (e: Error & { status?: number }) => e + ); + + expect(err.message).toBe('Instance not provisioned'); + expect(err.status).toBe(404); + }); + it('clears all state when both Fly deletes succeed', async () => { const { instance, storage } = createInstance(); await seedRunning(storage); @@ -1246,6 +1260,22 @@ describe('alarm runs for all live statuses', () => { }); }); +describe('start: not provisioned', () => { + it('throws with status 404 when instance was never provisioned', async () => { + const { instance } = createInstance(); + + const err: Error & { status?: number } = await instance.start('user-1').then( + () => { + throw new Error('expected rejection'); + }, + (e: Error & { status?: number }) => e + ); + + expect(err.message).toBe('Instance not provisioned'); + expect(err.status).toBe(404); + }); +}); + describe('startExistingMachine: transient vs 404 errors', () => { it('does NOT recreate machine on transient 500 error', async () => { const { instance, storage } = createInstance(); @@ -2888,6 +2918,20 @@ describe('stop: error propagation', () => { expect(storage._store.get('status')).toBe('stopped'); expect(storage._store.get('lastStoppedAt')).toBeDefined(); }); + + it('throws with status 404 when instance was never provisioned', async () => { + const { instance } = createInstance(); + + const err: Error & { status?: number } = await instance.stop().then( + () => { + throw new Error('expected rejection'); + }, + (e: Error & { status?: number }) => e + ); + + expect(err.message).toBe('Instance not provisioned'); + expect(err.status).toBe(404); + }); }); // ============================================================================ diff --git a/kiloclaw/src/durable-objects/kiloclaw-instance/index.ts b/kiloclaw/src/durable-objects/kiloclaw-instance/index.ts index 18350fec7..e631d0f18 100644 --- a/kiloclaw/src/durable-objects/kiloclaw-instance/index.ts +++ b/kiloclaw/src/durable-objects/kiloclaw-instance/index.ts @@ -690,7 +690,7 @@ export class KiloClawInstance extends DurableObject { } if (!this.s.userId || !this.s.sandboxId) { - throw new Error('Instance not provisioned'); + throw Object.assign(new Error('Instance not provisioned'), { status: 404 }); } const flyConfig = getFlyConfig(this.env, this.s); @@ -944,7 +944,7 @@ export class KiloClawInstance extends DurableObject { await this.loadState(); if (!this.s.userId || !this.s.sandboxId) { - throw new Error('Instance not provisioned'); + throw Object.assign(new Error('Instance not provisioned'), { status: 404 }); } if ( this.s.status === 'stopped' || @@ -990,8 +990,8 @@ export class KiloClawInstance extends DurableObject { async destroy(): Promise { await this.loadState(); - if (!this.s.userId) { - throw new Error('Instance not provisioned'); + if (!this.s.userId || !this.s.sandboxId) { + throw Object.assign(new Error('Instance not provisioned'), { status: 404 }); } const machineUptimeMs = this.s.lastStartedAt ? Date.now() - this.s.lastStartedAt : 0;