using XFESystem.Console; using XFESystem.Core; using Xunit; namespace XFESystem.Desktop.Tests; /// Shell input, storage and ANSI output checks without a native VM. 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(() => TerminalShell.ParseCommandLine("echo 'broken")); var shell = new TerminalShell(new MemoryWorkspace(false)); Assert.Equal(SystemPaths.Root + "File.txt", shell.ResolvePath("/../../File.txt")); Assert.Throws(() => 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)); }