fix(sessions): fall back when lastDirectory is a deleted worktree
New chats inherited a persisted lastDirectory even after that worktree was removed, so the first message saved but the prompt never started. Validate the implicit draft directory, fall back to the active project only when OpenCode confirms the path is missing, and leave explicit worktree targets and unknown probes unchanged. Co-authored-by: serkraser <serkraser@gmail.com>
This commit is contained in:
co-authored by
serkraser
parent
6b1e677aaf
commit
90e79b04a4
@@ -9,6 +9,7 @@ let configCalls = 0;
|
||||
let runtimeKey = 'test-runtime';
|
||||
const promptAsyncCalls: unknown[][] = [];
|
||||
const promptAsyncResults: Array<unknown> = [];
|
||||
const pathGetResults: Array<unknown> = [];
|
||||
|
||||
const promptAsyncMock = mock(async (...args: unknown[]) => {
|
||||
promptAsyncCalls.push(args);
|
||||
@@ -17,6 +18,12 @@ const promptAsyncMock = mock(async (...args: unknown[]) => {
|
||||
return next ?? { response: new Response(null, { status: 200 }) };
|
||||
});
|
||||
|
||||
const pathGetMock = mock(async () => {
|
||||
const next = pathGetResults.shift();
|
||||
if (next instanceof Error) throw next;
|
||||
return next ?? { data: { directory: '/workspace/project' } };
|
||||
});
|
||||
|
||||
mock.module('@opencode-ai/sdk/v2', () => ({
|
||||
createOpencodeClient: mock(() => ({
|
||||
config: {
|
||||
@@ -30,6 +37,9 @@ mock.module('@opencode-ai/sdk/v2', () => ({
|
||||
session: {
|
||||
promptAsync: promptAsyncMock,
|
||||
},
|
||||
path: {
|
||||
get: pathGetMock,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -64,6 +74,17 @@ beforeEach(() => {
|
||||
runtimeKey = 'test-runtime';
|
||||
promptAsyncCalls.length = 0;
|
||||
promptAsyncResults.length = 0;
|
||||
pathGetResults.length = 0;
|
||||
});
|
||||
|
||||
describe('opencodeClient directory availability', () => {
|
||||
test('distinguishes a missing directory from an unavailable path probe', async () => {
|
||||
pathGetResults.push({ error: { code: 'ENOENT', message: 'no such file or directory' } });
|
||||
expect(await opencodeClient.getDirectoryAvailability('/private/deleted-worktree')).toBe('missing');
|
||||
|
||||
pathGetResults.push(new Error('offline'));
|
||||
expect(await opencodeClient.getDirectoryAvailability('/private/deleted-worktree')).toBe('unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('opencodeClient getConfig cache', () => {
|
||||
|
||||
@@ -68,6 +68,21 @@ type SdkResult<T> = {
|
||||
response?: { status?: number };
|
||||
};
|
||||
|
||||
export type DirectoryAvailability = "available" | "missing" | "unknown";
|
||||
|
||||
const isMissingDirectoryError = (error: unknown): boolean => {
|
||||
if (error instanceof FilesystemError) {
|
||||
return error.reason === "not-found" || error.reason === "not-directory";
|
||||
}
|
||||
if (error && typeof error === "object") {
|
||||
const code = (error as { code?: unknown }).code;
|
||||
if (code === "ENOENT" || code === "ENOTDIR") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return /\bENOENT\b|\bENOTDIR\b|no such file or directory/i.test(formatSdkError(error));
|
||||
};
|
||||
|
||||
function unwrapSdkData<T>(result: SdkResult<T>, operation: string): T {
|
||||
if (result.error) {
|
||||
const status = result.response?.status;
|
||||
@@ -506,17 +521,28 @@ class OpencodeService {
|
||||
* This is intentionally NOT the same as local filesystem access in the UI runtime.
|
||||
*/
|
||||
async probeDirectory(directory: string): Promise<boolean> {
|
||||
return (await this.getDirectoryAvailability(directory)) === "available";
|
||||
}
|
||||
|
||||
/**
|
||||
* Distinguishes a confirmed-missing directory from an unavailable probe.
|
||||
* Offline, permission, and other transport failures stay `unknown` so callers
|
||||
* do not treat a temporary outage as proof the path was deleted.
|
||||
*/
|
||||
async getDirectoryAvailability(directory: string): Promise<DirectoryAvailability> {
|
||||
const normalized = this.normalizeCandidatePath(directory);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
return "unknown";
|
||||
}
|
||||
try {
|
||||
const response = await this.client.path.get({ directory: normalized });
|
||||
const info = response.data as { directory?: unknown } | undefined;
|
||||
const returned = typeof info?.directory === 'string' ? info.directory : null;
|
||||
return Boolean(returned && returned.trim().length > 0);
|
||||
} catch {
|
||||
return false;
|
||||
const response = await this.client.path.get({ directory: normalized }) as SdkResult<{ directory?: unknown }>;
|
||||
if (response.error) {
|
||||
return isMissingDirectoryError(response.error) ? "missing" : "unknown";
|
||||
}
|
||||
const returned = typeof response.data?.directory === "string" ? response.data.directory.trim() : "";
|
||||
return returned ? "available" : "unknown";
|
||||
} catch (error) {
|
||||
return isMissingDirectoryError(error) ? "missing" : "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user