Download packages/core/src/agents/agentLoader.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 34.6 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/agents/agentLoader.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/agents/agentLoader.test.ts
-
curl -L -o agentLoader.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/agents/agentLoader.test.ts
34.6 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { describe, it, expect, beforeEach, afterEach } from 'vitest'; | |
| import * as fs from 'node:fs/promises'; | |
| import * as path from 'node:path'; | |
| import * as os from 'node:os'; | |
| import { | |
| parseAgentMarkdown, | |
| markdownToAgentDefinition, | |
| loadAgentsFromDirectory, | |
| AgentLoadError, | |
| } from './agentLoader.js'; | |
| import { GEMINI_MODEL_ALIAS_PRO } from '../config/models.js'; | |
| import { | |
| DEFAULT_MAX_TIME_MINUTES, | |
| DEFAULT_MAX_TURNS, | |
| type LocalAgentDefinition, | |
| type RemoteAgentDefinition, | |
| getAgentCardLoadOptions, | |
| getRemoteAgentTargetUrl, | |
| } from './types.js'; | |
| describe('loader', () => { | |
| let tempDir: string; | |
| beforeEach(async () => { | |
| tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-test-')); | |
| }); | |
| afterEach(async () => { | |
| if (tempDir) { | |
| await fs.rm(tempDir, { recursive: true, force: true }); | |
| } | |
| }); | |
| async function writeAgentMarkdown(content: string, fileName = 'test.md') { | |
| const filePath = path.join(tempDir, fileName); | |
| await fs.writeFile(filePath, content); | |
| return filePath; | |
| } | |
| describe('parseAgentMarkdown', () => { | |
| it('should parse a valid markdown agent file', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| name: test-agent-md | |
| description: A markdown agent | |
| --- | |
| You are a markdown agent.`); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| name: 'test-agent-md', | |
| description: 'A markdown agent', | |
| kind: 'local', | |
| system_prompt: 'You are a markdown agent.', | |
| }); | |
| }); | |
| it('should parse frontmatter with tools and model config', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| name: complex-agent | |
| description: A complex markdown agent | |
| tools: | |
| - run_shell_command | |
| model: gemini-pro | |
| temperature: 0.7 | |
| --- | |
| System prompt content.`); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| name: 'complex-agent', | |
| description: 'A complex markdown agent', | |
| tools: ['run_shell_command'], | |
| model: 'gemini-pro', | |
| temperature: 0.7, | |
| system_prompt: 'System prompt content.', | |
| }); | |
| }); | |
| it('should parse frontmatter with mcp_servers', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| name: mcp-agent | |
| description: An agent with MCP servers | |
| mcp_servers: | |
| test-server: | |
| command: node | |
| args: [server.js] | |
| include_tools: [tool1, tool2] | |
| --- | |
| System prompt content.`); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| name: 'mcp-agent', | |
| description: 'An agent with MCP servers', | |
| mcp_servers: { | |
| 'test-server': { | |
| command: 'node', | |
| args: ['server.js'], | |
| include_tools: ['tool1', 'tool2'], | |
| }, | |
| }, | |
| }); | |
| }); | |
| it('should throw AgentLoadError if frontmatter is missing', async () => { | |
| const filePath = await writeAgentMarkdown(`Just some markdown content.`); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow( | |
| AgentLoadError, | |
| ); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow( | |
| 'Missing mandatory YAML frontmatter', | |
| ); | |
| }); | |
| it('should throw AgentLoadError if frontmatter is invalid YAML', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| name: [invalid yaml | |
| --- | |
| Body`); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow( | |
| AgentLoadError, | |
| ); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow( | |
| 'YAML frontmatter parsing failed', | |
| ); | |
| }); | |
| it('should throw AgentLoadError if validation fails (missing required field)', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| name: test-agent | |
| # missing description | |
| --- | |
| Body`); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow( | |
| /Validation failed/, | |
| ); | |
| }); | |
| it('should parse a valid remote agent markdown file', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: remote-agent | |
| description: A remote agent | |
| agent_card_url: https://example.com/card | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toEqual({ | |
| kind: 'remote', | |
| name: 'remote-agent', | |
| description: 'A remote agent', | |
| agent_card_url: 'https://example.com/card', | |
| }); | |
| }); | |
| it('should infer remote agent kind from agent_card_url', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| name: inferred-remote | |
| description: Inferred | |
| agent_card_url: https://example.com/inferred | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toEqual({ | |
| kind: 'remote', | |
| name: 'inferred-remote', | |
| description: 'Inferred', | |
| agent_card_url: 'https://example.com/inferred', | |
| }); | |
| }); | |
| it('should parse a remote agent with no body', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: no-body-remote | |
| agent_card_url: https://example.com/card | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toEqual({ | |
| kind: 'remote', | |
| name: 'no-body-remote', | |
| agent_card_url: 'https://example.com/card', | |
| }); | |
| }); | |
| it('should parse multiple remote agents in a list', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| - kind: remote | |
| name: remote-1 | |
| agent_card_url: https://example.com/1 | |
| - kind: remote | |
| name: remote-2 | |
| agent_card_url: https://example.com/2 | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(2); | |
| expect(result[0]).toEqual({ | |
| kind: 'remote', | |
| name: 'remote-1', | |
| agent_card_url: 'https://example.com/1', | |
| }); | |
| expect(result[1]).toEqual({ | |
| kind: 'remote', | |
| name: 'remote-2', | |
| agent_card_url: 'https://example.com/2', | |
| }); | |
| }); | |
| it('should parse frontmatter without a trailing newline', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: no-trailing-newline | |
| agent_card_url: https://example.com/card | |
| ---`); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toEqual({ | |
| kind: 'remote', | |
| name: 'no-trailing-newline', | |
| agent_card_url: 'https://example.com/card', | |
| }); | |
| }); | |
| it('should parse a remote agent with agent_card_json', async () => { | |
| const cardJson = JSON.stringify({ | |
| name: 'json-agent', | |
| url: 'https://example.com/agent', | |
| version: '1.0', | |
| }); | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: json-remote | |
| description: A JSON-based remote agent | |
| agent_card_json: '${cardJson}' | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| kind: 'remote', | |
| name: 'json-remote', | |
| description: 'A JSON-based remote agent', | |
| agent_card_json: cardJson, | |
| }); | |
| // Should NOT have agent_card_url | |
| expect(result[0]).not.toHaveProperty('agent_card_url'); | |
| }); | |
| it('should reject agent_card_json that is not valid JSON', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: invalid-json-remote | |
| agent_card_json: "not valid json {{" | |
| --- | |
| `); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow( | |
| /agent_card_json must be valid JSON/, | |
| ); | |
| }); | |
| it('should reject a remote agent with both agent_card_url and agent_card_json', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: both-fields | |
| agent_card_url: https://example.com/card | |
| agent_card_json: '{"name":"test"}' | |
| --- | |
| `); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow( | |
| /Validation failed/, | |
| ); | |
| }); | |
| it('should infer remote kind from agent_card_json', async () => { | |
| const cardJson = JSON.stringify({ | |
| name: 'test', | |
| url: 'https://example.com', | |
| }); | |
| const filePath = await writeAgentMarkdown(`--- | |
| name: inferred-json-remote | |
| agent_card_json: '${cardJson}' | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| kind: 'remote', | |
| name: 'inferred-json-remote', | |
| agent_card_json: cardJson, | |
| }); | |
| }); | |
| it('should throw AgentLoadError if agent name is not a valid slug', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| name: Invalid Name With Spaces | |
| description: Test | |
| --- | |
| Body`); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow( | |
| /Name must be a valid slug/, | |
| ); | |
| }); | |
| describe('error formatting and kind inference', () => { | |
| it('should only show local agent errors when kind is inferred as local (via kind field)', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: local | |
| name: invalid-local | |
| # missing description | |
| --- | |
| Body`); | |
| const error = await parseAgentMarkdown(filePath).catch((e) => e); | |
| expect(error).toBeInstanceOf(AgentLoadError); | |
| expect(error.message).toContain('Validation failed'); | |
| expect(error.message).toContain('description: Required'); | |
| expect(error.message).not.toContain('Remote Agent'); | |
| }); | |
| it('should only show local agent errors when kind is inferred as local (via local-specific keys)', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| name: invalid-local | |
| # missing description | |
| tools: | |
| - run_shell_command | |
| --- | |
| Body`); | |
| const error = await parseAgentMarkdown(filePath).catch((e) => e); | |
| expect(error).toBeInstanceOf(AgentLoadError); | |
| expect(error.message).toContain('Validation failed'); | |
| expect(error.message).toContain('description: Required'); | |
| expect(error.message).not.toContain('Remote Agent'); | |
| }); | |
| it('should only show remote agent errors when kind is inferred as remote (via kind field)', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: invalid-remote | |
| # missing agent_card_url | |
| --- | |
| Body`); | |
| const error = await parseAgentMarkdown(filePath).catch((e) => e); | |
| expect(error).toBeInstanceOf(AgentLoadError); | |
| expect(error.message).toContain('Validation failed'); | |
| expect(error.message).toContain('agent_card_url: Required'); | |
| expect(error.message).not.toContain('Local Agent'); | |
| }); | |
| it('should only show remote agent errors when kind is inferred as remote (via remote-specific keys)', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| name: invalid-remote | |
| auth: | |
| type: apiKey | |
| key: my_key | |
| # missing agent_card_url | |
| --- | |
| Body`); | |
| const error = await parseAgentMarkdown(filePath).catch((e) => e); | |
| expect(error).toBeInstanceOf(AgentLoadError); | |
| expect(error.message).toContain('Validation failed'); | |
| expect(error.message).toContain('agent_card_url: Required'); | |
| expect(error.message).not.toContain('Local Agent'); | |
| }); | |
| it('should show errors for both types when kind cannot be inferred', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| name: invalid-unknown | |
| # missing description and missing agent_card_url, no specific keys | |
| --- | |
| Body`); | |
| const error = await parseAgentMarkdown(filePath).catch((e) => e); | |
| expect(error).toBeInstanceOf(AgentLoadError); | |
| expect(error.message).toContain('Validation failed'); | |
| expect(error.message).toContain('(Local Agent)'); | |
| expect(error.message).toContain('(Remote Agent)'); | |
| expect(error.message).toContain('description: Required'); | |
| expect(error.message).toContain('agent_card_url: Required'); | |
| }); | |
| it('should format errors without a stray colon when the path is empty (e.g. strict object with unknown keys)', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: local | |
| name: my-agent | |
| description: test | |
| unknown_field: true | |
| --- | |
| Body`); | |
| const error = await parseAgentMarkdown(filePath).catch((e) => e); | |
| expect(error).toBeInstanceOf(AgentLoadError); | |
| expect(error.message).toContain( | |
| "Unrecognized key(s) in object: 'unknown_field'", | |
| ); | |
| expect(error.message).not.toContain(': Unrecognized key(s)'); | |
| expect(error.message).not.toContain('Required'); | |
| }); | |
| }); | |
| }); | |
| describe('markdownToAgentDefinition', () => { | |
| it('should convert valid Markdown DTO to AgentDefinition with defaults', () => { | |
| const markdown = { | |
| kind: 'local' as const, | |
| name: 'test-agent', | |
| description: 'A test agent', | |
| system_prompt: 'You are a test agent.', | |
| }; | |
| const result = markdownToAgentDefinition(markdown); | |
| expect(result).toMatchObject({ | |
| name: 'test-agent', | |
| description: 'A test agent', | |
| promptConfig: { | |
| systemPrompt: 'You are a test agent.', | |
| query: '${query}', | |
| }, | |
| modelConfig: { | |
| model: 'inherit', | |
| generateContentConfig: { | |
| topP: 0.95, | |
| }, | |
| }, | |
| runConfig: { | |
| maxTimeMinutes: DEFAULT_MAX_TIME_MINUTES, | |
| maxTurns: DEFAULT_MAX_TURNS, | |
| }, | |
| inputConfig: { | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| query: { | |
| type: 'string', | |
| description: 'The task for the agent.', | |
| }, | |
| }, | |
| required: [], | |
| }, | |
| }, | |
| }); | |
| }); | |
| it('should pass through model aliases', () => { | |
| const markdown = { | |
| kind: 'local' as const, | |
| name: 'test-agent', | |
| description: 'A test agent', | |
| model: GEMINI_MODEL_ALIAS_PRO, | |
| system_prompt: 'You are a test agent.', | |
| }; | |
| const result = markdownToAgentDefinition( | |
| markdown, | |
| ) as LocalAgentDefinition; | |
| expect(result.modelConfig.model).toBe(GEMINI_MODEL_ALIAS_PRO); | |
| }); | |
| it('should convert mcp_servers in local agent', () => { | |
| const markdown = { | |
| kind: 'local' as const, | |
| name: 'mcp-agent', | |
| description: 'An agent with MCP servers', | |
| mcp_servers: { | |
| 'test-server': { | |
| command: 'node', | |
| args: ['server.js'], | |
| include_tools: ['tool1'], | |
| }, | |
| }, | |
| system_prompt: 'prompt', | |
| }; | |
| const result = markdownToAgentDefinition( | |
| markdown, | |
| ) as LocalAgentDefinition; | |
| expect(result.kind).toBe('local'); | |
| expect(result.mcpServers).toBeDefined(); | |
| expect(result.mcpServers!['test-server']).toMatchObject({ | |
| command: 'node', | |
| args: ['server.js'], | |
| includeTools: ['tool1'], | |
| }); | |
| }); | |
| it('should convert mcp_servers with auth block in local agent (google-credentials)', () => { | |
| const markdown = { | |
| kind: 'local' as const, | |
| name: 'spanner-test-agent', | |
| description: 'An agent to test Spanner MCP with auth', | |
| mcp_servers: { | |
| spanner: { | |
| url: 'https://spanner.googleapis.com/mcp', | |
| type: 'http' as const, | |
| auth: { | |
| type: 'google-credentials' as const, | |
| scopes: ['https://www.googleapis.com/auth/cloud-platform'], | |
| }, | |
| timeout: 30000, | |
| }, | |
| }, | |
| system_prompt: 'You are a Spanner test agent.', | |
| }; | |
| const result = markdownToAgentDefinition( | |
| markdown, | |
| ) as LocalAgentDefinition; | |
| expect(result.kind).toBe('local'); | |
| expect(result.mcpServers).toBeDefined(); | |
| expect(result.mcpServers!['spanner']).toMatchObject({ | |
| url: 'https://spanner.googleapis.com/mcp', | |
| type: 'http', | |
| authProviderType: 'google_credentials', | |
| oauth: { | |
| enabled: true, | |
| scopes: ['https://www.googleapis.com/auth/cloud-platform'], | |
| }, | |
| timeout: 30000, | |
| }); | |
| }); | |
| it('should convert mcp_servers with auth block in local agent (oauth with full fields)', () => { | |
| const markdown = { | |
| kind: 'local' as const, | |
| name: 'oauth-test-agent', | |
| description: 'An agent to test OAuth MCP with full fields', | |
| mcp_servers: { | |
| 'test-server': { | |
| url: 'https://api.example.com/mcp', | |
| type: 'http' as const, | |
| auth: { | |
| type: 'oauth' as const, | |
| client_id: 'my-client-id', | |
| client_secret: 'my-client-secret', | |
| scopes: ['read', 'write'], | |
| authorization_url: 'https://auth.example.com/authorize', | |
| token_url: 'https://auth.example.com/token', | |
| issuer: 'https://auth.example.com', | |
| audiences: ['audience1'], | |
| redirect_uri: 'http://localhost:8080/callback', | |
| token_param_name: 'access_token', | |
| registration_url: 'https://auth.example.com/register', | |
| }, | |
| timeout: 30000, | |
| }, | |
| }, | |
| system_prompt: 'You are a test agent.', | |
| }; | |
| const result = markdownToAgentDefinition( | |
| markdown, | |
| ) as LocalAgentDefinition; | |
| expect(result.kind).toBe('local'); | |
| expect(result.mcpServers).toBeDefined(); | |
| expect(result.mcpServers!['test-server']).toMatchObject({ | |
| url: 'https://api.example.com/mcp', | |
| type: 'http', | |
| oauth: { | |
| enabled: true, | |
| clientId: 'my-client-id', | |
| clientSecret: 'my-client-secret', | |
| scopes: ['read', 'write'], | |
| authorizationUrl: 'https://auth.example.com/authorize', | |
| tokenUrl: 'https://auth.example.com/token', | |
| issuer: 'https://auth.example.com', | |
| audiences: ['audience1'], | |
| redirectUri: 'http://localhost:8080/callback', | |
| tokenParamName: 'access_token', | |
| registrationUrl: 'https://auth.example.com/register', | |
| }, | |
| timeout: 30000, | |
| }); | |
| }); | |
| it('should pass through unknown model names (e.g. auto)', () => { | |
| const markdown = { | |
| kind: 'local' as const, | |
| name: 'test-agent', | |
| description: 'A test agent', | |
| model: 'auto', | |
| system_prompt: 'You are a test agent.', | |
| }; | |
| const result = markdownToAgentDefinition( | |
| markdown, | |
| ) as LocalAgentDefinition; | |
| expect(result.modelConfig.model).toBe('auto'); | |
| }); | |
| it('should convert remote agent definition', () => { | |
| const markdown = { | |
| kind: 'remote' as const, | |
| name: 'remote-agent', | |
| description: 'A remote agent', | |
| agent_card_url: 'https://example.com/card', | |
| }; | |
| const result = markdownToAgentDefinition(markdown); | |
| expect(result).toEqual({ | |
| kind: 'remote', | |
| name: 'remote-agent', | |
| description: 'A remote agent', | |
| displayName: undefined, | |
| agentCardUrl: 'https://example.com/card', | |
| inputConfig: { | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| query: { | |
| type: 'string', | |
| description: 'The task for the agent.', | |
| }, | |
| }, | |
| required: [], | |
| }, | |
| }, | |
| }); | |
| }); | |
| it('should convert remote agent definition with agent_card_json', () => { | |
| const cardJson = JSON.stringify({ | |
| name: 'json-agent', | |
| url: 'https://example.com/agent', | |
| }); | |
| const markdown = { | |
| kind: 'remote' as const, | |
| name: 'json-remote', | |
| description: 'A JSON remote agent', | |
| agent_card_json: cardJson, | |
| }; | |
| const result = markdownToAgentDefinition( | |
| markdown, | |
| ) as RemoteAgentDefinition; | |
| expect(result.kind).toBe('remote'); | |
| expect(result.name).toBe('json-remote'); | |
| expect(result.agentCardJson).toBe(cardJson); | |
| expect(result.agentCardUrl).toBeUndefined(); | |
| }); | |
| it('should throw for remote agent with neither agent_card_url nor agent_card_json', () => { | |
| // Cast to bypass compile-time check — this tests the runtime guard | |
| const markdown = { | |
| kind: 'remote' as const, | |
| name: 'no-card-agent', | |
| description: 'Missing card info', | |
| } as Parameters<typeof markdownToAgentDefinition>[0]; | |
| expect(() => markdownToAgentDefinition(markdown)).toThrow( | |
| /neither agent_card_json nor agent_card_url/, | |
| ); | |
| }); | |
| }); | |
| describe('loadAgentsFromDirectory', () => { | |
| it('should load definitions from a directory (Markdown only)', async () => { | |
| await writeAgentMarkdown( | |
| `--- | |
| name: agent-1 | |
| description: Agent 1 | |
| --- | |
| Prompt 1`, | |
| 'valid.md', | |
| ); | |
| // Create a non-supported file | |
| await fs.writeFile(path.join(tempDir, 'other.txt'), 'content'); | |
| // Create a hidden file | |
| await writeAgentMarkdown( | |
| `--- | |
| name: hidden | |
| description: Hidden | |
| --- | |
| Hidden`, | |
| '_hidden.md', | |
| ); | |
| const result = await loadAgentsFromDirectory(tempDir); | |
| expect(result.agents).toHaveLength(1); | |
| expect(result.agents[0].name).toBe('agent-1'); | |
| expect(result.errors).toHaveLength(0); | |
| }); | |
| it('should return empty result if directory does not exist', async () => { | |
| const nonExistentDir = path.join(tempDir, 'does-not-exist'); | |
| const result = await loadAgentsFromDirectory(nonExistentDir); | |
| expect(result.agents).toHaveLength(0); | |
| expect(result.errors).toHaveLength(0); | |
| }); | |
| it('should capture errors for malformed individual files', async () => { | |
| // Create a malformed Markdown file | |
| await writeAgentMarkdown('invalid markdown', 'malformed.md'); | |
| const result = await loadAgentsFromDirectory(tempDir); | |
| expect(result.agents).toHaveLength(0); | |
| expect(result.errors).toHaveLength(1); | |
| }); | |
| }); | |
| describe('remote agent auth configuration', () => { | |
| it('should parse remote agent with apiKey auth', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: api-key-agent | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: apiKey | |
| key: $MY_API_KEY | |
| name: X-Custom-Key | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| kind: 'remote', | |
| name: 'api-key-agent', | |
| auth: { | |
| type: 'apiKey', | |
| key: '$MY_API_KEY', | |
| name: 'X-Custom-Key', | |
| }, | |
| }); | |
| }); | |
| it('should parse remote agent with http Bearer auth', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: bearer-agent | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: http | |
| scheme: Bearer | |
| token: $BEARER_TOKEN | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| kind: 'remote', | |
| name: 'bearer-agent', | |
| auth: { | |
| type: 'http', | |
| scheme: 'Bearer', | |
| token: '$BEARER_TOKEN', | |
| }, | |
| }); | |
| }); | |
| it('should parse remote agent with http Basic auth', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: basic-agent | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: http | |
| scheme: Basic | |
| username: $AUTH_USER | |
| password: $AUTH_PASS | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| kind: 'remote', | |
| name: 'basic-agent', | |
| auth: { | |
| type: 'http', | |
| scheme: 'Basic', | |
| username: '$AUTH_USER', | |
| password: '$AUTH_PASS', | |
| }, | |
| }); | |
| }); | |
| it('should parse remote agent with Digest via raw value', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: digest-agent | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: http | |
| scheme: Digest | |
| value: username="admin", response="abc123" | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| kind: 'remote', | |
| name: 'digest-agent', | |
| auth: { | |
| type: 'http', | |
| scheme: 'Digest', | |
| value: 'username="admin", response="abc123"', | |
| }, | |
| }); | |
| }); | |
| it('should parse remote agent with generic raw auth value', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: raw-agent | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: http | |
| scheme: CustomScheme | |
| value: raw-token-value | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| kind: 'remote', | |
| name: 'raw-agent', | |
| auth: { | |
| type: 'http', | |
| scheme: 'CustomScheme', | |
| value: 'raw-token-value', | |
| }, | |
| }); | |
| }); | |
| it('should throw error for Bearer auth without token', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: invalid-bearer | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: http | |
| scheme: Bearer | |
| --- | |
| `); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow( | |
| /Bearer scheme requires "token"/, | |
| ); | |
| }); | |
| it('should throw error for Basic auth without credentials', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: invalid-basic | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: http | |
| scheme: Basic | |
| username: user | |
| --- | |
| `); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow( | |
| /Basic authentication requires "password"/, | |
| ); | |
| }); | |
| it('should throw error for apiKey auth without key', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: invalid-apikey | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: apiKey | |
| --- | |
| `); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow( | |
| /auth\.key.*Required/, | |
| ); | |
| }); | |
| it('should convert auth config in markdownToAgentDefinition', () => { | |
| const markdown = { | |
| kind: 'remote' as const, | |
| name: 'auth-agent', | |
| agent_card_url: 'https://example.com/card', | |
| auth: { | |
| type: 'apiKey' as const, | |
| key: '$API_KEY', | |
| }, | |
| }; | |
| const result = markdownToAgentDefinition(markdown); | |
| expect(result).toMatchObject({ | |
| kind: 'remote', | |
| name: 'auth-agent', | |
| auth: { | |
| type: 'apiKey', | |
| key: '$API_KEY', | |
| }, | |
| }); | |
| }); | |
| it('should parse remote agent with oauth2 auth', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: oauth2-agent | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: oauth | |
| client_id: $MY_OAUTH_CLIENT_ID | |
| scopes: | |
| - read | |
| - write | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| kind: 'remote', | |
| name: 'oauth2-agent', | |
| auth: { | |
| type: 'oauth', | |
| client_id: '$MY_OAUTH_CLIENT_ID', | |
| scopes: ['read', 'write'], | |
| }, | |
| }); | |
| }); | |
| it('should parse remote agent with oauth2 auth including all fields', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: oauth2-full-agent | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: oauth | |
| client_id: my-client-id | |
| client_secret: my-client-secret | |
| scopes: | |
| - openid | |
| - profile | |
| authorization_url: https://auth.example.com/authorize | |
| token_url: https://auth.example.com/token | |
| issuer: https://auth.example.com | |
| audiences: | |
| - audience1 | |
| redirect_uri: http://localhost:8080/callback | |
| token_param_name: access_token | |
| registration_url: https://auth.example.com/register | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| kind: 'remote', | |
| name: 'oauth2-full-agent', | |
| auth: { | |
| type: 'oauth', | |
| client_id: 'my-client-id', | |
| client_secret: 'my-client-secret', | |
| scopes: ['openid', 'profile'], | |
| authorization_url: 'https://auth.example.com/authorize', | |
| token_url: 'https://auth.example.com/token', | |
| issuer: 'https://auth.example.com', | |
| audiences: ['audience1'], | |
| redirect_uri: 'http://localhost:8080/callback', | |
| token_param_name: 'access_token', | |
| registration_url: 'https://auth.example.com/register', | |
| }, | |
| }); | |
| }); | |
| it('should parse remote agent with minimal oauth2 config (type only)', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: oauth2-minimal-agent | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: oauth | |
| --- | |
| `); | |
| const result = await parseAgentMarkdown(filePath); | |
| expect(result).toHaveLength(1); | |
| expect(result[0]).toMatchObject({ | |
| kind: 'remote', | |
| name: 'oauth2-minimal-agent', | |
| auth: { | |
| type: 'oauth', | |
| }, | |
| }); | |
| }); | |
| it('should reject oauth2 auth with invalid authorization_url', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: invalid-oauth2-agent | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: oauth | |
| client_id: my-client | |
| authorization_url: not-a-valid-url | |
| --- | |
| `); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow(/Invalid url/); | |
| }); | |
| it('should reject oauth2 auth with invalid token_url', async () => { | |
| const filePath = await writeAgentMarkdown(`--- | |
| kind: remote | |
| name: invalid-oauth2-agent | |
| agent_card_url: https://example.com/card | |
| auth: | |
| type: oauth | |
| client_id: my-client | |
| token_url: not-a-valid-url | |
| --- | |
| `); | |
| await expect(parseAgentMarkdown(filePath)).rejects.toThrow(/Invalid url/); | |
| }); | |
| it('should convert oauth2 auth config in markdownToAgentDefinition', () => { | |
| const markdown = { | |
| kind: 'remote' as const, | |
| name: 'oauth2-convert-agent', | |
| agent_card_url: 'https://example.com/card', | |
| auth: { | |
| type: 'oauth' as const, | |
| client_id: '$MY_CLIENT_ID', | |
| scopes: ['read'], | |
| authorization_url: 'https://auth.example.com/authorize', | |
| token_url: 'https://auth.example.com/token', | |
| }, | |
| }; | |
| const result = markdownToAgentDefinition(markdown); | |
| expect(result).toMatchObject({ | |
| kind: 'remote', | |
| name: 'oauth2-convert-agent', | |
| auth: { | |
| type: 'oauth2', | |
| client_id: '$MY_CLIENT_ID', | |
| scopes: ['read'], | |
| authorization_url: 'https://auth.example.com/authorize', | |
| token_url: 'https://auth.example.com/token', | |
| }, | |
| }); | |
| }); | |
| it('should throw an error for an unknown auth type in markdownToAgentDefinition', () => { | |
| const markdown = { | |
| kind: 'remote' as const, | |
| name: 'unknown-auth-agent', | |
| agent_card_url: 'https://example.com/card', | |
| auth: { | |
| type: 'apiKey' as const, | |
| key: 'some-key', | |
| }, | |
| }; | |
| // Mutate the object at runtime to bypass TypeScript compile-time checks cleanly | |
| Object.assign(markdown.auth, { type: 'some-unknown-type' }); | |
| expect(() => markdownToAgentDefinition(markdown)).toThrow( | |
| /Unknown auth type: some-unknown-type/, | |
| ); | |
| }); | |
| }); | |
| describe('getAgentCardLoadOptions', () => { | |
| it('should return json options when agentCardJson is present', () => { | |
| const def = { | |
| name: 'test', | |
| agentCardJson: '{"url":"http://x"}', | |
| } as RemoteAgentDefinition; | |
| const opts = getAgentCardLoadOptions(def); | |
| expect(opts).toEqual({ type: 'json', json: '{"url":"http://x"}' }); | |
| }); | |
| it('should return url options when agentCardUrl is present', () => { | |
| const def = { | |
| name: 'test', | |
| agentCardUrl: 'http://x/card', | |
| } as RemoteAgentDefinition; | |
| const opts = getAgentCardLoadOptions(def); | |
| expect(opts).toEqual({ type: 'url', url: 'http://x/card' }); | |
| }); | |
| it('should prefer agentCardJson over agentCardUrl when both present', () => { | |
| const def = { | |
| name: 'test', | |
| agentCardJson: '{"url":"http://x"}', | |
| agentCardUrl: 'http://x/card', | |
| } as RemoteAgentDefinition; | |
| const opts = getAgentCardLoadOptions(def); | |
| expect(opts.type).toBe('json'); | |
| }); | |
| it('should throw when neither is present', () => { | |
| const def = { name: 'orphan' } as RemoteAgentDefinition; | |
| expect(() => getAgentCardLoadOptions(def)).toThrow( | |
| /Remote agent 'orphan' has neither agentCardUrl nor agentCardJson/, | |
| ); | |
| }); | |
| }); | |
| describe('getRemoteAgentTargetUrl', () => { | |
| it('should return agentCardUrl when present', () => { | |
| const def = { | |
| name: 'test', | |
| agentCardUrl: 'http://x/card', | |
| } as RemoteAgentDefinition; | |
| expect(getRemoteAgentTargetUrl(def)).toBe('http://x/card'); | |
| }); | |
| it('should extract url from agentCardJson when agentCardUrl is absent', () => { | |
| const def = { | |
| name: 'test', | |
| agentCardJson: JSON.stringify({ | |
| name: 'agent', | |
| url: 'https://example.com/agent', | |
| }), | |
| } as RemoteAgentDefinition; | |
| expect(getRemoteAgentTargetUrl(def)).toBe('https://example.com/agent'); | |
| }); | |
| it('should return undefined when JSON has no url field', () => { | |
| const def = { | |
| name: 'test', | |
| agentCardJson: JSON.stringify({ name: 'agent' }), | |
| } as RemoteAgentDefinition; | |
| expect(getRemoteAgentTargetUrl(def)).toBeUndefined(); | |
| }); | |
| it('should return undefined when agentCardJson is invalid JSON', () => { | |
| const def = { | |
| name: 'test', | |
| agentCardJson: 'not json', | |
| } as RemoteAgentDefinition; | |
| expect(getRemoteAgentTargetUrl(def)).toBeUndefined(); | |
| }); | |
| it('should return undefined when neither field is present', () => { | |
| const def = { name: 'test' } as RemoteAgentDefinition; | |
| expect(getRemoteAgentTargetUrl(def)).toBeUndefined(); | |
| }); | |
| }); | |
| }); | |