Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion apps/api/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import { config as baseConfig } from "@cortex/eslint-config/base";

/** @type {import("eslint").Linter.Config[]} */
export default baseConfig;
export default [
...baseConfig,
{
files: ["jest.config.js"],
languageOptions: {
globals: {
module: "writable",
require: "readonly",
__dirname: "readonly",
__filename: "readonly",
},
},
},
];
28 changes: 28 additions & 0 deletions apps/api/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/** @type {import('jest').Config} */
module.exports = {
moduleFileExtensions: ["js", "json", "ts"],
rootDir: "src",
testRegex: ".*\\.spec\\.ts$",
transform: {
"^.+\\.(t|j)s$": [
"ts-jest",
{
tsconfig: {
module: "CommonJS",
moduleResolution: "Node",
experimentalDecorators: true,
emitDecoratorMetadata: true,
strict: true,
esModuleInterop: true,
skipLibCheck: true,
},
},
],
},
collectCoverageFrom: ["**/*.(t|j)s"],
coverageDirectory: "../coverage",
testEnvironment: "node",
moduleNameMapper: {
"^@cortex/shared$": "<rootDir>/../../../packages/shared/src/index.ts",
},
};
6 changes: 5 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"start": "nest start",
"start:prod": "node dist/main",
"lint": "eslint . --max-warnings 0",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "jest"
},
"keywords": [],
"author": "",
Expand All @@ -29,9 +29,13 @@
"@cortex/eslint-config": "workspace:*",
"@cortex/typescript-config": "workspace:*",
"@nestjs/cli": "^11.0.21",
"@nestjs/testing": "^11.1.24",
"@types/express": "^5.0.3",
"@types/jest": "^30.0.0",
"@types/node": "^22.15.3",
"eslint": "^9.39.1",
"jest": "^30.4.2",
"ts-jest": "^29.4.11",
"typescript": "5.9.2"
}
}
47 changes: 47 additions & 0 deletions apps/api/src/app.module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Test } from "@nestjs/testing";
import { AppModule } from "./app.module";
import { MCPController } from "./mcp/mcp.controller";

describe("AppModule", () => {
it("should be defined and compile successfully", async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();

expect(module).toBeDefined();
});

it("should not throw when creating the application module", async () => {
await expect(
Test.createTestingModule({
imports: [AppModule],
}).compile()
).resolves.not.toThrow();
});

it("should provide MCPController via imported MCPModule", async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();

const controller = module.get(MCPController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(MCPController);
});

it("should wire MCPController so handleMCP is callable", async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();

const controller = module.get(MCPController);
const response = await controller.handleMCP({
userId: "test_user",
organizationId: "test_org",
departmentId: "test_dept",
query: "integration check",
});

expect(response.answer).toBe("Based on company standards: integration check");
});
});
217 changes: 217 additions & 0 deletions apps/api/src/mcp/mcp.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { MCPController } from "./mcp.controller";
import type { MCPRequest } from "@cortex/shared";

describe("MCPController", () => {
let controller: MCPController;

beforeEach(() => {
controller = new MCPController();
});

describe("handleMCP", () => {
const validRequest: MCPRequest = {
userId: "user_123",
organizationId: "org_456",
departmentId: "dept_789",
query: "What is the coding standard?",
};

it("should return a response with context, policy, and answer", async () => {
const result = await controller.handleMCP(validRequest);

expect(result).toHaveProperty("context");
expect(result).toHaveProperty("policy");
expect(result).toHaveProperty("answer");
});

it("should return the fixed context array with company architecture entries", async () => {
const result = await controller.handleMCP(validRequest);

expect(result.context).toEqual([
"Company uses modular monolith architecture",
"No direct DB access from controllers",
]);
});

it("should return exactly two context entries", async () => {
const result = await controller.handleMCP(validRequest);

expect(result.context).toHaveLength(2);
});

it("should return the fixed policy with TypeScript and clean architecture rules", async () => {
const result = await controller.handleMCP(validRequest);

expect(result.policy).toEqual({
rules: ["Use TypeScript", "Follow clean architecture"],
});
});

it("should return policy rules as an array with exactly two entries", async () => {
const result = await controller.handleMCP(validRequest);

expect(result.policy.rules).toHaveLength(2);
expect(Array.isArray(result.policy.rules)).toBe(true);
});

it("should include the query in the answer using the expected format", async () => {
const result = await controller.handleMCP(validRequest);

expect(result.answer).toBe(
"Based on company standards: What is the coding standard?"
);
});

it("should prefix the answer with 'Based on company standards: '", async () => {
const result = await controller.handleMCP(validRequest);

expect(result.answer).toMatch(/^Based on company standards: /);
});

it("should reflect the query verbatim in the answer", async () => {
const query = "How do I structure my service layer?";
const result = await controller.handleMCP({ ...validRequest, query });

expect(result.answer).toBe(`Based on company standards: ${query}`);
});

it("should handle an empty query string", async () => {
const result = await controller.handleMCP({ ...validRequest, query: "" });

expect(result.answer).toBe("Based on company standards: ");
expect(result.context).toHaveLength(2);
expect(result.policy.rules).toHaveLength(2);
});

it("should handle a query with special characters", async () => {
const specialQuery = 'What about "quotes" & <angle> brackets?';
const result = await controller.handleMCP({
...validRequest,
query: specialQuery,
});

expect(result.answer).toBe(`Based on company standards: ${specialQuery}`);
});

it("should handle a query with newlines and whitespace", async () => {
const multilineQuery = "First line\nSecond line\t tabbed";
const result = await controller.handleMCP({
...validRequest,
query: multilineQuery,
});

expect(result.answer).toBe(
`Based on company standards: ${multilineQuery}`
);
});

it("should not use userId in the response", async () => {
const resultA = await controller.handleMCP({
...validRequest,
userId: "user_aaa",
});
const resultB = await controller.handleMCP({
...validRequest,
userId: "user_bbb",
});

expect(resultA.answer).toBe(resultB.answer);
expect(resultA.context).toEqual(resultB.context);
expect(resultA.policy).toEqual(resultB.policy);
});

it("should not use organizationId in the response", async () => {
const resultA = await controller.handleMCP({
...validRequest,
organizationId: "org_111",
});
const resultB = await controller.handleMCP({
...validRequest,
organizationId: "org_999",
});

expect(resultA.answer).toBe(resultB.answer);
expect(resultA.context).toEqual(resultB.context);
expect(resultA.policy).toEqual(resultB.policy);
});

it("should not use departmentId in the response", async () => {
const resultA = await controller.handleMCP({
...validRequest,
departmentId: "dept_aaa",
});
const resultB = await controller.handleMCP({
...validRequest,
departmentId: "dept_zzz",
});

expect(resultA.answer).toBe(resultB.answer);
expect(resultA.context).toEqual(resultB.context);
expect(resultA.policy).toEqual(resultB.policy);
});

it("should return consistent context regardless of query", async () => {
const result1 = await controller.handleMCP({
...validRequest,
query: "query one",
});
const result2 = await controller.handleMCP({
...validRequest,
query: "query two",
});

expect(result1.context).toEqual(result2.context);
});

it("should return consistent policy regardless of query", async () => {
const result1 = await controller.handleMCP({
...validRequest,
query: "query one",
});
const result2 = await controller.handleMCP({
...validRequest,
query: "completely different query",
});

expect(result1.policy).toEqual(result2.policy);
});

it("should return a Promise (async method)", () => {
const returnValue = controller.handleMCP(validRequest);

expect(returnValue).toBeInstanceOf(Promise);
});

it("should return context as an array of strings", async () => {
const result = await controller.handleMCP(validRequest);

result.context.forEach((item) => {
expect(typeof item).toBe("string");
});
});

it("should return policy rules as an array of strings", async () => {
const result = await controller.handleMCP(validRequest);

result.policy.rules.forEach((rule) => {
expect(typeof rule).toBe("string");
});
});

it("should return answer as a string", async () => {
const result = await controller.handleMCP(validRequest);

expect(typeof result.answer).toBe("string");
});

it("should handle a very long query string", async () => {
const longQuery = "A".repeat(10000);
const result = await controller.handleMCP({
...validRequest,
query: longQuery,
});

expect(result.answer).toBe(`Based on company standards: ${longQuery}`);
});
});
});
40 changes: 40 additions & 0 deletions apps/api/src/mcp/mcp.module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Test } from "@nestjs/testing";
import { MCPModule } from "./mcp.module";
import { MCPController } from "./mcp.controller";

describe("MCPModule", () => {
it("should be defined and compile successfully", async () => {
const module = await Test.createTestingModule({
imports: [MCPModule],
}).compile();

expect(module).toBeDefined();
});

it("should provide MCPController", async () => {
const module = await Test.createTestingModule({
imports: [MCPModule],
}).compile();

const controller = module.get(MCPController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(MCPController);
});

it("should not throw when creating the module", async () => {
await expect(
Test.createTestingModule({
imports: [MCPModule],
}).compile()
).resolves.not.toThrow();
});

it("should expose MCPController handleMCP method via module", async () => {
const module = await Test.createTestingModule({
imports: [MCPModule],
}).compile();

const controller = module.get(MCPController);
expect(typeof controller.handleMCP).toBe("function");
});
});
Loading
Loading