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

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:
Cliff Hall 2026-07-24 17:20:12 -04:00 • committed by GitHub
commit 373f346e5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 2210 additions and 467 deletions

View file

@ -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}

View file

@ -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(
{

View file

@ -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 .",

View file

@ -1660,6 +1660,7 @@ function App({
<InfoTab
serverName={selectedServer}
serverConfig={selectedServerConfig}
serverSettings={selectedServerEntry?.settings ?? null}
serverState={currentServerState}
width={contentWidth}
height={contentHeight}

View file

@ -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>

View file

@ -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,

View file

@ -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"]
}