CodeNomad/scripts/test-git-init-native.mjs
Pascal André 2570764c41
feat(files): initialize Git from the Files panel for plain folders (#933)
## Summary
When Git is available but the opened folder is not a repository
(temporary workspaces, plain folders), the Files panel's Changes and
Commits views now say so and offer **Initialize Git**. Directory-only
mode, where the backend cannot run Git, is unchanged.

## Design
- **Route:** `POST /api/workspaces/:id/git-init` accepts only the exact
opened folder.
- It refuses directory-only mode, folders already inside a repository,
unowned folders and folders being deleted.
- It checks that OpenCode resolves the same folder as the project
directory, then calls native `vcs.init` once, without retry.
  - A 501 maps to a localized "not supported" message.
- **Refresh after init:**
- The server re-reads the worktree inventory and resets the git-status
cache.
- The UI refreshes file caches, worktrees, project metadata and the
session list.
- Initialization changes the folder's native project ID, so stale
project metadata would otherwise leave later session pages empty.
- **No confirmation dialog:** creating `.git` is non-destructive and
easily undone.
- **Also fixed:** a spurious "detached HEAD" suffix on non-repository
folders in the worktree selector.

## Validation
- Server unit and route tests: authorization, refusals, 501, refresh.
- New browser regression `git-init.test.ts` (2/2): empty state, button,
refreshed Changes and branch, 501 message with no retry. The existing
Files panel suite passes 18/18.
- Isolated native check `scripts/test-git-init-native.mjs` against a
private OpenCode 2.0.27 daemon.
- Server and UI typechecks pass.

Oversized files touched: `workspaces/manager.ts` (~1307 lines) and
`lib/api-client.ts` (~809 lines).

An independent gatekeeper review will be published before the admin
merge.
2026-10-13 03:11:00 +02:00

49 lines
3.2 KiB
JavaScript

// Native Git initialization fixture. Invoke with an absolute CLI path:
// node scripts/test-git-init-native.mjs <absolute opencode executable>
// runIsolated starts a private daemon with its own HOME/XDG/config/database and
// uses synthetic folders only; it never touches the shared daemon.
import assert from "node:assert/strict"
import { mkdir, writeFile } from "node:fs/promises"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { tsImport } from "tsx/esm/api"
import { runIsolated } from "./test-opencode-location-native.mjs"
export async function testNativeGitInit({ client, root }) {
const { initializeNativeGit, GitInitError } = await tsImport("../packages/server/src/workspaces/git-init.ts", import.meta.url)
const { listNativeWorktrees } = await tsImport("../packages/server/src/workspaces/native-worktrees.ts", import.meta.url)
const { getWorktreeGitStatus } = await tsImport("../packages/server/src/workspaces/git-status.ts", import.meta.url)
const { getGitHistory } = await tsImport("../packages/server/src/workspaces/git-history.ts", import.meta.url)
const parent = path.join(root, "markerless-parent")
const folder = path.join(parent, "opened")
await mkdir(folder, { recursive: true })
await writeFile(path.join(folder, "notes.txt"), "synthetic fixture\n")
const context = { client, location: { directory: folder }, workspacePath: folder, toHost: async directory => directory }
// A markerless child resolves to its own project directory, never its parent.
const before = await client.location.get({ location: { directory: folder } })
assert.equal(path.resolve(before.project.directory), path.resolve(folder))
const session = await client.session.create({ location: { directory: folder } })
assert.equal((await listNativeWorktrees(context)).isGitRepo, false)
await initializeNativeGit(context)
const after = await client.location.get({ location: { directory: folder } })
assert.equal(path.resolve(after.project.directory), path.resolve(folder))
// The native project identity changes with the repository; sessions follow it.
const moved = await client.session.get({ sessionID: session.id })
assert.equal(moved.projectID, after.project.id)
assert.ok((await client.session.list({ directory: folder })).data.some(entry => entry.id === session.id))
const catalogue = await listNativeWorktrees(context)
assert.equal(catalogue.isGitRepo, true)
assert.deepEqual(catalogue.worktrees.map(entry => entry.kind), ["root"])
const status = await getWorktreeGitStatus({ workspaceFolder: folder })
assert.ok(status.some(entry => entry.path === "notes.txt"), "Changes lists the untracked file of the unborn repository")
assert.deepEqual((await getGitHistory(folder)).commits, [])
await assert.rejects(initializeNativeGit(context), error => error instanceof GitInitError && error.code === "git_init_repository_exists")
await client.session.remove({ sessionID: session.id })
console.log("PASS: native vcs.init on the exact markerless folder, project/session continuity, unborn inventory/status/history and repeat refusal")
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await runIsolated(process.argv[2], testNativeGitInit)