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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
62 changes: 62 additions & 0 deletions scripts/agent-prime.mjs
Original file line number Diff line number Diff line change
@@ -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();
}
152 changes: 152 additions & 0 deletions tests/unit/agent-prime.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import('node:child_process')>();
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();
});
});
});
Loading