@stonerzju/opencode 1.2.16-offline.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 +10 -0
- package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
- package/Dockerfile +18 -0
- package/README.md +15 -0
- package/bin/opencode +179 -0
- package/bunfig.toml +7 -0
- package/drizzle.config.ts +10 -0
- package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
- package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
- package/migration/20260211171708_add_project_commands/migration.sql +1 -0
- package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
- package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
- package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
- package/migration/20260225215848_workspace/migration.sql +7 -0
- package/migration/20260225215848_workspace/snapshot.json +959 -0
- package/package.json +140 -0
- package/package.json.bak +140 -0
- package/parsers-config.ts +254 -0
- package/script/build.ts +224 -0
- package/script/check-migrations.ts +16 -0
- package/script/postinstall.mjs +131 -0
- package/script/publish.ts +181 -0
- package/script/schema.ts +63 -0
- package/script/seed-e2e.ts +50 -0
- package/src/acp/README.md +174 -0
- package/src/acp/agent.ts +1741 -0
- package/src/acp/session.ts +116 -0
- package/src/acp/types.ts +23 -0
- package/src/agent/agent.ts +339 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/compaction.txt +14 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +11 -0
- package/src/agent/prompt/title.txt +44 -0
- package/src/auth/index.ts +68 -0
- package/src/bun/index.ts +131 -0
- package/src/bun/registry.ts +50 -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 +70 -0
- package/src/cli/cmd/agent.ts +257 -0
- package/src/cli/cmd/auth.ts +449 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/db.ts +118 -0
- package/src/cli/cmd/debug/agent.ts +167 -0
- package/src/cli/cmd/debug/config.ts +16 -0
- package/src/cli/cmd/debug/file.ts +97 -0
- package/src/cli/cmd/debug/index.ts +48 -0
- package/src/cli/cmd/debug/lsp.ts +52 -0
- package/src/cli/cmd/debug/ripgrep.ts +87 -0
- package/src/cli/cmd/debug/scrap.ts +16 -0
- package/src/cli/cmd/debug/skill.ts +16 -0
- package/src/cli/cmd/debug/snapshot.ts +52 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/generate.ts +38 -0
- package/src/cli/cmd/github.ts +1631 -0
- package/src/cli/cmd/import.ts +170 -0
- package/src/cli/cmd/mcp.ts +754 -0
- package/src/cli/cmd/models.ts +77 -0
- package/src/cli/cmd/pr.ts +112 -0
- package/src/cli/cmd/run.ts +625 -0
- package/src/cli/cmd/serve.ts +31 -0
- package/src/cli/cmd/session.ts +156 -0
- package/src/cli/cmd/stats.ts +410 -0
- package/src/cli/cmd/tui/app.tsx +845 -0
- package/src/cli/cmd/tui/attach.ts +88 -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 +147 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +165 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +259 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
- package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +167 -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/logo.tsx +85 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +667 -0
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +1155 -0
- package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
- package/src/cli/cmd/tui/component/spinner.tsx +24 -0
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
- package/src/cli/cmd/tui/component/tips.tsx +152 -0
- package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
- package/src/cli/cmd/tui/context/args.tsx +15 -0
- package/src/cli/cmd/tui/context/directory.ts +13 -0
- package/src/cli/cmd/tui/context/exit.tsx +53 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +102 -0
- package/src/cli/cmd/tui/context/kv.tsx +52 -0
- package/src/cli/cmd/tui/context/local.tsx +406 -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 +101 -0
- package/src/cli/cmd/tui/context/sync.tsx +488 -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/carbonfox.json +248 -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 +242 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
- package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -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 +249 -0
- package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -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 +1152 -0
- package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
- package/src/cli/cmd/tui/event.ts +48 -0
- package/src/cli/cmd/tui/routes/home.tsx +145 -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 +91 -0
- package/src/cli/cmd/tui/routes/session/header.tsx +135 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +2219 -0
- package/src/cli/cmd/tui/routes/session/permission.tsx +685 -0
- package/src/cli/cmd/tui/routes/session/question.tsx +466 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +321 -0
- package/src/cli/cmd/tui/thread.ts +199 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +59 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +85 -0
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +207 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +40 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +80 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +401 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +182 -0
- package/src/cli/cmd/tui/ui/link.tsx +28 -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 +164 -0
- package/src/cli/cmd/tui/util/editor.ts +33 -0
- package/src/cli/cmd/tui/util/selection.ts +25 -0
- package/src/cli/cmd/tui/util/signal.ts +7 -0
- package/src/cli/cmd/tui/util/terminal.ts +114 -0
- package/src/cli/cmd/tui/util/transcript.ts +98 -0
- package/src/cli/cmd/tui/win32.ts +129 -0
- package/src/cli/cmd/tui/worker.ts +157 -0
- package/src/cli/cmd/uninstall.ts +356 -0
- package/src/cli/cmd/upgrade.ts +73 -0
- package/src/cli/cmd/web.ts +81 -0
- package/src/cli/cmd/workspace-serve.ts +16 -0
- package/src/cli/error.ts +57 -0
- package/src/cli/logo.ts +6 -0
- package/src/cli/network.ts +60 -0
- package/src/cli/ui.ts +116 -0
- package/src/cli/upgrade.ts +25 -0
- package/src/command/index.ts +150 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/command/template/review.txt +101 -0
- package/src/config/config.ts +1408 -0
- package/src/config/markdown.ts +99 -0
- package/src/config/migrate-tui-config.ts +155 -0
- package/src/config/paths.ts +174 -0
- package/src/config/tui-schema.ts +34 -0
- package/src/config/tui.ts +118 -0
- package/src/control/control.sql.ts +22 -0
- package/src/control/index.ts +67 -0
- package/src/control-plane/adaptors/index.ts +10 -0
- package/src/control-plane/adaptors/types.ts +7 -0
- package/src/control-plane/adaptors/worktree.ts +26 -0
- package/src/control-plane/config.ts +10 -0
- package/src/control-plane/session-proxy-middleware.ts +46 -0
- package/src/control-plane/sse.ts +66 -0
- package/src/control-plane/workspace-server/routes.ts +33 -0
- package/src/control-plane/workspace-server/server.ts +24 -0
- package/src/control-plane/workspace.sql.ts +12 -0
- package/src/control-plane/workspace.ts +160 -0
- package/src/env/index.ts +28 -0
- package/src/file/ignore.ts +82 -0
- package/src/file/index.ts +646 -0
- package/src/file/ripgrep.ts +372 -0
- package/src/file/time.ts +71 -0
- package/src/file/watcher.ts +128 -0
- package/src/flag/flag.ts +109 -0
- package/src/format/formatter.ts +395 -0
- package/src/format/index.ts +140 -0
- package/src/global/index.ts +54 -0
- package/src/id/id.ts +84 -0
- package/src/ide/index.ts +76 -0
- package/src/index.ts +210 -0
- package/src/installation/index.ts +266 -0
- package/src/lsp/client.ts +251 -0
- package/src/lsp/index.ts +485 -0
- package/src/lsp/language.ts +120 -0
- package/src/lsp/server.ts +2142 -0
- package/src/mcp/auth.ts +130 -0
- package/src/mcp/index.ts +937 -0
- package/src/mcp/oauth-callback.ts +200 -0
- package/src/mcp/oauth-provider.ts +176 -0
- package/src/patch/index.ts +680 -0
- package/src/permission/arity.ts +163 -0
- package/src/permission/index.ts +210 -0
- package/src/permission/next.ts +286 -0
- package/src/plugin/codex.ts +624 -0
- package/src/plugin/copilot.ts +327 -0
- package/src/plugin/index.ts +143 -0
- package/src/project/bootstrap.ts +33 -0
- package/src/project/instance.ts +114 -0
- package/src/project/project.sql.ts +15 -0
- package/src/project/project.ts +441 -0
- package/src/project/state.ts +70 -0
- package/src/project/vcs.ts +76 -0
- package/src/provider/auth.ts +147 -0
- package/src/provider/error.ts +189 -0
- package/src/provider/models.ts +146 -0
- package/src/provider/provider.ts +1338 -0
- package/src/provider/sdk/copilot/README.md +5 -0
- package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +164 -0
- package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
- package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +17 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +780 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +87 -0
- package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
- package/src/provider/sdk/copilot/index.ts +2 -0
- package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
- package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +303 -0
- package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
- package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +207 -0
- package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1732 -0
- package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +177 -0
- package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
- package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +88 -0
- package/src/provider/sdk/copilot/responses/tool/file-search.ts +128 -0
- package/src/provider/sdk/copilot/responses/tool/image-generation.ts +115 -0
- package/src/provider/sdk/copilot/responses/tool/local-shell.ts +65 -0
- package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +104 -0
- package/src/provider/sdk/copilot/responses/tool/web-search.ts +103 -0
- package/src/provider/transform.ts +955 -0
- package/src/pty/index.ts +324 -0
- package/src/question/index.ts +171 -0
- package/src/scheduler/index.ts +61 -0
- package/src/server/error.ts +36 -0
- package/src/server/event.ts +7 -0
- package/src/server/mdns.ts +60 -0
- package/src/server/routes/config.ts +92 -0
- package/src/server/routes/experimental.ts +270 -0
- package/src/server/routes/file.ts +197 -0
- package/src/server/routes/global.ts +185 -0
- package/src/server/routes/mcp.ts +225 -0
- package/src/server/routes/permission.ts +68 -0
- package/src/server/routes/project.ts +82 -0
- package/src/server/routes/provider.ts +165 -0
- package/src/server/routes/pty.ts +200 -0
- package/src/server/routes/question.ts +98 -0
- package/src/server/routes/session.ts +974 -0
- package/src/server/routes/tui.ts +379 -0
- package/src/server/routes/workspace.ts +104 -0
- package/src/server/server.ts +623 -0
- package/src/session/compaction.ts +261 -0
- package/src/session/index.ts +877 -0
- package/src/session/instruction.ts +192 -0
- package/src/session/llm.ts +279 -0
- package/src/session/message-v2.ts +899 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +421 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +105 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex_header.txt +79 -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/qwen.txt +109 -0
- package/src/session/prompt/trinity.txt +97 -0
- package/src/session/prompt.ts +1959 -0
- package/src/session/retry.ts +101 -0
- package/src/session/revert.ts +138 -0
- package/src/session/session.sql.ts +88 -0
- package/src/session/status.ts +76 -0
- package/src/session/summary.ts +161 -0
- package/src/session/system.ts +54 -0
- package/src/session/todo.ts +56 -0
- package/src/share/share-next.ts +210 -0
- package/src/share/share.sql.ts +13 -0
- package/src/shell/shell.ts +68 -0
- package/src/skill/discovery.ts +98 -0
- package/src/skill/index.ts +1 -0
- package/src/skill/skill.ts +189 -0
- package/src/snapshot/index.ts +297 -0
- package/src/sql.d.ts +4 -0
- package/src/storage/db.ts +155 -0
- package/src/storage/json-migration.ts +425 -0
- package/src/storage/schema.sql.ts +10 -0
- package/src/storage/schema.ts +5 -0
- package/src/storage/storage.ts +220 -0
- package/src/tool/apply_patch.ts +281 -0
- package/src/tool/apply_patch.txt +33 -0
- package/src/tool/bash.ts +274 -0
- package/src/tool/bash.txt +115 -0
- package/src/tool/batch.ts +181 -0
- package/src/tool/batch.txt +24 -0
- package/src/tool/codesearch.ts +132 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +654 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/external-directory.ts +32 -0
- package/src/tool/glob.ts +78 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +156 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +121 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/lsp.ts +97 -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/plan-enter.txt +14 -0
- package/src/tool/plan-exit.txt +13 -0
- package/src/tool/plan.ts +131 -0
- package/src/tool/question.ts +33 -0
- package/src/tool/question.txt +10 -0
- package/src/tool/read.ts +293 -0
- package/src/tool/read.txt +14 -0
- package/src/tool/registry.ts +173 -0
- package/src/tool/skill.ts +123 -0
- package/src/tool/task.ts +165 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +53 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +89 -0
- package/src/tool/truncation.ts +107 -0
- package/src/tool/webfetch.ts +206 -0
- package/src/tool/webfetch.txt +13 -0
- package/src/tool/websearch.ts +150 -0
- package/src/tool/websearch.txt +14 -0
- package/src/tool/write.ts +84 -0
- package/src/tool/write.txt +8 -0
- package/src/util/abort.ts +35 -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 +189 -0
- package/src/util/fn.ts +11 -0
- package/src/util/format.ts +20 -0
- package/src/util/git.ts +35 -0
- package/src/util/glob.ts +34 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +103 -0
- package/src/util/lazy.ts +23 -0
- package/src/util/locale.ts +81 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +182 -0
- package/src/util/process.ts +126 -0
- package/src/util/proxied.ts +3 -0
- package/src/util/queue.ts +32 -0
- package/src/util/rpc.ts +66 -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 +59 -0
- package/src/worktree/index.ts +643 -0
- package/sst-env.d.ts +10 -0
- package/test/AGENTS.md +81 -0
- package/test/acp/agent-interface.test.ts +51 -0
- package/test/acp/event-subscription.test.ts +683 -0
- package/test/agent/agent.test.ts +689 -0
- package/test/bun.test.ts +53 -0
- package/test/cli/github-action.test.ts +197 -0
- package/test/cli/github-remote.test.ts +80 -0
- package/test/cli/import.test.ts +38 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/cli/tui/transcript.test.ts +322 -0
- package/test/config/agent-color.test.ts +71 -0
- package/test/config/config.test.ts +1886 -0
- package/test/config/fixtures/empty-frontmatter.md +4 -0
- package/test/config/fixtures/frontmatter.md +28 -0
- package/test/config/fixtures/markdown-header.md +11 -0
- package/test/config/fixtures/no-frontmatter.md +1 -0
- package/test/config/fixtures/weird-model-id.md +13 -0
- package/test/config/markdown.test.ts +228 -0
- package/test/config/tui.test.ts +510 -0
- package/test/control-plane/session-proxy-middleware.test.ts +147 -0
- package/test/control-plane/sse.test.ts +56 -0
- package/test/control-plane/workspace-server-sse.test.ts +65 -0
- package/test/control-plane/workspace-sync.test.ts +97 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/file/index.test.ts +394 -0
- package/test/file/path-traversal.test.ts +198 -0
- package/test/file/ripgrep.test.ts +39 -0
- package/test/file/time.test.ts +361 -0
- package/test/fixture/db.ts +11 -0
- package/test/fixture/fixture.ts +45 -0
- package/test/fixture/lsp/fake-lsp-server.js +77 -0
- package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
- package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
- package/test/fixture/skills/cloudflare/SKILL.md +211 -0
- package/test/fixture/skills/index.json +6 -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/mcp/oauth-browser.test.ts +249 -0
- package/test/memory/abort-leak.test.ts +136 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/permission/arity.test.ts +33 -0
- package/test/permission/next.test.ts +689 -0
- package/test/permission-task.test.ts +319 -0
- package/test/plugin/auth-override.test.ts +44 -0
- package/test/plugin/codex.test.ts +123 -0
- package/test/preload.ts +80 -0
- package/test/project/project.test.ts +348 -0
- package/test/project/worktree-remove.test.ts +65 -0
- package/test/provider/amazon-bedrock.test.ts +446 -0
- package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
- package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
- package/test/provider/gitlab-duo.test.ts +262 -0
- package/test/provider/provider.test.ts +2220 -0
- package/test/provider/transform.test.ts +2353 -0
- package/test/pty/pty-output-isolation.test.ts +140 -0
- package/test/question/question.test.ts +300 -0
- package/test/scheduler.test.ts +73 -0
- package/test/server/global-session-list.test.ts +89 -0
- package/test/server/session-list.test.ts +90 -0
- package/test/server/session-select.test.ts +78 -0
- package/test/session/compaction.test.ts +423 -0
- package/test/session/instruction.test.ts +170 -0
- package/test/session/llm.test.ts +667 -0
- package/test/session/message-v2.test.ts +924 -0
- package/test/session/prompt.test.ts +211 -0
- package/test/session/retry.test.ts +188 -0
- package/test/session/revert-compact.test.ts +285 -0
- package/test/session/session.test.ts +71 -0
- package/test/session/structured-output-integration.test.ts +233 -0
- package/test/session/structured-output.test.ts +385 -0
- package/test/skill/discovery.test.ts +110 -0
- package/test/skill/skill.test.ts +388 -0
- package/test/snapshot/snapshot.test.ts +1180 -0
- package/test/storage/json-migration.test.ts +846 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/apply_patch.test.ts +566 -0
- package/test/tool/bash.test.ts +402 -0
- package/test/tool/edit.test.ts +496 -0
- package/test/tool/external-directory.test.ts +127 -0
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +38413 -0
- package/test/tool/grep.test.ts +110 -0
- package/test/tool/question.test.ts +107 -0
- package/test/tool/read.test.ts +504 -0
- package/test/tool/registry.test.ts +122 -0
- package/test/tool/skill.test.ts +112 -0
- package/test/tool/truncation.test.ts +160 -0
- package/test/tool/webfetch.test.ts +100 -0
- package/test/tool/write.test.ts +348 -0
- package/test/util/filesystem.test.ts +443 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/glob.test.ts +164 -0
- package/test/util/iife.test.ts +36 -0
- package/test/util/lazy.test.ts +50 -0
- package/test/util/lock.test.ts +72 -0
- package/test/util/process.test.ts +59 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/wildcard.test.ts +90 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
import { test, expect } from "bun:test"
|
|
2
|
+
import { $ } from "bun"
|
|
3
|
+
import fs from "fs/promises"
|
|
4
|
+
import path from "path"
|
|
5
|
+
import { Snapshot } from "../../src/snapshot"
|
|
6
|
+
import { Instance } from "../../src/project/instance"
|
|
7
|
+
import { Filesystem } from "../../src/util/filesystem"
|
|
8
|
+
import { tmpdir } from "../fixture/fixture"
|
|
9
|
+
|
|
10
|
+
// Git always outputs /-separated paths internally. Snapshot.patch() joins them
|
|
11
|
+
// with path.join (which produces \ on Windows) then normalizes back to /.
|
|
12
|
+
// This helper does the same for expected values so assertions match cross-platform.
|
|
13
|
+
const fwd = (...parts: string[]) => path.join(...parts).replaceAll("\\", "/")
|
|
14
|
+
|
|
15
|
+
async function bootstrap() {
|
|
16
|
+
return tmpdir({
|
|
17
|
+
git: true,
|
|
18
|
+
init: async (dir) => {
|
|
19
|
+
const unique = Math.random().toString(36).slice(2)
|
|
20
|
+
const aContent = `A${unique}`
|
|
21
|
+
const bContent = `B${unique}`
|
|
22
|
+
await Filesystem.write(`${dir}/a.txt`, aContent)
|
|
23
|
+
await Filesystem.write(`${dir}/b.txt`, bContent)
|
|
24
|
+
await $`git add .`.cwd(dir).quiet()
|
|
25
|
+
await $`git commit --no-gpg-sign -m init`.cwd(dir).quiet()
|
|
26
|
+
return {
|
|
27
|
+
aContent,
|
|
28
|
+
bContent,
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test("tracks deleted files correctly", async () => {
|
|
35
|
+
await using tmp = await bootstrap()
|
|
36
|
+
await Instance.provide({
|
|
37
|
+
directory: tmp.path,
|
|
38
|
+
fn: async () => {
|
|
39
|
+
const before = await Snapshot.track()
|
|
40
|
+
expect(before).toBeTruthy()
|
|
41
|
+
|
|
42
|
+
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
43
|
+
|
|
44
|
+
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "a.txt"))
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("revert should remove new files", async () => {
|
|
50
|
+
await using tmp = await bootstrap()
|
|
51
|
+
await Instance.provide({
|
|
52
|
+
directory: tmp.path,
|
|
53
|
+
fn: async () => {
|
|
54
|
+
const before = await Snapshot.track()
|
|
55
|
+
expect(before).toBeTruthy()
|
|
56
|
+
|
|
57
|
+
await Filesystem.write(`${tmp.path}/new.txt`, "NEW")
|
|
58
|
+
|
|
59
|
+
await Snapshot.revert([await Snapshot.patch(before!)])
|
|
60
|
+
|
|
61
|
+
expect(
|
|
62
|
+
await fs
|
|
63
|
+
.access(`${tmp.path}/new.txt`)
|
|
64
|
+
.then(() => true)
|
|
65
|
+
.catch(() => false),
|
|
66
|
+
).toBe(false)
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test("revert in subdirectory", async () => {
|
|
72
|
+
await using tmp = await bootstrap()
|
|
73
|
+
await Instance.provide({
|
|
74
|
+
directory: tmp.path,
|
|
75
|
+
fn: async () => {
|
|
76
|
+
const before = await Snapshot.track()
|
|
77
|
+
expect(before).toBeTruthy()
|
|
78
|
+
|
|
79
|
+
await $`mkdir -p ${tmp.path}/sub`.quiet()
|
|
80
|
+
await Filesystem.write(`${tmp.path}/sub/file.txt`, "SUB")
|
|
81
|
+
|
|
82
|
+
await Snapshot.revert([await Snapshot.patch(before!)])
|
|
83
|
+
|
|
84
|
+
expect(
|
|
85
|
+
await fs
|
|
86
|
+
.access(`${tmp.path}/sub/file.txt`)
|
|
87
|
+
.then(() => true)
|
|
88
|
+
.catch(() => false),
|
|
89
|
+
).toBe(false)
|
|
90
|
+
// Note: revert currently only removes files, not directories
|
|
91
|
+
// The empty subdirectory will remain
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("multiple file operations", async () => {
|
|
97
|
+
await using tmp = await bootstrap()
|
|
98
|
+
await Instance.provide({
|
|
99
|
+
directory: tmp.path,
|
|
100
|
+
fn: async () => {
|
|
101
|
+
const before = await Snapshot.track()
|
|
102
|
+
expect(before).toBeTruthy()
|
|
103
|
+
|
|
104
|
+
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
105
|
+
await Filesystem.write(`${tmp.path}/c.txt`, "C")
|
|
106
|
+
await $`mkdir -p ${tmp.path}/dir`.quiet()
|
|
107
|
+
await Filesystem.write(`${tmp.path}/dir/d.txt`, "D")
|
|
108
|
+
await Filesystem.write(`${tmp.path}/b.txt`, "MODIFIED")
|
|
109
|
+
|
|
110
|
+
await Snapshot.revert([await Snapshot.patch(before!)])
|
|
111
|
+
|
|
112
|
+
expect(await fs.readFile(`${tmp.path}/a.txt`, "utf-8")).toBe(tmp.extra.aContent)
|
|
113
|
+
expect(
|
|
114
|
+
await fs
|
|
115
|
+
.access(`${tmp.path}/c.txt`)
|
|
116
|
+
.then(() => true)
|
|
117
|
+
.catch(() => false),
|
|
118
|
+
).toBe(false)
|
|
119
|
+
// Note: revert currently only removes files, not directories
|
|
120
|
+
// The empty directory will remain
|
|
121
|
+
expect(await fs.readFile(`${tmp.path}/b.txt`, "utf-8")).toBe(tmp.extra.bContent)
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test("empty directory handling", async () => {
|
|
127
|
+
await using tmp = await bootstrap()
|
|
128
|
+
await Instance.provide({
|
|
129
|
+
directory: tmp.path,
|
|
130
|
+
fn: async () => {
|
|
131
|
+
const before = await Snapshot.track()
|
|
132
|
+
expect(before).toBeTruthy()
|
|
133
|
+
|
|
134
|
+
await $`mkdir ${tmp.path}/empty`.quiet()
|
|
135
|
+
|
|
136
|
+
expect((await Snapshot.patch(before!)).files.length).toBe(0)
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test("binary file handling", async () => {
|
|
142
|
+
await using tmp = await bootstrap()
|
|
143
|
+
await Instance.provide({
|
|
144
|
+
directory: tmp.path,
|
|
145
|
+
fn: async () => {
|
|
146
|
+
const before = await Snapshot.track()
|
|
147
|
+
expect(before).toBeTruthy()
|
|
148
|
+
|
|
149
|
+
await Filesystem.write(`${tmp.path}/image.png`, new Uint8Array([0x89, 0x50, 0x4e, 0x47]))
|
|
150
|
+
|
|
151
|
+
const patch = await Snapshot.patch(before!)
|
|
152
|
+
expect(patch.files).toContain(fwd(tmp.path, "image.png"))
|
|
153
|
+
|
|
154
|
+
await Snapshot.revert([patch])
|
|
155
|
+
expect(
|
|
156
|
+
await fs
|
|
157
|
+
.access(`${tmp.path}/image.png`)
|
|
158
|
+
.then(() => true)
|
|
159
|
+
.catch(() => false),
|
|
160
|
+
).toBe(false)
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test("symlink handling", async () => {
|
|
166
|
+
await using tmp = await bootstrap()
|
|
167
|
+
await Instance.provide({
|
|
168
|
+
directory: tmp.path,
|
|
169
|
+
fn: async () => {
|
|
170
|
+
const before = await Snapshot.track()
|
|
171
|
+
expect(before).toBeTruthy()
|
|
172
|
+
|
|
173
|
+
await fs.symlink(`${tmp.path}/a.txt`, `${tmp.path}/link.txt`, "file")
|
|
174
|
+
|
|
175
|
+
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "link.txt"))
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test("large file handling", async () => {
|
|
181
|
+
await using tmp = await bootstrap()
|
|
182
|
+
await Instance.provide({
|
|
183
|
+
directory: tmp.path,
|
|
184
|
+
fn: async () => {
|
|
185
|
+
const before = await Snapshot.track()
|
|
186
|
+
expect(before).toBeTruthy()
|
|
187
|
+
|
|
188
|
+
await Filesystem.write(`${tmp.path}/large.txt`, "x".repeat(1024 * 1024))
|
|
189
|
+
|
|
190
|
+
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "large.txt"))
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test("nested directory revert", async () => {
|
|
196
|
+
await using tmp = await bootstrap()
|
|
197
|
+
await Instance.provide({
|
|
198
|
+
directory: tmp.path,
|
|
199
|
+
fn: async () => {
|
|
200
|
+
const before = await Snapshot.track()
|
|
201
|
+
expect(before).toBeTruthy()
|
|
202
|
+
|
|
203
|
+
await $`mkdir -p ${tmp.path}/level1/level2/level3`.quiet()
|
|
204
|
+
await Filesystem.write(`${tmp.path}/level1/level2/level3/deep.txt`, "DEEP")
|
|
205
|
+
|
|
206
|
+
await Snapshot.revert([await Snapshot.patch(before!)])
|
|
207
|
+
|
|
208
|
+
expect(
|
|
209
|
+
await fs
|
|
210
|
+
.access(`${tmp.path}/level1/level2/level3/deep.txt`)
|
|
211
|
+
.then(() => true)
|
|
212
|
+
.catch(() => false),
|
|
213
|
+
).toBe(false)
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test("special characters in filenames", async () => {
|
|
219
|
+
await using tmp = await bootstrap()
|
|
220
|
+
await Instance.provide({
|
|
221
|
+
directory: tmp.path,
|
|
222
|
+
fn: async () => {
|
|
223
|
+
const before = await Snapshot.track()
|
|
224
|
+
expect(before).toBeTruthy()
|
|
225
|
+
|
|
226
|
+
await Filesystem.write(`${tmp.path}/file with spaces.txt`, "SPACES")
|
|
227
|
+
await Filesystem.write(`${tmp.path}/file-with-dashes.txt`, "DASHES")
|
|
228
|
+
await Filesystem.write(`${tmp.path}/file_with_underscores.txt`, "UNDERSCORES")
|
|
229
|
+
|
|
230
|
+
const files = (await Snapshot.patch(before!)).files
|
|
231
|
+
expect(files).toContain(fwd(tmp.path, "file with spaces.txt"))
|
|
232
|
+
expect(files).toContain(fwd(tmp.path, "file-with-dashes.txt"))
|
|
233
|
+
expect(files).toContain(fwd(tmp.path, "file_with_underscores.txt"))
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test("revert with empty patches", async () => {
|
|
239
|
+
await using tmp = await bootstrap()
|
|
240
|
+
await Instance.provide({
|
|
241
|
+
directory: tmp.path,
|
|
242
|
+
fn: async () => {
|
|
243
|
+
// Should not crash with empty patches
|
|
244
|
+
expect(Snapshot.revert([])).resolves.toBeUndefined()
|
|
245
|
+
|
|
246
|
+
// Should not crash with patches that have empty file lists
|
|
247
|
+
expect(Snapshot.revert([{ hash: "dummy", files: [] }])).resolves.toBeUndefined()
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test("patch with invalid hash", async () => {
|
|
253
|
+
await using tmp = await bootstrap()
|
|
254
|
+
await Instance.provide({
|
|
255
|
+
directory: tmp.path,
|
|
256
|
+
fn: async () => {
|
|
257
|
+
const before = await Snapshot.track()
|
|
258
|
+
expect(before).toBeTruthy()
|
|
259
|
+
|
|
260
|
+
// Create a change
|
|
261
|
+
await Filesystem.write(`${tmp.path}/test.txt`, "TEST")
|
|
262
|
+
|
|
263
|
+
// Try to patch with invalid hash - should handle gracefully
|
|
264
|
+
const patch = await Snapshot.patch("invalid-hash-12345")
|
|
265
|
+
expect(patch.files).toEqual([])
|
|
266
|
+
expect(patch.hash).toBe("invalid-hash-12345")
|
|
267
|
+
},
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test("revert non-existent file", async () => {
|
|
272
|
+
await using tmp = await bootstrap()
|
|
273
|
+
await Instance.provide({
|
|
274
|
+
directory: tmp.path,
|
|
275
|
+
fn: async () => {
|
|
276
|
+
const before = await Snapshot.track()
|
|
277
|
+
expect(before).toBeTruthy()
|
|
278
|
+
|
|
279
|
+
// Try to revert a file that doesn't exist in the snapshot
|
|
280
|
+
// This should not crash
|
|
281
|
+
expect(
|
|
282
|
+
Snapshot.revert([
|
|
283
|
+
{
|
|
284
|
+
hash: before!,
|
|
285
|
+
files: [`${tmp.path}/nonexistent.txt`],
|
|
286
|
+
},
|
|
287
|
+
]),
|
|
288
|
+
).resolves.toBeUndefined()
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test("unicode filenames", async () => {
|
|
294
|
+
await using tmp = await bootstrap()
|
|
295
|
+
await Instance.provide({
|
|
296
|
+
directory: tmp.path,
|
|
297
|
+
fn: async () => {
|
|
298
|
+
const before = await Snapshot.track()
|
|
299
|
+
expect(before).toBeTruthy()
|
|
300
|
+
|
|
301
|
+
const unicodeFiles = [
|
|
302
|
+
{ path: fwd(tmp.path, "文件.txt"), content: "chinese content" },
|
|
303
|
+
{ path: fwd(tmp.path, "🚀rocket.txt"), content: "emoji content" },
|
|
304
|
+
{ path: fwd(tmp.path, "café.txt"), content: "accented content" },
|
|
305
|
+
{ path: fwd(tmp.path, "файл.txt"), content: "cyrillic content" },
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
for (const file of unicodeFiles) {
|
|
309
|
+
await Filesystem.write(file.path, file.content)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const patch = await Snapshot.patch(before!)
|
|
313
|
+
expect(patch.files.length).toBe(4)
|
|
314
|
+
|
|
315
|
+
for (const file of unicodeFiles) {
|
|
316
|
+
expect(patch.files).toContain(file.path)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
await Snapshot.revert([patch])
|
|
320
|
+
|
|
321
|
+
for (const file of unicodeFiles) {
|
|
322
|
+
expect(
|
|
323
|
+
await fs
|
|
324
|
+
.access(file.path)
|
|
325
|
+
.then(() => true)
|
|
326
|
+
.catch(() => false),
|
|
327
|
+
).toBe(false)
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
test.skip("unicode filenames modification and restore", async () => {
|
|
334
|
+
await using tmp = await bootstrap()
|
|
335
|
+
await Instance.provide({
|
|
336
|
+
directory: tmp.path,
|
|
337
|
+
fn: async () => {
|
|
338
|
+
const chineseFile = fwd(tmp.path, "文件.txt")
|
|
339
|
+
const cyrillicFile = fwd(tmp.path, "файл.txt")
|
|
340
|
+
|
|
341
|
+
await Filesystem.write(chineseFile, "original chinese")
|
|
342
|
+
await Filesystem.write(cyrillicFile, "original cyrillic")
|
|
343
|
+
|
|
344
|
+
const before = await Snapshot.track()
|
|
345
|
+
expect(before).toBeTruthy()
|
|
346
|
+
|
|
347
|
+
await Filesystem.write(chineseFile, "modified chinese")
|
|
348
|
+
await Filesystem.write(cyrillicFile, "modified cyrillic")
|
|
349
|
+
|
|
350
|
+
const patch = await Snapshot.patch(before!)
|
|
351
|
+
expect(patch.files).toContain(chineseFile)
|
|
352
|
+
expect(patch.files).toContain(cyrillicFile)
|
|
353
|
+
|
|
354
|
+
await Snapshot.revert([patch])
|
|
355
|
+
|
|
356
|
+
expect(await fs.readFile(chineseFile, "utf-8")).toBe("original chinese")
|
|
357
|
+
expect(await fs.readFile(cyrillicFile, "utf-8")).toBe("original cyrillic")
|
|
358
|
+
},
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test("unicode filenames in subdirectories", async () => {
|
|
363
|
+
await using tmp = await bootstrap()
|
|
364
|
+
await Instance.provide({
|
|
365
|
+
directory: tmp.path,
|
|
366
|
+
fn: async () => {
|
|
367
|
+
const before = await Snapshot.track()
|
|
368
|
+
expect(before).toBeTruthy()
|
|
369
|
+
|
|
370
|
+
await $`mkdir -p "${tmp.path}/目录/подкаталог"`.quiet()
|
|
371
|
+
const deepFile = fwd(tmp.path, "目录", "подкаталог", "文件.txt")
|
|
372
|
+
await Filesystem.write(deepFile, "deep unicode content")
|
|
373
|
+
|
|
374
|
+
const patch = await Snapshot.patch(before!)
|
|
375
|
+
expect(patch.files).toContain(deepFile)
|
|
376
|
+
|
|
377
|
+
await Snapshot.revert([patch])
|
|
378
|
+
expect(
|
|
379
|
+
await fs
|
|
380
|
+
.access(deepFile)
|
|
381
|
+
.then(() => true)
|
|
382
|
+
.catch(() => false),
|
|
383
|
+
).toBe(false)
|
|
384
|
+
},
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test("very long filenames", async () => {
|
|
389
|
+
await using tmp = await bootstrap()
|
|
390
|
+
await Instance.provide({
|
|
391
|
+
directory: tmp.path,
|
|
392
|
+
fn: async () => {
|
|
393
|
+
const before = await Snapshot.track()
|
|
394
|
+
expect(before).toBeTruthy()
|
|
395
|
+
|
|
396
|
+
const longName = "a".repeat(200) + ".txt"
|
|
397
|
+
const longFile = fwd(tmp.path, longName)
|
|
398
|
+
|
|
399
|
+
await Filesystem.write(longFile, "long filename content")
|
|
400
|
+
|
|
401
|
+
const patch = await Snapshot.patch(before!)
|
|
402
|
+
expect(patch.files).toContain(longFile)
|
|
403
|
+
|
|
404
|
+
await Snapshot.revert([patch])
|
|
405
|
+
expect(
|
|
406
|
+
await fs
|
|
407
|
+
.access(longFile)
|
|
408
|
+
.then(() => true)
|
|
409
|
+
.catch(() => false),
|
|
410
|
+
).toBe(false)
|
|
411
|
+
},
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
test("hidden files", async () => {
|
|
416
|
+
await using tmp = await bootstrap()
|
|
417
|
+
await Instance.provide({
|
|
418
|
+
directory: tmp.path,
|
|
419
|
+
fn: async () => {
|
|
420
|
+
const before = await Snapshot.track()
|
|
421
|
+
expect(before).toBeTruthy()
|
|
422
|
+
|
|
423
|
+
await Filesystem.write(`${tmp.path}/.hidden`, "hidden content")
|
|
424
|
+
await Filesystem.write(`${tmp.path}/.gitignore`, "*.log")
|
|
425
|
+
await Filesystem.write(`${tmp.path}/.config`, "config content")
|
|
426
|
+
|
|
427
|
+
const patch = await Snapshot.patch(before!)
|
|
428
|
+
expect(patch.files).toContain(fwd(tmp.path, ".hidden"))
|
|
429
|
+
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
|
|
430
|
+
expect(patch.files).toContain(fwd(tmp.path, ".config"))
|
|
431
|
+
},
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test("nested symlinks", async () => {
|
|
436
|
+
await using tmp = await bootstrap()
|
|
437
|
+
await Instance.provide({
|
|
438
|
+
directory: tmp.path,
|
|
439
|
+
fn: async () => {
|
|
440
|
+
const before = await Snapshot.track()
|
|
441
|
+
expect(before).toBeTruthy()
|
|
442
|
+
|
|
443
|
+
await $`mkdir -p ${tmp.path}/sub/dir`.quiet()
|
|
444
|
+
await Filesystem.write(`${tmp.path}/sub/dir/target.txt`, "target content")
|
|
445
|
+
await fs.symlink(`${tmp.path}/sub/dir/target.txt`, `${tmp.path}/sub/dir/link.txt`, "file")
|
|
446
|
+
await fs.symlink(`${tmp.path}/sub`, `${tmp.path}/sub-link`, "dir")
|
|
447
|
+
|
|
448
|
+
const patch = await Snapshot.patch(before!)
|
|
449
|
+
expect(patch.files).toContain(fwd(tmp.path, "sub", "dir", "link.txt"))
|
|
450
|
+
expect(patch.files).toContain(fwd(tmp.path, "sub-link"))
|
|
451
|
+
},
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
test("file permissions and ownership changes", async () => {
|
|
456
|
+
await using tmp = await bootstrap()
|
|
457
|
+
await Instance.provide({
|
|
458
|
+
directory: tmp.path,
|
|
459
|
+
fn: async () => {
|
|
460
|
+
const before = await Snapshot.track()
|
|
461
|
+
expect(before).toBeTruthy()
|
|
462
|
+
|
|
463
|
+
// Change permissions multiple times
|
|
464
|
+
await $`chmod 600 ${tmp.path}/a.txt`.quiet()
|
|
465
|
+
await $`chmod 755 ${tmp.path}/a.txt`.quiet()
|
|
466
|
+
await $`chmod 644 ${tmp.path}/a.txt`.quiet()
|
|
467
|
+
|
|
468
|
+
const patch = await Snapshot.patch(before!)
|
|
469
|
+
// Note: git doesn't track permission changes on existing files by default
|
|
470
|
+
// Only tracks executable bit when files are first added
|
|
471
|
+
expect(patch.files.length).toBe(0)
|
|
472
|
+
},
|
|
473
|
+
})
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
test("circular symlinks", async () => {
|
|
477
|
+
await using tmp = await bootstrap()
|
|
478
|
+
await Instance.provide({
|
|
479
|
+
directory: tmp.path,
|
|
480
|
+
fn: async () => {
|
|
481
|
+
const before = await Snapshot.track()
|
|
482
|
+
expect(before).toBeTruthy()
|
|
483
|
+
|
|
484
|
+
// Create circular symlink
|
|
485
|
+
await fs.symlink(`${tmp.path}/circular`, `${tmp.path}/circular`, "dir").catch(() => {})
|
|
486
|
+
|
|
487
|
+
const patch = await Snapshot.patch(before!)
|
|
488
|
+
expect(patch.files.length).toBeGreaterThanOrEqual(0) // Should not crash
|
|
489
|
+
},
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
test("gitignore changes", async () => {
|
|
494
|
+
await using tmp = await bootstrap()
|
|
495
|
+
await Instance.provide({
|
|
496
|
+
directory: tmp.path,
|
|
497
|
+
fn: async () => {
|
|
498
|
+
const before = await Snapshot.track()
|
|
499
|
+
expect(before).toBeTruthy()
|
|
500
|
+
|
|
501
|
+
await Filesystem.write(`${tmp.path}/.gitignore`, "*.ignored")
|
|
502
|
+
await Filesystem.write(`${tmp.path}/test.ignored`, "ignored content")
|
|
503
|
+
await Filesystem.write(`${tmp.path}/normal.txt`, "normal content")
|
|
504
|
+
|
|
505
|
+
const patch = await Snapshot.patch(before!)
|
|
506
|
+
|
|
507
|
+
// Should track gitignore itself
|
|
508
|
+
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
|
|
509
|
+
// Should track normal files
|
|
510
|
+
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
|
|
511
|
+
// Should not track ignored files (git won't see them)
|
|
512
|
+
expect(patch.files).not.toContain(fwd(tmp.path, "test.ignored"))
|
|
513
|
+
},
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
test("git info exclude changes", async () => {
|
|
518
|
+
await using tmp = await bootstrap()
|
|
519
|
+
await Instance.provide({
|
|
520
|
+
directory: tmp.path,
|
|
521
|
+
fn: async () => {
|
|
522
|
+
const before = await Snapshot.track()
|
|
523
|
+
expect(before).toBeTruthy()
|
|
524
|
+
|
|
525
|
+
const file = `${tmp.path}/.git/info/exclude`
|
|
526
|
+
const text = await Bun.file(file).text()
|
|
527
|
+
await Bun.write(file, `${text.trimEnd()}\nignored.txt\n`)
|
|
528
|
+
await Bun.write(`${tmp.path}/ignored.txt`, "ignored content")
|
|
529
|
+
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
|
|
530
|
+
|
|
531
|
+
const patch = await Snapshot.patch(before!)
|
|
532
|
+
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
|
|
533
|
+
expect(patch.files).not.toContain(fwd(tmp.path, "ignored.txt"))
|
|
534
|
+
|
|
535
|
+
const after = await Snapshot.track()
|
|
536
|
+
const diffs = await Snapshot.diffFull(before!, after!)
|
|
537
|
+
expect(diffs.some((x) => x.file === "normal.txt")).toBe(true)
|
|
538
|
+
expect(diffs.some((x) => x.file === "ignored.txt")).toBe(false)
|
|
539
|
+
},
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
test("git info exclude keeps global excludes", async () => {
|
|
544
|
+
await using tmp = await bootstrap()
|
|
545
|
+
await Instance.provide({
|
|
546
|
+
directory: tmp.path,
|
|
547
|
+
fn: async () => {
|
|
548
|
+
const global = `${tmp.path}/global.ignore`
|
|
549
|
+
const config = `${tmp.path}/global.gitconfig`
|
|
550
|
+
await Bun.write(global, "global.tmp\n")
|
|
551
|
+
await Bun.write(config, `[core]\n\texcludesFile = ${global.replaceAll("\\", "/")}\n`)
|
|
552
|
+
|
|
553
|
+
const prev = process.env.GIT_CONFIG_GLOBAL
|
|
554
|
+
process.env.GIT_CONFIG_GLOBAL = config
|
|
555
|
+
try {
|
|
556
|
+
const before = await Snapshot.track()
|
|
557
|
+
expect(before).toBeTruthy()
|
|
558
|
+
|
|
559
|
+
const file = `${tmp.path}/.git/info/exclude`
|
|
560
|
+
const text = await Bun.file(file).text()
|
|
561
|
+
await Bun.write(file, `${text.trimEnd()}\ninfo.tmp\n`)
|
|
562
|
+
|
|
563
|
+
await Bun.write(`${tmp.path}/global.tmp`, "global content")
|
|
564
|
+
await Bun.write(`${tmp.path}/info.tmp`, "info content")
|
|
565
|
+
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
|
|
566
|
+
|
|
567
|
+
const patch = await Snapshot.patch(before!)
|
|
568
|
+
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
|
|
569
|
+
expect(patch.files).not.toContain(fwd(tmp.path, "global.tmp"))
|
|
570
|
+
expect(patch.files).not.toContain(fwd(tmp.path, "info.tmp"))
|
|
571
|
+
} finally {
|
|
572
|
+
if (prev) process.env.GIT_CONFIG_GLOBAL = prev
|
|
573
|
+
else delete process.env.GIT_CONFIG_GLOBAL
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
})
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
test("concurrent file operations during patch", async () => {
|
|
580
|
+
await using tmp = await bootstrap()
|
|
581
|
+
await Instance.provide({
|
|
582
|
+
directory: tmp.path,
|
|
583
|
+
fn: async () => {
|
|
584
|
+
const before = await Snapshot.track()
|
|
585
|
+
expect(before).toBeTruthy()
|
|
586
|
+
|
|
587
|
+
// Start creating files
|
|
588
|
+
const createPromise = (async () => {
|
|
589
|
+
for (let i = 0; i < 10; i++) {
|
|
590
|
+
await Filesystem.write(`${tmp.path}/concurrent${i}.txt`, `concurrent${i}`)
|
|
591
|
+
// Small delay to simulate concurrent operations
|
|
592
|
+
await new Promise((resolve) => setTimeout(resolve, 1))
|
|
593
|
+
}
|
|
594
|
+
})()
|
|
595
|
+
|
|
596
|
+
// Get patch while files are being created
|
|
597
|
+
const patchPromise = Snapshot.patch(before!)
|
|
598
|
+
|
|
599
|
+
await createPromise
|
|
600
|
+
const patch = await patchPromise
|
|
601
|
+
|
|
602
|
+
// Should capture some or all of the concurrent files
|
|
603
|
+
expect(patch.files.length).toBeGreaterThanOrEqual(0)
|
|
604
|
+
},
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
test("snapshot state isolation between projects", async () => {
|
|
609
|
+
// Test that different projects don't interfere with each other
|
|
610
|
+
await using tmp1 = await bootstrap()
|
|
611
|
+
await using tmp2 = await bootstrap()
|
|
612
|
+
|
|
613
|
+
await Instance.provide({
|
|
614
|
+
directory: tmp1.path,
|
|
615
|
+
fn: async () => {
|
|
616
|
+
const before1 = await Snapshot.track()
|
|
617
|
+
await Filesystem.write(`${tmp1.path}/project1.txt`, "project1 content")
|
|
618
|
+
const patch1 = await Snapshot.patch(before1!)
|
|
619
|
+
expect(patch1.files).toContain(fwd(tmp1.path, "project1.txt"))
|
|
620
|
+
},
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
await Instance.provide({
|
|
624
|
+
directory: tmp2.path,
|
|
625
|
+
fn: async () => {
|
|
626
|
+
const before2 = await Snapshot.track()
|
|
627
|
+
await Filesystem.write(`${tmp2.path}/project2.txt`, "project2 content")
|
|
628
|
+
const patch2 = await Snapshot.patch(before2!)
|
|
629
|
+
expect(patch2.files).toContain(fwd(tmp2.path, "project2.txt"))
|
|
630
|
+
|
|
631
|
+
// Ensure project1 files don't appear in project2
|
|
632
|
+
expect(patch2.files).not.toContain(fwd(tmp1?.path ?? "", "project1.txt"))
|
|
633
|
+
},
|
|
634
|
+
})
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
test("patch detects changes in secondary worktree", async () => {
|
|
638
|
+
await using tmp = await bootstrap()
|
|
639
|
+
const worktreePath = `${tmp.path}-worktree`
|
|
640
|
+
await $`git worktree add ${worktreePath} HEAD`.cwd(tmp.path).quiet()
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
await Instance.provide({
|
|
644
|
+
directory: tmp.path,
|
|
645
|
+
fn: async () => {
|
|
646
|
+
expect(await Snapshot.track()).toBeTruthy()
|
|
647
|
+
},
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
await Instance.provide({
|
|
651
|
+
directory: worktreePath,
|
|
652
|
+
fn: async () => {
|
|
653
|
+
const before = await Snapshot.track()
|
|
654
|
+
expect(before).toBeTruthy()
|
|
655
|
+
|
|
656
|
+
const worktreeFile = fwd(worktreePath, "worktree.txt")
|
|
657
|
+
await Filesystem.write(worktreeFile, "worktree content")
|
|
658
|
+
|
|
659
|
+
const patch = await Snapshot.patch(before!)
|
|
660
|
+
expect(patch.files).toContain(worktreeFile)
|
|
661
|
+
},
|
|
662
|
+
})
|
|
663
|
+
} finally {
|
|
664
|
+
await $`git worktree remove --force ${worktreePath}`.cwd(tmp.path).quiet().nothrow()
|
|
665
|
+
await $`rm -rf ${worktreePath}`.quiet()
|
|
666
|
+
}
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
test("revert only removes files in invoking worktree", async () => {
|
|
670
|
+
await using tmp = await bootstrap()
|
|
671
|
+
const worktreePath = `${tmp.path}-worktree`
|
|
672
|
+
await $`git worktree add ${worktreePath} HEAD`.cwd(tmp.path).quiet()
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
await Instance.provide({
|
|
676
|
+
directory: tmp.path,
|
|
677
|
+
fn: async () => {
|
|
678
|
+
expect(await Snapshot.track()).toBeTruthy()
|
|
679
|
+
},
|
|
680
|
+
})
|
|
681
|
+
const primaryFile = `${tmp.path}/worktree.txt`
|
|
682
|
+
await Filesystem.write(primaryFile, "primary content")
|
|
683
|
+
|
|
684
|
+
await Instance.provide({
|
|
685
|
+
directory: worktreePath,
|
|
686
|
+
fn: async () => {
|
|
687
|
+
const before = await Snapshot.track()
|
|
688
|
+
expect(before).toBeTruthy()
|
|
689
|
+
|
|
690
|
+
const worktreeFile = fwd(worktreePath, "worktree.txt")
|
|
691
|
+
await Filesystem.write(worktreeFile, "worktree content")
|
|
692
|
+
|
|
693
|
+
const patch = await Snapshot.patch(before!)
|
|
694
|
+
await Snapshot.revert([patch])
|
|
695
|
+
|
|
696
|
+
expect(
|
|
697
|
+
await fs
|
|
698
|
+
.access(worktreeFile)
|
|
699
|
+
.then(() => true)
|
|
700
|
+
.catch(() => false),
|
|
701
|
+
).toBe(false)
|
|
702
|
+
},
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
expect(await fs.readFile(primaryFile, "utf-8")).toBe("primary content")
|
|
706
|
+
} finally {
|
|
707
|
+
await $`git worktree remove --force ${worktreePath}`.cwd(tmp.path).quiet().nothrow()
|
|
708
|
+
await $`rm -rf ${worktreePath}`.quiet()
|
|
709
|
+
await $`rm -f ${tmp.path}/worktree.txt`.quiet()
|
|
710
|
+
}
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
test("diff reports worktree-only/shared edits and ignores primary-only", async () => {
|
|
714
|
+
await using tmp = await bootstrap()
|
|
715
|
+
const worktreePath = `${tmp.path}-worktree`
|
|
716
|
+
await $`git worktree add ${worktreePath} HEAD`.cwd(tmp.path).quiet()
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
await Instance.provide({
|
|
720
|
+
directory: tmp.path,
|
|
721
|
+
fn: async () => {
|
|
722
|
+
expect(await Snapshot.track()).toBeTruthy()
|
|
723
|
+
},
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
await Instance.provide({
|
|
727
|
+
directory: worktreePath,
|
|
728
|
+
fn: async () => {
|
|
729
|
+
const before = await Snapshot.track()
|
|
730
|
+
expect(before).toBeTruthy()
|
|
731
|
+
|
|
732
|
+
await Filesystem.write(`${worktreePath}/worktree-only.txt`, "worktree diff content")
|
|
733
|
+
await Filesystem.write(`${worktreePath}/shared.txt`, "worktree edit")
|
|
734
|
+
await Filesystem.write(`${tmp.path}/shared.txt`, "primary edit")
|
|
735
|
+
await Filesystem.write(`${tmp.path}/primary-only.txt`, "primary change")
|
|
736
|
+
|
|
737
|
+
const diff = await Snapshot.diff(before!)
|
|
738
|
+
expect(diff).toContain("worktree-only.txt")
|
|
739
|
+
expect(diff).toContain("shared.txt")
|
|
740
|
+
expect(diff).not.toContain("primary-only.txt")
|
|
741
|
+
},
|
|
742
|
+
})
|
|
743
|
+
} finally {
|
|
744
|
+
await $`git worktree remove --force ${worktreePath}`.cwd(tmp.path).quiet().nothrow()
|
|
745
|
+
await $`rm -rf ${worktreePath}`.quiet()
|
|
746
|
+
await $`rm -f ${tmp.path}/shared.txt`.quiet()
|
|
747
|
+
await $`rm -f ${tmp.path}/primary-only.txt`.quiet()
|
|
748
|
+
}
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
test("track with no changes returns same hash", async () => {
|
|
752
|
+
await using tmp = await bootstrap()
|
|
753
|
+
await Instance.provide({
|
|
754
|
+
directory: tmp.path,
|
|
755
|
+
fn: async () => {
|
|
756
|
+
const hash1 = await Snapshot.track()
|
|
757
|
+
expect(hash1).toBeTruthy()
|
|
758
|
+
|
|
759
|
+
// Track again with no changes
|
|
760
|
+
const hash2 = await Snapshot.track()
|
|
761
|
+
expect(hash2).toBe(hash1!)
|
|
762
|
+
|
|
763
|
+
// Track again
|
|
764
|
+
const hash3 = await Snapshot.track()
|
|
765
|
+
expect(hash3).toBe(hash1!)
|
|
766
|
+
},
|
|
767
|
+
})
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
test("diff function with various changes", async () => {
|
|
771
|
+
await using tmp = await bootstrap()
|
|
772
|
+
await Instance.provide({
|
|
773
|
+
directory: tmp.path,
|
|
774
|
+
fn: async () => {
|
|
775
|
+
const before = await Snapshot.track()
|
|
776
|
+
expect(before).toBeTruthy()
|
|
777
|
+
|
|
778
|
+
// Make various changes
|
|
779
|
+
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
780
|
+
await Filesystem.write(`${tmp.path}/new.txt`, "new content")
|
|
781
|
+
await Filesystem.write(`${tmp.path}/b.txt`, "modified content")
|
|
782
|
+
|
|
783
|
+
const diff = await Snapshot.diff(before!)
|
|
784
|
+
expect(diff).toContain("a.txt")
|
|
785
|
+
expect(diff).toContain("b.txt")
|
|
786
|
+
expect(diff).toContain("new.txt")
|
|
787
|
+
},
|
|
788
|
+
})
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
test("restore function", async () => {
|
|
792
|
+
await using tmp = await bootstrap()
|
|
793
|
+
await Instance.provide({
|
|
794
|
+
directory: tmp.path,
|
|
795
|
+
fn: async () => {
|
|
796
|
+
const before = await Snapshot.track()
|
|
797
|
+
expect(before).toBeTruthy()
|
|
798
|
+
|
|
799
|
+
// Make changes
|
|
800
|
+
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
801
|
+
await Filesystem.write(`${tmp.path}/new.txt`, "new content")
|
|
802
|
+
await Filesystem.write(`${tmp.path}/b.txt`, "modified")
|
|
803
|
+
|
|
804
|
+
// Restore to original state
|
|
805
|
+
await Snapshot.restore(before!)
|
|
806
|
+
|
|
807
|
+
expect(
|
|
808
|
+
await fs
|
|
809
|
+
.access(`${tmp.path}/a.txt`)
|
|
810
|
+
.then(() => true)
|
|
811
|
+
.catch(() => false),
|
|
812
|
+
).toBe(true)
|
|
813
|
+
expect(await fs.readFile(`${tmp.path}/a.txt`, "utf-8")).toBe(tmp.extra.aContent)
|
|
814
|
+
expect(
|
|
815
|
+
await fs
|
|
816
|
+
.access(`${tmp.path}/new.txt`)
|
|
817
|
+
.then(() => true)
|
|
818
|
+
.catch(() => false),
|
|
819
|
+
).toBe(true) // New files should remain
|
|
820
|
+
expect(await fs.readFile(`${tmp.path}/b.txt`, "utf-8")).toBe(tmp.extra.bContent)
|
|
821
|
+
},
|
|
822
|
+
})
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
test("revert should not delete files that existed but were deleted in snapshot", async () => {
|
|
826
|
+
await using tmp = await bootstrap()
|
|
827
|
+
await Instance.provide({
|
|
828
|
+
directory: tmp.path,
|
|
829
|
+
fn: async () => {
|
|
830
|
+
const snapshot1 = await Snapshot.track()
|
|
831
|
+
expect(snapshot1).toBeTruthy()
|
|
832
|
+
|
|
833
|
+
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
834
|
+
|
|
835
|
+
const snapshot2 = await Snapshot.track()
|
|
836
|
+
expect(snapshot2).toBeTruthy()
|
|
837
|
+
|
|
838
|
+
await Filesystem.write(`${tmp.path}/a.txt`, "recreated content")
|
|
839
|
+
|
|
840
|
+
const patch = await Snapshot.patch(snapshot2!)
|
|
841
|
+
expect(patch.files).toContain(fwd(tmp.path, "a.txt"))
|
|
842
|
+
|
|
843
|
+
await Snapshot.revert([patch])
|
|
844
|
+
|
|
845
|
+
expect(
|
|
846
|
+
await fs
|
|
847
|
+
.access(`${tmp.path}/a.txt`)
|
|
848
|
+
.then(() => true)
|
|
849
|
+
.catch(() => false),
|
|
850
|
+
).toBe(false)
|
|
851
|
+
},
|
|
852
|
+
})
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
test("revert preserves file that existed in snapshot when deleted then recreated", async () => {
|
|
856
|
+
await using tmp = await bootstrap()
|
|
857
|
+
await Instance.provide({
|
|
858
|
+
directory: tmp.path,
|
|
859
|
+
fn: async () => {
|
|
860
|
+
await Filesystem.write(`${tmp.path}/existing.txt`, "original content")
|
|
861
|
+
|
|
862
|
+
const snapshot = await Snapshot.track()
|
|
863
|
+
expect(snapshot).toBeTruthy()
|
|
864
|
+
|
|
865
|
+
await $`rm ${tmp.path}/existing.txt`.quiet()
|
|
866
|
+
await Filesystem.write(`${tmp.path}/existing.txt`, "recreated")
|
|
867
|
+
await Filesystem.write(`${tmp.path}/newfile.txt`, "new")
|
|
868
|
+
|
|
869
|
+
const patch = await Snapshot.patch(snapshot!)
|
|
870
|
+
expect(patch.files).toContain(fwd(tmp.path, "existing.txt"))
|
|
871
|
+
expect(patch.files).toContain(fwd(tmp.path, "newfile.txt"))
|
|
872
|
+
|
|
873
|
+
await Snapshot.revert([patch])
|
|
874
|
+
|
|
875
|
+
expect(
|
|
876
|
+
await fs
|
|
877
|
+
.access(`${tmp.path}/newfile.txt`)
|
|
878
|
+
.then(() => true)
|
|
879
|
+
.catch(() => false),
|
|
880
|
+
).toBe(false)
|
|
881
|
+
expect(
|
|
882
|
+
await fs
|
|
883
|
+
.access(`${tmp.path}/existing.txt`)
|
|
884
|
+
.then(() => true)
|
|
885
|
+
.catch(() => false),
|
|
886
|
+
).toBe(true)
|
|
887
|
+
expect(await fs.readFile(`${tmp.path}/existing.txt`, "utf-8")).toBe("original content")
|
|
888
|
+
},
|
|
889
|
+
})
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
test("diffFull sets status based on git change type", async () => {
|
|
893
|
+
await using tmp = await bootstrap()
|
|
894
|
+
await Instance.provide({
|
|
895
|
+
directory: tmp.path,
|
|
896
|
+
fn: async () => {
|
|
897
|
+
await Filesystem.write(`${tmp.path}/grow.txt`, "one\n")
|
|
898
|
+
await Filesystem.write(`${tmp.path}/trim.txt`, "line1\nline2\n")
|
|
899
|
+
await Filesystem.write(`${tmp.path}/delete.txt`, "gone")
|
|
900
|
+
|
|
901
|
+
const before = await Snapshot.track()
|
|
902
|
+
expect(before).toBeTruthy()
|
|
903
|
+
|
|
904
|
+
await Filesystem.write(`${tmp.path}/grow.txt`, "one\ntwo\n")
|
|
905
|
+
await Filesystem.write(`${tmp.path}/trim.txt`, "line1\n")
|
|
906
|
+
await $`rm ${tmp.path}/delete.txt`.quiet()
|
|
907
|
+
await Filesystem.write(`${tmp.path}/added.txt`, "new")
|
|
908
|
+
|
|
909
|
+
const after = await Snapshot.track()
|
|
910
|
+
expect(after).toBeTruthy()
|
|
911
|
+
|
|
912
|
+
const diffs = await Snapshot.diffFull(before!, after!)
|
|
913
|
+
expect(diffs.length).toBe(4)
|
|
914
|
+
|
|
915
|
+
const added = diffs.find((d) => d.file === "added.txt")
|
|
916
|
+
expect(added).toBeDefined()
|
|
917
|
+
expect(added!.status).toBe("added")
|
|
918
|
+
|
|
919
|
+
const deleted = diffs.find((d) => d.file === "delete.txt")
|
|
920
|
+
expect(deleted).toBeDefined()
|
|
921
|
+
expect(deleted!.status).toBe("deleted")
|
|
922
|
+
|
|
923
|
+
const grow = diffs.find((d) => d.file === "grow.txt")
|
|
924
|
+
expect(grow).toBeDefined()
|
|
925
|
+
expect(grow!.status).toBe("modified")
|
|
926
|
+
expect(grow!.additions).toBeGreaterThan(0)
|
|
927
|
+
expect(grow!.deletions).toBe(0)
|
|
928
|
+
|
|
929
|
+
const trim = diffs.find((d) => d.file === "trim.txt")
|
|
930
|
+
expect(trim).toBeDefined()
|
|
931
|
+
expect(trim!.status).toBe("modified")
|
|
932
|
+
expect(trim!.additions).toBe(0)
|
|
933
|
+
expect(trim!.deletions).toBeGreaterThan(0)
|
|
934
|
+
},
|
|
935
|
+
})
|
|
936
|
+
})
|
|
937
|
+
|
|
938
|
+
test("diffFull with new file additions", async () => {
|
|
939
|
+
await using tmp = await bootstrap()
|
|
940
|
+
await Instance.provide({
|
|
941
|
+
directory: tmp.path,
|
|
942
|
+
fn: async () => {
|
|
943
|
+
const before = await Snapshot.track()
|
|
944
|
+
expect(before).toBeTruthy()
|
|
945
|
+
|
|
946
|
+
await Filesystem.write(`${tmp.path}/new.txt`, "new content")
|
|
947
|
+
|
|
948
|
+
const after = await Snapshot.track()
|
|
949
|
+
expect(after).toBeTruthy()
|
|
950
|
+
|
|
951
|
+
const diffs = await Snapshot.diffFull(before!, after!)
|
|
952
|
+
expect(diffs.length).toBe(1)
|
|
953
|
+
|
|
954
|
+
const newFileDiff = diffs[0]
|
|
955
|
+
expect(newFileDiff.file).toBe("new.txt")
|
|
956
|
+
expect(newFileDiff.before).toBe("")
|
|
957
|
+
expect(newFileDiff.after).toBe("new content")
|
|
958
|
+
expect(newFileDiff.additions).toBe(1)
|
|
959
|
+
expect(newFileDiff.deletions).toBe(0)
|
|
960
|
+
},
|
|
961
|
+
})
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
test("diffFull with file modifications", async () => {
|
|
965
|
+
await using tmp = await bootstrap()
|
|
966
|
+
await Instance.provide({
|
|
967
|
+
directory: tmp.path,
|
|
968
|
+
fn: async () => {
|
|
969
|
+
const before = await Snapshot.track()
|
|
970
|
+
expect(before).toBeTruthy()
|
|
971
|
+
|
|
972
|
+
await Filesystem.write(`${tmp.path}/b.txt`, "modified content")
|
|
973
|
+
|
|
974
|
+
const after = await Snapshot.track()
|
|
975
|
+
expect(after).toBeTruthy()
|
|
976
|
+
|
|
977
|
+
const diffs = await Snapshot.diffFull(before!, after!)
|
|
978
|
+
expect(diffs.length).toBe(1)
|
|
979
|
+
|
|
980
|
+
const modifiedFileDiff = diffs[0]
|
|
981
|
+
expect(modifiedFileDiff.file).toBe("b.txt")
|
|
982
|
+
expect(modifiedFileDiff.before).toBe(tmp.extra.bContent)
|
|
983
|
+
expect(modifiedFileDiff.after).toBe("modified content")
|
|
984
|
+
expect(modifiedFileDiff.additions).toBeGreaterThan(0)
|
|
985
|
+
expect(modifiedFileDiff.deletions).toBeGreaterThan(0)
|
|
986
|
+
},
|
|
987
|
+
})
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
test("diffFull with file deletions", async () => {
|
|
991
|
+
await using tmp = await bootstrap()
|
|
992
|
+
await Instance.provide({
|
|
993
|
+
directory: tmp.path,
|
|
994
|
+
fn: async () => {
|
|
995
|
+
const before = await Snapshot.track()
|
|
996
|
+
expect(before).toBeTruthy()
|
|
997
|
+
|
|
998
|
+
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
999
|
+
|
|
1000
|
+
const after = await Snapshot.track()
|
|
1001
|
+
expect(after).toBeTruthy()
|
|
1002
|
+
|
|
1003
|
+
const diffs = await Snapshot.diffFull(before!, after!)
|
|
1004
|
+
expect(diffs.length).toBe(1)
|
|
1005
|
+
|
|
1006
|
+
const removedFileDiff = diffs[0]
|
|
1007
|
+
expect(removedFileDiff.file).toBe("a.txt")
|
|
1008
|
+
expect(removedFileDiff.before).toBe(tmp.extra.aContent)
|
|
1009
|
+
expect(removedFileDiff.after).toBe("")
|
|
1010
|
+
expect(removedFileDiff.additions).toBe(0)
|
|
1011
|
+
expect(removedFileDiff.deletions).toBe(1)
|
|
1012
|
+
},
|
|
1013
|
+
})
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
test("diffFull with multiple line additions", async () => {
|
|
1017
|
+
await using tmp = await bootstrap()
|
|
1018
|
+
await Instance.provide({
|
|
1019
|
+
directory: tmp.path,
|
|
1020
|
+
fn: async () => {
|
|
1021
|
+
const before = await Snapshot.track()
|
|
1022
|
+
expect(before).toBeTruthy()
|
|
1023
|
+
|
|
1024
|
+
await Filesystem.write(`${tmp.path}/multi.txt`, "line1\nline2\nline3")
|
|
1025
|
+
|
|
1026
|
+
const after = await Snapshot.track()
|
|
1027
|
+
expect(after).toBeTruthy()
|
|
1028
|
+
|
|
1029
|
+
const diffs = await Snapshot.diffFull(before!, after!)
|
|
1030
|
+
expect(diffs.length).toBe(1)
|
|
1031
|
+
|
|
1032
|
+
const multiDiff = diffs[0]
|
|
1033
|
+
expect(multiDiff.file).toBe("multi.txt")
|
|
1034
|
+
expect(multiDiff.before).toBe("")
|
|
1035
|
+
expect(multiDiff.after).toBe("line1\nline2\nline3")
|
|
1036
|
+
expect(multiDiff.additions).toBe(3)
|
|
1037
|
+
expect(multiDiff.deletions).toBe(0)
|
|
1038
|
+
},
|
|
1039
|
+
})
|
|
1040
|
+
})
|
|
1041
|
+
|
|
1042
|
+
test("diffFull with addition and deletion", async () => {
|
|
1043
|
+
await using tmp = await bootstrap()
|
|
1044
|
+
await Instance.provide({
|
|
1045
|
+
directory: tmp.path,
|
|
1046
|
+
fn: async () => {
|
|
1047
|
+
const before = await Snapshot.track()
|
|
1048
|
+
expect(before).toBeTruthy()
|
|
1049
|
+
|
|
1050
|
+
await Filesystem.write(`${tmp.path}/added.txt`, "added content")
|
|
1051
|
+
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
1052
|
+
|
|
1053
|
+
const after = await Snapshot.track()
|
|
1054
|
+
expect(after).toBeTruthy()
|
|
1055
|
+
|
|
1056
|
+
const diffs = await Snapshot.diffFull(before!, after!)
|
|
1057
|
+
expect(diffs.length).toBe(2)
|
|
1058
|
+
|
|
1059
|
+
const addedFileDiff = diffs.find((d) => d.file === "added.txt")
|
|
1060
|
+
expect(addedFileDiff).toBeDefined()
|
|
1061
|
+
expect(addedFileDiff!.before).toBe("")
|
|
1062
|
+
expect(addedFileDiff!.after).toBe("added content")
|
|
1063
|
+
expect(addedFileDiff!.additions).toBe(1)
|
|
1064
|
+
expect(addedFileDiff!.deletions).toBe(0)
|
|
1065
|
+
|
|
1066
|
+
const removedFileDiff = diffs.find((d) => d.file === "a.txt")
|
|
1067
|
+
expect(removedFileDiff).toBeDefined()
|
|
1068
|
+
expect(removedFileDiff!.before).toBe(tmp.extra.aContent)
|
|
1069
|
+
expect(removedFileDiff!.after).toBe("")
|
|
1070
|
+
expect(removedFileDiff!.additions).toBe(0)
|
|
1071
|
+
expect(removedFileDiff!.deletions).toBe(1)
|
|
1072
|
+
},
|
|
1073
|
+
})
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
test("diffFull with multiple additions and deletions", async () => {
|
|
1077
|
+
await using tmp = await bootstrap()
|
|
1078
|
+
await Instance.provide({
|
|
1079
|
+
directory: tmp.path,
|
|
1080
|
+
fn: async () => {
|
|
1081
|
+
const before = await Snapshot.track()
|
|
1082
|
+
expect(before).toBeTruthy()
|
|
1083
|
+
|
|
1084
|
+
await Filesystem.write(`${tmp.path}/multi1.txt`, "line1\nline2\nline3")
|
|
1085
|
+
await Filesystem.write(`${tmp.path}/multi2.txt`, "single line")
|
|
1086
|
+
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
1087
|
+
await $`rm ${tmp.path}/b.txt`.quiet()
|
|
1088
|
+
|
|
1089
|
+
const after = await Snapshot.track()
|
|
1090
|
+
expect(after).toBeTruthy()
|
|
1091
|
+
|
|
1092
|
+
const diffs = await Snapshot.diffFull(before!, after!)
|
|
1093
|
+
expect(diffs.length).toBe(4)
|
|
1094
|
+
|
|
1095
|
+
const multi1Diff = diffs.find((d) => d.file === "multi1.txt")
|
|
1096
|
+
expect(multi1Diff).toBeDefined()
|
|
1097
|
+
expect(multi1Diff!.additions).toBe(3)
|
|
1098
|
+
expect(multi1Diff!.deletions).toBe(0)
|
|
1099
|
+
|
|
1100
|
+
const multi2Diff = diffs.find((d) => d.file === "multi2.txt")
|
|
1101
|
+
expect(multi2Diff).toBeDefined()
|
|
1102
|
+
expect(multi2Diff!.additions).toBe(1)
|
|
1103
|
+
expect(multi2Diff!.deletions).toBe(0)
|
|
1104
|
+
|
|
1105
|
+
const removedADiff = diffs.find((d) => d.file === "a.txt")
|
|
1106
|
+
expect(removedADiff).toBeDefined()
|
|
1107
|
+
expect(removedADiff!.additions).toBe(0)
|
|
1108
|
+
expect(removedADiff!.deletions).toBe(1)
|
|
1109
|
+
|
|
1110
|
+
const removedBDiff = diffs.find((d) => d.file === "b.txt")
|
|
1111
|
+
expect(removedBDiff).toBeDefined()
|
|
1112
|
+
expect(removedBDiff!.additions).toBe(0)
|
|
1113
|
+
expect(removedBDiff!.deletions).toBe(1)
|
|
1114
|
+
},
|
|
1115
|
+
})
|
|
1116
|
+
})
|
|
1117
|
+
|
|
1118
|
+
test("diffFull with no changes", async () => {
|
|
1119
|
+
await using tmp = await bootstrap()
|
|
1120
|
+
await Instance.provide({
|
|
1121
|
+
directory: tmp.path,
|
|
1122
|
+
fn: async () => {
|
|
1123
|
+
const before = await Snapshot.track()
|
|
1124
|
+
expect(before).toBeTruthy()
|
|
1125
|
+
|
|
1126
|
+
const after = await Snapshot.track()
|
|
1127
|
+
expect(after).toBeTruthy()
|
|
1128
|
+
|
|
1129
|
+
const diffs = await Snapshot.diffFull(before!, after!)
|
|
1130
|
+
expect(diffs.length).toBe(0)
|
|
1131
|
+
},
|
|
1132
|
+
})
|
|
1133
|
+
})
|
|
1134
|
+
|
|
1135
|
+
test("diffFull with binary file changes", async () => {
|
|
1136
|
+
await using tmp = await bootstrap()
|
|
1137
|
+
await Instance.provide({
|
|
1138
|
+
directory: tmp.path,
|
|
1139
|
+
fn: async () => {
|
|
1140
|
+
const before = await Snapshot.track()
|
|
1141
|
+
expect(before).toBeTruthy()
|
|
1142
|
+
|
|
1143
|
+
await Filesystem.write(`${tmp.path}/binary.bin`, new Uint8Array([0x00, 0x01, 0x02, 0x03]))
|
|
1144
|
+
|
|
1145
|
+
const after = await Snapshot.track()
|
|
1146
|
+
expect(after).toBeTruthy()
|
|
1147
|
+
|
|
1148
|
+
const diffs = await Snapshot.diffFull(before!, after!)
|
|
1149
|
+
expect(diffs.length).toBe(1)
|
|
1150
|
+
|
|
1151
|
+
const binaryDiff = diffs[0]
|
|
1152
|
+
expect(binaryDiff.file).toBe("binary.bin")
|
|
1153
|
+
expect(binaryDiff.before).toBe("")
|
|
1154
|
+
},
|
|
1155
|
+
})
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
test("diffFull with whitespace changes", async () => {
|
|
1159
|
+
await using tmp = await bootstrap()
|
|
1160
|
+
await Instance.provide({
|
|
1161
|
+
directory: tmp.path,
|
|
1162
|
+
fn: async () => {
|
|
1163
|
+
await Filesystem.write(`${tmp.path}/whitespace.txt`, "line1\nline2")
|
|
1164
|
+
const before = await Snapshot.track()
|
|
1165
|
+
expect(before).toBeTruthy()
|
|
1166
|
+
|
|
1167
|
+
await Filesystem.write(`${tmp.path}/whitespace.txt`, "line1\n\nline2\n")
|
|
1168
|
+
|
|
1169
|
+
const after = await Snapshot.track()
|
|
1170
|
+
expect(after).toBeTruthy()
|
|
1171
|
+
|
|
1172
|
+
const diffs = await Snapshot.diffFull(before!, after!)
|
|
1173
|
+
expect(diffs.length).toBe(1)
|
|
1174
|
+
|
|
1175
|
+
const whitespaceDiff = diffs[0]
|
|
1176
|
+
expect(whitespaceDiff.file).toBe("whitespace.txt")
|
|
1177
|
+
expect(whitespaceDiff.additions).toBeGreaterThan(0)
|
|
1178
|
+
},
|
|
1179
|
+
})
|
|
1180
|
+
})
|