openclaw/test/unit/agent.test.ts
Shunsuke Hayashi 4d74fdf593 feat(aws): add ECS Fargate deployment for Clawdbot Discord bot
Add complete ECS Fargate infrastructure for Clawdbot Discord bot
deployment on AWS.

## Infrastructure (Terraform)
- **VPC**: 100.64.0.0/16 with DNS support
- **Public Subnets**: 2 subnets in different AZs
- **ECR Repository**: clawdbot/bot with lifecycle policy
- **IAM Roles**: Task Role (DynamoDB+S3) + Execution Role
- **ECS Resources**: Fargate (256 CPU, 2048 MB memory)
- **Security Group**: Outbound only
- **CloudWatch**: Log group with 7-day retention

## Security Improvements
- Discord Bot Token → AWS Secrets Manager
- IAM policy with least privilege principle
- No plaintext secrets in code

## Code Quality
- Fix all 22 lint errors
- Add *.d.ts to oxlint ignore patterns
- Fix Dockerfile .buildstamp for Linux compatibility

Closes #2049

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 13:38:43 +09:00

665 lines
18 KiB
TypeScript

/**
* Agent Unit Tests
*
* Tests Clawdbot agent functionality:
* - Agent configuration resolution
* - Agent ID normalization and routing
* - Agent workspace and directory resolution
* - Agent model configuration
* - Session-based agent selection
*/
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdirSync, rmSync, existsSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import { randomBytes } from "crypto";
// Import the functions to test
import {
listAgentIds,
resolveDefaultAgentId,
resolveSessionAgentId,
resolveSessionAgentIds,
resolveAgentConfig,
resolveAgentModelPrimary,
resolveAgentModelFallbacksOverride,
resolveAgentWorkspaceDir,
resolveAgentDir,
} from "../../src/agents/agent-scope.js";
import type { ClawdbotConfig } from "../../src/config/config.js";
// Test utilities
function createTempDir(): string {
const tempDir = join(tmpdir(), `clawdbot-test-${randomBytes(8).toString("hex")}`);
mkdirSync(tempDir, { recursive: true });
return tempDir;
}
function cleanupTempDir(tempDir: string): void {
if (existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true });
}
}
// Test data
function createMockConfig(overrides: Partial<ClawdbotConfig> = {}): ClawdbotConfig {
return {
meta: {
lastTouchedVersion: "2026.1.24-0",
lastTouchedAt: new Date().toISOString(),
},
agents: {
defaults: {
model: {
primary: "test-model",
},
},
list: overrides.agents?.list || [],
},
...overrides,
} as ClawdbotConfig;
}
describe("Agent Unit Tests", () => {
describe("listAgentIds", () => {
it("should return default agent ID when no agents configured", () => {
const config = createMockConfig();
const ids = listAgentIds(config);
expect(ids).toEqual(["main"]);
});
it("should return all configured agent IDs", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [
{ id: "agent-1", name: "Agent 1" },
{ id: "agent-2", name: "Agent 2" },
{ id: "agent-1", name: "Duplicate" }, // Duplicate should be ignored
],
},
});
const ids = listAgentIds(config);
expect(ids).toHaveLength(2);
expect(ids).toContain("agent-1");
expect(ids).toContain("agent-2");
});
it("should normalize agent IDs (lowercase, trim)", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: " Test-Agent " }, { id: "ANOTHER-AGENT" }],
},
});
const ids = listAgentIds(config);
expect(ids).toContain("test-agent");
expect(ids).toContain("another-agent");
});
});
describe("importDefaultAgentId", () => {
it("should return main when no agents configured", () => {
const config = createMockConfig();
const agentId = resolveDefaultAgentId(config);
expect(agentId).toBe("main");
});
it("should return agent marked as default", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [
{ id: "agent-1", name: "Agent 1", default: false },
{ id: "agent-2", name: "Agent 2", default: true },
{ id: "agent-3", name: "Agent 3", default: false },
],
},
});
const agentId = resolveDefaultAgentId(config);
expect(agentId).toBe("agent-2");
});
it("should return first agent when none marked as default", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [
{ id: "agent-1", name: "Agent 1" },
{ id: "agent-2", name: "Agent 2" },
],
},
});
const agentId = resolveDefaultAgentId(config);
expect(agentId).toBe("agent-1");
});
it("should handle empty ID gracefully", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: " ", name: "Empty ID" }],
},
});
const agentId = resolveDefaultAgentId(config);
expect(agentId).toBe("main");
});
});
describe("resolveSessionAgentId", () => {
const mockConfig = createMockConfig({
agents: {
defaults: {},
list: [
{ id: "main", name: "Main Agent", default: true },
{ id: "specialist", name: "Specialist" },
],
},
});
it("should return default agent when no session key provided", () => {
const agentId = resolveSessionAgentId({ config: mockConfig });
expect(agentId).toBe("main");
});
it("should parse agent ID from session key", () => {
const agentId = resolveSessionAgentId({
config: mockConfig,
sessionKey: "agent:specialist:active",
});
expect(agentId).toBe("specialist");
});
it("should normalize session key and agent ID", () => {
const agentId = resolveSessionAgentId({
config: mockConfig,
sessionKey: " AGENT:SPECIALIST:ACTIVE ",
});
expect(agentId).toBe("specialist");
});
it("should return default agent for invalid session key", () => {
const agentId = resolveSessionAgentId({
config: mockConfig,
sessionKey: "invalid:session:key",
});
expect(agentId).toBe("main");
});
});
describe("resolveSessionAgentIds", () => {
const mockConfig = createMockConfig({
agents: {
defaults: {},
list: [
{ id: "main", name: "Main Agent", default: true },
{ id: "specialist", name: "Specialist" },
],
},
});
it("should return both default and session agent IDs", () => {
const result = resolveSessionAgentIds({
config: mockConfig,
sessionKey: "agent:specialist:active",
});
expect(result.defaultAgentId).toBe("main");
expect(result.sessionAgentId).toBe("specialist");
});
it("should return same agent twice when session matches default", () => {
const result = resolveSessionAgentIds({
config: mockConfig,
sessionKey: "agent:main",
});
expect(result.defaultAgentId).toBe("main");
expect(result.sessionAgentId).toBe("main");
});
});
describe("resolveAgentConfig", () => {
it("should resolve all agent configuration fields", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [
{
id: "test-agent",
name: "Test Agent",
workspace: "/test/workspace",
agentDir: "/test/agent/dir",
model: "test-model",
memorySearch: { provider: "test", endpoint: "http://test" },
humanDelay: { enabled: true },
heartbeat: { interval: 1000 },
identity: { name: "Test", role: "assistant" },
groupChat: { enabled: true },
subagents: { maxConcurrent: 4 },
sandbox: { enabled: false },
tools: ["test-tool"],
},
],
},
});
const resolved = resolveAgentConfig(config, "test-agent");
expect(resolved).toBeDefined();
expect(resolved?.name).toBe("Test Agent");
expect(resolved?.workspace).toBe("/test/workspace");
expect(resolved?.agentDir).toBe("/test/agent/dir");
expect(resolved?.model).toBe("test-model");
expect(resolved?.memorySearch).toEqual({
provider: "test",
endpoint: "http://test",
});
expect(resolved?.humanDelay).toEqual({ enabled: true });
expect(resolved?.heartbeat).toEqual({ interval: 1000 });
expect(resolved?.identity).toEqual({ name: "Test", role: "assistant" });
expect(resolved?.groupChat).toEqual({ enabled: true });
expect(resolved?.subagents).toEqual({ maxConcurrent: 4 });
expect(resolved?.sandbox).toEqual({ enabled: false });
expect(resolved?.tools).toEqual(["test-tool"]);
});
it("should return undefined for unknown agent", () => {
const config = createMockConfig();
const resolved = resolveAgentConfig(config, "unknown-agent");
expect(resolved).toBeUndefined();
});
it("should handle optional fields gracefully", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "minimal-agent" }],
},
});
const resolved = resolveAgentConfig(config, "minimal-agent");
expect(resolved).toBeDefined();
expect(resolved?.name).toBeUndefined();
expect(resolved?.workspace).toBeUndefined();
expect(resolved?.model).toBeUndefined();
});
});
describe("resolveAgentModelPrimary", () => {
it("should return primary model from string", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "test-agent", model: "test-model" }],
},
});
const primary = resolveAgentModelPrimary(config, "test-agent");
expect(primary).toBe("test-model");
});
it("should return primary model from object", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [
{
id: "test-agent",
model: { primary: "test-model", fallbacks: ["fallback-1", "fallback-2"] },
},
],
},
});
const primary = resolveAgentModelPrimary(config, "test-agent");
expect(primary).toBe("test-model");
});
it("should return undefined when no model configured", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "test-agent" }],
},
});
const primary = resolveAgentModelPrimary(config, "test-agent");
expect(primary).toBeUndefined();
});
});
describe("resolveAgentModelFallbacksOverride", () => {
it("should return fallbacks when explicitly set", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [
{
id: "test-agent",
model: { primary: "test-model", fallbacks: ["fallback-1", "fallback-2"] },
},
],
},
});
const fallbacks = resolveAgentModelFallbacksOverride(config, "test-agent");
expect(fallbacks).toEqual(["fallback-1", "fallback-2"]);
});
it("should return undefined when model is string", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "test-agent", model: "test-model" }],
},
});
const fallbacks = resolveAgentModelFallbacksOverride(config, "test-agent");
expect(fallbacks).toBeUndefined();
});
it("should return undefined when fallbacks not explicitly set", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "test-agent", model: { primary: "test-model" } }],
},
});
const fallbacks = resolveAgentModelFallbacksOverride(config, "test-agent");
expect(fallbacks).toBeUndefined();
});
it("should return empty array when explicitly set to empty", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "test-agent", model: { primary: "test-model", fallbacks: [] } }],
},
});
const fallbacks = resolveAgentModelFallbacksOverride(config, "test-agent");
expect(fallbacks).toEqual([]);
});
});
describe("resolveAgentWorkspaceDir", () => {
let tempDir: string;
beforeEach(() => {
tempDir = createTempDir();
});
afterEach(() => {
cleanupTempDir(tempDir);
});
it("should return configured workspace", () => {
const workspacePath = join(tempDir, "custom-workspace");
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "test-agent", workspace: workspacePath }],
},
});
const resolved = resolveAgentWorkspaceDir(config, "test-agent");
expect(resolved).toBe(workspacePath);
});
it("should return default workspace for default agent", () => {
const defaultWorkspace = join(tempDir, "default-workspace");
const config = createMockConfig({
agents: {
defaults: { workspace: defaultWorkspace },
list: [{ id: "main", default: true }],
},
});
const resolved = resolveAgentWorkspaceDir(config, "main");
expect(resolved).toBe(defaultWorkspace);
});
it("should return hardcoded default for non-default agent", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "main", default: true }, { id: "other-agent" }],
},
});
const resolved = resolveAgentWorkspaceDir(config, "other-agent");
// Should return ~/clawd-other-agent
expect(resolved).toContain("clawd-other-agent");
});
});
describe("resolveAgentDir", () => {
let tempDir: string;
beforeEach(() => {
tempDir = createTempDir();
process.env.CLAWDBOT_STATE_DIR = tempDir;
});
afterEach(() => {
cleanupTempDir(tempDir);
delete process.env.CLAWDBOT_STATE_DIR;
});
it("should return configured agent directory", () => {
const agentDirPath = join(tempDir, "custom-agent");
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "test-agent", agentDir: agentDirPath }],
},
});
const resolved = resolveAgentDir(config, "test-agent");
expect(resolved).toBe(agentDirPath);
});
it("should return default path based on state directory", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "test-agent" }],
},
});
const resolved = resolveAgentDir(config, "test-agent");
expect(resolved).toBe(join(tempDir, "agents", "test-agent", "agent"));
});
it("should create state directory when it doesn't exist", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "test-agent" }],
},
});
// State dir should be created if it doesn't exist
const resolved = resolveAgentDir(config, "test-agent");
expect(resolved).toContain("agents");
});
});
describe("Agent Routing (エージェントルーティング)", () => {
it("should route message to correct agent based on session", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [
{ id: "main", name: "Main", default: true },
{ id: "specialist", name: "Specialist" },
],
},
});
// No session key → use default
const agent1 = resolveSessionAgentId({ config });
expect(agent1).toBe("main");
// With session key → use specialist (format: agent:agentId:rest)
const agent2 = resolveSessionAgentId({
config,
sessionKey: "agent:specialist:active",
});
expect(agent2).toBe("specialist");
});
it("should handle invalid agent IDs gracefully", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [{ id: "main", name: "Main" }],
},
});
// Invalid session key should fall back to default
const agent = resolveSessionAgentId({
config,
sessionKey: "agent:nonexistent",
});
expect(agent).toBe("main");
});
});
describe("Agent Configuration Validation", () => {
it("should validate agent configuration structure", () => {
const validConfig = createMockConfig({
agents: {
defaults: {
model: { primary: "test-model" },
maxConcurrent: 4,
},
list: [
{
id: "test-agent",
name: "Test",
model: "test-model",
workspace: "/test",
},
],
},
});
expect(validConfig.agents?.list).toBeDefined();
expect(Array.isArray(validConfig.agents?.list)).toBe(true);
});
it("should handle empty agent list", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [],
},
});
const ids = listAgentIds(config);
expect(ids).toEqual(["main"]);
});
});
describe("Agent Model Resolution", () => {
it("should resolve model with primary and fallbacks", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [
{
id: "test-agent",
model: {
primary: "anthropic/claude-opus-4",
fallbacks: ["openai/gpt-4", "google/gemini-pro"],
},
},
],
},
});
const primary = resolveAgentModelPrimary(config, "test-agent");
const fallbacks = resolveAgentModelFallbacksOverride(config, "test-agent");
expect(primary).toBe("anthropic/claude-opus-4");
expect(fallbacks).toEqual(["openai/gpt-4", "google/gemini-pro"]);
});
it("should handle string model configuration", () => {
const config = createMockConfig({
agents: {
defaults: {},
list: [
{
id: "test-agent",
model: "simple-model",
},
],
},
});
const primary = resolveAgentModelPrimary(config, "test-agent");
const fallbacks = resolveAgentModelFallbacksOverride(config, "test-agent");
expect(primary).toBe("simple-model");
expect(fallbacks).toBeUndefined();
});
});
});
/**
* Agent Unit Test Summary
*
* Test Coverage:
* ✅ Agent ID listing and normalization
* ✅ Default agent resolution
* ✅ Session-based agent selection
* ✅ Agent configuration resolution
* ✅ Model configuration (primary + fallbacks)
* ✅ Workspace directory resolution
* ✅ Agent directory resolution
* ✅ Agent routing logic
* ✅ Configuration validation
*
* Run with:
* pnpm test test/unit/agent.test.ts
*
* For coverage:
* pnpm test:coverage test/unit/agent.test.ts
*/