Compare sandbox Claude MCP identities independent of ordering
Canonicalize known external identity triples while preserving duplicate multiplicity and rejecting unknown fields. Keep local comparison behavior unchanged. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
355590536e
commit
2959fcb43d
|
|
@ -146,6 +146,7 @@ export function claudeSessionMcpServersMatch(input: {
|
|||
try {
|
||||
const value: unknown = JSON.parse(raw);
|
||||
if (!Array.isArray(value) || !value.every((entry) => entry && typeof entry === "object"
|
||||
&& Object.keys(entry).length === 3
|
||||
&& typeof entry.name === "string" && typeof entry.url === "string" && typeof entry.connectionId === "string")) return null;
|
||||
return value;
|
||||
} catch { return null; }
|
||||
|
|
@ -170,8 +171,11 @@ export function claudeSessionMcpServersMatch(input: {
|
|||
}
|
||||
const saved = parse(input.savedIdentity);
|
||||
if (!saved || !current) return false;
|
||||
return JSON.stringify(saved.filter((entry) => !builtin(entry)))
|
||||
=== JSON.stringify(current.filter((entry) => !builtin(entry)));
|
||||
// Database order and JSON property order are not tool identity. Keep every
|
||||
// occurrence so adding or removing a duplicate still changes the identity.
|
||||
const externalIdentities = (entries: typeof saved) => entries.filter((entry) => !builtin(entry))
|
||||
.map((entry) => JSON.stringify([entry.name, entry.url, entry.connectionId])).sort();
|
||||
return JSON.stringify(externalIdentities(saved)) === JSON.stringify(externalIdentities(current));
|
||||
}
|
||||
|
||||
export function claudeSessionCwdMatchesExecutionTarget(input: {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,30 @@ describe("legacy sandbox conversation persistence", () => {
|
|||
expect(claudeSessionMcpServersMatch({ ...input, savedIdentity: "malformed" })).toBe(false);
|
||||
});
|
||||
|
||||
it("matches reordered sandbox external MCP identities without losing duplicate multiplicity", () => {
|
||||
const first = { name: "First tool", url: "https://tools.test/first", connectionId: "external:first" };
|
||||
const second = { name: "Second tool", url: "https://tools.test/second", connectionId: "external:second" };
|
||||
const builtin = { name: "Paperclip projects", url: "https://paperclip.test/api/mcp/project-tools", connectionId: "paperclip-project-tools" };
|
||||
const input = { savedIdentity: JSON.stringify([first, second]), currentIdentity: JSON.stringify([second, builtin, first]),
|
||||
currentConnectionIds: [second.connectionId, builtin.connectionId, first.connectionId], legacyPlatformSession: false,
|
||||
paperclipApiUrl: "https://paperclip.test", sandboxUpgrade: true };
|
||||
expect(claudeSessionMcpServersMatch(input)).toBe(true);
|
||||
expect(claudeSessionMcpServersMatch({ ...input, sandboxUpgrade: false })).toBe(false);
|
||||
expect(claudeSessionMcpServersMatch({ ...input, currentIdentity: JSON.stringify([
|
||||
{ connectionId: second.connectionId, url: second.url, name: second.name }, builtin, first,
|
||||
]) })).toBe(true);
|
||||
for (const entries of [
|
||||
[second, builtin],
|
||||
[second, builtin, first, first],
|
||||
[second, builtin, { ...first, url: "https://foreign.test/first" }],
|
||||
[second, builtin, { ...first, name: "Changed tool" }],
|
||||
[second, builtin, { ...first, connectionId: "external:replacement" }],
|
||||
[second, builtin, { ...first, permissionScope: "unknown" }],
|
||||
]) expect(claudeSessionMcpServersMatch({ ...input, currentIdentity: JSON.stringify(entries) })).toBe(false);
|
||||
expect(claudeSessionMcpServersMatch({ ...input, savedIdentity: JSON.stringify([first, first, second]) })).toBe(false);
|
||||
expect(claudeSessionMcpServersMatch({ ...input, savedIdentity: JSON.stringify([{ ...first, permissionScope: "unknown" }, second]) })).toBe(false);
|
||||
});
|
||||
|
||||
for (const [name, codec] of [["codex_local", codex], ["claude_local", claude]] as const) {
|
||||
it(`${name} recovers old metadata, persists it, and resumes across host leases`, () => {
|
||||
const input = fixture(); input.adapterType = name;
|
||||
|
|
|
|||
Loading…
Reference in New Issue