feat: add shell interface, registry, and bash shell emulator
Implement Phase 1.4: replaces the hardcoded banner/timeout stub with a proper shell system. Adds a Shell interface with weighted registry for shell selection, a RecordingChannel wrapper (pass-through for now, prep for Phase 2.3 replay), and a bash-like shell with fake filesystem, terminal line reader, and command handling (pwd, ls, cd, cat, whoami, hostname, id, uname, exit). Sessions now log command/output pairs to the store and record the shell name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
140
internal/shell/bash/filesystem_test.go
Normal file
140
internal/shell/bash/filesystem_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package bash
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewFilesystem(t *testing.T) {
|
||||
fs := newFilesystem("testhost")
|
||||
|
||||
// Standard directories should exist.
|
||||
for _, dir := range []string{"/etc", "/root", "/home", "/var/log", "/tmp", "/usr/bin"} {
|
||||
if !fs.isDirectory(dir) {
|
||||
t.Errorf("%s should be a directory", dir)
|
||||
}
|
||||
}
|
||||
|
||||
// Standard files should exist.
|
||||
for _, file := range []string{"/etc/passwd", "/etc/hostname", "/root/.bashrc", "/tmp/notes.txt"} {
|
||||
if !fs.exists(file) {
|
||||
t.Errorf("%s should exist", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemHostname(t *testing.T) {
|
||||
fs := newFilesystem("myhost")
|
||||
content, err := fs.read("/etc/hostname")
|
||||
if err != nil {
|
||||
t.Fatalf("read /etc/hostname: %v", err)
|
||||
}
|
||||
if content != "myhost\n" {
|
||||
t.Errorf("hostname content = %q, want %q", content, "myhost\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
cwd string
|
||||
arg string
|
||||
want string
|
||||
}{
|
||||
{"/root", "file.txt", "/root/file.txt"},
|
||||
{"/root", "/etc/passwd", "/etc/passwd"},
|
||||
{"/root", "..", "/"},
|
||||
{"/var/log", "../..", "/"},
|
||||
{"/root", ".", "/root"},
|
||||
{"/root", "./sub/file", "/root/sub/file"},
|
||||
{"/", "etc", "/etc"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := resolvePath(tt.cwd, tt.arg)
|
||||
if got != tt.want {
|
||||
t.Errorf("resolvePath(%q, %q) = %q, want %q", tt.cwd, tt.arg, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemList(t *testing.T) {
|
||||
fs := newFilesystem("testhost")
|
||||
|
||||
names, err := fs.list("/etc")
|
||||
if err != nil {
|
||||
t.Fatalf("list /etc: %v", err)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
// Should contain at least passwd, hostname, hosts.
|
||||
found := map[string]bool{}
|
||||
for _, n := range names {
|
||||
found[n] = true
|
||||
}
|
||||
for _, want := range []string{"passwd", "hostname", "hosts"} {
|
||||
if !found[want] {
|
||||
t.Errorf("list /etc missing %q, got %v", want, names)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemListNonexistent(t *testing.T) {
|
||||
fs := newFilesystem("testhost")
|
||||
_, err := fs.list("/nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error listing nonexistent directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemListFile(t *testing.T) {
|
||||
fs := newFilesystem("testhost")
|
||||
_, err := fs.list("/etc/passwd")
|
||||
if err == nil {
|
||||
t.Fatal("expected error listing a file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemRead(t *testing.T) {
|
||||
fs := newFilesystem("testhost")
|
||||
content, err := fs.read("/etc/passwd")
|
||||
if err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
if content == "" {
|
||||
t.Error("expected non-empty content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemReadNonexistent(t *testing.T) {
|
||||
fs := newFilesystem("testhost")
|
||||
_, err := fs.read("/no/such/file")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemReadDirectory(t *testing.T) {
|
||||
fs := newFilesystem("testhost")
|
||||
_, err := fs.read("/etc")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for reading a directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemDirectoryListing(t *testing.T) {
|
||||
fs := newFilesystem("testhost")
|
||||
names, err := fs.list("/")
|
||||
if err != nil {
|
||||
t.Fatalf("list /: %v", err)
|
||||
}
|
||||
|
||||
// Root directories should end with /
|
||||
found := map[string]bool{}
|
||||
for _, n := range names {
|
||||
found[n] = true
|
||||
}
|
||||
for _, want := range []string{"etc/", "root/", "home/", "var/", "tmp/", "usr/"} {
|
||||
if !found[want] {
|
||||
t.Errorf("list / missing %q, got %v", want, names)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user