fix: resolve remaining PR315 Bugbot findings
This commit is contained in:
parent
11ea174a81
commit
c8f55e712c
|
|
@ -0,0 +1,16 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { asValidOpenCodeSessionId } from "../opencode-session-id.js";
|
||||
|
||||
describe("asValidOpenCodeSessionId", () => {
|
||||
it("accepts valid OpenCode session ids", () => {
|
||||
expect(asValidOpenCodeSessionId("ses_abc123")).toBe("ses_abc123");
|
||||
expect(asValidOpenCodeSessionId(" ses_ABC-123_xyz ")).toBe("ses_ABC-123_xyz");
|
||||
});
|
||||
|
||||
it("rejects invalid OpenCode session ids", () => {
|
||||
expect(asValidOpenCodeSessionId("")).toBeUndefined();
|
||||
expect(asValidOpenCodeSessionId("ses bad")).toBeUndefined();
|
||||
expect(asValidOpenCodeSessionId("abc123")).toBeUndefined();
|
||||
expect(asValidOpenCodeSessionId(123)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1969,6 +1969,34 @@ describe("remap", () => {
|
|||
const meta = readMetadataRaw(sessionsDir, "app-1");
|
||||
expect(meta?.["opencodeSessionId"]).toBe("ses_discovered");
|
||||
});
|
||||
|
||||
it("prefers most recently updated title match when remapping", async () => {
|
||||
const deleteLogPath = join(tmpDir, "opencode-delete-remap-ordering.log");
|
||||
const mockBin = installMockOpencode(
|
||||
JSON.stringify([
|
||||
{ id: "ses_old", title: "AO:app-1", updated: "2025-01-01T00:00:00.000Z" },
|
||||
{ id: "ses_new", title: "AO:app-1", updated: 1_772_777_328_000 },
|
||||
]),
|
||||
deleteLogPath,
|
||||
);
|
||||
process.env.PATH = `${mockBin}:${originalPath ?? ""}`;
|
||||
|
||||
writeMetadata(sessionsDir, "app-1", {
|
||||
worktree: "/tmp",
|
||||
branch: "main",
|
||||
status: "working",
|
||||
project: "my-app",
|
||||
agent: "opencode",
|
||||
runtimeHandle: JSON.stringify(makeHandle("rt-1")),
|
||||
});
|
||||
|
||||
const sm = createSessionManager({ config, registry: mockRegistry });
|
||||
const mapped = await sm.remap("app-1");
|
||||
|
||||
expect(mapped).toBe("ses_new");
|
||||
const meta = readMetadataRaw(sessionsDir, "app-1");
|
||||
expect(meta?.["opencodeSessionId"]).toBe("ses_new");
|
||||
});
|
||||
});
|
||||
|
||||
describe("spawnOrchestrator", () => {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export type { OrchestratorPromptConfig } from "./orchestrator-prompt.js";
|
|||
|
||||
// Shared utilities
|
||||
export { shellEscape, escapeAppleScript, validateUrl, readLastJsonlEntry } from "./utils.js";
|
||||
export { asValidOpenCodeSessionId } from "./opencode-session-id.js";
|
||||
|
||||
// Path utilities — hash-based directory structure
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
const OPENCODE_SESSION_ID_RE = /^ses_[A-Za-z0-9_-]+$/;
|
||||
|
||||
export function asValidOpenCodeSessionId(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length === 0) return undefined;
|
||||
return OPENCODE_SESSION_ID_RE.test(trimmed) ? trimmed : undefined;
|
||||
}
|
||||
|
|
@ -60,17 +60,19 @@ import {
|
|||
generateConfigHash,
|
||||
validateAndStoreOrigin,
|
||||
} from "./paths.js";
|
||||
import { asValidOpenCodeSessionId } from "./opencode-session-id.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const OPENCODE_DISCOVERY_TIMEOUT_MS = 2_000;
|
||||
const OPENCODE_INTERACTIVE_DISCOVERY_TIMEOUT_MS = 10_000;
|
||||
const OPENCODE_SESSION_ID_RE = /^ses_[A-Za-z0-9_-]+$/;
|
||||
|
||||
function asValidOpenCodeSessionId(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length === 0) return undefined;
|
||||
return OPENCODE_SESSION_ID_RE.test(trimmed) ? trimmed : undefined;
|
||||
function scoreOpenCodeUpdated(value: unknown): number {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string") {
|
||||
const parsed = Date.parse(value);
|
||||
if (!Number.isNaN(parsed)) return parsed;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function errorIncludesSessionNotFound(err: unknown): boolean {
|
||||
|
|
@ -115,11 +117,13 @@ async function discoverOpenCodeSessionIdsByTitle(
|
|||
const parsed = safeJsonParse<Array<Record<string, unknown>>>(stdout);
|
||||
if (!parsed) return [];
|
||||
const title = `AO:${sessionId}`;
|
||||
const candidates = parsed.filter((entry) => {
|
||||
const candidateTitle = typeof entry["title"] === "string" ? entry["title"] : "";
|
||||
const candidateId = typeof entry["id"] === "string" ? entry["id"] : "";
|
||||
return candidateTitle === title && candidateId.length > 0;
|
||||
});
|
||||
const candidates = parsed
|
||||
.filter((entry) => {
|
||||
const candidateTitle = typeof entry["title"] === "string" ? entry["title"] : "";
|
||||
const candidateId = typeof entry["id"] === "string" ? entry["id"] : "";
|
||||
return candidateTitle === title && candidateId.length > 0;
|
||||
})
|
||||
.sort((a, b) => scoreOpenCodeUpdated(b["updated"]) - scoreOpenCodeUpdated(a["updated"]));
|
||||
|
||||
return candidates
|
||||
.map((entry) => asValidOpenCodeSessionId(entry["id"]))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
shellEscape,
|
||||
asValidOpenCodeSessionId,
|
||||
type Agent,
|
||||
type AgentSessionInfo,
|
||||
type AgentLaunchConfig,
|
||||
|
|
@ -20,15 +21,6 @@ interface OpenCodeSessionListEntry {
|
|||
updated?: string;
|
||||
}
|
||||
|
||||
const OPENCODE_SESSION_ID_RE = /^ses_[A-Za-z0-9_-]+$/;
|
||||
|
||||
function asValidOpenCodeSessionId(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length === 0) return undefined;
|
||||
return OPENCODE_SESSION_ID_RE.test(trimmed) ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function parseSessionList(raw: string): OpenCodeSessionListEntry[] {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export function DirectTerminal({
|
|||
const [status, setStatus] = useState<"connecting" | "connected" | "error">("connecting");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [reloading, setReloading] = useState(false);
|
||||
const [reloadError, setReloadError] = useState<string | null>(null);
|
||||
const [resolvedReloadCommand, setResolvedReloadCommand] = useState<string | null>(null);
|
||||
|
||||
// Update URL when fullscreen changes
|
||||
|
|
@ -74,6 +75,7 @@ export function DirectTerminal({
|
|||
|
||||
async function handleReload(): Promise<void> {
|
||||
if (!isOpenCodeSession || reloading) return;
|
||||
setReloadError(null);
|
||||
setReloading(true);
|
||||
try {
|
||||
let commandToSend = resolvedReloadCommand ?? reloadCommand;
|
||||
|
|
@ -96,11 +98,16 @@ export function DirectTerminal({
|
|||
setResolvedReloadCommand(commandToSend);
|
||||
}
|
||||
|
||||
await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/send`, {
|
||||
const sendRes = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/send`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: commandToSend }),
|
||||
});
|
||||
if (!sendRes.ok) {
|
||||
throw new Error(`Failed to send reload command: ${sendRes.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setReloadError(err instanceof Error ? err.message : "Failed to reload OpenCode session");
|
||||
} finally {
|
||||
setReloading(false);
|
||||
}
|
||||
|
|
@ -590,6 +597,14 @@ export function DirectTerminal({
|
|||
)}
|
||||
</button>
|
||||
) : null}
|
||||
{reloadError ? (
|
||||
<span
|
||||
className="max-w-[40ch] truncate text-[10px] font-medium text-[var(--color-status-error)]"
|
||||
title={reloadError}
|
||||
>
|
||||
{reloadError}
|
||||
</span>
|
||||
) : null}
|
||||
<button
|
||||
onClick={() => setFullscreen(!fullscreen)}
|
||||
className={cn(
|
||||
|
|
|
|||
Loading…
Reference in New Issue