diff --git a/test/env.ts b/test/env.ts
index 930b39b..d46233c 100644
--- a/test/env.ts
+++ b/test/env.ts
@@ -15,11 +15,11 @@ export const WHITELISTED_WALLET_PRIVATE_KEY = getEnvVar
(
"WHITELISTED_WALLET_PRIVATE_KEY",
);
-// export const BUYER_AGENT_WALLET_ADDRESS = getEnvVar(
-// "BUYER_AGENT_WALLET_ADDRESS",
-// );
+export const BUYER_AGENT_WALLET_ADDRESS = getEnvVar(
+ "BUYER_AGENT_WALLET_ADDRESS",
+);
-// export const BUYER_ENTITY_ID = parseInt(getEnvVar("BUYER_ENTITY_ID"));
+export const BUYER_ENTITY_ID = parseInt(getEnvVar("BUYER_ENTITY_ID"));
export const SELLER_AGENT_WALLET_ADDRESS = getEnvVar(
"SELLER_AGENT_WALLET_ADDRESS",
@@ -28,7 +28,7 @@ export const SELLER_AGENT_WALLET_ADDRESS = getEnvVar(
export const SELLER_ENTITY_ID = parseInt(getEnvVar("SELLER_ENTITY_ID"));
const entities = {
- // BUYER_ENTITY_ID,
+ BUYER_ENTITY_ID,
SELLER_ENTITY_ID,
};
diff --git a/test/integration/acpClient.integration.test.ts b/test/integration/acpClient.integration.test.ts
new file mode 100644
index 0000000..b616b04
--- /dev/null
+++ b/test/integration/acpClient.integration.test.ts
@@ -0,0 +1,283 @@
+import { Address } from "viem";
+import AcpClient from "../../src/acpClient";
+import AcpContractClientV2 from "../../src/contractClients/acpContractClientV2";
+import {
+ AcpAgentSort,
+ AcpGraduationStatus,
+ AcpOnlineStatus,
+} from "../../src/interfaces";
+import AcpJobOffering from "../../src/acpJobOffering";
+import AcpJob from "../../src/acpJob";
+import {
+ WHITELISTED_WALLET_PRIVATE_KEY,
+ BUYER_ENTITY_ID,
+ BUYER_AGENT_WALLET_ADDRESS,
+} from "../env";
+
+describe("AcpClient Integration Testing", () => {
+ let acpClient: AcpClient;
+ let contractClient: AcpContractClientV2;
+
+ beforeAll(async () => {
+ contractClient = await AcpContractClientV2.build(
+ WHITELISTED_WALLET_PRIVATE_KEY as Address,
+ BUYER_ENTITY_ID,
+ BUYER_AGENT_WALLET_ADDRESS as Address,
+ );
+
+ acpClient = new AcpClient({ acpContractClient: contractClient });
+ }, 45000);
+
+ describe("Initialization (init)", () => {
+ it("should initialize client successfully", () => {
+ expect(acpClient).toBeDefined();
+ expect(acpClient).toBeInstanceOf(AcpClient);
+ });
+
+ it("should have correct wallet address", () => {
+ expect(acpClient.walletAddress).toBe(BUYER_AGENT_WALLET_ADDRESS);
+ });
+
+ it("should have valid acpUrl", () => {
+ expect(acpClient.acpUrl).toBeDefined();
+ expect(acpClient.acpUrl).toBe("https://acpx.virtuals.io");
+ });
+
+ it("should have contract client initialized", () => {
+ expect(acpClient.acpContractClient).toBeDefined();
+ expect(acpClient.acpContractClient).toBe(contractClient);
+ });
+
+ it("should establish socket connection on initialization", (done) => {
+ // The socket connection is established in the constructor via init()
+ // If we reach this point without errors, the connection was successful
+ expect(acpClient).toBeDefined();
+
+ // Give socket time to connect
+ setTimeout(() => {
+ // If no connection errors thrown, test passes
+ done();
+ }, 2000);
+ }, 10000);
+
+ it("should handle onNewTask callback when provided", (done) => {
+ const onNewTaskMock = jest.fn((job: AcpJob) => {
+ expect(job).toBeInstanceOf(AcpJob);
+ done();
+ });
+
+ // Create a new client with callback
+ const clientWithCallback = new AcpClient({
+ acpContractClient: contractClient,
+ onNewTask: onNewTaskMock,
+ });
+
+ expect(clientWithCallback).toBeDefined();
+
+ // Note: This test will pass even if event doesn't fire
+ // Real socket event testing would require triggering an actual job
+ setTimeout(() => {
+ if (onNewTaskMock.mock.calls.length === 0) {
+ // No event fired, but that's expected in test environment
+ done();
+ }
+ }, 5000);
+ }, 10000);
+
+ it("should handle onEvaluate callback when provided", (done) => {
+ const onEvaluateMock = jest.fn((job: AcpJob) => {
+ expect(job).toBeInstanceOf(AcpJob);
+ done();
+ });
+
+ const clientWithCallback = new AcpClient({
+ acpContractClient: contractClient,
+ onEvaluate: onEvaluateMock,
+ });
+
+ expect(clientWithCallback).toBeDefined();
+
+ setTimeout(() => {
+ if (onEvaluateMock.mock.calls.length === 0) {
+ done();
+ }
+ }, 5000);
+ }, 10000);
+ });
+
+ describe("Agent Browsing (browseAgents)", () => {
+ it("should browse agents with keyword", async () => {
+ const keyword = "trading";
+ const options = {
+ top_k: 5,
+ };
+
+ const result = await acpClient.browseAgents(keyword, options);
+
+ expect(Array.isArray(result)).toBe(true);
+
+ console.log(`Found ${result.length} agents for keyword: ${keyword}`);
+ }, 30000);
+
+ it("should return agents with correct structure", async () => {
+ const keyword = "agent";
+ const options = {
+ top_k: 3,
+ };
+
+ const result = await acpClient.browseAgents(keyword, options);
+
+ if (result.length > 0) {
+ const firstAgent = result[0];
+
+ expect(firstAgent).toHaveProperty("id");
+ expect(firstAgent).toHaveProperty("name");
+ expect(firstAgent).toHaveProperty("description");
+ expect(firstAgent).toHaveProperty("walletAddress");
+ expect(firstAgent).toHaveProperty("contractAddress");
+ expect(firstAgent).toHaveProperty("jobOfferings");
+ expect(firstAgent).toHaveProperty("twitterHandle");
+
+ expect(typeof firstAgent.id).toBe("number");
+ expect(typeof firstAgent.name).toBe("string");
+ expect(typeof firstAgent.walletAddress).toBe("string");
+ expect(Array.isArray(firstAgent.jobOfferings)).toBe(true);
+
+ console.log("First agent:", {
+ id: firstAgent.id,
+ name: firstAgent.name,
+ jobCount: firstAgent.jobOfferings.length,
+ });
+ }
+ }, 30000);
+
+ it("should return job offerings as AcpJobOffering instances", async () => {
+ const keyword = "agent";
+ const options = {
+ top_k: 5,
+ };
+
+ const result = await acpClient.browseAgents(keyword, options);
+
+ const agentWithJobs = result.find(
+ (agent) => agent.jobOfferings.length > 0,
+ );
+
+ if (agentWithJobs) {
+ const jobOffering = agentWithJobs.jobOfferings[0];
+
+ expect(jobOffering).toBeInstanceOf(AcpJobOffering);
+ expect(typeof jobOffering.initiateJob).toBe("function");
+
+ console.log("Job offering:", {
+ name: jobOffering.name,
+ price: jobOffering.price,
+ });
+ } else {
+ console.log("No agents with job offerings found");
+ }
+ }, 30000);
+
+ it("should filter out own wallet address", async () => {
+ const keyword = "agent";
+ const options = {
+ top_k: 10,
+ };
+
+ const result = await acpClient.browseAgents(keyword, options);
+
+ // Verify own wallet is not in results
+ const ownWalletInResults = result.some(
+ (agent) =>
+ agent.walletAddress.toLowerCase() ===
+ BUYER_AGENT_WALLET_ADDRESS.toLowerCase(),
+ );
+
+ expect(ownWalletInResults).toBe(false);
+ }, 30000);
+
+ it("should filter by contract address", async () => {
+ const keyword = "agent";
+ const options = {
+ top_k: 10,
+ };
+
+ const result = await acpClient.browseAgents(keyword, options);
+
+ if (result.length > 0) {
+ // All returned agents should have matching contract address
+ const allHaveMatchingContract = result.every(
+ (agent) =>
+ agent.contractAddress.toLowerCase() ===
+ contractClient.contractAddress.toLowerCase(),
+ );
+
+ expect(allHaveMatchingContract).toBe(true);
+ }
+ }, 30000);
+
+ it("should respect top_k parameter", async () => {
+ const keyword = "agent";
+ const topK = 2;
+ const options = {
+ top_k: topK,
+ };
+
+ const result = await acpClient.browseAgents(keyword, options);
+
+ expect(result.length).toBeLessThanOrEqual(topK);
+ }, 30000);
+
+ it("should handle search with sort options", async () => {
+ const keyword = "trading";
+ const options = {
+ top_k: 5,
+ sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT],
+ };
+
+ const result = await acpClient.browseAgents(keyword, options);
+
+ expect(Array.isArray(result)).toBe(true);
+ console.log(`Found ${result.length} agents sorted by successfulJobCount`);
+ }, 30000);
+
+ it("should handle search with graduation status filter", async () => {
+ const keyword = "agent";
+ const options = {
+ top_k: 5,
+ graduationStatus: AcpGraduationStatus.GRADUATED,
+ };
+
+ const result = await acpClient.browseAgents(keyword, options);
+
+ expect(Array.isArray(result)).toBe(true);
+ console.log(`Found ${result.length} graduated agents`);
+ }, 30000);
+
+ it("should handle search with online status filter", async () => {
+ const keyword = "agent";
+ const options = {
+ top_k: 5,
+ onlineStatus: AcpOnlineStatus.ONLINE,
+ };
+
+ const result = await acpClient.browseAgents(keyword, options);
+
+ expect(Array.isArray(result)).toBe(true);
+ console.log(`Found ${result.length} online agents`);
+ }, 30000);
+
+ it("should return empty or minimal results for non-existent keyword", async () => {
+ const keyword = "thiskeywordisnotakeyworddonotreturnanyagents";
+ const options = {
+ top_k: 5,
+ };
+
+ const result = await acpClient.browseAgents(keyword, options);
+
+ expect(Array.isArray(result)).toBe(true);
+ // May or may not be empty depending on API behavior
+ console.log(`Found ${result.length} agents for non-existent keyword`);
+ }, 30000);
+ });
+});
diff --git a/test/unit/acpClient.test.ts b/test/unit/acpClient.test.ts
new file mode 100644
index 0000000..5ba9ca2
--- /dev/null
+++ b/test/unit/acpClient.test.ts
@@ -0,0 +1,1165 @@
+import { Address } from "viem";
+import { BaseAcpContractClient } from "../../src";
+import AcpClient, { EvaluateResult } from "../../src/acpClient";
+import AcpError from "../../src/acpError";
+import AcpMemo from "../../src/acpMemo";
+import AcpJob from "../../src/acpJob";
+import { AcpJobPhases } from "../../src";
+import { AcpAccount } from "../../src/acpAccount";
+import {
+ AcpAgentSort,
+ AcpGraduationStatus,
+ AcpOnlineStatus,
+} from "../../src/interfaces";
+
+jest.mock("socket.io-client", () => ({
+ io: jest.fn(() => ({
+ on: jest.fn(),
+ disconnect: jest.fn(),
+ })),
+}));
+
+describe("AcpClient Unit Testing", () => {
+ let acpClient: AcpClient;
+ let mockContractClient: jest.Mocked;
+
+ beforeEach(() => {
+ mockContractClient = {
+ contractAddress: "0x1234567890123456789012345678901234567890" as Address,
+ walletAddress: "0x0987654321098765432109876543210987654321" as Address,
+ config: {
+ acpUrl: "https://test-acp-url.com",
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ chain: { id: 1 },
+ },
+ handleOperation: jest.fn(),
+ getJobId: jest.fn(),
+ } as any;
+
+ acpClient = new AcpClient({
+ acpContractClient: mockContractClient,
+ });
+ });
+
+ it("should able to create EvaluateResult instance", () => {
+ const result = new EvaluateResult(true, "Approved");
+
+ expect(result.isApproved).toBe(true);
+ expect(result.reasoning).toBe("Approved");
+ });
+
+ it("should return first client when address is undefined", () => {
+ const result = acpClient.contractClientByAddress(undefined);
+ expect(result).toBe(mockContractClient);
+ });
+
+ it("should throw error when contract client not found by address", () => {
+ expect(() => {
+ acpClient.contractClientByAddress("0xNonexistent" as Address);
+ }).toThrow("ACP contract client not found");
+ });
+
+ it("should call defaultOnEvaluate when onEvaluate callback is not provided", async () => {
+ const mockJob = {
+ evaluate: jest.fn().mockResolvedValue(undefined),
+ } as unknown as AcpJob;
+
+ const defaultOnEvaluate = (acpClient as any)["defaultOnEvaluate"];
+
+ await defaultOnEvaluate.call(acpClient, mockJob);
+
+ expect(mockJob.evaluate).toHaveBeenCalledWith(true, "Evaluated by default");
+ });
+
+ it("should register SIGINT and SIGTERM cleanup handlers", () => {
+ const processSpy = jest.spyOn(process, "on");
+
+ const mockClient = new AcpClient({
+ acpContractClient: mockContractClient,
+ });
+
+ expect(processSpy).toHaveBeenCalledWith("SIGINT", expect.any(Function));
+ expect(processSpy).toHaveBeenCalledWith("SIGTERM", expect.any(Function));
+ });
+
+ // describe("Socket Event Handlers", () => {
+ // it("should handle ON_EVALUATE socket event with memos", (done) => {
+ // const mockSocketData = {
+ // id: 123,
+ // clientAddress: "0xClient" as Address,
+ // providerAddress: "0xProvider" as Address,
+ // evaluatorAddress: "0xEvaluator" as Address,
+ // price: 10,
+ // priceTokenAddress: "0xUSDCTokenAddress" as Address,
+ // memos: [
+ // {
+ // id: 1,
+ // memoType: 0,
+ // content: "evaluation memo",
+ // nextPhase: 2,
+ // status: "PENDING",
+ // senderAddress: "0xSender" as Address,
+ // signedReason: undefined,
+ // expiry: undefined,
+ // payableDetails: undefined,
+ // txHash: "0xtxhash123",
+ // signedTxHash: "0xsignedtxhash123",
+ // },
+ // ],
+ // phase: 1,
+ // context: { test: "data" },
+ // contractAddress: "0xContract" as Address,
+ // netPayableAmount: 10,
+ // };
+ //
+ // const onEvaluateMock = jest.fn((job: AcpJob) => {
+ // expect(job).toBeInstanceOf(AcpJob);
+ // expect(job.id).toBe(123);
+ // expect(job.clientAddress).toBe("0xClient");
+ // expect(job.memos.length).toBe(1);
+ // expect(job.memos[0]).toBeInstanceOf(AcpMemo);
+ // expect(job.memos[0].content).toBe("evaluation memo");
+ // expect(job.memos[0].txHash).toBe("0xtxhash123");
+ // done();
+ // });
+ //
+ // const mockSocketInstance = {
+ // on: jest.fn((event: string, handler: any) => {
+ // if (event === "ON_EVALUATE") {
+ // setImmediate(() => handler(mockSocketData, jest.fn()));
+ // }
+ // }),
+ // disconnect: jest.fn(),
+ // };
+ //
+ // const ioMock = require("socket.io-client");
+ // ioMock.io.mockReturnValue(mockSocketInstance);
+ //
+ // new AcpClient({
+ // acpContractClient: mockContractClient,
+ // onEvaluate: onEvaluateMock,
+ // });
+ // });
+ // });
+
+ describe("Agent Browsing (browseAgents)", () => {
+ const createMockAgent = (overrides = {}) => ({
+ id: 1,
+ documentId: "doc123",
+ name: "Test Agent",
+ description: "A test agent",
+ walletAddress: "0xAgent" as Address,
+ isVirtualAgent: false,
+ profilePic: "pic.jpg",
+ category: "test",
+ tokenAddress: null,
+ ownerAddress: "0xOwner" as Address,
+ cluster: null,
+ twitterHandle: "@testagent",
+ jobs: [
+ {
+ id: 1,
+ name: "Test Job",
+ description: "A test job",
+ priceV2: {
+ value: 100,
+ type: "NATIVE",
+ },
+ requirement: "Test requirement",
+ status: "active",
+ },
+ ],
+ resources: [],
+ metrics: {},
+ symbol: null,
+ virtualAgentId: null,
+ contractAddress: "0x1234567890123456789012345678901234567890" as Address,
+ ...overrides,
+ });
+
+ it("should filter out own wallet address from results", async () => {
+ const mockAgents = [
+ createMockAgent({
+ id: 1,
+ walletAddress: "0xOther" as Address,
+ }),
+ createMockAgent({
+ id: 2,
+ walletAddress:
+ "0x0987654321098765432109876543210987654321" as Address,
+ }),
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockAgents }),
+ });
+
+ const result = await acpClient.browseAgents("keyword", { top_k: 10 });
+
+ expect(result.length).toBe(1);
+ expect(result[0].walletAddress).toBe("0xOther");
+ });
+
+ it("should filter agents by matching contract addresses", async () => {
+ const mockAgents = [
+ createMockAgent({
+ id: 1,
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ }),
+ createMockAgent({
+ id: 2,
+ contractAddress: "0xDifferent" as Address,
+ }),
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockAgents }),
+ });
+
+ const result = await acpClient.browseAgents("keyword", { top_k: 10 });
+
+ expect(result.length).toBe(1);
+ expect(result[0].contractAddress).toBe(
+ "0x1234567890123456789012345678901234567890",
+ );
+ });
+
+ it("should transform agents to include job offerings", async () => {
+ const mockAgents = [createMockAgent()];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockAgents }),
+ });
+
+ const result = await acpClient.browseAgents("keyword", { top_k: 10 });
+
+ expect(result[0]).toHaveProperty("jobOfferings");
+ expect(Array.isArray(result[0].jobOfferings)).toBe(true);
+ expect(result[0].jobOfferings.length).toBe(1);
+ });
+
+ it("should return empty array when no agents match filters", async () => {
+ const mockAgents = [
+ createMockAgent({
+ contractAddress: "0xDifferent" as Address,
+ }),
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockAgents }),
+ });
+
+ const result = await acpClient.browseAgents("keyword", { top_k: 10 });
+
+ expect(result).toEqual([]);
+ });
+
+ it("should build URL with correct query parameters", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: [] }),
+ });
+
+ const keyword = "Trading";
+ const top_k_value = 5;
+
+ await acpClient.browseAgents(keyword, {
+ top_k: top_k_value,
+ sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT],
+ graduationStatus: AcpGraduationStatus.GRADUATED,
+ onlineStatus: AcpOnlineStatus.ALL,
+ });
+
+ const fetchCall = (global.fetch as jest.Mock).mock.calls[0][0];
+ expect(fetchCall).toContain(
+ `https://test-acp-url.com/api/agents/v4/search?search=${keyword}&sortBy=successfulJobCount&top_k=${top_k_value}&walletAddressesToExclude=${acpClient.walletAddress}&graduationStatus=graduated&onlineStatus=all`,
+ );
+ });
+
+ it("should handle agents with empty jobs array", async () => {
+ const mockAgents = [
+ createMockAgent({
+ jobs: [],
+ }),
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockAgents }),
+ });
+
+ const result = await acpClient.browseAgents("keyword", { top_k: 10 });
+
+ expect(result[0].jobOfferings).toEqual([]);
+ });
+
+ it("should include cluster parameter in URL when provided", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: [] }),
+ });
+
+ await acpClient.browseAgents("keyword", {
+ top_k: 5,
+ cluster: "defi",
+ });
+
+ expect(global.fetch).toHaveBeenCalledWith(
+ expect.stringContaining("&cluster=defi"),
+ );
+ });
+ });
+
+ describe("Constructor Validations", () => {
+ it("should throw error when no contract clients are provided", () => {
+ expect(() => {
+ new AcpClient({ acpContractClient: [] as any });
+ }).toThrow("ACP contract client is required");
+ });
+ it("should throw error when contract clients have different addresses", () => {
+ const mockClient1 = {
+ contractAddress:
+ "0x1111111111111111111111111111111111111111" as Address,
+ walletAddress: "0x0987654321098765432109876543210987654320" as Address,
+ config: {
+ acpUrl: "https://test-acp-url.com",
+ contractAddress:
+ "0x1111111111111111111111111111111111111111" as Address,
+ chain: { id: 1 },
+ },
+ handleOperation: jest.fn(),
+ getJobId: jest.fn(),
+ } as any;
+
+ const mockClient2 = {
+ contractAddress:
+ "0x2222222222222222222222222222222222222222" as Address,
+ walletAddress: "0x0987654321098765432109876543210987654321" as Address,
+ config: {
+ acpUrl: "https://test-acp-url.com",
+ contractAddress:
+ "0x2222222222222222222222222222222222222222" as Address,
+ chain: { id: 1 },
+ },
+ handleOperation: jest.fn(),
+ getJobId: jest.fn(),
+ } as any;
+
+ expect(() => {
+ new AcpClient({
+ acpContractClient: [mockClient1, mockClient2],
+ });
+ }).toThrow(
+ "All contract clients must have the same agent wallet address",
+ );
+ });
+ });
+
+ describe("Getting Active Jobs", () => {
+ it("should get all active jobs successfully", async () => {
+ const mockIAcpJobResponse = [
+ {
+ id: 1,
+ phase: AcpJobPhases.REQUEST,
+ description: "bullish",
+ clientAddress: "0xClient" as Address,
+ providerAddress: "0xProvider" as Address,
+ evaluatorAddress: "0xEvaluator" as Address,
+ price: 10,
+ priceTokenAddress: "0xPriceToken" as Address,
+ deliverable: null,
+ memos: [],
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ },
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockIAcpJobResponse }),
+ });
+
+ const result = await acpClient.getActiveJobs();
+
+ expect(Array.isArray(result)).toBe(true);
+ expect(result[0]).toBeInstanceOf(AcpJob);
+ expect(global.fetch).toHaveBeenCalledWith(
+ "https://test-acp-url.com/api/jobs/active?pagination[page]=1&pagination[pageSize]=10",
+ {
+ headers: {
+ "wallet-address": "0x0987654321098765432109876543210987654321",
+ },
+ },
+ );
+ });
+
+ it("should map memos to AcpMemo instances in active jobs", async () => {
+ const mockIAcpJobResponse = [
+ {
+ id: 1,
+ phase: AcpJobPhases.REQUEST,
+ description: "bullish",
+ clientAddress: "0xClient" as Address,
+ providerAddress: "0xProvider" as Address,
+ evaluatorAddress: "0xEvaluator" as Address,
+ price: 10,
+ priceTokenAddress: "0xPriceToken" as Address,
+ deliverable: null,
+ memos: [
+ {
+ id: 1,
+ memoType: 0,
+ content: "test memo",
+ nextPhase: 1,
+ status: "PENDING",
+ senderAddress: "0xSender" as Address,
+ signedReason: undefined,
+ expiry: undefined,
+ payableDetails: undefined,
+ txHash: "0xtxhash",
+ signedTxHash: "0xsignedtxhash",
+ },
+ ],
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ },
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockIAcpJobResponse }),
+ });
+
+ const result = await acpClient.getActiveJobs();
+
+ expect(result[0].memos.length).toBe(1);
+ expect(result[0].memos[0]).toBeInstanceOf(AcpMemo);
+ expect(result[0].memos[0].content).toBe("test memo");
+ });
+
+ it("should throw AcpError when API returns error", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ error: { message: "Jobs Not Found" } }),
+ });
+
+ await expect(acpClient.getActiveJobs()).resolves.toThrow(AcpError);
+ await expect(acpClient.getActiveJobs()).resolves.toThrow(
+ "Jobs Not Found",
+ );
+ });
+
+ it("should throw AcpError when fetch fails", async () => {
+ global.fetch = jest.fn().mockRejectedValue(new Error("Network Error"));
+
+ await expect(acpClient.getActiveJobs()).rejects.toThrow(AcpError);
+ await expect(acpClient.getActiveJobs()).rejects.toThrow(
+ "Failed to get active jobs",
+ );
+ });
+ });
+
+ describe("Getting Pending Memo Jobs", () => {
+ it("should get all pending memo jobs successfully", async () => {
+ const mockIAcpJobResponse = [
+ {
+ id: 1,
+ phase: AcpJobPhases.REQUEST,
+ description: "bullish",
+ clientAddress: "0xClient" as Address,
+ providerAddress: "0xProvider" as Address,
+ evaluatorAddress: "0xEvaluator" as Address,
+ price: 10,
+ priceTokenAddress: "0xPriceToken" as Address,
+ deliverable: null,
+ memos: [],
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ },
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockIAcpJobResponse }),
+ });
+
+ const result = await acpClient.getPendingMemoJobs();
+
+ expect(Array.isArray(result)).toBe(true);
+ expect(result[0]).toBeInstanceOf(AcpJob);
+ expect(global.fetch).toHaveBeenCalledWith(
+ "https://test-acp-url.com/api/jobs/pending-memos?pagination[page]=1&pagination[pageSize]=10",
+ {
+ headers: {
+ "wallet-address": "0x0987654321098765432109876543210987654321",
+ },
+ },
+ );
+ });
+
+ it("should map memos to AcpMemo instances in pending memo jobs", async () => {
+ const mockIAcpJobResponse = [
+ {
+ id: 1,
+ phase: AcpJobPhases.REQUEST,
+ description: "bullish",
+ clientAddress: "0xClient" as Address,
+ providerAddress: "0xProvider" as Address,
+ evaluatorAddress: "0xEvaluator" as Address,
+ price: 10,
+ priceTokenAddress: "0xPriceToken" as Address,
+ deliverable: null,
+ memos: [
+ {
+ id: 1,
+ memoType: 0,
+ content: "pending memo",
+ nextPhase: 1,
+ status: "PENDING",
+ senderAddress: "0xSender" as Address,
+ signedReason: undefined,
+ expiry: undefined,
+ payableDetails: undefined,
+ txHash: "0xtxhash",
+ signedTxHash: "0xsignedtxhash",
+ },
+ ],
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ },
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockIAcpJobResponse }),
+ });
+
+ const result = await acpClient.getPendingMemoJobs();
+
+ expect(result[0].memos.length).toBe(1);
+ expect(result[0].memos[0]).toBeInstanceOf(AcpMemo);
+ expect(result[0].memos[0].content).toBe("pending memo");
+ });
+
+ it("should throw AcpError when API returns error", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ error: { message: "Jobs Not Found" } }),
+ });
+
+ await expect(acpClient.getPendingMemoJobs()).resolves.toThrow(AcpError);
+ await expect(acpClient.getPendingMemoJobs()).resolves.toThrow(
+ "Jobs Not Found",
+ );
+ });
+
+ it("should throw AcpError when fetch fails", async () => {
+ global.fetch = jest.fn().mockRejectedValue(new Error("Network Error"));
+
+ await expect(acpClient.getPendingMemoJobs()).rejects.toThrow(AcpError);
+ await expect(acpClient.getPendingMemoJobs()).rejects.toThrow(
+ "Failed to get pending memo jobs",
+ );
+ });
+ });
+
+ describe("Getting Completed Jobs", () => {
+ it("should get all completed jobs successfully", async () => {
+ const mockIAcpJobResponse = [
+ {
+ id: 1,
+ phase: AcpJobPhases.REQUEST,
+ description: "bullish",
+ clientAddress: "0xClient" as Address,
+ providerAddress: "0xProvider" as Address,
+ evaluatorAddress: "0xEvaluator" as Address,
+ price: 10,
+ priceTokenAddress: "0xPriceToken" as Address,
+ deliverable: null,
+ memos: [],
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ },
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockIAcpJobResponse }),
+ });
+
+ const result = await acpClient.getCompletedJobs();
+
+ expect(Array.isArray(result)).toBe(true);
+ expect(result[0]).toBeInstanceOf(AcpJob);
+ expect(global.fetch).toHaveBeenCalledWith(
+ "https://test-acp-url.com/api/jobs/completed?pagination[page]=1&pagination[pageSize]=10",
+ {
+ headers: {
+ "wallet-address": "0x0987654321098765432109876543210987654321",
+ },
+ },
+ );
+ });
+
+ it("should map memos to AcpMemo instances in completed jobs", async () => {
+ const mockIAcpJobResponse = [
+ {
+ id: 1,
+ phase: AcpJobPhases.REQUEST,
+ description: "bullish",
+ clientAddress: "0xClient" as Address,
+ providerAddress: "0xProvider" as Address,
+ evaluatorAddress: "0xEvaluator" as Address,
+ price: 10,
+ priceTokenAddress: "0xPriceToken" as Address,
+ deliverable: null,
+ memos: [
+ {
+ id: 1,
+ memoType: 0,
+ content: "completed memo",
+ nextPhase: 1,
+ status: "COMPLETED",
+ senderAddress: "0xSender" as Address,
+ signedReason: undefined,
+ expiry: undefined,
+ payableDetails: undefined,
+ txHash: "0xtxhash",
+ signedTxHash: "0xsignedtxhash",
+ },
+ ],
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ },
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockIAcpJobResponse }),
+ });
+
+ const result = await acpClient.getCompletedJobs();
+
+ expect(result[0].memos.length).toBe(1);
+ expect(result[0].memos[0]).toBeInstanceOf(AcpMemo);
+ expect(result[0].memos[0].content).toBe("completed memo");
+ });
+
+ it("should throw AcpError when API returns error", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ error: { message: "Jobs Not Found" } }),
+ });
+
+ await expect(acpClient.getCompletedJobs()).resolves.toThrow(AcpError);
+ await expect(acpClient.getCompletedJobs()).resolves.toThrow(
+ "Jobs Not Found",
+ );
+ });
+
+ it("should throw AcpError when fetch fails", async () => {
+ global.fetch = jest.fn().mockRejectedValue(new Error("Network Error"));
+
+ await expect(acpClient.getCompletedJobs()).rejects.toThrow(AcpError);
+ await expect(acpClient.getCompletedJobs()).rejects.toThrow(
+ "Failed to get completed jobs",
+ );
+ });
+ });
+
+ describe("Getting Cancelled Jobs", () => {
+ it("should get all cancelled jobs successfully", async () => {
+ const mockIAcpJobResponse = [
+ {
+ id: 1,
+ phase: AcpJobPhases.REQUEST,
+ description: "bullish",
+ clientAddress: "0xClient" as Address,
+ providerAddress: "0xProvider" as Address,
+ evaluatorAddress: "0xEvaluator" as Address,
+ price: 10,
+ priceTokenAddress: "0xPriceToken" as Address,
+ deliverable: null,
+ memos: [],
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ },
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockIAcpJobResponse }),
+ });
+
+ const result = await acpClient.getCancelledJobs();
+
+ expect(Array.isArray(result)).toBe(true);
+ expect(result[0]).toBeInstanceOf(AcpJob);
+ expect(global.fetch).toHaveBeenCalledWith(
+ "https://test-acp-url.com/api/jobs/cancelled?pagination[page]=1&pagination[pageSize]=10",
+ {
+ headers: {
+ "wallet-address": "0x0987654321098765432109876543210987654321",
+ },
+ },
+ );
+ });
+
+ it("should map memos to AcpMemo instances in cancelled jobs", async () => {
+ const mockIAcpJobResponse = [
+ {
+ id: 1,
+ phase: AcpJobPhases.REQUEST,
+ description: "bullish",
+ clientAddress: "0xClient" as Address,
+ providerAddress: "0xProvider" as Address,
+ evaluatorAddress: "0xEvaluator" as Address,
+ price: 10,
+ priceTokenAddress: "0xPriceToken" as Address,
+ deliverable: null,
+ memos: [
+ {
+ id: 1,
+ memoType: 0,
+ content: "cancelled memo",
+ nextPhase: 1,
+ status: "CANCELLED",
+ senderAddress: "0xSender" as Address,
+ signedReason: undefined,
+ expiry: undefined,
+ payableDetails: undefined,
+ txHash: "0xtxhash",
+ signedTxHash: "0xsignedtxhash",
+ },
+ ],
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ },
+ ];
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockIAcpJobResponse }),
+ });
+
+ const result = await acpClient.getCancelledJobs();
+
+ expect(result[0].memos.length).toBe(1);
+ expect(result[0].memos[0]).toBeInstanceOf(AcpMemo);
+ expect(result[0].memos[0].content).toBe("cancelled memo");
+ });
+
+ it("should throw AcpError when API returns error", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ error: { message: "Jobs Not Found" } }),
+ });
+
+ await expect(acpClient.getCancelledJobs()).resolves.toThrow(AcpError);
+ await expect(acpClient.getCancelledJobs()).resolves.toThrow(
+ "Jobs Not Found",
+ );
+ });
+
+ it("should throw AcpError when fetch fails", async () => {
+ global.fetch = jest.fn().mockRejectedValue(new Error("Network Error"));
+
+ await expect(acpClient.getCancelledJobs()).rejects.toThrow(AcpError);
+ await expect(acpClient.getCancelledJobs()).rejects.toThrow(
+ "Failed to get cancelled jobs",
+ );
+ });
+ });
+
+ describe("Getting Job by Id", () => {
+ it("should get job by job id successfully", async () => {
+ const mockJobId = 123;
+
+ const mockAcpJobResponse = {
+ id: 1,
+ phase: AcpJobPhases.REQUEST,
+ description: "bullish",
+ clientAddress: "0xClient" as Address,
+ providerAddress: "0xProvider" as Address,
+ evaluatorAddress: "0xEvaluator" as Address,
+ price: 10,
+ priceTokenAddress: "0xPriceToken" as Address,
+ deliverable: null,
+ memos: [],
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ };
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockAcpJobResponse }),
+ });
+
+ const result = await acpClient.getJobById(mockJobId);
+
+ expect(result).toBeInstanceOf(AcpJob);
+ expect(result?.id).toBe(1);
+ expect(global.fetch).toHaveBeenCalledWith(
+ `https://test-acp-url.com/api/jobs/${mockJobId}`,
+ {
+ headers: {
+ "wallet-address": "0x0987654321098765432109876543210987654321",
+ },
+ },
+ );
+ });
+
+ it("should map memos to AcpMemo instances when getting job by id", async () => {
+ const mockJobId = 123;
+
+ const mockAcpJobResponse = {
+ id: 1,
+ phase: AcpJobPhases.REQUEST,
+ description: "bullish",
+ clientAddress: "0xClient" as Address,
+ providerAddress: "0xProvider" as Address,
+ evaluatorAddress: "0xEvaluator" as Address,
+ price: 10,
+ priceTokenAddress: "0xPriceToken" as Address,
+ deliverable: null,
+ memos: [
+ {
+ id: 1,
+ memoType: 0,
+ content: "job memo",
+ nextPhase: 1,
+ status: "PENDING",
+ senderAddress: "0xSender" as Address,
+ signedReason: undefined,
+ expiry: undefined,
+ payableDetails: undefined,
+ txHash: "0xtxhash",
+ signedTxHash: "0xsignedtxhash",
+ },
+ ],
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ };
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockAcpJobResponse }),
+ });
+
+ const result = await acpClient.getJobById(mockJobId);
+
+ expect(result?.memos.length).toBe(1);
+ expect(result?.memos[0]).toBeInstanceOf(AcpMemo);
+ expect(result?.memos[0].content).toBe("job memo");
+ });
+
+ it("should return undefined when job doesn't exist", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: null }),
+ });
+
+ const result = await acpClient.getJobById(123);
+
+ expect(result).toBeUndefined();
+ });
+
+ it("should throw AcpError when API returns error", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ error: { message: "Job Not Found" } }),
+ });
+
+ await expect(acpClient.getJobById(123)).resolves.toThrow(AcpError);
+ await expect(acpClient.getJobById(123)).resolves.toThrow("Job Not Found");
+ });
+
+ it("should throw AcpError when fetch fails", async () => {
+ global.fetch = jest.fn().mockRejectedValue(new Error("Network Fail"));
+
+ await expect(acpClient.getJobById(123)).rejects.toThrow(AcpError);
+ await expect(acpClient.getJobById(123)).rejects.toThrow(
+ "Failed to get job by id",
+ );
+ });
+ });
+
+ describe("Getting Memo by Id", () => {
+ it("should get memo by job id successfully", async () => {
+ const mockJobId = 123;
+ const mockMemoId = 456;
+
+ const mockMemoData = {
+ id: mockMemoId,
+ type: "MESSAGE",
+ content: "Test memo content",
+ createdAt: "2024-01-01",
+ memoType: 0,
+ nextPhase: 1,
+ status: "PENDING",
+ senderAddress: "0xSender" as Address,
+ signedReason: undefined,
+ expiry: undefined,
+ payableDetails: undefined,
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ };
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: mockMemoData }),
+ });
+
+ const result = await acpClient.getMemoById(mockJobId, mockMemoId);
+
+ expect(result).toBeInstanceOf(AcpMemo);
+ expect(result?.content).toBe("Test memo content");
+ expect(global.fetch).toHaveBeenCalledWith(
+ `https://test-acp-url.com/api/jobs/${mockJobId}/memos/${mockMemoId}`,
+ {
+ headers: {
+ "wallet-address": "0x0987654321098765432109876543210987654321",
+ },
+ },
+ );
+ });
+
+ it("should return undefined when memo doesn't exist", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: null }),
+ });
+
+ const result = await acpClient.getMemoById(123, 456);
+
+ expect(result).toBeUndefined();
+ });
+
+ it("should throw AcpError when API returns error", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ error: { message: "Memo Not Found" } }),
+ });
+
+ await expect(acpClient.getMemoById(123, 456)).resolves.toThrow(AcpError);
+ await expect(acpClient.getMemoById(123, 456)).resolves.toThrow(
+ "Memo Not Found",
+ );
+ });
+
+ it("should throw AcpError when fetch fails", async () => {
+ global.fetch = jest.fn().mockRejectedValue(new Error("Network Error"));
+
+ await expect(acpClient.getMemoById(123, 456)).rejects.toThrow(AcpError);
+ await expect(acpClient.getMemoById(123, 456)).rejects.toThrow(
+ "Failed to get memo by id",
+ );
+ });
+ });
+
+ describe("Getting Agent from Wallet Address", () => {
+ it("should get first agent from wallet address successfully", async () => {
+ const mockWalletAddress = "0xClient" as Address;
+
+ const mockAgent1 = {
+ id: 1,
+ documentId: "doc123",
+ name: "Agent One",
+ description: "Test agent",
+ walletAddress: mockWalletAddress,
+ isVirtualAgent: false,
+ profilePic: "pic.jpg",
+ category: "test",
+ tokenAddress: null,
+ ownerAddress: "0xOwner",
+ cluster: null,
+ twitterHandle: "@agent",
+ jobs: [],
+ resources: [],
+ symbol: null,
+ virtualAgentId: null,
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ };
+
+ const mockAgent2 = {
+ id: 2,
+ documentId: "doc456",
+ name: "Agent Two",
+ description: "Second agent",
+ walletAddress: mockWalletAddress,
+ isVirtualAgent: false,
+ profilePic: "pic2.jpg",
+ category: "test",
+ tokenAddress: null,
+ ownerAddress: "0xOwner",
+ cluster: null,
+ twitterHandle: "@agent2",
+ jobs: [],
+ resources: [],
+ symbol: null,
+ virtualAgentId: null,
+ contractAddress:
+ "0x1234567890123456789012345678901234567890" as Address,
+ };
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: [mockAgent1, mockAgent2] }),
+ });
+
+ const result = await acpClient.getAgent(mockWalletAddress);
+
+ expect(result).toEqual(mockAgent1);
+ expect(result?.id).toBe(1);
+ expect(result?.name).toBe("Agent One");
+ expect(result?.id).not.toBe(2);
+ expect(global.fetch).toHaveBeenCalledWith(
+ `https://test-acp-url.com/api/agents?filters[walletAddress]=${mockWalletAddress}`,
+ );
+ });
+
+ it("should return undefined when no agents found", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: [] }),
+ }) as jest.Mock;
+
+ const result = await acpClient.getAgent("0xClient" as Address);
+
+ expect(result).toBeUndefined();
+ });
+
+ it("should return undefined when data is null", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: null }),
+ }) as jest.Mock;
+
+ const result = await acpClient.getAgent("0xNonexistent" as Address);
+
+ expect(result).toBeUndefined();
+ });
+ });
+
+ describe("Getting Account by Job Id ", () => {
+ it("should get account by job id", async () => {
+ const mockJobId = 123;
+
+ const mockResponseData = {
+ data: {
+ id: 0,
+ clientAddress: "0xjohnson" as Address,
+ providerAddress: "0xjoshua" as Address,
+ metadata: { status: "Bullish" },
+ },
+ };
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => mockResponseData,
+ });
+
+ const result = await acpClient.getAccountByJobId(mockJobId);
+
+ expect(result).toBeInstanceOf(AcpAccount);
+ expect(result?.id).toBe(0);
+ expect(result?.clientAddress).toBe("0xjohnson");
+ expect(global.fetch).toHaveBeenCalledWith(
+ `https://test-acp-url.com/api/accounts/job/${mockJobId}`,
+ );
+ });
+
+ it("should return null when account doesn't exist", async () => {
+ const mockJobId = 123;
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: null }),
+ });
+
+ const result = await acpClient.getAccountByJobId(mockJobId);
+
+ expect(result).toBeNull();
+ });
+
+ it("should throw AcpError when fetch fails", async () => {
+ global.fetch = jest
+ .fn()
+ .mockRejectedValue(new Error("Network Error")) as jest.Mock;
+
+ await expect(acpClient.getAccountByJobId).rejects.toThrow(AcpError);
+ });
+ });
+
+ describe("Getting Account by Client and Provider", () => {
+ it("should get account by client and provider successfully", async () => {
+ const mockClientAddress = "0xClient" as Address;
+ const mockProviderAddress = "0xProvider" as Address;
+
+ const mockResponseData = {
+ data: {
+ id: 0,
+ clientAddress: "0xjohnson" as Address,
+ providerAddress: "0xjoshua" as Address,
+ metadata: { status: "Bullish" },
+ },
+ };
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => mockResponseData,
+ });
+
+ const result = await acpClient.getByClientAndProvider(
+ mockClientAddress,
+ mockProviderAddress,
+ );
+
+ expect(result).toBeInstanceOf(AcpAccount);
+ expect(result?.id).toBe(0);
+ expect(result?.clientAddress).toBe("0xjohnson");
+ expect(global.fetch).toHaveBeenCalledWith(
+ `https://test-acp-url.com/api/accounts/client/${mockClientAddress}/provider/${mockProviderAddress}`,
+ );
+ });
+
+ it("should return null when account doesn't exist", async () => {
+ const mockClientAddress = "0xClient";
+ const mockProviderAddress = "0xProvider";
+
+ global.fetch = jest.fn().mockResolvedValue({
+ json: async () => ({ data: null }),
+ });
+
+ const result = await acpClient.getByClientAndProvider(
+ mockClientAddress,
+ mockProviderAddress,
+ );
+
+ expect(result).toBeNull();
+ });
+
+ it("should throw AcpError when fetch fails", async () => {
+ global.fetch = jest
+ .fn()
+ .mockRejectedValue(new Error("Network error")) as jest.Mock;
+
+ await expect(
+ acpClient.getByClientAndProvider(
+ "0xClient" as Address,
+ "0xProvider" as Address,
+ ),
+ ).rejects.toThrow(AcpError);
+
+ await expect(
+ acpClient.getByClientAndProvider(
+ "0xClient" as Address,
+ "0xProvider" as Address,
+ ),
+ ).rejects.toThrow("Failed to get account by client and provider");
+ });
+ });
+
+ describe("Getter Methods", () => {
+ it("should get wallet address correctly", () => {
+ expect(acpClient.walletAddress).toBe(
+ "0x0987654321098765432109876543210987654321",
+ );
+ });
+
+ it("should able to get acpUrl correctly", () => {
+ expect(acpClient.acpUrl).toBe("https://test-acp-url.com");
+ });
+
+ it("should able to get the first acpContractClient when multi-client exists", () => {
+ const mockClient1 = {
+ ...mockContractClient,
+ walletAddress: "0xfirst" as Address,
+ } as any;
+ const mockClient2 = {
+ ...mockContractClient,
+ walletAddress: "0xfirst" as Address,
+ } as any;
+
+ const multiClient = new AcpClient({
+ acpContractClient: [mockClient1, mockClient2],
+ });
+
+ expect(multiClient.acpContractClient).toBe(mockClient1);
+ });
+ });
+});