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
52 changes: 52 additions & 0 deletions tests/evals/graders/__tests__/react-provider-mount.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { findMountedAuthKitProvider } from '../react.grader.js';

const PROVIDER = `import { AuthKitProvider } from '@workos-inc/authkit-react';
export function AuthProvider({ children }) {
return <AuthKitProvider clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}>{children}</AuthKitProvider>;
}
`;

describe('findMountedAuthKitProvider', () => {
let workDir: string;

beforeEach(async () => {
workDir = await mkdtemp(join(tmpdir(), 'react-grader-'));
await mkdir(join(workDir, 'src/auth'), { recursive: true });
});

afterEach(async () => {
await rm(workDir, { recursive: true, force: true });
});

it('accepts AuthKitProvider in the entry file', async () => {
await writeFile(join(workDir, 'src/main.tsx'), PROVIDER);
expect(await findMountedAuthKitProvider(workDir)).toBe('src/main.tsx');
});

it('accepts a provider module imported by the entry file', async () => {
await writeFile(join(workDir, 'src/auth/AuthProvider.tsx'), PROVIDER);
await writeFile(join(workDir, 'src/main.tsx'), `import { AuthProvider } from './auth/AuthProvider';\n`);
expect(await findMountedAuthKitProvider(workDir)).toBe('src/auth/AuthProvider.tsx');
});

it('accepts a provider module imported via the @/ alias from another component', async () => {
await writeFile(join(workDir, 'src/auth/index.tsx'), PROVIDER);
await writeFile(join(workDir, 'src/App.tsx'), `import { AuthProvider } from '@/auth';\n`);
expect(await findMountedAuthKitProvider(workDir)).toBe('src/auth/index.tsx');
});

it('rejects a provider module nothing imports', async () => {
await writeFile(join(workDir, 'src/auth/AuthProvider.tsx'), PROVIDER);
await writeFile(join(workDir, 'src/main.tsx'), `import App from './App';\n`);
expect(await findMountedAuthKitProvider(workDir)).toBeNull();
});

it('rejects a provider from a different SDK', async () => {
await writeFile(join(workDir, 'src/main.tsx'), `import { AuthKitProvider } from '@workos-inc/authkit-js';\n`);
expect(await findMountedAuthKitProvider(workDir)).toBeNull();
});
});
6 changes: 3 additions & 3 deletions tests/evals/graders/nextjs.grader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,11 @@ export class NextjsGrader implements Grader {
const sdkImportChecks = await this.fileGrader.checkFileContains(middlewareFile, ['@workos-inc/authkit-nextjs']);
checks.push(...sdkImportChecks);

// Check for authkit integration: authkitMiddleware OR (authkit + handleAuthkitHeaders)
// Check for authkit integration: authkitMiddleware OR (authkit + handleAuthkitProxy, or its deprecated alias handleAuthkitHeaders)
const middlewareChecks = await this.fileGrader.checkFileContains(middlewareFile, ['authkitMiddleware']);
const composableChecks = await this.fileGrader.checkFileContains(middlewareFile, [
'authkit(',
'handleAuthkitHeaders',
/handleAuthkit(Proxy|Headers)\b/,
]);

const usesAuthkitMiddleware = middlewareChecks.every((c) => c.passed);
Expand All @@ -129,7 +129,7 @@ export class NextjsGrader implements Grader {
message: usesAuthkitMiddleware
? 'Uses authkitMiddleware'
: usesComposable
? 'Uses authkit() composable with handleAuthkitHeaders'
? 'Uses authkit() composable with handleAuthkitProxy'
: 'Missing authkitMiddleware or authkit() composable integration',
};
checks.push(authkitCheck);
Expand Down
64 changes: 50 additions & 14 deletions tests/evals/graders/react.grader.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import fg from 'fast-glob';
import { readFile } from 'node:fs/promises';
import { dirname, join, relative, resolve } from 'node:path';
import { FileGrader } from './file-grader.js';
import { BuildGrader } from './build-grader.js';
import type { Grader, GradeResult, GradeCheck } from '../types.js';
Expand All @@ -18,23 +21,23 @@ export class ReactGrader implements Grader {
private fileGrader: FileGrader;
private buildGrader: BuildGrader;

constructor(workDir: string) {
constructor(private workDir: string) {
this.fileGrader = new FileGrader(workDir);
this.buildGrader = new BuildGrader(workDir);
}

async grade(): Promise<GradeResult> {
const checks: GradeCheck[] = [];

// Check AuthKitProvider wrapper exists in entry file
// Can be main.tsx (Vite) or index.tsx (CRA)
checks.push(
await this.fileGrader.checkFileWithPattern(
'src/{main,index}.tsx',
['AuthKitProvider', '@workos-inc/authkit-react'],
'AuthKitProvider configured with correct SDK',
),
);
// AuthKitProvider may live in the entry file or an app-owned provider module, but it must be mounted
const provider = await findMountedAuthKitProvider(this.workDir);
checks.push({
name: 'AuthKitProvider configured with correct SDK',
passed: provider !== null,
message: provider
? `Found in: ${provider}`
: 'No mounted AuthKitProvider from @workos-inc/authkit-react (entry file or a module imported elsewhere in src/)',
});

// Check useAuth hook usage somewhere in the app
// Can be in App.tsx, pages/, components/, or anywhere
Expand All @@ -46,12 +49,11 @@ export class ReactGrader implements Grader {
),
);

// Check environment config in entry file
// Supports both Vite (import.meta.env) and CRA (process.env)
// Client ID comes from build-time env (Vite or CRA), read anywhere under src/
checks.push(
await this.fileGrader.checkFileWithPattern(
'src/{main,index}.tsx',
[/VITE_WORKOS_CLIENT_ID|REACT_APP_WORKOS_CLIENT_ID|import\.meta\.env|process\.env/],
'src/**/*.{ts,tsx,js,jsx}',
[/(VITE|REACT_APP)_WORKOS_CLIENT_ID/],
'Environment variable configuration',
),
);
Expand All @@ -65,3 +67,37 @@ export class ReactGrader implements Grader {
};
}
}

const SOURCE_EXT = /\.(tsx|ts|jsx|js)$/;
const IMPORT_SPEC = /(?:from\s+|import\s*\(\s*|import\s+)['"]([^'"]+)['"]/g;

/**
* Returns the src-relative file that defines AuthKitProvider when it is mounted:
* either it is the entry file, or another src file imports its module.
*/
export async function findMountedAuthKitProvider(workDir: string): Promise<string | null> {
const files = await fg('src/**/*.{tsx,ts,jsx,js}', { cwd: workDir });
const contents = new Map<string, string>();
for (const file of files) contents.set(file, await readFile(join(workDir, file), 'utf-8'));

const providers = files.filter((f) => {
const c = contents.get(f)!;
return c.includes('AuthKitProvider') && c.includes('@workos-inc/authkit-react');
});

for (const provider of providers) {
if (/^src\/(main|index)\.(tsx|jsx)$/.test(provider)) return provider;
const target = provider.replace(SOURCE_EXT, '').replace(/\/index$/, '');
for (const [importer, content] of contents) {
if (importer === provider) continue;
for (const [, spec] of content.matchAll(IMPORT_SPEC)) {
let resolved: string | null = null;
if (spec.startsWith('.')) resolved = relative(workDir, resolve(workDir, dirname(importer), spec));
else if (spec.startsWith('@/')) resolved = join('src', spec.slice(2));
else if (spec.startsWith('src/')) resolved = spec;
if (resolved && resolved.replace(SOURCE_EXT, '').replace(/\/index$/, '') === target) return provider;
}
}
}
return null;
}
Loading