chore(mcp): add js tool verification

This commit is contained in:
Guillaume ARM 2026-06-10 21:58:14 +02:00
parent d688006958
commit a4a098e923
8 changed files with 1357 additions and 14 deletions

View File

@ -6,7 +6,7 @@ default:
@just --list @just --list
# Install local development tooling. # Install local development tooling.
install: install-git-hooks check-install generate-env install: install-git-hooks check-install npm-install generate-env
# Remove generated local environment files. # Remove generated local environment files.
clean: clean:
@ -112,6 +112,33 @@ generate-env:
done < .env.test > .env done < .env.test > .env
printf '%s\n' 'Generated .env' printf '%s\n' 'Generated .env'
# Install Node dependencies for repository tools.
npm-install:
npm install --prefix tools/mcp-bridge
# Build Node-based repository tools.
npm-build:
npm run build --prefix tools/mcp-bridge
# Check Node-based repository tools.
npm-check:
npm run check --prefix tools/mcp-bridge
# Run Node-based tool tests.
npm-test:
npm test --prefix tools/mcp-bridge
# Run Node-based tool integration tests.
npm-test-integration:
npm run test-integration --prefix tools/mcp-bridge
# Run Node-based tool CI.
npm-ci:
npm run ci --prefix tools/mcp-bridge
# Build generated artifacts.
build: npm-build
# Pass args through to `craftos`. Prefer `just trapos-exec '<lua>'` for # Pass args through to `craftos`. Prefer `just trapos-exec '<lua>'` for
# automated probes that must not hang the terminal. # automated probes that must not hang the terminal.
# Launch the TrapOS dev environment in CraftOS-PC with repo-local data # Launch the TrapOS dev environment in CraftOS-PC with repo-local data
@ -392,14 +419,22 @@ opencode-attach *args:
fi fi
exec opencode attach "$target" "$@" exec opencode attach "$target" "$@"
# Local CI entry point used by Git hooks. Pass args through to `test`. # Local CI entry point used by Git hooks. Pass args through to CraftOS tests.
ci *args: check-craftos check check-packages ci *args: check-craftos check check-packages npm-ci
@just test {{args}} @just _craftos-test {{args}}
@just test-timeout @just test-integration
# Run all standard tests. Pass `--pretty` for grouped CraftOS output.
[positional-arguments]
test *args: build npm-test
@just _craftos-test {{args}}
# Run integration and harness tests that are too broad for unit test targets.
test-integration: npm-test-integration test-timeout
# Run CraftOS-PC headless integration tests. Pass `--pretty` for grouped output. # Run CraftOS-PC headless integration tests. Pass `--pretty` for grouped output.
[positional-arguments] [positional-arguments]
test *args: _craftos-test *args:
#!/usr/bin/env bash #!/usr/bin/env bash
set -uo pipefail set -uo pipefail
@ -541,8 +576,8 @@ test-timeout-shell: (_timeout-fixture "/tests/harness/slow-case.lua" "${TRAP_CCL
# Fast regression guard for both timeout layers. Wired into `ci`. # Fast regression guard for both timeout layers. Wired into `ci`.
test-timeout: test-timeout-lua test-timeout-shell test-timeout: test-timeout-lua test-timeout-shell
# Lint all Lua source with luacheck and validate markdown links. # Lint Lua/TypeScript source and validate markdown links.
check: check-luacheck check-lychee check: check-luacheck check-lychee npm-check
luacheck --quiet . luacheck --quiet .
@just lint-markdown @just lint-markdown

View File

@ -14,5 +14,6 @@ Future ADRs can reuse the shape of the existing files when it is useful.
- [`adr-0007-test-framework.md`](adr-0007-test-framework.md) — `libtest` per-case helper, `runtest` suite orchestration, and the two-layer timeout (libtest + shell watchdog). - [`adr-0007-test-framework.md`](adr-0007-test-framework.md) — `libtest` per-case helper, `runtest` suite orchestration, and the two-layer timeout (libtest + shell watchdog).
- [`adr-0010-ccpm-package-manager.md`](adr-0010-ccpm-package-manager.md) — `ccpm` package manager (packages, registries, package-aware bootstrap). - [`adr-0010-ccpm-package-manager.md`](adr-0010-ccpm-package-manager.md) — `ccpm` package manager (packages, registries, package-aware bootstrap).
- [`adr-0011-repo-conventions.md`](adr-0011-repo-conventions.md) — Git hooks own commit/push verification; markdown link syntax for cross-references. - [`adr-0011-repo-conventions.md`](adr-0011-repo-conventions.md) — Git hooks own commit/push verification; markdown link syntax for cross-references.
- [`adr-0016-js-tool-verification.md`](adr-0016-js-tool-verification.md) — JavaScript/TypeScript tool build, check, test, CI, and future integration-test split.
Gaps in numbering (0003, 0004, 0006, 0008, 0009, 0012, 0013, 0014, 0015) are records that were either superseded by later decisions or consolidated into the surviving ADRs above. Gaps in numbering (0003, 0004, 0006, 0008, 0009, 0012, 0013, 0014, 0015) are records that were either superseded by later decisions or consolidated into the surviving ADRs above.

View File

@ -0,0 +1,45 @@
# ADR 0016: JavaScript Tool Verification
## Status
Accepted
## Date
2026-06-10
## Context
The repository now contains `tools/mcp-bridge`, a TypeScript Node.js tool that sits next to the ComputerCraft Lua code. It has a different build and test lifecycle than TrapOS packages, but it still participates in the same local developer workflow and Git hooks described by [ADR-0011](adr-0011-repo-conventions.md).
The bridge also needs future end-to-end coverage that spans both runtimes: a host Node process and a headless CraftOS-PC computer. That is broader and slower than normal unit tests, and it needs the same kind of timeout discipline as the Lua harness in [ADR-0007](adr-0007-test-framework.md).
## Decision
Keep the Node package scripts simple and one-purpose:
- `npm run build` emits TypeScript into `dist/`.
- `npm run test` runs only the already-built Node unit tests.
- `npm run check` runs ESLint. TypeScript compilation is covered by `npm run build` and `npm run ci` to avoid duplicate compiler runs in repository CI.
- `npm run ci` runs `npm run build && npm run test`.
- `npm run test-integration` is reserved for bridge-to-CraftOS integration coverage and currently prints an explicit TODO.
Expose matching repository recipes for the Node lifecycle:
- `just build` delegates to `npm run build` for the bridge.
- `just test` depends on `just build`, runs `npm run test`, then runs the existing CraftOS-PC test suite.
- `just check` includes `npm run check` alongside Lua and Markdown checks.
- `just ci` uses `npm run ci`, then runs CraftOS-PC tests and the broader integration/harness target.
- `just test-integration` runs the placeholder Node integration target and the Lua timeout harness checks.
## Consequences
- `npm run test` no longer hides a build step. Callers that need a fresh build must use `npm run ci` or `just test`.
- `just ci` avoids duplicating Node unit tests by calling `npm run ci` directly and then invoking only the CraftOS-side test body.
- ESLint failures are part of `just check`, so they are covered by the same pre-commit and pre-push hooks as Lua and Markdown checks. TypeScript compiler failures are covered by `just build`, `just test`, and `just ci`.
- Future bridge integration tests have a named home before they exist, reducing the chance that slow end-to-end behavior is mixed into fast unit tests.
## Future Work
- Replace the `npm run test-integration` placeholder with a harness that launches the bridge and CraftOS-PC headless, connects a websocket client from CraftOS, probes through the MCP surface, and tears down both processes reliably.
- Give the bridge integration harness host-level timeout handling and readable diagnostics modelled on the CraftOS-PC test recipes.

View File

@ -0,0 +1,26 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist/**"] },
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
globals: globals.node,
parserOptions: {
projectService: {
allowDefaultProject: ["eslint.config.js"],
},
tsconfigRootDir: import.meta.dirname,
},
},
},
{
files: ["test/**/*.ts"],
rules: {
"@typescript-eslint/no-floating-promises": "off",
},
},
);

File diff suppressed because it is too large Load Diff

View File

@ -5,18 +5,25 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "tsc --noEmit false", "build": "tsc --noEmit false",
"check": "eslint .",
"ci": "npm run build && npm run test",
"dev": "tsx src/index.ts", "dev": "tsx src/index.ts",
"start": "node dist/src/index.js", "start": "node dist/src/index.js",
"test": "npm run build && node --test dist/test/*.test.js" "test": "node --test dist/test/*.test.js",
"test-integration": "node -e \"console.log('TODO: add mcp-bridge integration tests that launch CraftOS-PC headless, start the bridge, connect a ComputerCraft websocket client from inside CraftOS, and exercise the MCP probe path end-to-end. The intended harness should verify startup ordering, websocket hello/response framing, timeout behavior when a computer is silent, and cleanup of both the CraftOS process and Node bridge server. Keep this separate from unit tests because it will need host ports, a CraftOS watchdog, and careful failure diagnostics similar to the existing Lua timeout fixtures.')\""
}, },
"dependencies": { "dependencies": {
"ws": "^8.17.1" "ws": "^8.17.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^20.14.10", "@types/node": "^20.14.10",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"eslint": "^10.4.1",
"globals": "^17.6.0",
"tsx": "^4.16.2", "tsx": "^4.16.2",
"typescript": "^5.5.3" "typescript": "^5.5.3",
"typescript-eslint": "^8.61.0"
}, },
"allowScripts": { "allowScripts": {
"esbuild@0.28.0": true, "esbuild@0.28.0": true,

View File

@ -117,7 +117,7 @@ function writeJson(res: ServerResponse, statusCode: number, body: unknown): void
async function readBody(req: IncomingMessage): Promise<string> { async function readBody(req: IncomingMessage): Promise<string> {
const chunks: Buffer[] = []; const chunks: Buffer[] = [];
for await (const chunk of req) { for await (const chunk of req) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
} }
return Buffer.concat(chunks).toString("utf8"); return Buffer.concat(chunks).toString("utf8");
} }

View File

@ -14,7 +14,7 @@ export type ResponseMessage = {
export type LinkMessage = HelloMessage | ResponseMessage; export type LinkMessage = HelloMessage | ResponseMessage;
export function parseJsonFrame(data: unknown): unknown | null { export function parseJsonFrame(data: unknown): unknown {
if (typeof data === "string") { if (typeof data === "string") {
return parseJson(data); return parseJson(data);
} }
@ -76,7 +76,7 @@ export function formatComputer(computerId: number, label: string | null): string
return `${computerId} (Label: ${label ?? "null"})`; return `${computerId} (Label: ${label ?? "null"})`;
} }
function parseJson(text: string): unknown | null { function parseJson(text: string): unknown {
try { try {
return JSON.parse(text) as unknown; return JSON.parse(text) as unknown;
} catch { } catch {