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
151 changes: 151 additions & 0 deletions packages/mcp/src/tool-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,157 @@ test("impact surfaces cochanges for the target's file as a side section", async
);
});

test("impact: untestedBlastRadius classifies direct dependents by coverage", async () => {
await withTestHarness(
{
nodes: [
{ id: "Function:src/foo.ts:foo", name: "foo", kind: "Function", file_path: "src/foo.ts" },
// Two direct dependents (depth-1, upstream): one well-covered, one thin.
{
id: "Function:src/well.ts:well",
name: "well",
kind: "Function",
file_path: "src/well.ts",
coveragePercent: 0.9,
},
{
id: "Function:src/thin.ts:thin",
name: "thin",
kind: "Function",
file_path: "src/thin.ts",
coveragePercent: 0.1,
},
],
relations: [
{
id: "E:1",
from_id: "Function:src/well.ts:well",
to_id: "Function:src/foo.ts:foo",
type: "CALLS",
confidence: 0.9,
},
{
id: "E:2",
from_id: "Function:src/thin.ts:thin",
to_id: "Function:src/foo.ts:foo",
type: "CALLS",
confidence: 0.9,
},
],
},
async (ctx, server) => {
registerImpactTool(server, ctx);
const handler = getHandler(server, "impact");
const result = await handler(
{ target: "Function:src/foo.ts:foo", direction: "upstream", repo: "fakerepo" },
{},
);
const sc = result.structuredContent as {
untestedBlastRadius?: {
threshold: number;
directCount: number;
testedCount: number;
untestedCount: number;
unknownCount: number;
untested: Array<{ id: string; coveragePercent: number | null }>;
unknownCoverage: Array<{ id: string }>;
};
};
assert.ok(
sc.untestedBlastRadius,
"untestedBlastRadius present when there are direct dependents",
);
const ubr = sc.untestedBlastRadius;
assert.equal(ubr?.directCount, 2);
assert.equal(ubr?.testedCount, 1, "well-covered dependent counted as tested");
assert.equal(ubr?.untestedCount, 1, "thin dependent counted as untested");
assert.equal(ubr?.unknownCount, 0);
assert.equal(ubr?.untested[0]?.id, "Function:src/thin.ts:thin");
assert.equal(ubr?.untested[0]?.coveragePercent, 0.1);
const first = result.content[0];
assert.ok(first && first.type === "text");
assert.match(first.text, /Untested blast radius/);
},
);
});

test("impact: dependents with no ingested coverage land in unknownCoverage, never untested", async () => {
await withTestHarness(
{
nodes: [
{ id: "Function:src/foo.ts:foo", name: "foo", kind: "Function", file_path: "src/foo.ts" },
// Direct dependent with NO coveragePercent and NO covered File node →
// must be UNKNOWN, not a false 0% / untested.
{
id: "Function:src/dep.ts:dep",
name: "dep",
kind: "Function",
file_path: "src/dep.ts",
},
],
relations: [
{
id: "E:1",
from_id: "Function:src/dep.ts:dep",
to_id: "Function:src/foo.ts:foo",
type: "CALLS",
confidence: 0.9,
},
],
},
async (ctx, server) => {
registerImpactTool(server, ctx);
const handler = getHandler(server, "impact");
const result = await handler(
{ target: "Function:src/foo.ts:foo", direction: "upstream", repo: "fakerepo" },
{},
);
const sc = result.structuredContent as {
untestedBlastRadius?: {
untestedCount: number;
unknownCount: number;
untested: Array<{ id: string }>;
unknownCoverage: Array<{ id: string; coveragePercent: number | null }>;
};
};
assert.ok(sc.untestedBlastRadius);
assert.equal(sc.untestedBlastRadius?.untestedCount, 0, "no coverage ≠ untested");
assert.equal(sc.untestedBlastRadius?.unknownCount, 1);
assert.equal(sc.untestedBlastRadius?.unknownCoverage[0]?.id, "Function:src/dep.ts:dep");
assert.equal(
sc.untestedBlastRadius?.unknownCoverage[0]?.coveragePercent,
null,
"unknown coverage carries null, not 0",
);
},
);
});

test("impact: untestedBlastRadius omitted when there are no direct dependents", async () => {
await withTestHarness(
{
nodes: [
{ id: "Function:src/foo.ts:foo", name: "foo", kind: "Function", file_path: "src/foo.ts" },
],
relations: [],
},
async (ctx, server) => {
registerImpactTool(server, ctx);
const handler = getHandler(server, "impact");
const result = await handler(
{ target: "Function:src/foo.ts:foo", direction: "upstream", repo: "fakerepo" },
{},
);
const sc = result.structuredContent as { untestedBlastRadius?: unknown };
assert.equal(
sc.untestedBlastRadius,
undefined,
"no direct dependents → no untestedBlastRadius noise",
);
},
);
});

test("context: confidenceBreakdown tallies LSP-confirmed vs heuristic vs demoted edges", async () => {
await withTestHarness(
{
Expand Down
115 changes: 115 additions & 0 deletions packages/mcp/src/tools/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,121 @@ test("context: include_content attaches source (capped at 2000 chars)", async ()
);
});

test("context: surfaces per-symbol coverage when the overlay ingested a ratio", async () => {
await withHarness(
{
nodes: [
// Callable carries a per-symbol coveragePercent from the coverage phase.
{
id: "F:tested",
name: "tested",
kind: "Function",
file_path: "src/tested.ts",
coveragePercent: 0.92,
},
],
relations: [],
},
async (ctx, server) => {
registerContextTool(server, ctx);
const handler = getToolHandler(server, "context");
const result = await handler({ uid: "F:tested", repo: "fakerepo" }, {});
const sc = result.structuredContent as {
coverage?: { percent: number; covered: boolean; source: string };
};
assert.ok(sc.coverage, "coverage block present when a ratio was ingested");
assert.equal(sc.coverage?.percent, 0.92);
assert.equal(sc.coverage?.covered, true, "0.92 ≥ 0.5 threshold → covered");
assert.equal(sc.coverage?.source, "symbol");
const first = result.content[0];
assert.ok(first && first.type === "text");
assert.match(first.text, /Coverage: 92\.0% \(covered, from symbol\)/);
},
);
});

test("context: thin per-symbol coverage is reported as not covered", async () => {
await withHarness(
{
nodes: [
{
id: "F:thin",
name: "thin",
kind: "Function",
file_path: "src/thin.ts",
coveragePercent: 0.1,
},
],
relations: [],
},
async (ctx, server) => {
registerContextTool(server, ctx);
const handler = getToolHandler(server, "context");
const result = await handler({ uid: "F:thin", repo: "fakerepo" }, {});
const sc = result.structuredContent as {
coverage?: { percent: number; covered: boolean };
};
assert.ok(sc.coverage);
assert.equal(sc.coverage?.percent, 0.1);
assert.equal(sc.coverage?.covered, false, "0.1 < 0.5 threshold → not covered");
},
);
});

test("context: a symbol with no coverage inherits its enclosing File ratio", async () => {
await withHarness(
{
nodes: [
// The symbol carries NO coveragePercent; its enclosing File node does.
{ id: "F:nocov", name: "nocov", kind: "Function", file_path: "src/mixed.ts" },
{
id: "File:mixed",
name: "mixed.ts",
kind: "File",
file_path: "src/mixed.ts",
coveragePercent: 0.7,
},
],
relations: [],
},
async (ctx, server) => {
registerContextTool(server, ctx);
const handler = getToolHandler(server, "context");
const result = await handler({ uid: "F:nocov", repo: "fakerepo" }, {});
const sc = result.structuredContent as {
coverage?: { percent: number; covered: boolean; source: string };
};
assert.ok(sc.coverage, "coverage inherited from the enclosing File node");
assert.equal(sc.coverage?.percent, 0.7);
assert.equal(sc.coverage?.source, "file");
},
);
});

test("context: coverage field is OMITTED when no report was ingested (never a false 0%)", async () => {
await withHarness(
{
nodes: [
// No coveragePercent anywhere on the symbol or its file.
{ id: "F:uncov", name: "uncov", kind: "Function", file_path: "src/uncov.ts" },
],
relations: [],
},
async (ctx, server) => {
registerContextTool(server, ctx);
const handler = getToolHandler(server, "context");
const result = await handler({ uid: "F:uncov", repo: "fakerepo" }, {});
const sc = result.structuredContent as { coverage?: unknown };
assert.equal(sc.coverage, undefined, "absent coverage → field omitted, not 0%");
const first = result.content[0];
assert.ok(first && first.type === "text");
assert.match(first.text, /Coverage: unknown/);
// Critically, the text must NOT imply 0% when coverage was never ingested.
assert.doesNotMatch(first.text, /Coverage: 0\.0%/);
},
);
});

test("context: categorises incoming + outgoing edges by edge type", async () => {
await withHarness(
{
Expand Down
Loading
Loading