Remove unused root Claude guide
This commit is contained in:
100
CLAUDE.md
100
CLAUDE.md
@@ -1,100 +0,0 @@
|
|||||||
# EJClaw
|
|
||||||
|
|
||||||
Dual-agent AI assistant (Claude Code + Codex) over Discord. Originally derived from [qwibitai/nanoclaw](https://github.com/qwibitai/nanoclaw).
|
|
||||||
|
|
||||||
## Quick Context
|
|
||||||
|
|
||||||
Single unified service (`ejclaw`) manages three Discord bots (owner, reviewer, arbiter) in one process. Agent types behind each role remain configurable via `OWNER_AGENT_TYPE`, `REVIEWER_AGENT_TYPE`, and `ARBITER_AGENT_TYPE` in `.env`. Claude Code uses the Agent SDK; Codex uses the Codex SDK (`codex exec`). Auth via `CLAUDE_CODE_OAUTH_TOKEN` in `.env` (1-year token from `claude setup-token`).
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `src/index.ts` | Orchestrator: state, message loop, agent invocation |
|
|
||||||
| `src/agent-runner.ts` | Spawns agent processes, manages env/sessions/skills |
|
|
||||||
| `src/channels/discord.ts` | Discord channel (8s typing refresh, Groq/OpenAI Whisper transcription) |
|
|
||||||
| `src/ipc.ts` | IPC watcher and task processing |
|
|
||||||
| `src/router.ts` | Message formatting and outbound routing |
|
|
||||||
| `src/config.ts` | Trigger pattern, paths, intervals |
|
|
||||||
| `src/task-scheduler.ts` | Runs scheduled tasks |
|
|
||||||
| `src/db.ts` | SQLite operations |
|
|
||||||
| `runners/agent-runner/` | Claude Code runner (Agent SDK) |
|
|
||||||
| `runners/codex-runner/` | Codex runner (SDK, `codex exec` wrapper) |
|
|
||||||
| `groups/{name}/CLAUDE.md` | Per-group memory (isolated) |
|
|
||||||
|
|
||||||
## Skills
|
|
||||||
|
|
||||||
| Skill | When to Use |
|
|
||||||
|-------|-------------|
|
|
||||||
| `/setup` | First-time installation, authentication, service configuration |
|
|
||||||
| `/customize` | Adding channels, integrations, changing behavior |
|
|
||||||
| `/debug` | Agent issues, logs, troubleshooting |
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Run commands directly—don't tell the user to run them.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run build # Build main project
|
|
||||||
bun run build:runners # Install + build both runners
|
|
||||||
bun run build:runtime # Build host runtime only
|
|
||||||
bun run dev # Dev mode with hot reload
|
|
||||||
```
|
|
||||||
|
|
||||||
Service management (Linux):
|
|
||||||
```bash
|
|
||||||
systemctl --user restart ejclaw # Restart unified service
|
|
||||||
systemctl --user status ejclaw # Check status
|
|
||||||
journalctl --user -u ejclaw -f # Follow logs
|
|
||||||
```
|
|
||||||
|
|
||||||
Deploy:
|
|
||||||
```bash
|
|
||||||
bun run deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
`deploy` rebuilds only the host runtime, and verification also runs directly on
|
|
||||||
the host runtime.
|
|
||||||
|
|
||||||
## Service Stack Architecture
|
|
||||||
|
|
||||||
Single unified service manages all three Discord bots in one process:
|
|
||||||
- `ejclaw.service` — Single unified process
|
|
||||||
- Discord bots: `DISCORD_OWNER_BOT_TOKEN` (owner), `DISCORD_REVIEWER_BOT_TOKEN` (reviewer), `DISCORD_ARBITER_BOT_TOKEN` (arbiter)
|
|
||||||
- Old service-based token names are no longer accepted; migrate them to the canonical role-based keys above before starting the service
|
|
||||||
- Paired review: owner (`OWNER_AGENT_TYPE`, default: codex) ↔ reviewer (`REVIEWER_AGENT_TYPE`, default: claude-code)
|
|
||||||
- Provider fallback is internal routing only — visible Discord bots stay owner/reviewer/arbiter fixed
|
|
||||||
- Shared dirs: `store/`, `groups/`, `data/`
|
|
||||||
- SQLite WAL mode + `busy_timeout=5000` for concurrent access
|
|
||||||
|
|
||||||
## Debugging Paths
|
|
||||||
|
|
||||||
Unified DB + directories (both services share `store/`, `groups/`, `data/`):
|
|
||||||
|
|
||||||
| 항목 | 경로 |
|
|
||||||
|------|------|
|
|
||||||
| **DB** | `store/messages.db` (공유, WAL 모드) |
|
|
||||||
| 서비스 로그 | `journalctl --user -u ejclaw -f` 또는 `logs/ejclaw.log` |
|
|
||||||
| 그룹별 로그 | `groups/{name}/logs/` (공유 채널은 양쪽 봇 로그가 같은 폴더) |
|
|
||||||
| Claude 세션 | `data/sessions/{name}/.claude/` |
|
|
||||||
| Codex 세션 | `data/sessions/{name}/.codex/` |
|
|
||||||
| Claude 플랫폼 규칙 | `prompts/claude-platform.md` |
|
|
||||||
| Codex 플랫폼 규칙 | `prompts/codex-platform.md` |
|
|
||||||
| Claude 글로벌 메모리 | `groups/global/CLAUDE.md` |
|
|
||||||
|
|
||||||
## Codex SDK
|
|
||||||
|
|
||||||
Codex runner uses `@openai/codex-sdk` (wraps `codex exec`):
|
|
||||||
- `codex.startThread()` / `codex.resumeThread()` for session persistence
|
|
||||||
- `thread.run(input)` for single-shot turn execution (completes all work before returning)
|
|
||||||
- `approvalPolicy: "never"` + `sandboxMode: "danger-full-access"` for bypass
|
|
||||||
- Per-group: model (`CODEX_MODEL`), effort (`CODEX_EFFORT`), MCP servers via `config.toml`
|
|
||||||
- `CODEX_HOME` set to per-group session dir, reads `AGENTS.md` from there + CWD
|
|
||||||
|
|
||||||
## Voice Transcription
|
|
||||||
|
|
||||||
Audio attachments in Discord are transcribed via Groq Whisper (primary) or OpenAI Whisper (fallback):
|
|
||||||
- `GROQ_API_KEY` — Groq `whisper-large-v3-turbo`, ~200x real-time, free tier (console.groq.com)
|
|
||||||
- `OPENAI_API_KEY` — OpenAI `whisper-1`, fallback if Groq key not set
|
|
||||||
- Shared file cache (`cache/transcriptions/`) deduplicates across both services
|
|
||||||
- `.pending` file coordination prevents duplicate API calls
|
|
||||||
@@ -25,9 +25,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
computeVerificationSnapshotId,
|
computeVerificationSnapshotId,
|
||||||
formatVerificationResponse,
|
formatVerificationResponse,
|
||||||
resolveVerificationResponsesDir,
|
runVerificationRequestDirect,
|
||||||
VERIFICATION_PROFILES,
|
VERIFICATION_PROFILES,
|
||||||
waitForVerificationResponse,
|
|
||||||
} from './verification.js';
|
} from './verification.js';
|
||||||
import { resolveIpcDirectories } from './ipc-paths.js';
|
import { resolveIpcDirectories } from './ipc-paths.js';
|
||||||
import { buildSendMessageIpcPayload } from './ipc-message.js';
|
import { buildSendMessageIpcPayload } from './ipc-message.js';
|
||||||
@@ -38,8 +37,6 @@ const MESSAGES_DIR = path.join(HOST_IPC_DIR, 'messages');
|
|||||||
const TASKS_DIR = path.join(HOST_IPC_DIR, 'tasks');
|
const TASKS_DIR = path.join(HOST_IPC_DIR, 'tasks');
|
||||||
const HOST_EVIDENCE_RESPONSES_DIR =
|
const HOST_EVIDENCE_RESPONSES_DIR =
|
||||||
resolveHostEvidenceResponsesDir(HOST_IPC_DIR);
|
resolveHostEvidenceResponsesDir(HOST_IPC_DIR);
|
||||||
const VERIFICATION_RESPONSES_DIR =
|
|
||||||
resolveVerificationResponsesDir(HOST_IPC_DIR);
|
|
||||||
const REPO_ROOT = process.env.EJCLAW_WORK_DIR || process.cwd();
|
const REPO_ROOT = process.env.EJCLAW_WORK_DIR || process.cwd();
|
||||||
|
|
||||||
// Context from environment variables (set by the agent runner)
|
// Context from environment variables (set by the agent runner)
|
||||||
@@ -453,20 +450,12 @@ server.tool(
|
|||||||
.toString(36)
|
.toString(36)
|
||||||
.slice(2, 8)}`;
|
.slice(2, 8)}`;
|
||||||
|
|
||||||
writeIpcFile(TASKS_DIR, {
|
|
||||||
type: 'verification_request',
|
|
||||||
requestId,
|
|
||||||
profile: args.profile,
|
|
||||||
expected_snapshot_id: snapshotId,
|
|
||||||
groupFolder,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await waitForVerificationResponse(
|
const response = await runVerificationRequestDirect(REPO_ROOT, {
|
||||||
VERIFICATION_RESPONSES_DIR,
|
|
||||||
requestId,
|
requestId,
|
||||||
);
|
profile: args.profile,
|
||||||
|
expectedSnapshotId: snapshotId,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import fs from 'fs';
|
import { execFile } from 'child_process';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { pathToFileURL } from 'url';
|
||||||
export {
|
export {
|
||||||
computeVerificationSnapshotId,
|
computeVerificationSnapshotId,
|
||||||
resolveVerificationResponsesDir,
|
|
||||||
} from '../../../shared/verification-snapshot.js';
|
} from '../../../shared/verification-snapshot.js';
|
||||||
|
|
||||||
export const VERIFICATION_PROFILES = [
|
export const VERIFICATION_PROFILES = [
|
||||||
@@ -13,6 +13,45 @@ export const VERIFICATION_PROFILES = [
|
|||||||
|
|
||||||
export type VerificationProfile = (typeof VERIFICATION_PROFILES)[number];
|
export type VerificationProfile = (typeof VERIFICATION_PROFILES)[number];
|
||||||
|
|
||||||
|
type VerificationHelperEnvKey =
|
||||||
|
| 'PATH'
|
||||||
|
| 'HOME'
|
||||||
|
| 'USER'
|
||||||
|
| 'LOGNAME'
|
||||||
|
| 'SHELL'
|
||||||
|
| 'LANG'
|
||||||
|
| 'LC_ALL'
|
||||||
|
| 'LC_CTYPE'
|
||||||
|
| 'NODE_PATH'
|
||||||
|
| 'NODE_OPTIONS'
|
||||||
|
| 'TERM'
|
||||||
|
| 'COLORTERM'
|
||||||
|
| 'FORCE_COLOR';
|
||||||
|
|
||||||
|
const VERIFICATION_HELPER_ENV_KEYS: readonly VerificationHelperEnvKey[] = [
|
||||||
|
'PATH',
|
||||||
|
'HOME',
|
||||||
|
'USER',
|
||||||
|
'LOGNAME',
|
||||||
|
'SHELL',
|
||||||
|
'LANG',
|
||||||
|
'LC_ALL',
|
||||||
|
'LC_CTYPE',
|
||||||
|
'NODE_PATH',
|
||||||
|
'NODE_OPTIONS',
|
||||||
|
'TERM',
|
||||||
|
'COLORTERM',
|
||||||
|
'FORCE_COLOR',
|
||||||
|
];
|
||||||
|
const HELPER_TIMEOUT_MS = 20 * 60 * 1000;
|
||||||
|
const HELPER_MAX_BUFFER = 20 * 1024 * 1024;
|
||||||
|
|
||||||
|
export interface VerificationRequest {
|
||||||
|
requestId: string;
|
||||||
|
profile: VerificationProfile;
|
||||||
|
expectedSnapshotId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VerificationResponse {
|
export interface VerificationResponse {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
@@ -36,33 +75,87 @@ export function isVerificationProfile(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitForVerificationResponse(
|
function buildVerificationHelperEnv(): NodeJS.ProcessEnv {
|
||||||
responseDir: string,
|
const env: NodeJS.ProcessEnv = {
|
||||||
requestId: string,
|
TZ: 'Asia/Seoul',
|
||||||
options?: {
|
CI: '1',
|
||||||
timeoutMs?: number;
|
};
|
||||||
pollMs?: number;
|
|
||||||
},
|
|
||||||
): Promise<VerificationResponse> {
|
|
||||||
const timeoutMs = options?.timeoutMs ?? 20 * 60 * 1000;
|
|
||||||
const pollMs = options?.pollMs ?? 100;
|
|
||||||
const responsePath = path.join(responseDir, `${requestId}.json`);
|
|
||||||
const deadline = Date.now() + timeoutMs;
|
|
||||||
|
|
||||||
while (Date.now() <= deadline) {
|
for (const key of VERIFICATION_HELPER_ENV_KEYS) {
|
||||||
if (fs.existsSync(responsePath)) {
|
const value = process.env[key];
|
||||||
const response = JSON.parse(
|
if (value) {
|
||||||
fs.readFileSync(responsePath, 'utf-8'),
|
env[key] = value;
|
||||||
) as VerificationResponse;
|
|
||||||
fs.unlinkSync(responsePath);
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
return env;
|
||||||
`Timed out waiting for verification response: ${requestId}`,
|
}
|
||||||
|
|
||||||
|
function execFileCapture(
|
||||||
|
file: string,
|
||||||
|
args: string[],
|
||||||
|
options?: {
|
||||||
|
cwd?: string;
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
},
|
||||||
|
): Promise<{ stdout: string; stderr: string }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
execFile(
|
||||||
|
file,
|
||||||
|
args,
|
||||||
|
{
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: HELPER_TIMEOUT_MS,
|
||||||
|
maxBuffer: HELPER_MAX_BUFFER,
|
||||||
|
cwd: options?.cwd,
|
||||||
|
env: options?.env,
|
||||||
|
},
|
||||||
|
(error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(
|
||||||
|
Object.assign(error, {
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ stdout, stderr });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runVerificationRequestDirect(
|
||||||
|
repoRoot: string,
|
||||||
|
request: VerificationRequest,
|
||||||
|
): Promise<VerificationResponse> {
|
||||||
|
const helperPath = path.join(
|
||||||
|
repoRoot,
|
||||||
|
'shared',
|
||||||
|
'verification-request-runner.js',
|
||||||
);
|
);
|
||||||
|
const helperEnv = buildVerificationHelperEnv();
|
||||||
|
const { stdout, stderr } = await execFileCapture(
|
||||||
|
'bun',
|
||||||
|
[helperPath, repoRoot, JSON.stringify(request)],
|
||||||
|
{
|
||||||
|
cwd: repoRoot,
|
||||||
|
env: helperEnv,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(stdout) as VerificationResponse;
|
||||||
|
} catch (error) {
|
||||||
|
const detail = stderr.trim() || stdout.trim();
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse verification response from ${pathToFileURL(helperPath).href}: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}${detail ? `\n${detail}` : ''}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatVerificationResponse(
|
export function formatVerificationResponse(
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
computeVerificationSnapshotId,
|
computeVerificationSnapshotId,
|
||||||
formatVerificationResponse,
|
formatVerificationResponse,
|
||||||
resolveVerificationResponsesDir,
|
|
||||||
waitForVerificationResponse,
|
|
||||||
} from '../src/verification.js';
|
} from '../src/verification.js';
|
||||||
|
|
||||||
describe('runner verification helpers', () => {
|
describe('runner verification helpers', () => {
|
||||||
@@ -26,39 +24,6 @@ describe('runner verification helpers', () => {
|
|||||||
expect(second).toBe(first);
|
expect(second).toBe(first);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reads and removes the verification response file', async () => {
|
|
||||||
const ipcDir = fs.mkdtempSync(
|
|
||||||
path.join(os.tmpdir(), 'ejclaw-verification-ipc-'),
|
|
||||||
);
|
|
||||||
const responseDir = resolveVerificationResponsesDir(ipcDir);
|
|
||||||
fs.mkdirSync(responseDir, { recursive: true });
|
|
||||||
|
|
||||||
const responsePath = path.join(responseDir, 'req-1.json');
|
|
||||||
fs.writeFileSync(
|
|
||||||
responsePath,
|
|
||||||
JSON.stringify({
|
|
||||||
requestId: 'req-1',
|
|
||||||
ok: true,
|
|
||||||
profile: 'typecheck',
|
|
||||||
command: 'npm run typecheck',
|
|
||||||
stdout: '',
|
|
||||||
stderr: '',
|
|
||||||
exitCode: 0,
|
|
||||||
snapshotId: 'fs:abc123',
|
|
||||||
runtimeVersion: 'host:bun@test',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await waitForVerificationResponse(responseDir, 'req-1', {
|
|
||||||
timeoutMs: 100,
|
|
||||||
pollMs: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.ok).toBe(true);
|
|
||||||
expect(response.snapshotId).toBe('fs:abc123');
|
|
||||||
expect(fs.existsSync(responsePath)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('formats the response into a compact MCP-friendly text block', () => {
|
it('formats the response into a compact MCP-friendly text block', () => {
|
||||||
const text = formatVerificationResponse({
|
const text = formatVerificationResponse({
|
||||||
requestId: 'req-2',
|
requestId: 'req-2',
|
||||||
|
|||||||
35
shared/verification-request-runner.js
Normal file
35
shared/verification-request-runner.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { pathToFileURL } from 'url';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const [repoDir, requestJson] = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (!repoDir || !requestJson) {
|
||||||
|
throw new Error(
|
||||||
|
'usage: bun shared/verification-request-runner.js <repoDir> <requestJson>',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const verificationModuleUrl = pathToFileURL(
|
||||||
|
path.join(repoDir, 'src', 'verification.ts'),
|
||||||
|
).href;
|
||||||
|
const verificationModule = await import(verificationModuleUrl);
|
||||||
|
const request = JSON.parse(requestJson);
|
||||||
|
const result = await verificationModule.runVerificationRequest(request, {
|
||||||
|
repoDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdout.write(
|
||||||
|
JSON.stringify({
|
||||||
|
requestId: request.requestId,
|
||||||
|
...result,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.stack || error.message : String(error);
|
||||||
|
process.stderr.write(message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
64
src/ipc.ts
64
src/ipc.ts
@@ -9,11 +9,6 @@ import {
|
|||||||
runHostEvidenceRequest,
|
runHostEvidenceRequest,
|
||||||
writeHostEvidenceResponse,
|
writeHostEvidenceResponse,
|
||||||
} from './host-evidence.js';
|
} from './host-evidence.js';
|
||||||
import {
|
|
||||||
isVerificationProfile,
|
|
||||||
runVerificationRequest,
|
|
||||||
writeVerificationResponse,
|
|
||||||
} from './verification.js';
|
|
||||||
import { readJsonFile } from './utils.js';
|
import { readJsonFile } from './utils.js';
|
||||||
import { AvailableGroup } from './agent-runner.js';
|
import { AvailableGroup } from './agent-runner.js';
|
||||||
import {
|
import {
|
||||||
@@ -642,65 +637,6 @@ export async function processTaskIpc(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'verification_request':
|
|
||||||
if (!data.requestId) {
|
|
||||||
logger.warn(
|
|
||||||
{ sourceGroup },
|
|
||||||
'Ignoring verification_request without requestId',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isVerificationProfile(data.profile)) {
|
|
||||||
writeVerificationResponse(sourceGroup, {
|
|
||||||
requestId: data.requestId,
|
|
||||||
ok: false,
|
|
||||||
profile: 'test',
|
|
||||||
command: '',
|
|
||||||
stdout: '',
|
|
||||||
stderr: '',
|
|
||||||
exitCode: 1,
|
|
||||||
snapshotId: 'unknown',
|
|
||||||
runtimeVersion: '',
|
|
||||||
workdir: process.cwd(),
|
|
||||||
error: `Unsupported verification profile: ${String(data.profile)}`,
|
|
||||||
});
|
|
||||||
logger.warn(
|
|
||||||
{ sourceGroup, requestId: data.requestId, profile: data.profile },
|
|
||||||
'Rejected unsupported verification profile',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const result = await runVerificationRequest({
|
|
||||||
requestId: data.requestId,
|
|
||||||
profile: data.profile,
|
|
||||||
expectedSnapshotId:
|
|
||||||
typeof data.expected_snapshot_id === 'string'
|
|
||||||
? data.expected_snapshot_id
|
|
||||||
: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
writeVerificationResponse(sourceGroup, {
|
|
||||||
requestId: data.requestId,
|
|
||||||
...result,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
{
|
|
||||||
sourceGroup,
|
|
||||||
requestId: data.requestId,
|
|
||||||
profile: data.profile,
|
|
||||||
ok: result.ok,
|
|
||||||
exitCode: result.exitCode,
|
|
||||||
snapshotId: result.snapshotId,
|
|
||||||
},
|
|
||||||
'Processed verification request via IPC',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'update_task':
|
case 'update_task':
|
||||||
if (data.taskId) {
|
if (data.taskId) {
|
||||||
const task = getTaskById(data.taskId);
|
const task = getTaskById(data.taskId);
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
const { runVerificationRequestMock } = vi.hoisted(() => ({
|
|
||||||
runVerificationRequestMock: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('./verification.js', async () => {
|
|
||||||
const actual =
|
|
||||||
await vi.importActual<typeof import('./verification.js')>(
|
|
||||||
'./verification.js',
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
runVerificationRequest: runVerificationRequestMock,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
import { _initTestDatabase, _setRegisteredGroupForTests } from './db.js';
|
|
||||||
import { resolveGroupIpcPath } from './group-folder.js';
|
|
||||||
import { processTaskIpc, type IpcDeps } from './ipc.js';
|
|
||||||
import type { RegisteredGroup } from './types.js';
|
|
||||||
|
|
||||||
const VERIFICATION_GROUP: RegisteredGroup = {
|
|
||||||
name: 'Verification',
|
|
||||||
folder: 'verification-group',
|
|
||||||
trigger: '@Andy',
|
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('verification IPC', () => {
|
|
||||||
let deps: IpcDeps;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
_initTestDatabase();
|
|
||||||
_setRegisteredGroupForTests('verification@g.us', VERIFICATION_GROUP);
|
|
||||||
runVerificationRequestMock.mockReset();
|
|
||||||
|
|
||||||
deps = {
|
|
||||||
sendMessage: async () => {},
|
|
||||||
registeredGroups: () => ({ 'verification@g.us': VERIFICATION_GROUP }),
|
|
||||||
assignRoom: () => {},
|
|
||||||
syncGroups: async () => {},
|
|
||||||
getAvailableGroups: () => [],
|
|
||||||
writeGroupsSnapshot: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
fs.rmSync(resolveGroupIpcPath('verification-group'), {
|
|
||||||
recursive: true,
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('writes verification responses into the source group namespace', async () => {
|
|
||||||
runVerificationRequestMock.mockResolvedValue({
|
|
||||||
ok: true,
|
|
||||||
profile: 'typecheck',
|
|
||||||
command: 'npm run typecheck',
|
|
||||||
stdout: '',
|
|
||||||
stderr: '',
|
|
||||||
exitCode: 0,
|
|
||||||
snapshotId: 'fs:abc123',
|
|
||||||
runtimeVersion: 'host:bun@test',
|
|
||||||
});
|
|
||||||
|
|
||||||
await processTaskIpc(
|
|
||||||
{
|
|
||||||
type: 'verification_request',
|
|
||||||
requestId: 'req-1',
|
|
||||||
profile: 'typecheck',
|
|
||||||
expected_snapshot_id: 'fs:abc123',
|
|
||||||
},
|
|
||||||
'verification-group',
|
|
||||||
false,
|
|
||||||
deps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const responsePath = path.join(
|
|
||||||
resolveGroupIpcPath('verification-group'),
|
|
||||||
'verification-responses',
|
|
||||||
'req-1.json',
|
|
||||||
);
|
|
||||||
const response = JSON.parse(fs.readFileSync(responsePath, 'utf-8')) as {
|
|
||||||
ok: boolean;
|
|
||||||
requestId: string;
|
|
||||||
snapshotId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(runVerificationRequestMock).toHaveBeenCalledWith({
|
|
||||||
requestId: 'req-1',
|
|
||||||
profile: 'typecheck',
|
|
||||||
expectedSnapshotId: 'fs:abc123',
|
|
||||||
});
|
|
||||||
expect(response.requestId).toBe('req-1');
|
|
||||||
expect(response.ok).toBe(true);
|
|
||||||
expect(response.snapshotId).toBe('fs:abc123');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an error response for unsupported profiles without executing', async () => {
|
|
||||||
await processTaskIpc(
|
|
||||||
{
|
|
||||||
type: 'verification_request',
|
|
||||||
requestId: 'req-2',
|
|
||||||
profile: 'rm -rf /',
|
|
||||||
},
|
|
||||||
'verification-group',
|
|
||||||
false,
|
|
||||||
deps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const responsePath = path.join(
|
|
||||||
resolveGroupIpcPath('verification-group'),
|
|
||||||
'verification-responses',
|
|
||||||
'req-2.json',
|
|
||||||
);
|
|
||||||
const response = JSON.parse(fs.readFileSync(responsePath, 'utf-8')) as {
|
|
||||||
ok: boolean;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(runVerificationRequestMock).not.toHaveBeenCalled();
|
|
||||||
expect(response.ok).toBe(false);
|
|
||||||
expect(response.error).toContain('Unsupported verification profile');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -4,16 +4,12 @@ import os from 'os';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { TIMEZONE } from './config.js';
|
import { TIMEZONE } from './config.js';
|
||||||
import { resolveGroupIpcPath } from './group-folder.js';
|
|
||||||
import {
|
import {
|
||||||
buildWorkspaceScriptCommand,
|
buildWorkspaceScriptCommand,
|
||||||
ensureWorkspaceDependenciesInstalled,
|
ensureWorkspaceDependenciesInstalled,
|
||||||
hasInstalledNodeModules,
|
hasInstalledNodeModules,
|
||||||
} from './workspace-package-manager.js';
|
} from './workspace-package-manager.js';
|
||||||
import {
|
import { computeVerificationSnapshotId } from '../shared/verification-snapshot.js';
|
||||||
computeVerificationSnapshotId,
|
|
||||||
resolveVerificationResponsesDir,
|
|
||||||
} from '../shared/verification-snapshot.js';
|
|
||||||
|
|
||||||
export const VERIFICATION_PROFILES = ['test', 'typecheck', 'build'] as const;
|
export const VERIFICATION_PROFILES = ['test', 'typecheck', 'build'] as const;
|
||||||
|
|
||||||
@@ -38,10 +34,6 @@ export interface VerificationResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VerificationResponse extends VerificationResult {
|
|
||||||
requestId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VerificationSnapshot {
|
export interface VerificationSnapshot {
|
||||||
snapshotId: string;
|
snapshotId: string;
|
||||||
}
|
}
|
||||||
@@ -393,21 +385,3 @@ export async function runVerificationRequest(
|
|||||||
fs.rmSync(directExecution.tempRoot, { recursive: true, force: true });
|
fs.rmSync(directExecution.tempRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveVerificationResponseDir(groupFolder: string): string {
|
|
||||||
return resolveVerificationResponsesDir(resolveGroupIpcPath(groupFolder));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeVerificationResponse(
|
|
||||||
groupFolder: string,
|
|
||||||
response: VerificationResponse,
|
|
||||||
): string {
|
|
||||||
const responseDir = resolveVerificationResponseDir(groupFolder);
|
|
||||||
fs.mkdirSync(responseDir, { recursive: true });
|
|
||||||
|
|
||||||
const outputPath = path.join(responseDir, `${response.requestId}.json`);
|
|
||||||
const tempPath = `${outputPath}.tmp`;
|
|
||||||
fs.writeFileSync(tempPath, JSON.stringify(response, null, 2));
|
|
||||||
fs.renameSync(tempPath, outputPath);
|
|
||||||
return outputPath;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user