codeblog-app 2.3.1 → 2.3.2

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.
Files changed (83) hide show
  1. package/package.json +8 -73
  2. package/drizzle/0000_init.sql +0 -34
  3. package/drizzle/meta/_journal.json +0 -13
  4. package/drizzle.config.ts +0 -10
  5. package/src/ai/__tests__/chat.test.ts +0 -188
  6. package/src/ai/__tests__/compat.test.ts +0 -46
  7. package/src/ai/__tests__/home.ai-stream.integration.test.ts +0 -77
  8. package/src/ai/__tests__/provider-registry.test.ts +0 -61
  9. package/src/ai/__tests__/provider.test.ts +0 -238
  10. package/src/ai/__tests__/stream-events.test.ts +0 -152
  11. package/src/ai/__tests__/tools.test.ts +0 -93
  12. package/src/ai/chat.ts +0 -336
  13. package/src/ai/configure.ts +0 -143
  14. package/src/ai/models.ts +0 -26
  15. package/src/ai/provider-registry.ts +0 -150
  16. package/src/ai/provider.ts +0 -264
  17. package/src/ai/stream-events.ts +0 -64
  18. package/src/ai/tools.ts +0 -118
  19. package/src/ai/types.ts +0 -105
  20. package/src/auth/index.ts +0 -49
  21. package/src/auth/oauth.ts +0 -123
  22. package/src/cli/__tests__/commands.test.ts +0 -229
  23. package/src/cli/cmd/agent.ts +0 -97
  24. package/src/cli/cmd/ai.ts +0 -10
  25. package/src/cli/cmd/chat.ts +0 -190
  26. package/src/cli/cmd/comment.ts +0 -67
  27. package/src/cli/cmd/config.ts +0 -153
  28. package/src/cli/cmd/feed.ts +0 -53
  29. package/src/cli/cmd/forum.ts +0 -106
  30. package/src/cli/cmd/login.ts +0 -45
  31. package/src/cli/cmd/logout.ts +0 -12
  32. package/src/cli/cmd/me.ts +0 -188
  33. package/src/cli/cmd/post.ts +0 -25
  34. package/src/cli/cmd/publish.ts +0 -64
  35. package/src/cli/cmd/scan.ts +0 -78
  36. package/src/cli/cmd/search.ts +0 -35
  37. package/src/cli/cmd/setup.ts +0 -622
  38. package/src/cli/cmd/tui.ts +0 -20
  39. package/src/cli/cmd/uninstall.ts +0 -281
  40. package/src/cli/cmd/update.ts +0 -123
  41. package/src/cli/cmd/vote.ts +0 -50
  42. package/src/cli/cmd/whoami.ts +0 -18
  43. package/src/cli/mcp-print.ts +0 -6
  44. package/src/cli/ui.ts +0 -357
  45. package/src/config/index.ts +0 -92
  46. package/src/flag/index.ts +0 -23
  47. package/src/global/index.ts +0 -38
  48. package/src/id/index.ts +0 -20
  49. package/src/index.ts +0 -203
  50. package/src/mcp/__tests__/client.test.ts +0 -149
  51. package/src/mcp/__tests__/e2e.ts +0 -331
  52. package/src/mcp/__tests__/integration.ts +0 -148
  53. package/src/mcp/client.ts +0 -118
  54. package/src/server/index.ts +0 -48
  55. package/src/storage/chat.ts +0 -73
  56. package/src/storage/db.ts +0 -85
  57. package/src/storage/schema.sql.ts +0 -39
  58. package/src/storage/schema.ts +0 -1
  59. package/src/tui/__tests__/input-intent.test.ts +0 -27
  60. package/src/tui/__tests__/stream-assembler.test.ts +0 -33
  61. package/src/tui/ai-stream.ts +0 -28
  62. package/src/tui/app.tsx +0 -210
  63. package/src/tui/commands.ts +0 -220
  64. package/src/tui/context/exit.tsx +0 -15
  65. package/src/tui/context/helper.tsx +0 -25
  66. package/src/tui/context/route.tsx +0 -24
  67. package/src/tui/context/theme.tsx +0 -471
  68. package/src/tui/input-intent.ts +0 -26
  69. package/src/tui/routes/home.tsx +0 -1060
  70. package/src/tui/routes/model.tsx +0 -210
  71. package/src/tui/routes/notifications.tsx +0 -87
  72. package/src/tui/routes/post.tsx +0 -102
  73. package/src/tui/routes/search.tsx +0 -105
  74. package/src/tui/routes/setup.tsx +0 -267
  75. package/src/tui/routes/trending.tsx +0 -107
  76. package/src/tui/stream-assembler.ts +0 -49
  77. package/src/util/__tests__/context.test.ts +0 -31
  78. package/src/util/__tests__/lazy.test.ts +0 -37
  79. package/src/util/context.ts +0 -23
  80. package/src/util/error.ts +0 -46
  81. package/src/util/lazy.ts +0 -18
  82. package/src/util/log.ts +0 -144
  83. package/tsconfig.json +0 -11
@@ -1,238 +0,0 @@
1
- import fs from "fs/promises"
2
- import os from "os"
3
- import path from "path"
4
- import { describe, test, expect, beforeAll, beforeEach, afterEach, afterAll } from "bun:test"
5
- import { Config } from "../../config"
6
-
7
- describe("AIProvider", () => {
8
- const originalEnv = { ...process.env }
9
- const testHome = path.join(os.tmpdir(), `codeblog-provider-test-${process.pid}-${Date.now()}`)
10
- const configFile = path.join(testHome, ".config", "codeblog", "config.json")
11
- const xdgData = path.join(testHome, ".local", "share")
12
- const xdgCache = path.join(testHome, ".cache")
13
- const xdgConfig = path.join(testHome, ".config")
14
- const xdgState = path.join(testHome, ".local", "state")
15
- const envKeys = [
16
- "ANTHROPIC_API_KEY",
17
- "ANTHROPIC_AUTH_TOKEN",
18
- "OPENAI_API_KEY",
19
- "GOOGLE_GENERATIVE_AI_API_KEY",
20
- "GOOGLE_API_KEY",
21
- "OPENAI_COMPATIBLE_API_KEY",
22
- "ANTHROPIC_BASE_URL",
23
- "OPENAI_BASE_URL",
24
- "OPENAI_API_BASE",
25
- "GOOGLE_API_BASE_URL",
26
- "OPENAI_COMPATIBLE_BASE_URL",
27
- ]
28
- let AIProvider: (typeof import("../provider"))["AIProvider"]
29
-
30
- beforeAll(async () => {
31
- process.env.CODEBLOG_TEST_HOME = testHome
32
- process.env.XDG_DATA_HOME = xdgData
33
- process.env.XDG_CACHE_HOME = xdgCache
34
- process.env.XDG_CONFIG_HOME = xdgConfig
35
- process.env.XDG_STATE_HOME = xdgState
36
- process.env.CODEBLOG_AI_PROVIDER_REGISTRY_V2 = "0"
37
- await fs.mkdir(path.dirname(configFile), { recursive: true })
38
- await fs.writeFile(configFile, "{}\n")
39
- ;({ AIProvider } = await import("../provider"))
40
- })
41
-
42
- beforeEach(async () => {
43
- process.env.CODEBLOG_TEST_HOME = testHome
44
- process.env.XDG_DATA_HOME = xdgData
45
- process.env.XDG_CACHE_HOME = xdgCache
46
- process.env.XDG_CONFIG_HOME = xdgConfig
47
- process.env.XDG_STATE_HOME = xdgState
48
- process.env.CODEBLOG_AI_PROVIDER_REGISTRY_V2 = "0"
49
- for (const key of envKeys) delete process.env[key]
50
- await fs.writeFile(configFile, "{}\n")
51
- })
52
-
53
- afterEach(() => {
54
- for (const key of Object.keys(process.env)) {
55
- if (!(key in originalEnv)) delete process.env[key]
56
- }
57
- for (const [key, val] of Object.entries(originalEnv)) {
58
- if (val !== undefined) process.env[key] = val
59
- }
60
- })
61
-
62
- afterAll(async () => {
63
- await fs.rm(testHome, { recursive: true, force: true })
64
- })
65
-
66
- // ---------------------------------------------------------------------------
67
- // BUILTIN_MODELS
68
- // ---------------------------------------------------------------------------
69
-
70
- test("BUILTIN_MODELS has 7 models", () => {
71
- expect(Object.keys(AIProvider.BUILTIN_MODELS)).toHaveLength(7)
72
- })
73
-
74
- test("each model has required fields", () => {
75
- for (const [id, model] of Object.entries(AIProvider.BUILTIN_MODELS)) {
76
- expect(model.id).toBe(id)
77
- expect(model.providerID).toBeTruthy()
78
- expect(model.name).toBeTruthy()
79
- expect(model.contextWindow).toBeGreaterThan(0)
80
- expect(model.outputTokens).toBeGreaterThan(0)
81
- }
82
- })
83
-
84
- test("DEFAULT_MODEL is a valid builtin model", () => {
85
- expect(AIProvider.BUILTIN_MODELS[AIProvider.DEFAULT_MODEL]).toBeDefined()
86
- })
87
-
88
- test("all providers are covered: anthropic, openai, google", () => {
89
- const providerIDs = new Set(Object.values(AIProvider.BUILTIN_MODELS).map((m) => m.providerID))
90
- expect(providerIDs.has("anthropic")).toBe(true)
91
- expect(providerIDs.has("openai")).toBe(true)
92
- expect(providerIDs.has("google")).toBe(true)
93
- })
94
-
95
- // ---------------------------------------------------------------------------
96
- // getApiKey
97
- // ---------------------------------------------------------------------------
98
-
99
- test("getApiKey returns env var when set", async () => {
100
- process.env.ANTHROPIC_API_KEY = "sk-ant-test123"
101
- const key = await AIProvider.getApiKey("anthropic")
102
- expect(key).toBe("sk-ant-test123")
103
- })
104
-
105
- test("getApiKey checks secondary env var", async () => {
106
- process.env.ANTHROPIC_AUTH_TOKEN = "token-test"
107
- const key = await AIProvider.getApiKey("anthropic")
108
- expect(key).toBe("token-test")
109
- })
110
-
111
- test("getApiKey returns undefined when no key set", async () => {
112
- const key = await AIProvider.getApiKey("anthropic")
113
- // May return undefined or a config value — just check it doesn't crash
114
- expect(key === undefined || typeof key === "string").toBe(true)
115
- })
116
-
117
- test("getApiKey works for openai", async () => {
118
- process.env.OPENAI_API_KEY = "sk-openai-test"
119
- const key = await AIProvider.getApiKey("openai")
120
- expect(key).toBe("sk-openai-test")
121
- })
122
-
123
- test("getApiKey works for google", async () => {
124
- process.env.GOOGLE_GENERATIVE_AI_API_KEY = "aiza-test"
125
- const key = await AIProvider.getApiKey("google")
126
- expect(key).toBe("aiza-test")
127
- })
128
-
129
- // ---------------------------------------------------------------------------
130
- // getBaseUrl
131
- // ---------------------------------------------------------------------------
132
-
133
- test("getBaseUrl returns env var when set", async () => {
134
- process.env.ANTHROPIC_BASE_URL = "https://custom.api.com"
135
- const url = await AIProvider.getBaseUrl("anthropic")
136
- expect(url).toBe("https://custom.api.com")
137
- })
138
-
139
- test("getBaseUrl returns undefined when no env var set", async () => {
140
- const url = await AIProvider.getBaseUrl("anthropic")
141
- expect(url === undefined || typeof url === "string").toBe(true)
142
- })
143
-
144
- // ---------------------------------------------------------------------------
145
- // hasAnyKey
146
- // ---------------------------------------------------------------------------
147
-
148
- test("hasAnyKey returns true when any key is set", async () => {
149
- process.env.OPENAI_API_KEY = "sk-test"
150
- const has = await AIProvider.hasAnyKey()
151
- expect(has).toBe(true)
152
- })
153
-
154
- // ---------------------------------------------------------------------------
155
- // parseModel
156
- // ---------------------------------------------------------------------------
157
-
158
- test("parseModel splits provider/model", () => {
159
- const result = AIProvider.parseModel("anthropic/claude-sonnet-4-20250514")
160
- expect(result.providerID).toBe("anthropic")
161
- expect(result.modelID).toBe("claude-sonnet-4-20250514")
162
- })
163
-
164
- test("parseModel handles nested slashes", () => {
165
- const result = AIProvider.parseModel("openai/gpt-4o/latest")
166
- expect(result.providerID).toBe("openai")
167
- expect(result.modelID).toBe("gpt-4o/latest")
168
- })
169
-
170
- test("parseModel handles no slash", () => {
171
- const result = AIProvider.parseModel("gpt-4o")
172
- expect(result.providerID).toBe("gpt-4o")
173
- expect(result.modelID).toBe("")
174
- })
175
-
176
- // ---------------------------------------------------------------------------
177
- // available
178
- // ---------------------------------------------------------------------------
179
-
180
- test("available returns at least builtin models with hasKey status", async () => {
181
- const models = await AIProvider.available()
182
- expect(models.length).toBeGreaterThanOrEqual(7)
183
- for (const entry of models) {
184
- expect(entry.model).toBeDefined()
185
- expect(typeof entry.hasKey).toBe("boolean")
186
- }
187
- // The first 7 should always be builtins
188
- const builtinCount = models.filter((m) => AIProvider.BUILTIN_MODELS[m.model.id]).length
189
- expect(builtinCount).toBe(7)
190
- })
191
-
192
- test("available includes openai-compatible remote models when configured", async () => {
193
- // With a valid key and base URL from config, remote models should be fetched.
194
- // This test verifies that available() attempts to include openai-compatible models.
195
- // Use localhost to ensure fast failure (connection refused) instead of DNS timeout.
196
- process.env.OPENAI_COMPATIBLE_API_KEY = "sk-test"
197
- process.env.OPENAI_COMPATIBLE_BASE_URL = "http://127.0.0.1:1"
198
- const models = await AIProvider.available()
199
- // Should still return at least builtins even if remote fetch fails
200
- expect(models.length).toBeGreaterThanOrEqual(7)
201
- })
202
-
203
- // ---------------------------------------------------------------------------
204
- // getModel
205
- // ---------------------------------------------------------------------------
206
-
207
- test("getModel throws when no API key for builtin model", async () => {
208
- const load = Config.load
209
- Config.load = async () => ({ api_url: "https://codeblog.ai" })
210
- try {
211
- await expect(AIProvider.getModel("gpt-4o")).rejects.toThrow("No API key for openai")
212
- } finally {
213
- Config.load = load
214
- }
215
- })
216
-
217
- test("getModel falls back to provider with base_url for unknown model", async () => {
218
- // When a provider with base_url is configured, unknown models get sent there
219
- // instead of throwing. This test verifies the fallback behavior.
220
- // If no provider has a base_url, it would throw.
221
- const result = AIProvider.getModel("nonexistent-model-xyz")
222
- // Either resolves (provider with base_url available) or rejects
223
- const settled = await Promise.allSettled([result])
224
- expect(settled[0]!.status).toBeDefined()
225
- })
226
-
227
- // ---------------------------------------------------------------------------
228
- // listProviders
229
- // ---------------------------------------------------------------------------
230
-
231
- test("listProviders returns provider info", async () => {
232
- process.env.OPENAI_API_KEY = "sk-test"
233
- const providers = await AIProvider.listProviders()
234
- expect(providers.openai).toBeDefined()
235
- expect(providers.openai!.hasKey).toBe(true)
236
- expect(providers.openai!.models.length).toBeGreaterThan(0)
237
- })
238
- })
@@ -1,152 +0,0 @@
1
- import { describe, test, expect, beforeEach, mock } from "bun:test"
2
-
3
- const mockListTools = mock(async () => ({ tools: [] }))
4
-
5
- mock.module("../../mcp/client", () => ({
6
- McpBridge: {
7
- listTools: mockListTools,
8
- callToolJSON: mock(async () => ({})),
9
- },
10
- }))
11
-
12
- let streamFactory: () => { fullStream: AsyncGenerator<any, void, unknown> } = () => ({
13
- fullStream: (async function* () {
14
- yield { type: "text-delta", textDelta: "hello" }
15
- })(),
16
- })
17
-
18
- mock.module("ai", () => ({
19
- streamText: () => streamFactory(),
20
- stepCountIs: (n: number) => ({ type: "step-count", count: n }),
21
- tool: (config: any) => config,
22
- jsonSchema: (schema: any) => schema,
23
- }))
24
-
25
- mock.module("../provider", () => ({
26
- AIProvider: {
27
- getModel: mock(async () => ({ id: "test-model" })),
28
- resolveModelCompat: mock(async () => ({
29
- providerID: "openai-compatible",
30
- modelID: "test-model",
31
- api: "openai-compatible",
32
- compatProfile: "openai-compatible",
33
- cacheKey: "openai-compatible:openai-compatible",
34
- stripParallelToolCalls: true,
35
- normalizeToolSchema: true,
36
- })),
37
- DEFAULT_MODEL: "test-model",
38
- },
39
- }))
40
-
41
- const { AIChat } = await import("../chat")
42
-
43
- describe("stream events", () => {
44
- beforeEach(() => {
45
- mockListTools.mockClear()
46
- })
47
-
48
- test("emits ordered run-start -> deltas -> run-finish sequence", async () => {
49
- streamFactory = () => ({
50
- fullStream: (async function* () {
51
- yield { type: "text-delta", textDelta: "Hello " }
52
- yield { type: "text-delta", textDelta: "World" }
53
- })(),
54
- })
55
-
56
- const events: any[] = []
57
- for await (const event of AIChat.streamEvents([{ role: "user", content: "hi" }])) {
58
- events.push(event)
59
- }
60
-
61
- expect(events.map((e: any) => e.type)).toEqual(["run-start", "text-delta", "text-delta", "run-finish"])
62
- expect(events.every((e: any) => e.runId === events[0]!.runId)).toBe(true)
63
- expect(events.map((e: any) => e.seq)).toEqual([1, 2, 3, 4])
64
- })
65
-
66
- test("tool-start and tool-result are paired", async () => {
67
- streamFactory = () => ({
68
- fullStream: (async function* () {
69
- yield { type: "tool-call", toolName: "scan_sessions", args: { limit: 5 } }
70
- yield { type: "tool-result", toolName: "scan_sessions", result: { sessions: [] } }
71
- yield { type: "text-delta", textDelta: "done" }
72
- })(),
73
- })
74
-
75
- const starts: string[] = []
76
- const results: string[] = []
77
- const ids: Array<{ start?: string; result?: string }> = []
78
- for await (const event of AIChat.streamEvents([{ role: "user", content: "scan" }])) {
79
- if (event.type === "tool-start") {
80
- starts.push(event.name)
81
- ids.push({ start: event.callID })
82
- }
83
- if (event.type === "tool-result") {
84
- results.push(event.name)
85
- ids[0] = { ...ids[0], result: event.callID }
86
- }
87
- }
88
-
89
- expect(starts).toEqual(["scan_sessions"])
90
- expect(results).toEqual(["scan_sessions"])
91
- expect(ids[0]?.start).toBe(ids[0]?.result)
92
- })
93
-
94
- test("abort keeps lifecycle consistent and marks run-finish.aborted", async () => {
95
- streamFactory = () => ({
96
- fullStream: (async function* () {
97
- yield { type: "text-delta", textDelta: "partial" }
98
- await Bun.sleep(40)
99
- yield { type: "text-delta", textDelta: " late" }
100
- })(),
101
- })
102
-
103
- const ctrl = new AbortController()
104
- let seenFinish = false
105
- for await (const event of AIChat.streamEvents([{ role: "user", content: "stop test" }], undefined, ctrl.signal)) {
106
- if (event.type === "text-delta") ctrl.abort()
107
- if (event.type === "run-finish") {
108
- seenFinish = true
109
- expect(event.aborted).toBe(true)
110
- }
111
- }
112
-
113
- expect(seenFinish).toBe(true)
114
- })
115
-
116
- test("error part is surfaced and lifecycle still finishes", async () => {
117
- streamFactory = () => ({
118
- fullStream: (async function* () {
119
- yield { type: "error", error: new Error("boom") }
120
- })(),
121
- })
122
-
123
- const types: string[] = []
124
- for await (const event of AIChat.streamEvents([{ role: "user", content: "error path" }])) {
125
- types.push(event.type)
126
- if (event.type === "error") expect(event.error.message).toBe("boom")
127
- }
128
-
129
- expect(types).toContain("error")
130
- expect(types[types.length - 1]).toBe("run-finish")
131
- })
132
-
133
- test("tool timeout emits error and still reaches run-finish", async () => {
134
- streamFactory = () => ({
135
- fullStream: (async function* () {
136
- yield { type: "tool-call", toolName: "scan_sessions", args: { limit: 1 } }
137
- await Bun.sleep(40)
138
- })(),
139
- })
140
-
141
- const seen: string[] = []
142
- for await (const event of AIChat.streamEvents([{ role: "user", content: "scan" }], undefined, undefined, {
143
- toolTimeoutMs: 15,
144
- idleTimeoutMs: 1000,
145
- })) {
146
- seen.push(event.type)
147
- }
148
-
149
- expect(seen).toContain("error")
150
- expect(seen[seen.length - 1]).toBe("run-finish")
151
- })
152
- })
@@ -1,93 +0,0 @@
1
- import { describe, test, expect, mock } from "bun:test"
2
-
3
- // Mock MCP bridge so tests don't need a running MCP server
4
- const MOCK_MCP_TOOLS = [
5
- {
6
- name: "scan_sessions",
7
- description: "Scan IDE sessions for coding activity",
8
- inputSchema: { type: "object", properties: { limit: { type: "number" } } },
9
- },
10
- {
11
- name: "my_dashboard",
12
- description: "Show the user dashboard with stats",
13
- inputSchema: {},
14
- },
15
- {
16
- name: "browse_posts",
17
- description: "Browse posts on the forum with filters",
18
- inputSchema: { type: "object", properties: { page: { type: "number" }, tag: { type: "string" } } },
19
- },
20
- ]
21
-
22
- mock.module("../../mcp/client", () => ({
23
- McpBridge: {
24
- listTools: mock(() => Promise.resolve({ tools: MOCK_MCP_TOOLS })),
25
- callToolJSON: mock((name: string) => Promise.resolve({ ok: true, tool: name })),
26
- },
27
- }))
28
-
29
- mock.module("ai", () => ({
30
- tool: (config: any) => config,
31
- jsonSchema: (schema: any) => schema,
32
- }))
33
-
34
- const { getChatTools, TOOL_LABELS, clearChatToolsCache } = await import("../tools")
35
-
36
- describe("AI Tools (dynamic MCP discovery)", () => {
37
- let chatTools: Record<string, any>
38
-
39
- test("getChatTools() discovers tools from MCP server", async () => {
40
- clearChatToolsCache()
41
- chatTools = await getChatTools()
42
- const names = Object.keys(chatTools)
43
- expect(names.length).toBe(MOCK_MCP_TOOLS.length)
44
- expect(names).toContain("scan_sessions")
45
- expect(names).toContain("my_dashboard")
46
- expect(names).toContain("browse_posts")
47
- })
48
-
49
- test("each tool has inputSchema and execute", () => {
50
- for (const [name, t] of Object.entries(chatTools)) {
51
- const tool = t as any
52
- expect(tool.inputSchema).toBeDefined()
53
- expect(tool.execute).toBeDefined()
54
- expect(typeof tool.execute).toBe("function")
55
- }
56
- })
57
-
58
- test("each tool has a description", () => {
59
- for (const [name, t] of Object.entries(chatTools)) {
60
- const tool = t as any
61
- expect(tool.description).toBeDefined()
62
- expect(typeof tool.description).toBe("string")
63
- expect(tool.description.length).toBeGreaterThan(10)
64
- }
65
- })
66
-
67
- test("normalizeToolSchema adds type:object to empty schemas", async () => {
68
- // my_dashboard has empty inputSchema {} — should be normalized
69
- const dashboard = chatTools["my_dashboard"] as any
70
- expect(dashboard.inputSchema.type).toBe("object")
71
- expect(dashboard.inputSchema.properties).toEqual({})
72
- })
73
-
74
- // ---------------------------------------------------------------------------
75
- // TOOL_LABELS tests (static fallback map)
76
- // ---------------------------------------------------------------------------
77
-
78
- test("TOOL_LABELS values are non-empty strings", () => {
79
- for (const [key, label] of Object.entries(TOOL_LABELS)) {
80
- expect(label.length).toBeGreaterThan(0)
81
- }
82
- })
83
-
84
- // ---------------------------------------------------------------------------
85
- // Caching
86
- // ---------------------------------------------------------------------------
87
-
88
- test("getChatTools() returns cached result on second call", async () => {
89
- const tools1 = await getChatTools()
90
- const tools2 = await getChatTools()
91
- expect(tools1).toBe(tools2) // same reference
92
- })
93
- })