codeblog-app 1.6.5 → 2.0.1
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/package.json +9 -23
- package/src/ai/__tests__/chat.test.ts +110 -0
- package/src/ai/__tests__/provider.test.ts +184 -0
- package/src/ai/__tests__/tools.test.ts +90 -0
- package/src/ai/chat.ts +14 -14
- package/src/ai/provider.ts +24 -250
- package/src/ai/tools.ts +46 -281
- package/src/auth/oauth.ts +7 -0
- package/src/cli/__tests__/commands.test.ts +225 -0
- package/src/cli/__tests__/setup.test.ts +57 -0
- package/src/cli/cmd/agent.ts +102 -0
- package/src/cli/cmd/chat.ts +1 -1
- package/src/cli/cmd/comment.ts +47 -16
- package/src/cli/cmd/feed.ts +18 -30
- package/src/cli/cmd/forum.ts +123 -0
- package/src/cli/cmd/login.ts +9 -2
- package/src/cli/cmd/me.ts +202 -0
- package/src/cli/cmd/post.ts +6 -88
- package/src/cli/cmd/publish.ts +44 -23
- package/src/cli/cmd/scan.ts +45 -34
- package/src/cli/cmd/search.ts +8 -70
- package/src/cli/cmd/setup.ts +160 -62
- package/src/cli/cmd/vote.ts +29 -14
- package/src/cli/cmd/whoami.ts +7 -36
- package/src/cli/ui.ts +50 -0
- package/src/index.ts +80 -59
- package/src/mcp/__tests__/client.test.ts +149 -0
- package/src/mcp/__tests__/e2e.ts +327 -0
- package/src/mcp/__tests__/integration.ts +148 -0
- package/src/mcp/client.ts +148 -0
- package/src/api/agents.ts +0 -103
- package/src/api/bookmarks.ts +0 -25
- package/src/api/client.ts +0 -96
- package/src/api/debates.ts +0 -35
- package/src/api/feed.ts +0 -25
- package/src/api/notifications.ts +0 -31
- package/src/api/posts.ts +0 -116
- package/src/api/search.ts +0 -29
- package/src/api/tags.ts +0 -13
- package/src/api/trending.ts +0 -38
- package/src/api/users.ts +0 -8
- package/src/cli/cmd/agents.ts +0 -77
- package/src/cli/cmd/ai-publish.ts +0 -118
- package/src/cli/cmd/bookmark.ts +0 -27
- package/src/cli/cmd/bookmarks.ts +0 -42
- package/src/cli/cmd/dashboard.ts +0 -59
- package/src/cli/cmd/debate.ts +0 -89
- package/src/cli/cmd/delete.ts +0 -35
- package/src/cli/cmd/edit.ts +0 -42
- package/src/cli/cmd/explore.ts +0 -63
- package/src/cli/cmd/follow.ts +0 -34
- package/src/cli/cmd/myposts.ts +0 -50
- package/src/cli/cmd/notifications.ts +0 -65
- package/src/cli/cmd/tags.ts +0 -58
- package/src/cli/cmd/trending.ts +0 -64
- package/src/cli/cmd/weekly-digest.ts +0 -117
- package/src/publisher/index.ts +0 -139
- package/src/scanner/__tests__/analyzer.test.ts +0 -67
- package/src/scanner/__tests__/fs-utils.test.ts +0 -50
- package/src/scanner/__tests__/platform.test.ts +0 -27
- package/src/scanner/__tests__/registry.test.ts +0 -56
- package/src/scanner/aider.ts +0 -96
- package/src/scanner/analyzer.ts +0 -237
- package/src/scanner/claude-code.ts +0 -188
- package/src/scanner/codex.ts +0 -127
- package/src/scanner/continue-dev.ts +0 -95
- package/src/scanner/cursor.ts +0 -299
- package/src/scanner/fs-utils.ts +0 -123
- package/src/scanner/index.ts +0 -26
- package/src/scanner/platform.ts +0 -44
- package/src/scanner/registry.ts +0 -68
- package/src/scanner/types.ts +0 -62
- package/src/scanner/vscode-copilot.ts +0 -125
- package/src/scanner/warp.ts +0 -19
- package/src/scanner/windsurf.ts +0 -147
- package/src/scanner/zed.ts +0 -88
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test"
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// We test the McpBridge module by mocking the MCP SDK classes.
|
|
5
|
+
// The actual module spawns a subprocess, which we don't want in unit tests.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
// Mock the MCP SDK
|
|
9
|
+
const mockCallTool = mock(() =>
|
|
10
|
+
Promise.resolve({
|
|
11
|
+
content: [{ type: "text", text: '{"ok":true}' }],
|
|
12
|
+
isError: false,
|
|
13
|
+
}),
|
|
14
|
+
)
|
|
15
|
+
const mockListTools = mock(() =>
|
|
16
|
+
Promise.resolve({ tools: [{ name: "test_tool", description: "A test tool" }] }),
|
|
17
|
+
)
|
|
18
|
+
const mockConnect = mock(() => Promise.resolve())
|
|
19
|
+
const mockGetServerVersion = mock(() => ({ name: "test-server", version: "1.0.0" }))
|
|
20
|
+
const mockClose = mock(() => Promise.resolve())
|
|
21
|
+
|
|
22
|
+
mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
|
|
23
|
+
Client: class MockClient {
|
|
24
|
+
callTool = mockCallTool
|
|
25
|
+
listTools = mockListTools
|
|
26
|
+
connect = mockConnect
|
|
27
|
+
getServerVersion = mockGetServerVersion
|
|
28
|
+
},
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
mock.module("@modelcontextprotocol/sdk/client/stdio.js", () => ({
|
|
32
|
+
StdioClientTransport: class MockTransport {
|
|
33
|
+
close = mockClose
|
|
34
|
+
},
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
// Must import AFTER mocks are set up
|
|
38
|
+
const { McpBridge } = await import("../client")
|
|
39
|
+
|
|
40
|
+
describe("McpBridge", () => {
|
|
41
|
+
afterEach(async () => {
|
|
42
|
+
await McpBridge.disconnect()
|
|
43
|
+
mockCallTool.mockClear()
|
|
44
|
+
mockListTools.mockClear()
|
|
45
|
+
mockConnect.mockClear()
|
|
46
|
+
mockClose.mockClear()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("callTool returns text content from MCP result", async () => {
|
|
50
|
+
const result = await McpBridge.callTool("test_tool", { key: "value" })
|
|
51
|
+
expect(result).toBe('{"ok":true}')
|
|
52
|
+
expect(mockCallTool).toHaveBeenCalledWith({
|
|
53
|
+
name: "test_tool",
|
|
54
|
+
arguments: { key: "value" },
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("callToolJSON parses JSON result", async () => {
|
|
59
|
+
const result = await McpBridge.callToolJSON("test_tool")
|
|
60
|
+
expect(result).toEqual({ ok: true })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test("callToolJSON falls back to raw text when JSON parse fails", async () => {
|
|
64
|
+
mockCallTool.mockImplementationOnce(() =>
|
|
65
|
+
Promise.resolve({
|
|
66
|
+
content: [{ type: "text", text: "not json" }],
|
|
67
|
+
isError: false,
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
const result = await McpBridge.callToolJSON("test_tool")
|
|
71
|
+
expect(result).toBe("not json")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("callTool throws on error result", async () => {
|
|
75
|
+
mockCallTool.mockImplementationOnce(() =>
|
|
76
|
+
Promise.resolve({
|
|
77
|
+
content: [{ type: "text", text: "Something went wrong" }],
|
|
78
|
+
isError: true,
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
expect(McpBridge.callTool("failing_tool")).rejects.toThrow("Something went wrong")
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("callTool throws generic message when error has no text", async () => {
|
|
85
|
+
mockCallTool.mockImplementationOnce(() =>
|
|
86
|
+
Promise.resolve({
|
|
87
|
+
content: [],
|
|
88
|
+
isError: true,
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
expect(McpBridge.callTool("failing_tool")).rejects.toThrow('MCP tool "failing_tool" returned an error')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("callTool handles empty content array", async () => {
|
|
95
|
+
mockCallTool.mockImplementationOnce(() =>
|
|
96
|
+
Promise.resolve({
|
|
97
|
+
content: [],
|
|
98
|
+
isError: false,
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
const result = await McpBridge.callTool("test_tool")
|
|
102
|
+
expect(result).toBe("")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("callTool joins multiple text content items", async () => {
|
|
106
|
+
mockCallTool.mockImplementationOnce(() =>
|
|
107
|
+
Promise.resolve({
|
|
108
|
+
content: [
|
|
109
|
+
{ type: "text", text: "line1" },
|
|
110
|
+
{ type: "text", text: "line2" },
|
|
111
|
+
{ type: "image", data: "..." },
|
|
112
|
+
],
|
|
113
|
+
isError: false,
|
|
114
|
+
}),
|
|
115
|
+
)
|
|
116
|
+
const result = await McpBridge.callTool("test_tool")
|
|
117
|
+
expect(result).toBe("line1\nline2")
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test("listTools delegates to MCP client", async () => {
|
|
121
|
+
const result = await McpBridge.listTools()
|
|
122
|
+
expect(result.tools).toHaveLength(1)
|
|
123
|
+
expect(result.tools[0].name).toBe("test_tool")
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test("disconnect cleans up transport and client", async () => {
|
|
127
|
+
// First connect by making a call
|
|
128
|
+
await McpBridge.callTool("test_tool")
|
|
129
|
+
// Then disconnect
|
|
130
|
+
await McpBridge.disconnect()
|
|
131
|
+
// Verify close was called
|
|
132
|
+
expect(mockClose).toHaveBeenCalled()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test("connection is reused across multiple calls", async () => {
|
|
136
|
+
await McpBridge.callTool("test_tool")
|
|
137
|
+
await McpBridge.callTool("test_tool")
|
|
138
|
+
// connect should only be called once
|
|
139
|
+
expect(mockConnect).toHaveBeenCalledTimes(1)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("default args is empty object", async () => {
|
|
143
|
+
await McpBridge.callTool("test_tool")
|
|
144
|
+
expect(mockCallTool).toHaveBeenCalledWith({
|
|
145
|
+
name: "test_tool",
|
|
146
|
+
arguments: {},
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
})
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full E2E test: test ALL MCP tools as a real user would.
|
|
3
|
+
* This script walks through the entire user journey:
|
|
4
|
+
*
|
|
5
|
+
* 1. Status check
|
|
6
|
+
* 2. Scan IDE sessions
|
|
7
|
+
* 3. Read a session
|
|
8
|
+
* 4. Analyze a session
|
|
9
|
+
* 5. Post to CodeBlog
|
|
10
|
+
* 6. Browse posts
|
|
11
|
+
* 7. Search posts
|
|
12
|
+
* 8. Read a specific post
|
|
13
|
+
* 9. Upvote a post
|
|
14
|
+
* 10. Comment on a post
|
|
15
|
+
* 11. Edit the post
|
|
16
|
+
* 12. Bookmark the post
|
|
17
|
+
* 13. Browse by tag
|
|
18
|
+
* 14. Trending topics
|
|
19
|
+
* 15. Explore and engage
|
|
20
|
+
* 16. My posts
|
|
21
|
+
* 17. My dashboard
|
|
22
|
+
* 18. My notifications
|
|
23
|
+
* 19. Manage agents
|
|
24
|
+
* 20. Follow a user
|
|
25
|
+
* 21. Join debate
|
|
26
|
+
* 22. Weekly digest (dry run)
|
|
27
|
+
* 23. Delete the test post
|
|
28
|
+
* 24. Unbookmark
|
|
29
|
+
*
|
|
30
|
+
* Usage: bun run src/mcp/__tests__/e2e.ts
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { McpBridge } from "../client"
|
|
34
|
+
|
|
35
|
+
let testPostId = ""
|
|
36
|
+
let testCommentId = ""
|
|
37
|
+
let passed = 0
|
|
38
|
+
let failed = 0
|
|
39
|
+
|
|
40
|
+
async function test(name: string, fn: () => Promise<void>) {
|
|
41
|
+
try {
|
|
42
|
+
await fn()
|
|
43
|
+
console.log(` ✓ ${name}`)
|
|
44
|
+
passed++
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
47
|
+
console.log(` ✗ ${name}`)
|
|
48
|
+
console.log(` Error: ${msg.slice(0, 200)}`)
|
|
49
|
+
failed++
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assert(condition: boolean, msg: string) {
|
|
54
|
+
if (!condition) throw new Error(`Assertion failed: ${msg}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function main() {
|
|
58
|
+
console.log("=== CodeBlog E2E Test — Full User Journey ===\n")
|
|
59
|
+
|
|
60
|
+
// 1. Status check
|
|
61
|
+
await test("1. codeblog_status", async () => {
|
|
62
|
+
const result = await McpBridge.callTool("codeblog_status")
|
|
63
|
+
assert(result.includes("CodeBlog MCP Server"), "should include server info")
|
|
64
|
+
assert(result.includes("Agent:"), "should include agent info (authenticated)")
|
|
65
|
+
console.log(` → ${result.split("\n")[0]}`)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// 2. Scan IDE sessions
|
|
69
|
+
let sessionPath = ""
|
|
70
|
+
let sessionSource = ""
|
|
71
|
+
await test("2. scan_sessions", async () => {
|
|
72
|
+
const raw = await McpBridge.callTool("scan_sessions", { limit: 5 })
|
|
73
|
+
const sessions = JSON.parse(raw)
|
|
74
|
+
assert(Array.isArray(sessions), "should return array")
|
|
75
|
+
assert(sessions.length > 0, "should have at least 1 session")
|
|
76
|
+
sessionPath = sessions[0].path
|
|
77
|
+
sessionSource = sessions[0].source
|
|
78
|
+
console.log(` → Found ${sessions.length} sessions, first: [${sessionSource}] ${sessions[0].project}`)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// 3. Read a session
|
|
82
|
+
await test("3. read_session", async () => {
|
|
83
|
+
assert(sessionPath !== "", "need a session path from step 2")
|
|
84
|
+
const raw = await McpBridge.callTool("read_session", {
|
|
85
|
+
path: sessionPath,
|
|
86
|
+
source: sessionSource,
|
|
87
|
+
max_turns: 3,
|
|
88
|
+
})
|
|
89
|
+
assert(raw.length > 50, "should return session content")
|
|
90
|
+
console.log(` → Got ${raw.length} chars of session content`)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// 4. Analyze a session
|
|
94
|
+
await test("4. analyze_session", async () => {
|
|
95
|
+
const raw = await McpBridge.callTool("analyze_session", {
|
|
96
|
+
path: sessionPath,
|
|
97
|
+
source: sessionSource,
|
|
98
|
+
})
|
|
99
|
+
assert(raw.length > 50, "should return analysis")
|
|
100
|
+
console.log(` → Got ${raw.length} chars of analysis`)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// 5. Post to CodeBlog
|
|
104
|
+
await test("5. post_to_codeblog", async () => {
|
|
105
|
+
const raw = await McpBridge.callTool("post_to_codeblog", {
|
|
106
|
+
title: "[E2E Test] MCP Integration Test Post",
|
|
107
|
+
content: "This is an automated test post from the E2E test suite.\n\n## Test Content\n\n```typescript\nconsole.log('Hello from E2E test!')\n```\n\nThis post will be deleted after testing.",
|
|
108
|
+
source_session: sessionPath,
|
|
109
|
+
tags: ["e2e-test", "automated", "mcp"],
|
|
110
|
+
summary: "Automated test post — will be deleted",
|
|
111
|
+
category: "general",
|
|
112
|
+
})
|
|
113
|
+
// MCP returns text like "✅ Posted! View at: https://codeblog.ai/post/<id>"
|
|
114
|
+
// or JSON. Handle both.
|
|
115
|
+
try {
|
|
116
|
+
const result = JSON.parse(raw)
|
|
117
|
+
testPostId = result.id || result.post?.id || ""
|
|
118
|
+
} catch {
|
|
119
|
+
// Extract post ID from URL in text
|
|
120
|
+
const urlMatch = raw.match(/\/post\/([a-z0-9]+)/)
|
|
121
|
+
testPostId = urlMatch?.[1] || ""
|
|
122
|
+
}
|
|
123
|
+
assert(testPostId !== "", `should extract post ID from: ${raw.slice(0, 100)}`)
|
|
124
|
+
console.log(` → Created post: ${testPostId}`)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// 6. Browse posts
|
|
128
|
+
await test("6. browse_posts", async () => {
|
|
129
|
+
const raw = await McpBridge.callTool("browse_posts", { sort: "new", limit: 5 })
|
|
130
|
+
assert(raw.length > 10, "should return posts")
|
|
131
|
+
const result = JSON.parse(raw)
|
|
132
|
+
assert(result.posts || Array.isArray(result), "should be parseable")
|
|
133
|
+
const posts = result.posts || result
|
|
134
|
+
console.log(` → Got ${posts.length} posts`)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// 7. Search posts
|
|
138
|
+
await test("7. search_posts", async () => {
|
|
139
|
+
const raw = await McpBridge.callTool("search_posts", { query: "E2E Test", limit: 5 })
|
|
140
|
+
assert(raw.length > 5, "should return results")
|
|
141
|
+
console.log(` → Search returned ${raw.length} chars`)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// 8. Read the test post
|
|
145
|
+
await test("8. read_post", async () => {
|
|
146
|
+
assert(testPostId !== "", "need post ID from step 5")
|
|
147
|
+
const raw = await McpBridge.callTool("read_post", { post_id: testPostId })
|
|
148
|
+
assert(raw.length > 50, "should return post content")
|
|
149
|
+
assert(raw.includes("E2E Test") || raw.includes("e2e"), "should contain test post content")
|
|
150
|
+
console.log(` → Read post: ${raw.length} chars`)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// 9. Upvote the post
|
|
154
|
+
await test("9. vote_on_post (upvote)", async () => {
|
|
155
|
+
assert(testPostId !== "", "need post ID from step 5")
|
|
156
|
+
const raw = await McpBridge.callTool("vote_on_post", { post_id: testPostId, value: 1 })
|
|
157
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// 10. Comment on the post
|
|
161
|
+
await test("10. comment_on_post", async () => {
|
|
162
|
+
assert(testPostId !== "", "need post ID from step 5")
|
|
163
|
+
const raw = await McpBridge.callTool("comment_on_post", {
|
|
164
|
+
post_id: testPostId,
|
|
165
|
+
content: "This is an automated E2E test comment. Testing the comment system!",
|
|
166
|
+
})
|
|
167
|
+
// Extract comment ID from text or JSON
|
|
168
|
+
try {
|
|
169
|
+
const result = JSON.parse(raw)
|
|
170
|
+
testCommentId = result.id || result.comment?.id || ""
|
|
171
|
+
} catch {
|
|
172
|
+
const idMatch = raw.match(/Comment ID:\s*([a-z0-9]+)/)
|
|
173
|
+
testCommentId = idMatch?.[1] || ""
|
|
174
|
+
}
|
|
175
|
+
console.log(` → ${raw.split("\n")[0].slice(0, 80)}`)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// 11. Edit the post
|
|
179
|
+
await test("11. edit_post", async () => {
|
|
180
|
+
assert(testPostId !== "", "need post ID from step 5")
|
|
181
|
+
const raw = await McpBridge.callTool("edit_post", {
|
|
182
|
+
post_id: testPostId,
|
|
183
|
+
title: "[E2E Test] MCP Integration Test Post (Edited)",
|
|
184
|
+
summary: "Automated test post — EDITED — will be deleted",
|
|
185
|
+
})
|
|
186
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// 12. Bookmark the post
|
|
190
|
+
await test("12. bookmark_post (toggle)", async () => {
|
|
191
|
+
assert(testPostId !== "", "need post ID from step 5")
|
|
192
|
+
const raw = await McpBridge.callTool("bookmark_post", {
|
|
193
|
+
action: "toggle",
|
|
194
|
+
post_id: testPostId,
|
|
195
|
+
})
|
|
196
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// 13. List bookmarks
|
|
200
|
+
await test("13. bookmark_post (list)", async () => {
|
|
201
|
+
const raw = await McpBridge.callTool("bookmark_post", { action: "list" })
|
|
202
|
+
assert(raw.length > 0, "should return bookmarks")
|
|
203
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// 14. Browse by tag
|
|
207
|
+
await test("14. browse_by_tag (trending)", async () => {
|
|
208
|
+
const raw = await McpBridge.callTool("browse_by_tag", { action: "trending", limit: 5 })
|
|
209
|
+
assert(raw.length > 0, "should return trending tags")
|
|
210
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
await test("15. browse_by_tag (posts)", async () => {
|
|
214
|
+
const raw = await McpBridge.callTool("browse_by_tag", { action: "posts", tag: "e2e-test", limit: 5 })
|
|
215
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// 16. Trending topics
|
|
219
|
+
await test("16. trending_topics", async () => {
|
|
220
|
+
const raw = await McpBridge.callTool("trending_topics")
|
|
221
|
+
assert(raw.includes("Trending"), "should include trending info")
|
|
222
|
+
console.log(` → ${raw.split("\n")[0]}`)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// 17. Explore and engage
|
|
226
|
+
await test("17. explore_and_engage (browse)", async () => {
|
|
227
|
+
const raw = await McpBridge.callTool("explore_and_engage", { action: "browse", limit: 3 })
|
|
228
|
+
assert(raw.length > 0, "should return content")
|
|
229
|
+
console.log(` → ${raw.split("\n")[0]}`)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// 18. My posts
|
|
233
|
+
await test("18. my_posts", async () => {
|
|
234
|
+
const raw = await McpBridge.callTool("my_posts", { limit: 5 })
|
|
235
|
+
assert(raw.length > 0, "should return my posts")
|
|
236
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// 19. My dashboard
|
|
240
|
+
await test("19. my_dashboard", async () => {
|
|
241
|
+
const raw = await McpBridge.callTool("my_dashboard")
|
|
242
|
+
assert(raw.length > 0, "should return dashboard data")
|
|
243
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// 20. My notifications
|
|
247
|
+
await test("20. my_notifications (list)", async () => {
|
|
248
|
+
const raw = await McpBridge.callTool("my_notifications", { action: "list", limit: 5 })
|
|
249
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// 21. Manage agents
|
|
253
|
+
await test("21. manage_agents (list)", async () => {
|
|
254
|
+
const raw = await McpBridge.callTool("manage_agents", { action: "list" })
|
|
255
|
+
assert(raw.length > 0, "should return agents")
|
|
256
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// 22. Follow agent / user
|
|
260
|
+
await test("22. follow_agent (list_following)", async () => {
|
|
261
|
+
const raw = await McpBridge.callTool("follow_agent", { action: "list_following", limit: 5 })
|
|
262
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// 23. Join debate
|
|
266
|
+
await test("23. join_debate (list)", async () => {
|
|
267
|
+
const raw = await McpBridge.callTool("join_debate", { action: "list" })
|
|
268
|
+
console.log(` → ${raw.slice(0, 80)}`)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// 24. Weekly digest (dry run)
|
|
272
|
+
await test("24. weekly_digest (dry_run)", async () => {
|
|
273
|
+
const raw = await McpBridge.callTool("weekly_digest", { dry_run: true })
|
|
274
|
+
assert(raw.length > 0, "should return digest preview")
|
|
275
|
+
console.log(` → ${raw.split("\n")[0]}`)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// 25. Auto post (dry run)
|
|
279
|
+
await test("25. auto_post (dry_run)", async () => {
|
|
280
|
+
const raw = await McpBridge.callTool("auto_post", { dry_run: true })
|
|
281
|
+
assert(raw.length > 0, "should return post preview")
|
|
282
|
+
console.log(` → ${raw.split("\n")[0].slice(0, 100)}`)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// 26. Remove vote
|
|
286
|
+
await test("26. vote_on_post (remove)", async () => {
|
|
287
|
+
assert(testPostId !== "", "need post ID")
|
|
288
|
+
const raw = await McpBridge.callTool("vote_on_post", { post_id: testPostId, value: 0 })
|
|
289
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// 27. Unbookmark
|
|
293
|
+
await test("27. bookmark_post (unbookmark)", async () => {
|
|
294
|
+
assert(testPostId !== "", "need post ID")
|
|
295
|
+
const raw = await McpBridge.callTool("bookmark_post", {
|
|
296
|
+
action: "toggle",
|
|
297
|
+
post_id: testPostId,
|
|
298
|
+
})
|
|
299
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// 28. Delete the test post (cleanup)
|
|
303
|
+
await test("28. delete_post (cleanup)", async () => {
|
|
304
|
+
assert(testPostId !== "", "need post ID to delete")
|
|
305
|
+
const raw = await McpBridge.callTool("delete_post", {
|
|
306
|
+
post_id: testPostId,
|
|
307
|
+
confirm: true,
|
|
308
|
+
})
|
|
309
|
+
console.log(` → ${raw.slice(0, 100)}`)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
// Disconnect
|
|
313
|
+
await McpBridge.disconnect()
|
|
314
|
+
|
|
315
|
+
console.log("\n=== Summary ===")
|
|
316
|
+
console.log(`Passed: ${passed}/${passed + failed}`)
|
|
317
|
+
console.log(`Failed: ${failed}/${passed + failed}`)
|
|
318
|
+
|
|
319
|
+
if (failed > 0) {
|
|
320
|
+
process.exit(1)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
main().catch((err) => {
|
|
325
|
+
console.error("Fatal:", err)
|
|
326
|
+
process.exit(1)
|
|
327
|
+
})
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: verify all 26 MCP tools are accessible via McpBridge.
|
|
3
|
+
*
|
|
4
|
+
* This script:
|
|
5
|
+
* 1. Connects to the MCP server (spawns codeblog-mcp subprocess)
|
|
6
|
+
* 2. Lists all available tools
|
|
7
|
+
* 3. Tests calling each tool that can be safely invoked without side effects
|
|
8
|
+
* 4. Reports results
|
|
9
|
+
*
|
|
10
|
+
* Usage: bun run src/mcp/__tests__/integration.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { McpBridge } from "../client"
|
|
14
|
+
|
|
15
|
+
const EXPECTED_TOOLS = [
|
|
16
|
+
"scan_sessions",
|
|
17
|
+
"read_session",
|
|
18
|
+
"analyze_session",
|
|
19
|
+
"post_to_codeblog",
|
|
20
|
+
"auto_post",
|
|
21
|
+
"weekly_digest",
|
|
22
|
+
"browse_posts",
|
|
23
|
+
"search_posts",
|
|
24
|
+
"read_post",
|
|
25
|
+
"comment_on_post",
|
|
26
|
+
"vote_on_post",
|
|
27
|
+
"edit_post",
|
|
28
|
+
"delete_post",
|
|
29
|
+
"bookmark_post",
|
|
30
|
+
"browse_by_tag",
|
|
31
|
+
"trending_topics",
|
|
32
|
+
"explore_and_engage",
|
|
33
|
+
"join_debate",
|
|
34
|
+
"my_notifications",
|
|
35
|
+
"manage_agents",
|
|
36
|
+
"my_posts",
|
|
37
|
+
"my_dashboard",
|
|
38
|
+
"follow_agent",
|
|
39
|
+
"codeblog_status",
|
|
40
|
+
"codeblog_setup",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
// Tools that are safe to call without side effects (read-only)
|
|
44
|
+
const SAFE_TOOLS: Record<string, Record<string, unknown>> = {
|
|
45
|
+
codeblog_status: {},
|
|
46
|
+
scan_sessions: { limit: 3 },
|
|
47
|
+
browse_posts: { sort: "new", limit: 2 },
|
|
48
|
+
search_posts: { query: "test", limit: 2 },
|
|
49
|
+
browse_by_tag: { action: "trending", limit: 3 },
|
|
50
|
+
trending_topics: {},
|
|
51
|
+
explore_and_engage: { action: "browse", limit: 2 },
|
|
52
|
+
join_debate: { action: "list" },
|
|
53
|
+
my_notifications: { action: "list", limit: 2 },
|
|
54
|
+
manage_agents: { action: "list" },
|
|
55
|
+
my_posts: { limit: 2 },
|
|
56
|
+
my_dashboard: {},
|
|
57
|
+
follow_agent: { action: "list_following", limit: 2 },
|
|
58
|
+
bookmark_post: { action: "list" },
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
console.log("=== MCP Integration Test ===\n")
|
|
63
|
+
|
|
64
|
+
// Step 1: List tools
|
|
65
|
+
console.log("1. Listing MCP tools...")
|
|
66
|
+
let tools: Array<{ name: string; description?: string }>
|
|
67
|
+
try {
|
|
68
|
+
const result = await McpBridge.listTools()
|
|
69
|
+
tools = result.tools
|
|
70
|
+
console.log(` ✓ Found ${tools.length} tools\n`)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error(` ✗ Failed to list tools: ${err instanceof Error ? err.message : err}`)
|
|
73
|
+
await McpBridge.disconnect()
|
|
74
|
+
process.exit(1)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 2: Check expected tools
|
|
79
|
+
console.log("2. Checking expected tools...")
|
|
80
|
+
const toolNames = tools.map((t) => t.name)
|
|
81
|
+
let missing = 0
|
|
82
|
+
for (const expected of EXPECTED_TOOLS) {
|
|
83
|
+
if (toolNames.includes(expected)) {
|
|
84
|
+
console.log(` ✓ ${expected}`)
|
|
85
|
+
} else {
|
|
86
|
+
console.log(` ✗ MISSING: ${expected}`)
|
|
87
|
+
missing++
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const extra = toolNames.filter((t) => !EXPECTED_TOOLS.includes(t))
|
|
92
|
+
if (extra.length > 0) {
|
|
93
|
+
console.log(`\n Extra tools not in expected list: ${extra.join(", ")}`)
|
|
94
|
+
}
|
|
95
|
+
console.log(`\n Expected: ${EXPECTED_TOOLS.length}, Found: ${toolNames.length}, Missing: ${missing}\n`)
|
|
96
|
+
|
|
97
|
+
// Step 3: Call safe tools
|
|
98
|
+
console.log("3. Testing safe tool calls...")
|
|
99
|
+
let passed = 0
|
|
100
|
+
let failed = 0
|
|
101
|
+
|
|
102
|
+
for (const [name, args] of Object.entries(SAFE_TOOLS)) {
|
|
103
|
+
if (!toolNames.includes(name)) {
|
|
104
|
+
console.log(` ⊘ ${name} — skipped (not available)`)
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const result = await McpBridge.callTool(name, args)
|
|
110
|
+
const preview = result.slice(0, 80).replace(/\n/g, " ")
|
|
111
|
+
console.log(` ✓ ${name} — ${preview}${result.length > 80 ? "..." : ""}`)
|
|
112
|
+
passed++
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
115
|
+
// Auth errors are expected in test environment
|
|
116
|
+
if (msg.includes("auth") || msg.includes("API key") || msg.includes("token") || msg.includes("401") || msg.includes("unauthorized") || msg.includes("Unauthorized")) {
|
|
117
|
+
console.log(` ⊘ ${name} — auth required (expected in test env)`)
|
|
118
|
+
passed++ // Count as pass — tool is reachable, just needs auth
|
|
119
|
+
} else {
|
|
120
|
+
console.log(` ✗ ${name} — ${msg}`)
|
|
121
|
+
failed++
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`\n Passed: ${passed}, Failed: ${failed}\n`)
|
|
127
|
+
|
|
128
|
+
// Cleanup
|
|
129
|
+
console.log("4. Disconnecting...")
|
|
130
|
+
await McpBridge.disconnect()
|
|
131
|
+
console.log(" ✓ Disconnected\n")
|
|
132
|
+
|
|
133
|
+
console.log("=== Summary ===")
|
|
134
|
+
console.log(`Tools found: ${toolNames.length}`)
|
|
135
|
+
console.log(`Tools tested: ${passed + failed}/${Object.keys(SAFE_TOOLS).length}`)
|
|
136
|
+
console.log(`Tests passed: ${passed}`)
|
|
137
|
+
console.log(`Tests failed: ${failed}`)
|
|
138
|
+
console.log(`Missing expected tools: ${missing}`)
|
|
139
|
+
|
|
140
|
+
if (failed > 0 || missing > 0) {
|
|
141
|
+
process.exit(1)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main().catch((err) => {
|
|
146
|
+
console.error("Fatal error:", err)
|
|
147
|
+
process.exit(1)
|
|
148
|
+
})
|