Files
EJClaw/runners/codex-runner/test/reviewer-runtime.test.ts
2026-04-11 04:47:16 +09:00

225 lines
6.2 KiB
TypeScript

import { execFileSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
assertReadonlyWorkspaceRepoConnectivity,
buildReviewerGitGuardEnv,
isReviewerRuntime,
} from '../src/reviewer-runtime.js';
const ORIGINAL_UNSAFE_HOST_PAIRED_MODE =
process.env.EJCLAW_UNSAFE_HOST_PAIRED_MODE;
afterEach(() => {
if (ORIGINAL_UNSAFE_HOST_PAIRED_MODE == null) {
delete process.env.EJCLAW_UNSAFE_HOST_PAIRED_MODE;
} else {
process.env.EJCLAW_UNSAFE_HOST_PAIRED_MODE =
ORIGINAL_UNSAFE_HOST_PAIRED_MODE;
}
});
beforeEach(() => {
delete process.env.EJCLAW_UNSAFE_HOST_PAIRED_MODE;
});
function createTempRepo(prefix: string): string {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
execFileSync('git', ['init'], {
cwd,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
execFileSync('git', ['config', 'user.email', 'test@example.com'], {
cwd,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
execFileSync('git', ['config', 'user.name', 'EJClaw Test'], {
cwd,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
fs.writeFileSync(path.join(cwd, 'README.md'), 'seed\n');
execFileSync('git', ['add', 'README.md'], {
cwd,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
execFileSync('git', ['commit', '-m', 'seed'], {
cwd,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
return cwd;
}
describe('codex reviewer runtime guard', () => {
it('detects reviewer room metadata', () => {
expect(
isReviewerRuntime({
serviceId: 'codex-review',
role: 'reviewer',
ownerServiceId: 'codex-main',
reviewerServiceId: 'codex-review',
failoverOwner: false,
}),
).toBe(true);
});
it('prepends a git wrapper to PATH for reviewer runtimes', () => {
const env = buildReviewerGitGuardEnv({ PATH: process.env.PATH }, true);
expect(env.PATH).toContain('ejclaw-reviewer-git-');
expect(env.EJCLAW_REAL_GIT).toBeTruthy();
});
it('prefers an executable HOME-scoped wrapper dir before tmp', () => {
const homeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'ejclaw-reviewer-home-'),
);
try {
const env = buildReviewerGitGuardEnv(
{
PATH: process.env.PATH,
HOME: homeDir,
},
true,
);
const wrapperDir = env.PATH?.split(path.delimiter)[0] ?? '';
expect(wrapperDir).toContain(
path.join(homeDir, '.ejclaw-reviewer-runtime'),
);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
});
it('allows mutating git commands in temp repos outside the protected workspace', () => {
const protectedDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'ejclaw-protected-workspace-'),
);
const env = buildReviewerGitGuardEnv(
{
PATH: process.env.PATH,
EJCLAW_WORK_DIR: protectedDir,
},
true,
);
const cwd = createTempRepo('ejclaw-reviewer-temp-repo-');
fs.writeFileSync(path.join(cwd, 'note.txt'), 'ok\n');
expect(() =>
execFileSync('git', ['add', 'note.txt'], {
cwd,
env,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
}),
).not.toThrow();
});
it('blocks mutating git subcommands inside the protected reviewer workspace', () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-reviewer-test-'));
const env = buildReviewerGitGuardEnv(
{
PATH: process.env.PATH,
EJCLAW_WORK_DIR: cwd,
},
true,
);
try {
execFileSync('git', ['-c', 'color.ui=false', 'commit', '-m', 'x'], {
cwd,
env,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
throw new Error('expected git wrapper to block commit');
} catch (error) {
const stderr =
error instanceof Error && 'stderr' in error
? String((error as Error & { stderr?: string | Buffer }).stderr ?? '')
: '';
expect(stderr).toContain(
'EJClaw reviewer runtime blocks mutating git subcommands: commit',
);
}
});
it('accepts a mounted local origin path that resolves as a git repo', () => {
const originDir = createTempRepo('ejclaw-reviewer-origin-');
const cwd = createTempRepo('ejclaw-reviewer-workspace-');
execFileSync('git', ['remote', 'add', 'origin', originDir], {
cwd,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
const env = buildReviewerGitGuardEnv(
{
PATH: process.env.PATH,
EJCLAW_WORK_DIR: cwd,
},
true,
);
expect(() =>
assertReadonlyWorkspaceRepoConnectivity(env, true),
).not.toThrow();
});
it.each([
'https://github.com/EyeJoker-Internal/eyejoker-db.git',
'git@github.com:EyeJoker-Internal/eyejoker-db.git',
])(
'accepts remote origin %s without requiring a mounted local canonical path',
(originUrl) => {
const cwd = createTempRepo('ejclaw-reviewer-remote-origin-');
execFileSync('git', ['remote', 'add', 'origin', originUrl], {
cwd,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
const env = buildReviewerGitGuardEnv(
{
PATH: process.env.PATH,
EJCLAW_WORK_DIR: cwd,
},
true,
);
expect(() =>
assertReadonlyWorkspaceRepoConnectivity(env, true),
).not.toThrow();
},
);
it('fails fast when the local origin path is not mounted as a git repo', () => {
const cwd = createTempRepo('ejclaw-reviewer-workspace-');
const missingOriginDir = path.join(
os.tmpdir(),
`ejclaw-reviewer-missing-origin-${Date.now()}`,
);
execFileSync('git', ['remote', 'add', 'origin', missingOriginDir], {
cwd,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
const env = buildReviewerGitGuardEnv(
{
PATH: process.env.PATH,
EJCLAW_WORK_DIR: cwd,
},
true,
);
expect(() => assertReadonlyWorkspaceRepoConnectivity(env, true)).toThrow(
`EJClaw readonly runtime cannot access local git origin path: ${missingOriginDir}`,
);
});
});