mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
acfe80ab6b
## What Adds type-aware linting to the workspace. Until now the only static gate on JS/TS was Prettier (formatting) and `tsc` — there was no linter, so the whole class of bugs `tsc` doesn't catch (floating promises, misused promises, throwing non-Errors, unsafe stringification, dead code) went unguarded across ~450 TS files. - **`eslint.config.mjs`** — flat config. `recommendedTypeChecked` runs on the TS sources via an explicit `project` list (each package keeps tests in a separate `tsconfig.test.json` that `projectService` can't auto-discover, so it's listed explicitly). Type-checked rules are disabled for the loosely-typed plain-JS build/dev scripts. The `argent-private` submodule and build artifacts are ignored (mirrors `.prettierignore`). - **`lint` / `lint:fix`** npm scripts. - **`.github/workflows/lint.yml`** — builds the workspace (type-aware rules need the referenced projects' declarations) then runs `eslint .`. Node 24 (ESLint 10 requires `^20.19 || ^22.13 || >=24`). ## Rule calibration This is an adoption PR, so it's deliberately calibrated rather than maximal: - **Kept on (bug-catchers):** `no-floating-promises`, `no-misused-promises` (with `checksVoidReturn: false` to drop the noisy callback variant), `only-throw-error`, `no-base-to-string`, `prefer-promise-reject-errors`, `no-unused-vars` (`_`-prefix escape hatch), plus the ESLint recommended set. - **Parked off as documented debt:** the high-volume style/`any`-family rules — `no-unsafe-*`, `require-await`, `no-unnecessary-type-assertion`, `restrict-template-expressions`, `unbound-method`, `no-explicit-any`, etc. These flag broad classes of pre-existing code and are out of scope here; each is commented as a ratchet target for follow-up passes. All findings from the kept-on rules are fixed in this PR (dead code/imports, error-cause chaining, **3 genuine floating promises**, non-Error throws, unsafe stringification). Stale unused `eslint-disable` directives left over in the tree were removed by `--fix`. ## Verification Locally, all green: `eslint .` (0 errors), `tsc --build`, `typecheck:tests`/`typecheck:scripts`, the full `vitest` suite (no behavior change from the fixes), and `prettier --check`. ## Not included (intentionally) Other static-analysis gates discussed (CodeQL, dependency-audit, ShellCheck, actionlint) are out of scope for this PR.
163 lines
4.4 KiB
TypeScript
163 lines
4.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { createHttpApp } from "../src/http";
|
|
import type { Registry } from "@argent/registry";
|
|
|
|
function stubRegistry() {
|
|
return {
|
|
getSnapshot: vi.fn(() => ({ services: new Map(), namespaces: [], tools: [] })),
|
|
getTool: vi.fn(() => undefined),
|
|
invokeTool: vi.fn(),
|
|
} as unknown as Registry;
|
|
}
|
|
|
|
describe("idle timeout", () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("fires onIdle after configured timeout with no activity", () => {
|
|
const onIdle = vi.fn();
|
|
const handle = createHttpApp(stubRegistry(), {
|
|
idleTimeoutMs: 5 * 60_000,
|
|
onIdle,
|
|
});
|
|
|
|
vi.advanceTimersByTime(5 * 60_000);
|
|
expect(onIdle).toHaveBeenCalledOnce();
|
|
|
|
handle.dispose();
|
|
});
|
|
|
|
it("does NOT fire when activity keeps resetting the timer", async () => {
|
|
const onIdle = vi.fn();
|
|
const registry = stubRegistry();
|
|
(registry.getTool as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
description: "test",
|
|
inputSchema: {},
|
|
services: () => ({}),
|
|
execute: async () => ({ ok: true }),
|
|
});
|
|
(registry.invokeTool as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
ok: true,
|
|
});
|
|
|
|
const handle = createHttpApp(registry, {
|
|
idleTimeoutMs: 5 * 60_000,
|
|
onIdle,
|
|
});
|
|
|
|
const request = await import("supertest").then((m) => m.default);
|
|
|
|
// Simulate activity every 3 minutes (well within the 5-min timeout)
|
|
for (let i = 0; i < 4; i++) {
|
|
vi.advanceTimersByTime(3 * 60_000);
|
|
await request(handle.app).post("/tools/test-tool").send({}).expect(200);
|
|
}
|
|
|
|
expect(onIdle).not.toHaveBeenCalled();
|
|
|
|
handle.dispose();
|
|
});
|
|
|
|
it("resets the timer on tool invocation", async () => {
|
|
const onIdle = vi.fn();
|
|
const registry = stubRegistry();
|
|
(registry.getTool as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
description: "test",
|
|
inputSchema: {},
|
|
services: () => ({}),
|
|
execute: async () => ({ ok: true }),
|
|
});
|
|
(registry.invokeTool as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
ok: true,
|
|
});
|
|
|
|
const handle = createHttpApp(registry, {
|
|
idleTimeoutMs: 5 * 60_000,
|
|
onIdle,
|
|
});
|
|
|
|
const request = await import("supertest").then((m) => m.default);
|
|
|
|
// Advance 4 minutes (not enough to trigger)
|
|
vi.advanceTimersByTime(4 * 60_000);
|
|
expect(onIdle).not.toHaveBeenCalled();
|
|
|
|
// Call a tool — resets the timer
|
|
await request(handle.app).post("/tools/test-tool").send({}).expect(200);
|
|
|
|
// Advance another 4 minutes — still not 5 from last activity
|
|
vi.advanceTimersByTime(4 * 60_000);
|
|
expect(onIdle).not.toHaveBeenCalled();
|
|
|
|
// Advance 1 more minute — now 5 since last activity
|
|
vi.advanceTimersByTime(1 * 60_000);
|
|
expect(onIdle).toHaveBeenCalledOnce();
|
|
|
|
handle.dispose();
|
|
});
|
|
|
|
it("does not start a timer when timeout is 0", () => {
|
|
const onIdle = vi.fn();
|
|
const handle = createHttpApp(stubRegistry(), {
|
|
idleTimeoutMs: 0,
|
|
onIdle,
|
|
});
|
|
|
|
vi.advanceTimersByTime(60 * 60_000);
|
|
expect(onIdle).not.toHaveBeenCalled();
|
|
|
|
handle.dispose();
|
|
});
|
|
|
|
it("does not start a timer when no onIdle callback", () => {
|
|
const handle = createHttpApp(stubRegistry(), {
|
|
idleTimeoutMs: 5 * 60_000,
|
|
});
|
|
|
|
// Should not throw or fire anything
|
|
vi.advanceTimersByTime(10 * 60_000);
|
|
|
|
handle.dispose();
|
|
});
|
|
|
|
it("clears the timer on dispose()", () => {
|
|
const onIdle = vi.fn();
|
|
const handle = createHttpApp(stubRegistry(), {
|
|
idleTimeoutMs: 5 * 60_000,
|
|
onIdle,
|
|
});
|
|
|
|
handle.dispose();
|
|
|
|
vi.advanceTimersByTime(10 * 60_000);
|
|
expect(onIdle).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("resets the timer on GET /tools (list tools)", async () => {
|
|
const onIdle = vi.fn();
|
|
const handle = createHttpApp(stubRegistry(), {
|
|
idleTimeoutMs: 5 * 60_000,
|
|
onIdle,
|
|
});
|
|
|
|
const request = await import("supertest").then((m) => m.default);
|
|
|
|
// Poll GET /tools every minute — well within the 5-min idle window.
|
|
for (let i = 0; i < 6; i++) {
|
|
vi.advanceTimersByTime(60_000);
|
|
await request(handle.app).get("/tools").expect(200);
|
|
}
|
|
|
|
// 6 minutes have passed total, but the timer should have been
|
|
// refreshed each time — so onIdle must NOT fire.
|
|
expect(onIdle).not.toHaveBeenCalled();
|
|
|
|
handle.dispose();
|
|
});
|
|
});
|