Files
Carlos Florêncio bc218dfbbe feat: add workspace sources from files panel (#1900)
* feat: add workspace sources from files panel

* fix: preserve worktree identity during recovery

* feat: unify workspace source actions

* fix: validate recovered workspace repositories

* fix: harden workspace source filesystem boundaries

* test: split workspace repository chip coverage

* fix: restrict repository rescan recovery paths

* fix: stabilize workspace source CI checks

* test: canonicalize workspace source paths

* test: align workspace source container paths

* fix: populate direct workspace repository names

* test: preserve git fixture rewrites across restarts

* test: isolate backend git fixture config

* fix: resume remote workspace executions before creation

* fix: preserve ssh agent identity during resume

* fix: preserve SSH workspace across backend restart

* fix: simplify workspace actions trigger

* fix: refine add workspace repositories UX

* docs: narrow workspace source UX updates

* fix: avoid phantom turn on session resume

* test(web): normalize workspace source request headers

* feat(web): streamline workspace source picker

* docs: remove trailing spaces from workspace source plan

* fix: prevent resume status from creating phantom turns

* fix(web): stabilize workspace source attachment

* refactor(web): extract workspace source store contract

* fix: avoid phantom turns on idle agent resume

* fix: address backend static analysis findings

* fix: preserve agentctl readiness status value

* fix: preserve resume messages without active turns

* fix: resume agents from promoted workspace root

* chore: merge origin/main into feature/add-new-repo-folder-d97

* feat(web): explain workspace source consequences

* test(websocket): count session activity subscription
2026-07-25 21:29:53 +01:00

144 lines
4.5 KiB
Go

package backendapp
import (
"context"
"os"
"os/exec"
"testing"
"github.com/kandev/kandev/internal/agent/runtime/lifecycle"
"github.com/kandev/kandev/internal/task/models"
)
func TestBootMessageAdapterResumedMessageDoesNotLeaveActiveTurn(t *testing.T) {
harness := newBootStateTestHarness(t)
ctx := context.Background()
if err := harness.taskRepo.CreateWorkspace(ctx, &models.Workspace{
ID: "workspace-1",
Name: "Workspace",
}); err != nil {
t.Fatalf("CreateWorkspace: %v", err)
}
if err := harness.taskRepo.CreateTask(ctx, &models.Task{
ID: "task-1",
WorkspaceID: "workspace-1",
Title: "Task",
Priority: "medium",
}); err != nil {
t.Fatalf("CreateTask: %v", err)
}
if err := harness.taskRepo.CreateTaskSession(ctx, &models.TaskSession{
ID: "session-1",
TaskID: "task-1",
State: models.TaskSessionStateWaitingForInput,
}); err != nil {
t.Fatalf("CreateTaskSession: %v", err)
}
message, err := (&bootMsgAdapter{svc: harness.taskSvc}).CreateMessage(
ctx,
&lifecycle.BootMessageRequest{
TaskSessionID: "session-1",
TaskID: "task-1",
AuthorType: "agent",
Type: "script_execution",
Metadata: map[string]interface{}{
"script_type": "agent_boot",
"is_resuming": true,
},
},
)
if err != nil {
t.Fatalf("CreateMessage: %v", err)
}
if message == nil {
t.Fatal("CreateMessage returned nil")
}
activeTurn, err := harness.taskSvc.GetActiveTurn(ctx, "session-1")
if err != nil {
t.Fatalf("GetActiveTurn: %v", err)
}
if activeTurn != nil {
t.Fatalf("active turn = %q, want none", activeTurn.ID)
}
turn, err := harness.taskSvc.GetTurn(ctx, message.TurnID)
if err != nil {
t.Fatalf("GetTurn: %v", err)
}
if turn.CompletedAt == nil {
t.Fatalf("resume message turn %q is not completed", turn.ID)
}
}
// TestDetectBranchRemote_ReturnsConfiguredUpstream covers the happy path: a
// branch with an explicit `branch.<name>.remote` config returns that remote
// (covers fork-workflow repos whose primary remote is named "upstream",
// "github", etc., where hard-coding "origin" would push to the wrong place).
func TestDetectBranchRemote_ReturnsConfiguredUpstream(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not available")
}
dir := t.TempDir()
mustGit(t, dir, "init", "--quiet")
mustGit(t, dir, "commit", "--allow-empty", "-m", "init")
mustGit(t, dir, "checkout", "-b", "feature")
// Mimic what `git push --set-upstream <remote> <branch>` would write.
mustGit(t, dir, "config", "branch.feature.remote", "upstream")
if got := detectBranchRemote(context.Background(), dir, "feature"); got != "upstream" {
t.Errorf("got %q, want upstream", got)
}
}
func TestDetectBranchRemote_NoUpstreamFallsBackToOrigin(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not available")
}
dir := t.TempDir()
mustGit(t, dir, "init", "--quiet")
mustGit(t, dir, "commit", "--allow-empty", "-m", "init")
mustGit(t, dir, "checkout", "-b", "feature")
// No branch.feature.remote config — git config returns non-zero.
if got := detectBranchRemote(context.Background(), dir, "feature"); got != defaultGitRemote {
t.Errorf("got %q, want %s", got, defaultGitRemote)
}
}
func TestDetectBranchRemote_NonGitDirFallsBackToOrigin(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not available")
}
// `git config` in a non-git dir errors out, so detectBranchRemote
// should fall back to the default remote rather than propagate the error.
if got := detectBranchRemote(context.Background(), t.TempDir(), "feature"); got != defaultGitRemote {
t.Errorf("got %q, want %s", got, defaultGitRemote)
}
}
// mustGit runs `git -C dir <args...>` and fails the test on non-zero exit.
func mustGit(t *testing.T, dir string, args ...string) {
t.Helper()
full := append([]string{"-C", dir}, args...)
cmd := exec.Command("git", full...)
// Start from the parent environment so git keeps HOME/PATH/etc., then:
// - disable system config (/etc/gitconfig) to prevent host policy from
// interfering with the test
// - set an identity inline so `git commit` works on CI runners that
// have no user.email/user.name configured. Both AUTHOR and COMMITTER
// are needed — git fails fast if either is missing.
cmd.Env = append(os.Environ(),
"GIT_CONFIG_NOSYSTEM=1",
"GIT_AUTHOR_NAME=test",
"GIT_AUTHOR_EMAIL=test@example.com",
"GIT_COMMITTER_NAME=test",
"GIT_COMMITTER_EMAIL=test@example.com",
)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %v: %v\n%s", args, err, out)
}
}