feat(editor): F4.6b the command line on a computer's desktop
A new oracle, TerminalWindowRun, types into Java's own command line window: the window writes each line after its prompt, takes a blank line, cls, reset and exit itself, keeps the lines for the arrow keys, and keeps its text, history and working directory over a stop and a start. The room now holds a command line window per computer for everyone (TerminalWindow), kept over stops; the protocol carries its lines and the one way to read them (terminalScreen); the core goes on in the working directory of the run before (continueIn). The desktop opens programs in Java's frame, and the command line works with keyboard and finger: grey on black, the prompt hidden while a command runs, history, chips for the commands, Ctrl+C. The room's failure is now tested in the browser. Fixed: the room handed the engine the wall clock with fractions of a millisecond, and a ping hung after its first timeout. The engine and the room count whole milliseconds, as Java's clock does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,13 +7,16 @@
|
||||
// Time: the engine's clock follows the wall clock, a millisecond for a
|
||||
// millisecond, as Java's threads do; the speed is Java's slider, the delay of
|
||||
// a cable (RuntimeSpeed), not a time lapse — unlike Swift's tick (plan §6.3).
|
||||
// The engine counts whole milliseconds, as Java's clock does; the wall
|
||||
// clock's fractions are carried to the next tick, so that no time is lost.
|
||||
// A tick catches up at most MAXIMUM_CATCH_UP_MILLISECONDS: after a tab slept,
|
||||
// the simulation goes on from where it was rather than running the missed
|
||||
// minutes in one go.
|
||||
//
|
||||
// The engine outlives a stop: the capture stays, as Java's Lauscher does, and
|
||||
// the next start goes on at the time it stopped, so that the capture's times
|
||||
// keep rising (plan §6.2).
|
||||
// keep rising (plan §6.2). So do the command lines' windows, as Java's do
|
||||
// (TerminalWindow).
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { DEFAULT_SPEED_PERCENT, NetworkRuntimeEngine, captureRows, interfaceKey, stableGeneratedMacAddress, type CapturedFrame, type CaptureRow, type Id, type LogLine } from '@mdg-filius/core';
|
||||
@@ -30,6 +33,7 @@ import {
|
||||
type Topic,
|
||||
type TraceFrame,
|
||||
} from '@mdg-filius/protocol';
|
||||
import { TerminalWindow } from './terminalWindow.js';
|
||||
|
||||
export const TICK_MILLISECONDS = 200;
|
||||
export const MAXIMUM_CATCH_UP_MILLISECONDS = 2_000;
|
||||
@@ -71,6 +75,8 @@ export class SimulationRoom {
|
||||
/** Per card, how many frames it held at the last tick. */
|
||||
private framesSeen = new Map<Id, number>();
|
||||
private lastActivity: string[] = [];
|
||||
/** Each computer's command line window, kept over stops. */
|
||||
private readonly terminalWindows = new Map<Id, TerminalWindow>();
|
||||
|
||||
constructor(
|
||||
private readonly doc: Y.Doc,
|
||||
@@ -162,6 +168,13 @@ export class SimulationRoom {
|
||||
this.engine.setSimulationSpeed(this.speed);
|
||||
if (!this.guarded(() => this.engine.handle({ type: 'start', snapshot, seed: this.options.seed(), initialTimeMilliseconds: this.engine.state.currentTimeMilliseconds }))) return;
|
||||
this.linkOfPort = new Map(snapshot.links.flatMap((link) => [[link.sourcePortId, link.id] as const, [link.targetPortId, link.id] as const]));
|
||||
for (const node of snapshot.nodes) {
|
||||
const terminal = this.engine.terminal(node.id);
|
||||
if (terminal === undefined) continue;
|
||||
const window = this.terminalWindows.get(node.id);
|
||||
if (window === undefined) this.terminalWindows.set(node.id, new TerminalWindow(terminal));
|
||||
else window.attach(terminal);
|
||||
}
|
||||
this.mode = 'simulation';
|
||||
this.lastWall = this.options.now();
|
||||
this.broadcast(this.state());
|
||||
@@ -181,8 +194,10 @@ export class SimulationRoom {
|
||||
tick(): void {
|
||||
if (this.mode !== 'simulation') return;
|
||||
const now = this.options.now();
|
||||
const elapsed = Math.min(MAXIMUM_CATCH_UP_MILLISECONDS, Math.max(0, now - (this.lastWall ?? now)));
|
||||
this.lastWall = now;
|
||||
const behind = Math.max(0, now - (this.lastWall ?? now));
|
||||
// Whole milliseconds, as the engine counts them; the wall clock's fraction is kept for the next tick.
|
||||
const elapsed = Math.min(MAXIMUM_CATCH_UP_MILLISECONDS, Math.floor(behind));
|
||||
this.lastWall = behind > MAXIMUM_CATCH_UP_MILLISECONDS ? now : (this.lastWall ?? now) + elapsed;
|
||||
if (!this.guarded(() => this.engine.handle({ type: 'advance', toMilliseconds: this.engine.state.currentTimeMilliseconds + elapsed }))) return;
|
||||
this.flush([...this.clients.values()], false);
|
||||
}
|
||||
@@ -212,15 +227,16 @@ export class SimulationRoom {
|
||||
if (this.mode !== 'simulation') return 'notRunning';
|
||||
if (!this.engine.state.topologySnapshot.nodes.some((node) => node.id === input.node)) return 'unknownNode';
|
||||
if (input.app !== 'terminal') return 'unknownAction';
|
||||
const terminal = this.engine.terminal(input.node);
|
||||
if (terminal === undefined) return 'unknownApp';
|
||||
// A window has a program while the simulation runs (checked above), from its first start on.
|
||||
const window = this.terminalWindows.get(input.node);
|
||||
if (window === undefined || this.engine.terminal(input.node) === undefined) return 'unknownApp';
|
||||
switch (input.action) {
|
||||
case 'enter':
|
||||
if (typeof input.args.line !== 'string') return 'badArgs';
|
||||
terminal.enter(input.args.line);
|
||||
window.enter(input.args.line);
|
||||
return undefined;
|
||||
case 'interrupt':
|
||||
terminal.interrupt();
|
||||
window.interrupt();
|
||||
return undefined;
|
||||
default:
|
||||
return 'unknownAction';
|
||||
@@ -251,6 +267,7 @@ export class SimulationRoom {
|
||||
/** What the clients are owed: the cables' activity, new frames of the cards and new lines of the windows they watch. */
|
||||
private flush(clients: readonly Client[], subscribing: boolean): void {
|
||||
if (!subscribing) this.updateActivity();
|
||||
for (const window of this.terminalWindows.values()) window.pull();
|
||||
const rowsCache = new Map<Id, TraceFrame[]>();
|
||||
const traceFrames = (portId: Id, after: number): TraceFrame[] => {
|
||||
const frames = this.engine.capture.frames(portId).filter((frame) => frame.number > after);
|
||||
@@ -281,8 +298,17 @@ export class SimulationRoom {
|
||||
client.send(message({ kind: 'trace', portId, frames, reset }));
|
||||
} else {
|
||||
const [, node, app] = /^app:([^/]+)\/(.+)$/.exec(topic)!;
|
||||
const program = this.program(node!, app as AppKind);
|
||||
const sent = client.appSent.get(topic);
|
||||
if (app === 'terminal') {
|
||||
// The window's lines, kept over stops: a client goes on from those it has.
|
||||
const lines = this.terminalWindows.get(node!)?.lines ?? [];
|
||||
const from = sent ?? 0;
|
||||
if (sent !== undefined && lines.length === from) continue;
|
||||
client.appSent.set(topic, lines.length);
|
||||
client.send(message({ kind: 'app', node: node!, app: 'terminal', from, lines: lines.slice(from) }));
|
||||
continue;
|
||||
}
|
||||
const program = this.program(node!, app as AppKind);
|
||||
if (program === undefined) {
|
||||
// A window of a program that is not running: it starts empty with the next start.
|
||||
if (sent !== undefined || subscribing) client.send(message({ kind: 'app', node: node!, app: app as AppKind, from: 0, lines: [] }));
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// A computer's command line window in the room (plan §7.3): one for everyone
|
||||
// who opens it, as Java has one per computer. It writes the program's lines
|
||||
// into the window's lines after the window's own (the start screen, each
|
||||
// entered line after its prompt, a blank line, cls, exit), in the order they
|
||||
// happened. Like Java's window it outlives a stop and a start: its text, its
|
||||
// history and the working directory stay (TerminalWindowRun); each start
|
||||
// brings a new program, which goes on where the last one was.
|
||||
|
||||
import type { Terminal } from '@mdg-filius/core';
|
||||
import type { TerminalLine } from '@mdg-filius/protocol';
|
||||
|
||||
/** StringUtils.isBlank: nothing but whitespace. */
|
||||
const blank = (line: string) => line.trim() === '';
|
||||
|
||||
export class TerminalWindow {
|
||||
private readonly linesValue: TerminalLine[] = [];
|
||||
private terminal!: Terminal;
|
||||
/** How many of the program's lines are in the window. */
|
||||
private seen = 0;
|
||||
private busy = false;
|
||||
private prompt: string | undefined;
|
||||
private directory = '/';
|
||||
|
||||
/** A window on the program of the first start. */
|
||||
constructor(terminal: Terminal) {
|
||||
this.attach(terminal);
|
||||
}
|
||||
|
||||
get lines(): readonly TerminalLine[] {
|
||||
return this.linesValue;
|
||||
}
|
||||
|
||||
/** The program of a start: the first time the window shows its start screen, later it goes on in the working directory it had. */
|
||||
attach(terminal: Terminal): void {
|
||||
this.terminal = terminal;
|
||||
this.seen = 0;
|
||||
terminal.continueIn(this.directory);
|
||||
if (this.linesValue.length === 0) this.linesValue.push({ kind: 'text', text: terminal.startScreen });
|
||||
if (this.busy) {
|
||||
// A command cut off by the stop has ended.
|
||||
this.busy = false;
|
||||
this.linesValue.push({ kind: 'busy', busy: false });
|
||||
}
|
||||
this.syncPrompt();
|
||||
}
|
||||
|
||||
/** Takes the program's new lines into the window. */
|
||||
pull(): void {
|
||||
const terminal = this.terminal;
|
||||
for (const entry of terminal.log.all.slice(this.seen)) {
|
||||
if (entry.kind === 'text') this.linesValue.push({ kind: 'text', text: entry.text });
|
||||
else {
|
||||
this.busy = entry.busy;
|
||||
this.linesValue.push({ kind: 'busy', busy: entry.busy });
|
||||
}
|
||||
}
|
||||
this.seen = terminal.log.all.length;
|
||||
this.directory = terminal.directory;
|
||||
if (!this.busy) this.syncPrompt();
|
||||
}
|
||||
|
||||
/** A line entered in the window. */
|
||||
enter(line: string): void {
|
||||
const terminal = this.terminal;
|
||||
this.pull();
|
||||
if (blank(line)) {
|
||||
this.linesValue.push({ kind: 'text', text: ' \n' });
|
||||
return;
|
||||
}
|
||||
const prompt = this.prompt ?? terminal.prompt;
|
||||
const action = terminal.enter(line);
|
||||
this.linesValue.push({ kind: 'input', prompt, line, command: action === undefined });
|
||||
if (action === 'exit') this.linesValue.push({ kind: 'exit' });
|
||||
if (action === 'clear') this.linesValue.push({ kind: 'clear' }, { kind: 'text', text: terminal.startScreen });
|
||||
this.pull();
|
||||
}
|
||||
|
||||
/** Ctrl+C in the window. */
|
||||
interrupt(): void {
|
||||
this.terminal.interrupt();
|
||||
}
|
||||
|
||||
private syncPrompt(): void {
|
||||
const prompt = this.terminal.prompt;
|
||||
if (prompt === this.prompt) return;
|
||||
this.prompt = prompt;
|
||||
this.linesValue.push({ kind: 'prompt', prompt });
|
||||
}
|
||||
}
|
||||
@@ -107,8 +107,11 @@ describe('a simulation room', () => {
|
||||
expect(each.from).toBe(each.from === 0 ? 0 : lines.length);
|
||||
lines = [...lines.slice(0, each.from), ...each.lines];
|
||||
}
|
||||
expect(lines).toEqual(engine.terminal(notebook.id)!.log.all);
|
||||
expect(lines.length).toBeGreaterThan(4);
|
||||
// The window: its start screen, the line entered after its prompt, then what the program wrote, as the engine has it.
|
||||
const program = engine.terminal(notebook.id)!.log.all.map(({ timeMilliseconds: _time, ...entry }) => entry);
|
||||
expect(lines.slice(0, 3)).toEqual([{ kind: 'text', text: engine.terminal(notebook.id)!.startScreen }, { kind: 'prompt', prompt: '/> ' }, { kind: 'input', prompt: '/> ', line: 'ping 141.99.5.10', command: true }]);
|
||||
expect(lines.slice(3).filter((line) => (line as { kind: string }).kind !== 'prompt')).toEqual(program);
|
||||
expect(program.length).toBeGreaterThan(4);
|
||||
});
|
||||
|
||||
it('sends the cables that carry frames when they change, and only to who asked', () => {
|
||||
@@ -160,7 +163,7 @@ describe('a simulation room', () => {
|
||||
expect(reset.frames.length).toBeGreaterThan(8);
|
||||
});
|
||||
|
||||
it('empties a window when its program starts anew', () => {
|
||||
it("keeps a command line's window over a stop and a start, as Java does", () => {
|
||||
const { got, send, node, run } = setUp();
|
||||
const notebook = node('Notebook 1.10');
|
||||
send({ kind: 'sub', topics: [`app:${notebook.id}/terminal`] });
|
||||
@@ -168,11 +171,17 @@ describe('a simulation room', () => {
|
||||
send({ kind: 'start' });
|
||||
send({ kind: 'input', node: notebook.id, app: 'terminal', action: 'enter', args: { line: 'ipconfig' }, reqId: 'r1' });
|
||||
run(400);
|
||||
expect(ofKind(got, 'app').at(-1)!.lines.length).toBeGreaterThan(0);
|
||||
const before = ofKind(got, 'app').reduce((count, each) => each.from + each.lines.length, 0);
|
||||
expect(before).toBeGreaterThan(3);
|
||||
const sentBefore = ofKind(got, 'app').length;
|
||||
send({ kind: 'stop' });
|
||||
expect(ofKind(got, 'app').at(-1)).toMatchObject({ from: 0, lines: [] });
|
||||
send({ kind: 'start' });
|
||||
expect(ofKind(got, 'app').at(-1)).toMatchObject({ from: 0 });
|
||||
run(400);
|
||||
// Nothing taken away: the window goes on after its lines.
|
||||
expect(ofKind(got, 'app').slice(sentBefore).every((each) => each.from >= before)).toBe(true);
|
||||
send({ kind: 'input', node: notebook.id, app: 'terminal', action: 'enter', args: { line: 'pwd' }, reqId: 'r2' });
|
||||
run(400);
|
||||
expect(ofKind(got, 'app').at(-1)!.from).toBeGreaterThanOrEqual(before);
|
||||
});
|
||||
|
||||
it('answers an action that cannot be done with its reason', () => {
|
||||
@@ -212,12 +221,14 @@ describe('a simulation room', () => {
|
||||
expect(roundTrip(90)).toBeGreaterThan(roundTrip(100));
|
||||
});
|
||||
|
||||
it('catches up at most two seconds after the clock jumped', () => {
|
||||
const { got, send, jump } = setUp();
|
||||
it('catches up at most two seconds after the clock jumped, and goes on from there', () => {
|
||||
const { got, send, jump, run } = setUp();
|
||||
send({ kind: 'start' });
|
||||
jump(60_000);
|
||||
// The missed minute is not made up over the next ticks either.
|
||||
run(TICK_MILLISECONDS);
|
||||
send({ kind: 'stop' });
|
||||
expect(ofKind(got, 'state').at(-1)!.t).toBe(MAXIMUM_CATCH_UP_MILLISECONDS);
|
||||
expect(ofKind(got, 'state').at(-1)!.t).toBe(MAXIMUM_CATCH_UP_MILLISECONDS + TICK_MILLISECONDS);
|
||||
});
|
||||
|
||||
it('starts with the document as it is then', () => {
|
||||
@@ -329,5 +340,34 @@ describe('a simulation room', () => {
|
||||
send({ kind: 'sub', topics: [`node:${server.id}`] });
|
||||
expect(ofKind(got.slice(before), 'node')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("hands the engine whole milliseconds of a wall clock with fractions, losing none, and a ping goes through", () => {
|
||||
// performance.now() has fractions; the core counts whole milliseconds as Java does (a fraction once made the ping hang).
|
||||
const doc = exampleDoc();
|
||||
let now = 0.718;
|
||||
const room = new SimulationRoom(doc, { now: () => now, seed: () => SEED });
|
||||
const got: HostMessage[] = [];
|
||||
const client = room.connect((message) => got.push(message));
|
||||
const notebook = readProject(doc).graph.nodes.find((each) => each.displayName === 'Notebook 1.10')!;
|
||||
room.receive(client, { v: 1, kind: 'sub', topics: [`app:${notebook.id}/terminal`] });
|
||||
room.receive(client, { v: 1, kind: 'start' });
|
||||
for (let tick = 0; tick < 15; tick++) {
|
||||
now += 200.4;
|
||||
room.tick();
|
||||
}
|
||||
room.receive(client, { v: 1, kind: 'input', node: notebook.id, app: 'terminal', action: 'enter', args: { line: 'ping 141.99.5.10' }, reqId: 'r' });
|
||||
for (let tick = 0; tick < 60; tick++) {
|
||||
now += 200.4;
|
||||
room.tick();
|
||||
}
|
||||
room.receive(client, { v: 1, kind: 'speed', value: 90 });
|
||||
const t = ofKind(got, 'state').at(-1)!.t;
|
||||
expect(Number.isInteger(t)).toBe(true);
|
||||
// Less than a millisecond behind the wall clock since the start, never ahead of it.
|
||||
expect(t).toBeLessThanOrEqual(now - 0.718);
|
||||
expect(t).toBeGreaterThan(now - 0.718 - 1);
|
||||
const text = ofKind(got, 'app').flatMap((each) => each.lines as { kind: string; text?: string }[]).map((line) => line.text ?? '').join('');
|
||||
expect(text).toContain('4 Paket(e) gesendet, 4 Paket(e) empfangen');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"step": "open",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n",
|
||||
"prompt": "/> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "enter:",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n",
|
||||
"prompt": "/> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "enter: ",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n",
|
||||
"prompt": "/> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "enter:pwd",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n\n/> pwd\n/",
|
||||
"prompt": "/> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "enter:mkdir dir",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n\n/> pwd\n/\n/> mkdir dir\nOrdner erstellt\n",
|
||||
"prompt": "/> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "enter:cd dir",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n\n/> pwd\n/\n/> mkdir dir\nOrdner erstellt\n\n/> cd dir\n",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "enter:pwd",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n\n/> pwd\n/\n/> mkdir dir\nOrdner erstellt\n\n/> cd dir\n\n/dir> pwd\n/dir",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "enter:nosuchcommand",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n\n/> pwd\n/\n/> mkdir dir\nOrdner erstellt\n\n/> cd dir\n\n/dir> pwd\n/dir\n/dir> nosuchcommand\nUnbekannter Befehl!\nMit 'help' erhalten Sie eine Liste bekannter Befehle.",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "up",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n\n/> pwd\n/\n/> mkdir dir\nOrdner erstellt\n\n/> cd dir\n\n/dir> pwd\n/dir\n/dir> nosuchcommand\nUnbekannter Befehl!\nMit 'help' erhalten Sie eine Liste bekannter Befehle.",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": "nosuchcommand"
|
||||
},
|
||||
{
|
||||
"step": "up",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n\n/> pwd\n/\n/> mkdir dir\nOrdner erstellt\n\n/> cd dir\n\n/dir> pwd\n/dir\n/dir> nosuchcommand\nUnbekannter Befehl!\nMit 'help' erhalten Sie eine Liste bekannter Befehle.",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": "pwd"
|
||||
},
|
||||
{
|
||||
"step": "up",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n\n/> pwd\n/\n/> mkdir dir\nOrdner erstellt\n\n/> cd dir\n\n/dir> pwd\n/dir\n/dir> nosuchcommand\nUnbekannter Befehl!\nMit 'help' erhalten Sie eine Liste bekannter Befehle.",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": "cd dir"
|
||||
},
|
||||
{
|
||||
"step": "down",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n\n/> pwd\n/\n/> mkdir dir\nOrdner erstellt\n\n/> cd dir\n\n/dir> pwd\n/dir\n/dir> nosuchcommand\nUnbekannter Befehl!\nMit 'help' erhalten Sie eine Liste bekannter Befehle.",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": "pwd"
|
||||
},
|
||||
{
|
||||
"step": "down",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n\n/> pwd\n/\n/> mkdir dir\nOrdner erstellt\n\n/> cd dir\n\n/dir> pwd\n/dir\n/dir> nosuchcommand\nUnbekannter Befehl!\nMit 'help' erhalten Sie eine Liste bekannter Befehle.",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": "nosuchcommand"
|
||||
},
|
||||
{
|
||||
"step": "down",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n \n \n\n/> pwd\n/\n/> mkdir dir\nOrdner erstellt\n\n/> cd dir\n\n/dir> pwd\n/dir\n/dir> nosuchcommand\nUnbekannter Befehl!\nMit 'help' erhalten Sie eine Liste bekannter Befehle.",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "enter:cls",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "enter:ls",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n\n/dir> ls\nVerzeichnis ist leer\n",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "restart",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n\n/dir> ls\nVerzeichnis ist leer\n",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "enter:pwd",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n\n/dir> ls\nVerzeichnis ist leer\n\n/dir> pwd\n/dir",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
},
|
||||
{
|
||||
"step": "up",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n\n/dir> ls\nVerzeichnis ist leer\n\n/dir> pwd\n/dir",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": "pwd"
|
||||
},
|
||||
{
|
||||
"step": "enter:exit",
|
||||
"text": "\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n\n==========================================================================\nVerwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.\n==========================================================================\n\n/dir> ls\nVerzeichnis ist leer\n\n/dir> pwd\n/dir\n/dir> exit\n",
|
||||
"prompt": "/dir> ",
|
||||
"promptShown": true,
|
||||
"input": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// The room's command line window against Java's (tools/java-oracle
|
||||
// TerminalWindowRun): the same keys typed into a computer's window, the same
|
||||
// text, prompt and history after each, over a stop and a start. One
|
||||
// difference is named: what a run made in the files is not yet written back
|
||||
// into the document (F4.6f), so after the restart the folder made before is
|
||||
// gone and the window goes on at the root, where Java's goes on in it.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
import { addDevice, emptyProject, ROOTS, writeProject } from '@mdg-filius/doc';
|
||||
import { historyLine, historyStep, terminalScreen, type HostMessage, type TerminalLine } from '@mdg-filius/protocol';
|
||||
import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js';
|
||||
|
||||
interface JavaStep {
|
||||
readonly step: string;
|
||||
readonly text: string;
|
||||
readonly prompt: string;
|
||||
readonly promptShown: boolean;
|
||||
readonly input: string;
|
||||
}
|
||||
|
||||
const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'terminalWindow.java.json'), 'utf8')) as { readonly steps: readonly JavaStep[] };
|
||||
|
||||
/** A computer with a command line, in a room on a clock the test moves, and a client that keeps the window's lines. */
|
||||
function setUp(programs: readonly string[] = ['commandPrompt']) {
|
||||
const doc = new Y.Doc();
|
||||
writeProject(doc, emptyProject());
|
||||
let counter = 0;
|
||||
addDevice(doc, { kind: 'pc', id: 'pc', name: 'PC', x: 0, y: 0, newId: () => `port-${++counter}` }, 'me');
|
||||
const installed = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.get('programs') as Y.Map<Y.Map<unknown>>;
|
||||
for (const program of programs) {
|
||||
const entry = new Y.Map<unknown>();
|
||||
entry.set('active', true);
|
||||
installed.set(program, entry);
|
||||
}
|
||||
let now = 0;
|
||||
const room = new SimulationRoom(doc, { now: () => now, seed: () => 7 });
|
||||
const got: HostMessage[] = [];
|
||||
const client = room.connect((message) => got.push(message));
|
||||
let lines: TerminalLine[] = [];
|
||||
const read = () => {
|
||||
for (const message of got.splice(0)) {
|
||||
if (message.kind !== 'app' || message.app !== 'terminal') continue;
|
||||
expect(message.from).toBe(message.from === 0 ? 0 : lines.length);
|
||||
lines = [...lines.slice(0, message.from), ...(message.lines as TerminalLine[])];
|
||||
}
|
||||
return lines;
|
||||
};
|
||||
const send = (fields: object) => room.receive(client, { v: 1, ...fields });
|
||||
const run = (milliseconds: number) => {
|
||||
for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) {
|
||||
now += TICK_MILLISECONDS;
|
||||
room.tick();
|
||||
}
|
||||
};
|
||||
return { doc, room, send, run, read, got, connect: (listener: (message: HostMessage) => void) => room.connect(listener) };
|
||||
}
|
||||
|
||||
/** Until the files of a run are written back (F4.6f): the folder made before the restart is gone, the window goes on at the root. */
|
||||
const FILES_NOT_KEPT_YET = (text: string) => text.replaceAll('/dir', '/');
|
||||
|
||||
describe("a command line's window", () => {
|
||||
it("shows what Java's window shows, key by key, over a stop and a start", () => {
|
||||
const { send, run, read } = setUp();
|
||||
send({ kind: 'sub', topics: ['app:pc/terminal'] });
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
let pointer = -1;
|
||||
let input = '';
|
||||
let restartedAt: number | undefined;
|
||||
for (const step of java.steps) {
|
||||
if (step.step.startsWith('enter:')) {
|
||||
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: step.step.slice('enter:'.length) }, reqId: step.step });
|
||||
run(400);
|
||||
pointer = -1;
|
||||
input = '';
|
||||
} else if (step.step === 'up' || step.step === 'down') {
|
||||
const history = terminalScreen(read()).history;
|
||||
pointer = historyStep(pointer, step.step, history.length);
|
||||
input = historyLine(history, pointer);
|
||||
} else if (step.step === 'restart') {
|
||||
send({ kind: 'stop' });
|
||||
send({ kind: 'start' });
|
||||
run(400);
|
||||
restartedAt = terminalScreen(read()).text.length;
|
||||
}
|
||||
const screen = terminalScreen(read());
|
||||
const expected =
|
||||
restartedAt === undefined
|
||||
? step
|
||||
: { ...step, text: step.text.slice(0, restartedAt) + FILES_NOT_KEPT_YET(step.text.slice(restartedAt)), prompt: FILES_NOT_KEPT_YET(step.prompt) };
|
||||
expect({ text: screen.text, prompt: screen.prompt, promptShown: screen.promptShown, input }, step.step).toEqual({
|
||||
text: expected.text,
|
||||
prompt: expected.prompt,
|
||||
promptShown: expected.promptShown,
|
||||
input: expected.input,
|
||||
});
|
||||
}
|
||||
expect(read().at(-1)).toEqual({ kind: 'exit' });
|
||||
});
|
||||
|
||||
it('keeps the working directory over a stop and a start, where it is still a folder', () => {
|
||||
const { send, run, read, doc } = setUp();
|
||||
const files = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.get('files') as Y.Map<Y.Map<unknown>>;
|
||||
const folder = new Y.Map<unknown>();
|
||||
folder.set('order', files.size);
|
||||
folder.set('kind', 'directory');
|
||||
files.set('/docs', folder);
|
||||
send({ kind: 'sub', topics: ['app:pc/terminal'] });
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
// A folder the project has, so that the next start's file system has it too.
|
||||
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'cd /docs' }, reqId: 'a' });
|
||||
run(400);
|
||||
expect(terminalScreen(read()).prompt).toBe('/docs> ');
|
||||
send({ kind: 'stop' });
|
||||
send({ kind: 'start' });
|
||||
run(400);
|
||||
expect(terminalScreen(read()).prompt).toBe('/docs> ');
|
||||
// The folder gone from the project: the next start goes on at the root.
|
||||
files.delete('/docs');
|
||||
send({ kind: 'stop' });
|
||||
send({ kind: 'start' });
|
||||
run(400);
|
||||
expect(terminalScreen(read()).prompt).toBe('/> ');
|
||||
});
|
||||
|
||||
it('hides the prompt while a command runs on, and Ctrl+C ends it', () => {
|
||||
const { send, run, read } = setUp();
|
||||
send({ kind: 'sub', topics: ['app:pc/terminal'] });
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'ping 192.168.0.99' }, reqId: 'a' });
|
||||
run(1_000);
|
||||
expect(terminalScreen(read()).promptShown).toBe(false);
|
||||
expect(read().some((line) => line.kind === 'busy' && line.busy)).toBe(true);
|
||||
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'interrupt', args: {}, reqId: 'b' });
|
||||
run(15_000);
|
||||
expect(terminalScreen(read()).promptShown).toBe(true);
|
||||
expect(read().filter((line) => line.kind === 'busy').at(-1)).toEqual({ kind: 'busy', busy: false });
|
||||
});
|
||||
|
||||
it('shows the prompt again after a stop that cut a command off', () => {
|
||||
const { send, run, read } = setUp();
|
||||
send({ kind: 'sub', topics: ['app:pc/terminal'] });
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'ping 192.168.0.99' }, reqId: 'a' });
|
||||
run(1_000);
|
||||
expect(terminalScreen(read()).promptShown).toBe(false);
|
||||
send({ kind: 'stop' });
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
expect(terminalScreen(read()).promptShown).toBe(true);
|
||||
});
|
||||
|
||||
it('takes no line once its computer has no command line any more, and keeps what it showed', () => {
|
||||
const { doc, send, run, read, got } = setUp();
|
||||
send({ kind: 'sub', topics: ['app:pc/terminal'] });
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'pwd' }, reqId: 'a' });
|
||||
run(400);
|
||||
const shown = terminalScreen(read()).text;
|
||||
send({ kind: 'stop' });
|
||||
(doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.get('programs') as Y.Map<unknown>).delete('commandPrompt');
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'ls' }, reqId: 'b' });
|
||||
expect(got.filter((message) => message.kind === 'error')).toEqual([{ v: 1, kind: 'error', code: 'unknownApp', reqId: 'b' }]);
|
||||
expect(terminalScreen(read()).text).toBe(shown);
|
||||
});
|
||||
|
||||
it('is the same window for everyone who opens it, and a client that comes later gets all of it', () => {
|
||||
const { room, send, run, read, connect } = setUp();
|
||||
send({ kind: 'sub', topics: ['app:pc/terminal'] });
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'pwd' }, reqId: 'a' });
|
||||
run(400);
|
||||
const later: HostMessage[] = [];
|
||||
const other = connect((message) => later.push(message));
|
||||
room.receive(other, { v: 1, kind: 'sub', topics: ['app:pc/terminal'] });
|
||||
const first = read();
|
||||
expect(first.some((line) => line.kind === 'input' && line.line === 'pwd')).toBe(true);
|
||||
expect(later.filter((message) => message.kind === 'app')).toEqual([{ v: 1, kind: 'app', node: 'pc', app: 'terminal', from: 0, lines: first }]);
|
||||
// What the later client types, the first one sees.
|
||||
room.receive(other, { v: 1, kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'ls' }, reqId: 'b' });
|
||||
run(400);
|
||||
expect(read().some((line) => line.kind === 'input' && line.line === 'ls')).toBe(true);
|
||||
});
|
||||
|
||||
it('has no window on a computer without a command line, and takes no line while the simulation stands', () => {
|
||||
const { send, run, read, got } = setUp([]);
|
||||
send({ kind: 'sub', topics: ['app:pc/terminal'] });
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
expect(read()).toEqual([]);
|
||||
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'pwd' }, reqId: 'a' });
|
||||
expect(got.filter((message) => message.kind === 'error')).toEqual([{ v: 1, kind: 'error', code: 'unknownApp', reqId: 'a' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the window's history", () => {
|
||||
it("steps as Java's arrow keys do, never beyond its ends", () => {
|
||||
expect(historyStep(-1, 'up', 0)).toBe(-1);
|
||||
expect(historyStep(-1, 'up', 2)).toBe(0);
|
||||
expect(historyStep(1, 'up', 2)).toBe(1);
|
||||
expect(historyStep(0, 'down', 2)).toBe(-1);
|
||||
expect(historyStep(-1, 'down', 2)).toBe(-1);
|
||||
expect([historyLine(['a', 'b'], -1), historyLine(['a', 'b'], 0), historyLine(['a', 'b'], 1)]).toEqual(['', 'b', 'a']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user