@bugmole/cli 0.4.0
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/.bugmole.env.example +20 -0
- package/LICENSE +7 -0
- package/README.md +293 -0
- package/TESTING.md +117 -0
- package/bugmole.config.yaml +39 -0
- package/package.json +85 -0
- package/scripts/billing/paypal-setup.mjs +121 -0
- package/scripts/bugmole-continue.ts +318 -0
- package/scripts/bugmole-init.ts +188 -0
- package/scripts/bugmole.cjs +17 -0
- package/scripts/bugmole.test.ts +344 -0
- package/scripts/bugmole.ts +657 -0
- package/scripts/ensure-maestro.cjs +79 -0
- package/scripts/ios-tunnel-keeper.sh +45 -0
- package/scripts/ios-wda-keeper.sh +66 -0
- package/scripts/sync-plan-catalog.d.mts +3 -0
- package/scripts/sync-plan-catalog.mjs +16 -0
- package/scripts/ui-parity-diff.py +65 -0
- package/scripts/ui-parity-requirements.txt +1 -0
- package/scripts/verify-manage-to-plans.mts +194 -0
- package/spec/app-ui-audit.schema.json +176 -0
- package/spec/blockers.yaml +79 -0
- package/spec/bugs.index.json +42 -0
- package/spec/design-dna.schema.json +38 -0
- package/spec/domain_rules.yaml +24 -0
- package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
- package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
- package/spec/journey_graph.yaml +126 -0
- package/spec/journeys.graph.json +2618 -0
- package/spec/plans/dashboard-smoke.flow.yaml +20 -0
- package/spec/plans/example.flow.yaml +99 -0
- package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
- package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
- package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
- package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
- package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
- package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
- package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
- package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
- package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
- package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
- package/spec/plans/login.flow.yaml +22 -0
- package/spec/plans/owner-operations.flow.yaml +20 -0
- package/spec/project_config.yaml +55 -0
- package/spec/roles.yaml +30 -0
- package/spec/schema.md +394 -0
- package/spec/test-case-results.schema.json +62 -0
- package/spec/test-cases.schema.json +85 -0
- package/spec/ui-parity-audit.schema.json +194 -0
- package/spec/ui-reverse-engineering.schema.json +94 -0
- package/src/billing/plan-catalog.test.ts +46 -0
- package/src/billing/plan-catalog.ts +199 -0
- package/src/integrations/aws-sigv4.test.ts +42 -0
- package/src/integrations/aws-sigv4.ts +72 -0
- package/src/integrations/device-farm.ts +155 -0
- package/src/integrations/github-app.test.ts +57 -0
- package/src/integrations/github-app.ts +143 -0
- package/src/integrations/gitlab.ts +81 -0
- package/src/integrations/temp-email.test.ts +123 -0
- package/src/integrations/temp-email.ts +175 -0
- package/src/integrations/testflight-feedback.test.ts +51 -0
- package/src/integrations/testflight-feedback.ts +173 -0
- package/src/integrations/webdriver-client.ts +131 -0
- package/src/mcp/server.test.ts +1220 -0
- package/src/mcp/server.ts +3064 -0
- package/src/mcp/write-test-cases.test.ts +287 -0
- package/src/registry/api-key-client.ts +39 -0
- package/src/registry/control-plane-client.ts +212 -0
- package/src/registry/migrations/0001_registry.sql +47 -0
- package/src/registry/migrations/0002_device_authorizations.sql +23 -0
- package/src/registry/migrations/0003_project_environments.sql +25 -0
- package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
- package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
- package/src/registry/migrations/0006_workspaces.sql +36 -0
- package/src/registry/migrations/0007_project_apps.sql +26 -0
- package/src/registry/migrations/0008_agent_tasks.sql +30 -0
- package/src/registry/migrations/0009_journey_revisions.sql +17 -0
- package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
- package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
- package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
- package/src/registry/migrations/0013_device_sessions.sql +22 -0
- package/src/registry/migrations/0014_agent_task_step.sql +1 -0
- package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
- package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
- package/src/registry/migrations/0017_run_targets.sql +7 -0
- package/src/registry/migrations/0018_run_fix_from.sql +4 -0
- package/src/registry/migrations/0019_workspace_flags.sql +9 -0
- package/src/registry/migrations/0020_orgs.sql +40 -0
- package/src/registry/migrations/0021_billing_core.sql +58 -0
- package/src/registry/migrations/0022_cloud_runners.sql +19 -0
- package/src/registry/migrations/0023_signup.sql +4 -0
- package/src/registry/migrations/0024_billing.sql +67 -0
- package/src/registry/migrations/0025_notifications.sql +47 -0
- package/src/registry/migrations/0026_repo_bindings.sql +28 -0
- package/src/registry/migrations/0027_feedback.sql +29 -0
- package/src/registry/migrations/0028_devices.sql +48 -0
- package/src/registry/migrations/0029_sso.sql +31 -0
- package/src/registry/migrations/0030_workspace_domains.sql +18 -0
- package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
- package/src/registry/migrations/0032_personas.sql +15 -0
- package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
- package/src/registry/task-scheduling.test.ts +100 -0
- package/src/registry/task-scheduling.ts +80 -0
- package/src/registry-worker/ai/platform-model.ts +77 -0
- package/src/registry-worker/ai/routes.test.ts +88 -0
- package/src/registry-worker/ai/routes.ts +90 -0
- package/src/registry-worker/artifacts.test.ts +98 -0
- package/src/registry-worker/artifacts.ts +85 -0
- package/src/registry-worker/billing/billing-core.test.ts +175 -0
- package/src/registry-worker/billing/checkout-routes.ts +209 -0
- package/src/registry-worker/billing/enforcement.ts +69 -0
- package/src/registry-worker/billing/entitlements.ts +108 -0
- package/src/registry-worker/billing/ledger.ts +186 -0
- package/src/registry-worker/billing/paypal/api.ts +259 -0
- package/src/registry-worker/billing/paypal/client.ts +91 -0
- package/src/registry-worker/billing/paypal/provider.ts +143 -0
- package/src/registry-worker/billing/paypal.test.ts +466 -0
- package/src/registry-worker/billing/provider.ts +114 -0
- package/src/registry-worker/billing/routes.ts +66 -0
- package/src/registry-worker/billing/subscriptions.ts +780 -0
- package/src/registry-worker/billing/thresholds.ts +107 -0
- package/src/registry-worker/core.ts +308 -0
- package/src/registry-worker/devices/devices.test.ts +185 -0
- package/src/registry-worker/devices/policy.ts +71 -0
- package/src/registry-worker/devices/routes.ts +453 -0
- package/src/registry-worker/domains/domains.test.ts +210 -0
- package/src/registry-worker/domains/routes.ts +139 -0
- package/src/registry-worker/domains.ts +88 -0
- package/src/registry-worker/email/sender.ts +75 -0
- package/src/registry-worker/env.d.ts +14716 -0
- package/src/registry-worker/features.ts +20 -0
- package/src/registry-worker/feedback/feedback.test.ts +230 -0
- package/src/registry-worker/feedback/format.ts +148 -0
- package/src/registry-worker/feedback/routes.ts +386 -0
- package/src/registry-worker/flags.ts +39 -0
- package/src/registry-worker/github/checks.test.ts +177 -0
- package/src/registry-worker/github/checks.ts +374 -0
- package/src/registry-worker/gitlab/checks.test.ts +141 -0
- package/src/registry-worker/gitlab/checks.ts +349 -0
- package/src/registry-worker/hooks.ts +54 -0
- package/src/registry-worker/index.ts +2077 -0
- package/src/registry-worker/jobs/index.ts +29 -0
- package/src/registry-worker/jobs/retention.ts +68 -0
- package/src/registry-worker/mcp/mcp.test.ts +355 -0
- package/src/registry-worker/mcp/routes.ts +215 -0
- package/src/registry-worker/mcp/token.ts +126 -0
- package/src/registry-worker/mcp/tools.ts +563 -0
- package/src/registry-worker/notifications/alerts.ts +212 -0
- package/src/registry-worker/notifications/notifications.test.ts +298 -0
- package/src/registry-worker/notifications/outbox.ts +83 -0
- package/src/registry-worker/notifications/routes.ts +280 -0
- package/src/registry-worker/notifications/secrets.ts +49 -0
- package/src/registry-worker/notifications/slack.ts +96 -0
- package/src/registry-worker/notifications/teams.ts +46 -0
- package/src/registry-worker/org/audit.ts +116 -0
- package/src/registry-worker/org/routes.test.ts +163 -0
- package/src/registry-worker/org/routes.ts +302 -0
- package/src/registry-worker/personas/personas.test.ts +78 -0
- package/src/registry-worker/personas/routes.ts +100 -0
- package/src/registry-worker/repo-triggers.ts +20 -0
- package/src/registry-worker/routes/index.ts +74 -0
- package/src/registry-worker/run-events.ts +24 -0
- package/src/registry-worker/runner/dispatch.ts +219 -0
- package/src/registry-worker/runner/jobs.ts +43 -0
- package/src/registry-worker/runner/metering.ts +82 -0
- package/src/registry-worker/runner/policy.ts +59 -0
- package/src/registry-worker/runner/routes.ts +171 -0
- package/src/registry-worker/runner/runner.test.ts +358 -0
- package/src/registry-worker/runner/tokens.ts +93 -0
- package/src/registry-worker/runs.test.ts +60 -0
- package/src/registry-worker/signup/policy.ts +57 -0
- package/src/registry-worker/signup/routes.ts +106 -0
- package/src/registry-worker/signup/signup.test.ts +81 -0
- package/src/registry-worker/sso/aegis.ts +141 -0
- package/src/registry-worker/sso/membership.ts +157 -0
- package/src/registry-worker/sso/routes.ts +458 -0
- package/src/registry-worker/sso/sso.test.ts +344 -0
- package/src/registry-worker/testing/d1-shim.ts +180 -0
- package/src/registry-worker/testing/harness.ts +137 -0
- package/src/runner-worker/index.ts +108 -0
- package/src/runtime/ai-analysis.ts +97 -0
- package/src/runtime/ai-exploration.test.ts +32 -0
- package/src/runtime/ai-exploration.ts +69 -0
- package/src/runtime/ai-repair.ts +74 -0
- package/src/runtime/ai-work.test.ts +99 -0
- package/src/runtime/android-screen-record.test.ts +75 -0
- package/src/runtime/android-screen-record.ts +192 -0
- package/src/runtime/app-understanding.test.ts +123 -0
- package/src/runtime/app-understanding.ts +201 -0
- package/src/runtime/appium-driver.test.ts +179 -0
- package/src/runtime/appium-driver.ts +295 -0
- package/src/runtime/blocker-resolution.test.ts +113 -0
- package/src/runtime/blocker-resolution.ts +111 -0
- package/src/runtime/browser-matrix.integration.test.ts +212 -0
- package/src/runtime/browser-matrix.test.ts +143 -0
- package/src/runtime/browser-matrix.ts +200 -0
- package/src/runtime/canonical-flow.test.ts +52 -0
- package/src/runtime/config-validate.ts +185 -0
- package/src/runtime/continuous-execution.ts +291 -0
- package/src/runtime/cursor-applescript.ts +573 -0
- package/src/runtime/cursor-cli-driver.test.ts +78 -0
- package/src/runtime/cursor-cli-driver.ts +156 -0
- package/src/runtime/cursor-driver-example.ts +117 -0
- package/src/runtime/cursor-driver-index.ts +65 -0
- package/src/runtime/cursor-driver-init.ts +277 -0
- package/src/runtime/cursor-driver-run.test.ts +15 -0
- package/src/runtime/cursor-driver-run.ts +323 -0
- package/src/runtime/cursor-driver.ts +332 -0
- package/src/runtime/cursor-llm-example.ts +90 -0
- package/src/runtime/cursor-llm.ts +206 -0
- package/src/runtime/cursor-mcp-monitor.ts +386 -0
- package/src/runtime/device-clouds/browserstack.ts +73 -0
- package/src/runtime/device-clouds/device-farm.ts +52 -0
- package/src/runtime/device-clouds/index.ts +92 -0
- package/src/runtime/device-clouds/kobiton.ts +70 -0
- package/src/runtime/device-clouds/targets.ts +44 -0
- package/src/runtime/device-clouds/types.ts +62 -0
- package/src/runtime/diff-proposal.ts +84 -0
- package/src/runtime/discovery-task.test.ts +29 -0
- package/src/runtime/discovery-task.ts +284 -0
- package/src/runtime/driver-recovery.ts +69 -0
- package/src/runtime/driver.ts +79 -0
- package/src/runtime/environment.test.ts +104 -0
- package/src/runtime/environment.ts +137 -0
- package/src/runtime/executor.test.ts +509 -0
- package/src/runtime/executor.ts +921 -0
- package/src/runtime/explorer.test.ts +101 -0
- package/src/runtime/explorer.ts +1013 -0
- package/src/runtime/failure-analysis.test.ts +111 -0
- package/src/runtime/failure-analysis.ts +272 -0
- package/src/runtime/fixtures/fake-maestro.sh +36 -0
- package/src/runtime/flow-language.test.ts +268 -0
- package/src/runtime/flow-language.ts +414 -0
- package/src/runtime/init-wizard.ts +354 -0
- package/src/runtime/ios-screen-record.test.ts +68 -0
- package/src/runtime/ios-screen-record.ts +155 -0
- package/src/runtime/journey-editor.ts +452 -0
- package/src/runtime/journey-evidence.test.ts +161 -0
- package/src/runtime/journey-evidence.ts +180 -0
- package/src/runtime/journey-graph.test.ts +257 -0
- package/src/runtime/journey-graph.ts +170 -0
- package/src/runtime/legacy-names.ts +32 -0
- package/src/runtime/llm-example.ts +105 -0
- package/src/runtime/llm.ts +527 -0
- package/src/runtime/local-browser.test.ts +45 -0
- package/src/runtime/local-browser.ts +48 -0
- package/src/runtime/local-registry-stub.test.ts +325 -0
- package/src/runtime/local-registry-stub.ts +803 -0
- package/src/runtime/maestro-driver.test.ts +84 -0
- package/src/runtime/maestro-driver.ts +209 -0
- package/src/runtime/mole-voice.ts +21 -0
- package/src/runtime/nav-crawl.test.ts +100 -0
- package/src/runtime/nav-crawl.ts +153 -0
- package/src/runtime/pipeline.test.ts +405 -0
- package/src/runtime/pipeline.ts +833 -0
- package/src/runtime/planner.test.ts +37 -0
- package/src/runtime/planner.ts +274 -0
- package/src/runtime/platform-ai.ts +76 -0
- package/src/runtime/playwright-driver.test.ts +93 -0
- package/src/runtime/playwright-driver.ts +620 -0
- package/src/runtime/project-spec.ts +140 -0
- package/src/runtime/record-run-verdicts.ts +68 -0
- package/src/runtime/reporter.test.ts +56 -0
- package/src/runtime/reporter.ts +158 -0
- package/src/runtime/reset.test.ts +44 -0
- package/src/runtime/reset.ts +61 -0
- package/src/runtime/reviewer.test.ts +73 -0
- package/src/runtime/reviewer.ts +158 -0
- package/src/runtime/run-job.ts +136 -0
- package/src/runtime/run-once.test.ts +207 -0
- package/src/runtime/run-once.ts +168 -0
- package/src/runtime/run.ts +132 -0
- package/src/runtime/screen-recording.ts +34 -0
- package/src/runtime/serve-gateway.test.ts +74 -0
- package/src/runtime/serve-gateway.ts +164 -0
- package/src/runtime/serve-worker.test.ts +23 -0
- package/src/runtime/serve-worker.ts +278 -0
- package/src/runtime/site-discovery.test.ts +168 -0
- package/src/runtime/site-discovery.ts +308 -0
- package/src/runtime/target-runner.ts +144 -0
- package/src/runtime/test-case-verdicts.test.ts +94 -0
- package/src/runtime/test-case-verdicts.ts +120 -0
- package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
- package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
- package/src/runtime/ui-reverse-engineering/types.ts +197 -0
- package/src/runtime/web-suite.test.ts +97 -0
- package/src/runtime/web-suite.ts +176 -0
- package/src/storage/create-object-store.ts +144 -0
- package/src/storage/keys.ts +34 -0
- package/src/storage/local-artifact-server.test.ts +314 -0
- package/src/storage/local-artifact-server.ts +357 -0
- package/src/storage/object-store.test.ts +28 -0
- package/src/storage/object-store.ts +101 -0
- package/src/storage/registry-object-store.ts +88 -0
- package/src/storage/remote-object-store.ts +104 -0
- package/src/storage/storage-directory.test.ts +43 -0
- package/src/storage/storage-directory.ts +24 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,1220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive unit tests for MCP server tools
|
|
3
|
+
*
|
|
4
|
+
* These tests ensure all MCP tools handle errors gracefully and never crash the server.
|
|
5
|
+
* Every tool must be tested for:
|
|
6
|
+
* - Valid inputs
|
|
7
|
+
* - Invalid inputs
|
|
8
|
+
* - Missing files
|
|
9
|
+
* - Path resolution issues
|
|
10
|
+
* - Error handling
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, before, after } from "node:test";
|
|
14
|
+
import { strict as assert } from "node:assert";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
// Import the functions we need to test
|
|
21
|
+
// Note: We'll need to export these from server.ts for testing
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
|
|
24
|
+
// Mock config for testing
|
|
25
|
+
// The "dashboard" journey / "home" node fixture that bugmole_write_ui_audit
|
|
26
|
+
// tests below assert against. Re-seeded immediately before that describe
|
|
27
|
+
// block, not just once in the outer before(): every test in this file
|
|
28
|
+
// shares one testDir, and the bugmole_explore test in between legitimately
|
|
29
|
+
// rebuilds journeys.graph.json's nodes from a scanned source tree ("Rebuild
|
|
30
|
+
// the sitemap from the current source tree on every run" in explorer.ts) --
|
|
31
|
+
// which has always dropped this fixture's "home" node by the time these
|
|
32
|
+
// tests run. That dangling "dashboard" -> "home" reference used to go
|
|
33
|
+
// unnoticed because nothing validated it; readJourneyGraph now repairs it
|
|
34
|
+
// (see AGENTS.md's "Journey graph reads and mutations must tolerate legacy
|
|
35
|
+
// corruption"), correctly pruning it -- which surfaced this pre-existing
|
|
36
|
+
// test-isolation gap instead of a bug in that repair.
|
|
37
|
+
function seedDashboardJourney(testDir: string) {
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(testDir, "spec", "journeys.graph.json"),
|
|
40
|
+
JSON.stringify({
|
|
41
|
+
version: 2,
|
|
42
|
+
projectId: "default",
|
|
43
|
+
journeys: [{
|
|
44
|
+
id: "dashboard",
|
|
45
|
+
name: "Dashboard",
|
|
46
|
+
actor: "user",
|
|
47
|
+
nodeIds: ["home"],
|
|
48
|
+
edgeIds: [],
|
|
49
|
+
revision: 1,
|
|
50
|
+
status: "draft",
|
|
51
|
+
}],
|
|
52
|
+
nodes: [{ id: "home", title: "Home", kind: "screen", route: "/" }],
|
|
53
|
+
edges: [],
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createTestConfig(testDir: string) {
|
|
59
|
+
return {
|
|
60
|
+
name: "Bugmole MCP Test",
|
|
61
|
+
runtime: {
|
|
62
|
+
env: "test",
|
|
63
|
+
tenant: "test",
|
|
64
|
+
artifacts_dir: path.join(testDir, "artifacts"),
|
|
65
|
+
spec_dir: path.join(testDir, "spec"),
|
|
66
|
+
},
|
|
67
|
+
mcp: {
|
|
68
|
+
enabled: true,
|
|
69
|
+
host: "127.0.0.1",
|
|
70
|
+
port: 3187,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("MCP Server Tools", () => {
|
|
76
|
+
let testDir: string;
|
|
77
|
+
let cfg: any;
|
|
78
|
+
|
|
79
|
+
it("formats tool results for MCP clients", async () => {
|
|
80
|
+
const { formatMcpToolResult } = await import("./server.js");
|
|
81
|
+
assert.deepEqual(
|
|
82
|
+
formatMcpToolResult({ status: "success", auditId: "parity-rerun" }),
|
|
83
|
+
{
|
|
84
|
+
content: [{
|
|
85
|
+
type: "text",
|
|
86
|
+
text: '{"status":"success","auditId":"parity-rerun"}',
|
|
87
|
+
}],
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("serves wrapped tool results over HTTP and rejects invalid JSON-RPC requests", async () => {
|
|
93
|
+
const { startMcpServer } = await import("./server.js");
|
|
94
|
+
const httpServer = await startMcpServer({
|
|
95
|
+
...createTestConfig(testDir),
|
|
96
|
+
mcp: { ...createTestConfig(testDir).mcp, port: 0 },
|
|
97
|
+
}, { forceHttp: true });
|
|
98
|
+
assert.ok(httpServer);
|
|
99
|
+
try {
|
|
100
|
+
const toolResponse = await fetch(`${httpServer.origin}/mcp`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "content-type": "application/json" },
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
jsonrpc: "2.0",
|
|
105
|
+
id: "http-tool",
|
|
106
|
+
method: "tools/call",
|
|
107
|
+
params: {
|
|
108
|
+
name: "bugmole_read_spec",
|
|
109
|
+
arguments: { filePath: "journeys.graph.json" },
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
assert.equal(toolResponse.status, 200);
|
|
114
|
+
const toolBody = await toolResponse.json() as {
|
|
115
|
+
id: string;
|
|
116
|
+
result?: { content?: Array<{ type?: string; text?: string }> };
|
|
117
|
+
};
|
|
118
|
+
assert.equal(toolBody.id, "http-tool");
|
|
119
|
+
assert.equal(toolBody.result?.content?.[0]?.type, "text");
|
|
120
|
+
assert.equal(
|
|
121
|
+
(JSON.parse(toolBody.result?.content?.[0]?.text ?? "{}") as { content?: string }).content,
|
|
122
|
+
fs.readFileSync(path.join(testDir, "spec", "journeys.graph.json"), "utf8"),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const invalidResponse = await fetch(`${httpServer.origin}/mcp`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { "content-type": "application/json" },
|
|
128
|
+
body: "null",
|
|
129
|
+
});
|
|
130
|
+
assert.equal(invalidResponse.status, 200);
|
|
131
|
+
assert.deepEqual(await invalidResponse.json(), {
|
|
132
|
+
jsonrpc: "2.0",
|
|
133
|
+
id: null,
|
|
134
|
+
error: { code: -32600, message: "Invalid Request" },
|
|
135
|
+
});
|
|
136
|
+
} finally {
|
|
137
|
+
await httpServer.close();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
before(() => {
|
|
142
|
+
// Create temporary test directory
|
|
143
|
+
testDir = path.join(tmpdir(), `bugmole-test-${Date.now()}`);
|
|
144
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
145
|
+
fs.mkdirSync(path.join(testDir, "spec"), { recursive: true });
|
|
146
|
+
fs.mkdirSync(path.join(testDir, "spec", "plans"), { recursive: true });
|
|
147
|
+
fs.mkdirSync(path.join(testDir, "artifacts"), { recursive: true });
|
|
148
|
+
seedDashboardJourney(testDir);
|
|
149
|
+
|
|
150
|
+
cfg = createTestConfig(testDir);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
after(() => {
|
|
154
|
+
// Cleanup: remove test directory
|
|
155
|
+
if (fs.existsSync(testDir)) {
|
|
156
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("bugmole_read_spec", () => {
|
|
161
|
+
it("should read existing spec file", async () => {
|
|
162
|
+
// Create test file
|
|
163
|
+
const testFile = path.join(testDir, "spec", "test.yaml");
|
|
164
|
+
const testContent = "test: content";
|
|
165
|
+
fs.writeFileSync(testFile, testContent);
|
|
166
|
+
|
|
167
|
+
// Import and test
|
|
168
|
+
const { readSpecFile } = await import("./server.js");
|
|
169
|
+
const result = readSpecFile("test.yaml", cfg);
|
|
170
|
+
|
|
171
|
+
assert.strictEqual(result.content, testContent);
|
|
172
|
+
assert.strictEqual(result.path, "test.yaml");
|
|
173
|
+
assert.strictEqual(result.size, testContent.length);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should handle file not found gracefully", async () => {
|
|
177
|
+
const { readSpecFile } = await import("./server.js");
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
readSpecFile("nonexistent.yaml", cfg);
|
|
181
|
+
assert.fail("Should have thrown error");
|
|
182
|
+
} catch (error: any) {
|
|
183
|
+
// Should throw an error with helpful message
|
|
184
|
+
assert(error instanceof Error);
|
|
185
|
+
assert(error.message);
|
|
186
|
+
assert(
|
|
187
|
+
error.message.includes("Spec file not found") ||
|
|
188
|
+
error.message.includes("nonexistent.yaml") ||
|
|
189
|
+
error.message.includes("Tried"),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should resolve paths with spec/ prefix", async () => {
|
|
195
|
+
const testFile = path.join(testDir, "spec", "plans", "test.yaml");
|
|
196
|
+
const testContent = "plan: test";
|
|
197
|
+
fs.writeFileSync(testFile, testContent);
|
|
198
|
+
|
|
199
|
+
const { readSpecFile } = await import("./server.js");
|
|
200
|
+
const result = readSpecFile("plans/test.yaml", cfg);
|
|
201
|
+
|
|
202
|
+
assert.strictEqual(result.content, testContent);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should handle absolute paths", async () => {
|
|
206
|
+
const testFile = path.join(testDir, "spec", "absolute.yaml");
|
|
207
|
+
const testContent = "absolute: test";
|
|
208
|
+
fs.writeFileSync(testFile, testContent);
|
|
209
|
+
|
|
210
|
+
const { readSpecFile } = await import("./server.js");
|
|
211
|
+
const result = readSpecFile(testFile, cfg);
|
|
212
|
+
|
|
213
|
+
assert.strictEqual(result.content, testContent);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should try multiple path resolution strategies", async () => {
|
|
217
|
+
// Test that it tries multiple paths when file not found
|
|
218
|
+
const { readSpecFile } = await import("./server.js");
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
readSpecFile("missing.yaml", cfg);
|
|
222
|
+
assert.fail("Should have thrown error");
|
|
223
|
+
} catch (error: any) {
|
|
224
|
+
// Error message should show multiple paths were tried
|
|
225
|
+
const errorMsg = error.message;
|
|
226
|
+
assert(errorMsg.includes("Tried"));
|
|
227
|
+
assert(errorMsg.includes("paths"));
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("bugmole_write_spec", () => {
|
|
233
|
+
it("should write spec file successfully", async () => {
|
|
234
|
+
const { writeSpecFile } = await import("./server.js");
|
|
235
|
+
const content = "test: write content";
|
|
236
|
+
|
|
237
|
+
const result = writeSpecFile("write_test.yaml", content, cfg);
|
|
238
|
+
|
|
239
|
+
assert.strictEqual(result.success, true);
|
|
240
|
+
assert.strictEqual(result.path, "write_test.yaml");
|
|
241
|
+
|
|
242
|
+
// Verify file was created
|
|
243
|
+
const filePath = path.join(testDir, "spec", "write_test.yaml");
|
|
244
|
+
assert(fs.existsSync(filePath));
|
|
245
|
+
assert.strictEqual(fs.readFileSync(filePath, "utf-8"), content);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should create directory if it does not exist", async () => {
|
|
249
|
+
const { writeSpecFile } = await import("./server.js");
|
|
250
|
+
const content = "nested: content";
|
|
251
|
+
|
|
252
|
+
const result = writeSpecFile("nested/deep/test.yaml", content, cfg);
|
|
253
|
+
|
|
254
|
+
assert.strictEqual(result.success, true);
|
|
255
|
+
|
|
256
|
+
// Verify nested directory was created
|
|
257
|
+
const filePath = path.join(testDir, "spec", "nested", "deep", "test.yaml");
|
|
258
|
+
assert(fs.existsSync(filePath));
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should handle invalid file paths", async () => {
|
|
262
|
+
const { writeSpecFile } = await import("./server.js");
|
|
263
|
+
|
|
264
|
+
// Should not crash on invalid paths
|
|
265
|
+
try {
|
|
266
|
+
writeSpecFile("", "content", cfg);
|
|
267
|
+
// Empty path might be handled or throw - either is acceptable
|
|
268
|
+
} catch (error: any) {
|
|
269
|
+
// If it throws, error should be clear
|
|
270
|
+
assert(error.message.length > 0);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("bugmole_report_progress", () => {
|
|
276
|
+
it("should create progress report file", async () => {
|
|
277
|
+
// We need to import callToolInternal or the actual function
|
|
278
|
+
// For now, test the directory structure
|
|
279
|
+
const progressDir = path.join(testDir, "artifacts", "progress_reports");
|
|
280
|
+
fs.mkdirSync(progressDir, { recursive: true });
|
|
281
|
+
|
|
282
|
+
const progressId = `progress_${Date.now()}`;
|
|
283
|
+
const progressPath = path.join(progressDir, `${progressId}.json`);
|
|
284
|
+
const progressData = {
|
|
285
|
+
id: progressId,
|
|
286
|
+
timestamp: new Date().toISOString(),
|
|
287
|
+
status: "working",
|
|
288
|
+
message: "Test progress",
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
fs.writeFileSync(progressPath, JSON.stringify(progressData, null, 2));
|
|
292
|
+
|
|
293
|
+
assert(fs.existsSync(progressPath));
|
|
294
|
+
const saved = JSON.parse(fs.readFileSync(progressPath, "utf-8"));
|
|
295
|
+
assert.strictEqual(saved.status, "working");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should handle all status types", async () => {
|
|
299
|
+
const statuses = ["working", "blocked", "completed"];
|
|
300
|
+
|
|
301
|
+
for (const status of statuses) {
|
|
302
|
+
// Test that each status is accepted
|
|
303
|
+
assert(["working", "blocked", "completed"].includes(status));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("bugmole_commit_response", () => {
|
|
309
|
+
it("should save commit response with all fields", async () => {
|
|
310
|
+
const commitDir = path.join(testDir, "artifacts", "cursor_commits");
|
|
311
|
+
fs.mkdirSync(commitDir, { recursive: true });
|
|
312
|
+
|
|
313
|
+
const commitId = `commit_${Date.now()}`;
|
|
314
|
+
const commitPath = path.join(commitDir, `${commitId}.json`);
|
|
315
|
+
const commitData = {
|
|
316
|
+
id: commitId,
|
|
317
|
+
timestamp: new Date().toISOString(),
|
|
318
|
+
payload: {
|
|
319
|
+
status: "ok",
|
|
320
|
+
summary: "Test commit",
|
|
321
|
+
next_steps: ["Step 1", "Step 2"],
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
fs.writeFileSync(commitPath, JSON.stringify(commitData, null, 2));
|
|
326
|
+
|
|
327
|
+
assert(fs.existsSync(commitPath));
|
|
328
|
+
const saved = JSON.parse(fs.readFileSync(commitPath, "utf-8"));
|
|
329
|
+
assert.strictEqual(saved.payload.status, "ok");
|
|
330
|
+
assert(Array.isArray(saved.payload.next_steps));
|
|
331
|
+
assert.strictEqual(saved.payload.next_steps.length, 2);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("should always include next_steps as array", async () => {
|
|
335
|
+
// Test that next_steps is never undefined
|
|
336
|
+
const commitDir = path.join(testDir, "artifacts", "cursor_commits");
|
|
337
|
+
const commitId = `commit_${Date.now()}`;
|
|
338
|
+
const commitPath = path.join(commitDir, `${commitId}.json`);
|
|
339
|
+
|
|
340
|
+
const commitData = {
|
|
341
|
+
id: commitId,
|
|
342
|
+
timestamp: new Date().toISOString(),
|
|
343
|
+
payload: {
|
|
344
|
+
status: "ok",
|
|
345
|
+
summary: "Test",
|
|
346
|
+
next_steps: [], // Should always be array
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
fs.writeFileSync(commitPath, JSON.stringify(commitData, null, 2));
|
|
351
|
+
const saved = JSON.parse(fs.readFileSync(commitPath, "utf-8"));
|
|
352
|
+
|
|
353
|
+
assert("next_steps" in saved.payload);
|
|
354
|
+
assert(Array.isArray(saved.payload.next_steps));
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe("bugmole_explore", () => {
|
|
359
|
+
it("should discover application screens and update the journey graph", async () => {
|
|
360
|
+
const appDir = path.join(testDir, "app");
|
|
361
|
+
fs.mkdirSync(path.join(appDir, "settings"), { recursive: true });
|
|
362
|
+
fs.writeFileSync(
|
|
363
|
+
path.join(appDir, "page.tsx"),
|
|
364
|
+
`export default function Home() { return <a href="/settings">Settings</a>; }`,
|
|
365
|
+
);
|
|
366
|
+
fs.writeFileSync(
|
|
367
|
+
path.join(appDir, "settings", "page.tsx"),
|
|
368
|
+
`export default function Settings() { return <main>Settings</main>; }`,
|
|
369
|
+
);
|
|
370
|
+
const previousProjectDirectory = cfg.cursor_driver?.project_directory;
|
|
371
|
+
cfg.cursor_driver = { project_directory: testDir };
|
|
372
|
+
|
|
373
|
+
// Import the explore function
|
|
374
|
+
const { explore } = await import("../runtime/explorer.js");
|
|
375
|
+
const result = await explore(cfg, {
|
|
376
|
+
baseUrl: "http://localhost:8000",
|
|
377
|
+
maxDepth: 3,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
assert.strictEqual(result.status, "success");
|
|
381
|
+
assert.strictEqual(result.screensDiscovered, 2);
|
|
382
|
+
assert.strictEqual(result.actionsDiscovered, 1);
|
|
383
|
+
assert.strictEqual(result.journeyGraphUpdated, true);
|
|
384
|
+
assert(result.message?.includes("Review the graph"));
|
|
385
|
+
assert(fs.existsSync(path.join(cfg.runtime.spec_dir, "journeys.graph.json")));
|
|
386
|
+
if (previousProjectDirectory === undefined) delete cfg.cursor_driver.project_directory;
|
|
387
|
+
else cfg.cursor_driver.project_directory = previousProjectDirectory;
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("should not crash on invalid baseUrl", async () => {
|
|
391
|
+
const { explore } = await import("../runtime/explorer.js");
|
|
392
|
+
|
|
393
|
+
// Should handle invalid URLs gracefully
|
|
394
|
+
const result = await explore(cfg, {
|
|
395
|
+
baseUrl: "invalid-url",
|
|
396
|
+
maxDepth: 1,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Should return error, not crash
|
|
400
|
+
assert(result.status === "error" || result.status === "success");
|
|
401
|
+
assert(result.error || result.message);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe("bugmole_plan", () => {
|
|
406
|
+
it("should handle missing journey gracefully", async () => {
|
|
407
|
+
const { plan } = await import("../runtime/planner.js");
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
await plan(cfg, {
|
|
411
|
+
journeyId: "nonexistent_journey",
|
|
412
|
+
actor: "staff",
|
|
413
|
+
});
|
|
414
|
+
// Might succeed or fail, but should not crash
|
|
415
|
+
} catch (error: any) {
|
|
416
|
+
// If it throws, should be a clear error
|
|
417
|
+
assert(error.message.length > 0);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe("bugmole_run", () => {
|
|
423
|
+
it("should handle missing plan gracefully", async () => {
|
|
424
|
+
const { execute } = await import("../runtime/executor.js");
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
await execute(cfg, {
|
|
428
|
+
planId: "nonexistent_plan",
|
|
429
|
+
journeyId: "test",
|
|
430
|
+
});
|
|
431
|
+
// Might succeed or fail, but should not crash
|
|
432
|
+
} catch (error: any) {
|
|
433
|
+
// If it throws, should be a clear error
|
|
434
|
+
assert(error.message.length > 0);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe("bugmole_propose_diff", () => {
|
|
440
|
+
it("should handle missing file gracefully", async () => {
|
|
441
|
+
const { proposeDiff } = await import("../runtime/diff-proposal.js");
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
await proposeDiff(cfg, {
|
|
445
|
+
filePath: "nonexistent.yaml",
|
|
446
|
+
evidence: { test: "data" },
|
|
447
|
+
failures: [],
|
|
448
|
+
});
|
|
449
|
+
// Might succeed or fail, but should not crash
|
|
450
|
+
} catch (error: any) {
|
|
451
|
+
// If it throws, should be a clear error
|
|
452
|
+
assert(error.message.length > 0);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("should handle invalid evidence gracefully", async () => {
|
|
457
|
+
const { proposeDiff } = await import("../runtime/diff-proposal.js");
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
await proposeDiff(cfg, {
|
|
461
|
+
filePath: "test.yaml",
|
|
462
|
+
evidence: null as any,
|
|
463
|
+
failures: [],
|
|
464
|
+
});
|
|
465
|
+
// Might succeed or fail, but should not crash
|
|
466
|
+
} catch (error: any) {
|
|
467
|
+
// If it throws, should be a clear error
|
|
468
|
+
assert(error.message.length > 0);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe("bugmole_review", () => {
|
|
474
|
+
it("should handle missing file gracefully", async () => {
|
|
475
|
+
const { review } = await import("../runtime/reviewer.js");
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
await review(cfg, {
|
|
479
|
+
filePath: "nonexistent.yaml",
|
|
480
|
+
changeSummary: "Test change",
|
|
481
|
+
diff: "test diff",
|
|
482
|
+
evidence: { test: "data" },
|
|
483
|
+
});
|
|
484
|
+
// Might succeed or fail, but should not crash
|
|
485
|
+
} catch (error: any) {
|
|
486
|
+
// If it throws, should be a clear error
|
|
487
|
+
assert(error.message.length > 0);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("should handle missing arguments gracefully", async () => {
|
|
492
|
+
const { review } = await import("../runtime/reviewer.js");
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
await review(cfg, {
|
|
496
|
+
filePath: "test.yaml",
|
|
497
|
+
changeSummary: "",
|
|
498
|
+
diff: "",
|
|
499
|
+
evidence: {},
|
|
500
|
+
});
|
|
501
|
+
// Might succeed or fail, but should not crash
|
|
502
|
+
} catch (error: any) {
|
|
503
|
+
// If it throws, should be a clear error
|
|
504
|
+
assert(error.message.length > 0);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe("bugmole_report", () => {
|
|
510
|
+
it("should handle missing runId gracefully", async () => {
|
|
511
|
+
const { generateReport } = await import("../runtime/reporter.js");
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
await generateReport(cfg, {
|
|
515
|
+
runId: "nonexistent_run",
|
|
516
|
+
format: "json",
|
|
517
|
+
});
|
|
518
|
+
// Might succeed or fail, but should not crash
|
|
519
|
+
} catch (error: any) {
|
|
520
|
+
// If it throws, should be a clear error
|
|
521
|
+
assert(error.message.length > 0);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("should handle different formats gracefully", async () => {
|
|
526
|
+
const { generateReport } = await import("../runtime/reporter.js");
|
|
527
|
+
|
|
528
|
+
const formats = ["json", "markdown"];
|
|
529
|
+
for (const format of formats) {
|
|
530
|
+
try {
|
|
531
|
+
await generateReport(cfg, {
|
|
532
|
+
runId: "test_run",
|
|
533
|
+
format: format as "json" | "markdown",
|
|
534
|
+
});
|
|
535
|
+
// Might succeed or fail, but should not crash
|
|
536
|
+
} catch (error: any) {
|
|
537
|
+
// If it throws, should be a clear error
|
|
538
|
+
assert(error.message.length > 0);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe("bugmole_write_ui_audit", () => {
|
|
545
|
+
before(() => seedDashboardJourney(testDir));
|
|
546
|
+
|
|
547
|
+
it("writes a dashboard-consumable JSON audit with stable totals", async () => {
|
|
548
|
+
const { callTool } = await import("./server.js");
|
|
549
|
+
// Runtime evidence must reference a real artifact; write one before
|
|
550
|
+
// the audit claims it, exercising the same existence check a bogus
|
|
551
|
+
// location would fail.
|
|
552
|
+
const evidenceKey = "projects/default/ui-audits-evidence/open-project.png";
|
|
553
|
+
const evidencePath = path.join(testDir, "artifacts", evidenceKey);
|
|
554
|
+
fs.mkdirSync(path.dirname(evidencePath), { recursive: true });
|
|
555
|
+
fs.writeFileSync(evidencePath, Buffer.from([0]));
|
|
556
|
+
const audit = {
|
|
557
|
+
schemaVersion: 1,
|
|
558
|
+
auditId: "dashboard-home",
|
|
559
|
+
requestedFlow: "Open the dashboard",
|
|
560
|
+
journeyId: "dashboard",
|
|
561
|
+
journeyRevision: 1,
|
|
562
|
+
flowStatus: "exhausted",
|
|
563
|
+
exploredNodeIds: ["home"],
|
|
564
|
+
exploredEdgeIds: [],
|
|
565
|
+
summary: {
|
|
566
|
+
total: 1,
|
|
567
|
+
implemented: 0,
|
|
568
|
+
partiallyImplemented: 1,
|
|
569
|
+
notImplemented: 0,
|
|
570
|
+
},
|
|
571
|
+
screens: [
|
|
572
|
+
{
|
|
573
|
+
screenId: "home",
|
|
574
|
+
route: "/",
|
|
575
|
+
controls: [
|
|
576
|
+
{
|
|
577
|
+
controlId: "open-project",
|
|
578
|
+
label: "Open project",
|
|
579
|
+
kind: "button",
|
|
580
|
+
status: "Partially implemented",
|
|
581
|
+
destinationOrHandler: "openProject()",
|
|
582
|
+
evidence: [{ type: "runtime", description: "Button responds to click", location: evidenceKey }],
|
|
583
|
+
missingBehavior: ["Project selection is not persisted"],
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
highestPriorityMissingImplementations: [],
|
|
589
|
+
blockedVerification: [],
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const result = await callTool({ name: "bugmole_write_ui_audit", arguments: { audit } }, cfg);
|
|
593
|
+
assert.equal(result.status, "success");
|
|
594
|
+
assert.equal(result.schema, "bugmole://spec/app-ui-audit.schema.json");
|
|
595
|
+
const filePath = path.join(
|
|
596
|
+
testDir,
|
|
597
|
+
"artifacts",
|
|
598
|
+
"projects",
|
|
599
|
+
"default",
|
|
600
|
+
"ui-audits",
|
|
601
|
+
"dashboard-home.json",
|
|
602
|
+
);
|
|
603
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(filePath, "utf8")), {
|
|
604
|
+
...audit,
|
|
605
|
+
projectId: "default",
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("rejects runtime evidence without a location", async () => {
|
|
610
|
+
const { callTool } = await import("./server.js");
|
|
611
|
+
await assert.rejects(
|
|
612
|
+
callTool(
|
|
613
|
+
{
|
|
614
|
+
name: "bugmole_write_ui_audit",
|
|
615
|
+
arguments: {
|
|
616
|
+
audit: {
|
|
617
|
+
schemaVersion: 1,
|
|
618
|
+
auditId: "missing-location",
|
|
619
|
+
journeyId: "dashboard",
|
|
620
|
+
journeyRevision: 1,
|
|
621
|
+
flowStatus: "exhausted",
|
|
622
|
+
exploredNodeIds: ["home"],
|
|
623
|
+
exploredEdgeIds: [],
|
|
624
|
+
summary: { total: 1, implemented: 1, partiallyImplemented: 0, notImplemented: 0 },
|
|
625
|
+
screens: [
|
|
626
|
+
{
|
|
627
|
+
screenId: "home",
|
|
628
|
+
controls: [
|
|
629
|
+
{
|
|
630
|
+
controlId: "open-project",
|
|
631
|
+
label: "Open project",
|
|
632
|
+
kind: "button",
|
|
633
|
+
status: "Implemented",
|
|
634
|
+
destinationOrHandler: "openProject()",
|
|
635
|
+
evidence: [{ type: "runtime", description: "Trust me, it works" }],
|
|
636
|
+
missingBehavior: [],
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
},
|
|
640
|
+
],
|
|
641
|
+
highestPriorityMissingImplementations: [],
|
|
642
|
+
blockedVerification: [],
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
cfg,
|
|
647
|
+
),
|
|
648
|
+
/runtime evidence without a location/,
|
|
649
|
+
);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("rejects runtime evidence whose referenced artifact does not exist", async () => {
|
|
653
|
+
const { callTool } = await import("./server.js");
|
|
654
|
+
await assert.rejects(
|
|
655
|
+
callTool(
|
|
656
|
+
{
|
|
657
|
+
name: "bugmole_write_ui_audit",
|
|
658
|
+
arguments: {
|
|
659
|
+
audit: {
|
|
660
|
+
schemaVersion: 1,
|
|
661
|
+
auditId: "phantom-evidence",
|
|
662
|
+
journeyId: "dashboard",
|
|
663
|
+
journeyRevision: 1,
|
|
664
|
+
flowStatus: "exhausted",
|
|
665
|
+
exploredNodeIds: ["home"],
|
|
666
|
+
exploredEdgeIds: [],
|
|
667
|
+
summary: { total: 1, implemented: 1, partiallyImplemented: 0, notImplemented: 0 },
|
|
668
|
+
screens: [
|
|
669
|
+
{
|
|
670
|
+
screenId: "home",
|
|
671
|
+
controls: [
|
|
672
|
+
{
|
|
673
|
+
controlId: "open-project",
|
|
674
|
+
label: "Open project",
|
|
675
|
+
kind: "button",
|
|
676
|
+
status: "Implemented",
|
|
677
|
+
destinationOrHandler: "openProject()",
|
|
678
|
+
evidence: [
|
|
679
|
+
{
|
|
680
|
+
type: "runtime",
|
|
681
|
+
description: "Captured a screenshot",
|
|
682
|
+
location: "projects/default/ui-audits-evidence/does-not-exist.png",
|
|
683
|
+
},
|
|
684
|
+
],
|
|
685
|
+
missingBehavior: [],
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
},
|
|
689
|
+
],
|
|
690
|
+
highestPriorityMissingImplementations: [],
|
|
691
|
+
blockedVerification: [],
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
cfg,
|
|
696
|
+
),
|
|
697
|
+
/no artifact exists at that key/,
|
|
698
|
+
);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it("rejects inconsistent status totals", async () => {
|
|
702
|
+
const { callTool } = await import("./server.js");
|
|
703
|
+
await assert.rejects(
|
|
704
|
+
callTool(
|
|
705
|
+
{
|
|
706
|
+
name: "bugmole_write_ui_audit",
|
|
707
|
+
arguments: {
|
|
708
|
+
audit: {
|
|
709
|
+
schemaVersion: 1,
|
|
710
|
+
auditId: "invalid",
|
|
711
|
+
journeyId: "dashboard",
|
|
712
|
+
journeyRevision: 1,
|
|
713
|
+
flowStatus: "exhausted",
|
|
714
|
+
exploredNodeIds: ["home"],
|
|
715
|
+
exploredEdgeIds: [],
|
|
716
|
+
summary: {
|
|
717
|
+
total: 0,
|
|
718
|
+
implemented: 0,
|
|
719
|
+
partiallyImplemented: 0,
|
|
720
|
+
notImplemented: 0,
|
|
721
|
+
},
|
|
722
|
+
screens: [
|
|
723
|
+
{
|
|
724
|
+
screenId: "home",
|
|
725
|
+
controls: [
|
|
726
|
+
{
|
|
727
|
+
controlId: "button",
|
|
728
|
+
label: "Button",
|
|
729
|
+
status: "Implemented",
|
|
730
|
+
destinationOrHandler: "buttonHandler",
|
|
731
|
+
evidence: [],
|
|
732
|
+
missingBehavior: [],
|
|
733
|
+
},
|
|
734
|
+
],
|
|
735
|
+
},
|
|
736
|
+
],
|
|
737
|
+
blockedVerification: [],
|
|
738
|
+
highestPriorityMissingImplementations: [],
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
cfg,
|
|
743
|
+
),
|
|
744
|
+
/summary must match control status totals/,
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
describe("bugmole_write_ui_parity_audit", () => {
|
|
750
|
+
it("writes a scored reference/current parity artifact", async () => {
|
|
751
|
+
const { callTool } = await import("./server.js");
|
|
752
|
+
const audit = {
|
|
753
|
+
schemaVersion: 1,
|
|
754
|
+
auditId: "home-parity",
|
|
755
|
+
reference: { kind: "url", location: "https://reference.example/home" },
|
|
756
|
+
current: { kind: "url", location: "http://localhost:3020" },
|
|
757
|
+
summary: {
|
|
758
|
+
screensTotal: 1,
|
|
759
|
+
actionsTotal: 1,
|
|
760
|
+
visualDifferencePercent: 8,
|
|
761
|
+
interactionParityScore: 100,
|
|
762
|
+
apiParityScore: 88,
|
|
763
|
+
authenticationParityScore: 100,
|
|
764
|
+
fixedIssues: 2,
|
|
765
|
+
unresolvedIssues: 1,
|
|
766
|
+
},
|
|
767
|
+
flowMatrix: [
|
|
768
|
+
{
|
|
769
|
+
flowId: "home",
|
|
770
|
+
referenceScreen: "Home",
|
|
771
|
+
currentRoute: "/",
|
|
772
|
+
authState: "guest",
|
|
773
|
+
actions: ["Open project"],
|
|
774
|
+
status: "matched",
|
|
775
|
+
},
|
|
776
|
+
],
|
|
777
|
+
comparisons: [
|
|
778
|
+
{
|
|
779
|
+
screenId: "home",
|
|
780
|
+
differencePercent: 8,
|
|
781
|
+
changedPixels: 10,
|
|
782
|
+
totalPixels: 100,
|
|
783
|
+
heatmap: "parity-output/heatmap.png",
|
|
784
|
+
mismatches: ["Card spacing differs by 4px"],
|
|
785
|
+
},
|
|
786
|
+
],
|
|
787
|
+
issues: [
|
|
788
|
+
{
|
|
789
|
+
issueId: "home-card-spacing",
|
|
790
|
+
category: "visual",
|
|
791
|
+
status: "unresolved",
|
|
792
|
+
description: "Card spacing differs by 4px",
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
fixPrompt: "Adjust home card spacing to match the reference.",
|
|
796
|
+
blockedVerification: [],
|
|
797
|
+
};
|
|
798
|
+
const result = await callTool(
|
|
799
|
+
{ name: "bugmole_write_ui_parity_audit", arguments: { audit } },
|
|
800
|
+
cfg,
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
assert.equal(result.status, "success");
|
|
804
|
+
assert.equal(result.schema, "bugmole://spec/ui-parity-audit.schema.json");
|
|
805
|
+
assert.equal(
|
|
806
|
+
JSON.parse(
|
|
807
|
+
fs.readFileSync(
|
|
808
|
+
path.join(testDir, "artifacts/projects/default/ui-parity-audits/home-parity.json"),
|
|
809
|
+
"utf8",
|
|
810
|
+
),
|
|
811
|
+
).summary.visualDifferencePercent,
|
|
812
|
+
8,
|
|
813
|
+
);
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
describe("Error Handling", () => {
|
|
818
|
+
it("should wrap all tool calls in error handler", async () => {
|
|
819
|
+
// Test that callToolInternal is wrapped
|
|
820
|
+
// This ensures no tool can crash the server
|
|
821
|
+
const { callTool } = await import("./server.js");
|
|
822
|
+
|
|
823
|
+
// Call with invalid tool name
|
|
824
|
+
try {
|
|
825
|
+
await callTool(
|
|
826
|
+
{
|
|
827
|
+
name: "nonexistent_tool",
|
|
828
|
+
arguments: {},
|
|
829
|
+
},
|
|
830
|
+
cfg,
|
|
831
|
+
);
|
|
832
|
+
// Should throw error, not crash
|
|
833
|
+
} catch (error: any) {
|
|
834
|
+
assert(error.message.includes("Unknown tool") || error.message.length > 0);
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it("should return JSON-RPC error format", async () => {
|
|
839
|
+
// Errors should be in JSON-RPC format
|
|
840
|
+
const { callTool } = await import("./server.js");
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
await callTool(
|
|
844
|
+
{
|
|
845
|
+
name: "bugmole_read_spec",
|
|
846
|
+
arguments: { filePath: "nonexistent.yaml" },
|
|
847
|
+
},
|
|
848
|
+
cfg,
|
|
849
|
+
);
|
|
850
|
+
assert.fail("Should have thrown error");
|
|
851
|
+
} catch (error: any) {
|
|
852
|
+
// Error should have message
|
|
853
|
+
assert(error.message);
|
|
854
|
+
// Should not be a crash/exception that kills the server
|
|
855
|
+
assert(typeof error.message === "string");
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
describe("Resources", () => {
|
|
861
|
+
it("exposes first-class journey editing and continuation tools", async () => {
|
|
862
|
+
const { getTools } = await import("./server.js");
|
|
863
|
+
const names = getTools(cfg).map((tool) => tool.name);
|
|
864
|
+
for (const name of [
|
|
865
|
+
"bugmole_journey_list",
|
|
866
|
+
"bugmole_journey_get",
|
|
867
|
+
"bugmole_journey_create",
|
|
868
|
+
"bugmole_journey_add_step",
|
|
869
|
+
"bugmole_journey_add_transition",
|
|
870
|
+
"bugmole_journey_update",
|
|
871
|
+
"bugmole_journey_remove",
|
|
872
|
+
"bugmole_journey_next",
|
|
873
|
+
"bugmole_resolve_blocker",
|
|
874
|
+
]) {
|
|
875
|
+
assert(names.includes(name), `missing MCP tool ${name}`);
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it("should list available resources", async () => {
|
|
880
|
+
const { getResources } = await import("./server.js");
|
|
881
|
+
const resources = getResources(cfg);
|
|
882
|
+
|
|
883
|
+
assert(Array.isArray(resources));
|
|
884
|
+
assert(resources.length > 0);
|
|
885
|
+
// Should have standard resources
|
|
886
|
+
const resourceUris = resources.map((r) => r.uri);
|
|
887
|
+
assert(resourceUris.some((uri) => uri.includes("roles.yaml")));
|
|
888
|
+
assert(resourceUris.some((uri) => uri.includes("domain_rules.yaml")));
|
|
889
|
+
assert(resourceUris.includes("bugmole://guidance/app-ui-audit"));
|
|
890
|
+
assert(resourceUris.includes("bugmole://guidance/ui-parity-audit"));
|
|
891
|
+
assert(resourceUris.includes("bugmole://guidance/ui-reverse-engineering"));
|
|
892
|
+
assert(resourceUris.includes("bugmole://spec/ui-reverse-engineering.schema.json"));
|
|
893
|
+
assert(resourceUris.includes("bugmole://spec/design-dna.schema.json"));
|
|
894
|
+
assert(resourceUris.includes("bugmole://spec/ui-parity-audit.schema.json"));
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it("should read the application UI audit guidance", async () => {
|
|
898
|
+
const { readResource } = await import("./server.js");
|
|
899
|
+
const result = await readResource({ uri: "bugmole://guidance/app-ui-audit" }, cfg);
|
|
900
|
+
const text = result.contents[0].text;
|
|
901
|
+
|
|
902
|
+
assert.match(text, /reproduce the requested user flow/);
|
|
903
|
+
assert.match(text, /screen-by-screen grouping/);
|
|
904
|
+
assert.match(text, /Implemented: the intended action works end-to-end/);
|
|
905
|
+
// The audit is the only task that populates the component library, so
|
|
906
|
+
// the component scan has to be part of its instructions.
|
|
907
|
+
assert.match(text, /bugmole_ui_re_start/);
|
|
908
|
+
assert.match(text, /stage `atoms`/);
|
|
909
|
+
assert.match(text, /do not invent components/i);
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
it("should read UI parity guidance without control-audit overlap", async () => {
|
|
913
|
+
const { readResource } = await import("./server.js");
|
|
914
|
+
const result = await readResource({ uri: "bugmole://guidance/ui-parity-audit" }, cfg);
|
|
915
|
+
const text = result.contents[0].text;
|
|
916
|
+
|
|
917
|
+
assert.match(text, /reference experience/);
|
|
918
|
+
assert.match(text, /flow matrix/);
|
|
919
|
+
assert.match(text, /bugmole_write_ui_parity_audit/);
|
|
920
|
+
assert.match(text, /does not replace the application UI audit/);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it("should read existing resource", async () => {
|
|
924
|
+
// Create a test resource file
|
|
925
|
+
const testFile = path.join(testDir, "spec", "roles.yaml");
|
|
926
|
+
const testContent = "roles: test";
|
|
927
|
+
fs.writeFileSync(testFile, testContent);
|
|
928
|
+
|
|
929
|
+
const { readResource } = await import("./server.js");
|
|
930
|
+
const result = await readResource({ uri: "bugmole://spec/roles.yaml" }, cfg);
|
|
931
|
+
|
|
932
|
+
assert(result.contents);
|
|
933
|
+
assert(Array.isArray(result.contents));
|
|
934
|
+
assert.strictEqual(result.contents[0].text, testContent);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it("should handle missing resource gracefully", async () => {
|
|
938
|
+
const { readResource } = await import("./server.js");
|
|
939
|
+
|
|
940
|
+
try {
|
|
941
|
+
await readResource({ uri: "bugmole://spec/nonexistent.yaml" }, cfg);
|
|
942
|
+
assert.fail("Should have thrown error");
|
|
943
|
+
} catch (error: any) {
|
|
944
|
+
assert(error.message.includes("not found") || error.message.includes("Resource"));
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it("should handle invalid URI format", async () => {
|
|
949
|
+
const { readResource } = await import("./server.js");
|
|
950
|
+
|
|
951
|
+
try {
|
|
952
|
+
await readResource({ uri: "invalid://uri" }, cfg);
|
|
953
|
+
assert.fail("Should have thrown error");
|
|
954
|
+
} catch (error: any) {
|
|
955
|
+
assert(error.message.includes("Unknown resource URI"));
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
describe("Prompts", () => {
|
|
961
|
+
it("should list available prompts", async () => {
|
|
962
|
+
const { getPrompts } = await import("./server.js");
|
|
963
|
+
const prompts = getPrompts(cfg);
|
|
964
|
+
|
|
965
|
+
assert(Array.isArray(prompts));
|
|
966
|
+
// Should have at least one prompt
|
|
967
|
+
assert(prompts.length >= 0);
|
|
968
|
+
assert(prompts.some((prompt) => prompt.name === "app_ui_audit"));
|
|
969
|
+
assert(prompts.some((prompt) => prompt.name === "ui_parity_audit"));
|
|
970
|
+
assert(prompts.some((prompt) => prompt.name === "ui_reverse_engineering"));
|
|
971
|
+
assert(prompts.some((prompt) => prompt.name === "agent_task_execution"));
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it("should return the application UI audit prompt", async () => {
|
|
975
|
+
const { getPrompt } = await import("./server.js");
|
|
976
|
+
const result = await getPrompt({ name: "app_ui_audit" }, cfg);
|
|
977
|
+
const text = result.messages[0].content.text;
|
|
978
|
+
|
|
979
|
+
assert.match(text, /every user-facing button, link, row, card/);
|
|
980
|
+
assert.match(text, /runtime or source-code evidence/);
|
|
981
|
+
assert.match(text, /highest-priority missing implementations/);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it("should return the UI parity audit prompt", async () => {
|
|
985
|
+
const { getPrompt } = await import("./server.js");
|
|
986
|
+
const result = await getPrompt({ name: "ui_parity_audit" }, cfg);
|
|
987
|
+
const text = result.messages[0].content.text;
|
|
988
|
+
|
|
989
|
+
assert.match(text, /flow matrix/);
|
|
990
|
+
assert.match(text, /visual, behavioral, backend, and authentication parity/);
|
|
991
|
+
assert.match(text, /bugmole_write_ui_parity_audit/);
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
it("should expose the agent task queue guidance", async () => {
|
|
995
|
+
const { readResource } = await import("./server.js");
|
|
996
|
+
const result = await readResource({ uri: "bugmole://guidance/agent-task-queue" }, cfg);
|
|
997
|
+
const text = result.contents[0].text;
|
|
998
|
+
assert.match(text, /bugmole_task_next/);
|
|
999
|
+
assert.match(text, /fresh parity artifact/);
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
it("should return the agent task execution prompt", async () => {
|
|
1003
|
+
const { getPrompt } = await import("./server.js");
|
|
1004
|
+
const result = await getPrompt({ name: "agent_task_execution" }, cfg);
|
|
1005
|
+
const text = result.messages[0].content.text;
|
|
1006
|
+
assert.match(text, /bugmole_task_next/);
|
|
1007
|
+
assert.match(text, /fresh parity audit id and match score/);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it("should get prompt with arguments", async () => {
|
|
1011
|
+
const { getPrompt } = await import("./server.js");
|
|
1012
|
+
|
|
1013
|
+
try {
|
|
1014
|
+
await getPrompt(
|
|
1015
|
+
{
|
|
1016
|
+
name: "file_authoring",
|
|
1017
|
+
arguments: {
|
|
1018
|
+
filePath: "test.yaml",
|
|
1019
|
+
filePurpose: "Test file",
|
|
1020
|
+
},
|
|
1021
|
+
},
|
|
1022
|
+
cfg,
|
|
1023
|
+
);
|
|
1024
|
+
// Might succeed or fail depending on template existence
|
|
1025
|
+
} catch (error: any) {
|
|
1026
|
+
// If it throws, should be a clear error
|
|
1027
|
+
assert(error.message.length > 0);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
it("should handle unknown prompt gracefully", async () => {
|
|
1032
|
+
const { getPrompt } = await import("./server.js");
|
|
1033
|
+
|
|
1034
|
+
try {
|
|
1035
|
+
await getPrompt({ name: "nonexistent_prompt" }, cfg);
|
|
1036
|
+
assert.fail("Should have thrown error");
|
|
1037
|
+
} catch (error: any) {
|
|
1038
|
+
assert(error.message.includes("Unknown prompt"));
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
describe("bugmole_commit_response edge cases", () => {
|
|
1044
|
+
it("should extract next_steps from answer_markdown when missing", async () => {
|
|
1045
|
+
// This tests the fallback extraction logic
|
|
1046
|
+
const { callTool } = await import("./server.js");
|
|
1047
|
+
const commitDir = path.join(testDir, "artifacts", "cursor_commits");
|
|
1048
|
+
fs.mkdirSync(commitDir, { recursive: true });
|
|
1049
|
+
|
|
1050
|
+
// Call with answer_markdown containing "Next Steps" but no next_steps field
|
|
1051
|
+
const result = await callTool(
|
|
1052
|
+
{
|
|
1053
|
+
name: "bugmole_commit_response",
|
|
1054
|
+
arguments: {
|
|
1055
|
+
status: "ok",
|
|
1056
|
+
summary: "Test commit",
|
|
1057
|
+
answer_markdown: "## Next Steps\n\n1. Do something\n2. Do something else",
|
|
1058
|
+
// Intentionally missing next_steps
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
cfg,
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
assert.strictEqual(result.success, true);
|
|
1065
|
+
|
|
1066
|
+
// Verify next_steps was extracted and saved
|
|
1067
|
+
const commitFiles = fs.readdirSync(commitDir).filter((f) => f.endsWith(".json"));
|
|
1068
|
+
assert(commitFiles.length > 0);
|
|
1069
|
+
|
|
1070
|
+
const latestCommit = JSON.parse(
|
|
1071
|
+
fs.readFileSync(path.join(commitDir, commitFiles[0]), "utf-8"),
|
|
1072
|
+
);
|
|
1073
|
+
assert("next_steps" in latestCommit.payload);
|
|
1074
|
+
assert(Array.isArray(latestCommit.payload.next_steps));
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it("should normalize next_steps from string to array", async () => {
|
|
1078
|
+
const { callTool } = await import("./server.js");
|
|
1079
|
+
const commitDir = path.join(testDir, "artifacts", "cursor_commits");
|
|
1080
|
+
fs.mkdirSync(commitDir, { recursive: true });
|
|
1081
|
+
|
|
1082
|
+
// Call with next_steps as string (should be normalized to array)
|
|
1083
|
+
const result = await callTool(
|
|
1084
|
+
{
|
|
1085
|
+
name: "bugmole_commit_response",
|
|
1086
|
+
arguments: {
|
|
1087
|
+
status: "ok",
|
|
1088
|
+
summary: "Test",
|
|
1089
|
+
next_steps: "Single step" as any, // Wrong type, should be normalized
|
|
1090
|
+
},
|
|
1091
|
+
},
|
|
1092
|
+
cfg,
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
assert.strictEqual(result.success, true);
|
|
1096
|
+
|
|
1097
|
+
const commitFiles = fs.readdirSync(commitDir).filter((f) => f.endsWith(".json"));
|
|
1098
|
+
const latestCommit = JSON.parse(
|
|
1099
|
+
fs.readFileSync(path.join(commitDir, commitFiles[commitFiles.length - 1]), "utf-8"),
|
|
1100
|
+
);
|
|
1101
|
+
assert(Array.isArray(latestCommit.payload.next_steps));
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("should handle null/undefined next_steps", async () => {
|
|
1105
|
+
const { callTool } = await import("./server.js");
|
|
1106
|
+
const commitDir = path.join(testDir, "artifacts", "cursor_commits");
|
|
1107
|
+
fs.mkdirSync(commitDir, { recursive: true });
|
|
1108
|
+
|
|
1109
|
+
// Call with null next_steps (should become empty array)
|
|
1110
|
+
const result = await callTool(
|
|
1111
|
+
{
|
|
1112
|
+
name: "bugmole_commit_response",
|
|
1113
|
+
arguments: {
|
|
1114
|
+
status: "ok",
|
|
1115
|
+
summary: "Test",
|
|
1116
|
+
next_steps: null as any,
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
cfg,
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
assert.strictEqual(result.success, true);
|
|
1123
|
+
|
|
1124
|
+
const commitFiles = fs.readdirSync(commitDir).filter((f) => f.endsWith(".json"));
|
|
1125
|
+
const latestCommit = JSON.parse(
|
|
1126
|
+
fs.readFileSync(path.join(commitDir, commitFiles[commitFiles.length - 1]), "utf-8"),
|
|
1127
|
+
);
|
|
1128
|
+
assert(Array.isArray(latestCommit.payload.next_steps));
|
|
1129
|
+
assert.strictEqual(latestCommit.payload.next_steps.length, 0);
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
describe("Tool Input Validation", () => {
|
|
1134
|
+
it("should handle missing required arguments", async () => {
|
|
1135
|
+
const { callTool } = await import("./server.js");
|
|
1136
|
+
|
|
1137
|
+
try {
|
|
1138
|
+
await callTool(
|
|
1139
|
+
{
|
|
1140
|
+
name: "bugmole_read_spec",
|
|
1141
|
+
arguments: {}, // Missing required filePath
|
|
1142
|
+
},
|
|
1143
|
+
cfg,
|
|
1144
|
+
);
|
|
1145
|
+
// Should either throw or return error
|
|
1146
|
+
} catch (error: any) {
|
|
1147
|
+
assert(error.message.length > 0);
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it("should handle invalid argument types", async () => {
|
|
1152
|
+
const { callTool } = await import("./server.js");
|
|
1153
|
+
|
|
1154
|
+
try {
|
|
1155
|
+
await callTool(
|
|
1156
|
+
{
|
|
1157
|
+
name: "bugmole_read_spec",
|
|
1158
|
+
arguments: {
|
|
1159
|
+
filePath: 123 as any, // Wrong type
|
|
1160
|
+
},
|
|
1161
|
+
},
|
|
1162
|
+
cfg,
|
|
1163
|
+
);
|
|
1164
|
+
// Should handle gracefully
|
|
1165
|
+
} catch (error: any) {
|
|
1166
|
+
// If it throws, should be a clear error
|
|
1167
|
+
assert(error.message.length > 0);
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
describe("Path Resolution", () => {
|
|
1173
|
+
it("should handle relative paths from different directories", async () => {
|
|
1174
|
+
const { readSpecFile } = await import("./server.js");
|
|
1175
|
+
|
|
1176
|
+
// Create file in spec/plans/
|
|
1177
|
+
const testFile = path.join(testDir, "spec", "plans", "relative.yaml");
|
|
1178
|
+
fs.writeFileSync(testFile, "relative: test");
|
|
1179
|
+
|
|
1180
|
+
// Try different path formats
|
|
1181
|
+
const paths = [
|
|
1182
|
+
"plans/relative.yaml",
|
|
1183
|
+
"spec/plans/relative.yaml",
|
|
1184
|
+
path.join(testDir, "spec", "plans", "relative.yaml"),
|
|
1185
|
+
];
|
|
1186
|
+
|
|
1187
|
+
for (const testPath of paths) {
|
|
1188
|
+
try {
|
|
1189
|
+
const result = readSpecFile(testPath, cfg);
|
|
1190
|
+
assert.strictEqual(result.content, "relative: test");
|
|
1191
|
+
break; // If one works, that's good
|
|
1192
|
+
} catch {
|
|
1193
|
+
// Continue trying other paths
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
it("should use environment variables for project directory", async () => {
|
|
1199
|
+
// Set environment variable
|
|
1200
|
+
const originalDir = process.env.BUGMOLE_PROJECT_DIR;
|
|
1201
|
+
process.env.BUGMOLE_PROJECT_DIR = testDir;
|
|
1202
|
+
|
|
1203
|
+
try {
|
|
1204
|
+
const { readSpecFile } = await import("./server.js");
|
|
1205
|
+
const testFile = path.join(testDir, "spec", "env_test.yaml");
|
|
1206
|
+
fs.writeFileSync(testFile, "env: test");
|
|
1207
|
+
|
|
1208
|
+
const result = readSpecFile("env_test.yaml", cfg);
|
|
1209
|
+
assert.strictEqual(result.content, "env: test");
|
|
1210
|
+
} finally {
|
|
1211
|
+
// Restore
|
|
1212
|
+
if (originalDir) {
|
|
1213
|
+
process.env.BUGMOLE_PROJECT_DIR = originalDir;
|
|
1214
|
+
} else {
|
|
1215
|
+
delete process.env.BUGMOLE_PROJECT_DIR;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
});
|
|
1220
|
+
});
|