diff --git a/.changeset/agent-rules-review-tests.md b/.changeset/agent-rules-review-tests.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/agent-rules-review-tests.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/nextjs/src/server/__tests__/agent-rules-node.test.ts b/packages/nextjs/src/server/__tests__/agent-rules-node.test.ts index ead72cbbe4a..4624650e662 100644 --- a/packages/nextjs/src/server/__tests__/agent-rules-node.test.ts +++ b/packages/nextjs/src/server/__tests__/agent-rules-node.test.ts @@ -6,17 +6,19 @@ vi.mock('@clerk/shared/agentRules', async importOriginal => ({ ...(await importOriginal()), 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, ''); @@ -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 () => { @@ -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'); diff --git a/packages/shared/src/__tests__/agentRules.spec.ts b/packages/shared/src/__tests__/agentRules.spec.ts index 0eded3e381b..2a2225ee9b6 100644 --- a/packages/shared/src/__tests__/agentRules.spec.ts +++ b/packages/shared/src/__tests__/agentRules.spec.ts @@ -11,15 +11,22 @@ import { writeClerkAgentRules, } from '../agentRules'; -const createMemoryFs = (files: Record = {}, { withRepoPackageJson = true } = {}) => { +const createMemoryFs = ( + files: Record = {}, + { + withRepoPackageJson = true, + symlinks = {}, + }: { withRepoPackageJson?: boolean; symlinks?: Record } = {}, +) => { const store = new Map(Object.entries(withRepoPackageJson ? { '/repo/package.json': '{}', ...files } : files)); const dirs = new Set(); + 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); @@ -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\nold\n\n', @@ -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(''); + 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' }); diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts index 9d9dc5b03c0..77b4e9d9901 100644 --- a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -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);