@stonerzju/opencode 1.2.17 → 1.2.18
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/bin/opencode +29 -157
- package/package.json +29 -29
- package/src/acp/agent.ts +4 -4
- package/src/acp/session.ts +1 -1
- package/src/agent/agent.ts +3 -3
- package/src/bun/index.ts +2 -2
- package/src/cli/cmd/acp.ts +3 -3
- package/src/cli/cmd/debug/file.ts +1 -1
- package/src/cli/cmd/github.ts +2 -2
- package/src/cli/cmd/pr.ts +1 -1
- package/src/cli/cmd/tui/app.tsx +24 -24
- package/src/cli/cmd/tui/attach.ts +3 -3
- package/src/cli/cmd/tui/component/dialog-agent.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-command.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog-model.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog-provider.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-skill.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-stash.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-status.tsx +2 -2
- package/src/cli/cmd/tui/component/dialog-tag.tsx +3 -3
- package/src/cli/cmd/tui/component/logo.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +6 -6
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/history.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/index.tsx +14 -14
- package/src/cli/cmd/tui/component/prompt/stash.tsx +2 -2
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +1 -1
- package/src/cli/cmd/tui/component/tips.tsx +1 -1
- package/src/cli/cmd/tui/context/directory.ts +1 -1
- package/src/cli/cmd/tui/context/exit.tsx +1 -1
- package/src/cli/cmd/tui/context/keybind.tsx +2 -2
- package/src/cli/cmd/tui/context/kv.tsx +2 -2
- package/src/cli/cmd/tui/context/local.tsx +6 -6
- package/src/cli/cmd/tui/context/sync.tsx +4 -4
- package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
- package/src/cli/cmd/tui/context/theme.tsx +2 -2
- package/src/cli/cmd/tui/context/tui-config.tsx +1 -1
- package/src/cli/cmd/tui/event.ts +2 -2
- package/src/cli/cmd/tui/routes/home.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +2 -2
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +3 -3
- package/src/cli/cmd/tui/routes/session/header.tsx +5 -5
- package/src/cli/cmd/tui/routes/session/index.tsx +32 -32
- package/src/cli/cmd/tui/routes/session/permission.tsx +4 -4
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +4 -4
- package/src/cli/cmd/tui/thread.ts +9 -9
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +1 -1
- package/src/cli/cmd/tui/ui/dialog-help.tsx +2 -2
- package/src/cli/cmd/tui/ui/dialog-select.tsx +5 -5
- package/src/cli/cmd/tui/ui/dialog.tsx +3 -3
- package/src/cli/cmd/tui/ui/toast.tsx +1 -1
- package/src/cli/cmd/tui/util/editor.ts +3 -3
- package/src/cli/cmd/tui/util/transcript.ts +1 -1
- package/src/cli/cmd/tui/worker.ts +10 -10
- package/src/cli/error.ts +1 -1
- package/src/cli/ui.ts +1 -1
- package/src/cli/upgrade.ts +4 -4
- package/src/command/index.ts +1 -1
- package/src/config/config.ts +10 -10
- package/src/config/markdown.ts +1 -1
- package/src/config/migrate-tui-config.ts +5 -5
- package/src/config/paths.ts +4 -4
- package/src/config/tui.ts +4 -4
- package/src/control/control.sql.ts +1 -1
- package/src/control/index.ts +1 -1
- package/src/control-plane/adaptors/worktree.ts +1 -1
- package/src/control-plane/session-proxy-middleware.ts +1 -1
- package/src/control-plane/workspace.sql.ts +1 -1
- package/src/control-plane/workspace.ts +7 -7
- package/src/file/index.ts +1 -1
- package/src/file/ripgrep.ts +2 -2
- package/src/file/watcher.ts +5 -5
- package/src/format/formatter.ts +1 -1
- package/src/ide/index.ts +3 -3
- package/src/index.ts +1 -1
- package/src/installation/index.ts +3 -3
- package/src/lsp/client.ts +3 -3
- package/src/lsp/index.ts +3 -3
- package/src/mcp/index.ts +4 -4
- package/src/permission/index.ts +2 -2
- package/src/permission/next.ts +10 -10
- package/src/plugin/codex.ts +1 -1
- package/src/plugin/copilot.ts +2 -2
- package/src/plugin/index.ts +1 -1
- package/src/project/bootstrap.ts +2 -2
- package/src/project/instance.ts +4 -4
- package/src/project/project.sql.ts +1 -1
- package/src/project/project.ts +5 -5
- package/src/project/state.ts +1 -1
- package/src/project/vcs.ts +4 -4
- package/src/provider/auth.ts +4 -4
- package/src/provider/error.ts +1 -1
- package/src/provider/models-snapshot.ts +2 -0
- package/src/provider/models.ts +1 -1
- package/src/provider/provider.ts +2 -2
- package/src/provider/transform.ts +2 -2
- package/src/pty/index.ts +5 -5
- package/src/question/index.ts +5 -5
- package/src/server/event.ts +1 -1
- package/src/server/mdns.ts +1 -1
- package/src/server/routes/global.ts +3 -3
- package/src/server/routes/permission.ts +1 -1
- package/src/server/routes/pty.ts +1 -1
- package/src/server/routes/session.ts +4 -4
- package/src/server/routes/tui.ts +1 -1
- package/src/server/server.ts +3 -3
- package/src/session/compaction.ts +7 -7
- package/src/session/index.ts +10 -10
- package/src/session/instruction.ts +1 -1
- package/src/session/llm.ts +11 -11
- package/src/session/message-v2.ts +10 -10
- package/src/session/message.ts +1 -1
- package/src/session/processor.ts +10 -10
- package/src/session/prompt.ts +8 -8
- package/src/session/retry.ts +2 -2
- package/src/session/revert.ts +1 -1
- package/src/session/session.sql.ts +3 -3
- package/src/session/status.ts +3 -3
- package/src/session/summary.ts +5 -5
- package/src/session/system.ts +1 -1
- package/src/session/todo.ts +2 -2
- package/src/share/share-next.ts +7 -7
- package/src/share/share.sql.ts +1 -1
- package/src/shell/shell.ts +3 -3
- package/src/skill/skill.ts +6 -6
- package/src/storage/db.ts +1 -1
- package/src/storage/storage.ts +1 -1
- package/src/tool/bash.ts +6 -6
- package/src/tool/edit.ts +1 -1
- package/src/tool/registry.ts +2 -2
- package/src/tool/skill.ts +1 -1
- package/src/tool/task.ts +3 -3
- package/src/util/array.ts +10 -0
- package/src/util/binary.ts +41 -0
- package/src/util/encode.ts +51 -0
- package/src/util/error.ts +54 -0
- package/src/util/identifier.ts +48 -0
- package/src/util/lazy.ts +4 -16
- package/src/util/path.ts +37 -0
- package/src/util/retry.ts +41 -0
- package/src/util/slug.ts +74 -0
- package/src/worktree/index.ts +3 -3
- package/AGENTS.md +0 -10
- package/BUN_SHELL_MIGRATION_PLAN.md +0 -136
- package/Dockerfile +0 -18
- package/README.md +0 -15
- package/bunfig.toml +0 -7
- package/drizzle.config.ts +0 -10
- package/script/build.ts +0 -224
- package/script/check-migrations.ts +0 -16
- package/script/postinstall.mjs +0 -131
- package/script/publish.ts +0 -181
- package/script/schema.ts +0 -63
- package/script/seed-e2e.ts +0 -50
- package/sst-env.d.ts +0 -10
- package/test/AGENTS.md +0 -81
- package/test/acp/agent-interface.test.ts +0 -51
- package/test/acp/event-subscription.test.ts +0 -683
- package/test/agent/agent.test.ts +0 -689
- package/test/bun.test.ts +0 -53
- package/test/cli/github-action.test.ts +0 -197
- package/test/cli/github-remote.test.ts +0 -80
- package/test/cli/import.test.ts +0 -38
- package/test/cli/plugin-auth-picker.test.ts +0 -120
- package/test/cli/tui/transcript.test.ts +0 -322
- package/test/config/agent-color.test.ts +0 -71
- package/test/config/config.test.ts +0 -1886
- package/test/config/fixtures/empty-frontmatter.md +0 -4
- package/test/config/fixtures/frontmatter.md +0 -28
- package/test/config/fixtures/markdown-header.md +0 -11
- package/test/config/fixtures/no-frontmatter.md +0 -1
- package/test/config/fixtures/weird-model-id.md +0 -13
- package/test/config/markdown.test.ts +0 -228
- package/test/config/tui.test.ts +0 -510
- package/test/control-plane/session-proxy-middleware.test.ts +0 -147
- package/test/control-plane/sse.test.ts +0 -56
- package/test/control-plane/workspace-server-sse.test.ts +0 -65
- package/test/control-plane/workspace-sync.test.ts +0 -97
- package/test/file/ignore.test.ts +0 -10
- package/test/file/index.test.ts +0 -394
- package/test/file/path-traversal.test.ts +0 -198
- package/test/file/ripgrep.test.ts +0 -39
- package/test/file/time.test.ts +0 -361
- package/test/fixture/db.ts +0 -11
- package/test/fixture/fixture.ts +0 -45
- package/test/fixture/lsp/fake-lsp-server.js +0 -77
- package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
- package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
- package/test/fixture/skills/cloudflare/SKILL.md +0 -211
- package/test/fixture/skills/index.json +0 -6
- package/test/ide/ide.test.ts +0 -82
- package/test/keybind.test.ts +0 -421
- package/test/lsp/client.test.ts +0 -95
- package/test/mcp/headers.test.ts +0 -153
- package/test/mcp/oauth-browser.test.ts +0 -249
- package/test/memory/abort-leak.test.ts +0 -136
- package/test/patch/patch.test.ts +0 -348
- package/test/permission/arity.test.ts +0 -33
- package/test/permission/next.test.ts +0 -689
- package/test/permission-task.test.ts +0 -319
- package/test/plugin/auth-override.test.ts +0 -44
- package/test/plugin/codex.test.ts +0 -123
- package/test/preload.ts +0 -80
- package/test/project/project.test.ts +0 -348
- package/test/project/worktree-remove.test.ts +0 -65
- package/test/provider/amazon-bedrock.test.ts +0 -446
- package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
- package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
- package/test/provider/gitlab-duo.test.ts +0 -262
- package/test/provider/provider.test.ts +0 -2220
- package/test/provider/transform.test.ts +0 -2353
- package/test/pty/pty-output-isolation.test.ts +0 -140
- package/test/question/question.test.ts +0 -300
- package/test/scheduler.test.ts +0 -73
- package/test/server/global-session-list.test.ts +0 -89
- package/test/server/session-list.test.ts +0 -90
- package/test/server/session-select.test.ts +0 -78
- package/test/session/compaction.test.ts +0 -423
- package/test/session/instruction.test.ts +0 -170
- package/test/session/llm.test.ts +0 -667
- package/test/session/message-v2.test.ts +0 -924
- package/test/session/prompt.test.ts +0 -211
- package/test/session/retry.test.ts +0 -188
- package/test/session/revert-compact.test.ts +0 -285
- package/test/session/session.test.ts +0 -71
- package/test/session/structured-output-integration.test.ts +0 -233
- package/test/session/structured-output.test.ts +0 -385
- package/test/skill/discovery.test.ts +0 -110
- package/test/skill/skill.test.ts +0 -388
- package/test/snapshot/snapshot.test.ts +0 -1180
- package/test/storage/json-migration.test.ts +0 -846
- package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
- package/test/tool/apply_patch.test.ts +0 -566
- package/test/tool/bash.test.ts +0 -402
- package/test/tool/edit.test.ts +0 -496
- package/test/tool/external-directory.test.ts +0 -127
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +0 -38413
- package/test/tool/grep.test.ts +0 -110
- package/test/tool/question.test.ts +0 -107
- package/test/tool/read.test.ts +0 -504
- package/test/tool/registry.test.ts +0 -122
- package/test/tool/skill.test.ts +0 -112
- package/test/tool/truncation.test.ts +0 -160
- package/test/tool/webfetch.test.ts +0 -100
- package/test/tool/write.test.ts +0 -348
- package/test/util/filesystem.test.ts +0 -443
- package/test/util/format.test.ts +0 -59
- package/test/util/glob.test.ts +0 -164
- package/test/util/iife.test.ts +0 -36
- package/test/util/lazy.test.ts +0 -50
- package/test/util/lock.test.ts +0 -72
- package/test/util/process.test.ts +0 -59
- package/test/util/timeout.test.ts +0 -21
- package/test/util/wildcard.test.ts +0 -90
- package/tsconfig.json +0 -16
package/test/tool/skill.test.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import { pathToFileURL } from "url"
|
|
4
|
-
import type { PermissionNext } from "../../src/permission/next"
|
|
5
|
-
import type { Tool } from "../../src/tool/tool"
|
|
6
|
-
import { Instance } from "../../src/project/instance"
|
|
7
|
-
import { SkillTool } from "../../src/tool/skill"
|
|
8
|
-
import { tmpdir } from "../fixture/fixture"
|
|
9
|
-
|
|
10
|
-
const baseCtx: Omit<Tool.Context, "ask"> = {
|
|
11
|
-
sessionID: "test",
|
|
12
|
-
messageID: "",
|
|
13
|
-
callID: "",
|
|
14
|
-
agent: "build",
|
|
15
|
-
abort: AbortSignal.any([]),
|
|
16
|
-
messages: [],
|
|
17
|
-
metadata: () => {},
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe("tool.skill", () => {
|
|
21
|
-
test("description lists skill location URL", async () => {
|
|
22
|
-
await using tmp = await tmpdir({
|
|
23
|
-
git: true,
|
|
24
|
-
init: async (dir) => {
|
|
25
|
-
const skillDir = path.join(dir, ".opencode", "skill", "tool-skill")
|
|
26
|
-
await Bun.write(
|
|
27
|
-
path.join(skillDir, "SKILL.md"),
|
|
28
|
-
`---
|
|
29
|
-
name: tool-skill
|
|
30
|
-
description: Skill for tool tests.
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
# Tool Skill
|
|
34
|
-
`,
|
|
35
|
-
)
|
|
36
|
-
},
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
const home = process.env.OPENCODE_TEST_HOME
|
|
40
|
-
process.env.OPENCODE_TEST_HOME = tmp.path
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
await Instance.provide({
|
|
44
|
-
directory: tmp.path,
|
|
45
|
-
fn: async () => {
|
|
46
|
-
const tool = await SkillTool.init()
|
|
47
|
-
const skillPath = path.join(tmp.path, ".opencode", "skill", "tool-skill", "SKILL.md")
|
|
48
|
-
expect(tool.description).toContain(`<location>${pathToFileURL(skillPath).href}</location>`)
|
|
49
|
-
},
|
|
50
|
-
})
|
|
51
|
-
} finally {
|
|
52
|
-
process.env.OPENCODE_TEST_HOME = home
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test("execute returns skill content block with files", async () => {
|
|
57
|
-
await using tmp = await tmpdir({
|
|
58
|
-
git: true,
|
|
59
|
-
init: async (dir) => {
|
|
60
|
-
const skillDir = path.join(dir, ".opencode", "skill", "tool-skill")
|
|
61
|
-
await Bun.write(
|
|
62
|
-
path.join(skillDir, "SKILL.md"),
|
|
63
|
-
`---
|
|
64
|
-
name: tool-skill
|
|
65
|
-
description: Skill for tool tests.
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
# Tool Skill
|
|
69
|
-
|
|
70
|
-
Use this skill.
|
|
71
|
-
`,
|
|
72
|
-
)
|
|
73
|
-
await Bun.write(path.join(skillDir, "scripts", "demo.txt"), "demo")
|
|
74
|
-
},
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
const home = process.env.OPENCODE_TEST_HOME
|
|
78
|
-
process.env.OPENCODE_TEST_HOME = tmp.path
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
await Instance.provide({
|
|
82
|
-
directory: tmp.path,
|
|
83
|
-
fn: async () => {
|
|
84
|
-
const tool = await SkillTool.init()
|
|
85
|
-
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
|
|
86
|
-
const ctx: Tool.Context = {
|
|
87
|
-
...baseCtx,
|
|
88
|
-
ask: async (req) => {
|
|
89
|
-
requests.push(req)
|
|
90
|
-
},
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const result = await tool.execute({ name: "tool-skill" }, ctx)
|
|
94
|
-
const dir = path.join(tmp.path, ".opencode", "skill", "tool-skill")
|
|
95
|
-
const file = path.resolve(dir, "scripts", "demo.txt")
|
|
96
|
-
|
|
97
|
-
expect(requests.length).toBe(1)
|
|
98
|
-
expect(requests[0].permission).toBe("skill")
|
|
99
|
-
expect(requests[0].patterns).toContain("tool-skill")
|
|
100
|
-
expect(requests[0].always).toContain("tool-skill")
|
|
101
|
-
|
|
102
|
-
expect(result.metadata.dir).toBe(dir)
|
|
103
|
-
expect(result.output).toContain(`<skill_content name="tool-skill">`)
|
|
104
|
-
expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(dir).href}`)
|
|
105
|
-
expect(result.output).toContain(`<file>${file}</file>`)
|
|
106
|
-
},
|
|
107
|
-
})
|
|
108
|
-
} finally {
|
|
109
|
-
process.env.OPENCODE_TEST_HOME = home
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
})
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, afterAll } from "bun:test"
|
|
2
|
-
import { Truncate } from "../../src/tool/truncation"
|
|
3
|
-
import { Identifier } from "../../src/id/id"
|
|
4
|
-
import { Filesystem } from "../../src/util/filesystem"
|
|
5
|
-
import fs from "fs/promises"
|
|
6
|
-
import path from "path"
|
|
7
|
-
|
|
8
|
-
const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
|
|
9
|
-
|
|
10
|
-
describe("Truncate", () => {
|
|
11
|
-
describe("output", () => {
|
|
12
|
-
test("truncates large json file by bytes", async () => {
|
|
13
|
-
const content = await Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json"))
|
|
14
|
-
const result = await Truncate.output(content)
|
|
15
|
-
|
|
16
|
-
expect(result.truncated).toBe(true)
|
|
17
|
-
expect(result.content).toContain("truncated...")
|
|
18
|
-
if (result.truncated) expect(result.outputPath).toBeDefined()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test("returns content unchanged when under limits", async () => {
|
|
22
|
-
const content = "line1\nline2\nline3"
|
|
23
|
-
const result = await Truncate.output(content)
|
|
24
|
-
|
|
25
|
-
expect(result.truncated).toBe(false)
|
|
26
|
-
expect(result.content).toBe(content)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
test("truncates by line count", async () => {
|
|
30
|
-
const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n")
|
|
31
|
-
const result = await Truncate.output(lines, { maxLines: 10 })
|
|
32
|
-
|
|
33
|
-
expect(result.truncated).toBe(true)
|
|
34
|
-
expect(result.content).toContain("...90 lines truncated...")
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test("truncates by byte count", async () => {
|
|
38
|
-
const content = "a".repeat(1000)
|
|
39
|
-
const result = await Truncate.output(content, { maxBytes: 100 })
|
|
40
|
-
|
|
41
|
-
expect(result.truncated).toBe(true)
|
|
42
|
-
expect(result.content).toContain("truncated...")
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
test("truncates from head by default", async () => {
|
|
46
|
-
const lines = Array.from({ length: 10 }, (_, i) => `line${i}`).join("\n")
|
|
47
|
-
const result = await Truncate.output(lines, { maxLines: 3 })
|
|
48
|
-
|
|
49
|
-
expect(result.truncated).toBe(true)
|
|
50
|
-
expect(result.content).toContain("line0")
|
|
51
|
-
expect(result.content).toContain("line1")
|
|
52
|
-
expect(result.content).toContain("line2")
|
|
53
|
-
expect(result.content).not.toContain("line9")
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test("truncates from tail when direction is tail", async () => {
|
|
57
|
-
const lines = Array.from({ length: 10 }, (_, i) => `line${i}`).join("\n")
|
|
58
|
-
const result = await Truncate.output(lines, { maxLines: 3, direction: "tail" })
|
|
59
|
-
|
|
60
|
-
expect(result.truncated).toBe(true)
|
|
61
|
-
expect(result.content).toContain("line7")
|
|
62
|
-
expect(result.content).toContain("line8")
|
|
63
|
-
expect(result.content).toContain("line9")
|
|
64
|
-
expect(result.content).not.toContain("line0")
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test("uses default MAX_LINES and MAX_BYTES", () => {
|
|
68
|
-
expect(Truncate.MAX_LINES).toBe(2000)
|
|
69
|
-
expect(Truncate.MAX_BYTES).toBe(50 * 1024)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test("large single-line file truncates with byte message", async () => {
|
|
73
|
-
const content = await Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json"))
|
|
74
|
-
const result = await Truncate.output(content)
|
|
75
|
-
|
|
76
|
-
expect(result.truncated).toBe(true)
|
|
77
|
-
expect(result.content).toContain("bytes truncated...")
|
|
78
|
-
expect(Buffer.byteLength(content, "utf-8")).toBeGreaterThan(Truncate.MAX_BYTES)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test("writes full output to file when truncated", async () => {
|
|
82
|
-
const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n")
|
|
83
|
-
const result = await Truncate.output(lines, { maxLines: 10 })
|
|
84
|
-
|
|
85
|
-
expect(result.truncated).toBe(true)
|
|
86
|
-
expect(result.content).toContain("The tool call succeeded but the output was truncated")
|
|
87
|
-
expect(result.content).toContain("Grep")
|
|
88
|
-
if (!result.truncated) throw new Error("expected truncated")
|
|
89
|
-
expect(result.outputPath).toBeDefined()
|
|
90
|
-
expect(result.outputPath).toContain("tool_")
|
|
91
|
-
|
|
92
|
-
const written = await Filesystem.readText(result.outputPath!)
|
|
93
|
-
expect(written).toBe(lines)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test("suggests Task tool when agent has task permission", async () => {
|
|
97
|
-
const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n")
|
|
98
|
-
const agent = { permission: [{ permission: "task", pattern: "*", action: "allow" as const }] }
|
|
99
|
-
const result = await Truncate.output(lines, { maxLines: 10 }, agent as any)
|
|
100
|
-
|
|
101
|
-
expect(result.truncated).toBe(true)
|
|
102
|
-
expect(result.content).toContain("Grep")
|
|
103
|
-
expect(result.content).toContain("Task tool")
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
test("omits Task tool hint when agent lacks task permission", async () => {
|
|
107
|
-
const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n")
|
|
108
|
-
const agent = { permission: [{ permission: "task", pattern: "*", action: "deny" as const }] }
|
|
109
|
-
const result = await Truncate.output(lines, { maxLines: 10 }, agent as any)
|
|
110
|
-
|
|
111
|
-
expect(result.truncated).toBe(true)
|
|
112
|
-
expect(result.content).toContain("Grep")
|
|
113
|
-
expect(result.content).not.toContain("Task tool")
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test("does not write file when not truncated", async () => {
|
|
117
|
-
const content = "short content"
|
|
118
|
-
const result = await Truncate.output(content)
|
|
119
|
-
|
|
120
|
-
expect(result.truncated).toBe(false)
|
|
121
|
-
if (result.truncated) throw new Error("expected not truncated")
|
|
122
|
-
expect("outputPath" in result).toBe(false)
|
|
123
|
-
})
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
describe("cleanup", () => {
|
|
127
|
-
const DAY_MS = 24 * 60 * 60 * 1000
|
|
128
|
-
let oldFile: string
|
|
129
|
-
let recentFile: string
|
|
130
|
-
|
|
131
|
-
afterAll(async () => {
|
|
132
|
-
await fs.unlink(oldFile).catch(() => {})
|
|
133
|
-
await fs.unlink(recentFile).catch(() => {})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
test("deletes files older than 7 days and preserves recent files", async () => {
|
|
137
|
-
await fs.mkdir(Truncate.DIR, { recursive: true })
|
|
138
|
-
|
|
139
|
-
// Create an old file (10 days ago)
|
|
140
|
-
const oldTimestamp = Date.now() - 10 * DAY_MS
|
|
141
|
-
const oldId = Identifier.create("tool", false, oldTimestamp)
|
|
142
|
-
oldFile = path.join(Truncate.DIR, oldId)
|
|
143
|
-
await Filesystem.write(oldFile, "old content")
|
|
144
|
-
|
|
145
|
-
// Create a recent file (3 days ago)
|
|
146
|
-
const recentTimestamp = Date.now() - 3 * DAY_MS
|
|
147
|
-
const recentId = Identifier.create("tool", false, recentTimestamp)
|
|
148
|
-
recentFile = path.join(Truncate.DIR, recentId)
|
|
149
|
-
await Filesystem.write(recentFile, "recent content")
|
|
150
|
-
|
|
151
|
-
await Truncate.cleanup()
|
|
152
|
-
|
|
153
|
-
// Old file should be deleted
|
|
154
|
-
expect(await Filesystem.exists(oldFile)).toBe(false)
|
|
155
|
-
|
|
156
|
-
// Recent file should still exist
|
|
157
|
-
expect(await Filesystem.exists(recentFile)).toBe(true)
|
|
158
|
-
})
|
|
159
|
-
})
|
|
160
|
-
})
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import { Instance } from "../../src/project/instance"
|
|
4
|
-
import { WebFetchTool } from "../../src/tool/webfetch"
|
|
5
|
-
|
|
6
|
-
const projectRoot = path.join(import.meta.dir, "../..")
|
|
7
|
-
|
|
8
|
-
const ctx = {
|
|
9
|
-
sessionID: "test",
|
|
10
|
-
messageID: "message",
|
|
11
|
-
callID: "",
|
|
12
|
-
agent: "build",
|
|
13
|
-
abort: AbortSignal.any([]),
|
|
14
|
-
messages: [],
|
|
15
|
-
metadata: () => {},
|
|
16
|
-
ask: async () => {},
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function withFetch(
|
|
20
|
-
mockFetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,
|
|
21
|
-
fn: () => Promise<void>,
|
|
22
|
-
) {
|
|
23
|
-
const originalFetch = globalThis.fetch
|
|
24
|
-
globalThis.fetch = mockFetch as unknown as typeof fetch
|
|
25
|
-
try {
|
|
26
|
-
await fn()
|
|
27
|
-
} finally {
|
|
28
|
-
globalThis.fetch = originalFetch
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("tool.webfetch", () => {
|
|
33
|
-
test("returns image responses as file attachments", async () => {
|
|
34
|
-
const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])
|
|
35
|
-
await withFetch(
|
|
36
|
-
async () => new Response(bytes, { status: 200, headers: { "content-type": "IMAGE/PNG; charset=binary" } }),
|
|
37
|
-
async () => {
|
|
38
|
-
await Instance.provide({
|
|
39
|
-
directory: projectRoot,
|
|
40
|
-
fn: async () => {
|
|
41
|
-
const webfetch = await WebFetchTool.init()
|
|
42
|
-
const result = await webfetch.execute({ url: "https://example.com/image.png", format: "markdown" }, ctx)
|
|
43
|
-
expect(result.output).toBe("Image fetched successfully")
|
|
44
|
-
expect(result.attachments).toBeDefined()
|
|
45
|
-
expect(result.attachments?.length).toBe(1)
|
|
46
|
-
expect(result.attachments?.[0].type).toBe("file")
|
|
47
|
-
expect(result.attachments?.[0].mime).toBe("image/png")
|
|
48
|
-
expect(result.attachments?.[0].url.startsWith("data:image/png;base64,")).toBe(true)
|
|
49
|
-
expect(result.attachments?.[0]).not.toHaveProperty("id")
|
|
50
|
-
expect(result.attachments?.[0]).not.toHaveProperty("sessionID")
|
|
51
|
-
expect(result.attachments?.[0]).not.toHaveProperty("messageID")
|
|
52
|
-
},
|
|
53
|
-
})
|
|
54
|
-
},
|
|
55
|
-
)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test("keeps svg as text output", async () => {
|
|
59
|
-
const svg = '<svg xmlns="http://www.w3.org/2000/svg"><text>hello</text></svg>'
|
|
60
|
-
await withFetch(
|
|
61
|
-
async () =>
|
|
62
|
-
new Response(svg, {
|
|
63
|
-
status: 200,
|
|
64
|
-
headers: { "content-type": "image/svg+xml; charset=UTF-8" },
|
|
65
|
-
}),
|
|
66
|
-
async () => {
|
|
67
|
-
await Instance.provide({
|
|
68
|
-
directory: projectRoot,
|
|
69
|
-
fn: async () => {
|
|
70
|
-
const webfetch = await WebFetchTool.init()
|
|
71
|
-
const result = await webfetch.execute({ url: "https://example.com/image.svg", format: "html" }, ctx)
|
|
72
|
-
expect(result.output).toContain("<svg")
|
|
73
|
-
expect(result.attachments).toBeUndefined()
|
|
74
|
-
},
|
|
75
|
-
})
|
|
76
|
-
},
|
|
77
|
-
)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
test("keeps text responses as text output", async () => {
|
|
81
|
-
await withFetch(
|
|
82
|
-
async () =>
|
|
83
|
-
new Response("hello from webfetch", {
|
|
84
|
-
status: 200,
|
|
85
|
-
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
86
|
-
}),
|
|
87
|
-
async () => {
|
|
88
|
-
await Instance.provide({
|
|
89
|
-
directory: projectRoot,
|
|
90
|
-
fn: async () => {
|
|
91
|
-
const webfetch = await WebFetchTool.init()
|
|
92
|
-
const result = await webfetch.execute({ url: "https://example.com/file.txt", format: "text" }, ctx)
|
|
93
|
-
expect(result.output).toBe("hello from webfetch")
|
|
94
|
-
expect(result.attachments).toBeUndefined()
|
|
95
|
-
},
|
|
96
|
-
})
|
|
97
|
-
},
|
|
98
|
-
)
|
|
99
|
-
})
|
|
100
|
-
})
|