From d4f3711e59e34e8ca6b447844d670ea3fa98fc00 Mon Sep 17 00:00:00 2001 From: Abdurrehman Subhani Date: Wed, 1 Jan 2025 17:52:36 +0500 Subject: [PATCH 1/8] add basic usdk version command test --- packages/usdk/jest.config.mjs | 5 +++++ packages/usdk/package.json | 4 ++-- packages/usdk/tests/commands/version.test.js | 22 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 packages/usdk/jest.config.mjs create mode 100644 packages/usdk/tests/commands/version.test.js diff --git a/packages/usdk/jest.config.mjs b/packages/usdk/jest.config.mjs new file mode 100644 index 000000000..426948dd1 --- /dev/null +++ b/packages/usdk/jest.config.mjs @@ -0,0 +1,5 @@ +export default { + testEnvironment: 'node', + moduleFileExtensions: ['js', 'mjs'], + testMatch: ['**/tests/**/*.test.js'], +}; diff --git a/packages/usdk/package.json b/packages/usdk/package.json index 4fde58ea3..f4480a3a1 100644 --- a/packages/usdk/package.json +++ b/packages/usdk/package.json @@ -18,7 +18,8 @@ "type": "module", "main": "module.mjs", "scripts": { - "preinstall": "[[ $npm_config_global == 'true' ]] || exit 0; cd packages/upstreet-agent && pnpm install --ignore-workspace --no-frozen-lockfile" + "preinstall": "[[ $npm_config_global == 'true' ]] || exit 0; cd packages/upstreet-agent && pnpm install --ignore-workspace --no-frozen-lockfile", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "keywords": [ "ai", @@ -85,7 +86,6 @@ "gitignore-parser": "0.0.2", "hono": "^4.6.9", "javascript-time-ago": "^2.5.11", - "jest": "^29.7.0", "jimp": "^1.6.0", "json5": "^2.2.3", "jszip": "^3.10.1", diff --git a/packages/usdk/tests/commands/version.test.js b/packages/usdk/tests/commands/version.test.js new file mode 100644 index 000000000..4a3983975 --- /dev/null +++ b/packages/usdk/tests/commands/version.test.js @@ -0,0 +1,22 @@ +import { version } from '../../lib/version.mjs'; +import { readFile } from 'fs/promises'; +import { fileURLToPath } from 'url'; +import { dirname, resolve } from 'path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +describe('usdk version command', () => { + it('should return a valid version string', async () => { + const ver = version(); + expect(typeof ver).toBe('string'); + expect(ver).toMatch(/^\d+\.\d+\.\d+/); + }); + + it('should match the package.json version', async () => { + const ver = version(); + const packageJson = JSON.parse( + await readFile(resolve(__dirname, '../../package.json'), 'utf8') + ); + expect(ver).toBe(packageJson.version); + }); +}); \ No newline at end of file From f1cc4a2458bca7aaf7bb0ad88189f50221519c82 Mon Sep 17 00:00:00 2001 From: Abdurrehman Subhani Date: Wed, 1 Jan 2025 18:13:52 +0500 Subject: [PATCH 2/8] add login tests, invalid base64 code failing --- packages/usdk/tests/commands/login.test.js | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/usdk/tests/commands/login.test.js diff --git a/packages/usdk/tests/commands/login.test.js b/packages/usdk/tests/commands/login.test.js new file mode 100644 index 000000000..b4438c06c --- /dev/null +++ b/packages/usdk/tests/commands/login.test.js @@ -0,0 +1,42 @@ +import { login } from '../../lib/login.mjs'; +import { jest } from '@jest/globals'; + +describe('login', () => { + + let consoleSpy; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'warn'); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + it('should successfully login when valid code parameter is provided', async () => { + const mockLoginData = { + id: 'test-id', + jwt: 'test-jwt' + }; + const encodedData = Buffer.from(JSON.stringify(mockLoginData)).toString('base64'); + + const result = await login({ code: encodedData }); + + expect(result).toEqual({ + id: 'test-id', + jwt: 'test-jwt' + }); + }); + + it('should reject when invalid base64 code is provided', async () => { + const invalidCode = 'invalid-base64-!@#$'; + + await expect(async () => { + await login({ code: invalidCode }); + }).rejects.toThrow(); + + expect(consoleSpy).toHaveBeenCalled(); + + consoleSpy.mockRestore(); + }); +}); From 420497d0ffa1404fad168337e4e0655f7c8be6f2 Mon Sep 17 00:00:00 2001 From: Abdurrehman Subhani Date: Wed, 1 Jan 2025 18:37:41 +0500 Subject: [PATCH 3/8] remove consolespy for invalid login code --- packages/usdk/tests/commands/login.test.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/usdk/tests/commands/login.test.js b/packages/usdk/tests/commands/login.test.js index b4438c06c..f62bf5372 100644 --- a/packages/usdk/tests/commands/login.test.js +++ b/packages/usdk/tests/commands/login.test.js @@ -3,16 +3,6 @@ import { jest } from '@jest/globals'; describe('login', () => { - let consoleSpy; - - beforeEach(() => { - consoleSpy = jest.spyOn(console, 'warn'); - }); - - afterEach(() => { - consoleSpy.mockRestore(); - }); - it('should successfully login when valid code parameter is provided', async () => { const mockLoginData = { id: 'test-id', @@ -30,13 +20,9 @@ describe('login', () => { it('should reject when invalid base64 code is provided', async () => { const invalidCode = 'invalid-base64-!@#$'; - + await expect(async () => { await login({ code: invalidCode }); - }).rejects.toThrow(); - - expect(consoleSpy).toHaveBeenCalled(); - - consoleSpy.mockRestore(); + }).rejects.toThrow(SyntaxError); }); }); From 3876292ad3c6661d5458d9d3c9ca472e95c4d55d Mon Sep 17 00:00:00 2001 From: Abdurrehman Subhani Date: Wed, 1 Jan 2025 18:44:55 +0500 Subject: [PATCH 4/8] replace run usdk command with run tests --- .github/workflows/usdk-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/usdk-test.yml b/.github/workflows/usdk-test.yml index 34ad65d80..3b0492d7c 100644 --- a/.github/workflows/usdk-test.yml +++ b/.github/workflows/usdk-test.yml @@ -26,5 +26,5 @@ jobs: - name: Install dependencies run: cd packages/usdk && pnpm install --no-optional --no-frozen-lockfile - - name: Run USDK command - run: cd packages/usdk && ./usdk.js --version + - name: Run USDK tests + run: cd packages/usdk && pnpm run test \ No newline at end of file From 04e5c4ad95008f2fbf1d6ecf2f4a9772379aeb5a Mon Sep 17 00:00:00 2001 From: Abdurrehman Subhani Date: Wed, 1 Jan 2025 21:06:37 +0500 Subject: [PATCH 5/8] add logout command tests --- packages/usdk/tests/commands/logout.test.js | 48 +++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/usdk/tests/commands/logout.test.js diff --git a/packages/usdk/tests/commands/logout.test.js b/packages/usdk/tests/commands/logout.test.js new file mode 100644 index 000000000..208e58e97 --- /dev/null +++ b/packages/usdk/tests/commands/logout.test.js @@ -0,0 +1,48 @@ +import { jest } from '@jest/globals'; + +// We need to mock before importing the module that uses it +const mockGetLoginJwt = jest.fn(); +jest.unstable_mockModule('../../util/login-util.mjs', () => ({ + getLoginJwt: mockGetLoginJwt +})); + +// Import after mocking +const { logout } = await import('../../lib/login.mjs'); + +describe('logout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return true when jwt exists', async () => { + mockGetLoginJwt.mockResolvedValue('mock-jwt-token'); + + const result = await logout({}); + + expect(result).toBe(true); + expect(mockGetLoginJwt).toHaveBeenCalledTimes(1); + }); + + it('should return false when jwt is null', async () => { + mockGetLoginJwt.mockResolvedValue(null); + + const result = await logout({}); + + expect(result).toBe(false); + expect(mockGetLoginJwt).toHaveBeenCalledTimes(1); + }); + + it('should return false when jwt is undefined', async () => { + const result = await logout({}); + + expect(result).toBe(false); + expect(mockGetLoginJwt).toHaveBeenCalledTimes(1); + }); + + it('should handle rejected jwt promise', async () => { + mockGetLoginJwt.mockRejectedValue(new Error('Failed to get JWT')); + + await expect(logout({})).rejects.toThrow('Failed to get JWT'); + expect(mockGetLoginJwt).toHaveBeenCalledTimes(1); + }); +}); \ No newline at end of file From ea4f4735b200c4de5e7729dd53b3c8f15c6d2a9c Mon Sep 17 00:00:00 2001 From: Abdurrehman Subhani Date: Wed, 1 Jan 2025 21:08:21 +0500 Subject: [PATCH 6/8] add empty jwt case --- packages/usdk/tests/commands/logout.test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/usdk/tests/commands/logout.test.js b/packages/usdk/tests/commands/logout.test.js index 208e58e97..5b027b81e 100644 --- a/packages/usdk/tests/commands/logout.test.js +++ b/packages/usdk/tests/commands/logout.test.js @@ -23,6 +23,15 @@ describe('logout', () => { expect(mockGetLoginJwt).toHaveBeenCalledTimes(1); }); + it('should return false when jwt is empty', async () => { + mockGetLoginJwt.mockResolvedValue(''); + + const result = await logout({}); + + expect(result).toBe(false); + expect(mockGetLoginJwt).toHaveBeenCalledTimes(1); + }); + it('should return false when jwt is null', async () => { mockGetLoginJwt.mockResolvedValue(null); From 4582c52aaa283046a326b90b55abc639dd43e990 Mon Sep 17 00:00:00 2001 From: Abdurrehman Subhani Date: Wed, 1 Jan 2025 21:19:41 +0500 Subject: [PATCH 7/8] add status command test cases --- packages/usdk/tests/commands/status.test.js | 114 ++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 packages/usdk/tests/commands/status.test.js diff --git a/packages/usdk/tests/commands/status.test.js b/packages/usdk/tests/commands/status.test.js new file mode 100644 index 000000000..a16394598 --- /dev/null +++ b/packages/usdk/tests/commands/status.test.js @@ -0,0 +1,114 @@ +import { jest } from '@jest/globals'; + +// Mock dependencies before importing the module that uses them +const mockGetUserIdForJwt = jest.fn(); +const mockSupabaseStorage = jest.fn(); + +jest.unstable_mockModule('../../packages/upstreet-agent/packages/react-agents/util/jwt-utils.mjs', () => ({ + getUserIdForJwt: mockGetUserIdForJwt +})); + +jest.unstable_mockModule('../../packages/upstreet-agent/packages/react-agents/storage/supabase-storage.mjs', () => ({ + SupabaseStorage: mockSupabaseStorage +})); + +// Import after mocking +const { status } = await import('../../lib/status.mjs'); + +describe('status', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('throws error when no JWT provided', async () => { + await expect(status({}, {})).rejects.toThrow('not logged in'); + }); + + test('returns user and wearing data when user has active asset', async () => { + const mockJwt = 'mock-jwt'; + const mockUserId = 'user-123'; + const mockUserData = { + id: mockUserId, + active_asset: 'asset-123', + }; + const mockAssetData = { + id: 'asset-123', + type: 'npc', + name: 'Test Avatar', + }; + + mockGetUserIdForJwt.mockResolvedValue(mockUserId); + + const mockSupabase = { + from: jest.fn().mockReturnThis(), + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + maybeSingle: jest.fn(), + }; + + mockSupabaseStorage.mockImplementation(() => mockSupabase); + + mockSupabase.maybeSingle + .mockResolvedValueOnce({ error: null, data: mockUserData }) + .mockResolvedValueOnce({ error: null, data: mockAssetData }); + + const result = await status({}, { jwt: mockJwt }); + + expect(result).toEqual({ + user: mockUserData, + wearing: mockAssetData, + }); + expect(mockGetUserIdForJwt).toHaveBeenCalledWith(mockJwt); + expect(mockSupabaseStorage).toHaveBeenCalledWith({ jwt: mockJwt }); + }); + + test('returns only user data when user has no active asset', async () => { + const mockJwt = 'mock-jwt'; + const mockUserId = 'user-123'; + const mockUserData = { + id: mockUserId, + active_asset: null, + }; + + mockGetUserIdForJwt.mockResolvedValue(mockUserId); + + const mockSupabase = { + from: jest.fn().mockReturnThis(), + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + maybeSingle: jest.fn(), + }; + + mockSupabaseStorage.mockImplementation(() => mockSupabase); + mockSupabase.maybeSingle.mockResolvedValueOnce({ error: null, data: mockUserData }); + + const result = await status({}, { jwt: mockJwt }); + + expect(result).toEqual({ + user: mockUserData, + wearing: null, + }); + }); + + test('throws error when account query fails', async () => { + const mockJwt = 'mock-jwt'; + const mockUserId = 'user-123'; + const mockError = 'Database error'; + + mockGetUserIdForJwt.mockResolvedValue(mockUserId); + + const mockSupabase = { + from: jest.fn().mockReturnThis(), + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + maybeSingle: jest.fn(), + }; + + mockSupabaseStorage.mockImplementation(() => mockSupabase); + mockSupabase.maybeSingle.mockResolvedValueOnce({ error: mockError, data: null }); + + await expect(status({}, { jwt: mockJwt })) + .rejects + .toThrow(`could not get account ${mockUserId}: ${mockError}`); + }); +}); \ No newline at end of file From 417d6e5811f1752d272bfe76592222f8e7b3425b Mon Sep 17 00:00:00 2001 From: Abdurrehman Subhani Date: Wed, 1 Jan 2025 21:26:11 +0500 Subject: [PATCH 8/8] add pull command tests --- packages/usdk/tests/commands/pull.test.js | 143 ++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 packages/usdk/tests/commands/pull.test.js diff --git a/packages/usdk/tests/commands/pull.test.js b/packages/usdk/tests/commands/pull.test.js new file mode 100644 index 000000000..6eb8d3410 --- /dev/null +++ b/packages/usdk/tests/commands/pull.test.js @@ -0,0 +1,143 @@ +import { jest } from '@jest/globals'; + +// Mock all dependencies before importing modules +const mockGetUserIdForJwt = jest.fn(); +const mockCleanDir = jest.fn(); +const mockExtractZip = jest.fn(); +const mockNpmInstall = jest.fn(); +const mockFetch = jest.fn(); + +jest.unstable_mockModule('../../packages/upstreet-agent/packages/react-agents/util/jwt-utils.mjs', () => ({ + getUserIdForJwt: mockGetUserIdForJwt +})); + +jest.unstable_mockModule('../../lib/directory-util.mjs', () => ({ + cleanDir: mockCleanDir +})); + +jest.unstable_mockModule('../../lib/zip-util.mjs', () => ({ + extractZip: mockExtractZip +})); + +jest.unstable_mockModule('../../lib/npm-util.mjs', () => ({ + npmInstall: mockNpmInstall +})); + +// Import modules after mocking +const { pull } = await import('../../lib/pull.mjs'); +const { aiProxyHost } = await import('../../packages/upstreet-agent/packages/react-agents/util/endpoints.mjs'); + +describe('pull command', () => { + // Setup and teardown + beforeEach(() => { + // Clear all mocks before each test + jest.clearAllMocks(); + global.fetch = mockFetch; + global.console.log = jest.fn(); + global.console.warn = jest.fn(); + }); + + test('should throw error if not logged in', async () => { + const args = { _: ['agentId'] }; + const opts = { jwt: null }; + + await expect(pull(args, opts)).rejects.toThrow('You must be logged in to pull.'); + }); + + test('should handle successful pull with existing directory', async () => { + // Mock setup + mockGetUserIdForJwt.mockResolvedValue('testUserId'); + mockFetch.mockResolvedValue({ + ok: true, + arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)), + }); + + const args = { + _: ['agentId', '/test/dir'], + force: true, + forceNoConfirm: true, + }; + const opts = { jwt: 'testJwt' }; + + await pull(args, opts); + + // Verify calls + expect(mockCleanDir).toHaveBeenCalledWith('/test/dir', { + force: true, + forceNoConfirm: true, + }); + expect(mockExtractZip).toHaveBeenCalled(); + expect(mockNpmInstall).toHaveBeenCalledWith('/test/dir'); + expect(mockFetch).toHaveBeenCalledWith( + `https://${aiProxyHost}/agents/agentId/source`, + { + headers: { + Authorization: 'Bearer testJwt', + }, + } + ); + }); + + test('should skip npm install when noInstall flag is true', async () => { + // Mock setup + mockGetUserIdForJwt.mockResolvedValue('testUserId'); + mockFetch.mockResolvedValue({ + ok: true, + arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)), + }); + + const args = { + _: ['agentId', '/test/dir'], + noInstall: true, + }; + const opts = { jwt: 'testJwt' }; + + await pull(args, opts); + + expect(mockNpmInstall).not.toHaveBeenCalled(); + }); + + test('should handle failed fetch request', async () => { + // Mock setup + mockGetUserIdForJwt.mockResolvedValue('testUserId'); + mockFetch.mockResolvedValue({ + ok: false, + text: () => Promise.resolve('Error message'), + }); + const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); + + const args = { _: ['agentId', '/test/dir'] }; + const opts = { jwt: 'testJwt' }; + + await pull(args, opts); + + expect(console.warn).toHaveBeenCalledWith('pull request error', 'Error message'); + expect(mockExit).toHaveBeenCalledWith(1); + + mockExit.mockRestore(); + }); + + test('should handle events dispatch', async () => { + // Mock setup + mockGetUserIdForJwt.mockResolvedValue('testUserId'); + mockFetch.mockResolvedValue({ + ok: true, + arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)), + }); + + const mockDispatchEvent = jest.fn(); + const args = { + _: ['agentId', '/test/dir'], + events: { + dispatchEvent: mockDispatchEvent, + }, + }; + const opts = { jwt: 'testJwt' }; + + await pull(args, opts); + + expect(mockDispatchEvent).toHaveBeenCalledWith( + expect.any(MessageEvent) + ); + }); +}); \ No newline at end of file