diff --git a/package.json b/package.json index ceeb4ce16e0..6ff2948d600 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "prepreview": "node scripts/cleanup-ports.mjs", "preview": "vite preview", "test": "vitest run", + "agent:prime": "node scripts/agent-prime.mjs", "test:watch": "vitest", "test:e2e": "playwright test --grep-invert 'visual comparison|Jack & Jill O.Rama'", "test:e2e:targeted": "playwright test", diff --git a/scripts/agent-prime.mjs b/scripts/agent-prime.mjs new file mode 100644 index 00000000000..a1539d0fe17 --- /dev/null +++ b/scripts/agent-prime.mjs @@ -0,0 +1,62 @@ +import { writeFileSync, existsSync, readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { execSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = resolve(__dirname, '..'); + +export function generateAgentContext(cwd = rootDir) { + let gitSha = 'unknown'; + let submoduleSha = 'unknown'; + + try { + gitSha = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', stdio: 'pipe' }).trim(); + } catch (error) { + console.warn(`[agent:prime] Command failed: "git rev-parse HEAD". Using fallback "unknown". Error: ${error.message}`); + } + + try { + submoduleSha = execSync('git rev-parse HEAD:boomtick-pkg', { cwd, encoding: 'utf8', stdio: 'pipe' }).trim(); + } catch (error) { + console.warn(`[agent:prime] Command failed: "git rev-parse HEAD:boomtick-pkg". Using fallback "unknown". Error: ${error.message}`); + } + + let pkgName = 'tech-dancer'; + const pkgPath = resolve(cwd, 'package.json'); + if (existsSync(pkgPath)) { + try { + const pkgData = JSON.parse(readFileSync(pkgPath, 'utf8')); + if (pkgData && typeof pkgData === 'object' && typeof pkgData.name === 'string') { + pkgName = pkgData.name; + } else { + console.warn(`[agent:prime] The "name" property is missing or not a string in package.json. Using default name "${pkgName}".`); + } + } catch (error) { + console.warn(`[agent:prime] Failed to parse package.json. Using default name "${pkgName}". Error: ${error.message}`); + } + } + + return { + packageName: pkgName, + updatedAt: new Date().toISOString(), + gitCommit: gitSha, + submodules: { + 'boomtick-pkg': submoduleSha + }, + version: '1.0.0' + }; +} + +export function primeAgentContext(cwd = rootDir) { + const context = generateAgentContext(cwd); + const outputPath = resolve(cwd, '.agent-context.json'); + writeFileSync(outputPath, JSON.stringify(context, null, 2), 'utf8'); + console.log(`[agent:prime] Updated .agent-context.json successfully (${context.gitCommit.slice(0, 7)})`); + return context; +} + +if (process.argv[1] === __filename) { + primeAgentContext(); +} diff --git a/tests/unit/agent-prime.test.ts b/tests/unit/agent-prime.test.ts new file mode 100644 index 00000000000..b305cf4cff9 --- /dev/null +++ b/tests/unit/agent-prime.test.ts @@ -0,0 +1,152 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as os from 'node:os'; + +vi.mock('node:child_process', async (importOriginal) => { + const actual = await importOriginal(); + const viFn = vi.fn().mockReturnValue('mocked-git-sha\n'); + return { + ...actual, + execSync: viFn, + default: { + ...actual, + execSync: viFn, + } + }; +}); + +import * as child_process from 'node:child_process'; +import { generateAgentContext, primeAgentContext } from '../../scripts/agent-prime.mjs'; + +describe('agent-prime.mjs', () => { + let tmpDir: string; + let originalCwd: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-prime-test-')); + originalCwd = process.cwd(); + process.chdir(tmpDir); + vi.clearAllMocks(); + }); + + afterEach(() => { + process.chdir(originalCwd); + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + describe('generateAgentContext', () => { + it('returns context with valid git and package data', () => { + fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ name: 'mocked-pkg-name' })); + + vi.mocked(child_process.execSync).mockImplementation((_cmd: unknown, _opts?: unknown) => { + if (_cmd === 'git rev-parse HEAD:boomtick-pkg') return 'mocked-submodule-sha\n'; + if (_cmd === 'git rev-parse HEAD') return 'mocked-git-sha\n'; + return ''; + }); + + const context = generateAgentContext(tmpDir); + expect(context.gitCommit).toBe('mocked-git-sha'); + expect(context.submodules['boomtick-pkg']).toBe('mocked-submodule-sha'); + expect(context.packageName).toBe('mocked-pkg-name'); + }); + + it('handles git failure gracefully and logs fallback', () => { + vi.mocked(child_process.execSync).mockImplementation(() => { + throw new Error('git error'); + }); + + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const context = generateAgentContext(tmpDir); + expect(context.gitCommit).toBe('unknown'); + expect(context.submodules['boomtick-pkg']).toBe('unknown'); + expect(context.packageName).toBe('tech-dancer'); + + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + + it('handles JSON parse error gracefully and logs warning', () => { + fs.writeFileSync(path.join(tmpDir, 'package.json'), 'invalid-json'); + + vi.mocked(child_process.execSync).mockImplementation((_cmd: unknown) => { + return 'mocked-git-sha\n'; + }); + + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const context = generateAgentContext(tmpDir); + expect(context.packageName).toBe('tech-dancer'); + + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + + it('handles missing name property in package.json and logs warning', () => { + fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({})); + + vi.mocked(child_process.execSync).mockImplementation((_cmd: unknown) => { + return 'mocked-git-sha\n'; + }); + + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const context = generateAgentContext(tmpDir); + expect(context.packageName).toBe('tech-dancer'); // defaults to tech-dancer + + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('The "name" property is missing')); + + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + + it('handles empty package.json gracefully', () => { + fs.writeFileSync(path.join(tmpDir, 'package.json'), ''); + + vi.mocked(child_process.execSync).mockImplementation((_cmd: unknown) => { + return 'mocked-git-sha\n'; + }); + + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const context = generateAgentContext(tmpDir); + expect(context.packageName).toBe('tech-dancer'); + + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + }); + + describe('primeAgentContext', () => { + it('writes context to .agent-context.json', () => { + vi.mocked(child_process.execSync).mockImplementation((_cmd: unknown) => { + return 'mocked-git-sha\n'; + }); + + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const context = primeAgentContext(tmpDir); + + const outputPath = path.join(tmpDir, '.agent-context.json'); + expect(fs.existsSync(outputPath)).toBe(true); + + const writtenData = JSON.parse(fs.readFileSync(outputPath, 'utf8')); + + // Remove updatedAt for stable comparison + delete writtenData.updatedAt; + const expectedContext = { ...context }; + delete expectedContext.updatedAt; + + expect(writtenData).toEqual(expectedContext); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Updated .agent-context.json successfully')); + + consoleSpy.mockRestore(); + }); + }); +});