using XFESystem.Console;
using XFESystem.Core;
using Xunit;
namespace XFESystem.Desktop.Tests;
/// <summary>Shell input, storage and ANSI output checks without a native VM.</summary>
public sealed class TerminalShellTests
{
[Fact]
public void StartupOnlyShowsIdentityVersionAndHelpAndKeepsBlueAnsiTheme()
{
var shell = new TerminalShell(new MemoryWorkspace(false), "TestVersion");
string text = Screen(shell);
Assert.Contains("XFESystem|XFEstudio", text); Assert.Contains("TestVersion", text); Assert.Contains("help", text);
Assert.DoesNotContain("Run Demo", text); Assert.DoesNotContain("sample", text);
Assert.Equal(TerminalColor.ThemeForeground, shell.ReadOutput().GetCell(0, 0).Style.Foreground);
Assert.Equal("#75C9F5", shell.ReadOutput().GetCell(0, 0).Style.Foreground.Hex);
Assert.Equal(SystemPaths.Documents + "> ", shell.Prompt);
}
[Fact]
public void QuotedDirectoriesAndFilesUseRealRamStorageAndMFPreservesExistingData()
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
Assert.Equal(0, Run(shell, "md \"Folder One\"").ExitCode);
Assert.Equal(0, Run(shell, "cd \"Folder One\"").ExitCode);
Assert.Equal(0, Run(shell, "mf \"hello world.txt\"").ExitCode);
Assert.Equal(0, Run(shell, "write \"hello world.txt\" \"Hello 世界\"").ExitCode);
Assert.Equal(0, Run(shell, "append \"hello world.txt\" \" + XFE\"").ExitCode);
Assert.Equal(0, Run(shell, "mf \"hello world.txt\"").ExitCode);
string file = SystemPaths.Documents + "\\Folder One\\hello world.txt";
Assert.Equal("Hello 世界 + XFE", storage.ReadText(file));
Assert.Equal(0, Run(shell, "cat \"hello world.txt\"").ExitCode);
Assert.Contains("Hello 世界 + XFE", Screen(shell));
Assert.Equal(0, Run(shell, "file \"hello world.txt\"").ExitCode);
Assert.Contains("hello world.txt", Screen(shell));
Run(shell, "cp \"hello world.txt\" Copy.txt"); Run(shell, "mv Copy.txt Moved.txt");
Assert.Equal("Hello 世界 + XFE", storage.ReadText(shell.ResolvePath("Moved.txt")));
Run(shell, "del Moved.txt"); Assert.False(storage.Exists(shell.ResolvePath("Moved.txt")));
Assert.Equal(0, Run(shell, "cd ..").ExitCode); Assert.Equal(SystemPaths.Documents, shell.CurrentDirectory);
}
[Fact]
public void FailedCommandsDoNotChangeDirectoryOrOverwriteCopyDestinations()
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
Run(shell, "write First.txt original"); Run(shell, "write Second.txt retained");
Assert.Equal(1, Run(shell, "cp First.txt Second.txt").ExitCode);
Assert.Equal("retained", storage.ReadText(shell.ResolvePath("Second.txt")));
Assert.Equal(1, Run(shell, "cd First.txt").ExitCode); Assert.Equal(SystemPaths.Documents, shell.CurrentDirectory);
Assert.Equal(1, Run(shell, "cd Missing").ExitCode); Assert.Equal(SystemPaths.Documents, shell.CurrentDirectory);
Assert.Equal(1, Run(shell, "mf \"missing quote").ExitCode);
Assert.Equal(127, Run(shell, "unknowncommand").ExitCode);
Assert.Equal(0, Run(shell, "help").ExitCode); Assert.Contains("RAM session", Screen(shell));
}
[Fact]
public void HistoryCanVisitAllEntriesAndRestoreTheDraftWithoutExposingMutableStorage()
{
var shell = new TerminalShell(new MemoryWorkspace(false));
Run(shell, "echo first"); Run(shell, "echo second"); Run(shell, "echo third");
shell.InsertText("draft"); shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo third", shell.Input);
shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo second", shell.Input);
shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo first", shell.Input);
shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo first", shell.Input);
for (int index = 0; index < 3; index++) shell.HandleKey(TerminalShellKey.Down);
Assert.Equal("draft", shell.Input); Assert.Equal(5, shell.Caret);
}
[Fact]
public void CursorInsertionOverwriteAndDeletionPreserveUnicodeScalars()
{
var shell = new TerminalShell(new MemoryWorkspace(false));
shell.InsertText("a😀c"); shell.HandleKey(TerminalShellKey.Left); Assert.Equal(3, shell.Caret);
shell.HandleKey(TerminalShellKey.Left); Assert.Equal(1, shell.Caret);
shell.HandleKey(TerminalShellKey.Delete); Assert.Equal("ac", shell.Input);
shell.InsertText("b"); Assert.Equal("abc", shell.Input);
shell.HandleKey(TerminalShellKey.Home); shell.HandleKey(TerminalShellKey.Insert); shell.InsertText("😀");
Assert.Equal("😀bc", shell.Input); Assert.Equal(2, shell.Caret);
shell.HandleKey(TerminalShellKey.Backspace); Assert.Equal("bc", shell.Input); Assert.Equal(0, shell.Caret);
shell.HandleKey(TerminalShellKey.End); shell.HandleKey(TerminalShellKey.Right); Assert.Equal(2, shell.Caret);
}
[Fact]
public void CancelAndPasteCannotExecuteCommandsOrInjectAnsiControls()
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
shell.InsertText("mf MustNotExist.txt"); shell.HandleKey(TerminalShellKey.Cancel);
Assert.False(storage.Exists(shell.ResolvePath("MustNotExist.txt"))); Assert.Empty(shell.Input); Assert.Empty(shell.History);
Assert.False(shell.InsertText("mf Injected.txt\n")); Assert.Empty(shell.Input);
Assert.False(shell.InsertText("\u001b[2J")); Assert.False(shell.InsertText("\ud800"));
Assert.Contains("^C", Screen(shell));
}
[Fact]
public void InputHistoryAndOutputRemainBounded()
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
Assert.True(shell.InsertText(new string('x', TerminalShell.MaximumInputCharacters)));
Assert.False(shell.InsertText("y")); Assert.Equal(TerminalShell.MaximumInputCharacters, shell.Input.Length);
shell.HandleKey(TerminalShellKey.Cancel);
for (int index = 0; index < 80; index++) Run(shell, "echo " + index);
Assert.Equal(TerminalShell.MaximumHistoryEntries, shell.History.Count); Assert.Equal("echo 16", shell.History[0]);
storage.WriteText(shell.ResolvePath("Large.txt"), new string('A', 20_000)); Run(shell, "cat Large.txt");
Assert.Contains("Output truncated", Screen(shell));
}
[Fact]
public void ApplicationAnsiOutputUsesTheOriginalCellModelAndClsResetsIt()
{
var shell = new TerminalShell(new MemoryWorkspace(false)); Run(shell, "cls");
shell.ProgramOutput.Write("\u001b[38;2;23;45;67mX\u001b[0mY");
var frame = shell.ReadOutput(); Assert.Equal("X", frame.GetCell(0, 0).Text);
Assert.Equal(new TerminalColor(23, 45, 67), frame.GetCell(0, 0).Style.Foreground);
Assert.Equal(TerminalColor.ThemeForeground, frame.GetCell(1, 0).Style.Foreground);
Run(shell, "cls"); Assert.Equal(0, shell.ReadOutput().CursorRow); Assert.Equal(0, shell.ReadOutput().CursorColumn);
}
[Fact]
public void OpenRunAndExitReturnRequestsWithoutExecutingUiOrProgramsOnTheInputThread()
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage); Run(shell, "mf Example.cs");
Assert.Equal(shell.ResolvePath("Example.cs"), Run(shell, "open Example.cs").OpenPath);
Assert.Equal(shell.ResolvePath("Example.cs"), Run(shell, "run Example.cs").ManagedProgramPath);
Assert.True(Run(shell, "exit").CloseRequested);
Assert.Equal(1, Run(shell, "run Missing.dll").ExitCode);
}
[Fact]
public void ParserPreservesWindowsBackslashesEmptyArgumentsAndRejectsUnclosedQuotes()
{
Assert.Equal(new[] { "write", @"X:\Folder One\File.txt", "" }, TerminalShell.ParseCommandLine("write \"X:\\Folder One\\File.txt\" \"\""));
Assert.Equal(new[] { "echo", "Hello 世界" }, TerminalShell.ParseCommandLine("echo 'Hello 世界'"));
Assert.Throws<ArgumentException>(() => TerminalShell.ParseCommandLine("echo 'broken"));
var shell = new TerminalShell(new MemoryWorkspace(false));
Assert.Equal(SystemPaths.Root + "File.txt", shell.ResolvePath("/../../File.txt"));
Assert.Throws<ArgumentException>(() => shell.ResolvePath(@"C:\HostSecret.txt"));
}
[Fact]
public void NativeLfHostKeepsEveryEchoAndReadOutputAtColumnZeroIncludingPartialProgramLines()
{
var shell = new TerminalShell(new MemoryWorkspace(false), columns: 96, rows: 128);
shell.ProgramOutput.NewLine = "\n"; Run(shell, "cls");
shell.ProgramOutput.Write("progress\r");
string[] commands = ["help", "md CliProof", "mf CliProof/Command.txt", "write CliProof/Command.txt NativeTerminalReady",
"cat CliProof/Command.txt", "write CliProof/Command.txt NativeTerminalReady", "echo draft", "echo abd", "history"];
foreach (string command in commands)
{
int before = shell.ReadOutput().CursorRow;
Assert.Equal(0, Run(shell, command).ExitCode);
var frame = shell.ReadOutput(); Assert.Equal(0, frame.CursorColumn);
var fresh = Enumerable.Range(before, frame.CursorRow - before + 1).Select(row => Row(frame, row)).ToArray();
Assert.Contains(shell.Prompt + command, fresh);
if (command.StartsWith("cat", StringComparison.Ordinal)) Assert.Contains("NativeTerminalReady", fresh);
if (command == "echo draft") Assert.Contains("draft", fresh);
if (command == "echo abd") Assert.Contains("abd", fresh);
}
Assert.Equal("progress", Row(shell.ReadOutput(), 0));
}
[Theory]
[InlineData("\n")]
[InlineData("\r\n")]
public void CatDisplaysLogicalFileLinesAtColumnZeroWithoutChangingStoredBytes(string ending)
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
shell.ProgramOutput.NewLine = "\n"; Run(shell, "cls");
string path = shell.ResolvePath("Lines.txt"), source = "first" + ending + "second";
storage.WriteText(path, source); Assert.Equal(0, Run(shell, "cat Lines.txt").ExitCode);
var frame = shell.ReadOutput();
Assert.Equal(shell.Prompt + "cat Lines.txt", Row(frame, 0));
Assert.Equal("first", Row(frame, 1)); Assert.Equal("second", Row(frame, 2));
Assert.Equal(0, frame.CursorColumn); Assert.Equal(source, storage.ReadText(path));
}
private static string Row(TerminalSnapshot frame, int row) => string.Concat(Enumerable.Range(0, frame.Columns)
.Where(column => !frame.GetCell(column, row).IsContinuation).Select(column => frame.GetCell(column, row).Text)).TrimEnd();
private static TerminalShellCommandResult Run(TerminalShell shell, string command)
{ Assert.True(shell.InsertText(command)); return shell.Submit(); }
private static string Screen(TerminalShell shell) => string.Concat(shell.ReadOutput().Cells.Select(cell => cell.Text));
}
using XFESystem.Console;
using XFESystem.Core;
using Xunit;
namespace XFESystem.Desktop.Tests;
/// <summary>Shell input, storage and ANSI output checks without a native VM.</summary>
public sealed class TerminalShellTests
{
[Fact]
public void StartupOnlyShowsIdentityVersionAndHelpAndKeepsBlueAnsiTheme()
{
var shell = new TerminalShell(new MemoryWorkspace(false), "TestVersion");
string text = Screen(shell);
Assert.Contains("XFESystem|XFEstudio", text); Assert.Contains("TestVersion", text); Assert.Contains("help", text);
Assert.DoesNotContain("Run Demo", text); Assert.DoesNotContain("sample", text);
Assert.Equal(TerminalColor.ThemeForeground, shell.ReadOutput().GetCell(0, 0).Style.Foreground);
Assert.Equal("#75C9F5", shell.ReadOutput().GetCell(0, 0).Style.Foreground.Hex);
Assert.Equal(SystemPaths.Documents + "> ", shell.Prompt);
}
[Fact]
public void QuotedDirectoriesAndFilesUseRealRamStorageAndMFPreservesExistingData()
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
Assert.Equal(0, Run(shell, "md \"Folder One\"").ExitCode);
Assert.Equal(0, Run(shell, "cd \"Folder One\"").ExitCode);
Assert.Equal(0, Run(shell, "mf \"hello world.txt\"").ExitCode);
Assert.Equal(0, Run(shell, "write \"hello world.txt\" \"Hello 世界\"").ExitCode);
Assert.Equal(0, Run(shell, "append \"hello world.txt\" \" + XFE\"").ExitCode);
Assert.Equal(0, Run(shell, "mf \"hello world.txt\"").ExitCode);
string file = SystemPaths.Documents + "\\Folder One\\hello world.txt";
Assert.Equal("Hello 世界 + XFE", storage.ReadText(file));
Assert.Equal(0, Run(shell, "cat \"hello world.txt\"").ExitCode);
Assert.Contains("Hello 世界 + XFE", Screen(shell));
Assert.Equal(0, Run(shell, "file \"hello world.txt\"").ExitCode);
Assert.Contains("hello world.txt", Screen(shell));
Run(shell, "cp \"hello world.txt\" Copy.txt"); Run(shell, "mv Copy.txt Moved.txt");
Assert.Equal("Hello 世界 + XFE", storage.ReadText(shell.ResolvePath("Moved.txt")));
Run(shell, "del Moved.txt"); Assert.False(storage.Exists(shell.ResolvePath("Moved.txt")));
Assert.Equal(0, Run(shell, "cd ..").ExitCode); Assert.Equal(SystemPaths.Documents, shell.CurrentDirectory);
}
[Fact]
public void FailedCommandsDoNotChangeDirectoryOrOverwriteCopyDestinations()
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
Run(shell, "write First.txt original"); Run(shell, "write Second.txt retained");
Assert.Equal(1, Run(shell, "cp First.txt Second.txt").ExitCode);
Assert.Equal("retained", storage.ReadText(shell.ResolvePath("Second.txt")));
Assert.Equal(1, Run(shell, "cd First.txt").ExitCode); Assert.Equal(SystemPaths.Documents, shell.CurrentDirectory);
Assert.Equal(1, Run(shell, "cd Missing").ExitCode); Assert.Equal(SystemPaths.Documents, shell.CurrentDirectory);
Assert.Equal(1, Run(shell, "mf \"missing quote").ExitCode);
Assert.Equal(127, Run(shell, "unknowncommand").ExitCode);
Assert.Equal(0, Run(shell, "help").ExitCode); Assert.Contains("RAM session", Screen(shell));
}
[Fact]
public void HistoryCanVisitAllEntriesAndRestoreTheDraftWithoutExposingMutableStorage()
{
var shell = new TerminalShell(new MemoryWorkspace(false));
Run(shell, "echo first"); Run(shell, "echo second"); Run(shell, "echo third");
shell.InsertText("draft"); shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo third", shell.Input);
shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo second", shell.Input);
shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo first", shell.Input);
shell.HandleKey(TerminalShellKey.Up); Assert.Equal("echo first", shell.Input);
for (int index = 0; index < 3; index++) shell.HandleKey(TerminalShellKey.Down);
Assert.Equal("draft", shell.Input); Assert.Equal(5, shell.Caret);
}
[Fact]
public void CursorInsertionOverwriteAndDeletionPreserveUnicodeScalars()
{
var shell = new TerminalShell(new MemoryWorkspace(false));
shell.InsertText("a😀c"); shell.HandleKey(TerminalShellKey.Left); Assert.Equal(3, shell.Caret);
shell.HandleKey(TerminalShellKey.Left); Assert.Equal(1, shell.Caret);
shell.HandleKey(TerminalShellKey.Delete); Assert.Equal("ac", shell.Input);
shell.InsertText("b"); Assert.Equal("abc", shell.Input);
shell.HandleKey(TerminalShellKey.Home); shell.HandleKey(TerminalShellKey.Insert); shell.InsertText("😀");
Assert.Equal("😀bc", shell.Input); Assert.Equal(2, shell.Caret);
shell.HandleKey(TerminalShellKey.Backspace); Assert.Equal("bc", shell.Input); Assert.Equal(0, shell.Caret);
shell.HandleKey(TerminalShellKey.End); shell.HandleKey(TerminalShellKey.Right); Assert.Equal(2, shell.Caret);
}
[Fact]
public void CancelAndPasteCannotExecuteCommandsOrInjectAnsiControls()
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
shell.InsertText("mf MustNotExist.txt"); shell.HandleKey(TerminalShellKey.Cancel);
Assert.False(storage.Exists(shell.ResolvePath("MustNotExist.txt"))); Assert.Empty(shell.Input); Assert.Empty(shell.History);
Assert.False(shell.InsertText("mf Injected.txt\n")); Assert.Empty(shell.Input);
Assert.False(shell.InsertText("\u001b[2J")); Assert.False(shell.InsertText("\ud800"));
Assert.Contains("^C", Screen(shell));
}
[Fact]
public void InputHistoryAndOutputRemainBounded()
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
Assert.True(shell.InsertText(new string('x', TerminalShell.MaximumInputCharacters)));
Assert.False(shell.InsertText("y")); Assert.Equal(TerminalShell.MaximumInputCharacters, shell.Input.Length);
shell.HandleKey(TerminalShellKey.Cancel);
for (int index = 0; index < 80; index++) Run(shell, "echo " + index);
Assert.Equal(TerminalShell.MaximumHistoryEntries, shell.History.Count); Assert.Equal("echo 16", shell.History[0]);
storage.WriteText(shell.ResolvePath("Large.txt"), new string('A', 20_000)); Run(shell, "cat Large.txt");
Assert.Contains("Output truncated", Screen(shell));
}
[Fact]
public void ApplicationAnsiOutputUsesTheOriginalCellModelAndClsResetsIt()
{
var shell = new TerminalShell(new MemoryWorkspace(false)); Run(shell, "cls");
shell.ProgramOutput.Write("\u001b[38;2;23;45;67mX\u001b[0mY");
var frame = shell.ReadOutput(); Assert.Equal("X", frame.GetCell(0, 0).Text);
Assert.Equal(new TerminalColor(23, 45, 67), frame.GetCell(0, 0).Style.Foreground);
Assert.Equal(TerminalColor.ThemeForeground, frame.GetCell(1, 0).Style.Foreground);
Run(shell, "cls"); Assert.Equal(0, shell.ReadOutput().CursorRow); Assert.Equal(0, shell.ReadOutput().CursorColumn);
}
[Fact]
public void OpenRunAndExitReturnRequestsWithoutExecutingUiOrProgramsOnTheInputThread()
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage); Run(shell, "mf Example.cs");
Assert.Equal(shell.ResolvePath("Example.cs"), Run(shell, "open Example.cs").OpenPath);
Assert.Equal(shell.ResolvePath("Example.cs"), Run(shell, "run Example.cs").ManagedProgramPath);
Assert.True(Run(shell, "exit").CloseRequested);
Assert.Equal(1, Run(shell, "run Missing.dll").ExitCode);
}
[Fact]
public void ParserPreservesWindowsBackslashesEmptyArgumentsAndRejectsUnclosedQuotes()
{
Assert.Equal(new[] { "write", @"X:\Folder One\File.txt", "" }, TerminalShell.ParseCommandLine("write \"X:\\Folder One\\File.txt\" \"\""));
Assert.Equal(new[] { "echo", "Hello 世界" }, TerminalShell.ParseCommandLine("echo 'Hello 世界'"));
Assert.Throws<ArgumentException>(() => TerminalShell.ParseCommandLine("echo 'broken"));
var shell = new TerminalShell(new MemoryWorkspace(false));
Assert.Equal(SystemPaths.Root + "File.txt", shell.ResolvePath("/../../File.txt"));
Assert.Throws<ArgumentException>(() => shell.ResolvePath(@"C:\HostSecret.txt"));
}
[Fact]
public void NativeLfHostKeepsEveryEchoAndReadOutputAtColumnZeroIncludingPartialProgramLines()
{
var shell = new TerminalShell(new MemoryWorkspace(false), columns: 96, rows: 128);
shell.ProgramOutput.NewLine = "\n"; Run(shell, "cls");
shell.ProgramOutput.Write("progress\r");
string[] commands = ["help", "md CliProof", "mf CliProof/Command.txt", "write CliProof/Command.txt NativeTerminalReady",
"cat CliProof/Command.txt", "write CliProof/Command.txt NativeTerminalReady", "echo draft", "echo abd", "history"];
foreach (string command in commands)
{
int before = shell.ReadOutput().CursorRow;
Assert.Equal(0, Run(shell, command).ExitCode);
var frame = shell.ReadOutput(); Assert.Equal(0, frame.CursorColumn);
var fresh = Enumerable.Range(before, frame.CursorRow - before + 1).Select(row => Row(frame, row)).ToArray();
Assert.Contains(shell.Prompt + command, fresh);
if (command.StartsWith("cat", StringComparison.Ordinal)) Assert.Contains("NativeTerminalReady", fresh);
if (command == "echo draft") Assert.Contains("draft", fresh);
if (command == "echo abd") Assert.Contains("abd", fresh);
}
Assert.Equal("progress", Row(shell.ReadOutput(), 0));
}
[Theory]
[InlineData("\n")]
[InlineData("\r\n")]
public void CatDisplaysLogicalFileLinesAtColumnZeroWithoutChangingStoredBytes(string ending)
{
var storage = new MemoryWorkspace(false); var shell = new TerminalShell(storage);
shell.ProgramOutput.NewLine = "\n"; Run(shell, "cls");
string path = shell.ResolvePath("Lines.txt"), source = "first" + ending + "second";
storage.WriteText(path, source); Assert.Equal(0, Run(shell, "cat Lines.txt").ExitCode);
var frame = shell.ReadOutput();
Assert.Equal(shell.Prompt + "cat Lines.txt", Row(frame, 0));
Assert.Equal("first", Row(frame, 1)); Assert.Equal("second", Row(frame, 2));
Assert.Equal(0, frame.CursorColumn); Assert.Equal(source, storage.ReadText(path));
}
private static string Row(TerminalSnapshot frame, int row) => string.Concat(Enumerable.Range(0, frame.Columns)
.Where(column => !frame.GetCell(column, row).IsContinuation).Select(column => frame.GetCell(column, row).Text)).TrimEnd();
private static TerminalShellCommandResult Run(TerminalShell shell, string command)
{ Assert.True(shell.InsertText(command)); return shell.Submit(); }
private static string Screen(TerminalShell shell) => string.Concat(shell.ReadOutput().Cells.Select(cell => cell.Text));
}