Issue #193's tool-half has two open items. The shared, zod-agnostic tool-spec
registry (SHARED_TOOL_SPECS) for the identical tools is already merged
(f3fa15e7) and consumed by both layers, so that subset is done. The remaining
items are: (a) deriving the layer-3 hand-mirror `DocmostClientLike` from the
real client type, and (b) folding more tools into the registry. Both were
deferred as risky, and that deferral still holds (verified, see below) — so
this change ships the safest concrete increment instead of forcing the risk.
What this adds (behaviour-neutral, test-only + a doc comment):
- packages/mcp/test/unit/client-host-contract.test.mjs: pins the layer-3
contract from the ESM side, where the real DocmostClient is importable. It
asserts every method the in-app `DocmostClientLike` mirror declares exists as
a function on a real DocmostClient instance (constructor is side-effect-free).
A rename/removal in client.ts now fails this test instead of silently shipping
a runtime "x is not a function" into an agent tool call. Negative-case
verified (a bogus method name is detected).
- docmost-client.loader.ts: replaces the vague mirror comment with a pointer to
the guard test and a concrete, empirically-grounded staged plan for the full
type-derivation. Verified blockers kept it deferred: @docmost/mcp emits no
.d.ts (no `declaration`, no `types` export) and the server has no path mapping
for it, so there is no type to import today; and the real methods' inferred
CONCRETE return types conflict with the in-app adapter's loose
Record<string,unknown> + `as`-cast result handling (deriving the exact type
breaks the build / forces pervasive double-casts and full-surface test stubs).
Out of scope (noted in the issue): the PM<->Markdown converter unification.
Verified: server tsc clean; mcp tsc clean; mcp tests 369 pass (367 + 2 new);
ai-chat tools specs 51 pass. No behaviour change; committed mcp build untouched
(no mcp src changed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
3.3 KiB
JavaScript
101 lines
3.3 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { DocmostClient } from "../../build/index.js";
|
|
|
|
// Drift guard for the THIRD hand-written layer of the AI tool set (issue #193,
|
|
// layer 3): the in-app server hand-mirrors the DocmostClient method signatures
|
|
// it consumes as the `DocmostClientLike` interface in
|
|
// apps/server/src/core/ai-chat/tools/docmost-client.loader.ts ("Signatures here
|
|
// mirror that file exactly"). That mirror lives across the ESM(mcp)/CJS(server)
|
|
// boundary and the package ships NO .d.ts, so the server typecheck cannot verify
|
|
// the names against the real class — a rename/removal in client.ts would surface
|
|
// only as a runtime "x is not a function" inside an agent tool call.
|
|
//
|
|
// This test pins the contract from the mcp side (ESM, where the real class is
|
|
// directly importable): every method the embedding host depends on MUST exist as
|
|
// a function on a real DocmostClient instance. If you rename/remove a client
|
|
// method, this fails here AND you must update DocmostClientLike to match.
|
|
//
|
|
// Keep HOST_CONTRACT_METHODS in sync with the methods declared in the server's
|
|
// DocmostClientLike interface (the in-app per-user tool adapter only — it is the
|
|
// superset of what either transport calls). Full type-derivation of
|
|
// DocmostClientLike from this class is deferred (see the staged plan in
|
|
// docmost-client.loader.ts): the package emits no declarations and the real
|
|
// (inferred, concrete) return types conflict with the host's loose
|
|
// `Record<string,unknown>` + `as`-cast result handling.
|
|
const HOST_CONTRACT_METHODS = [
|
|
// read
|
|
"search",
|
|
"getPage",
|
|
"getWorkspace",
|
|
"getSpaces",
|
|
"listPages",
|
|
"listSidebarPages",
|
|
"getOutline",
|
|
"getPageJson",
|
|
"getNode",
|
|
"getTable",
|
|
"listComments",
|
|
"getComment",
|
|
"checkNewComments",
|
|
"listShares",
|
|
"listPageHistory",
|
|
"getPageHistory",
|
|
"diffPageVersions",
|
|
"exportPageMarkdown",
|
|
// write (page)
|
|
"createPage",
|
|
"updatePage",
|
|
"renamePage",
|
|
"movePage",
|
|
"deletePage",
|
|
"editPageText",
|
|
"patchNode",
|
|
"insertNode",
|
|
"deleteNode",
|
|
"updatePageJson",
|
|
"tableInsertRow",
|
|
"tableDeleteRow",
|
|
"tableUpdateCell",
|
|
"copyPageContent",
|
|
"importPageMarkdown",
|
|
"sharePage",
|
|
"unsharePage",
|
|
"restorePageVersion",
|
|
"transformPage",
|
|
// write (comment)
|
|
"createComment",
|
|
"resolveComment",
|
|
];
|
|
|
|
test("DocmostClient implements every method the in-app DocmostClientLike mirror declares", () => {
|
|
// The constructor is side-effect-free (no network/login on construction): it
|
|
// only stores config and creates an axios instance, so it is safe to build a
|
|
// throwaway instance here with a dummy token provider.
|
|
const client = new DocmostClient({
|
|
apiUrl: "http://127.0.0.1:1/api",
|
|
getToken: async () => "test-token",
|
|
});
|
|
|
|
const missing = HOST_CONTRACT_METHODS.filter(
|
|
(name) => typeof client[name] !== "function",
|
|
);
|
|
|
|
assert.deepEqual(
|
|
missing,
|
|
[],
|
|
`DocmostClient is missing host-contract method(s): ${missing.join(", ")}. ` +
|
|
`Update packages/mcp/src/client.ts and/or the server's DocmostClientLike ` +
|
|
`interface (apps/server/src/core/ai-chat/tools/docmost-client.loader.ts) ` +
|
|
`so the hand-mirrored signatures stay in sync.`,
|
|
);
|
|
});
|
|
|
|
test("HOST_CONTRACT_METHODS has no duplicates", () => {
|
|
assert.equal(
|
|
new Set(HOST_CONTRACT_METHODS).size,
|
|
HOST_CONTRACT_METHODS.length,
|
|
);
|
|
});
|