fix(core): read legacy metadata in json files (#1720)

This commit is contained in:
nikhil achale 2026-05-08 15:59:22 +05:30
parent b9b20e19d1
commit 24026caba9
3 changed files with 80 additions and 3 deletions

View File

@ -150,6 +150,30 @@ describe("writeMetadata + readMetadata", () => {
const meta = readMetadata(dataDir, "app-6");
expect(meta?.displayName).toBe("Refactor session manager");
});
it("reads legacy key=value metadata from a .json file", () => {
writeFileSync(
join(dataDir, "legacy-json-name.json"),
[
"role=orchestrator",
"status=working",
"runtimeHandle={\"id\":\"ao-orchestrator\",\"runtimeName\":\"tmux\",\"data\":{\"createdAt\":1778013362949,\"workspacePath\":\"/tmp/project\"}}",
"tmuxName=ao-orchestrator",
].join("\n"),
"utf-8",
);
const meta = readMetadata(dataDir, "legacy-json-name");
expect(meta).not.toBeNull();
expect(meta?.role).toBe("orchestrator");
expect(meta?.status).toBe("working");
expect(meta?.tmuxName).toBe("ao-orchestrator");
expect(meta?.runtimeHandle).toEqual({
id: "ao-orchestrator",
runtimeName: "tmux",
data: { createdAt: 1778013362949, workspacePath: "/tmp/project" },
});
});
});
describe("readMetadataRaw", () => {
@ -185,6 +209,28 @@ describe("readMetadataRaw", () => {
const raw = readMetadataRaw(dataDir, "raw-3");
expect(raw!["runtimeHandle"]).toBe('{"id":"foo","data":{"key":"val"}}');
});
it("reads legacy key=value metadata from a .json file", () => {
writeFileSync(
join(dataDir, "raw-legacy.json"),
[
"role=orchestrator",
"status=working",
"runtimeHandle={\"id\":\"ao-orchestrator\",\"runtimeName\":\"tmux\",\"data\":{\"workspacePath\":\"/tmp/project\"}}",
"tmuxName=ao-orchestrator",
].join("\n"),
"utf-8",
);
const raw = readMetadataRaw(dataDir, "raw-legacy");
expect(raw).not.toBeNull();
expect(raw?.["role"]).toBe("orchestrator");
expect(raw?.["status"]).toBe("working");
expect(raw?.["tmuxName"]).toBe("ao-orchestrator");
expect(raw?.["runtimeHandle"]).toBe(
"{\"id\":\"ao-orchestrator\",\"runtimeName\":\"tmux\",\"data\":{\"workspacePath\":\"/tmp/project\"}}",
);
});
});
describe("updateMetadata", () => {

View File

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdirSync, readFileSync, existsSync } from "node:fs";
import { mkdirSync, readFileSync, existsSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { createSessionManager } from "../../session-manager.js";
import { validateConfig } from "../../config.js";
@ -1710,6 +1710,32 @@ describe("spawn", () => {
expect(mockWorkspace.create).toHaveBeenCalledTimes(1);
});
it("ensureOrchestrator reuses a canonical orchestrator stored as legacy key=value metadata in a .json file", async () => {
const runtimeHandle = makeHandle("ao-orchestrator");
writeFileSync(
join(sessionsDir, "app-orchestrator.json"),
[
"role=orchestrator",
"project=my-app",
"status=working",
"branch=orchestrator/app-orchestrator",
`worktree=${join(tmpDir, "legacy-orchestrator")}`,
`runtimeHandle=${JSON.stringify(runtimeHandle)}`,
"tmuxName=ao-orchestrator",
].join("\n"),
"utf-8",
);
const sm = createSessionManager({ config, registry: mockRegistry });
const session = await sm.ensureOrchestrator({ projectId: "my-app" });
expect(session.id).toBe("app-orchestrator");
expect(session.metadata["role"]).toBe("orchestrator");
expect(session.runtimeHandle).toEqual(runtimeHandle);
expect(mockRuntime.create).not.toHaveBeenCalled();
expect(mockWorkspace.create).not.toHaveBeenCalled();
});
it("ensureOrchestrator coalesces concurrent creation calls", async () => {
const sm = createSessionManager({ config, registry: mockRegistry });

View File

@ -35,6 +35,7 @@ import { assertValidSessionIdComponent, SESSION_ID_COMPONENT_PATTERN } from "./u
import { flattenToStringRecord } from "./utils/metadata-flatten.js";
import { validateStatus } from "./utils/validation.js";
import { withFileLockSync } from "./file-lock.js";
import { parseKeyValueContent } from "./key-value.js";
const JSON_EXTENSION = ".json";
@ -43,14 +44,18 @@ function serializeMetadata(data: Record<string, unknown>): string {
return JSON.stringify(data, null, 2) + "\n";
}
/** Parse JSON metadata file content. Returns null on invalid JSON. */
/** Parse metadata file content. Supports JSON and legacy key=value content. */
function parseMetadataContent(content: string): Record<string, unknown> | null {
const trimmed = content.trimStart();
try {
const parsed = JSON.parse(content);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
return parsed as Record<string, unknown>;
} catch {
return null;
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return null;
const parsed = parseKeyValueContent(content);
return Object.keys(parsed).length > 0 ? parsed : null;
}
}