Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions kiloclaw/src/durable-objects/kiloclaw-instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
});
});

// ============================================================================
Expand Down
8 changes: 4 additions & 4 deletions kiloclaw/src/durable-objects/kiloclaw-instance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
}

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);
Expand Down Expand Up @@ -944,7 +944,7 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
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' ||
Expand Down Expand Up @@ -990,8 +990,8 @@ export class KiloClawInstance extends DurableObject<KiloClawEnv> {
async destroy(): Promise<DestroyResult> {
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;
Expand Down
Loading