chore(lint): enable no-floating-promises in all five ESLint scopes
A floated promise is invisible at review time — the call reads like an awaited one minus four characters — and the unhandled rejection it produces surfaces in a different test, in a different file, as a stack pointing at SDK internals. In #1947 two un-held `callTool` calls made `npm run ci` unpassable and took a full investigation to attribute; the fix was two lines. `@typescript-eslint/no-floating-promises` catches the class outright, so enable it at `error` everywhere. The rule is type-aware and `tseslint.configs.recommended` is not, so each scope's config gains a parser project. The four clients name their own tsconfig projects (both of them — `src` and `__tests__`/`server` live in different ones). The root scope has no tsconfig at all, so add `tsconfig.lint.json`: a lint-only project covering `core/**`, `test-servers/src/**`, and `vitest.shared.mts`, emitting nothing and gating nothing. It sets `moduleResolution: bundler` deliberately — under NodeNext `core/`'s extensionless imports fail to resolve, every import degrades to `any`, and the rule silently stops seeing promises at all. Measured surface, and the fixes: - core (2) — `stream.close()` in two Hono `onAbort` listeners. Hono invokes subscribers with a bare `subscriber()`, so a promise returned from the listener is floated one layer up; `closeAbortedStream` owns the rejection where it can actually be owned. - tui (5) — `handleDisconnect` had no catch of its own and its only caller is a key handler that cannot await, so a failing disconnect was a live unhandled rejection; it now surfaces the message the way `handleConnect` does. The rest are `void` at callers that genuinely cannot await, over callees that already own their failures. - web (92) — 83 are Storybook `expect(...)`, which is instrumented and returns a promise, now awaited (`scrollAreaStoryAssertions` is async for the same reason). Two tests were asserting against a provider call they never awaited. `open(url)` in both web servers now warns instead of escaping a synchronous callback. - cli, launcher — already clean. Type-aware linting costs time: `clients/web`'s `eslint .` goes from ~8s to ~19s, inside `validate`. Narrowing the loaded projects is the lever if that needs reducing later; dropping the rule is not. Closes #1959 Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
6bc355df63
commit
519ecf49ee
34 changed files with 359 additions and 111 deletions
|
|
@ -31,5 +31,32 @@ export default defineConfig([
|
|||
"react-refresh/only-export-components": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Type-aware pass for `no-floating-promises` (#1959). The rule needs type
|
||||
// information, which `tseslint.configs.recommended` does not provide, and
|
||||
// the parser needs a project that literally contains the linted file — so
|
||||
// all four leaf projects are listed (`tsconfig.json` itself is a
|
||||
// solution file with `files: []`, so it contains nothing).
|
||||
//
|
||||
// `.d.ts` is excluded: `vitest.shims.d.ts` is pulled in through a `types`
|
||||
// reference rather than an `include`, so no project owns it, and a
|
||||
// declaration file cannot float a promise anyway.
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
ignores: ["**/*.d.ts"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: [
|
||||
"./tsconfig.app.json",
|
||||
"./tsconfig.node.json",
|
||||
"./tsconfig.storybook.json",
|
||||
"./tsconfig.test.json",
|
||||
],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
},
|
||||
},
|
||||
...storybook.configs["flat/recommended"],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -125,7 +125,13 @@ export async function startHonoServer(
|
|||
sandboxUrl ?? undefined,
|
||||
);
|
||||
if (config.autoOpen) {
|
||||
open(url);
|
||||
// A headless or otherwise browser-less host rejects here. The server
|
||||
// is already listening and the banner above printed the URL, so warn
|
||||
// and carry on rather than letting the rejection escape this
|
||||
// synchronous listen callback and take the process down.
|
||||
open(url).catch((err: unknown) => {
|
||||
console.warn("Could not open a browser automatically:", err);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -114,7 +114,12 @@ export function honoMiddlewarePlugin(config: WebServerConfig): Plugin {
|
|||
);
|
||||
|
||||
if (config.autoOpen) {
|
||||
open(url);
|
||||
// Same as the prod server: a browser-less host rejects, the dev
|
||||
// server is already up, and the banner above printed the URL — so
|
||||
// warn rather than let the rejection escape this sync helper.
|
||||
open(url).catch((err: unknown) => {
|
||||
console.warn("Could not open a browser automatically:", err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const Transport: Story = {
|
|||
args: { category: "transport" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("transport")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("transport")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -22,6 +22,6 @@ export const Auth: Story = {
|
|||
args: { category: "auth" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("auth")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("auth")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const FullSize: Story = {
|
|||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("Row 1")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("Row 1")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -38,6 +38,6 @@ export const Embedded: Story = {
|
|||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("Row 1")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("Row 1")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ export const Collapsed: Story = {
|
|||
args: { expanded: false },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByRole("button", { name: "Expand" })).toBeInTheDocument();
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: "Expand" }),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -23,7 +25,7 @@ export const Expanded: Story = {
|
|||
args: { expanded: true },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: "Collapse" }),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const HeaderMismatch: Story = {
|
|||
args: { code: -32020, name: "HeaderMismatch" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("-32020 HeaderMismatch")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("-32020 HeaderMismatch")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ export const MissingCapability: Story = {
|
|||
args: { code: -32021, name: "MissingRequiredClientCapability" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(
|
||||
await expect(
|
||||
canvas.getByText("-32021 MissingRequiredClientCapability"),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
|
|
@ -32,7 +32,7 @@ export const UnsupportedVersion: Story = {
|
|||
args: { code: -32022, name: "UnsupportedProtocolVersion" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(
|
||||
await expect(
|
||||
canvas.getByText("-32022 UnsupportedProtocolVersion"),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
|
|
@ -47,6 +47,6 @@ export const MethodNotFound: Story = {
|
|||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("-32601 MethodNotFound")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("-32601 MethodNotFound")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const Outgoing: Story = {
|
|||
args: { direction: "outgoing" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("client → server")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("client → server")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -22,6 +22,6 @@ export const Incoming: Story = {
|
|||
args: { direction: "incoming" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("server → client")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("server → client")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const ToolsList: Story = {
|
|||
args: { method: "tools/list" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("tools/list")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("tools/list")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -22,6 +22,6 @@ export const TasksList: Story = {
|
|||
args: { method: "tasks/list" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("tasks/list")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("tasks/list")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const Closed: Story = {
|
|||
args: { open: false },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: "Open monitoring sidebar" }),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
|
|
@ -25,7 +25,7 @@ export const Open: Story = {
|
|||
args: { open: true },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: "Close monitoring sidebar" }),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ export const Unpinned: Story = {
|
|||
args: { pinned: false },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByRole("button", { name: "Pin" })).toBeInTheDocument();
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: "Pin" }),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -23,6 +25,8 @@ export const Pinned: Story = {
|
|||
args: { pinned: true },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByRole("button", { name: "Unpin" })).toBeInTheDocument();
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: "Unpin" }),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ type Story = StoryObj<typeof ReplayButton>;
|
|||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByRole("button", { name: "Replay" })).toBeInTheDocument();
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: "Replay" }),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const Default: Story = {
|
|||
render: () => <ResizeDemo />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(
|
||||
await expect(
|
||||
canvas.getByRole("separator", { name: "Resize demo panel" }),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export const FillsHostWidth: Story = {
|
|||
// sharper discriminator: a row flex leaves the layer full-width while
|
||||
// collapsing the child to the single "x").
|
||||
const layer = child.parentElement as HTMLElement;
|
||||
expect(child.getBoundingClientRect().width).toBeCloseTo(
|
||||
await expect(child.getBoundingClientRect().width).toBeCloseTo(
|
||||
layer.getBoundingClientRect().width,
|
||||
0,
|
||||
);
|
||||
|
|
@ -77,11 +77,11 @@ export const FillVariant: Story = {
|
|||
// Width still stretches onto the child (relative to the layer, as above);
|
||||
// the `fill` height comes from `bottom: 0` making the layer span the host.
|
||||
const layer = child.parentElement as HTMLElement;
|
||||
expect(child.getBoundingClientRect().width).toBeCloseTo(
|
||||
await expect(child.getBoundingClientRect().width).toBeCloseTo(
|
||||
layer.getBoundingClientRect().width,
|
||||
0,
|
||||
);
|
||||
expect(layer.getBoundingClientRect().height).toBeGreaterThan(160 - 4);
|
||||
await expect(layer.getBoundingClientRect().height).toBeGreaterThan(160 - 4);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -97,6 +97,6 @@ export const Inactive: Story = {
|
|||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.queryByTestId("stage-child")).not.toBeInTheDocument();
|
||||
await expect(canvas.queryByTestId("stage-child")).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -106,8 +106,10 @@ export const ServerInfoNotReported: Story = {
|
|||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.queryByText("my-catalog-name")).not.toBeInTheDocument();
|
||||
expect(canvas.getAllByText(SERVER_INFO_NOT_REPORTED_LABEL)).toHaveLength(2);
|
||||
await expect(canvas.queryByText("my-catalog-name")).not.toBeInTheDocument();
|
||||
await expect(
|
||||
canvas.getAllByText(SERVER_INFO_NOT_REPORTED_LABEL),
|
||||
).toHaveLength(2);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -187,11 +189,11 @@ export const WithOAuthIdToken: Story = {
|
|||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("ID Token")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("ID Token")).toBeInTheDocument();
|
||||
// Only the ID token is a JWT here, so exactly one decode toggle is offered.
|
||||
await userEvent.click(
|
||||
canvas.getByRole("button", { name: "Decode JWT for ID Token" }),
|
||||
);
|
||||
expect(canvas.getByText(/"sub": "user-42"/)).toBeInTheDocument();
|
||||
await expect(canvas.getByText(/"sub": "user-42"/)).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -77,6 +77,6 @@ export const WithEntries: Story = {
|
|||
},
|
||||
// List scrollbar reserves a gutter and stays hidden when idle (#1474).
|
||||
play: async ({ canvasElement }) => {
|
||||
expectScrollbarGutterIdleHidden(canvasElement);
|
||||
await expectScrollbarGutterIdleHidden(canvasElement);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ export const AllVisible: Story = {
|
|||
args: { visibleDirections: { client: true, server: true } },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: "client → server" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: "Deselect All" }),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ type Story = StoryObj<typeof MonitoringControls>;
|
|||
export const AllTabs: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByRole("radio", { name: "Logs" })).toBeChecked();
|
||||
await expect(canvas.getByRole("radio", { name: "Logs" })).toBeChecked();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -28,6 +28,6 @@ export const StdioServer: Story = {
|
|||
args: { tabs: ["Logs", "Protocol"], value: "Protocol" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.queryByRole("radio", { name: "Network" })).toBeNull();
|
||||
await expect(canvas.queryByRole("radio", { name: "Network" })).toBeNull();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ type Story = StoryObj<typeof MonitoringScreen>;
|
|||
export const Logs: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("Log stream")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("Log stream")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -55,6 +55,6 @@ export const Protocol: Story = {
|
|||
args: { value: "Protocol" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("Request history")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("Request history")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export const WithEntries: Story = {
|
|||
args: { entries: sample },
|
||||
// List scrollbar reserves a gutter and stays hidden when idle (#1474).
|
||||
play: async ({ canvasElement }) => {
|
||||
expectScrollbarGutterIdleHidden(canvasElement);
|
||||
await expectScrollbarGutterIdleHidden(canvasElement);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue