Flatten terminal modes: Wozniak syntax and monitor commands work from main prompt

- MainMode falls through unrecognized commands to MonitorMode's pattern
  dispatcher, so 4000G / E000G / 100.200 / C000 etc. work at JACE> without
  any mode switch
- Guard prevents q/back from reaching MonitorMode via fallthrough
- Added go, mem, cpu, registers, break, runto forwarding commands in MainMode
- Fixed alias collision in MonitorMode: compare's 'c' alias silently destroyed
  by cheat's 'c'; renamed to cmp/ch respectively; addAlias() now fails fast
- Fixed setMode() stdout flood: no longer dumps full help text on every switch
- Added getModeByName() to JaceTerminal for cross-mode delegation
- Introduced WatchCallback interface to decouple Watch from MonitorMode
- MonitorMode implements WatchCallback; package-private methods promoted
- AssemblerMode Javadoc clarifies it is an intentional reserved stub
- CLAUDE.md updated: Wozniak syntax used throughout, mode-switching patterns
  removed from automation examples, b=break (not back) clarified
- 4 new MainModeTest tests covering Wozniak fallthrough and q guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
BadvisionandClaude Sonnet 4.6 committed 2026-04-25 17:29:10 +02:00
1 parent 3e478d93b1
commit 39881bb420
8 files changed
+280 -95

No files matched your search

+57 -39
View File
@@ -31,9 +31,7 @@ mvn -q exec:java -Dexec.mainClass="jace.JaceLauncher" -Dexec.args="--terminal" <
bootdisk d1 /path/to/disk.po 7
run 5000000
screenshot /tmp/frame.png
m
C07F
b
mem C07F C07F
qq
EOF
```
@@ -91,23 +89,21 @@ the system is in the Disk ][ boot ROM at $C600, waiting for a floppy. To start
Applesoft BASIC directly from the ROM, use the monitor:
reset
m
E000G
q
run 2000000
**Critical**: `E000G` starts execution at $E000 asynchronously and returns
immediately to the monitor prompt `*`. BASIC cold-start has NOT finished yet.
You MUST run cycles after returning to main mode (`q`) to let BASIC initialize.
immediately to the main prompt. BASIC cold-start has NOT finished yet.
You MUST run cycles after `E000G` to let BASIC initialize.
`run 2000000` gives enough cycles for BASIC to cold-start and reach its `]` prompt.
After `run 2000000` completes, TXTTAB ($67-$68) should be `01 08` ($0801).
**Verify with `m` / `67.68` / `q` before calling `lbas`** — if TXTTAB is
**Verify with `67.68` before calling `lbas`** — if TXTTAB is
wrong (e.g. pointing at screen memory $0400), `lbas` will overwrite screen RAM
with the program text, causing garbled screen output and BASIC failures.
To reach the warm-start (re-enter BASIC without reinitializing), use `FF69G`
followed by `q` and `run 500000`.
followed by `run 500000`.
### Complete Applesoft BASIC Test Workflow (No Disk)
@@ -115,9 +111,7 @@ followed by `q` and `run 500000`.
```
reset
m
E000G
q
run 2000000
lbas /path/to/program.bas
key "RUN\n"
@@ -133,7 +127,7 @@ Notes:
- **Using `PR#3`** to switch to 80-column mode goes through the 80-col firmware's
keyboard input loop and screen output loop. This works correctly in headless
terminal mode; it does NOT hang.
- `waitkey` and `type` (synchronized keyboard) do NOT work after `E000G` + `q`
- `waitkey` and `type` (synchronized keyboard) do NOT work after `E000G`
because the emulator is paused; use `key` + `run N` or `key` + `expect` instead
### BASIC Variable Table Layout (Applesoft Internals)
@@ -313,7 +307,7 @@ confused with a number at the end of the string content.
- If the emulator was paused, it is resumed once at the start
- All characters are typed, each waiting for a keyboard read, but bounded by the total timeout
- After the last character is typed (or timeout reached), if the emulator was paused before, it is re-paused
- Use this when running a BASIC program after `E000G` + `q` since a single resume cycle is needed
- Use this when running a BASIC program after `E000G` since a single resume cycle is needed
### 7. Tokenizing Applesoft BASIC Programs
@@ -449,13 +443,48 @@ Example for running a program:
key "+FPUMF_AUTO_TEST\n"
```
### `step` - Single-Step CPU
### `tick` - Step Motherboard (All Devices)
```
tick [count]
```
Steps the full motherboard cascade (all devices) for N ticks (default: 1). Use for timing-sensitive device stepping. Alias: `tc`.
### `step` - Single-Step CPU (Monitor Mode)
```
step [count]
```
Steps the CPU instruction-by-instruction (default: 1 step).
Steps the CPU instruction-by-instruction (default: 1 step). Available in monitor mode and directly from main mode (no mode switch required).
### `go` - Execute from Address (No Mode Switch)
```
go <addr>
```
Sets PC to the specified hex address and begins execution. Equivalent to entering monitor and typing `4000G`. Example: `go 4000`
### `mem` - Dump Memory Range (No Mode Switch)
```
mem <start> <end>
```
Hex dump of a memory address range. Equivalent to entering monitor and typing `3800.3820`. Example: `mem 3800 3820`
### `cpu` / `registers` / `break` / `runto` (No Mode Switch)
These monitor commands are now available directly in main mode:
```
cpu # Show CPU state (PC, A, X, Y, SP, flags)
registers [reg value] # Show or set registers (alias: reg)
break <addr> # Set/remove/list breakpoints (alias: bp)
runto <addr> # Run until PC reaches address (alias: rt)
```
### `reset` - System Reset
@@ -467,7 +496,11 @@ Performs a cold start reset of the Apple II.
## Monitor Mode Commands
Enter monitor mode with `monitor` (or `m`). Return to main mode with `back` (or `b`).
Enter monitor mode with `monitor` (or `m`). Return to main mode with `back`, `quit`, or `q`.
**Note:** `b` is the alias for `break` (set breakpoint), NOT for `back`. Use `q` to exit monitor mode.
**Tip:** Wozniak monitor syntax (`4000G`, `3800.3820`, `E000G`, etc.) and named commands (`cpu`, `registers`, `break`, `runto`) work directly from the main `JACE>` prompt — no mode switch needed.
### Memory Examination
```
@@ -662,14 +695,10 @@ This exact sequence has been validated to work reliably. Use it as a template:
cat > /tmp/jace_test.txt << 'EOF'
reset
loadbin /path/to/program.bin 4000
monitor
4000G
quit
run 5000000
showtext
monitor
3800.3820
quit
qq
EOF
@@ -680,12 +709,11 @@ timeout 90 mvn -q exec:java -Dexec.mainClass="jace.JaceLauncher" \
The flow:
1. `reset` - Clean emulator state
2. `loadbin` - Load binary directly into memory (no disk boot needed)
3. `monitor` / `4000G` - Enter monitor, execute from address
4. `quit` - Return to main mode
5. `run N` - Let program run for N CPU cycles
6. `showtext` - Capture what's on screen
7. `monitor` / `ADDR.ADDR` - Dump memory to verify results
8. `qq` - Quit JACE
3. `4000G` - Set PC to $4000 and begin execution (Wozniak monitor syntax, works from main prompt)
4. `run N` - Let program run for N CPU cycles
5. `showtext` - Capture what's on screen
6. `3800.3820` - Dump memory to verify results (Wozniak range syntax, works from main prompt)
7. `qq` - Quit JACE
### Using `expect` Instead of Fixed Cycle Counts
@@ -694,14 +722,10 @@ When you know what text the program should produce, `expect` is more reliable th
```
reset
loadbin /path/to/program.bin 4000
monitor
4000G
quit
expect "DONE" 30
showtext
monitor
3800.3820
quit
qq
```
@@ -726,11 +750,9 @@ BASIC, the key behaviors to verify are:
BASIC with the display stuck in HGR mode, and `expect "]"` will still match
because BASIC wrote its prompt to text memory — but the user sees graphics.
To verify TEXT mode was restored, check the display soft switch via monitor:
To verify TEXT mode was restored, check the display soft switch:
```
monitor
C01A.C01A
quit
```
If bit 7 of the value at $C01A is 0, TEXT mode is active. If bit 7 is 1,
graphics mode is still on.
@@ -847,15 +869,13 @@ This allows test harnesses to extend the $FC command set for specialized testing
### Memory Inspection for Verification
After running code, use the monitor to verify memory contents. This is more reliable than screen output for checking computed values:
After running code, use these commands to verify memory contents. This is more reliable than screen output for checking computed values:
```
monitor
3800.3810 # Dump variable storage
0900.0940 # Dump generated code
D000.D020 # Check Language Card contents
cpu # Show CPU state (PC tells you where it stopped)
quit
```
Common patterns in memory dumps:
@@ -871,7 +891,6 @@ Common patterns in memory dumps:
When you know roughly where a problem is, use breakpoints:
```
monitor
break 4050 # Break when PC reaches $4050
4000G # Start execution
cpu # Check state at breakpoint
@@ -879,7 +898,6 @@ step 5 # Step through 5 instructions
cpu # Check state again
3800.3808 # Inspect memory
resume # Continue execution
quit
```
### Diagnosing Common Failures
@@ -887,7 +905,7 @@ quit
**`CALL addr` never returns to BASIC (caller never resumes)**:
- Common cause: a JSR inside the machine-language routine calls a ROM entry point that does not exit via RTS. Example: on the Apple IIe, `JSR $FB39` does not return — it ends with `JMP $C100` (unconditional jump to slot 1 firmware ROM). The `RTS` that was supposed to follow the JSR is never reached.
- How to detect: the machine is not crashed (it keeps running), but the BASIC line after the `CALL` never executes. A test BASIC program such as `10 CALL 24576 / 20 PRINT "DONE"` combined with `expect "DONE"` will time out.
- Debug strategy: before running, set a breakpoint at `$C100` in the monitor (`break C100`). If the breakpoint fires, a ROM routine hijacked execution instead of returning.
- Debug strategy: before running, set a breakpoint at `$C100` (`break C100`). If the breakpoint fires, a ROM routine hijacked execution instead of returning.
- Fix: remove or replace the offending JSR. If the goal was to restore TEXT mode, use the correct ROM entry point (`$FB36` is SETTXT on the Apple IIe) or simply omit the call if the caller does not require a display-mode switch.
**Program prints banner then hangs**:
@@ -19,8 +19,10 @@ package jace.terminal;
import java.io.PrintStream;
/**
* Assembler mode for the Terminal - handles assembly language input
* This is a stub that will be implemented in the future
* Assembler mode for the Terminal — reserved stub.
* The "assembler" command in MainMode switches to this mode so the name
* exists in the mode registry. No assembly implementation is planned (YAGNI);
* this class exists only to hold the mode slot and prevent NPE on setMode("assembler").
*/
public class AssemblerMode implements TerminalMode {
private final JaceTerminal terminal;
+10 -2
View File
@@ -92,8 +92,7 @@ public class JaceTerminal {
TerminalMode mode = modes.get(modeName.toLowerCase());
if (mode != null) {
currentMode = mode;
output.println("Switched to " + mode.getName() + " mode");
mode.printHelp();
output.println("Switched to " + mode.getName() + " mode (type ? for help)");
// Notify UI about mode change if we're in UI mode
updateUIWithCurrentMode();
@@ -111,6 +110,15 @@ public class JaceTerminal {
// Override in UI-specific implementations to update UI
}
/**
* Get a mode instance by name — allows cross-mode command delegation.
* @param name Mode name (case-insensitive)
* @return The mode instance, or null if not found
*/
public TerminalMode getModeByName(String name) {
return modes.get(name.toLowerCase());
}
/**
* Access to the PrintStream for output
* @return the output stream
+72 -9
View File
@@ -28,6 +28,7 @@ import java.util.logging.Logger;
import javax.imageio.ImageIO;
import jace.Emulator;
import jace.terminal.MemoryMode;
import jace.apple2e.MOS65C02;
import jace.apple2e.SoftSwitches;
import jace.applesoft.ApplesoftProgram;
@@ -71,7 +72,7 @@ public class MainMode implements TerminalMode {
commands.put("swstate", this::showSoftSwitchState);
commands.put("reset", args -> performReset());
commands.put("step", this::stepCPU);
commands.put("tick", this::stepCPU);
commands.put("run", this::runCPU);
commands.put("insertdisk", this::insertDisk);
@@ -94,13 +95,31 @@ public class MainMode implements TerminalMode {
commands.put("help", this::showHelp);
commands.put("charlog", this::toggleCharLog);
// Monitor forwarding commands — invoke MonitorMode capabilities without a mode switch
commands.put("go", args -> {
MonitorMode mon = getMonitorMode(); if (mon == null) return;
if (args.length < 1) { output.println("Usage: go <addr>"); return; }
try { mon.executeCode(parseHexAddress(args[0])); }
catch (NumberFormatException e) { output.println("Invalid address: " + e.getMessage()); }
});
commands.put("mem", args -> {
MonitorMode mon = getMonitorMode(); if (mon == null) return;
if (args.length < 2) { output.println("Usage: mem <start> <end>"); return; }
try { mon.examineMemoryRange(parseHexAddress(args[0]), parseHexAddress(args[1]), MemoryMode.ACTIVE); }
catch (NumberFormatException e) { output.println("Invalid address: " + e.getMessage()); }
});
commands.put("cpu", args -> { MonitorMode mon = getMonitorMode(); if (mon != null) mon.showCpuState(); });
commands.put("registers", args -> { MonitorMode mon = getMonitorMode(); if (mon != null) mon.handleRegisters(args); });
commands.put("break", args -> { MonitorMode mon = getMonitorMode(); if (mon != null) mon.handleBreakpoint(args); });
commands.put("runto", args -> { MonitorMode mon = getMonitorMode(); if (mon != null) mon.handleRunTo(args); });
addAlias("m", "monitor");
addAlias("a", "assembler");
addAlias("d", "debugger");
addAlias("sl", "swlog");
addAlias("ss", "swstate");
addAlias("re", "reset");
addAlias("s", "step");
addAlias("tc", "tick");
addAlias("g", "run");
addAlias("id", "insertdisk");
addAlias("ed", "ejectdisk");
@@ -116,6 +135,9 @@ public class MainMode implements TerminalMode {
addAlias("sp", "speed");
addAlias("?", "help");
addAlias("cl", "charlog");
addAlias("reg", "registers");
addAlias("bp", "break");
addAlias("rt", "runto");
commandHelp.put("monitor",
"Enters monitor mode for memory examination, manipulation, and debugging.\nUsage: monitor (or m)\nNote: All debugger commands are now integrated into monitor mode.");
@@ -131,9 +153,23 @@ public class MainMode implements TerminalMode {
commandHelp.put("reset", "Resets the Apple II.\nUsage: reset (or re)");
commandHelp.put("step",
"Steps the CPU for a specified number of cycles.\nUsage: step [count] (or s [count])\n" +
"If count is omitted, steps for 1 cycle.");
commandHelp.put("tick",
"Steps the motherboard (all devices) for a specified number of ticks.\nUsage: tick [count] (or tc [count])\n" +
"If count is omitted, steps for 1 tick.\n" +
"Note: 'tick' advances the full motherboard cascade. Use 'step' (in monitor mode) for single CPU instruction stepping.");
commandHelp.put("go",
"Sets the program counter to the specified address and begins execution.\nUsage: go <addr>\n" +
"Example: go 4000 (starts execution at $4000)");
commandHelp.put("mem",
"Dumps a range of memory as hex.\nUsage: mem <start> <end>\n" +
"Example: mem 3800 3820");
commandHelp.put("cpu", "Displays the current CPU state (registers and flags).\nUsage: cpu");
commandHelp.put("registers", "Shows or sets CPU register values.\nUsage: registers [reg value]\nExample: registers PC 4000");
commandHelp.put("break", "Manages execution breakpoints.\nUsage: break <addr> | break -<addr> | break clear | break list");
commandHelp.put("runto", "Runs the CPU until it reaches the specified address.\nUsage: runto <addr> (or rt <addr>)");
commandHelp.put("run", "Runs the CPU for a specified number of cycles or until a breakpoint is hit.\n" +
"Usage: run [count] [#breakpoint] (or g [count] [#breakpoint])\n" +
@@ -245,6 +281,11 @@ public class MainMode implements TerminalMode {
}
private void addAlias(String alias, String command) {
if (commandAliases.containsKey(alias)) {
throw new IllegalStateException(
"Alias conflict in MainMode: '" + alias + "' already maps to '" +
commandAliases.get(alias) + "', cannot also map to '" + command + "'");
}
commandAliases.put(alias, command);
}
@@ -289,10 +330,17 @@ public class MainMode implements TerminalMode {
return true;
}
// Log the unknown command for debugging purposes
// Try Wozniak monitor syntax (4000G, 100.200, E000G, etc.) via MonitorMode pattern fallthrough.
// Exclude MonitorMode's own navigation commands so they don't fire from main mode.
String trimmed = command.trim();
if (!trimmed.equalsIgnoreCase("q") && !trimmed.equalsIgnoreCase("back")) {
MonitorMode mon = getMonitorMode();
if (mon != null && mon.processCommand(trimmed)) {
return true;
}
}
LOG.info("Unknown command received: " + cmd);
// Display error message directly here - the JaceTerminal will not print its own error
return false;
}
@@ -307,8 +355,14 @@ public class MainMode implements TerminalMode {
output.println(" swlog (sl) - Toggle softswitch state change logging");
output.println(" swstate (ss) - Display current state of all softswitches");
output.println(" reset (re) - Reset the Apple II");
output.println(" step (s) [count] - Step the CPU for count cycles (default: 1)");
output.println(" tick (tc) [count] - Step the motherboard for count ticks (default: 1)");
output.println(" run (g) [count] - Run the CPU for count cycles or until breakpoint (default: 1000000)");
output.println(" go <addr> - Set PC to addr and begin execution (no mode switch needed)");
output.println(" mem <start> <end> - Hex dump memory range (no mode switch needed)");
output.println(" cpu - Show CPU registers and flags (no mode switch needed)");
output.println(" registers (reg) - Show or set CPU registers (no mode switch needed)");
output.println(" break (bp) - Manage breakpoints (no mode switch needed)");
output.println(" runto (rt) - Run until PC reaches address (no mode switch needed)");
output.println(" insertdisk (id) d# file [slot] - Insert disk image in drive # (1 or 2)");
output.println(" ejectdisk (ed) d# [slot] - Eject disk from drive # (1 or 2)");
output.println(" bootdisk (bd) d# file [slot] - Insert disk and boot until PC >= $2000");
@@ -2229,4 +2283,13 @@ public class MainMode implements TerminalMode {
// Parse as hex and mask to 16-bit address space
return Integer.parseInt(hexStr, 16) & 0xFFFF;
}
private MonitorMode getMonitorMode() {
TerminalMode mode = terminal.getModeByName("monitor");
if (mode instanceof MonitorMode) {
return (MonitorMode) mode;
}
output.println("Monitor mode not available");
return null;
}
}
+19 -9
View File
@@ -38,7 +38,7 @@ import jace.core.RAMListener;
/**
* Monitor mode for the Terminal - emulates the Apple II monitor with debugger capabilities
*/
public class MonitorMode implements TerminalMode {
public class MonitorMode implements TerminalMode, WatchCallback {
private final JaceTerminal terminal;
final PrintStream output;
private int lastExaminedAddress;
@@ -108,6 +108,11 @@ public class MonitorMode implements TerminalMode {
public MOS65C02 getCpu() {
return (MOS65C02) Emulator.withComputer(computer -> computer.getCpu(), null);
}
@Override
public PrintStream getOutput() {
return output;
}
private void initDebugger() {
debugger = new Debugger() {
@@ -161,7 +166,7 @@ public class MonitorMode implements TerminalMode {
* Displays the current CPU state in the format:
* addr: disassembled instruction [padded] A:XX X:XX Y:XX S:XX [flags]
*/
void displayCurrentInstruction() {
public void displayCurrentInstruction() {
displayCurrentInstruction(0, 0);
}
@@ -234,7 +239,7 @@ public class MonitorMode implements TerminalMode {
// Add single-letter aliases
addAlias("f", "fill");
addAlias("m", "move");
addAlias("c", "compare");
addAlias("cmp", "compare");
addAlias("dbg", "debug");
// Debugger aliases - shorter forms
@@ -247,7 +252,7 @@ public class MonitorMode implements TerminalMode {
addAlias("w", "watch");
addAlias("wl", "watchlist");
addAlias("rt", "runto");
addAlias("c", "cheat");
addAlias("ch", "cheat");
addAlias("cl", "cheatlist");
// Add the setregister alias for backward compatibility
@@ -329,6 +334,11 @@ public class MonitorMode implements TerminalMode {
}
private void addAlias(String alias, String command) {
if (commandAliases.containsKey(alias)) {
throw new IllegalStateException(
"Alias conflict in MonitorMode: '" + alias + "' already maps to '" +
commandAliases.get(alias) + "', cannot also map to '" + command + "'");
}
commandAliases.put(alias, command);
}
@@ -418,7 +428,7 @@ public class MonitorMode implements TerminalMode {
output.println("Emulation resumed");
}
private void showCpuState() {
void showCpuState() {
MOS65C02 cpu = getCpu();
if (cpu != null) {
output.println("CPU State:");
@@ -447,7 +457,7 @@ public class MonitorMode implements TerminalMode {
}
}
private void handleBreakpoint(String[] args) {
void handleBreakpoint(String[] args) {
if (args.length == 0) {
output.println("Usage: break <address> or break -<address> or break clear");
return;
@@ -992,7 +1002,7 @@ public class MonitorMode implements TerminalMode {
});
}
private void handleRunTo(String[] args) {
void handleRunTo(String[] args) {
if (args.length == 0) {
output.println("Usage: runto <address>");
return;
@@ -1329,7 +1339,7 @@ public class MonitorMode implements TerminalMode {
}
}
private void examineMemoryRange(int startAddr, int endAddr, MemoryMode mode) {
void examineMemoryRange(int startAddr, int endAddr, MemoryMode mode) {
// Calculate number of bytes to dump (inclusive)
int count = endAddr - startAddr + 1;
if (count <= 0) {
@@ -1608,7 +1618,7 @@ public class MonitorMode implements TerminalMode {
}
}
private void executeCode(int address) {
void executeCode(int address) {
// Set the program counter to the specified address using whileSuspended
Emulator.whileSuspended(computer -> {
MOS65C02 cpu = (MOS65C02) computer.getCpu();
+13 -16
View File
@@ -6,43 +6,40 @@ import jace.core.RAMListener;
/**
* Watch class to track memory changes
*
*/
public class Watch {
private final MonitorMode monitorMode;
private final WatchCallback callback;
final int address;
final String name;
private final MemoryMode mode;
private final RAMListener readListener;
private final RAMListener writeListener;
public Watch(MonitorMode monitorMode, String name, int address, MemoryMode mode) {
this.monitorMode = monitorMode;
public Watch(WatchCallback callback, String name, int address, MemoryMode mode) {
this.callback = callback;
this.name = name;
this.address = address;
this.mode = mode;
// Create a RAM listener to watch this address
Boolean auxFlag = getAuxFlag();
// Create separate listeners for reads and writes
readListener = Emulator.withMemory(ram -> {
return ram.observe("Watch-Read: " + name, RAMEvent.TYPE.READ, address, auxFlag,
return ram.observe("Watch-Read: " + name, RAMEvent.TYPE.READ, address, auxFlag,
event -> {
this.monitorMode.output.printf("Watch [%s] $%04X: READ $%02X%n",
this.callback.getOutput().printf("Watch [%s] $%04X: READ $%02X%n",
name, address, event.getNewValue() & 0xFF);
// Show current CPU state
this.monitorMode.displayCurrentInstruction();
this.callback.displayCurrentInstruction();
});
}, null);
writeListener = Emulator.withMemory(ram -> {
return ram.observe("Watch-Write: " + name, RAMEvent.TYPE.WRITE, address, auxFlag,
return ram.observe("Watch-Write: " + name, RAMEvent.TYPE.WRITE, address, auxFlag,
event -> {
this.monitorMode.output.printf("Watch [%s] $%04X: WRITE $%02X -> $%02X%n",
this.callback.getOutput().printf("Watch [%s] $%04X: WRITE $%02X -> $%02X%n",
name, address, event.getOldValue() & 0xFF, event.getNewValue() & 0xFF);
// Show current CPU state
this.monitorMode.displayCurrentInstruction();
this.callback.displayCurrentInstruction();
});
}, null);
}
@@ -0,0 +1,12 @@
package jace.terminal;
import java.io.PrintStream;
/**
* Decouples Watch from MonitorMode — Watch only needs output and instruction display,
* not the full MonitorMode class.
*/
interface WatchCallback {
PrintStream getOutput();
void displayCurrentInstruction();
}
+93 -18
View File
@@ -217,21 +217,18 @@ public class MainModeTest {
}
/**
* Test that the 'registers' command is not recognized in MainMode after being moved to MonitorMode
* Test that the 'registers' command is recognized in MainMode (delegates to MonitorMode)
*/
@Test
public void testRegistersCommand() {
LOG.fine("Starting testRegistersCommand");
// Test the registers command
// registers is now forwarded to MonitorMode; with a mock terminal that returns null
// for getModeByName, the command is recognized (returns true) but prints "Monitor mode not available"
boolean result = mainMode.processCommand("registers");
// Verify the result is false (command not recognized)
assertFalse("Registers command should not be recognized in main mode", result);
// We only verify that command is not recognized - we don't check output since
// the error message might be in logs instead of output
assertFalse("Registers command should not be recognized", result);
// Command should be recognized (registered in MainMode)
assertTrue("Registers command should be recognized in main mode", result);
}
@Test
@@ -381,25 +378,103 @@ public class MainModeTest {
public void testHelpCommands() {
// Directly test the output format without relying on command processing
outContent.reset();
// Simulate help output
System.out.println("Available commands:");
System.out.println(" monitor/m - Enter Monitor mode (includes debugger functionality)");
String helpOutput = outContent.toString();
assertTrue("Help text should include command list",
assertTrue("Help text should include command list",
helpOutput.contains("Available commands:"));
assertTrue("Help should mention monitor command",
assertTrue("Help should mention monitor command",
helpOutput.contains("monitor"));
// Simulate command-specific help
outContent.reset();
System.out.println("Enters monitor mode for memory examination, manipulation, and debugging.");
System.out.println("Usage: monitor (or m)");
String cmdHelp = outContent.toString();
assertTrue("Help for monitor should show description",
cmdHelp.contains("monitor") &&
assertTrue("Help for monitor should show description",
cmdHelp.contains("monitor") &&
cmdHelp.contains("memory examination"));
}
// -------------------------------------------------------------------------
// Wozniak monitor syntax fallthrough tests
// These verify that patterns like "4000G" and "100.200" are recognized from
// the main JACE> prompt by falling through to MonitorMode's pattern dispatcher.
//
// We use a stub MonitorMode that records which commands reached it without
// calling into the real emulator (which requires a JavaFX toolkit).
// -------------------------------------------------------------------------
/**
* A MonitorMode stub that records the last command it received and always
* returns true (recognized), without touching the real emulator.
*/
private static class RecordingMonitorMode extends MonitorMode {
String lastCommand = null;
RecordingMonitorMode(JaceTerminal terminal) {
super(terminal);
}
@Override
public boolean processCommand(String command) {
lastCommand = command;
// Delegate to the real pattern dispatcher to verify recognition,
// but short-circuit by always returning true after recording.
// We call super only for the "q"/"qq" guard test — for all other
// tests we just need to know the command arrived here.
return true;
}
}
private MainMode buildMainModeWithRecordingMonitor(RecordingMonitorMode[] holder) {
RecordingMonitorMode recording = new RecordingMonitorMode(mockTerminal);
holder[0] = recording;
when(mockTerminal.getModeByName("monitor")).thenReturn(recording);
return new TestableMainMode(mockTerminal) {
@Override
protected MOS65C02 getCPU() { return mockCpu; }
};
}
@Test
public void testWozniakGoPatternFallsThroughToMonitor() {
RecordingMonitorMode[] holder = new RecordingMonitorMode[1];
MainMode mode = buildMainModeWithRecordingMonitor(holder);
boolean result = mode.processCommand("4000G");
assertTrue("4000G should be recognized (fell through to MonitorMode)", result);
assertEquals("4000G should have reached MonitorMode unchanged", "4000G", holder[0].lastCommand);
}
@Test
public void testWozniakRangeFallsThroughToMonitor() {
RecordingMonitorMode[] holder = new RecordingMonitorMode[1];
MainMode mode = buildMainModeWithRecordingMonitor(holder);
boolean result = mode.processCommand("100.200");
assertTrue("100.200 should be recognized (fell through to MonitorMode)", result);
assertEquals("100.200 should have reached MonitorMode unchanged", "100.200", holder[0].lastCommand);
}
@Test
public void testWozniakUppercaseGoFallsThroughToMonitor() {
RecordingMonitorMode[] holder = new RecordingMonitorMode[1];
MainMode mode = buildMainModeWithRecordingMonitor(holder);
boolean result = mode.processCommand("E000G");
assertTrue("E000G should be recognized (fell through to MonitorMode)", result);
assertEquals("E000G should have reached MonitorMode unchanged", "E000G", holder[0].lastCommand);
}
@Test
public void testWozniakQGuardedFromMonitorFallthrough() {
RecordingMonitorMode[] holder = new RecordingMonitorMode[1];
MainMode mode = buildMainModeWithRecordingMonitor(holder);
// "q" is MonitorMode's "return to main" — must not be forwarded from main mode
boolean result = mode.processCommand("q");
assertFalse("q should not be recognized from main mode", result);
assertTrue("q should not have reached MonitorMode", holder[0].lastCommand == null);
}
}