Skip to content
Open
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
22 changes: 9 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,13 @@ function readJsonSafe(p) {
}

function rejectPendingRun(statePath) {
try {
const { getRepoRoot } = require('./src/gep/paths');
const { execSync } = require('child_process');
const repoRoot = getRepoRoot();

execSync('git checkout -- .', { cwd: repoRoot, encoding: 'utf8', timeout: 30000 });
execSync('git clean -fd', { cwd: repoRoot, encoding: 'utf8', timeout: 30000 });
} catch (e) {
console.warn('[Loop] Pending run rollback failed: ' + (e.message || e));
}

try {
const state = readJsonSafe(statePath);
if (state && state.last_run && state.last_run.run_id) {
state.last_solidify = {
run_id: state.last_run.run_id,
rejected: true,
reason: 'loop_bridge_disabled_autoreject',
reason: 'loop_bridge_disabled_autoreject_no_rollback',
timestamp: new Date().toISOString(),
};
fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
Expand Down Expand Up @@ -184,7 +173,7 @@ async function main() {
if (isPendingSolidify(stAfterRun)) {
const cleared = rejectPendingRun(solidifyStatePath);
if (cleared) {
console.warn('[Loop] Auto-rejected pending run because bridge is disabled in loop mode.');
console.warn('[Loop] Auto-rejected pending run because bridge is disabled in loop mode (state only, no rollback).');
}
}
}
Expand Down Expand Up @@ -528,3 +517,10 @@ async function main() {
if (require.main === module) {
main();
}

module.exports = {
main,
readJsonSafe,
rejectPendingRun,
isPendingSolidify,
};
70 changes: 70 additions & 0 deletions test/loopMode.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const { describe, it, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { rejectPendingRun } = require('../index.js');

describe('loop-mode auto reject', () => {
var tmpDir;
var originalRepoRoot;
var originalWorkspaceRoot;
var originalEvDir;
var originalMemoryDir;
var originalA2aHubUrl;
var originalHeartbeatMs;
var originalWorkerEnabled;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evolver-loop-test-'));
originalRepoRoot = process.env.EVOLVER_REPO_ROOT;
originalWorkspaceRoot = process.env.OPENCLAW_WORKSPACE;
originalEvDir = process.env.EVOLUTION_DIR;
originalMemoryDir = process.env.MEMORY_DIR;
originalA2aHubUrl = process.env.A2A_HUB_URL;
originalHeartbeatMs = process.env.HEARTBEAT_INTERVAL_MS;
originalWorkerEnabled = process.env.WORKER_ENABLED;
process.env.EVOLVER_REPO_ROOT = tmpDir;
process.env.OPENCLAW_WORKSPACE = tmpDir;
process.env.EVOLUTION_DIR = path.join(tmpDir, 'memory', 'evolution');
process.env.MEMORY_DIR = path.join(tmpDir, 'memory');
process.env.A2A_HUB_URL = '';
process.env.HEARTBEAT_INTERVAL_MS = '3600000';
delete process.env.WORKER_ENABLED;
});

afterEach(() => {
if (originalRepoRoot === undefined) delete process.env.EVOLVER_REPO_ROOT;
else process.env.EVOLVER_REPO_ROOT = originalRepoRoot;
if (originalWorkspaceRoot === undefined) delete process.env.OPENCLAW_WORKSPACE;
else process.env.OPENCLAW_WORKSPACE = originalWorkspaceRoot;
if (originalEvDir === undefined) delete process.env.EVOLUTION_DIR;
else process.env.EVOLUTION_DIR = originalEvDir;
if (originalMemoryDir === undefined) delete process.env.MEMORY_DIR;
else process.env.MEMORY_DIR = originalMemoryDir;
if (originalA2aHubUrl === undefined) delete process.env.A2A_HUB_URL;
else process.env.A2A_HUB_URL = originalA2aHubUrl;
if (originalHeartbeatMs === undefined) delete process.env.HEARTBEAT_INTERVAL_MS;
else process.env.HEARTBEAT_INTERVAL_MS = originalHeartbeatMs;
if (originalWorkerEnabled === undefined) delete process.env.WORKER_ENABLED;
else process.env.WORKER_ENABLED = originalWorkerEnabled;
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it('marks pending runs rejected without deleting untracked files', () => {
const stateDir = path.join(tmpDir, 'memory', 'evolution');
fs.mkdirSync(stateDir, { recursive: true });
fs.writeFileSync(path.join(stateDir, 'evolution_solidify_state.json'), JSON.stringify({
last_run: { run_id: 'run_123' }
}, null, 2));
fs.writeFileSync(path.join(tmpDir, 'PR_BODY.md'), 'keep me\n');
const changed = rejectPendingRun(path.join(stateDir, 'evolution_solidify_state.json'));

const state = JSON.parse(fs.readFileSync(path.join(stateDir, 'evolution_solidify_state.json'), 'utf8'));
assert.equal(changed, true);
assert.equal(state.last_solidify.run_id, 'run_123');
assert.equal(state.last_solidify.rejected, true);
assert.equal(state.last_solidify.reason, 'loop_bridge_disabled_autoreject_no_rollback');
assert.equal(fs.readFileSync(path.join(tmpDir, 'PR_BODY.md'), 'utf8'), 'keep me\n');
});
});