diff --git a/jest.config.js b/jest.config.js index c6d5b34d..01c49024 100644 --- a/jest.config.js +++ b/jest.config.js @@ -45,6 +45,7 @@ export default { '^figures$': '/tests/__mocks__/figures.ts', '^is-unicode-supported$': '/tests/__mocks__/is-unicode-supported.ts', '^conf$': '/tests/__mocks__/conf.ts', + '^signal-exit$': '/tests/__mocks__/signal-exit.ts', }, // Transform configuration diff --git a/tests/__mocks__/signal-exit.ts b/tests/__mocks__/signal-exit.ts index 45368b1f..679476f4 100644 --- a/tests/__mocks__/signal-exit.ts +++ b/tests/__mocks__/signal-exit.ts @@ -6,5 +6,6 @@ const noop = () => () => {}; export default noop; export const onExit = noop; - - +export const load = () => {}; +export const unload = () => {}; +export const signals: string[] = []; diff --git a/tests/__tests__/commands/devbox/create-mcp.test.ts b/tests/__tests__/commands/devbox/create-mcp.test.ts index 9930abd0..e12b8816 100644 --- a/tests/__tests__/commands/devbox/create-mcp.test.ts +++ b/tests/__tests__/commands/devbox/create-mcp.test.ts @@ -35,12 +35,12 @@ describe("createDevbox --mcp flag", () => { const { createDevbox } = await import("@/commands/devbox/create.js"); await createDevbox({ - mcp: ["github-readonly,my_secret"], + mcp: ["GH_TOKEN=github-readonly,my_secret"], }); expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ - mcp: [{ mcp_config: "github-readonly", secret: "my_secret" }], + mcp: { GH_TOKEN: { mcp_config: "github-readonly", secret: "my_secret" } }, }), ); expect(console.log).toHaveBeenCalledWith("dbx_mcp_test"); @@ -52,15 +52,15 @@ describe("createDevbox --mcp flag", () => { const { createDevbox } = await import("@/commands/devbox/create.js"); await createDevbox({ - mcp: ["github-readonly,secret1", "jira-config,secret2"], + mcp: ["GH_TOKEN=github-readonly,secret1", "JIRA_TOKEN=jira-config,secret2"], }); expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ - mcp: [ - { mcp_config: "github-readonly", secret: "secret1" }, - { mcp_config: "jira-config", secret: "secret2" }, - ], + mcp: { + GH_TOKEN: { mcp_config: "github-readonly", secret: "secret1" }, + JIRA_TOKEN: { mcp_config: "jira-config", secret: "secret2" }, + }, }), ); }); @@ -76,10 +76,10 @@ describe("createDevbox --mcp flag", () => { expect(createArg.mcp).toBeUndefined(); }); - it("should report error for invalid MCP spec format (missing comma)", async () => { + it("should report error for invalid MCP spec format (missing equals)", async () => { const { createDevbox } = await import("@/commands/devbox/create.js"); await createDevbox({ - mcp: ["invalid-no-comma"], + mcp: ["invalid-no-equals"], }); expect(mockOutputError).toHaveBeenCalledWith( @@ -96,15 +96,15 @@ describe("createDevbox --mcp flag", () => { const { createDevbox } = await import("@/commands/devbox/create.js"); await createDevbox({ name: "my-devbox", - mcp: ["github-readonly,my_secret"], + mcp: ["GH_TOKEN=github-readonly,my_secret"], blueprint: "my-blueprint", }); const createArg = mockCreate.mock.calls[0][0] as Record; expect(createArg.name).toBe("my-devbox"); expect(createArg.blueprint_name).toBe("my-blueprint"); - expect(createArg.mcp).toEqual([ - { mcp_config: "github-readonly", secret: "my_secret" }, - ]); + expect(createArg.mcp).toEqual({ + GH_TOKEN: { mcp_config: "github-readonly", secret: "my_secret" }, + }); }); }); diff --git a/tests/__tests__/components/UpdateNotification.test.tsx b/tests/__tests__/components/UpdateNotification.test.tsx index 7984f6cd..ad92159b 100644 --- a/tests/__tests__/components/UpdateNotification.test.tsx +++ b/tests/__tests__/components/UpdateNotification.test.tsx @@ -1,17 +1,32 @@ /** - * Tests for UpdateNotification component + * Tests for UpdateNotification component. + * + * The component uses the useUpdateCheck hook which calls global.fetch + * internally. We mock fetch to control update-check responses. + * + * Note: the setup-components.ts mocks for useUpdateCheck and + * UpdateNotification don't apply here due to module path resolution + * differences (.ts vs .js mapping), so the real component and hook run. */ import React from 'react'; import { jest } from '@jest/globals'; import { render } from 'ink-testing-library'; import { UpdateNotification } from '../../../src/components/UpdateNotification.js'; -// Mock fetch -global.fetch = jest.fn() as jest.Mock; +// Helper: wait for async state updates to propagate +function waitForUpdates(ms = 150): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} describe('UpdateNotification', () => { + const originalFetch = global.fetch; + beforeEach(() => { - jest.clearAllMocks(); + global.fetch = jest.fn() as jest.Mock; + }); + + afterEach(() => { + global.fetch = originalFetch; }); it('renders without crashing', () => { @@ -19,59 +34,51 @@ describe('UpdateNotification', () => { ok: true, json: async () => ({ version: '0.1.0' }), }); - + const { lastFrame } = render(); expect(lastFrame()).toBeDefined(); }); it('shows nothing while checking', () => { - (global.fetch as jest.Mock).mockImplementation(() => - new Promise(() => {}) // Never resolves + (global.fetch as jest.Mock).mockImplementation( + () => new Promise(() => {}), // Never resolves ); - + const { lastFrame } = render(); - // Should be empty while checking expect(lastFrame()).toBe(''); }); it('shows nothing when on latest version', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, - json: async () => ({ version: '0.1.0' }), // Same as current + json: async () => ({ version: '0.0.1' }), // Older than current }); - + const { lastFrame } = render(); - - // Wait for effect to run - await new Promise((resolve) => setTimeout(resolve, 50)); - + await waitForUpdates(); expect(lastFrame()).toBe(''); }); it('shows nothing on fetch error', async () => { - (global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error')); - + (global.fetch as jest.Mock).mockRejectedValueOnce( + new Error('Network error'), + ); + const { lastFrame } = render(); - - // Wait for effect to run - await new Promise((resolve) => setTimeout(resolve, 50)); - + await waitForUpdates(); expect(lastFrame()).toBe(''); }); it('shows update notification when newer version available', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, - json: async () => ({ version: '99.99.99' }), // Much higher version + json: async () => ({ version: '99.99.99' }), }); - + const { lastFrame } = render(); - - // Wait for effect to run - await new Promise((resolve) => setTimeout(resolve, 100)); - + await waitForUpdates(); + const frame = lastFrame() || ''; - // Should show update notification expect(frame).toContain('Update available'); expect(frame).toContain('99.99.99'); }); @@ -81,24 +88,20 @@ describe('UpdateNotification', () => { ok: true, json: async () => ({ version: '99.99.99' }), }); - + const { lastFrame } = render(); - - await new Promise((resolve) => setTimeout(resolve, 100)); - + await waitForUpdates(); expect(lastFrame()).toContain('npm i -g @runloop/rl-cli@latest'); }); - it('handles non-ok response', async () => { + it('shows nothing on non-ok response', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: false, status: 404, }); - + const { lastFrame } = render(); - - await new Promise((resolve) => setTimeout(resolve, 50)); - + await waitForUpdates(); expect(lastFrame()).toBe(''); }); });