fix: address Copilot review on the _meta widening
- Widen the three `_meta` annotations the bulk pass missed, all of them
return types rather than identifier declarations: `mergeMeta`,
`aggregateListParams`, and the `tools/call` params object. The second was
unsound in a way callers could see — it advertised
`{ _meta: Record<string, string> }` while the runtime carried nested JSON.
- `normalizeStoredMetadata`: keep an explicit `null` from a legacy pair
instead of folding it into `""` (only a missing `value` defaults now), and
store through `Object.defineProperty` so a legacy `__proto__` key becomes an
own property rather than hitting the prototype setter.
- Drop the unjustified `as unknown as` from the migration test; the legacy
fixture is untyped and cast once where it crosses into the typed parser.
- The TUI settings test asserted only that the app mounted, so it could not
have caught a pair array being forwarded as numeric `_meta` keys. The fake
client now captures its constructor options and the test asserts the nested
object arrives intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
04b5e70a53
commit
0f628a8174
1 changed files with 67 additions and 16 deletions
|
|
@ -1424,6 +1424,26 @@ describe("normalizeStoredMetadata (#1910)", () => {
|
|||
).toEqual({ kept: "yes" });
|
||||
});
|
||||
|
||||
it("preserves an explicit null in a legacy pair rather than blanking it", () => {
|
||||
// `null` is a legal `_meta` value, so the migration must not fold it into
|
||||
// `""` — only a genuinely absent `value` defaults.
|
||||
expect(normalizeStoredMetadata([{ key: "z", value: null }])).toEqual({
|
||||
z: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("stores a legacy `__proto__` key as an own property", () => {
|
||||
// Plain assignment would hit the prototype setter: the entry would vanish
|
||||
// (and an object value would mutate the prototype) instead of being stored.
|
||||
const out = normalizeStoredMetadata([
|
||||
{ key: "__proto__", value: { polluted: true } },
|
||||
]);
|
||||
expect(Object.hasOwn(out, "__proto__")).toBe(true);
|
||||
expect(out["__proto__"]).toEqual({ polluted: true });
|
||||
expect(Object.getPrototypeOf(out)).toBe(Object.prototype);
|
||||
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reads a legacy pair with no value as an empty string", () => {
|
||||
expect(normalizeStoredMetadata([{ key: "k" }])).toEqual({ k: "" });
|
||||
});
|
||||
|
|
@ -1460,18 +1480,19 @@ describe("normalizeStoredMetadata (#1910)", () => {
|
|||
it("migrates a legacy file to the object shape on the next write", () => {
|
||||
// Read side accepts the pair array; write side emits only the object, so
|
||||
// one round-trip is the migration.
|
||||
const entries = mcpConfigToServerEntries({
|
||||
// The legacy on-disk shape no longer matches `MCPConfig` — that is what
|
||||
// the test is about — so the fixture is untyped and cast once where it
|
||||
// crosses into the typed parser, the same boundary a real file crosses.
|
||||
const legacyFile: unknown = {
|
||||
mcpServers: {
|
||||
legacy: {
|
||||
type: "streamable-http",
|
||||
url: "https://x.test/mcp",
|
||||
metadata: [{ key: "tenant", value: "acme" }] as unknown as Record<
|
||||
string,
|
||||
never
|
||||
>,
|
||||
metadata: [{ key: "tenant", value: "acme" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
const entries = mcpConfigToServerEntries(legacyFile as MCPConfig);
|
||||
expect(entries[0]?.settings?.metadata).toEqual({ tenant: "acme" });
|
||||
const round = serverEntriesToMcpConfig(entries);
|
||||
expect(round.mcpServers.legacy?.metadata).toEqual({ tenant: "acme" });
|
||||
|
|
|
|||
Loading…
Reference in a new issue