mirror of
https://fastgit.cc/github.com/Yeachan-Heo/oh-my-claudecode
synced 2026-04-20 21:00:50 +08:00
Merge pull request #2712 from Yeachan-Heo/fix/issue-2710-omc-state-root
Fix OMC state root resolution for nested working directories
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, mkdtempSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
import { writeModeState, readModeState, clearModeStateFile } from '../mode-state-io.js';
|
||||
@@ -56,6 +57,18 @@ describe('mode-state-io', () => {
|
||||
expect(existsSync(join(tempDir, '.omc', 'state'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should resolve writes to the git worktree root when called from a subdirectory', () => {
|
||||
const nestedDir = join(tempDir, 'nested', 'cwd');
|
||||
mkdirSync(nestedDir, { recursive: true });
|
||||
execSync('git init', { cwd: tempDir, stdio: 'pipe' });
|
||||
|
||||
const result = writeModeState('autopilot', { phase: 'exec' }, nestedDir);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(existsSync(join(tempDir, '.omc', 'state', 'autopilot-state.json'))).toBe(true);
|
||||
expect(existsSync(join(nestedDir, '.omc', 'state', 'autopilot-state.json'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should write file with 0o600 permissions', () => {
|
||||
writeModeState('ralph', { active: true }, tempDir);
|
||||
const filePath = join(tempDir, '.omc', 'state', 'ralph-state.json');
|
||||
@@ -153,6 +166,23 @@ describe('mode-state-io', () => {
|
||||
expect(result.phase).toBe('running');
|
||||
});
|
||||
|
||||
it('should read state from the git worktree root when given a subdirectory', () => {
|
||||
const nestedDir = join(tempDir, 'nested', 'cwd');
|
||||
mkdirSync(nestedDir, { recursive: true });
|
||||
execSync('git init', { cwd: tempDir, stdio: 'pipe' });
|
||||
const stateDir = join(tempDir, '.omc', 'state');
|
||||
mkdirSync(stateDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(stateDir, 'ralph-state.json'),
|
||||
JSON.stringify({ active: true, _meta: { mode: 'ralph', written_at: '2026-01-01T00:00:00Z' } }),
|
||||
);
|
||||
|
||||
const result = readModeState('ralph', nestedDir);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.active).toBe(true);
|
||||
});
|
||||
|
||||
it('should read from session path when sessionId is provided', () => {
|
||||
const sessionDir = join(tempDir, '.omc', 'state', 'sessions', 'pid-999-2000');
|
||||
mkdirSync(sessionDir, { recursive: true });
|
||||
@@ -200,6 +230,22 @@ describe('mode-state-io', () => {
|
||||
// clearModeStateFile
|
||||
// -----------------------------------------------------------------------
|
||||
describe('clearModeStateFile', () => {
|
||||
it('should clear state from the git worktree root when given a subdirectory', () => {
|
||||
const nestedDir = join(tempDir, 'nested', 'cwd');
|
||||
mkdirSync(nestedDir, { recursive: true });
|
||||
execSync('git init', { cwd: tempDir, stdio: 'pipe' });
|
||||
const stateDir = join(tempDir, '.omc', 'state');
|
||||
mkdirSync(stateDir, { recursive: true });
|
||||
const filePath = join(stateDir, 'ralph-state.json');
|
||||
writeFileSync(filePath, JSON.stringify({ active: true }));
|
||||
|
||||
const result = clearModeStateFile('ralph', nestedDir);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(existsSync(filePath)).toBe(false);
|
||||
expect(existsSync(join(nestedDir, '.omc', 'state', 'ralph-state.json'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should delete the legacy state file', () => {
|
||||
const stateDir = join(tempDir, '.omc', 'state');
|
||||
mkdirSync(stateDir, { recursive: true });
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ensureSessionStateDir,
|
||||
ensureOmcDir,
|
||||
listSessionIds,
|
||||
getWorktreeRoot,
|
||||
} from './worktree-paths.js';
|
||||
import { atomicWriteJsonSync } from './atomic-write.js';
|
||||
|
||||
@@ -49,13 +50,18 @@ export function canClearStateForSession(
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function resolveStateRoot(directory?: string): string {
|
||||
const baseDir = directory || process.cwd();
|
||||
return getWorktreeRoot(baseDir) || baseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the state file path for a given mode.
|
||||
* When sessionId is provided, returns the session-scoped path.
|
||||
* Otherwise returns the legacy (global) path.
|
||||
*/
|
||||
function resolveFile(mode: string, directory?: string, sessionId?: string): string {
|
||||
const baseDir = directory || process.cwd();
|
||||
const baseDir = resolveStateRoot(directory);
|
||||
if (sessionId) {
|
||||
return resolveSessionStatePath(mode, sessionId, baseDir);
|
||||
}
|
||||
@@ -63,7 +69,7 @@ function resolveFile(mode: string, directory?: string, sessionId?: string): stri
|
||||
}
|
||||
|
||||
function getLegacyStateCandidates(mode: string, directory?: string): string[] {
|
||||
const baseDir = directory || process.cwd();
|
||||
const baseDir = resolveStateRoot(directory);
|
||||
const normalizedName = mode.endsWith('-state') ? mode : `${mode}-state`;
|
||||
|
||||
return [
|
||||
@@ -87,13 +93,14 @@ export function findSessionOwnedStateFiles(
|
||||
directory?: string,
|
||||
): string[] {
|
||||
const matches = new Set<string>();
|
||||
const expectedPath = resolveSessionStatePath(mode, sessionId, directory);
|
||||
const baseDir = resolveStateRoot(directory);
|
||||
const expectedPath = resolveSessionStatePath(mode, sessionId, baseDir);
|
||||
if (existsSync(expectedPath)) {
|
||||
matches.add(expectedPath);
|
||||
}
|
||||
|
||||
for (const sid of listSessionIds(directory)) {
|
||||
const candidatePath = resolveSessionStatePath(mode, sid, directory);
|
||||
for (const sid of listSessionIds(baseDir)) {
|
||||
const candidatePath = resolveSessionStatePath(mode, sid, baseDir);
|
||||
if (!existsSync(candidatePath)) {
|
||||
continue;
|
||||
}
|
||||
@@ -131,7 +138,7 @@ export function writeModeState(
|
||||
sessionId?: string,
|
||||
): boolean {
|
||||
try {
|
||||
const baseDir = directory || process.cwd();
|
||||
const baseDir = resolveStateRoot(directory);
|
||||
if (sessionId) {
|
||||
ensureSessionStateDir(sessionId, baseDir);
|
||||
} else {
|
||||
@@ -199,6 +206,7 @@ export function clearModeStateFile(
|
||||
sessionId?: string,
|
||||
): boolean {
|
||||
let success = true;
|
||||
const baseDir = resolveStateRoot(directory);
|
||||
const unlinkIfPresent = (filePath: string): void => {
|
||||
if (!existsSync(filePath)) {
|
||||
return;
|
||||
@@ -214,18 +222,18 @@ export function clearModeStateFile(
|
||||
if (sessionId) {
|
||||
unlinkIfPresent(resolveFile(mode, directory, sessionId));
|
||||
} else {
|
||||
for (const legacyPath of getLegacyStateCandidates(mode, directory)) {
|
||||
for (const legacyPath of getLegacyStateCandidates(mode, baseDir)) {
|
||||
unlinkIfPresent(legacyPath);
|
||||
}
|
||||
|
||||
for (const sid of listSessionIds(directory)) {
|
||||
unlinkIfPresent(resolveSessionStatePath(mode, sid, directory));
|
||||
for (const sid of listSessionIds(baseDir)) {
|
||||
unlinkIfPresent(resolveSessionStatePath(mode, sid, baseDir));
|
||||
}
|
||||
}
|
||||
|
||||
// Ghost-legacy cleanup: if sessionId provided, also check legacy path
|
||||
if (sessionId) {
|
||||
for (const legacyPath of getLegacyStateCandidates(mode, directory)) {
|
||||
for (const legacyPath of getLegacyStateCandidates(mode, baseDir)) {
|
||||
if (!existsSync(legacyPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user