diff --git a/core/test/agents/active_streaming_tool_test.ts b/core/test/agents/active_streaming_tool_test.ts new file mode 100644 index 00000000..e756022a --- /dev/null +++ b/core/test/agents/active_streaming_tool_test.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {describe, expect, it} from 'vitest'; +import {ActiveStreamingTool} from '../../src/agents/active_streaming_tool.js'; +import {LiveRequestQueue} from '../../src/agents/live_request_queue.js'; + +describe('ActiveStreamingTool', () => { + it('should have undefined fields when constructed with no params', () => { + const tool = new ActiveStreamingTool(); + expect(tool.task).toBeUndefined(); + expect(tool.stream).toBeUndefined(); + }); + + it('should set task when constructed with only task param', () => { + const task = Promise.resolve(); + const tool = new ActiveStreamingTool({task}); + expect(tool.task).toBe(task); + expect(tool.stream).toBeUndefined(); + }); + + it('should set stream when constructed with only stream param', () => { + const stream = new LiveRequestQueue(); + const tool = new ActiveStreamingTool({stream}); + expect(tool.task).toBeUndefined(); + expect(tool.stream).toBe(stream); + }); + + it('should set both task and stream when both params are provided', () => { + const task = Promise.resolve(); + const stream = new LiveRequestQueue(); + const tool = new ActiveStreamingTool({task, stream}); + expect(tool.task).toBe(task); + expect(tool.stream).toBe(stream); + }); + + it('should allow fields to be read independently after construction', () => { + const task = new Promise((resolve) => setTimeout(resolve, 100)); + const stream = new LiveRequestQueue(); + const tool = new ActiveStreamingTool({task, stream}); + + const readTask = tool.task; + const readStream = tool.stream; + expect(readTask).toBe(task); + expect(readStream).toBe(stream); + expect(readStream).toBeInstanceOf(LiveRequestQueue); + }); +}); diff --git a/core/test/agents/readonly_context_test.ts b/core/test/agents/readonly_context_test.ts new file mode 100644 index 00000000..7a7cfc5c --- /dev/null +++ b/core/test/agents/readonly_context_test.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + InvocationContext, + LlmAgent, + PluginManager, + ReadonlyContext, + Session, +} from '@google/adk'; +import {Content} from '@google/genai'; +import {describe, expect, it} from 'vitest'; + +function makeSession(overrides: Partial = {}): Session { + return { + id: 'session-1', + appName: 'test-app', + userId: 'user-1', + state: {}, + events: [], + lastUpdateTime: Date.now(), + ...overrides, + } as Session; +} + +function makeInvocationContext( + overrides: Partial<{ + invocationId: string; + userContent: Content; + session: Session; + agentName: string; + }> = {}, +): InvocationContext { + const agent = new LlmAgent({name: overrides.agentName ?? 'test-agent'}); + const session = overrides.session ?? makeSession(); + + return new InvocationContext({ + invocationId: overrides.invocationId ?? 'inv-123', + agent, + session, + userContent: overrides.userContent, + pluginManager: new PluginManager(), + }); +} + +describe('ReadonlyContext', () => { + it('should construct without errors given a minimal InvocationContext', () => { + const ic = makeInvocationContext(); + const ctx = new ReadonlyContext(ic); + expect(ctx).toBeInstanceOf(ReadonlyContext); + }); + + it('invocationId delegates to the underlying InvocationContext', () => { + const ic = makeInvocationContext({invocationId: 'my-inv-id'}); + const ctx = new ReadonlyContext(ic); + expect(ctx.invocationId).toBe('my-inv-id'); + }); + + it('userId delegates to the underlying session userId', () => { + const session = makeSession({userId: 'alice'}); + const ic = makeInvocationContext({session}); + const ctx = new ReadonlyContext(ic); + expect(ctx.userId).toBe('alice'); + }); + + it('sessionId delegates to the underlying session id', () => { + const session = makeSession({id: 'sess-42'}); + const ic = makeInvocationContext({session}); + const ctx = new ReadonlyContext(ic); + expect(ctx.sessionId).toBe('sess-42'); + }); + + it('agentName delegates to the underlying agent name', () => { + const ic = makeInvocationContext({agentName: 'my-agent'}); + const ctx = new ReadonlyContext(ic); + expect(ctx.agentName).toBe('my-agent'); + }); + + it('userContent delegates to the underlying InvocationContext userContent', () => { + const content: Content = { + role: 'user', + parts: [{text: 'hello'}], + }; + const ic = makeInvocationContext({userContent: content}); + const ctx = new ReadonlyContext(ic); + expect(ctx.userContent).toEqual(content); + }); + + it('userContent is undefined when not set on InvocationContext', () => { + const ic = makeInvocationContext(); + const ctx = new ReadonlyContext(ic); + expect(ctx.userContent).toBeUndefined(); + }); + + it('state returns a State wrapping the session state values', () => { + const session = makeSession({state: {foo: 'bar', count: 42}}); + const ic = makeInvocationContext({session}); + const ctx = new ReadonlyContext(ic); + const state = ctx.state; + expect(state.get('foo')).toBe('bar'); + expect(state.get('count')).toBe(42); + }); + + it('state returns a new State instance on each access', () => { + const session = makeSession({state: {x: 1}}); + const ic = makeInvocationContext({session}); + const ctx = new ReadonlyContext(ic); + const state1 = ctx.state; + const state2 = ctx.state; + // Each call returns a new wrapper around the same underlying data + expect(state1).not.toBe(state2); + expect(state1.get('x')).toBe(state2.get('x')); + }); +});