Skip to content
Open
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
76 changes: 76 additions & 0 deletions src/__tests__/remote-state-events.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { HttpClient } from '../client/http-client';
import { RemoteState } from '../conversation/remote-state';
import { ConversationStateUpdateEvent } from '../events/types';

const originalFetch = global.fetch;

/** A fetch mock that fails the test if the network is touched. */
function makeStateWithNoNetwork(): { state: RemoteState; fetchMock: jest.Mock } {
const fetchMock = jest
.fn()
.mockRejectedValue(new Error('network should not be called')) as jest.Mock;
global.fetch = fetchMock as typeof fetch;
const client = new HttpClient({ baseUrl: 'http://example.com' });
return { state: new RemoteState(client, 'abc'), fetchMock };
}

describe('RemoteState.updateStateFromEvent with the canonical ConversationStateUpdateEvent', () => {
afterEach(() => {
global.fetch = originalFetch;
jest.restoreAllMocks();
});

it('applies a __full_state__ snapshot event and serves it from cache', async () => {
const { state, fetchMock } = makeStateWithNoNetwork();

// Uses the deduplicated type from events/types: BaseEvent shape (id/timestamp)
// plus the optional previous_value field that the old local copy lacked.
const event: ConversationStateUpdateEvent = {
id: 'evt-1',
kind: 'ConversationStateUpdateEvent',
timestamp: '2024-01-01T00:00:00Z',
source: 'agent',
key: '__full_state__',
value: { full_state: { execution_status: 'running', persistence_dir: '/data/abc' } },
previous_value: undefined,
};

await state.updateStateFromEvent(event);

await expect(state.getExecutionStatus()).resolves.toBe('running');
await expect(state.getPersistenceDir()).resolves.toBe('/data/abc');
expect(fetchMock).not.toHaveBeenCalled();
});

it('applies a single-key update event', async () => {
const { state } = makeStateWithNoNetwork();

const event: ConversationStateUpdateEvent = {
id: 'evt-2',
kind: 'ConversationStateUpdateEvent',
timestamp: '2024-01-01T00:00:00Z',
key: 'execution_status',
value: 'idle',
};

await state.updateStateFromEvent(event);
await expect(state.getExecutionStatus()).resolves.toBe('idle');
});

it('routes ConversationStateUpdateEvent through createStateUpdateCallback', async () => {
const { state } = makeStateWithNoNetwork();
const callback = state.createStateUpdateCallback();

callback({
id: 'evt-3',
kind: 'ConversationStateUpdateEvent',
timestamp: '2024-01-01T00:00:00Z',
key: 'execution_status',
value: 'finished',
} as ConversationStateUpdateEvent);

// The callback dispatches asynchronously; allow the microtask queue to drain.
await new Promise((resolve) => setTimeout(resolve, 0));
await expect(state.getExecutionStatus()).resolves.toBe('finished');
});
});
12 changes: 5 additions & 7 deletions src/conversation/remote-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,10 @@
ConversationCallbackType,
} from '../types/base';
import { ConversationInfo } from '../models/conversation';
import { ConversationStateUpdateEvent } from '../events/types';

const FULL_STATE_KEY = '__full_state__';

export interface ConversationStateUpdateEvent extends Event {
kind: 'ConversationStateUpdateEvent';
key: string;
value: any;
}

export class RemoteState {
private client: HttpClient;
private conversationId: string;
Expand Down Expand Up @@ -51,7 +46,7 @@
}

// Fetch from REST API and normalize the `full_state` wrapper once.
const response = await this.client.get<any>(`/api/conversations/${this.conversationId}`);

Check warning on line 49 in src/conversation/remote-state.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

Unexpected any. Specify a different type

Check warning on line 49 in src/conversation/remote-state.ts

View workflow job for this annotation

GitHub Actions / test (22.12)

Unexpected any. Specify a different type
const conversationInfo = this.normalizeFullState(response.data);

this.cachedState = conversationInfo;
Expand All @@ -66,7 +61,7 @@
* unwrap themselves.
*/
private normalizeFullState(info: ConversationInfo): ConversationInfo {
return ((info as any).full_state ?? info) as ConversationInfo;

Check warning on line 64 in src/conversation/remote-state.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

Unexpected any. Specify a different type

Check warning on line 64 in src/conversation/remote-state.ts

View workflow job for this annotation

GitHub Actions / test (22.12)

Unexpected any. Specify a different type
}

/**
Expand All @@ -84,13 +79,16 @@
if (this.cachedState === null) {
this.cachedState = {} as ConversationInfo;
}
const stateValue = event.value?.full_state ?? event.value;
const stateValue =
typeof event.value === 'object' && event.value !== null && 'full_state' in event.value
? (event.value.full_state ?? event.value)
: event.value;
Object.assign(this.cachedState, stateValue);
} else {
if (this.cachedState === null) {
this.cachedState = {} as ConversationInfo;
}
(this.cachedState as any)[event.key] = event.value;

Check warning on line 91 in src/conversation/remote-state.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

Unexpected any. Specify a different type

Check warning on line 91 in src/conversation/remote-state.ts

View workflow job for this annotation

GitHub Actions / test (22.12)

Unexpected any. Specify a different type
}
this.cachedAt = Date.now();
});
Expand Down Expand Up @@ -171,7 +169,7 @@
return agentData;
}

async getWorkspace(): Promise<any> {

Check warning on line 172 in src/conversation/remote-state.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

Unexpected any. Specify a different type

Check warning on line 172 in src/conversation/remote-state.ts

View workflow job for this annotation

GitHub Actions / test (22.12)

Unexpected any. Specify a different type
const info = await this.getConversationInfo();
const workspace = info.workspace;
if (workspace === undefined || workspace === null) {
Expand All @@ -189,7 +187,7 @@
return persistenceDir;
}

async modelDump(): Promise<Record<string, any>> {

Check warning on line 190 in src/conversation/remote-state.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

Unexpected any. Specify a different type

Check warning on line 190 in src/conversation/remote-state.ts

View workflow job for this annotation

GitHub Actions / test (22.12)

Unexpected any. Specify a different type
const info = await this.getConversationInfo();
return info as Record<string, any>;
}
Expand Down
Loading