mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-21 17:54:23 +01:00
ignore: rework bootstrap so server lazy starts it
This commit is contained in:
@@ -1,19 +1,14 @@
|
||||
import { Format } from "../format"
|
||||
import { LSP } from "../lsp"
|
||||
import { Plugin } from "../plugin"
|
||||
import { InstanceBootstrap } from "../project/bootstrap"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Share } from "../share/share"
|
||||
import { Snapshot } from "../snapshot"
|
||||
|
||||
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
||||
return Instance.provide(directory, async () => {
|
||||
await Plugin.init()
|
||||
Share.init()
|
||||
Format.init()
|
||||
LSP.init()
|
||||
Snapshot.init()
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
fn: async () => {
|
||||
const result = await cb()
|
||||
await Instance.dispose()
|
||||
return result
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ const AgentCreateCommand = cmd({
|
||||
command: "create",
|
||||
describe: "create a new agent",
|
||||
async handler() {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
prompts.intro("Create agent")
|
||||
const project = Instance.project
|
||||
@@ -126,6 +128,7 @@ const AgentCreateCommand = cmd({
|
||||
|
||||
prompts.log.success(`Agent created: ${filePath}`)
|
||||
prompts.outro("Done")
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -74,7 +74,9 @@ export const AuthLoginCommand = cmd({
|
||||
type: "string",
|
||||
}),
|
||||
async handler(args) {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
prompts.intro("Add credential")
|
||||
if (args.url) {
|
||||
@@ -264,6 +266,7 @@ export const AuthLoginCommand = cmd({
|
||||
})
|
||||
|
||||
prompts.outro("Done")
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -21,7 +21,9 @@ export const GithubInstallCommand = cmd({
|
||||
command: "install",
|
||||
describe: "install the GitHub agent",
|
||||
async handler() {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
prompts.intro("Install GitHub agent")
|
||||
const app = await getAppInfo()
|
||||
@@ -190,7 +192,9 @@ export const GithubInstallCommand = cmd({
|
||||
s.stop("Installed GitHub app")
|
||||
|
||||
async function getInstallation() {
|
||||
return await fetch(`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`)
|
||||
return await fetch(
|
||||
`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`,
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.installation)
|
||||
}
|
||||
@@ -235,6 +239,7 @@ jobs:
|
||||
|
||||
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -6,7 +6,9 @@ export const ModelsCommand = cmd({
|
||||
command: "models",
|
||||
describe: "list all available models",
|
||||
handler: async () => {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
const providers = await Provider.list()
|
||||
|
||||
for (const [providerID, provider] of Object.entries(providers)) {
|
||||
@@ -14,6 +16,7 @@ export const ModelsCommand = cmd({
|
||||
console.log(`${providerID}/${modelID}`)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Global } from "../../global"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { Server } from "../../server/server"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import path from "path"
|
||||
@@ -16,6 +15,7 @@ import { Ide } from "../../ide"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Session } from "../../session"
|
||||
import { $ } from "bun"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_TUI_PATH: string
|
||||
|
||||
13
packages/opencode/src/project/bootstrap.ts
Normal file
13
packages/opencode/src/project/bootstrap.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Plugin } from "../plugin"
|
||||
import { Share } from "../share/share"
|
||||
import { Format } from "../format"
|
||||
import { LSP } from "../lsp"
|
||||
import { Snapshot } from "../snapshot"
|
||||
|
||||
export async function InstanceBootstrap() {
|
||||
await Plugin.init()
|
||||
Share.init()
|
||||
Format.init()
|
||||
LSP.init()
|
||||
Snapshot.init()
|
||||
}
|
||||
@@ -2,12 +2,32 @@ import { Context } from "../util/context"
|
||||
import { Project } from "./project"
|
||||
import { State } from "./state"
|
||||
|
||||
const context = Context.create<{ directory: string; worktree: string; project: Project.Info }>("path")
|
||||
interface Context {
|
||||
directory: string
|
||||
worktree: string
|
||||
project: Project.Info
|
||||
}
|
||||
const context = Context.create<Context>("instance")
|
||||
const cache = new Map<string, Context>()
|
||||
|
||||
export const Instance = {
|
||||
async provide<R>(directory: string, cb: () => R): Promise<R> {
|
||||
const project = await Project.fromDirectory(directory)
|
||||
return context.provide({ directory, worktree: project.worktree, project }, cb)
|
||||
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
|
||||
let existing = cache.get(input.directory)
|
||||
if (!existing) {
|
||||
const project = await Project.fromDirectory(input.directory)
|
||||
existing = {
|
||||
directory: input.directory,
|
||||
worktree: project.worktree,
|
||||
project,
|
||||
}
|
||||
}
|
||||
return context.provide(existing, async () => {
|
||||
if (!cache.has(input.directory)) {
|
||||
await input.init?.()
|
||||
cache.set(input.directory, existing)
|
||||
}
|
||||
return input.fn()
|
||||
})
|
||||
},
|
||||
get directory() {
|
||||
return context.use().directory
|
||||
|
||||
@@ -22,10 +22,8 @@ export namespace Project {
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
const cache = new Map<string, Info>()
|
||||
export async function fromDirectory(directory: string) {
|
||||
log.info("fromDirectory", { directory })
|
||||
const fn = async () => {
|
||||
const matches = Filesystem.up({ targets: [".git"], start: directory })
|
||||
const git = await matches.next().then((x) => x.value)
|
||||
await matches.return()
|
||||
@@ -83,13 +81,6 @@ export namespace Project {
|
||||
await Storage.write<Info>(["project", id], project)
|
||||
return project
|
||||
}
|
||||
if (cache.has(directory)) {
|
||||
return cache.get(directory)!
|
||||
}
|
||||
const result = await fn()
|
||||
cache.set(directory, result)
|
||||
return result
|
||||
}
|
||||
|
||||
export async function setInitialized(projectID: string) {
|
||||
await Storage.update<Info>(["project", projectID], (draft) => {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { SessionPrompt } from "../session/prompt"
|
||||
import { SessionCompaction } from "../session/compaction"
|
||||
import { SessionRevert } from "../session/revert"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { InstanceBootstrap } from "../project/bootstrap"
|
||||
|
||||
const ERRORS = {
|
||||
400: {
|
||||
@@ -90,8 +91,12 @@ export namespace Server {
|
||||
})
|
||||
.use(async (c, next) => {
|
||||
const directory = c.req.query("directory") ?? process.cwd()
|
||||
return Instance.provide(directory, async () => {
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
async fn() {
|
||||
return next()
|
||||
},
|
||||
})
|
||||
})
|
||||
.use(cors())
|
||||
|
||||
@@ -27,19 +27,19 @@ async function bootstrap() {
|
||||
|
||||
test("tracks deleted files correctly", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`rm ${tmp.dir}/a.txt`.quiet()
|
||||
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/a.txt`)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("revert should remove new files", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -48,12 +48,12 @@ test("revert should remove new files", async () => {
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
|
||||
expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(false)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("revert in subdirectory", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -65,12 +65,12 @@ test("revert in subdirectory", async () => {
|
||||
expect(await Bun.file(`${tmp.dir}/sub/file.txt`).exists()).toBe(false)
|
||||
// Note: revert currently only removes files, not directories
|
||||
// The empty subdirectory will remain
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("multiple file operations", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -87,24 +87,24 @@ test("multiple file operations", async () => {
|
||||
// Note: revert currently only removes files, not directories
|
||||
// The empty directory will remain
|
||||
expect(await Bun.file(`${tmp.dir}/b.txt`).text()).toBe(tmp.bContent)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("empty directory handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`mkdir ${tmp.dir}/empty`.quiet()
|
||||
|
||||
expect((await Snapshot.patch(before!)).files.length).toBe(0)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("binary file handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -115,36 +115,36 @@ test("binary file handling", async () => {
|
||||
|
||||
await Snapshot.revert([patch])
|
||||
expect(await Bun.file(`${tmp.dir}/image.png`).exists()).toBe(false)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("symlink handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`ln -s ${tmp.dir}/a.txt ${tmp.dir}/link.txt`.quiet()
|
||||
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/link.txt`)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("large file handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Bun.write(`${tmp.dir}/large.txt`, "x".repeat(1024 * 1024))
|
||||
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/large.txt`)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("nested directory revert", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -154,12 +154,12 @@ test("nested directory revert", async () => {
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
|
||||
expect(await Bun.file(`${tmp.dir}/level1/level2/level3/deep.txt`).exists()).toBe(false)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("special characters in filenames", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -171,23 +171,23 @@ test("special characters in filenames", async () => {
|
||||
expect(files).toContain(`${tmp.dir}/file with spaces.txt`)
|
||||
expect(files).toContain(`${tmp.dir}/file-with-dashes.txt`)
|
||||
expect(files).toContain(`${tmp.dir}/file_with_underscores.txt`)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("revert with empty patches", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
// Should not crash with empty patches
|
||||
expect(Snapshot.revert([])).resolves.toBeUndefined()
|
||||
|
||||
// Should not crash with patches that have empty file lists
|
||||
expect(Snapshot.revert([{ hash: "dummy", files: [] }])).resolves.toBeUndefined()
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("patch with invalid hash", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -198,12 +198,12 @@ test("patch with invalid hash", async () => {
|
||||
const patch = await Snapshot.patch("invalid-hash-12345")
|
||||
expect(patch.files).toEqual([])
|
||||
expect(patch.hash).toBe("invalid-hash-12345")
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("revert non-existent file", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -217,12 +217,12 @@ test("revert non-existent file", async () => {
|
||||
},
|
||||
]),
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("unicode filenames", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -244,12 +244,12 @@ test("unicode filenames", async () => {
|
||||
|
||||
// Skip revert test due to git filename escaping issues
|
||||
// The functionality works but git uses escaped filenames internally
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("very long filenames", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -263,12 +263,12 @@ test("very long filenames", async () => {
|
||||
|
||||
await Snapshot.revert([patch])
|
||||
expect(await Bun.file(longFile).exists()).toBe(false)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("hidden files", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -280,12 +280,12 @@ test("hidden files", async () => {
|
||||
expect(patch.files).toContain(`${tmp.dir}/.hidden`)
|
||||
expect(patch.files).toContain(`${tmp.dir}/.gitignore`)
|
||||
expect(patch.files).toContain(`${tmp.dir}/.config`)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("nested symlinks", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -297,12 +297,12 @@ test("nested symlinks", async () => {
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.dir}/sub/dir/link.txt`)
|
||||
expect(patch.files).toContain(`${tmp.dir}/sub-link`)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("file permissions and ownership changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -315,12 +315,12 @@ test("file permissions and ownership changes", async () => {
|
||||
// Note: git doesn't track permission changes on existing files by default
|
||||
// Only tracks executable bit when files are first added
|
||||
expect(patch.files.length).toBe(0)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("circular symlinks", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -329,12 +329,12 @@ test("circular symlinks", async () => {
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files.length).toBeGreaterThanOrEqual(0) // Should not crash
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("gitignore changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -350,12 +350,12 @@ test("gitignore changes", async () => {
|
||||
expect(patch.files).toContain(`${tmp.dir}/normal.txt`)
|
||||
// Should not track ignored files (git won't see them)
|
||||
expect(patch.files).not.toContain(`${tmp.dir}/test.ignored`)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("concurrent file operations during patch", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -376,7 +376,7 @@ test("concurrent file operations during patch", async () => {
|
||||
|
||||
// Should capture some or all of the concurrent files
|
||||
expect(patch.files.length).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("snapshot state isolation between projects", async () => {
|
||||
@@ -384,14 +384,14 @@ test("snapshot state isolation between projects", async () => {
|
||||
await using tmp1 = await bootstrap()
|
||||
await using tmp2 = await bootstrap()
|
||||
|
||||
await Instance.provide(tmp1.dir, async () => {
|
||||
await Instance.provide({ directory: tmp1.dir, fn: async () => {
|
||||
const before1 = await Snapshot.track()
|
||||
await Bun.write(`${tmp1.dir}/project1.txt`, "project1 content")
|
||||
const patch1 = await Snapshot.patch(before1!)
|
||||
expect(patch1.files).toContain(`${tmp1.dir}/project1.txt`)
|
||||
})
|
||||
}})
|
||||
|
||||
await Instance.provide(tmp2.dir, async () => {
|
||||
await Instance.provide({ directory: tmp2.dir, fn: async () => {
|
||||
const before2 = await Snapshot.track()
|
||||
await Bun.write(`${tmp2.dir}/project2.txt`, "project2 content")
|
||||
const patch2 = await Snapshot.patch(before2!)
|
||||
@@ -399,12 +399,12 @@ test("snapshot state isolation between projects", async () => {
|
||||
|
||||
// Ensure project1 files don't appear in project2
|
||||
expect(patch2.files).not.toContain(`${tmp1?.dir}/project1.txt`)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("track with no changes returns same hash", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const hash1 = await Snapshot.track()
|
||||
expect(hash1).toBeTruthy()
|
||||
|
||||
@@ -415,12 +415,12 @@ test("track with no changes returns same hash", async () => {
|
||||
// Track again
|
||||
const hash3 = await Snapshot.track()
|
||||
expect(hash3).toBe(hash1!)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("diff function with various changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -433,12 +433,12 @@ test("diff function with various changes", async () => {
|
||||
expect(diff).toContain("deleted")
|
||||
expect(diff).toContain("modified")
|
||||
// Note: git diff only shows changes to tracked files, not untracked files like new.txt
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
test("restore function", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide(tmp.dir, async () => {
|
||||
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -454,5 +454,5 @@ test("restore function", async () => {
|
||||
expect(await Bun.file(`${tmp.dir}/a.txt`).text()).toBe(tmp.aContent)
|
||||
expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(true) // New files should remain
|
||||
expect(await Bun.file(`${tmp.dir}/b.txt`).text()).toBe(tmp.bContent)
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
@@ -19,7 +19,9 @@ Log.init({ print: false })
|
||||
|
||||
describe("tool.bash", () => {
|
||||
test("basic", async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const result = await bash.execute(
|
||||
{
|
||||
command: "echo 'test'",
|
||||
@@ -29,11 +31,14 @@ describe("tool.bash", () => {
|
||||
)
|
||||
expect(result.metadata.exit).toBe(0)
|
||||
expect(result.metadata.output).toContain("test")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("cd ../ should fail outside of project root", async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
expect(
|
||||
bash.execute(
|
||||
{
|
||||
@@ -43,6 +48,7 @@ describe("tool.bash", () => {
|
||||
ctx,
|
||||
),
|
||||
).rejects.toThrow("This command references paths outside of")
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,7 +20,9 @@ const fixturePath = path.join(__dirname, "../fixtures/example")
|
||||
|
||||
describe("tool.glob", () => {
|
||||
test("truncate", async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "**/*",
|
||||
@@ -29,10 +31,13 @@ describe("tool.glob", () => {
|
||||
ctx,
|
||||
)
|
||||
expect(result.metadata.truncated).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
test("basic", async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "*.json",
|
||||
@@ -44,14 +49,18 @@ describe("tool.glob", () => {
|
||||
truncated: false,
|
||||
count: 2,
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool.ls", () => {
|
||||
test("basic", async () => {
|
||||
const result = await Instance.provide(projectRoot, async () => {
|
||||
const result = await Instance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
return await list.execute({ path: fixturePath, ignore: [".git"] }, ctx)
|
||||
},
|
||||
})
|
||||
|
||||
// Normalize absolute path to relative for consistent snapshots
|
||||
|
||||
Reference in New Issue
Block a user