fix(core): read legacy metadata in json files (#1720)
This commit is contained in:
parent
b9b20e19d1
commit
24026caba9
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue