Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
# → 404 class of bug. Static, fast.
# - lint : eslint — catches the undefined-identifier class (e.g.
# the `IntegrationNote` bug that once landed on main).
# - test:unit : Jest unit suite (--watchAll=false).
# - test:unit : Vitest unit suite.
# (Playwright/E2E stays a separate post-build gate below — it needs a
# running server + secrets, so it isn't part of the static verify bundle.)
run: npm run verify
Expand Down
12 changes: 6 additions & 6 deletions __mocks__/next-navigation.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Simple Jest mock for next/navigation
module.exports = {
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
prefetch: jest.fn(),
back: jest.fn(),
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
back: vi.fn(),
}),
useSearchParams: () => ({
get: jest.fn(),
get: vi.fn(),
}),
usePathname: jest.fn(() => '/'),
usePathname: vi.fn(() => '/'),
};
6 changes: 3 additions & 3 deletions __mocks__/next-server.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module.exports = {
NextRequest: class NextRequest {},
NextResponse: {
json: jest.fn(),
redirect: jest.fn(),
next: jest.fn(),
json: vi.fn(),
redirect: vi.fn(),
next: vi.fn(),
},
};
30 changes: 22 additions & 8 deletions __mocks__/nostr-nwc.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
// Mock for @/lib/nostr/nwc — prevents nostr-tools ESM/CJS issues in Jest.
// NWCClient is mocked with jest.fn() stubs; real NWC tests use integration environment.
// NWCClient is mocked with vi.fn() stubs; real NWC tests use integration environment.

class NWCClient {
constructor(_uri) {}
connect() { return Promise.resolve(); }
connect() {
return Promise.resolve();
}
disconnect() {}
payInvoice(_bolt11) { return Promise.resolve({ payment_hash: 'mock_hash', invoice: _bolt11 }); }
makeInvoice(_sats, _desc, _expiry) { return Promise.resolve({ invoice: 'lnbc_mock', payment_hash: 'mock_hash' }); }
getBalance() { return Promise.resolve(100000); }
lookupInvoice(_hash) { return Promise.resolve({ payment_hash: _hash }); }
payInvoice(_bolt11) {
return Promise.resolve({ payment_hash: 'mock_hash', invoice: _bolt11 });
}
makeInvoice(_sats, _desc, _expiry) {
return Promise.resolve({ invoice: 'lnbc_mock', payment_hash: 'mock_hash' });
}
getBalance() {
return Promise.resolve(100000);
}
lookupInvoice(_hash) {
return Promise.resolve({ payment_hash: _hash });
}
}

function parseNWCUri(uri) { return { walletPubkey: 'mock', relayUrl: 'wss://mock', secret: 'mock' }; }
function isValidNWCUri(uri) { return typeof uri === 'string' && uri.startsWith('nostr+walletconnect://'); }
function parseNWCUri(uri) {
return { walletPubkey: 'mock', relayUrl: 'wss://mock', secret: 'mock' };
}
function isValidNWCUri(uri) {
return typeof uri === 'string' && uri.startsWith('nostr+walletconnect://');
}

module.exports = { NWCClient, parseNWCUri, isValidNWCUri };
1 change: 0 additions & 1 deletion __mocks__/vitest.js

This file was deleted.

6 changes: 3 additions & 3 deletions __tests__/create/FormField.voice.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { AIFillPanel } from '@/components/create/AIFillPanel';
// Voice input is env-gated behind FEATURES.voiceInput, evaluated at module-load
// time — mock the flag ON so the voice path is exercised regardless of the
// deployment default.
jest.mock('@/config/features', () => ({ FEATURES: { voiceInput: true } }));
vi.mock('@/config/features', () => ({ FEATURES: { voiceInput: true } }));

jest.mock('@/components/ui/DictationButton', () => ({
vi.mock('@/components/ui/DictationButton', () => ({
__esModule: true,
DictationButton: ({
onTranscript,
Expand Down Expand Up @@ -52,7 +52,7 @@ describe('voice placement', () => {
});

it('renders ONE mic in the AI fill panel, appending to the description', () => {
const onDescriptionChange = jest.fn();
const onDescriptionChange = vi.fn();
render(
<AIFillPanel
description="sell my bike"
Expand Down
73 changes: 38 additions & 35 deletions __tests__/smoke/auth-login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,63 +6,66 @@
* This is a lightweight guard for “fast login” regressions.
*/

import { redirect } from 'next/navigation'
import { redirect } from 'next/navigation';

import type { Mock } from 'vitest';

describe('auth smoke', () => {
beforeEach(() => {
jest.resetModules()
})
vi.resetModules();
});

it('renders public home when not authenticated', async () => {
await jest.isolateModulesAsync(async () => {
jest.doMock('next/navigation', () => ({
redirect: jest.fn(),
}))
jest.doMock('@/lib/supabase/server', () => ({
// jest.isolateModulesAsync has no vitest equivalent; beforeEach's
// vi.resetModules() plus vi.doMock give each test a fresh module graph.
await (async () => {
vi.doMock('next/navigation', () => ({
redirect: vi.fn(),
}));
vi.doMock('@/lib/supabase/server', () => ({
createServerClient: () =>
Promise.resolve({
auth: {
getUser: jest.fn().mockResolvedValue({ data: { user: null } }),
getUser: vi.fn().mockResolvedValue({ data: { user: null } }),
},
}),
}))
}));

const { default: Home } = await import('@/app/page')
await Home()
const { redirect } = await import('next/navigation')
expect((redirect as jest.Mock)).not.toHaveBeenCalled()
})
})
const { default: Home } = await import('@/app/page');
await Home();
const { redirect } = await import('next/navigation');
expect(redirect as Mock).not.toHaveBeenCalled();
})();
});

it('redirects to dashboard when authenticated', async () => {
await jest.isolateModulesAsync(async () => {
jest.doMock('next/navigation', () => ({
redirect: jest.fn(),
}))
jest.doMock('@/lib/supabase/server', () => ({
await (async () => {
vi.doMock('next/navigation', () => ({
redirect: vi.fn(),
}));
vi.doMock('@/lib/supabase/server', () => ({
createServerClient: () =>
Promise.resolve({
auth: {
getUser: jest.fn().mockResolvedValue({ data: { user: { id: '123' } } }),
getUser: vi.fn().mockResolvedValue({ data: { user: { id: '123' } } }),
},
from: jest.fn().mockReturnValue({
select: jest.fn().mockReturnValue({
eq: jest.fn().mockReturnValue({
single: jest.fn().mockResolvedValue({
from: vi.fn().mockReturnValue({
select: vi.fn().mockReturnValue({
eq: vi.fn().mockReturnValue({
single: vi.fn().mockResolvedValue({
data: { onboarding_completed: true },
error: null,
}),
}),
}),
}),
}),
}))

const { default: Home } = await import('@/app/page')
await Home()
const { redirect } = await import('next/navigation')
expect((redirect as jest.Mock)).toHaveBeenCalled()
})
})
})
}));

const { default: Home } = await import('@/app/page');
await Home();
const { redirect } = await import('next/navigation');
expect(redirect as Mock).toHaveBeenCalled();
})();
});
});
6 changes: 3 additions & 3 deletions __tests__/unit/ai/link-health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ const CHAIN = [
describe('link-health', () => {
beforeEach(() => {
resetLinkHealth();
jest.useFakeTimers();
vi.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
vi.useRealTimers();
});

it('marks a link down and prunes it from the chain', () => {
Expand All @@ -34,7 +34,7 @@ describe('link-health', () => {

it('expires the down-mark after the TTL', () => {
markLinkDown('groq', 'llama-3.3-70b-versatile', 1000);
jest.advanceTimersByTime(1001);
vi.advanceTimersByTime(1001);
expect(isLinkDown('groq', 'llama-3.3-70b-versatile')).toBe(false);
expect(pruneDownLinks(CHAIN, l => l)).toHaveLength(3);
});
Expand Down
30 changes: 16 additions & 14 deletions __tests__/unit/api/ai-form-prefill-input-length.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,41 @@ import { POST } from '@/app/api/ai/form-prefill/route';
import { generateFormPrefill } from '@/lib/ai/form-prefill-service';
import { AI_ASSIST_MIN_INPUT_LENGTH, AI_ADJUSTMENTS } from '@/config/ai-form-assist';

jest.mock('@/utils/logger', () => ({
logger: { warn: jest.fn(), error: jest.fn(), info: jest.fn(), debug: jest.fn() },
import type { MockedFunction } from 'vitest';

vi.mock('@/utils/logger', () => ({
logger: { warn: vi.fn(), error: vi.fn(), info: vi.fn(), debug: vi.fn() },
}));

// withAuth wraps the handler in a real Supabase session lookup; the request
// itself is what this suite is about, so authentication is stubbed through.
jest.mock('@/lib/api/withAuth', () => ({
vi.mock('@/lib/api/withAuth', () => ({
withAuth:
(handler: (req: unknown) => Promise<unknown>) =>
(req: unknown): Promise<unknown> =>
handler(Object.assign(req as object, { user: { id: 'test-user' }, supabase: {} })),
}));

jest.mock('@/lib/rate-limit', () => ({
rateLimitWriteAsync: jest.fn().mockResolvedValue({ success: true }),
vi.mock('@/lib/rate-limit', () => ({
rateLimitWriteAsync: vi.fn().mockResolvedValue({ success: true }),
retryAfterSeconds: () => 0,
}));

// The response helpers wrap NextResponse, which needs the Next runtime; the
// established convention in route tests is to stub them.
jest.mock('@/lib/api/standardResponse', () => ({
apiSuccess: jest.fn((data: unknown) => ({ status: 200, body: { success: true, data } })),
apiValidationError: jest.fn((error: string) => ({ status: 400, error })),
apiBadRequest: jest.fn((error: string) => ({ status: 400, error })),
apiRateLimited: jest.fn(() => ({ status: 429 })),
apiInternalError: jest.fn(() => ({ status: 500 })),
vi.mock('@/lib/api/standardResponse', () => ({
apiSuccess: vi.fn((data: unknown) => ({ status: 200, body: { success: true, data } })),
apiValidationError: vi.fn((error: string) => ({ status: 400, error })),
apiBadRequest: vi.fn((error: string) => ({ status: 400, error })),
apiRateLimited: vi.fn(() => ({ status: 429 })),
apiInternalError: vi.fn(() => ({ status: 500 })),
}));

jest.mock('@/lib/ai/form-prefill-service', () => ({
generateFormPrefill: jest.fn(),
vi.mock('@/lib/ai/form-prefill-service', () => ({
generateFormPrefill: vi.fn(),
}));

const mockedPrefill = generateFormPrefill as jest.MockedFunction<typeof generateFormPrefill>;
const mockedPrefill = generateFormPrefill as MockedFunction<typeof generateFormPrefill>;

const BASE = {
entityType: 'service',
Expand Down
18 changes: 10 additions & 8 deletions __tests__/unit/api/entity-delete-wallet-cleanup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@
* someone's money built on rows that outlived their subject.
*/

const mockLinkDelete = jest.fn();
const mockAdminFrom = jest.fn();
const mockLinkDelete = vi.fn();
const mockAdminFrom = vi.fn();

jest.mock('@/utils/logger', () => ({
logger: { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() },
vi.mock('@/utils/logger', () => ({
logger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
}));

jest.mock('@/lib/supabase/admin', () => ({
vi.mock('@/lib/supabase/admin', () => ({
getAdminClient: () => ({ from: (t: string) => mockAdminFrom(t) }),
}));

import { DATABASE_TABLES } from '@/config/database-tables';

import type { Mock } from 'vitest';

/**
* The handler is a thick closure over Next request plumbing; this exercises the
* cleanup contract directly against the same admin-client shape it uses.
Expand All @@ -37,11 +39,11 @@ beforeEach(() => {
mockLinkDelete.mockReset();
mockAdminFrom.mockReset();
const eqChain = {
eq: jest.fn(function (this: unknown) {
eq: vi.fn(function (this: unknown) {
return eqChain;
}),
then: undefined,
} as never as { eq: jest.Mock };
} as never as { eq: Mock };
mockLinkDelete.mockReturnValue(eqChain);
mockAdminFrom.mockReturnValue({ delete: mockLinkDelete });
});
Expand All @@ -50,7 +52,7 @@ describe('entity delete → entity_wallets cleanup', () => {
it('targets the entity_wallets table scoped to BOTH type and id', async () => {
const eqCalls: Array<[string, string]> = [];
const chain: Record<string, unknown> = {};
chain.eq = jest.fn((col: string, val: string) => {
chain.eq = vi.fn((col: string, val: string) => {
eqCalls.push([col, val]);
return chain;
});
Expand Down
Loading
Loading