mastracode 0.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/AGENTS.md +27 -0
- package/Dockerfile +18 -0
- package/README.md +15 -0
- package/bin/opencode +84 -0
- package/bunfig.toml +4 -0
- package/package.json +113 -0
- package/parsers-config.ts +239 -0
- package/script/build.ts +167 -0
- package/script/postinstall.mjs +122 -0
- package/script/publish-registries.ts +187 -0
- package/script/publish.ts +70 -0
- package/script/schema.ts +47 -0
- package/src/acp/README.md +164 -0
- package/src/acp/agent.ts +1051 -0
- package/src/acp/session.ts +101 -0
- package/src/acp/types.ts +22 -0
- package/src/agent/agent.ts +398 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +10 -0
- package/src/agent/prompt/title.txt +36 -0
- package/src/auth/index.ts +70 -0
- package/src/bun/index.ts +114 -0
- package/src/bus/bus-event.ts +43 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +105 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/acp.ts +88 -0
- package/src/cli/cmd/agent.ts +256 -0
- package/src/cli/cmd/auth.ts +391 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/debug/config.ts +15 -0
- package/src/cli/cmd/debug/file.ts +91 -0
- package/src/cli/cmd/debug/index.ts +43 -0
- package/src/cli/cmd/debug/lsp.ts +48 -0
- package/src/cli/cmd/debug/ripgrep.ts +83 -0
- package/src/cli/cmd/debug/scrap.ts +15 -0
- package/src/cli/cmd/debug/skill.ts +15 -0
- package/src/cli/cmd/debug/snapshot.ts +48 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/generate.ts +38 -0
- package/src/cli/cmd/github.ts +1408 -0
- package/src/cli/cmd/import.ts +98 -0
- package/src/cli/cmd/mcp.ts +654 -0
- package/src/cli/cmd/models.ts +77 -0
- package/src/cli/cmd/pr.ts +112 -0
- package/src/cli/cmd/run.ts +368 -0
- package/src/cli/cmd/serve.ts +31 -0
- package/src/cli/cmd/session.ts +106 -0
- package/src/cli/cmd/stats.ts +298 -0
- package/src/cli/cmd/tui/app.tsx +686 -0
- package/src/cli/cmd/tui/attach.ts +30 -0
- package/src/cli/cmd/tui/component/border.tsx +21 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-command.tsx +124 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +230 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +224 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +102 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-stash.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +162 -0
- package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/did-you-know.tsx +85 -0
- package/src/cli/cmd/tui/component/logo.tsx +27 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +574 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +1117 -0
- package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
- package/src/cli/cmd/tui/component/tips.ts +103 -0
- package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
- package/src/cli/cmd/tui/context/args.tsx +14 -0
- package/src/cli/cmd/tui/context/directory.ts +13 -0
- package/src/cli/cmd/tui/context/exit.tsx +23 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +101 -0
- package/src/cli/cmd/tui/context/kv.tsx +49 -0
- package/src/cli/cmd/tui/context/local.tsx +339 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +46 -0
- package/src/cli/cmd/tui/context/sdk.tsx +74 -0
- package/src/cli/cmd/tui/context/sync.tsx +372 -0
- package/src/cli/cmd/tui/context/theme/aura.json +69 -0
- package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
- package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
- package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
- package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
- package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
- package/src/cli/cmd/tui/context/theme/github.json +233 -0
- package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
- package/src/cli/cmd/tui/context/theme/lucent-orng.json +227 -0
- package/src/cli/cmd/tui/context/theme/material.json +235 -0
- package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
- package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
- package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
- package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
- package/src/cli/cmd/tui/context/theme/nord.json +223 -0
- package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
- package/src/cli/cmd/tui/context/theme/orng.json +245 -0
- package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
- package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
- package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
- package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
- package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
- package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
- package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
- package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
- package/src/cli/cmd/tui/context/theme.tsx +1109 -0
- package/src/cli/cmd/tui/event.ts +40 -0
- package/src/cli/cmd/tui/routes/home.tsx +140 -0
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/footer.tsx +88 -0
- package/src/cli/cmd/tui/routes/session/header.tsx +141 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +1885 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +322 -0
- package/src/cli/cmd/tui/spawn.ts +60 -0
- package/src/cli/cmd/tui/thread.ts +120 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +332 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
- package/src/cli/cmd/tui/ui/spinner.ts +368 -0
- package/src/cli/cmd/tui/ui/toast.tsx +100 -0
- package/src/cli/cmd/tui/util/clipboard.ts +127 -0
- package/src/cli/cmd/tui/util/editor.ts +32 -0
- package/src/cli/cmd/tui/util/terminal.ts +114 -0
- package/src/cli/cmd/tui/worker.ts +63 -0
- package/src/cli/cmd/uninstall.ts +344 -0
- package/src/cli/cmd/upgrade.ts +67 -0
- package/src/cli/cmd/web.ts +84 -0
- package/src/cli/error.ts +56 -0
- package/src/cli/ui.ts +84 -0
- package/src/cli/upgrade.ts +25 -0
- package/src/command/index.ts +80 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/command/template/review.txt +97 -0
- package/src/config/config.ts +997 -0
- package/src/config/markdown.ts +41 -0
- package/src/env/index.ts +26 -0
- package/src/file/ignore.ts +83 -0
- package/src/file/index.ts +328 -0
- package/src/file/ripgrep.ts +393 -0
- package/src/file/time.ts +64 -0
- package/src/file/watcher.ts +103 -0
- package/src/flag/flag.ts +46 -0
- package/src/format/formatter.ts +315 -0
- package/src/format/index.ts +137 -0
- package/src/global/index.ts +52 -0
- package/src/id/id.ts +73 -0
- package/src/ide/index.ts +76 -0
- package/src/index.ts +158 -0
- package/src/installation/index.ts +196 -0
- package/src/lsp/client.ts +229 -0
- package/src/lsp/index.ts +485 -0
- package/src/lsp/language.ts +116 -0
- package/src/lsp/server.ts +1895 -0
- package/src/mcp/auth.ts +135 -0
- package/src/mcp/index.ts +654 -0
- package/src/mcp/oauth-callback.ts +200 -0
- package/src/mcp/oauth-provider.ts +154 -0
- package/src/patch/index.ts +622 -0
- package/src/permission/index.ts +199 -0
- package/src/plugin/index.ts +91 -0
- package/src/project/bootstrap.ts +31 -0
- package/src/project/instance.ts +78 -0
- package/src/project/project.ts +221 -0
- package/src/project/state.ts +65 -0
- package/src/project/vcs.ts +76 -0
- package/src/provider/auth.ts +143 -0
- package/src/provider/models-macro.ts +11 -0
- package/src/provider/models.ts +106 -0
- package/src/provider/provider.ts +1056 -0
- package/src/provider/sdk/openai-compatible/src/README.md +5 -0
- package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
- package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
- package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
- package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
- package/src/provider/transform.ts +455 -0
- package/src/pty/index.ts +231 -0
- package/src/server/error.ts +36 -0
- package/src/server/project.ts +79 -0
- package/src/server/server.ts +2642 -0
- package/src/server/tui.ts +71 -0
- package/src/session/compaction.ts +223 -0
- package/src/session/index.ts +458 -0
- package/src/session/llm-mastra.ts +412 -0
- package/src/session/llm-shared.ts +172 -0
- package/src/session/llm.ts +439 -0
- package/src/session/message-v2.ts +675 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +171 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +105 -0
- package/src/session/prompt/anthropic_spoof.txt +1 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex.txt +318 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/src/session/prompt/plan.txt +26 -0
- package/src/session/prompt/polaris.txt +107 -0
- package/src/session/prompt/qwen.txt +109 -0
- package/src/session/prompt.ts +1454 -0
- package/src/session/retry.ts +86 -0
- package/src/session/revert.ts +108 -0
- package/src/session/status.ts +76 -0
- package/src/session/summary.ts +194 -0
- package/src/session/system.ts +120 -0
- package/src/session/todo.ts +37 -0
- package/src/share/share-next.ts +194 -0
- package/src/share/share.ts +87 -0
- package/src/shell/shell.ts +67 -0
- package/src/skill/index.ts +1 -0
- package/src/skill/skill.ts +83 -0
- package/src/snapshot/index.ts +197 -0
- package/src/storage/storage.ts +226 -0
- package/src/tool/bash.ts +306 -0
- package/src/tool/bash.txt +158 -0
- package/src/tool/batch.ts +175 -0
- package/src/tool/batch.txt +24 -0
- package/src/tool/codesearch.ts +138 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +675 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/glob.ts +65 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +121 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +110 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/lsp-diagnostics.ts +26 -0
- package/src/tool/lsp-diagnostics.txt +1 -0
- package/src/tool/lsp-hover.ts +31 -0
- package/src/tool/lsp-hover.txt +1 -0
- package/src/tool/lsp.ts +87 -0
- package/src/tool/lsp.txt +19 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/patch.ts +233 -0
- package/src/tool/patch.txt +1 -0
- package/src/tool/read.ts +219 -0
- package/src/tool/read.txt +12 -0
- package/src/tool/registry.ts +162 -0
- package/src/tool/skill.ts +100 -0
- package/src/tool/task.ts +136 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +39 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +71 -0
- package/src/tool/webfetch.ts +187 -0
- package/src/tool/webfetch.txt +13 -0
- package/src/tool/websearch.ts +150 -0
- package/src/tool/websearch.txt +11 -0
- package/src/tool/write.ts +110 -0
- package/src/tool/write.txt +8 -0
- package/src/util/archive.ts +16 -0
- package/src/util/color.ts +19 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +83 -0
- package/src/util/fn.ts +11 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +102 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/locale.ts +81 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +180 -0
- package/src/util/queue.ts +32 -0
- package/src/util/rpc.ts +42 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +54 -0
- package/sst-env.d.ts +9 -0
- package/test/agent/agent.test.ts +146 -0
- package/test/bun.test.ts +53 -0
- package/test/cli/github-remote.test.ts +80 -0
- package/test/config/agent-color.test.ts +66 -0
- package/test/config/config.test.ts +535 -0
- package/test/config/markdown.test.ts +89 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/fixture/fixture.ts +34 -0
- package/test/fixture/lsp/fake-lsp-server.js +77 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/keybind.test.ts +421 -0
- package/test/lsp/client.test.ts +95 -0
- package/test/mcp/headers.test.ts +153 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/preload.ts +57 -0
- package/test/project/project.test.ts +72 -0
- package/test/provider/provider.test.ts +1809 -0
- package/test/provider/transform.test.ts +411 -0
- package/test/session/retry.test.ts +61 -0
- package/test/session/session.test.ts +71 -0
- package/test/skill/skill.test.ts +131 -0
- package/test/snapshot/snapshot.test.ts +939 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/bash.test.ts +434 -0
- package/test/tool/grep.test.ts +108 -0
- package/test/tool/patch.test.ts +259 -0
- package/test/tool/read.test.ts +42 -0
- package/test/util/iife.test.ts +36 -0
- package/test/util/lazy.test.ts +50 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/wildcard.test.ts +55 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { ProviderTransform } from "../../src/provider/transform"
|
|
3
|
+
|
|
4
|
+
const OUTPUT_TOKEN_MAX = 32000
|
|
5
|
+
|
|
6
|
+
describe("ProviderTransform.options - setCacheKey", () => {
|
|
7
|
+
const sessionID = "test-session-123"
|
|
8
|
+
|
|
9
|
+
const mockModel = {
|
|
10
|
+
id: "anthropic/claude-3-5-sonnet",
|
|
11
|
+
providerID: "anthropic",
|
|
12
|
+
api: {
|
|
13
|
+
id: "claude-3-5-sonnet-20241022",
|
|
14
|
+
url: "https://api.anthropic.com",
|
|
15
|
+
npm: "@ai-sdk/anthropic",
|
|
16
|
+
},
|
|
17
|
+
name: "Claude 3.5 Sonnet",
|
|
18
|
+
capabilities: {
|
|
19
|
+
temperature: true,
|
|
20
|
+
reasoning: false,
|
|
21
|
+
attachment: true,
|
|
22
|
+
toolcall: true,
|
|
23
|
+
input: { text: true, audio: false, image: true, video: false, pdf: true },
|
|
24
|
+
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
25
|
+
interleaved: false,
|
|
26
|
+
},
|
|
27
|
+
cost: {
|
|
28
|
+
input: 0.003,
|
|
29
|
+
output: 0.015,
|
|
30
|
+
cache: { read: 0.0003, write: 0.00375 },
|
|
31
|
+
},
|
|
32
|
+
limit: {
|
|
33
|
+
context: 200000,
|
|
34
|
+
output: 8192,
|
|
35
|
+
},
|
|
36
|
+
status: "active",
|
|
37
|
+
options: {},
|
|
38
|
+
headers: {},
|
|
39
|
+
} as any
|
|
40
|
+
|
|
41
|
+
test("should set promptCacheKey when providerOptions.setCacheKey is true", () => {
|
|
42
|
+
const result = ProviderTransform.options(mockModel, sessionID, { setCacheKey: true })
|
|
43
|
+
expect(result.promptCacheKey).toBe(sessionID)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("should not set promptCacheKey when providerOptions.setCacheKey is false", () => {
|
|
47
|
+
const result = ProviderTransform.options(mockModel, sessionID, { setCacheKey: false })
|
|
48
|
+
expect(result.promptCacheKey).toBeUndefined()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("should not set promptCacheKey when providerOptions is undefined", () => {
|
|
52
|
+
const result = ProviderTransform.options(mockModel, sessionID, undefined)
|
|
53
|
+
expect(result.promptCacheKey).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("should not set promptCacheKey when providerOptions does not have setCacheKey", () => {
|
|
57
|
+
const result = ProviderTransform.options(mockModel, sessionID, {})
|
|
58
|
+
expect(result.promptCacheKey).toBeUndefined()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test("should set promptCacheKey for openai provider regardless of setCacheKey", () => {
|
|
62
|
+
const openaiModel = {
|
|
63
|
+
...mockModel,
|
|
64
|
+
providerID: "openai",
|
|
65
|
+
api: {
|
|
66
|
+
id: "gpt-4",
|
|
67
|
+
url: "https://api.openai.com",
|
|
68
|
+
npm: "@ai-sdk/openai",
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
const result = ProviderTransform.options(openaiModel, sessionID, {})
|
|
72
|
+
expect(result.promptCacheKey).toBe(sessionID)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe("ProviderTransform.maxOutputTokens", () => {
|
|
77
|
+
test("returns 32k when modelLimit > 32k", () => {
|
|
78
|
+
const modelLimit = 100000
|
|
79
|
+
const result = ProviderTransform.maxOutputTokens("@ai-sdk/openai", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
80
|
+
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test("returns modelLimit when modelLimit < 32k", () => {
|
|
84
|
+
const modelLimit = 16000
|
|
85
|
+
const result = ProviderTransform.maxOutputTokens("@ai-sdk/openai", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
86
|
+
expect(result).toBe(16000)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe("azure", () => {
|
|
90
|
+
test("returns 32k when modelLimit > 32k", () => {
|
|
91
|
+
const modelLimit = 100000
|
|
92
|
+
const result = ProviderTransform.maxOutputTokens("@ai-sdk/azure", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
93
|
+
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("returns modelLimit when modelLimit < 32k", () => {
|
|
97
|
+
const modelLimit = 16000
|
|
98
|
+
const result = ProviderTransform.maxOutputTokens("@ai-sdk/azure", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
99
|
+
expect(result).toBe(16000)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe("bedrock", () => {
|
|
104
|
+
test("returns 32k when modelLimit > 32k", () => {
|
|
105
|
+
const modelLimit = 100000
|
|
106
|
+
const result = ProviderTransform.maxOutputTokens("@ai-sdk/amazon-bedrock", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
107
|
+
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test("returns modelLimit when modelLimit < 32k", () => {
|
|
111
|
+
const modelLimit = 16000
|
|
112
|
+
const result = ProviderTransform.maxOutputTokens("@ai-sdk/amazon-bedrock", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
113
|
+
expect(result).toBe(16000)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe("anthropic without thinking options", () => {
|
|
118
|
+
test("returns 32k when modelLimit > 32k", () => {
|
|
119
|
+
const modelLimit = 100000
|
|
120
|
+
const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
121
|
+
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test("returns modelLimit when modelLimit < 32k", () => {
|
|
125
|
+
const modelLimit = 16000
|
|
126
|
+
const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
127
|
+
expect(result).toBe(16000)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe("anthropic with thinking options", () => {
|
|
132
|
+
test("returns 32k when budgetTokens + 32k <= modelLimit", () => {
|
|
133
|
+
const modelLimit = 100000
|
|
134
|
+
const options = {
|
|
135
|
+
thinking: {
|
|
136
|
+
type: "enabled",
|
|
137
|
+
budgetTokens: 10000,
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", options, modelLimit, OUTPUT_TOKEN_MAX)
|
|
141
|
+
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test("returns modelLimit - budgetTokens when budgetTokens + 32k > modelLimit", () => {
|
|
145
|
+
const modelLimit = 50000
|
|
146
|
+
const options = {
|
|
147
|
+
thinking: {
|
|
148
|
+
type: "enabled",
|
|
149
|
+
budgetTokens: 30000,
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", options, modelLimit, OUTPUT_TOKEN_MAX)
|
|
153
|
+
expect(result).toBe(20000)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test("returns 32k when thinking type is not enabled", () => {
|
|
157
|
+
const modelLimit = 100000
|
|
158
|
+
const options = {
|
|
159
|
+
thinking: {
|
|
160
|
+
type: "disabled",
|
|
161
|
+
budgetTokens: 10000,
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", options, modelLimit, OUTPUT_TOKEN_MAX)
|
|
165
|
+
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe("ProviderTransform.schema - gemini array items", () => {
|
|
171
|
+
test("adds missing items for array properties", () => {
|
|
172
|
+
const geminiModel = {
|
|
173
|
+
providerID: "google",
|
|
174
|
+
api: {
|
|
175
|
+
id: "gemini-3-pro",
|
|
176
|
+
},
|
|
177
|
+
} as any
|
|
178
|
+
|
|
179
|
+
const schema = {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
nodes: { type: "array" },
|
|
183
|
+
edges: { type: "array", items: { type: "string" } },
|
|
184
|
+
},
|
|
185
|
+
} as any
|
|
186
|
+
|
|
187
|
+
const result = ProviderTransform.schema(geminiModel, schema) as any
|
|
188
|
+
|
|
189
|
+
expect(result.properties.nodes.items).toBeDefined()
|
|
190
|
+
expect(result.properties.edges.items.type).toBe("string")
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe("ProviderTransform.message - DeepSeek reasoning content", () => {
|
|
195
|
+
test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
|
|
196
|
+
const msgs = [
|
|
197
|
+
{
|
|
198
|
+
role: "assistant",
|
|
199
|
+
content: [
|
|
200
|
+
{ type: "reasoning", text: "Let me think about this..." },
|
|
201
|
+
{
|
|
202
|
+
type: "tool-call",
|
|
203
|
+
toolCallId: "test",
|
|
204
|
+
toolName: "bash",
|
|
205
|
+
input: { command: "echo hello" },
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
] as any[]
|
|
210
|
+
|
|
211
|
+
const result = ProviderTransform.message(msgs, {
|
|
212
|
+
id: "deepseek/deepseek-chat",
|
|
213
|
+
providerID: "deepseek",
|
|
214
|
+
api: {
|
|
215
|
+
id: "deepseek-chat",
|
|
216
|
+
url: "https://api.deepseek.com",
|
|
217
|
+
npm: "@ai-sdk/openai-compatible",
|
|
218
|
+
},
|
|
219
|
+
name: "DeepSeek Chat",
|
|
220
|
+
capabilities: {
|
|
221
|
+
temperature: true,
|
|
222
|
+
reasoning: true,
|
|
223
|
+
attachment: false,
|
|
224
|
+
toolcall: true,
|
|
225
|
+
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
226
|
+
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
227
|
+
interleaved: {
|
|
228
|
+
field: "reasoning_content",
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
cost: {
|
|
232
|
+
input: 0.001,
|
|
233
|
+
output: 0.002,
|
|
234
|
+
cache: { read: 0.0001, write: 0.0002 },
|
|
235
|
+
},
|
|
236
|
+
limit: {
|
|
237
|
+
context: 128000,
|
|
238
|
+
output: 8192,
|
|
239
|
+
},
|
|
240
|
+
status: "active",
|
|
241
|
+
options: {},
|
|
242
|
+
headers: {},
|
|
243
|
+
release_date: "2023-04-01",
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
expect(result).toHaveLength(1)
|
|
247
|
+
expect(result[0].content).toEqual([
|
|
248
|
+
{
|
|
249
|
+
type: "tool-call",
|
|
250
|
+
toolCallId: "test",
|
|
251
|
+
toolName: "bash",
|
|
252
|
+
input: { command: "echo hello" },
|
|
253
|
+
},
|
|
254
|
+
])
|
|
255
|
+
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Let me think about this...")
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test("Non-DeepSeek providers leave reasoning content unchanged", () => {
|
|
259
|
+
const msgs = [
|
|
260
|
+
{
|
|
261
|
+
role: "assistant",
|
|
262
|
+
content: [
|
|
263
|
+
{ type: "reasoning", text: "Should not be processed" },
|
|
264
|
+
{ type: "text", text: "Answer" },
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
] as any[]
|
|
268
|
+
|
|
269
|
+
const result = ProviderTransform.message(msgs, {
|
|
270
|
+
id: "openai/gpt-4",
|
|
271
|
+
providerID: "openai",
|
|
272
|
+
api: {
|
|
273
|
+
id: "gpt-4",
|
|
274
|
+
url: "https://api.openai.com",
|
|
275
|
+
npm: "@ai-sdk/openai",
|
|
276
|
+
},
|
|
277
|
+
name: "GPT-4",
|
|
278
|
+
capabilities: {
|
|
279
|
+
temperature: true,
|
|
280
|
+
reasoning: false,
|
|
281
|
+
attachment: true,
|
|
282
|
+
toolcall: true,
|
|
283
|
+
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
284
|
+
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
285
|
+
interleaved: false,
|
|
286
|
+
},
|
|
287
|
+
cost: {
|
|
288
|
+
input: 0.03,
|
|
289
|
+
output: 0.06,
|
|
290
|
+
cache: { read: 0.001, write: 0.002 },
|
|
291
|
+
},
|
|
292
|
+
limit: {
|
|
293
|
+
context: 128000,
|
|
294
|
+
output: 4096,
|
|
295
|
+
},
|
|
296
|
+
status: "active",
|
|
297
|
+
options: {},
|
|
298
|
+
headers: {},
|
|
299
|
+
release_date: "2023-04-01",
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
expect(result[0].content).toEqual([
|
|
303
|
+
{ type: "reasoning", text: "Should not be processed" },
|
|
304
|
+
{ type: "text", text: "Answer" },
|
|
305
|
+
])
|
|
306
|
+
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined()
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
describe("ProviderTransform.message - empty image handling", () => {
|
|
311
|
+
const mockModel = {
|
|
312
|
+
id: "anthropic/claude-3-5-sonnet",
|
|
313
|
+
providerID: "anthropic",
|
|
314
|
+
api: {
|
|
315
|
+
id: "claude-3-5-sonnet-20241022",
|
|
316
|
+
url: "https://api.anthropic.com",
|
|
317
|
+
npm: "@ai-sdk/anthropic",
|
|
318
|
+
},
|
|
319
|
+
name: "Claude 3.5 Sonnet",
|
|
320
|
+
capabilities: {
|
|
321
|
+
temperature: true,
|
|
322
|
+
reasoning: false,
|
|
323
|
+
attachment: true,
|
|
324
|
+
toolcall: true,
|
|
325
|
+
input: { text: true, audio: false, image: true, video: false, pdf: true },
|
|
326
|
+
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
327
|
+
interleaved: false,
|
|
328
|
+
},
|
|
329
|
+
cost: {
|
|
330
|
+
input: 0.003,
|
|
331
|
+
output: 0.015,
|
|
332
|
+
cache: { read: 0.0003, write: 0.00375 },
|
|
333
|
+
},
|
|
334
|
+
limit: {
|
|
335
|
+
context: 200000,
|
|
336
|
+
output: 8192,
|
|
337
|
+
},
|
|
338
|
+
status: "active",
|
|
339
|
+
options: {},
|
|
340
|
+
headers: {},
|
|
341
|
+
} as any
|
|
342
|
+
|
|
343
|
+
test("should replace empty base64 image with error text", () => {
|
|
344
|
+
const msgs = [
|
|
345
|
+
{
|
|
346
|
+
role: "user",
|
|
347
|
+
content: [
|
|
348
|
+
{ type: "text", text: "What is in this image?" },
|
|
349
|
+
{ type: "image", image: "data:image/png;base64," },
|
|
350
|
+
],
|
|
351
|
+
},
|
|
352
|
+
] as any[]
|
|
353
|
+
|
|
354
|
+
const result = ProviderTransform.message(msgs, mockModel)
|
|
355
|
+
|
|
356
|
+
expect(result).toHaveLength(1)
|
|
357
|
+
expect(result[0].content).toHaveLength(2)
|
|
358
|
+
expect(result[0].content[0]).toEqual({ type: "text", text: "What is in this image?" })
|
|
359
|
+
expect(result[0].content[1]).toEqual({
|
|
360
|
+
type: "text",
|
|
361
|
+
text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test("should keep valid base64 images unchanged", () => {
|
|
366
|
+
const validBase64 =
|
|
367
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
|
368
|
+
const msgs = [
|
|
369
|
+
{
|
|
370
|
+
role: "user",
|
|
371
|
+
content: [
|
|
372
|
+
{ type: "text", text: "What is in this image?" },
|
|
373
|
+
{ type: "image", image: `data:image/png;base64,${validBase64}` },
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
] as any[]
|
|
377
|
+
|
|
378
|
+
const result = ProviderTransform.message(msgs, mockModel)
|
|
379
|
+
|
|
380
|
+
expect(result).toHaveLength(1)
|
|
381
|
+
expect(result[0].content).toHaveLength(2)
|
|
382
|
+
expect(result[0].content[0]).toEqual({ type: "text", text: "What is in this image?" })
|
|
383
|
+
expect(result[0].content[1]).toEqual({ type: "image", image: `data:image/png;base64,${validBase64}` })
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
test("should handle mixed valid and empty images", () => {
|
|
387
|
+
const validBase64 =
|
|
388
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
|
389
|
+
const msgs = [
|
|
390
|
+
{
|
|
391
|
+
role: "user",
|
|
392
|
+
content: [
|
|
393
|
+
{ type: "text", text: "Compare these images" },
|
|
394
|
+
{ type: "image", image: `data:image/png;base64,${validBase64}` },
|
|
395
|
+
{ type: "image", image: "data:image/jpeg;base64," },
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
] as any[]
|
|
399
|
+
|
|
400
|
+
const result = ProviderTransform.message(msgs, mockModel)
|
|
401
|
+
|
|
402
|
+
expect(result).toHaveLength(1)
|
|
403
|
+
expect(result[0].content).toHaveLength(3)
|
|
404
|
+
expect(result[0].content[0]).toEqual({ type: "text", text: "Compare these images" })
|
|
405
|
+
expect(result[0].content[1]).toEqual({ type: "image", image: `data:image/png;base64,${validBase64}` })
|
|
406
|
+
expect(result[0].content[2]).toEqual({
|
|
407
|
+
type: "text",
|
|
408
|
+
text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { SessionRetry } from "../../src/session/retry"
|
|
3
|
+
import { MessageV2 } from "../../src/session/message-v2"
|
|
4
|
+
|
|
5
|
+
function apiError(headers?: Record<string, string>): MessageV2.APIError {
|
|
6
|
+
return new MessageV2.APIError({
|
|
7
|
+
message: "boom",
|
|
8
|
+
isRetryable: true,
|
|
9
|
+
responseHeaders: headers,
|
|
10
|
+
}).toObject() as MessageV2.APIError
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("session.retry.delay", () => {
|
|
14
|
+
test("caps delay at 30 seconds when headers missing", () => {
|
|
15
|
+
const error = apiError()
|
|
16
|
+
const delays = Array.from({ length: 10 }, (_, index) => SessionRetry.delay(index + 1, error))
|
|
17
|
+
expect(delays).toStrictEqual([2000, 4000, 8000, 16000, 30000, 30000, 30000, 30000, 30000, 30000])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("prefers retry-after-ms when shorter than exponential", () => {
|
|
21
|
+
const error = apiError({ "retry-after-ms": "1500" })
|
|
22
|
+
expect(SessionRetry.delay(4, error)).toBe(1500)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test("uses retry-after seconds when reasonable", () => {
|
|
26
|
+
const error = apiError({ "retry-after": "30" })
|
|
27
|
+
expect(SessionRetry.delay(3, error)).toBe(30000)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test("accepts http-date retry-after values", () => {
|
|
31
|
+
const date = new Date(Date.now() + 20000).toUTCString()
|
|
32
|
+
const error = apiError({ "retry-after": date })
|
|
33
|
+
const d = SessionRetry.delay(1, error)
|
|
34
|
+
expect(d).toBeGreaterThanOrEqual(19000)
|
|
35
|
+
expect(d).toBeLessThanOrEqual(20000)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test("ignores invalid retry hints", () => {
|
|
39
|
+
const error = apiError({ "retry-after": "not-a-number" })
|
|
40
|
+
expect(SessionRetry.delay(1, error)).toBe(2000)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("ignores malformed date retry hints", () => {
|
|
44
|
+
const error = apiError({ "retry-after": "Invalid Date String" })
|
|
45
|
+
expect(SessionRetry.delay(1, error)).toBe(2000)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("ignores past date retry hints", () => {
|
|
49
|
+
const pastDate = new Date(Date.now() - 5000).toUTCString()
|
|
50
|
+
const error = apiError({ "retry-after": pastDate })
|
|
51
|
+
expect(SessionRetry.delay(1, error)).toBe(2000)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("uses retry-after values even when exceeding 10 minutes with headers", () => {
|
|
55
|
+
const error = apiError({ "retry-after": "50" })
|
|
56
|
+
expect(SessionRetry.delay(1, error)).toBe(50000)
|
|
57
|
+
|
|
58
|
+
const longError = apiError({ "retry-after-ms": "700000" })
|
|
59
|
+
expect(SessionRetry.delay(1, longError)).toBe(700000)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { Session } from "../../src/session"
|
|
4
|
+
import { Bus } from "../../src/bus"
|
|
5
|
+
import { Log } from "../../src/util/log"
|
|
6
|
+
import { Instance } from "../../src/project/instance"
|
|
7
|
+
|
|
8
|
+
const projectRoot = path.join(__dirname, "../..")
|
|
9
|
+
Log.init({ print: false })
|
|
10
|
+
|
|
11
|
+
describe("session.started event", () => {
|
|
12
|
+
test("should emit session.started event when session is created", async () => {
|
|
13
|
+
await Instance.provide({
|
|
14
|
+
directory: projectRoot,
|
|
15
|
+
fn: async () => {
|
|
16
|
+
let eventReceived = false
|
|
17
|
+
let receivedInfo: Session.Info | undefined
|
|
18
|
+
|
|
19
|
+
const unsub = Bus.subscribe(Session.Event.Created, (event) => {
|
|
20
|
+
eventReceived = true
|
|
21
|
+
receivedInfo = event.properties.info as Session.Info
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const session = await Session.create({})
|
|
25
|
+
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
27
|
+
|
|
28
|
+
unsub()
|
|
29
|
+
|
|
30
|
+
expect(eventReceived).toBe(true)
|
|
31
|
+
expect(receivedInfo).toBeDefined()
|
|
32
|
+
expect(receivedInfo?.id).toBe(session.id)
|
|
33
|
+
expect(receivedInfo?.projectID).toBe(session.projectID)
|
|
34
|
+
expect(receivedInfo?.directory).toBe(session.directory)
|
|
35
|
+
expect(receivedInfo?.title).toBe(session.title)
|
|
36
|
+
|
|
37
|
+
await Session.remove(session.id)
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("session.started event should be emitted before session.updated", async () => {
|
|
43
|
+
await Instance.provide({
|
|
44
|
+
directory: projectRoot,
|
|
45
|
+
fn: async () => {
|
|
46
|
+
const events: string[] = []
|
|
47
|
+
|
|
48
|
+
const unsubStarted = Bus.subscribe(Session.Event.Created, () => {
|
|
49
|
+
events.push("started")
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const unsubUpdated = Bus.subscribe(Session.Event.Updated, () => {
|
|
53
|
+
events.push("updated")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const session = await Session.create({})
|
|
57
|
+
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
59
|
+
|
|
60
|
+
unsubStarted()
|
|
61
|
+
unsubUpdated()
|
|
62
|
+
|
|
63
|
+
expect(events).toContain("started")
|
|
64
|
+
expect(events).toContain("updated")
|
|
65
|
+
expect(events.indexOf("started")).toBeLessThan(events.indexOf("updated"))
|
|
66
|
+
|
|
67
|
+
await Session.remove(session.id)
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { test, expect } from "bun:test"
|
|
2
|
+
import { Skill } from "../../src/skill"
|
|
3
|
+
import { SystemPrompt } from "../../src/session/system"
|
|
4
|
+
import { Instance } from "../../src/project/instance"
|
|
5
|
+
import { tmpdir } from "../fixture/fixture"
|
|
6
|
+
import path from "path"
|
|
7
|
+
|
|
8
|
+
test("discovers skills from .opencode/skill/ directory", async () => {
|
|
9
|
+
await using tmp = await tmpdir({
|
|
10
|
+
git: true,
|
|
11
|
+
init: async (dir) => {
|
|
12
|
+
const skillDir = path.join(dir, ".opencode", "skill", "test-skill")
|
|
13
|
+
await Bun.write(
|
|
14
|
+
path.join(skillDir, "SKILL.md"),
|
|
15
|
+
`---
|
|
16
|
+
name: test-skill
|
|
17
|
+
description: A test skill for verification.
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Test Skill
|
|
21
|
+
|
|
22
|
+
Instructions here.
|
|
23
|
+
`,
|
|
24
|
+
)
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
await Instance.provide({
|
|
29
|
+
directory: tmp.path,
|
|
30
|
+
fn: async () => {
|
|
31
|
+
const skills = await Skill.all()
|
|
32
|
+
expect(skills.length).toBe(1)
|
|
33
|
+
expect(skills[0].name).toBe("test-skill")
|
|
34
|
+
expect(skills[0].description).toBe("A test skill for verification.")
|
|
35
|
+
expect(skills[0].location).toContain("skill/test-skill/SKILL.md")
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("discovers multiple skills from .opencode/skill/ directory", async () => {
|
|
41
|
+
await using tmp = await tmpdir({
|
|
42
|
+
git: true,
|
|
43
|
+
init: async (dir) => {
|
|
44
|
+
const skillDir = path.join(dir, ".opencode", "skill", "my-skill")
|
|
45
|
+
await Bun.write(
|
|
46
|
+
path.join(skillDir, "SKILL.md"),
|
|
47
|
+
`---
|
|
48
|
+
name: my-skill
|
|
49
|
+
description: Another test skill.
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
# My Skill
|
|
53
|
+
`,
|
|
54
|
+
)
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
await Instance.provide({
|
|
59
|
+
directory: tmp.path,
|
|
60
|
+
fn: async () => {
|
|
61
|
+
const skills = await Skill.all()
|
|
62
|
+
expect(skills.length).toBe(1)
|
|
63
|
+
expect(skills[0].name).toBe("my-skill")
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("skips skills with missing frontmatter", async () => {
|
|
69
|
+
await using tmp = await tmpdir({
|
|
70
|
+
git: true,
|
|
71
|
+
init: async (dir) => {
|
|
72
|
+
const skillDir = path.join(dir, ".opencode", "skill", "no-frontmatter")
|
|
73
|
+
await Bun.write(
|
|
74
|
+
path.join(skillDir, "SKILL.md"),
|
|
75
|
+
`# No Frontmatter
|
|
76
|
+
|
|
77
|
+
Just some content without YAML frontmatter.
|
|
78
|
+
`,
|
|
79
|
+
)
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
await Instance.provide({
|
|
84
|
+
directory: tmp.path,
|
|
85
|
+
fn: async () => {
|
|
86
|
+
const skills = await Skill.all()
|
|
87
|
+
expect(skills).toEqual([])
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test("returns empty array when no skills exist", async () => {
|
|
93
|
+
await using tmp = await tmpdir({ git: true })
|
|
94
|
+
|
|
95
|
+
await Instance.provide({
|
|
96
|
+
directory: tmp.path,
|
|
97
|
+
fn: async () => {
|
|
98
|
+
const skills = await Skill.all()
|
|
99
|
+
expect(skills).toEqual([])
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// test("discovers skills from .claude/skills/ directory", async () => {
|
|
105
|
+
// await using tmp = await tmpdir({
|
|
106
|
+
// git: true,
|
|
107
|
+
// init: async (dir) => {
|
|
108
|
+
// const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
|
|
109
|
+
// await Bun.write(
|
|
110
|
+
// path.join(skillDir, "SKILL.md"),
|
|
111
|
+
// `---
|
|
112
|
+
// name: claude-skill
|
|
113
|
+
// description: A skill in the .claude/skills directory.
|
|
114
|
+
// ---
|
|
115
|
+
|
|
116
|
+
// # Claude Skill
|
|
117
|
+
// `,
|
|
118
|
+
// )
|
|
119
|
+
// },
|
|
120
|
+
// })
|
|
121
|
+
|
|
122
|
+
// await Instance.provide({
|
|
123
|
+
// directory: tmp.path,
|
|
124
|
+
// fn: async () => {
|
|
125
|
+
// const skills = await Skill.all()
|
|
126
|
+
// expect(skills.length).toBe(1)
|
|
127
|
+
// expect(skills[0].name).toBe("claude-skill")
|
|
128
|
+
// expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
|
129
|
+
// },
|
|
130
|
+
// })
|
|
131
|
+
// })
|