fix: restore strict noUncheckedIndexedAccess and skip redundant docker build in CI

This commit is contained in:
Lucas Winther
2026-09-06 20:54:12 +02:00
parent a30d913470
commit 03a1f44517
6 changed files with 82 additions and 44 deletions
+2
View File
@@ -36,6 +36,8 @@ jobs:
- name: Run test suite
run: bun test
env:
SKIP_DOCKER_TESTS: "true"
docker:
name: Build & Publish Docker Image
+1 -1
View File
@@ -90,7 +90,7 @@ export async function getQueryResults(
if (err instanceof OpenProjectNotFoundError) {
const queryDetail = await opClient.get<HalResource>(`queries/${id}`);
const resultsLink = queryDetail._links?.results;
const resultsHref = Array.isArray(resultsLink) ? resultsLink[0]?.href : resultsLink?.href;
const resultsHref = Array.isArray(resultsLink) ? resultsLink?.[0]?.href : resultsLink?.href;
if (typeof resultsHref === "string" && resultsHref.length > 0) {
const response = await opClient.get<HalCollection<HalResource>>(resultsHref, query);
return unpackCollection(response, normalizeWorkPackageSummary);
+39 -3
View File
@@ -31,6 +31,11 @@ describe("Docker Container Packaging", () => {
});
test("Docker image builds and starts up cleanly in container", async () => {
if (process.env.SKIP_DOCKER_TESTS === "true" || process.env.CI === "true") {
console.log("Skipping live docker build test in CI / SKIP_DOCKER_TESTS environment");
return;
}
// Check if docker daemon is available in test environment
const proc = Bun.spawn(["which", "docker"], { stdout: "pipe", stderr: "pipe" });
const exitCode = await proc.exited;
@@ -70,12 +75,43 @@ describe("Docker Container Packaging", () => {
}
);
// Give it 1.5s to start and write banner to stderr
await Bun.sleep(1500);
// Read stderr with a timeout loop up to 5000ms waiting for the startup banner
let stderrText = "";
const decoder = new TextDecoder();
const reader = runProc.stderr.getReader();
const readLoop = (async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
stderrText += decoder.decode(value, { stream: true });
if (stderrText.includes("[openproject-mcp] Server started (readOnly=true)")) {
break;
}
}
} catch {
// stream closed or cancelled
}
})();
const startTime = Date.now();
while (
Date.now() - startTime < 5000 &&
!stderrText.includes("[openproject-mcp] Server started (readOnly=true)")
) {
await Bun.sleep(50);
}
runProc.kill();
await runProc.exited;
try {
await reader.cancel();
} catch {
// ignore
}
await readLoop;
const stderrText = await new Response(runProc.stderr).text();
expect(stderrText).toContain("[openproject-mcp] Server started (readOnly=true)");
}, 60000);
});
+26 -26
View File
@@ -115,9 +115,9 @@ describe("Projects Service", () => {
expect(requestedUrl).toContain("offset=1");
expect(requestedUrl).toContain("sortBy=");
expect(result.total).toBe(1);
expect(result.items[0].id).toBe(4);
expect(result.items[0].identifier).toBe("mcp-test-project");
expect(result.items[0].name).toBe("MCP Test Project");
expect(result.items[0]!.id).toBe(4);
expect(result.items[0]!.identifier).toBe("mcp-test-project");
expect(result.items[0]!.name).toBe("MCP Test Project");
});
test("getProject retrieves project detail by id or identifier", async () => {
@@ -304,10 +304,10 @@ describe("Work Packages Service", () => {
expect(requestedUrl).toContain("filters=");
expect(requestedUrl).toContain("pageSize=25");
expect(result.total).toBe(1);
expect(result.items[0].id).toBe(38);
expect(result.items[0].subject).toBe("Implement MCP Server Core Protocol");
expect(result.items[0].status).toBe("In progress");
expect(result.items[0].type).toBe("Task");
expect(result.items[0]!.id).toBe(38);
expect(result.items[0]!.subject).toBe("Implement MCP Server Core Protocol");
expect(result.items[0]!.status).toBe("In progress");
expect(result.items[0]!.type).toBe("Task");
});
test("getWorkPackage returns full details with parent and children", async () => {
@@ -329,7 +329,7 @@ describe("Work Packages Service", () => {
expect(wp.parent?.id).toBe(30);
expect(wp.parent?.subject).toBe("Epic Parent");
expect(wp.children?.length).toBe(2);
expect(wp.children?.[0].id).toBe(39);
expect(wp.children?.[0]?.id).toBe(39);
expect(wp.lockVersion).toBe(3);
});
@@ -425,9 +425,9 @@ describe("Queries Service", () => {
expect(requestedUrl).toContain("pageSize=20");
const decodedUrl = decodeURIComponent(requestedUrl);
expect(decodedUrl).toContain('"project"');
expect(result.items[0].id).toBe(30);
expect(result.items[0].name).toBe("MCP Active Tasks");
expect(result.items[0].projectId).toBe(4);
expect(result.items[0]!.id).toBe(30);
expect(result.items[0]!.name).toBe("MCP Active Tasks");
expect(result.items[0]!.projectId).toBe(4);
});
test("getQuery returns normalized details with columns and filters", async () => {
@@ -448,9 +448,9 @@ describe("Queries Service", () => {
expect(query.name).toBe("MCP Active Tasks");
expect(query.columns).toContain("ID");
expect(query.columns).toContain("Subject");
expect(query.sortBy?.[0].attribute).toBe("id");
expect(query.sortBy?.[0].direction).toBe("asc");
expect(query.filters?.[0].field).toBe("Status");
expect(query.sortBy?.[0]?.attribute).toBe("id");
expect(query.sortBy?.[0]?.direction).toBe("asc");
expect(query.filters?.[0]?.field).toBe("Status");
expect(query.resultsHref).toBe("/api/v3/queries/30/results");
});
@@ -493,8 +493,8 @@ describe("Queries Service", () => {
const result = await getQueryResults(30, { pageSize: 10 }, client);
expect(requestedUrl).toContain("/api/v3/queries/30/results");
expect(requestedUrl).toContain("pageSize=10");
expect(result.items[0].id).toBe(38);
expect(result.items[0].subject).toBe("Implement MCP Server Core Protocol");
expect(result.items[0]!.id).toBe(38);
expect(result.items[0]!.subject).toBe("Implement MCP Server Core Protocol");
});
});
@@ -523,9 +523,9 @@ describe("Metadata Service", () => {
const statuses = await listStatuses(client);
expect(statuses.length).toBe(2);
expect(statuses[0].name).toBe("New");
expect(statuses[0].isClosed).toBe(false);
expect(statuses[1].isClosed).toBe(true);
expect(statuses[0]!.name).toBe("New");
expect(statuses[0]!.isClosed).toBe(false);
expect(statuses[1]!.isClosed).toBe(true);
});
test("listTypes scopes to project when projectId is provided", async () => {
@@ -551,7 +551,7 @@ describe("Metadata Service", () => {
const types = await listTypes({ projectId: 4 }, client);
expect(requestedUrl).toContain("/api/v3/projects/4/types");
expect(types[0].name).toBe("Task");
expect(types[0]!.name).toBe("Task");
});
test("listTypes fetches global types when projectId is omitted", async () => {
@@ -578,7 +578,7 @@ describe("Metadata Service", () => {
const types = await listTypes(undefined, client);
expect(requestedUrl).toContain("/api/v3/types");
expect(requestedUrl).not.toContain("/projects/");
expect(types[0].name).toBe("Task");
expect(types[0]!.name).toBe("Task");
});
test("listPriorities retrieves priority items", async () => {
@@ -601,7 +601,7 @@ describe("Metadata Service", () => {
});
const priorities = await listPriorities(client);
expect(priorities[0].name).toBe("High");
expect(priorities[0]!.name).toBe("High");
});
test("listUsers returns paginated UserItem list", async () => {
@@ -631,8 +631,8 @@ describe("Metadata Service", () => {
const users = await listUsers({ pageSize: 25, offset: 1 }, client);
expect(requestedUrl).toContain("/api/v3/users");
expect(users.items[0].login).toBe("admin");
expect(users.items[0].admin).toBe(true);
expect(users.items[0]!.login).toBe("admin");
expect(users.items[0]!.admin).toBe(true);
});
test("src/services/index.ts exports all services properly", () => {
@@ -673,8 +673,8 @@ describe("Live Container Integration (Domain Services)", () => {
const wps = await domainServices.listWorkPackages({ projectId: "mcp-test-project" }, client);
expect(wps.items.length).toBeGreaterThan(0);
const firstWp = await domainServices.getWorkPackage(wps.items[0].id, client);
expect(firstWp.id).toBe(wps.items[0].id);
const firstWp = await domainServices.getWorkPackage(wps.items[0]!.id, client);
expect(firstWp.id).toBe(wps.items[0]!.id);
expect(firstWp.subject).toBeDefined();
const searchRes = await domainServices.searchWorkPackages("MCP", { projectId: "mcp-test-project" }, client);
+13 -13
View File
@@ -1059,7 +1059,7 @@ describe("Tool Execution Wrapper", () => {
>;
}
)._registeredTools["wrapped_tool"];
const result = await registered.handler({});
const result = await registered!.handler({});
expect(wrapperCalled).toBe(true);
expect(result.content[0]?.text).toBe("success");
});
@@ -1101,7 +1101,7 @@ describe("Tool Execution Wrapper", () => {
}
)._registeredTools["param_wrapped_tool"];
const result = await registered.handler({ id: 123 });
const result = await registered!.handler({ id: 123 });
expect(capturedToolName).toBe("param_wrapped_tool");
expect(capturedArgs).toEqual({ id: 123 });
expect(result.content[0]?.text).toBe("id: 123");
@@ -1146,7 +1146,7 @@ describe("Tool Execution Wrapper", () => {
} as unknown as OpenProjectClient;
await runWithContext({ client: dummyClient, isReadOnly: false }, () =>
listProjects.handler({})
listProjects!.handler({})
);
expect(interceptedTools).toContain("openproject_list_projects");
@@ -1163,7 +1163,7 @@ describe("Live Container Integration (All 10 MCP Tools)", () => {
await runWithContext({ client: liveClient, isReadOnly: false }, async () => {
const listRes = await handleListProjects({ pageSize: 5 });
expect(listRes.isError).toBeUndefined();
const listData = JSON.parse(listRes.content[0].text);
const listData = JSON.parse(listRes.content[0]!.text);
expect(listData.projects.length).toBeGreaterThan(0);
const targetProject =
@@ -1172,7 +1172,7 @@ describe("Live Container Integration (All 10 MCP Tools)", () => {
) || listData.projects[0];
const getRes = await handleGetProject({ projectId: targetProject.id });
expect(getRes.isError).toBeUndefined();
const getData = JSON.parse(getRes.content[0].text);
const getData = JSON.parse(getRes.content[0]!.text);
expect(getData.id).toBe(targetProject.id);
});
});
@@ -1181,13 +1181,13 @@ describe("Live Container Integration (All 10 MCP Tools)", () => {
await runWithContext({ client: liveClient, isReadOnly: false }, async () => {
const listRes = await handleListWorkPackages({ pageSize: 5 });
expect(listRes.isError).toBeUndefined();
const listData = JSON.parse(listRes.content[0].text);
const listData = JSON.parse(listRes.content[0]!.text);
expect(listData.workPackages.length).toBeGreaterThan(0);
const wpId = listData.workPackages[0].id;
const getRes = await handleGetWorkPackage({ workPackageId: wpId });
expect(getRes.isError).toBeUndefined();
const getData = JSON.parse(getRes.content[0].text);
const getData = JSON.parse(getRes.content[0]!.text);
expect(getData.id).toBe(wpId);
expect(getData.subject).toBeDefined();
});
@@ -1197,13 +1197,13 @@ describe("Live Container Integration (All 10 MCP Tools)", () => {
await runWithContext({ client: liveClient, isReadOnly: false }, async () => {
const listRes = await handleListQueries({});
expect(listRes.isError).toBeUndefined();
const listData = JSON.parse(listRes.content[0].text);
const listData = JSON.parse(listRes.content[0]!.text);
expect(listData.queries.length).toBeGreaterThan(0);
const queryId = listData.queries[0].id;
const getRes = await handleGetQuery({ queryId });
expect(getRes.isError).toBeUndefined();
const getData = JSON.parse(getRes.content[0].text);
const getData = JSON.parse(getRes.content[0]!.text);
expect(getData.query.id).toBe(queryId);
expect(getData.results).toBeDefined();
});
@@ -1213,22 +1213,22 @@ describe("Live Container Integration (All 10 MCP Tools)", () => {
await runWithContext({ client: liveClient, isReadOnly: false }, async () => {
const typesRes = await handleListTypes({});
expect(typesRes.isError).toBeUndefined();
const typesData = JSON.parse(typesRes.content[0].text);
const typesData = JSON.parse(typesRes.content[0]!.text);
expect(typesData.types.length).toBeGreaterThan(0);
const statusesRes = await handleListStatuses({});
expect(statusesRes.isError).toBeUndefined();
const statusesData = JSON.parse(statusesRes.content[0].text);
const statusesData = JSON.parse(statusesRes.content[0]!.text);
expect(statusesData.statuses.length).toBeGreaterThan(0);
const prioritiesRes = await handleListPriorities({});
expect(prioritiesRes.isError).toBeUndefined();
const prioritiesData = JSON.parse(prioritiesRes.content[0].text);
const prioritiesData = JSON.parse(prioritiesRes.content[0]!.text);
expect(prioritiesData.priorities.length).toBeGreaterThan(0);
const usersRes = await handleListUsers({ pageSize: 5 });
expect(usersRes.isError).toBeUndefined();
const usersData = JSON.parse(usersRes.content[0].text);
const usersData = JSON.parse(usersRes.content[0]!.text);
expect(usersData.users.length).toBeGreaterThan(0);
});
});
+1 -1
View File
@@ -18,7 +18,7 @@
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": false,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Developer ergonomic options