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:
2026-02-14 20:24:48 +01:00
parent ae9924ffbb
commit 8189a108d1
17 changed files with 1503 additions and 41 deletions

View File

@@ -169,6 +169,59 @@ retention_interval = "2h"
}
}
func TestLoadShellDefaults(t *testing.T) {
path := writeTemp(t, "")
cfg, err := Load(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Shell.Hostname != "ubuntu-server" {
t.Errorf("default hostname = %q, want %q", cfg.Shell.Hostname, "ubuntu-server")
}
if cfg.Shell.Banner == "" {
t.Error("default banner should not be empty")
}
if cfg.Shell.FakeUser != "" {
t.Errorf("default fake_user = %q, want empty", cfg.Shell.FakeUser)
}
}
func TestLoadShellConfig(t *testing.T) {
content := `
[shell]
hostname = "myhost"
banner = "Custom banner\r\n"
fake_user = "admin"
[shell.bash]
custom_key = "value"
`
path := writeTemp(t, content)
cfg, err := Load(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Shell.Hostname != "myhost" {
t.Errorf("hostname = %q, want %q", cfg.Shell.Hostname, "myhost")
}
if cfg.Shell.Banner != "Custom banner\r\n" {
t.Errorf("banner = %q, want %q", cfg.Shell.Banner, "Custom banner\r\n")
}
if cfg.Shell.FakeUser != "admin" {
t.Errorf("fake_user = %q, want %q", cfg.Shell.FakeUser, "admin")
}
if cfg.Shell.Shells == nil {
t.Fatal("Shells map should not be nil")
}
bashCfg, ok := cfg.Shell.Shells["bash"]
if !ok {
t.Fatal("Shells[\"bash\"] not found")
}
if bashCfg["custom_key"] != "value" {
t.Errorf("Shells[\"bash\"][\"custom_key\"] = %v, want %q", bashCfg["custom_key"], "value")
}
}
func TestLoadMissingFile(t *testing.T) {
_, err := Load("/nonexistent/path/config.toml")
if err == nil {