Skip to content
Closed
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: 2 additions & 0 deletions .changeset/agent-rules-review-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
22 changes: 20 additions & 2 deletions packages/nextjs/src/server/__tests__/agent-rules-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ vi.mock('@clerk/shared/agentRules', async importOriginal => ({
...(await importOriginal<typeof import('@clerk/shared/agentRules')>()),
writeClerkAgentRules: vi.fn(),
}));
vi.mock('../fs/utils', () => ({

const nodeApis = {
nodeCwdOrThrow: () => () => '/app',
nodeFsOrThrow: () => ({}),
nodePathOrThrow: () => ({}),
}));
};

const loadWriteAgentRulesOnce = async () => (await import('../agent-rules-node.js')).writeAgentRulesOnce;

describe('writeAgentRulesOnce', () => {
beforeEach(() => {
vi.resetModules();
vi.doMock('../fs/utils', () => nodeApis);
vi.mocked(writeClerkAgentRules).mockReset();
for (const name of automatedEnvironmentVariables) {
vi.stubEnv(name, '');
Expand All @@ -27,6 +29,7 @@ describe('writeAgentRulesOnce', () => {

afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});

it('writes the rules once per process in development, with or without an AI agent', async () => {
Expand Down Expand Up @@ -63,6 +66,21 @@ describe('writeAgentRulesOnce', () => {
expect(writeClerkAgentRules).not.toHaveBeenCalled();
});

it('skips without a warning on the Edge runtime, where Node APIs are missing', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
vi.doMock('../fs/utils', () => ({
...nodeApis,
nodeCwdOrThrow: () => {
throw new Error("Clerk: cwd is missing. This is an internal error. Please contact Clerk's support.");
},
}));

(await loadWriteAgentRulesOnce())();

expect(writeClerkAgentRules).not.toHaveBeenCalled();
expect(warn).not.toHaveBeenCalled();
});

it('does not throw when writing fails', async () => {
vi.mocked(writeClerkAgentRules).mockImplementation(() => {
throw new Error('EACCES');
Expand Down
46 changes: 42 additions & 4 deletions packages/shared/src/__tests__/agentRules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@ import {
writeClerkAgentRules,
} from '../agentRules';

const createMemoryFs = (files: Record<string, string> = {}, { withRepoPackageJson = true } = {}) => {
const createMemoryFs = (
files: Record<string, string> = {},
{
withRepoPackageJson = true,
symlinks = {},
}: { withRepoPackageJson?: boolean; symlinks?: Record<string, string> } = {},
) => {
const store = new Map(Object.entries(withRepoPackageJson ? { '/repo/package.json': '{}', ...files } : files));
const dirs = new Set<string>();
const follow = (path: string) => symlinks[path] ?? path;

const fs: FileSystemAdapter = {
existsSync: path => store.has(path) || dirs.has(path),
readFileSync: path => store.get(path) ?? '',
existsSync: path => store.has(follow(path)) || dirs.has(path),
readFileSync: path => store.get(follow(path)) ?? '',
writeFileSync: (path, data) => {
store.set(path, data);
store.set(follow(path), data);
},
mkdirSync: path => {
dirs.add(path);
Expand Down Expand Up @@ -108,6 +115,28 @@ describe('writeClerkAgentRules', () => {
expect(memory.read('/repo/apps/web/AGENTS.md')).toBeUndefined();
});

it('points to an SDK AGENTS.md that exists when the SDK is installed in a pnpm workspace app', () => {
const memory = createMemoryFs({
'/repo/pnpm-workspace.yaml': 'packages:\n - apps/*\n',
'/repo/package.json': '{"name":"root","private":true}',
'/repo/apps/web/package.json': '{"name":"web","dependencies":{"@clerk/nextjs":"*"}}',
'/repo/apps/web/node_modules/@clerk/nextjs/AGENTS.md': '# Clerk Next.js',
});

const { files } = writeClerkAgentRules({
cwd: '/repo/apps/web',
fs: memory.fs,
path: pathAdapter,
sdks: ['nextjs'],
});

const agentsMdPath = files.find(file => file.endsWith('/AGENTS.md')) ?? '';
const pointer = memory.read(agentsMdPath)?.match(/`(node_modules\/@clerk\/nextjs\/AGENTS\.md)`/)?.[1] ?? '';

expect(pointer).toBe('node_modules/@clerk/nextjs/AGENTS.md');
expect(memory.fs.existsSync(pathAdapter.join(pathAdapter.dirname(agentsMdPath), pointer))).toBe(true);
});

it('replaces only Clerk blocks and preserves surrounding content', () => {
const memory = createMemoryFs({
'/repo/AGENTS.md': 'Project rules\n\n<!-- BEGIN:clerk-agent-rules -->\nold\n<!-- END:clerk-agent-rules -->\n',
Expand Down Expand Up @@ -240,6 +269,15 @@ describe('writeClerkAgentRules with an existing CLAUDE.md', () => {
expect(memory.read('/repo/CLAUDE.md')).toBe('Shared rules\n');
});

it('keeps the Clerk block when CLAUDE.md is a symlink to an AGENTS.md that does not exist yet', () => {
const memory = createMemoryFs({}, { symlinks: { '/repo/CLAUDE.md': '/repo/AGENTS.md' } });

writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] });

expect(memory.read('/repo/AGENTS.md')).toContain('<!-- BEGIN:clerk-agent-rules:nextjs -->');
expect(memory.read('/repo/AGENTS.md')).not.toContain('@AGENTS.md');
});

it('writes the Clerk block into CLAUDE.md and skips the import when only CLAUDE.md exists', () => {
const memory = createMemoryFs({ '/repo/CLAUDE.md': 'Claude rules\n' });

Expand Down
13 changes: 8 additions & 5 deletions packages/shared/src/__tests__/runtimeEnvironment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,14 @@ describe('isClerkAgentRulesWriteAllowed', () => {
expect(isClerkAgentRulesWriteAllowed()).toBe(false);
});

it.each(['false', 'FALSE', '0', 'off', 'no'])('blocks writes when CLERK_AGENTS_MD is %s', value => {
vi.stubEnv('CLERK_AGENTS_MD', value);

expect(isClerkAgentRulesWriteAllowed()).toBe(false);
});
it.each(['false', 'FALSE', '0', 'off', 'no', ' false', 'false '])(
'blocks writes when CLERK_AGENTS_MD is %s',
value => {
vi.stubEnv('CLERK_AGENTS_MD', value);

expect(isClerkAgentRulesWriteAllowed()).toBe(false);
},
);

it('allows writes when CLERK_AGENTS_MD is unset', () => {
vi.stubEnv('CLERK_AGENTS_MD', undefined);
Expand Down
Loading