feat(scenario): run logs, lint/format tooling, CONTRIBUTING
- add ScenarioRunLogEntity to persist step script output to DB - stepLogger dual-writes to NestJS logger and DB (fire-and-forget) - add GET /scenarios/:id/run/:runId returning run, stepRuns and logs - add POST /scenarios/:id/run/:runId/wait (polls until terminal state) - 9 new integration tests for the two endpoints (136 total) - add eslint with typescript-eslint and eslint-config-prettier - add npm scripts: format, lint, lint:fix - resolve all lint errors across src and test (no any types) - add CONTRIBUTING.md covering dev workflow
This commit is contained in:
+46
-40
@@ -1,6 +1,6 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { buildTestApp } from './app.harness';
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import request from "supertest";
|
||||
import { buildTestApp } from "./app.harness";
|
||||
|
||||
/**
|
||||
* MCP controller integration tests.
|
||||
@@ -14,7 +14,7 @@ import { buildTestApp } from './app.harness';
|
||||
* Browser-dependent tools (open_url, exec_code) require a live Playwright
|
||||
* session and are not covered here.
|
||||
*/
|
||||
describe('McpController', () => {
|
||||
describe("McpController", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -35,13 +35,13 @@ describe('McpController', () => {
|
||||
/** Send a single MCP tool call and return the parsed response body. */
|
||||
async function mcpCall(toolName: string, args: Record<string, unknown> = {}) {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/mcp')
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json, text/event-stream')
|
||||
.post("/mcp")
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json, text/event-stream")
|
||||
.send({
|
||||
jsonrpc: '2.0',
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: 'tools/call',
|
||||
method: "tools/call",
|
||||
params: { name: toolName, arguments: args },
|
||||
});
|
||||
return { status: res.status, rpc: parseSse(res.text) };
|
||||
@@ -49,20 +49,20 @@ describe('McpController', () => {
|
||||
|
||||
// ── Connectivity ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('POST /mcp — connectivity', () => {
|
||||
it('is reachable and returns a non-5xx status', async () => {
|
||||
describe("POST /mcp — connectivity", () => {
|
||||
it("is reachable and returns a non-5xx status", async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/mcp')
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json, text/event-stream')
|
||||
.post("/mcp")
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json, text/event-stream")
|
||||
.send({
|
||||
jsonrpc: '2.0',
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
method: "initialize",
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'test', version: '0' },
|
||||
clientInfo: { name: "test", version: "0" },
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
@@ -71,9 +71,9 @@ describe('McpController', () => {
|
||||
|
||||
// ── list_keys tool ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('list_keys', () => {
|
||||
it('returns a result with text content containing a JSON array', async () => {
|
||||
const { status, rpc } = await mcpCall('list_keys');
|
||||
describe("list_keys", () => {
|
||||
it("returns a result with text content containing a JSON array", async () => {
|
||||
const { status, rpc } = await mcpCall("list_keys");
|
||||
expect(status).toBe(200);
|
||||
const result = rpc.result as { content: { text: string }[] };
|
||||
expect(Array.isArray(JSON.parse(result.content[0].text))).toBe(true);
|
||||
@@ -82,50 +82,56 @@ describe('McpController', () => {
|
||||
|
||||
// ── list_sessions tool ─────────────────────────────────────────────────────
|
||||
|
||||
describe('list_sessions', () => {
|
||||
it('returns a paginated result with a data array', async () => {
|
||||
const { status, rpc } = await mcpCall('list_sessions');
|
||||
describe("list_sessions", () => {
|
||||
it("returns a paginated result with a data array", async () => {
|
||||
const { status, rpc } = await mcpCall("list_sessions");
|
||||
expect(status).toBe(200);
|
||||
const result = rpc.result as { content: { text: string }[] };
|
||||
const body = JSON.parse(result.content[0].text) as { data: unknown[]; total: number };
|
||||
const body = JSON.parse(result.content[0].text) as {
|
||||
data: unknown[];
|
||||
total: number;
|
||||
};
|
||||
expect(Array.isArray(body.data)).toBe(true);
|
||||
expect(typeof body.total).toBe('number');
|
||||
expect(typeof body.total).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
// ── list_environments tool ─────────────────────────────────────────────────
|
||||
|
||||
describe('list_environments', () => {
|
||||
it('returns a paginated result with a data array', async () => {
|
||||
const { status, rpc } = await mcpCall('list_environments');
|
||||
describe("list_environments", () => {
|
||||
it("returns a paginated result with a data array", async () => {
|
||||
const { status, rpc } = await mcpCall("list_environments");
|
||||
expect(status).toBe(200);
|
||||
const result = rpc.result as { content: { text: string }[] };
|
||||
const body = JSON.parse(result.content[0].text) as { data: unknown[]; total: number };
|
||||
const body = JSON.parse(result.content[0].text) as {
|
||||
data: unknown[];
|
||||
total: number;
|
||||
};
|
||||
expect(Array.isArray(body.data)).toBe(true);
|
||||
expect(typeof body.total).toBe('number');
|
||||
expect(typeof body.total).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
// ── create_environment tool ────────────────────────────────────────────────
|
||||
|
||||
describe('create_environment', () => {
|
||||
it('creates an environment via MCP', async () => {
|
||||
const { status, rpc } = await mcpCall('create_environment', {
|
||||
name: 'mcp-test-env',
|
||||
urls: { id_url: 'https://id.example.com' },
|
||||
describe("create_environment", () => {
|
||||
it("creates an environment via MCP", async () => {
|
||||
const { status, rpc } = await mcpCall("create_environment", {
|
||||
name: "mcp-test-env",
|
||||
urls: { id_url: "https://id.example.com" },
|
||||
});
|
||||
expect(status).toBe(200);
|
||||
const result = rpc.result as { content: { text: string }[] };
|
||||
const created = JSON.parse(result.content[0].text) as { name: string };
|
||||
expect(created.name).toBe('mcp-test-env');
|
||||
expect(created.name).toBe("mcp-test-env");
|
||||
});
|
||||
});
|
||||
|
||||
// ── delete_session tool with unknown id ────────────────────────────────────
|
||||
|
||||
describe('delete_session', () => {
|
||||
it('returns an MCP error result for a non-existent session id', async () => {
|
||||
const { status, rpc } = await mcpCall('delete_session', { id: 999999 });
|
||||
describe("delete_session", () => {
|
||||
it("returns an MCP error result for a non-existent session id", async () => {
|
||||
const { status, rpc } = await mcpCall("delete_session", { id: 999999 });
|
||||
expect(status).toBe(200);
|
||||
// MCP wraps service errors as isError:true content, not HTTP errors
|
||||
const result = rpc.result as { isError: boolean };
|
||||
|
||||
Reference in New Issue
Block a user