gsd-pi 2.74.0-dev.2b524c3 → 2.74.0-dev.b741afb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +85 -0
- package/dist/headless-query.js +4 -1
- package/dist/help-text.js +23 -0
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +11 -4
- package/dist/resources/extensions/gsd/auto/phases.js +45 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +52 -56
- package/dist/resources/extensions/gsd/auto-prompts.js +12 -0
- package/dist/resources/extensions/gsd/auto.js +8 -2
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +21 -8
- package/dist/resources/extensions/gsd/commands/catalog.js +26 -1
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +20 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +68 -9
- package/dist/resources/extensions/gsd/commands-add-tests.js +111 -0
- package/dist/resources/extensions/gsd/commands-backlog.js +140 -0
- package/dist/resources/extensions/gsd/commands-do.js +79 -0
- package/dist/resources/extensions/gsd/commands-maintenance.js +6 -6
- package/dist/resources/extensions/gsd/commands-pr-branch.js +180 -0
- package/dist/resources/extensions/gsd/commands-session-report.js +82 -0
- package/dist/resources/extensions/gsd/commands-ship.js +187 -0
- package/dist/resources/extensions/gsd/db-writer.js +3 -5
- package/dist/resources/extensions/gsd/graph-context.js +66 -0
- package/dist/resources/extensions/gsd/gsd-db.js +321 -0
- package/dist/resources/extensions/gsd/index.js +15 -2
- package/dist/resources/extensions/gsd/md-importer.js +3 -4
- package/dist/resources/extensions/gsd/memory-store.js +19 -51
- package/dist/resources/extensions/gsd/milestone-validation-gates.js +13 -12
- package/dist/resources/extensions/gsd/native-git-bridge.js +7 -4
- package/dist/resources/extensions/gsd/prompts/add-tests.md +35 -0
- package/dist/resources/extensions/gsd/state.js +5 -1
- package/dist/resources/extensions/gsd/tools/complete-slice.js +15 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +3 -14
- package/dist/resources/extensions/gsd/triage-resolution.js +2 -5
- package/dist/resources/extensions/gsd/workflow-manifest.js +8 -69
- package/dist/resources/extensions/gsd/workflow-migration.js +21 -22
- package/dist/resources/extensions/gsd/workflow-projections.js +4 -1
- package/dist/resources/extensions/gsd/workflow-reconcile.js +14 -11
- package/dist/tsconfig.extensions.tsbuildinfo +1 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +3 -2
- package/packages/daemon/package.json +2 -2
- package/packages/mcp-server/dist/index.d.ts +3 -0
- package/packages/mcp-server/dist/index.d.ts.map +1 -1
- package/packages/mcp-server/dist/index.js +3 -0
- package/packages/mcp-server/dist/index.js.map +1 -1
- package/packages/mcp-server/dist/readers/graph.d.ts +87 -0
- package/packages/mcp-server/dist/readers/graph.d.ts.map +1 -0
- package/packages/mcp-server/dist/readers/graph.js +548 -0
- package/packages/mcp-server/dist/readers/graph.js.map +1 -0
- package/packages/mcp-server/dist/readers/index.d.ts +2 -0
- package/packages/mcp-server/dist/readers/index.d.ts.map +1 -1
- package/packages/mcp-server/dist/readers/index.js +1 -0
- package/packages/mcp-server/dist/readers/index.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +65 -0
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/package.json +2 -2
- package/packages/mcp-server/src/index.ts +15 -0
- package/packages/mcp-server/src/readers/graph.test.ts +426 -0
- package/packages/mcp-server/src/readers/graph.ts +708 -0
- package/packages/mcp-server/src/readers/index.ts +12 -0
- package/packages/mcp-server/src/server.ts +83 -0
- package/packages/mcp-server/tsconfig.json +1 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -0
- package/packages/native/package.json +2 -2
- package/packages/native/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-agent-core/tsconfig.json +1 -0
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-ai/tsconfig.json +1 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-coding-agent/tsconfig.json +1 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-tui/package.json +1 -1
- package/packages/pi-tui/tsconfig.json +1 -0
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -0
- package/packages/rpc-client/package.json +1 -1
- package/packages/rpc-client/tsconfig.json +1 -0
- package/packages/rpc-client/tsconfig.tsbuildinfo +1 -0
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +12 -4
- package/src/resources/extensions/gsd/auto/loop-deps.ts +6 -0
- package/src/resources/extensions/gsd/auto/phases.ts +68 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +60 -57
- package/src/resources/extensions/gsd/auto-prompts.ts +13 -0
- package/src/resources/extensions/gsd/auto.ts +7 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -8
- package/src/resources/extensions/gsd/commands/catalog.ts +26 -1
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +20 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +74 -9
- package/src/resources/extensions/gsd/commands-add-tests.ts +137 -0
- package/src/resources/extensions/gsd/commands-backlog.ts +182 -0
- package/src/resources/extensions/gsd/commands-do.ts +109 -0
- package/src/resources/extensions/gsd/commands-maintenance.ts +6 -6
- package/src/resources/extensions/gsd/commands-pr-branch.ts +234 -0
- package/src/resources/extensions/gsd/commands-session-report.ts +101 -0
- package/src/resources/extensions/gsd/commands-ship.ts +219 -0
- package/src/resources/extensions/gsd/db-writer.ts +3 -5
- package/src/resources/extensions/gsd/graph-context.ts +85 -0
- package/src/resources/extensions/gsd/gsd-db.ts +467 -0
- package/src/resources/extensions/gsd/index.ts +18 -2
- package/src/resources/extensions/gsd/md-importer.ts +3 -5
- package/src/resources/extensions/gsd/memory-store.ts +31 -62
- package/src/resources/extensions/gsd/milestone-validation-gates.ts +13 -14
- package/src/resources/extensions/gsd/native-git-bridge.ts +11 -12
- package/src/resources/extensions/gsd/prompts/add-tests.md +35 -0
- package/src/resources/extensions/gsd/state.ts +9 -2
- package/src/resources/extensions/gsd/tests/commands-backlog.test.ts +158 -0
- package/src/resources/extensions/gsd/tests/commands-do.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/commands-pr-branch.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/commands-session-report.test.ts +82 -0
- package/src/resources/extensions/gsd/tests/commands-ship.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +154 -0
- package/src/resources/extensions/gsd/tests/graph-context.test.ts +337 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +68 -1
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +223 -0
- package/src/resources/extensions/gsd/tools/complete-slice.ts +19 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +3 -11
- package/src/resources/extensions/gsd/triage-resolution.ts +2 -7
- package/src/resources/extensions/gsd/workflow-manifest.ts +9 -104
- package/src/resources/extensions/gsd/workflow-migration.ts +21 -29
- package/src/resources/extensions/gsd/workflow-projections.ts +8 -1
- package/src/resources/extensions/gsd/workflow-reconcile.ts +15 -15
- /package/dist/web/standalone/.next/static/{YzIEI9sxJy4t5xgClF08g → XnHY5eXUsTCFmNodWHetD}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{YzIEI9sxJy4t5xgClF08g → XnHY5eXUsTCFmNodWHetD}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
// Test the formatting logic used by session-report.
|
|
5
|
+
// The actual handler requires runtime context (metrics module), so we
|
|
6
|
+
// test the core formatting and aggregation patterns.
|
|
7
|
+
|
|
8
|
+
test("session-report: format cost correctly", () => {
|
|
9
|
+
// Simple cost formatting test
|
|
10
|
+
const formatCost = (cost: number): string => {
|
|
11
|
+
if (cost < 0.01) return "<$0.01";
|
|
12
|
+
return `$${cost.toFixed(2)}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
assert.equal(formatCost(0), "<$0.01");
|
|
16
|
+
assert.equal(formatCost(0.005), "<$0.01");
|
|
17
|
+
assert.equal(formatCost(1.5), "$1.50");
|
|
18
|
+
assert.equal(formatCost(10.999), "$11.00");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("session-report: format token count", () => {
|
|
22
|
+
const formatTokenCount = (count: number): string => {
|
|
23
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
24
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;
|
|
25
|
+
return String(count);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
assert.equal(formatTokenCount(500), "500");
|
|
29
|
+
assert.equal(formatTokenCount(1500), "1.5K");
|
|
30
|
+
assert.equal(formatTokenCount(1_200_000), "1.2M");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("session-report: aggregate by model", () => {
|
|
34
|
+
interface UnitMetric {
|
|
35
|
+
model: string;
|
|
36
|
+
cost: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const units: UnitMetric[] = [
|
|
40
|
+
{ model: "opus", cost: 1.0 },
|
|
41
|
+
{ model: "opus", cost: 0.8 },
|
|
42
|
+
{ model: "sonnet", cost: 0.3 },
|
|
43
|
+
{ model: "sonnet", cost: 0.5 },
|
|
44
|
+
{ model: "sonnet", cost: 0.2 },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const byModel = new Map<string, { count: number; cost: number }>();
|
|
48
|
+
for (const u of units) {
|
|
49
|
+
const existing = byModel.get(u.model) ?? { count: 0, cost: 0 };
|
|
50
|
+
existing.count++;
|
|
51
|
+
existing.cost += u.cost;
|
|
52
|
+
byModel.set(u.model, existing);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const opus = byModel.get("opus")!;
|
|
56
|
+
assert.equal(opus.count, 2);
|
|
57
|
+
assert.ok(Math.abs(opus.cost - 1.8) < 0.01);
|
|
58
|
+
|
|
59
|
+
const sonnet = byModel.get("sonnet")!;
|
|
60
|
+
assert.equal(sonnet.count, 3);
|
|
61
|
+
assert.ok(Math.abs(sonnet.cost - 1.0) < 0.01);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("session-report: --json flag detection", () => {
|
|
65
|
+
const args1 = "--json";
|
|
66
|
+
const args2 = "--save --json";
|
|
67
|
+
const args3 = "something else";
|
|
68
|
+
|
|
69
|
+
assert.ok(args1.includes("--json"));
|
|
70
|
+
assert.ok(args2.includes("--json"));
|
|
71
|
+
assert.ok(!args3.includes("--json"));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("session-report: --save flag detection", () => {
|
|
75
|
+
const args1 = "--save";
|
|
76
|
+
const args2 = "--save --json";
|
|
77
|
+
const args3 = "";
|
|
78
|
+
|
|
79
|
+
assert.ok(args1.includes("--save"));
|
|
80
|
+
assert.ok(args2.includes("--save"));
|
|
81
|
+
assert.ok(!args3.includes("--save"));
|
|
82
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
// Test the PR content generation logic used by /gsd ship.
|
|
5
|
+
// Full integration requires gh CLI + git, so we test the text generation.
|
|
6
|
+
|
|
7
|
+
test("ship: generates TL;DR format", () => {
|
|
8
|
+
// Simulate generatePRContent output structure
|
|
9
|
+
const milestoneId = "M001";
|
|
10
|
+
const milestoneTitle = "User authentication system";
|
|
11
|
+
|
|
12
|
+
const title = `feat: ${milestoneTitle}`;
|
|
13
|
+
assert.equal(title, "feat: User authentication system");
|
|
14
|
+
assert.ok(title.length < 80); // PR title should be short
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("ship: --dry-run flag detection", () => {
|
|
18
|
+
const args1 = "--dry-run";
|
|
19
|
+
const args2 = "--draft --dry-run";
|
|
20
|
+
const args3 = "--draft";
|
|
21
|
+
|
|
22
|
+
assert.ok(args1.includes("--dry-run"));
|
|
23
|
+
assert.ok(args2.includes("--dry-run"));
|
|
24
|
+
assert.ok(!args3.includes("--dry-run"));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("ship: --base flag parsing", () => {
|
|
28
|
+
const args = "--base develop --draft";
|
|
29
|
+
const baseMatch = args.match(/--base\s+(\S+)/);
|
|
30
|
+
assert.ok(baseMatch);
|
|
31
|
+
assert.equal(baseMatch[1], "develop");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("ship: --base flag absent defaults", () => {
|
|
35
|
+
const args = "--draft";
|
|
36
|
+
const baseMatch = args.match(/--base\s+(\S+)/);
|
|
37
|
+
assert.equal(baseMatch, null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("ship: --force flag detection", () => {
|
|
41
|
+
const args1 = "--force";
|
|
42
|
+
const args2 = "";
|
|
43
|
+
|
|
44
|
+
assert.ok(args1.includes("--force"));
|
|
45
|
+
assert.ok(!args2.includes("--force"));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("ship: change type checklist format", () => {
|
|
49
|
+
const checklist = [
|
|
50
|
+
"- [x] `feat` — New feature or capability",
|
|
51
|
+
"- [ ] `fix` — Bug fix",
|
|
52
|
+
"- [ ] `refactor` — Code restructuring",
|
|
53
|
+
"- [ ] `test` — Adding or updating tests",
|
|
54
|
+
"- [ ] `docs` — Documentation only",
|
|
55
|
+
"- [ ] `chore` — Build, CI, or tooling changes",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// Verify format matches CONTRIBUTING.md expectations
|
|
59
|
+
for (const line of checklist) {
|
|
60
|
+
assert.match(line, /^- \[[ x]\] `\w+` — .+$/);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("ship: PR body contains required sections", () => {
|
|
65
|
+
const requiredSections = ["## TL;DR", "## Change type"];
|
|
66
|
+
const body = "## TL;DR\n\n**What:** Ship M001\n\n## Change type\n\n- [x] `feat`";
|
|
67
|
+
|
|
68
|
+
for (const section of requiredSections) {
|
|
69
|
+
assert.ok(body.includes(section), `Missing section: ${section}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
@@ -217,6 +217,20 @@ describe("workflow command handler", () => {
|
|
|
217
217
|
);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
+
it("preserves quoted workflow run overrides (#4130)", async () => {
|
|
221
|
+
const { parseWorkflowRunArgs } = await import("../commands/handlers/workflow.ts");
|
|
222
|
+
assert.deepStrictEqual(
|
|
223
|
+
parseWorkflowRunArgs('demo-workflow target="multi word target" region=\'us east\''),
|
|
224
|
+
{
|
|
225
|
+
defName: "demo-workflow",
|
|
226
|
+
overrides: {
|
|
227
|
+
target: "multi word target",
|
|
228
|
+
region: "us east",
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
220
234
|
it("'/gsd workflow run nonexistent' shows error for missing definition", async () => {
|
|
221
235
|
const { handled, notifications } = await callHandler("workflow run nonexistent-def-12345");
|
|
222
236
|
assert.ok(handled, "should be handled");
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Structural contracts for GSD extension bootstrap isolation.
|
|
2
|
+
//
|
|
3
|
+
// The /gsd command must survive failures in the full extension bootstrap
|
|
4
|
+
// (register-extension.ts). This guards against the regression where a
|
|
5
|
+
// Windows-specific import failure in register-shortcuts.ts silently
|
|
6
|
+
// prevented /gsd from being registered at all (#4168, #4172).
|
|
7
|
+
|
|
8
|
+
import { describe, test } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { join, dirname } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const indexSrc = readFileSync(join(__dirname, "../index.ts"), "utf-8");
|
|
16
|
+
const registerExtSrc = readFileSync(
|
|
17
|
+
join(__dirname, "../bootstrap/register-extension.ts"),
|
|
18
|
+
"utf-8",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// ─── index.ts: core /gsd command must be registered before full bootstrap ─────
|
|
22
|
+
|
|
23
|
+
describe("index.ts bootstrap isolation", () => {
|
|
24
|
+
test("imports registerGSDCommand from commands/index.js separately", () => {
|
|
25
|
+
assert.ok(
|
|
26
|
+
indexSrc.includes('./commands/index.js"') || indexSrc.includes("./commands/index.js'"),
|
|
27
|
+
"index.ts must import registerGSDCommand from ./commands/index.js",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("calls registerGSDCommand before importing register-extension.js", () => {
|
|
32
|
+
const gsdCommandCallPos = indexSrc.indexOf("registerGSDCommand(pi)");
|
|
33
|
+
const bootstrapImportPos = indexSrc.indexOf(
|
|
34
|
+
'./bootstrap/register-extension.js"',
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
assert.ok(gsdCommandCallPos >= 0, "must call registerGSDCommand(pi)");
|
|
38
|
+
assert.ok(bootstrapImportPos >= 0, "must import register-extension.js");
|
|
39
|
+
assert.ok(
|
|
40
|
+
gsdCommandCallPos < bootstrapImportPos,
|
|
41
|
+
"registerGSDCommand(pi) must be called BEFORE importing register-extension.js",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("wraps register-extension.js import in try-catch", () => {
|
|
46
|
+
// The dynamic import of register-extension.js must be inside a try block
|
|
47
|
+
const tryPos = indexSrc.indexOf("try {");
|
|
48
|
+
const bootstrapImportPos = indexSrc.indexOf(
|
|
49
|
+
'./bootstrap/register-extension.js"',
|
|
50
|
+
);
|
|
51
|
+
const catchPos = indexSrc.indexOf("catch (err)");
|
|
52
|
+
|
|
53
|
+
assert.ok(tryPos >= 0, "must have try block");
|
|
54
|
+
assert.ok(catchPos >= 0, "must have catch block");
|
|
55
|
+
assert.ok(
|
|
56
|
+
tryPos < bootstrapImportPos && bootstrapImportPos < catchPos,
|
|
57
|
+
"register-extension.js import must be wrapped in try-catch",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("logs warning on bootstrap failure via workflow-logger", () => {
|
|
62
|
+
assert.ok(
|
|
63
|
+
indexSrc.includes("logWarning"),
|
|
64
|
+
"must use logWarning when bootstrap fails",
|
|
65
|
+
);
|
|
66
|
+
assert.ok(
|
|
67
|
+
indexSrc.includes("Extension setup partially failed"),
|
|
68
|
+
"warning message must indicate partial failure with /gsd still available",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ─── register-extension.ts: no double-registration + defensive wrapping ───────
|
|
74
|
+
|
|
75
|
+
describe("register-extension.ts defensive registration", () => {
|
|
76
|
+
test("does NOT import or call registerGSDCommand (avoids double-registration)", () => {
|
|
77
|
+
// registerGSDCommand is now called by index.ts, not register-extension.ts
|
|
78
|
+
assert.ok(
|
|
79
|
+
!registerExtSrc.includes("import { registerGSDCommand }"),
|
|
80
|
+
"register-extension.ts must NOT import registerGSDCommand",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Check the function body of registerGsdExtension doesn't call it
|
|
84
|
+
const funcBodyStart = registerExtSrc.indexOf(
|
|
85
|
+
"export function registerGsdExtension",
|
|
86
|
+
);
|
|
87
|
+
const funcBody = registerExtSrc.slice(funcBodyStart);
|
|
88
|
+
assert.ok(
|
|
89
|
+
!funcBody.includes("registerGSDCommand(pi)"),
|
|
90
|
+
"registerGsdExtension must NOT call registerGSDCommand(pi)",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("still registers worktree, exit, and kill commands", () => {
|
|
95
|
+
const funcBodyStart = registerExtSrc.indexOf(
|
|
96
|
+
"export function registerGsdExtension",
|
|
97
|
+
);
|
|
98
|
+
const funcBody = registerExtSrc.slice(funcBodyStart);
|
|
99
|
+
|
|
100
|
+
assert.ok(
|
|
101
|
+
funcBody.includes("registerWorktreeCommand(pi)"),
|
|
102
|
+
"must register worktree command",
|
|
103
|
+
);
|
|
104
|
+
assert.ok(
|
|
105
|
+
funcBody.includes("registerExitCommand(pi)"),
|
|
106
|
+
"must register exit command",
|
|
107
|
+
);
|
|
108
|
+
assert.ok(
|
|
109
|
+
funcBody.includes('"kill"'),
|
|
110
|
+
"must register kill command",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("wraps non-critical registrations in individual try-catch blocks", () => {
|
|
115
|
+
const funcBodyStart = registerExtSrc.indexOf(
|
|
116
|
+
"export function registerGsdExtension",
|
|
117
|
+
);
|
|
118
|
+
const funcBody = registerExtSrc.slice(funcBodyStart);
|
|
119
|
+
|
|
120
|
+
// Each non-critical registration should be wrapped with error handling
|
|
121
|
+
const registrationNames = [
|
|
122
|
+
"dynamic-tools",
|
|
123
|
+
"db-tools",
|
|
124
|
+
"journal-tools",
|
|
125
|
+
"query-tools",
|
|
126
|
+
"shortcuts",
|
|
127
|
+
"hooks",
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
for (const name of registrationNames) {
|
|
131
|
+
assert.ok(
|
|
132
|
+
funcBody.includes(`"${name}"`),
|
|
133
|
+
`non-critical registration "${name}" must be present`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Must have try-catch inside the registration loop
|
|
138
|
+
assert.ok(
|
|
139
|
+
funcBody.includes("try {") && funcBody.includes("catch (err)"),
|
|
140
|
+
"must have try-catch for non-critical registrations",
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("logs warning when a non-critical registration fails", () => {
|
|
145
|
+
assert.ok(
|
|
146
|
+
registerExtSrc.includes("Failed to register"),
|
|
147
|
+
"must log descriptive warning for individual registration failures",
|
|
148
|
+
);
|
|
149
|
+
assert.ok(
|
|
150
|
+
registerExtSrc.includes("logWarning"),
|
|
151
|
+
"must use logWarning from workflow-logger",
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graph-context.test.ts — Unit tests for inlineGraphSubgraph().
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* Group 1: Null-return paths (empty term, zero nodes, missing graph.json)
|
|
6
|
+
* Group 2: Correct output formatting (nodes, edges, stale annotation)
|
|
7
|
+
* Group 3: Node formatting (description, confidence, no-description)
|
|
8
|
+
*
|
|
9
|
+
* Testing strategy:
|
|
10
|
+
* @gsd-build/mcp-server is dynamically imported inside inlineGraphSubgraph().
|
|
11
|
+
* Because node:test (v22) does not support mock.module() without the
|
|
12
|
+
* --experimental-test-module-mocks flag (not enabled in test:unit), we
|
|
13
|
+
* exercise the real graphQuery/graphStatus functions by controlling the
|
|
14
|
+
* on-disk graph.json that those functions read. This is a clean, deterministic
|
|
15
|
+
* approach that avoids all module-level mocking.
|
|
16
|
+
*
|
|
17
|
+
* Fixture layout per test:
|
|
18
|
+
* <tmpDir>/.gsd/graphs/graph.json
|
|
19
|
+
*
|
|
20
|
+
* builtAt controls staleness: old timestamp → stale, recent → fresh.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it } from "node:test";
|
|
24
|
+
import assert from "node:assert/strict";
|
|
25
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { tmpdir } from "node:os";
|
|
28
|
+
|
|
29
|
+
import { inlineGraphSubgraph } from "../graph-context.ts";
|
|
30
|
+
|
|
31
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
interface TestNode {
|
|
34
|
+
id: string;
|
|
35
|
+
label: string;
|
|
36
|
+
type: string;
|
|
37
|
+
confidence: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
sourceFile?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TestEdge {
|
|
43
|
+
from: string;
|
|
44
|
+
to: string;
|
|
45
|
+
type: string;
|
|
46
|
+
confidence: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface GraphFixture {
|
|
50
|
+
nodes: TestNode[];
|
|
51
|
+
edges: TestEdge[];
|
|
52
|
+
/** ISO timestamp for graph.builtAt. Controls staleness. Default: recent (not stale). */
|
|
53
|
+
builtAt?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Returns an ISO timestamp that is stale (> 24h ago). */
|
|
57
|
+
function staleTimestamp(hoursAgo = 26): string {
|
|
58
|
+
return new Date(Date.now() - hoursAgo * 60 * 60 * 1000).toISOString();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Returns an ISO timestamp that is fresh (< 24h ago). */
|
|
62
|
+
function freshTimestamp(): string {
|
|
63
|
+
return new Date(Date.now() - 30 * 60 * 1000).toISOString(); // 30 minutes ago
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates a temp project directory with a .gsd/graphs/graph.json file.
|
|
68
|
+
* Returns the projectDir path. Caller is responsible for cleanup.
|
|
69
|
+
*/
|
|
70
|
+
function makeProjectDir(fixture: GraphFixture): string {
|
|
71
|
+
const projectDir = mkdtempSync(join(tmpdir(), "graph-ctx-test-"));
|
|
72
|
+
const gsdDir = join(projectDir, ".gsd");
|
|
73
|
+
const graphsDir = join(gsdDir, "graphs");
|
|
74
|
+
mkdirSync(graphsDir, { recursive: true });
|
|
75
|
+
|
|
76
|
+
const graph = {
|
|
77
|
+
nodes: fixture.nodes,
|
|
78
|
+
edges: fixture.edges,
|
|
79
|
+
builtAt: fixture.builtAt ?? freshTimestamp(),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
writeFileSync(join(graphsDir, "graph.json"), JSON.stringify(graph), "utf-8");
|
|
83
|
+
return projectDir;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Removes a temp directory, suppressing errors on Windows. */
|
|
87
|
+
function cleanup(dir: string): void {
|
|
88
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Minimal node factory. */
|
|
92
|
+
function makeNode(overrides: Partial<TestNode> & { id: string; label: string }): TestNode {
|
|
93
|
+
return {
|
|
94
|
+
type: "CLASS",
|
|
95
|
+
confidence: "INFERRED",
|
|
96
|
+
...overrides,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Minimal edge factory. */
|
|
101
|
+
function makeEdge(overrides: Partial<TestEdge> & { from: string; to: string }): TestEdge {
|
|
102
|
+
return {
|
|
103
|
+
type: "CALLS",
|
|
104
|
+
confidence: "INFERRED",
|
|
105
|
+
...overrides,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Group 1: Null returns ────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
describe("inlineGraphSubgraph — null returns", () => {
|
|
112
|
+
it("returns null immediately for empty string term", async () => {
|
|
113
|
+
// No graph.json needed — exits before any file I/O
|
|
114
|
+
const result = await inlineGraphSubgraph("/tmp/nonexistent", "", { budget: 3000 });
|
|
115
|
+
assert.strictEqual(result, null);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("returns null for whitespace-only term", async () => {
|
|
119
|
+
const result = await inlineGraphSubgraph("/tmp/nonexistent", " ", { budget: 3000 });
|
|
120
|
+
assert.strictEqual(result, null);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns null when graphQuery returns zero nodes (no matching term in graph)", async () => {
|
|
124
|
+
const projectDir = makeProjectDir({
|
|
125
|
+
nodes: [makeNode({ id: "n1", label: "AuthService" })],
|
|
126
|
+
edges: [],
|
|
127
|
+
});
|
|
128
|
+
try {
|
|
129
|
+
// "zzznomatch999" is intentionally absent from the fixture
|
|
130
|
+
const result = await inlineGraphSubgraph(projectDir, "zzznomatch999", { budget: 3000 });
|
|
131
|
+
assert.strictEqual(result, null);
|
|
132
|
+
} finally {
|
|
133
|
+
cleanup(projectDir);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns null (no throw) when graph.json is missing", async () => {
|
|
138
|
+
// A project dir with no .gsd directory at all — graphQuery returns zero nodes
|
|
139
|
+
const projectDir = mkdtempSync(join(tmpdir(), "graph-ctx-nofile-"));
|
|
140
|
+
try {
|
|
141
|
+
const result = await inlineGraphSubgraph(projectDir, "auth", { budget: 3000 });
|
|
142
|
+
assert.strictEqual(result, null);
|
|
143
|
+
} finally {
|
|
144
|
+
cleanup(projectDir);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ─── Group 2: Correct output formatting ──────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
describe("inlineGraphSubgraph — correct output", () => {
|
|
152
|
+
it("returns block with section header and node labels when term matches", async () => {
|
|
153
|
+
const projectDir = makeProjectDir({
|
|
154
|
+
nodes: [
|
|
155
|
+
makeNode({ id: "n1", label: "UserService" }),
|
|
156
|
+
makeNode({ id: "n2", label: "UserRepository" }),
|
|
157
|
+
],
|
|
158
|
+
edges: [],
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
const result = await inlineGraphSubgraph(projectDir, "User", { budget: 3000 });
|
|
162
|
+
assert.ok(result !== null, "result should not be null");
|
|
163
|
+
assert.ok(result!.includes("### Knowledge Graph Context"), "should include section header");
|
|
164
|
+
assert.ok(result!.includes("UserService"), "should include first node label");
|
|
165
|
+
assert.ok(result!.includes("UserRepository"), "should include second node label");
|
|
166
|
+
assert.ok(result!.includes("Nodes (2)"), "should show node count");
|
|
167
|
+
} finally {
|
|
168
|
+
cleanup(projectDir);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("does not include Relations section when edges array is empty", async () => {
|
|
173
|
+
const projectDir = makeProjectDir({
|
|
174
|
+
nodes: [makeNode({ id: "n1", label: "AuthController" })],
|
|
175
|
+
edges: [],
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
const result = await inlineGraphSubgraph(projectDir, "Auth", { budget: 3000 });
|
|
179
|
+
assert.ok(result !== null, "result should not be null");
|
|
180
|
+
assert.ok(!result!.includes("Relations"), "should not include Relations section for zero edges");
|
|
181
|
+
assert.ok(!result!.includes("⚠"), "should not include stale warning for fresh graph");
|
|
182
|
+
} finally {
|
|
183
|
+
cleanup(projectDir);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("includes Relations section when edges are present", async () => {
|
|
188
|
+
const projectDir = makeProjectDir({
|
|
189
|
+
nodes: [
|
|
190
|
+
makeNode({ id: "n1", label: "AuthService" }),
|
|
191
|
+
makeNode({ id: "n2", label: "UserRepo" }),
|
|
192
|
+
],
|
|
193
|
+
edges: [makeEdge({ from: "n1", to: "n2", type: "CALLS" })],
|
|
194
|
+
});
|
|
195
|
+
try {
|
|
196
|
+
const result = await inlineGraphSubgraph(projectDir, "Auth", { budget: 3000 });
|
|
197
|
+
assert.ok(result !== null, "result should not be null");
|
|
198
|
+
assert.ok(result!.includes("Relations (1)"), "should show edge count");
|
|
199
|
+
assert.ok(result!.includes("→[CALLS]→"), "should include edge type in arrow notation");
|
|
200
|
+
} finally {
|
|
201
|
+
cleanup(projectDir);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("includes stale annotation when graph was built more than 24h ago", async () => {
|
|
206
|
+
const projectDir = makeProjectDir({
|
|
207
|
+
nodes: [makeNode({ id: "n1", label: "AuthService" })],
|
|
208
|
+
edges: [],
|
|
209
|
+
builtAt: staleTimestamp(26), // 26 hours ago → stale
|
|
210
|
+
});
|
|
211
|
+
try {
|
|
212
|
+
const result = await inlineGraphSubgraph(projectDir, "Auth", { budget: 3000 });
|
|
213
|
+
assert.ok(result !== null, "result should not be null");
|
|
214
|
+
assert.ok(result!.includes("⚠ Graph last built"), "should include stale annotation");
|
|
215
|
+
assert.ok(result!.includes("h ago"), "should include hours-ago text");
|
|
216
|
+
} finally {
|
|
217
|
+
cleanup(projectDir);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("does not include stale annotation for a fresh graph", async () => {
|
|
222
|
+
const projectDir = makeProjectDir({
|
|
223
|
+
nodes: [makeNode({ id: "n1", label: "AuthService" })],
|
|
224
|
+
edges: [],
|
|
225
|
+
builtAt: freshTimestamp(), // 30 minutes ago → not stale
|
|
226
|
+
});
|
|
227
|
+
try {
|
|
228
|
+
const result = await inlineGraphSubgraph(projectDir, "Auth", { budget: 3000 });
|
|
229
|
+
assert.ok(result !== null, "result should not be null");
|
|
230
|
+
assert.ok(!result!.includes("⚠"), "should not include stale annotation for fresh graph");
|
|
231
|
+
} finally {
|
|
232
|
+
cleanup(projectDir);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("returns valid block even when graph.json has corrupted builtAt (graphStatus throws internally)", async () => {
|
|
237
|
+
// Write a graph.json with an invalid builtAt — graphStatus will catch and return {exists: false}
|
|
238
|
+
// inlineGraphSubgraph should still return the node block without stale annotation
|
|
239
|
+
const projectDir = mkdtempSync(join(tmpdir(), "graph-ctx-corrupt-"));
|
|
240
|
+
const gsdDir = join(projectDir, ".gsd");
|
|
241
|
+
const graphsDir = join(gsdDir, "graphs");
|
|
242
|
+
mkdirSync(graphsDir, { recursive: true });
|
|
243
|
+
|
|
244
|
+
const graph = {
|
|
245
|
+
nodes: [{ id: "n1", label: "AuthController", type: "CLASS", confidence: "INFERRED" }],
|
|
246
|
+
edges: [],
|
|
247
|
+
builtAt: "NOT-A-DATE", // invalid ISO — will cause Date.now() - NaN to produce NaN
|
|
248
|
+
};
|
|
249
|
+
writeFileSync(join(graphsDir, "graph.json"), JSON.stringify(graph), "utf-8");
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const result = await inlineGraphSubgraph(projectDir, "Auth", { budget: 3000 });
|
|
253
|
+
// graphQuery reads the file and finds the node; graphStatus may return {exists: true, stale: false/true}
|
|
254
|
+
// Either way, function must not throw and must return a string with node content
|
|
255
|
+
assert.ok(result !== null, "result should not be null");
|
|
256
|
+
assert.ok(result!.includes("AuthController"), "should include node label");
|
|
257
|
+
} finally {
|
|
258
|
+
cleanup(projectDir);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("passes the budget option to graphQuery (enforces node count limit)", async () => {
|
|
263
|
+
// Each node uses ~20 tokens. With budget=20, only ~1 node should be returned.
|
|
264
|
+
// Build a graph with many nodes all matching the same term.
|
|
265
|
+
const nodes: TestNode[] = Array.from({ length: 10 }, (_, i) =>
|
|
266
|
+
makeNode({ id: `n${i}`, label: `AuthModule${i}` })
|
|
267
|
+
);
|
|
268
|
+
const projectDir = makeProjectDir({ nodes, edges: [] });
|
|
269
|
+
try {
|
|
270
|
+
const resultSmall = await inlineGraphSubgraph(projectDir, "Auth", { budget: 20 });
|
|
271
|
+
const resultLarge = await inlineGraphSubgraph(projectDir, "Auth", { budget: 10000 });
|
|
272
|
+
|
|
273
|
+
// Both should return something (at least 1 node matches)
|
|
274
|
+
assert.ok(resultSmall !== null, "small-budget result should not be null");
|
|
275
|
+
assert.ok(resultLarge !== null, "large-budget result should not be null");
|
|
276
|
+
|
|
277
|
+
// With a very small budget (20 tokens ≈ 1 node), fewer nodes should appear
|
|
278
|
+
const smallNodeCount = (resultSmall!.match(/- \*\*/g) || []).length;
|
|
279
|
+
const largeNodeCount = (resultLarge!.match(/- \*\*/g) || []).length;
|
|
280
|
+
assert.ok(
|
|
281
|
+
smallNodeCount <= largeNodeCount,
|
|
282
|
+
`small-budget should return <= nodes than large-budget (got ${smallNodeCount} vs ${largeNodeCount})`,
|
|
283
|
+
);
|
|
284
|
+
} finally {
|
|
285
|
+
cleanup(projectDir);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// ─── Group 3: Node formatting ─────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
describe("inlineGraphSubgraph — node formatting", () => {
|
|
293
|
+
it("includes description after em-dash when node has description", async () => {
|
|
294
|
+
const projectDir = makeProjectDir({
|
|
295
|
+
nodes: [makeNode({ id: "n1", label: "JwtValidator", description: "JWT validation" })],
|
|
296
|
+
edges: [],
|
|
297
|
+
});
|
|
298
|
+
try {
|
|
299
|
+
const result = await inlineGraphSubgraph(projectDir, "Jwt", { budget: 3000 });
|
|
300
|
+
assert.ok(result !== null, "result should not be null");
|
|
301
|
+
assert.ok(result!.includes("— JWT validation"), "should include description after em-dash");
|
|
302
|
+
} finally {
|
|
303
|
+
cleanup(projectDir);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("omits em-dash suffix when node has no description", async () => {
|
|
308
|
+
const projectDir = makeProjectDir({
|
|
309
|
+
nodes: [makeNode({ id: "n1", label: "TokenStore" })], // no description
|
|
310
|
+
edges: [],
|
|
311
|
+
});
|
|
312
|
+
try {
|
|
313
|
+
const result = await inlineGraphSubgraph(projectDir, "Token", { budget: 3000 });
|
|
314
|
+
assert.ok(result !== null, "result should not be null");
|
|
315
|
+
const lines = result!.split("\n");
|
|
316
|
+
const nodeLine = lines.find((l) => l.includes("TokenStore"));
|
|
317
|
+
assert.ok(nodeLine !== undefined, "node line should be present");
|
|
318
|
+
assert.ok(!nodeLine.includes("—"), "node line should not include em-dash when no description");
|
|
319
|
+
} finally {
|
|
320
|
+
cleanup(projectDir);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("includes confidence tier in the node output line", async () => {
|
|
325
|
+
const projectDir = makeProjectDir({
|
|
326
|
+
nodes: [makeNode({ id: "n1", label: "AuthService", confidence: "EXTRACTED" })],
|
|
327
|
+
edges: [],
|
|
328
|
+
});
|
|
329
|
+
try {
|
|
330
|
+
const result = await inlineGraphSubgraph(projectDir, "Auth", { budget: 3000 });
|
|
331
|
+
assert.ok(result !== null, "result should not be null");
|
|
332
|
+
assert.ok(result!.includes("EXTRACTED"), "should include the confidence tier in node line");
|
|
333
|
+
} finally {
|
|
334
|
+
cleanup(projectDir);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
});
|