Skip to content

Commit a8a439c

Browse files
authored
fix: added helper func to get storage client + tests (#14)
* fix: added helper func to get storage client + tests * added jsdoc
1 parent 4984ec2 commit a8a439c

File tree

4 files changed

+159
-22
lines changed

4 files changed

+159
-22
lines changed

__tests__/clients/storage.test.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
2-
import { createStorageClient, StorageClientImpl, StorageClientInterface } from '../../src/clients/storage';
2+
import { createStorageClient, StorageClientInstanceImpl, StorageClientInterface } from '../../src/clients/storage';
33
import * as Storage from '@web3-storage/w3up-client';
44
import { StoreMemory } from '@web3-storage/w3up-client/stores/memory';
55
import { defaultGatewayUrl } from '../../src/utils';
@@ -62,27 +62,31 @@ vi.mock('../../src/environments', () => ({
6262
})
6363
}));
6464

65+
// Mock fetch for getContent tests
66+
global.fetch = vi.fn();
67+
6568
// Clear mocks once before all tests
6669
beforeEach(() => {
6770
vi.clearAllMocks();
6871
});
6972

7073
describe('StorageClientImpl', () => {
71-
let storageClientImpl: StorageClientImpl;
74+
let storageClientImpl: StorageClientInstanceImpl;
7275
let mockRuntime: any;
7376

7477
beforeEach(() => {
7578
mockRuntime = {
7679
getParameter: vi.fn(),
7780
};
7881
console.log = vi.fn(); // Mock console.log to avoid cluttering test output
79-
storageClientImpl = new StorageClientImpl(mockRuntime);
82+
storageClientImpl = new StorageClientInstanceImpl(mockRuntime);
83+
(global.fetch as any).mockReset();
8084
});
8185

8286
it('should initialize correctly', async () => {
8387
await storageClientImpl.start();
8488

85-
expect(storageClientImpl.getStorageClient()).not.toBeNull();
89+
expect(storageClientImpl.getStorage()).not.toBeNull();
8690
expect(elizaLogger.success).toHaveBeenCalledWith('✅ Storage client successfully started');
8791
});
8892

@@ -106,12 +110,12 @@ describe('StorageClientImpl', () => {
106110
});
107111

108112
it('should throw error if client is not initialized when getting storage client', () => {
109-
expect(() => storageClientImpl.getStorageClient()).toThrow('Storage client not initialized');
113+
expect(() => storageClientImpl.getStorage()).toThrow('Storage client not initialized');
110114
});
111115

112116
it('should return the storage client when initialized', async () => {
113117
await storageClientImpl.start();
114-
expect(storageClientImpl.getStorageClient()).not.toBeNull();
118+
expect(storageClientImpl.getStorage()).not.toBeNull();
115119
});
116120

117121
it('should throw error if client is not initialized when getting config', () => {
@@ -138,13 +142,48 @@ describe('StorageClientImpl', () => {
138142

139143
it('should properly clean up resources when stopped', async () => {
140144
await storageClientImpl.start();
141-
expect(storageClientImpl.getStorageClient()).not.toBeNull();
145+
expect(storageClientImpl.getStorage()).not.toBeNull();
142146

143147
await storageClientImpl.stop();
144148

145-
expect(() => storageClientImpl.getStorageClient()).toThrow('Storage client not initialized');
149+
expect(() => storageClientImpl.getStorage()).toThrow('Storage client not initialized');
146150
expect(() => storageClientImpl.getConfig()).toThrow('Storage client not initialized');
147151
});
152+
153+
describe('getContent', () => {
154+
it('should fetch content from the configured gateway URL', async () => {
155+
await storageClientImpl.start();
156+
const testCid = 'bafytest123';
157+
const mockResponse = new Response('mock content');
158+
(global.fetch as any).mockResolvedValueOnce(mockResponse);
159+
160+
const result = await storageClientImpl.getContent(testCid);
161+
162+
expect(global.fetch).toHaveBeenCalledWith('https://mock-gateway.link/ipfs/bafytest123');
163+
expect(result).toBe(mockResponse);
164+
});
165+
166+
it('should fetch content from the default gateway URL when not configured', async () => {
167+
storageClientImpl.config = null; // No config set
168+
const testCid = 'bafytest123';
169+
const mockResponse = new Response('mock content');
170+
(global.fetch as any).mockResolvedValueOnce(mockResponse);
171+
172+
const result = await storageClientImpl.getContent(testCid);
173+
174+
expect(global.fetch).toHaveBeenCalledWith(`${defaultGatewayUrl}/ipfs/bafytest123`);
175+
expect(result).toBe(mockResponse);
176+
});
177+
178+
it('should propagate fetch errors', async () => {
179+
await storageClientImpl.start();
180+
const testCid = 'bafytest123';
181+
const mockError = new Error('Network error');
182+
(global.fetch as any).mockRejectedValueOnce(mockError);
183+
184+
await expect(storageClientImpl.getContent(testCid)).rejects.toThrow('Network error');
185+
});
186+
});
148187
});
149188

150189
describe('StorageClientInterface', () => {

__tests__/index.test.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
1-
import { describe, it, expect } from 'vitest';
2-
import { storagePlugin } from '../src/index';
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { storagePlugin, getStorageClient } from '../src/index';
33
import { uploadAction, retrieveAction } from '../src/actions';
44
import { StorageClientInterface } from '../src/clients/storage';
55
import { storageClientEnvSchema } from '../src/environments';
66

7+
// Mock dependencies
8+
vi.mock('../src/clients/storage', () => {
9+
const mockStorageClient = {
10+
getStorage: vi.fn().mockReturnValue('mock-storage-client'),
11+
stop: vi.fn()
12+
};
13+
14+
return {
15+
StorageClientInterface: {
16+
name: 'storage',
17+
start: vi.fn().mockResolvedValue(mockStorageClient)
18+
},
19+
StorageClientInstanceImpl: class MockStorageClientImpl {
20+
getStorage = vi.fn().mockReturnValue('mock-storage-client');
21+
}
22+
};
23+
});
24+
725
describe('storagePlugin', () => {
826
it('should export the correct plugin structure', () => {
927
expect(storagePlugin).toBeDefined();
@@ -35,4 +53,53 @@ describe('storagePlugin', () => {
3553
expect(defaultExport).toBeDefined();
3654
expect(defaultExport.name).toBe('storage');
3755
});
56+
});
57+
58+
describe('getStorageClient', () => {
59+
beforeEach(() => {
60+
vi.clearAllMocks();
61+
});
62+
63+
it('should return the storage client when plugin is found in runtime', async () => {
64+
const mockRuntime = {
65+
plugins: [
66+
{
67+
name: 'storage',
68+
clients: [StorageClientInterface]
69+
}
70+
]
71+
};
72+
73+
const result = await getStorageClient(mockRuntime as any);
74+
75+
expect(result).toBeDefined();
76+
expect(result).toHaveProperty('getStorage');
77+
expect(StorageClientInterface.start).toHaveBeenCalledWith(mockRuntime);
78+
});
79+
80+
it('should throw an error when plugin is not found in runtime', async () => {
81+
const mockRuntime = {
82+
plugins: [
83+
{
84+
name: 'different-plugin',
85+
clients: []
86+
}
87+
]
88+
};
89+
90+
await expect(getStorageClient(mockRuntime as any)).rejects.toThrow('Storage client not found in runtime');
91+
});
92+
93+
it('should throw an error when plugin has no clients', async () => {
94+
const mockRuntime = {
95+
plugins: [
96+
{
97+
name: 'storage',
98+
clients: []
99+
}
100+
]
101+
};
102+
103+
await expect(getStorageClient(mockRuntime as any)).rejects.toThrow('Storage client not found in runtime');
104+
});
38105
});

src/clients/storage.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { Signer } from '@ucanto/principal/ed25519';
55
import { StorageClientConfig, validateStorageClientConfig } from "../environments";
66
import { defaultGatewayUrl, parseDelegation } from '../utils';
77

8-
export class StorageClientImpl {
8+
export class StorageClientInstanceImpl implements ClientInstance {
99
private readonly runtime: IAgentRuntime;
10-
private storageClient: Storage.Client | null = null;
10+
private storage: Storage.Client | null = null;
1111
config: StorageClientConfig | null = null;
1212

1313
constructor(runtime: IAgentRuntime) {
@@ -16,12 +16,12 @@ export class StorageClientImpl {
1616

1717
async start(): Promise<void> {
1818
try {
19-
if (this.storageClient) {
19+
if (this.storage) {
2020
elizaLogger.info("Storage client already initialized");
2121
return;
2222
}
2323
this.config = await validateStorageClientConfig(this.runtime);
24-
this.storageClient = await createStorageClient(this.config);
24+
this.storage = await createStorageClient(this.config);
2525
elizaLogger.success(`✅ Storage client successfully started`);
2626
} catch (error) {
2727
elizaLogger.error(`❌ Storage client failed to start: ${error}`);
@@ -30,15 +30,15 @@ export class StorageClientImpl {
3030
}
3131

3232
async stop(runtime?: IAgentRuntime): Promise<void> {
33-
this.storageClient = null;
33+
this.storage = null;
3434
this.config = null;
3535
}
3636

37-
getStorageClient() {
38-
if (!this.storageClient) {
37+
getStorage() {
38+
if (!this.storage) {
3939
throw new Error("Storage client not initialized");
4040
}
41-
return this.storageClient;
41+
return this.storage;
4242
}
4343

4444
getConfig() {
@@ -51,12 +51,16 @@ export class StorageClientImpl {
5151
getGatewayUrl(): string {
5252
return this.config?.GATEWAY_URL || defaultGatewayUrl;
5353
}
54+
55+
getContent(cid: string): Promise<Response> {
56+
return fetch(`${this.getGatewayUrl()}/ipfs/${cid}`);
57+
}
5458
}
5559

5660
export const StorageClientInterface: Client = {
5761
name: 'storage',
5862
start: async (runtime: IAgentRuntime): Promise<ClientInstance> => {
59-
const storageClient = new StorageClientImpl(runtime);
63+
const storageClient = new StorageClientInstanceImpl(runtime);
6064
await storageClient.start();
6165
return storageClient;
6266
}

src/index.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import type { Plugin } from "@elizaos/core";
1+
import type { ClientInstance, IAgentRuntime, Plugin } from "@elizaos/core";
22
import { uploadAction, retrieveAction } from "./actions";
33
import { storageClientEnvSchema } from "./environments.ts";
4-
import { StorageClientInterface } from "./clients/storage.ts";
4+
import { StorageClientInstanceImpl, StorageClientInterface } from "./clients/storage.ts";
5+
import * as Storage from '@web3-storage/w3up-client';
6+
export { StorageClientInterface, StorageClientInstanceImpl as StorageClientImpl } from "./clients/storage.ts";
7+
8+
9+
const PluginName = "storage";
510

611
export const storagePlugin: Plugin = {
7-
name: "storage",
12+
name: PluginName,
813
description: "Plugin to manage files in a decentralized storage network",
914
config: storageClientEnvSchema,
1015
actions: [uploadAction, retrieveAction],
@@ -13,5 +18,27 @@ export const storagePlugin: Plugin = {
1318
evaluators: [],
1419
providers: [],
1520
};
21+
22+
23+
/**
24+
* A helper function for Agent to get the storage client.
25+
* It returns the first storage client from the runtime that is identified as plugin.name === storage.
26+
*
27+
* @param runtime - The runtime to get the storage client from.
28+
* @returns The storage client.
29+
* @throws An error if no storage client is found.
30+
*/
31+
export const getStorageClient = async (runtime: IAgentRuntime): Promise<StorageClientInstanceImpl> => {
32+
const storagePlugin = runtime.plugins.find((plugin) => plugin.name === PluginName);
33+
if (storagePlugin && storagePlugin.clients && storagePlugin.clients.length > 0) {
34+
const [storageStarter] = storagePlugin.clients;
35+
if (storageStarter) {
36+
const storageClient = await storageStarter.start(runtime);
37+
return storageClient as StorageClientInstanceImpl;
38+
}
39+
}
40+
throw new Error("Storage client not found in runtime");
41+
}
42+
1643
export default storagePlugin;
1744

0 commit comments

Comments
 (0)