chore: close quality-gate coverage gaps (#1689) (#1766)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
Closes #1689. Typecheck cli/tui src, add a core/ format+lint gate, and gate core/json + core/client coverage. See PR #1766 for the full review history.
This commit is contained in:
parent
d76c4d5379
commit
373f346e5f
7 changed files with 2210 additions and 467 deletions
|
|
@ -11,6 +11,7 @@ import type {
|
|||
MCPServerConfig,
|
||||
ServerState,
|
||||
} from "@inspector/core/mcp/index.js";
|
||||
import { headersToServerSettings } from "@inspector/core/mcp/node/servers.js";
|
||||
import { InfoTab } from "../src/components/InfoTab.js";
|
||||
|
||||
// Ink processes stdin keypresses asynchronously — await this after stdin.write
|
||||
|
|
@ -127,14 +128,11 @@ describe("InfoTab", () => {
|
|||
type: "sse",
|
||||
url: "https://example.com/sse",
|
||||
};
|
||||
const withHeaders = {
|
||||
...config,
|
||||
headers: { Authorization: "Bearer x" },
|
||||
} as unknown as MCPServerConfig;
|
||||
const { lastFrame } = render(
|
||||
<InfoTab
|
||||
serverName="my-server"
|
||||
serverConfig={withHeaders}
|
||||
serverConfig={config}
|
||||
serverSettings={headersToServerSettings({ Authorization: "Bearer x" })}
|
||||
serverState={baseState}
|
||||
width={80}
|
||||
height={20}
|
||||
|
|
@ -167,15 +165,15 @@ describe("InfoTab", () => {
|
|||
});
|
||||
|
||||
it("renders a streamable-http config with headers", () => {
|
||||
const config = {
|
||||
const config: MCPServerConfig = {
|
||||
type: "streamable-http",
|
||||
url: "https://example.com/mcp",
|
||||
headers: { "X-Key": "abc" },
|
||||
} as unknown as MCPServerConfig;
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<InfoTab
|
||||
serverName="my-server"
|
||||
serverConfig={config}
|
||||
serverSettings={headersToServerSettings({ "X-Key": "abc" })}
|
||||
serverState={baseState}
|
||||
width={80}
|
||||
height={20}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,23 @@ describe("schemaToForm", () => {
|
|||
expect(form.sections[0]?.fields).toEqual([]);
|
||||
});
|
||||
|
||||
it("treats a non-object property value as an empty schema instead of throwing", () => {
|
||||
// A malformed server schema whose property value is null/primitive must not
|
||||
// crash — the field degrades to a plain string input labelled by its key.
|
||||
const form = schemaToForm(
|
||||
{ properties: { bad: null, worse: 42 } },
|
||||
"malformed",
|
||||
);
|
||||
const fields = form.sections[0]?.fields ?? [];
|
||||
expect(fields).toHaveLength(2);
|
||||
expect(
|
||||
fields.map((f) => ({ name: f.name, type: f.type, label: f.label })),
|
||||
).toEqual([
|
||||
{ name: "bad", type: "string", label: "bad" },
|
||||
{ name: "worse", type: "string", label: "worse" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps each JSON Schema type to the matching ink-form field type", () => {
|
||||
const form = schemaToForm(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
"scripts": {
|
||||
"dev": "vite-node --config vitest.config.ts dev.ts",
|
||||
"build": "tsup",
|
||||
"validate": "npm run format:check && npm run lint && npm run build && npm run test",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||
"validate": "npm run format:check && npm run lint && npm run typecheck && npm run build && npm run test",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint .",
|
||||
|
|
|
|||
|
|
@ -1660,6 +1660,7 @@ function App({
|
|||
<InfoTab
|
||||
serverName={selectedServer}
|
||||
serverConfig={selectedServerConfig}
|
||||
serverSettings={selectedServerEntry?.settings ?? null}
|
||||
serverState={currentServerState}
|
||||
width={contentWidth}
|
||||
height={contentHeight}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,16 @@ import type {
|
|||
MCPServerConfig,
|
||||
ServerState,
|
||||
} from "@inspector/core/mcp/index.js";
|
||||
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
|
||||
|
||||
interface InfoTabProps {
|
||||
serverName: string | null;
|
||||
serverConfig: MCPServerConfig | null;
|
||||
// HTTP headers live on the per-server settings (pair-array form), not on the
|
||||
// transport `serverConfig` — `mcpConfigToServerEntries` lifts the on-disk
|
||||
// `headers` map into `InspectorServerSettings.headers`. Read them here so the
|
||||
// SSE / streamable-http header display reflects the real source.
|
||||
serverSettings?: InspectorServerSettings | null;
|
||||
serverState: ServerState | null;
|
||||
width: number;
|
||||
height: number;
|
||||
|
|
@ -18,11 +24,24 @@ interface InfoTabProps {
|
|||
export function InfoTab({
|
||||
serverName,
|
||||
serverConfig,
|
||||
serverSettings = null,
|
||||
serverState,
|
||||
width,
|
||||
height,
|
||||
focused = false,
|
||||
}: InfoTabProps) {
|
||||
const headerPairs = serverSettings?.headers ?? [];
|
||||
// Shared header display for the sse / streamable-http branches (identical for
|
||||
// both transports — the header source is `serverSettings`, not the transport).
|
||||
const headersBlock =
|
||||
headerPairs.length > 0 ? (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Headers:{" "}
|
||||
{headerPairs.map(({ key, value }) => `${key}=${value}`).join(", ")}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null;
|
||||
const scrollViewRef = useRef<ScrollViewRef>(null);
|
||||
|
||||
// Handle keyboard input for scrolling
|
||||
|
|
@ -111,33 +130,13 @@ export function InfoTab({
|
|||
<>
|
||||
<Text dimColor>Type: sse</Text>
|
||||
<Text dimColor>URL: {serverConfig.url}</Text>
|
||||
{serverConfig.headers &&
|
||||
Object.keys(serverConfig.headers).length > 0 && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Headers:{" "}
|
||||
{Object.entries(serverConfig.headers)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(", ")}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{headersBlock}
|
||||
</>
|
||||
) : serverConfig.type === "streamable-http" ? (
|
||||
<>
|
||||
<Text dimColor>Type: streamable-http</Text>
|
||||
<Text dimColor>URL: {serverConfig.url}</Text>
|
||||
{serverConfig.headers &&
|
||||
Object.keys(serverConfig.headers).length > 0 && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Headers:{" "}
|
||||
{Object.entries(serverConfig.headers)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(", ")}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{headersBlock}
|
||||
</>
|
||||
) : null}
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -34,9 +34,14 @@ function toSelectOptions(
|
|||
}));
|
||||
}
|
||||
|
||||
/** Minimal JSON Schema object shape (properties + required) */
|
||||
/**
|
||||
* Minimal JSON Schema object shape (properties + required). Property values are
|
||||
* `unknown` so the SDK's broadly-typed `Tool["inputSchema"]` (whose `properties`
|
||||
* values are the recursive JSON type) is assignable here; each value is narrowed
|
||||
* to {@link JsonSchemaProperty} at the point of use below.
|
||||
*/
|
||||
interface JsonSchemaObject {
|
||||
properties?: Record<string, JsonSchemaProperty>;
|
||||
properties?: Record<string, unknown>;
|
||||
required?: string[];
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +65,12 @@ export function schemaToForm(
|
|||
const required = schema.required || [];
|
||||
|
||||
for (const [key, prop] of Object.entries(properties)) {
|
||||
const property = prop as JsonSchemaProperty;
|
||||
// `properties` values are `unknown` (the SDK schema admits anything), so
|
||||
// guard before treating a value as a schema object — a malformed server
|
||||
// schema with e.g. `properties: { foo: null }` must not throw on `.title`.
|
||||
const property = (
|
||||
typeof prop === "object" && prop !== null ? prop : {}
|
||||
) as JsonSchemaProperty;
|
||||
const baseField = {
|
||||
name: key,
|
||||
label: property.title || key,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,30 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "./build",
|
||||
"rootDir": ".",
|
||||
// Match clients/web/tsconfig.app.json's module/lib *resolution* options so
|
||||
// the imported `core/` sources are validated the same way web's own gate
|
||||
// validates them: DOM lib + bundler resolution are what core/ (browser-side
|
||||
// auth, DOM types) expects, `noUncheckedIndexedAccess` is off because web's
|
||||
// app config does not extend tsconfig.base (re-enabling it would flag core/
|
||||
// issues web never gates), and the `react` path redirect lets core/ source
|
||||
// resolve its bare `react` import from this client's node_modules. This does
|
||||
// NOT mirror web's extra strictness flags (noUnusedLocals, verbatimModule-
|
||||
// Syntax, ES2023 target, …), so tui's own src is checked slightly more
|
||||
// loosely than web's src.
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"types": ["node"],
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"module": "ESNext",
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"paths": {
|
||||
"@inspector/core/*": ["../../core/*"]
|
||||
"@inspector/core/*": ["../../core/*"],
|
||||
"react": ["./node_modules/@types/react"],
|
||||
"react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime"]
|
||||
}
|
||||
},
|
||||
"include": ["index.ts", "tui.tsx", "src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.test.ts", "build"]
|
||||
"exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx", "build"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue