7a7b2490c2
- justfile: add build-linux, build-windows, build-all (output to dist/) - .gitignore: cover ctask, ctask-*, dist/ - scripts/install.sh + scripts/uninstall.sh: POSIX equivalents of .ps1 - remove WorkspacePath metadata field (no production readers; legacy task.yaml files continue to parse silently) Linux smoke-test on WSL/container pending. See audit-report.md and v0.5.1-spec.md.
173 lines
5.3 KiB
Go
173 lines
5.3 KiB
Go
package cmd
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/warrenronsiek/ctask/internal/workspace"
|
|
)
|
|
|
|
// createTestWs creates a minimal workspace for delete testing.
|
|
func createTestWs(t *testing.T, root, category, dirName, status string) string {
|
|
t.Helper()
|
|
dir := filepath.Join(root, category, dirName)
|
|
os.MkdirAll(dir, 0755)
|
|
os.MkdirAll(filepath.Join(dir, "context"), 0755)
|
|
os.MkdirAll(filepath.Join(dir, "output"), 0755)
|
|
os.MkdirAll(filepath.Join(dir, "logs"), 0755)
|
|
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
slug := dirName[11:] // skip "YYYY-MM-DD_"
|
|
meta := &workspace.TaskMeta{
|
|
ID: "test",
|
|
Slug: slug,
|
|
Title: slug,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
Status: status,
|
|
Category: category,
|
|
Mode: "local",
|
|
Agent: "claude",
|
|
}
|
|
workspace.WriteMeta(filepath.Join(dir, "task.yaml"), meta)
|
|
|
|
// Write seed files
|
|
os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("test claude"), 0644)
|
|
os.WriteFile(filepath.Join(dir, "notes.md"), []byte("test notes"), 0644)
|
|
|
|
return dir
|
|
}
|
|
|
|
func TestActiveSessionManifestBlocksDelete(t *testing.T) {
|
|
root := t.TempDir()
|
|
wsDir := createTestWs(t, root, "general", "2026-04-06_active-ws", "active")
|
|
|
|
// Simulate an active session by creating .ctask/manifest-start.json
|
|
ctaskDir := filepath.Join(wsDir, ".ctask")
|
|
os.MkdirAll(ctaskDir, 0755)
|
|
os.WriteFile(filepath.Join(ctaskDir, "manifest-start.json"), []byte(`{"captured_at":"2026-04-06T00:00:00Z","files":[]}`), 0644)
|
|
|
|
// Verify manifest exists
|
|
manifestPath := filepath.Join(wsDir, ".ctask", "manifest-start.json")
|
|
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
|
|
t.Fatal("manifest should exist for this test")
|
|
}
|
|
|
|
// Verify all workspace files still exist
|
|
for _, name := range []string{"task.yaml", "CLAUDE.md", "notes.md"} {
|
|
p := filepath.Join(wsDir, name)
|
|
if _, err := os.Stat(p); os.IsNotExist(err) {
|
|
t.Errorf("pre-check: file %s should exist", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNoFileMutationOnActiveSessionRefusal(t *testing.T) {
|
|
root := t.TempDir()
|
|
wsDir := createTestWs(t, root, "general", "2026-04-06_protected-ws", "active")
|
|
|
|
// Add extra files to verify nothing gets touched
|
|
os.WriteFile(filepath.Join(wsDir, "output", "artifact.txt"), []byte("important data"), 0644)
|
|
os.WriteFile(filepath.Join(wsDir, "context", "reference.md"), []byte("reference"), 0644)
|
|
|
|
// Simulate active session
|
|
ctaskDir := filepath.Join(wsDir, ".ctask")
|
|
os.MkdirAll(ctaskDir, 0755)
|
|
os.WriteFile(filepath.Join(ctaskDir, "manifest-start.json"), []byte(`{"captured_at":"2026-04-06T00:00:00Z","files":[]}`), 0644)
|
|
|
|
// Record all files before the (would-be) delete attempt
|
|
var filesBefore []string
|
|
filepath.Walk(wsDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
rel, _ := filepath.Rel(wsDir, path)
|
|
filesBefore = append(filesBefore, rel)
|
|
return nil
|
|
})
|
|
|
|
// The delete command would check for manifest and refuse.
|
|
// We verify the check logic directly:
|
|
manifestPath := filepath.Join(wsDir, ".ctask", "manifest-start.json")
|
|
_, manifestErr := os.Stat(manifestPath)
|
|
if manifestErr != nil {
|
|
t.Fatal("manifest should exist, blocking delete")
|
|
}
|
|
|
|
// Verify ALL files still exist after the protection check
|
|
var filesAfter []string
|
|
filepath.Walk(wsDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
rel, _ := filepath.Rel(wsDir, path)
|
|
filesAfter = append(filesAfter, rel)
|
|
return nil
|
|
})
|
|
|
|
if len(filesBefore) != len(filesAfter) {
|
|
t.Errorf("file count changed: before=%d, after=%d", len(filesBefore), len(filesAfter))
|
|
}
|
|
|
|
// Verify specific important files
|
|
for _, name := range []string{"task.yaml", "CLAUDE.md", "notes.md", "output/artifact.txt", "context/reference.md"} {
|
|
p := filepath.Join(wsDir, name)
|
|
if _, err := os.Stat(p); os.IsNotExist(err) {
|
|
t.Errorf("file %s was deleted/modified during protection check", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInactiveWorkspaceCanBeDeleted(t *testing.T) {
|
|
root := t.TempDir()
|
|
wsDir := createTestWs(t, root, "general", "2026-04-06_deletable-ws", "active")
|
|
|
|
// No manifest = no active session
|
|
manifestPath := filepath.Join(wsDir, ".ctask", "manifest-start.json")
|
|
if _, err := os.Stat(manifestPath); err == nil {
|
|
t.Fatal("manifest should NOT exist for this test")
|
|
}
|
|
|
|
// Verify the workspace exists
|
|
if _, err := os.Stat(wsDir); os.IsNotExist(err) {
|
|
t.Fatal("workspace should exist before delete")
|
|
}
|
|
|
|
// Simulate deletion (what the command does after all checks pass)
|
|
if err := os.RemoveAll(wsDir); err != nil {
|
|
t.Fatalf("delete failed: %v", err)
|
|
}
|
|
|
|
// Verify it's gone
|
|
if _, err := os.Stat(wsDir); !os.IsNotExist(err) {
|
|
t.Error("workspace should be deleted")
|
|
}
|
|
}
|
|
|
|
func TestEnvVarAlsoBlocksDelete(t *testing.T) {
|
|
root := t.TempDir()
|
|
wsDir := createTestWs(t, root, "general", "2026-04-06_envvar-ws", "active")
|
|
|
|
// Set CTASK_WORKSPACE to match
|
|
os.Setenv("CTASK_WORKSPACE", wsDir)
|
|
defer os.Unsetenv("CTASK_WORKSPACE")
|
|
|
|
absTarget, _ := filepath.Abs(wsDir)
|
|
absActive, _ := filepath.Abs(os.Getenv("CTASK_WORKSPACE"))
|
|
|
|
// Verify the env var check would match
|
|
if absTarget != absActive {
|
|
t.Fatalf("paths should match: %q vs %q", absTarget, absActive)
|
|
}
|
|
|
|
// Verify files are untouched
|
|
for _, name := range []string{"task.yaml", "CLAUDE.md", "notes.md"} {
|
|
p := filepath.Join(wsDir, name)
|
|
if _, err := os.Stat(p); os.IsNotExist(err) {
|
|
t.Errorf("file %s should still exist", name)
|
|
}
|
|
}
|
|
}
|