feat: F4.6g4 the Gnutella window, measured against Java's

GnutellaWindowRun uses Java's Gnutella window on terminal-net beside the
line of GnutellaRun's story "search". The core cancels a search as Java
does (every connection of the program's own requests closed, passed-on
queries kept), lets the number of peers change and lists /peer2peer's
files; the room takes the window's actions and sends its view; the editor
shows Java's four tabs with Java's symbols. Not adopted from Java: a number
of peers of one digit only, a results table that keeps the last search's
rows, a files table that lags behind the folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 21:39:29 +00:00
co-authored by Claude Opus 5.5
parent 2b85e7432c
commit 5224e1a28b
24 changed files with 2211 additions and 17 deletions
+74 -9
View File
@@ -155,6 +155,28 @@ export function gnutellaPacket(text: string): GnutellaPacket | undefined {
return undefined;
}
/**
* A search result as the window's table shows it (updateErgebnisTabelle):
* the address before its first "/", the name up to the last ":" of what
* follows, the size after it (" 8 B", with Java's space).
*/
export function gnutellaResultRow(result: string): readonly [string, string, string] {
const [address = '', rest = ''] = result.split('/').filter((part) => part !== '');
const colon = rest.lastIndexOf(':');
return [address, colon < 0 ? rest : rest.slice(0, colon), colon < 0 ? '' : rest.slice(colon + 1)];
}
/**
* The number of peers typed into the window's settings, as it takes it: a
* whole number of Java's int; undefined for anything else. Java takes a
* single digit only (its pattern musterNurZahl is "\d"): 12 is refused.
*/
export function gnutellaPeerCountOfText(text: string): number | undefined {
if (!/^[0-9]+$/.test(text)) return undefined;
const count = Number(text);
return count <= 2_147_483_647 ? count : undefined;
}
/** What the program tells its window: its known peers, or its search results ("address/name: size B"), whenever they change. */
export type GnutellaLogEntry = { readonly kind: 'peers'; readonly peers: readonly string[] } | { readonly kind: 'results'; readonly results: readonly string[] };
@@ -221,7 +243,9 @@ export class GnutellaPeer {
readonly log: ProgramLog<GnutellaLogEntry>;
private peersValue: string[] = [];
private resultsValue: string[] = [];
private readonly maximumKnownPeers: number;
private maximumKnownPeersValue: number;
/** The connections of the program's own pings, queries and downloads (mitarbeiterEigeneAnfragen) while they are open. */
private readonly ownSockets = new Set<Id>();
/**
* The ids of the program's own pings and queries (eigeneAnfragen), and of
* those it sent on (schonmalVerschicktListe). Java also keeps others'
@@ -238,7 +262,7 @@ export class GnutellaPeer {
configuration: GnutellaConfiguration,
) {
this.log = new ProgramLog(engine);
this.maximumKnownPeers = configuration.maximumKnownPeers;
this.maximumKnownPeersValue = configuration.maximumKnownPeers;
this.server = new GnutellaServer(engine, nodeId, this);
}
@@ -252,6 +276,26 @@ export class GnutellaPeer {
return this.resultsValue;
}
/** How many peers the program keeps at most (getMaxTeilnehmerZahl). */
get maximumKnownPeers(): number {
return this.maximumKnownPeersValue;
}
/** setMaxTeilnehmerZahl: for the peers to come; those known stay. */
setMaximumKnownPeers(count: number): void {
this.maximumKnownPeersValue = count;
}
/**
* abbrechenSuche: every connection of the program's own requests closed —
* its searches' (hits still on their way are lost), its joins', its
* downloads' — as Java ends each worker and closes its socket.
*/
cancelSearch(): void {
// Each leaves the set when its thread ends.
for (const socketId of this.ownSockets) this.engine.requestTcpClose(socketId);
}
/** The node's first address (Betriebssystem.primaryIPAdresse). */
private get ownAddress(): string {
return this.engine.networkInterfaces(this.nodeId)[0]?.ipAddress ?? '';
@@ -313,7 +357,7 @@ export class GnutellaPeer {
/** hinzuTeilnehmer: a new peer while there is room. */
private addPeer(address: string): void {
if (this.peersValue.includes(address) || this.peersValue.length >= this.maximumKnownPeers) return;
if (this.peersValue.includes(address) || this.peersValue.length >= this.maximumKnownPeersValue) return;
this.peersValue = [...this.peersValue, address];
this.log.write({ kind: 'peers', peers: this.peersValue });
}
@@ -420,8 +464,10 @@ export class GnutellaPeer {
private *fetchFile(owner: string, name: string): Steps {
const engine = this.engine;
const socketId = engine.openTcpClientSocket(this.nodeId, owner, GNUTELLA_PORT);
if (socketId === undefined || (yield* engine.connectTcpSteps(socketId)) !== 'connected') {
if (socketId !== undefined) engine.requestTcpClose(socketId);
if (socketId === undefined) return;
this.ownSockets.add(socketId);
if ((yield* engine.connectTcpSteps(socketId)) !== 'connected') {
this.closeOwn(socketId);
return;
}
yield* engine.sendTcpSteps(socketId, new TextEncoder().encode(formatHttpRequest({ method: HTTP_GET, path: name, host: owner })));
@@ -431,7 +477,13 @@ export class GnutellaPeer {
const path = `${GNUTELLA_FOLDER}/${name}`;
this.keepFile(path, fileContentOfJavaType(response.contentType ?? '', response.body ?? '', path));
}
engine.requestTcpClose(socketId);
this.closeOwn(socketId);
}
/** A connection closed, and no longer one of the program's own (it may be so already). */
private closeOwn(socketId: Id): void {
this.engine.requestTcpClose(socketId);
this.ownSockets.delete(socketId);
}
/** speicherDatei: over a file of that name in any case, keeping its name (as the file system does). */
@@ -461,6 +513,17 @@ export class GnutellaPeer {
});
}
/** The files of /peer2peer as the window's table shows them (updateDateiTabelle): name and type. */
fileRows(): readonly (readonly [string, string])[] {
const files = this.engine.state.topologySnapshot.fileSystemsByNodeId.get(this.nodeId);
if (files === undefined || !files.contains(GNUTELLA_FOLDER)) return [];
return files.entriesInCreationOrder(GNUTELLA_FOLDER).flatMap((entry) => {
const content = entry.content;
const name = entry.path.slice(entry.path.lastIndexOf('/') + 1);
return content.kind === 'directory' ? [] : [[name, javaFileTypeName(name, content)] as const];
});
}
/** A file at the path, found in any case (Dateisystem.holeDatei); none for a folder or nothing. */
private sharedFile(path: string): { readonly path: string; readonly name: string; readonly content: Exclude<VirtualFileContent, { kind: 'directory' }> } | undefined {
const files = this.engine.state.topologySnapshot.fileSystemsByNodeId.get(this.nodeId);
@@ -480,8 +543,10 @@ export class GnutellaPeer {
private *sendTo(address: string, packet: GnutellaPacket): Steps {
const engine = this.engine;
const socketId = engine.openTcpClientSocket(this.nodeId, address, GNUTELLA_PORT);
if (socketId === undefined || (yield* engine.connectTcpSteps(socketId)) !== 'connected') {
if (socketId !== undefined) engine.requestTcpClose(socketId);
if (socketId === undefined) return;
if (this.ownRequests.has(packet.guid)) this.ownSockets.add(socketId);
if ((yield* engine.connectTcpSteps(socketId)) !== 'connected') {
this.closeOwn(socketId);
return;
}
yield* engine.sendTcpSteps(socketId, new TextEncoder().encode(gnutellaText(packet)));
@@ -492,6 +557,6 @@ export class GnutellaPeer {
if (answer?.kind === 'pong') this.takePong(answer);
else if (answer?.kind === 'queryHit') this.takeQueryHit(answer);
}
engine.requestTcpClose(socketId);
this.closeOwn(socketId);
}
}
+838
View File
@@ -0,0 +1,838 @@
{
"steps": [
{
"step": "open",
"address": {
"text": "192.168.0.1",
"red": false
},
"peers": [],
"search": "",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [],
"programResults": [],
"programFiles": [],
"dialogs": []
},
{
"step": "type an address that is none",
"address": {
"text": "192.168.0.300",
"red": true
},
"peers": [],
"search": "",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [],
"programResults": [],
"programFiles": [],
"dialogs": []
},
{
"step": "join with an address that is none",
"address": {
"text": "192.168.0.300",
"red": true
},
"peers": [],
"search": "",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [],
"programResults": [],
"programFiles": [],
"dialogs": [
"Dies ist keine gültige IP-Adresse"
]
},
{
"step": "join itself",
"address": {
"text": "192.168.0.20",
"red": false
},
"peers": [],
"search": "",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [],
"programResults": [],
"programFiles": [],
"dialogs": []
},
{
"step": "join 192.168.0.30",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [],
"programFiles": [],
"dialogs": []
},
{
"step": "search woche",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "woche",
"results": [
[
"10.0.0.2",
"Lied der Woche.txt",
" 3 B"
]
],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"10.0.0.2/Lied der Woche.txt: 3 B"
],
"programFiles": [],
"dialogs": []
},
{
"step": "download row 0",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "woche",
"results": [
[
"10.0.0.2",
"Lied der Woche.txt",
" 3 B"
]
],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"10.0.0.2/Lied der Woche.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "download row 0 again",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "woche",
"results": [
[
"10.0.0.2",
"Lied der Woche.txt",
" 3 B"
]
],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"10.0.0.2/Lied der Woche.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "search xyz",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "xyz",
"results": [
[
"10.0.0.2",
"Lied der Woche.txt",
" 3 B"
]
],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "empty the list",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "xyz",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "search notiz and abort at once",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "search notiz again",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "download with no row chosen",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "files tab",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "type a number of peers that is none",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "abc",
"red": true
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "take a number of peers that is none",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "abc",
"red": true
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": [
"Es sind nur Zahlen erlaubt!"
]
},
{
"step": "take 0 peers",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "0",
"red": false
},
"programMaximum": 0,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "take a number beyond Java's int",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "99999999999",
"red": true
},
"programMaximum": 0,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": [
"Es sind nur Zahlen erlaubt!"
]
},
{
"step": "take 12 peers",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "12",
"red": true
},
"programMaximum": 0,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": [
"Es sind nur Zahlen erlaubt!"
]
},
{
"step": "take 4 peers",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "4",
"red": false
},
"programMaximum": 4,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "leave the network",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "4",
"red": false
},
"programMaximum": 4,
"programPeers": [],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "a file added beside the window",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "4",
"red": false
},
"programMaximum": 4,
"programPeers": [],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
],
[
"neu.txt",
"txt",
"neu"
]
],
"dialogs": []
}
]
}
+251
View File
@@ -0,0 +1,251 @@
// The Gnutella program as Java's window uses it, against Java's
// (gnutellaWindow.java.json, GnutellaWindowRun) on terminal-net beside the
// line of GnutellaRun's story "search": Java's steps played on the engine's
// program — joining (not with what is no address, nor itself), searching,
// "Stopp!", downloading, emptying the list, the number of peers, leaving —
// and after each what the program keeps (its peers, results, files of
// /peer2peer, number of peers) and what the window's tables show. Where Java
// errs, what the program and window do instead (named at each step): the
// tables show what the program keeps at once, a number of peers is not one
// digit only, a hit names a text's size in characters (gnutellaJava.test.ts).
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { GNUTELLA_FOLDER, gnutellaPeerCountOfText, gnutellaResultRow, IP_ADDRESS_PATTERN, javaFileTypeName, NetworkRuntimeEngine, VirtualFileSystem, withSnapshot } from '../src/index.js';
import { snapshotOf, type Scenario } from './scenario.js';
interface JavaStep {
readonly step: string;
readonly address: { readonly text: string; readonly red: boolean };
readonly peers: readonly (readonly string[])[];
readonly results: readonly (readonly string[])[];
readonly files: readonly (readonly string[])[];
readonly maximum: { readonly text: string; readonly red: boolean };
readonly programMaximum: number;
readonly programPeers: readonly string[];
readonly programResults: readonly string[];
readonly programFiles: readonly (readonly string[])[];
readonly dialogs: readonly string[];
}
const here = dirname(fileURLToPath(import.meta.url));
const java = JSON.parse(readFileSync(join(here, 'gnutellaWindow.java.json'), 'utf8')) as { readonly steps: readonly JavaStep[] };
const scenario = JSON.parse(readFileSync(join(here, 'scenarios/terminal-net.scenario.json'), 'utf8')) as Scenario;
const CLIENT = '00000000-0000-0000-0000-000000000401';
const WEB = '00000000-0000-0000-0000-000000000403';
const ECHO = '00000000-0000-0000-0000-000000000404';
const FAR = '00000000-0000-0000-0000-000000000406';
const PEERS = [CLIENT, WEB, ECHO, FAR];
/** Each shared text's size in characters, as the program names it in a hit (Java decodes the text as Base64: "La la la" and "Notiz" are 3 B). */
const CHARACTERS: Readonly<Record<string, number>> = { 'Lied der Woche.txt': 8, 'notizen.txt': 5 };
const inCharacters = (text: string) => text.replace(/([^/]+): 3 B$/, (_, name: string) => `${name}: ${CHARACTERS[name]} B`);
function setUp() {
const files = new Map(PEERS.map((host) => [host, new VirtualFileSystem()]));
for (const host of [ECHO, FAR]) files.get(host)!.createDirectory(GNUTELLA_FOLDER);
files.get(ECHO)!.writeTextFile(`${GNUTELLA_FOLDER}/notizen.txt`, 'Notiz');
files.get(FAR)!.writeTextFile(`${GNUTELLA_FOLDER}/Lied der Woche.txt`, 'La la la');
const engine = new NetworkRuntimeEngine(scenario.seed);
engine.handle({
type: 'start',
snapshot: withSnapshot(snapshotOf(scenario), {
gnutellaByNodeId: new Map(PEERS.map((host) => [host, { maximumKnownPeers: host === CLIENT || host === FAR ? 1 : 2 }])),
fileSystemsByNodeId: files,
}),
seed: scenario.seed,
initialTimeMilliseconds: 0,
});
const run = (milliseconds: number) => engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + milliseconds });
return { engine, files, run };
}
describe("the Gnutella window, against Java's", () => {
it("plays Java's steps: the same where Java is right, as named where it errs", () => {
const { engine, files, run } = setUp();
const program = engine.gnutella(CLIENT)!;
const folder = files.get(CLIENT)!;
const kept = () => ({
peers: program.peers,
results: program.results,
maximum: program.maximumKnownPeers,
files: folder.contains(GNUTELLA_FOLDER)
? folder.entriesInCreationOrder(GNUTELLA_FOLDER).map((entry) => {
const content = entry.content;
if (content.kind !== 'text') throw new Error(`${entry.path} is no text`);
const name = entry.path.slice(GNUTELLA_FOLDER.length + 1);
return [name, javaFileTypeName(name, content), content.text];
})
: [],
});
const at = (name: string) => java.steps.find((step) => step.step === name)!;
const javaKept = (step: JavaStep) => ({ peers: step.programPeers, results: step.programResults.map(inCharacters), maximum: step.programMaximum, files: step.programFiles });
const check = (name: string) => expect(kept(), name).toEqual(javaKept(at(name)));
/** The window's tables as they show what the program keeps (Java's lag behind it, named where they do). */
const tables = () => ({
peers: program.peers.map((peer, index) => [String(index + 1), peer]),
results: program.results.map((result) => [...gnutellaResultRow(result)]),
files: kept().files.map(([name, type]) => [name, type]),
});
const javaTables = (step: JavaStep) => ({ peers: step.peers, results: step.results.map(([address, name]) => [address, name, ` ${CHARACTERS[name!]} B`]), files: step.files });
const join = (address: string) => {
if (IP_ADDRESS_PATTERN.test(address)) program.join(address);
};
// The others form their line first, as Java's did (the window opens 5.5 s in).
run(500);
engine.gnutella(WEB)!.join('192.168.0.31');
run(2500);
engine.gnutella(FAR)!.join('192.168.0.31');
run(2500);
check('open');
expect(at('open').address).toEqual({ text: '192.168.0.1', red: false });
expect(at('open').maximum).toEqual({ text: '1', red: false });
// What is no address: red, and not taken (Java's message).
expect([IP_ADDRESS_PATTERN.test('192.168.0.300'), at('type an address that is none').address.red]).toEqual([false, true]);
join('192.168.0.300');
run(1000);
check('join with an address that is none');
expect(at('join with an address that is none').dialogs).toEqual(['Dies ist keine gültige IP-Adresse']);
join('192.168.0.20');
run(2500);
check('join itself');
join('192.168.0.30');
run(3000);
check('join 192.168.0.30');
expect(tables(), 'join 192.168.0.30').toEqual(javaTables(at('join 192.168.0.30')));
program.search('woche');
run(3000);
check('search woche');
expect(tables()).toEqual(javaTables(at('search woche')));
program.download(0);
run(3000);
check('download row 0');
// The file's table shows it at once (Java's only when the program next tells the window of a change).
expect(at('download row 0').files).toEqual([]);
expect(tables().files).toEqual([['Lied der Woche.txt', 'txt']]);
program.download(0);
run(3000);
check('download row 0 again');
program.search('xyz');
run(3000);
check('search xyz');
// The table shows no result, as the program keeps none (Java's still shows the last search's).
expect([at('search xyz').results.length, tables().results]).toEqual([1, []]);
program.clearResults();
run(500);
check('empty the list');
// "Stopp!" half a second after "Suchen" (as Java's press came): the hits on their way are lost; a new search finds them.
program.search('notiz');
run(500);
program.cancelSearch();
run(3000);
check('search notiz and abort at once');
program.search('notiz');
run(3000);
check('search notiz again');
expect(tables()).toEqual(javaTables(at('search notiz again')));
run(2000);
check('download with no row chosen');
check('files tab');
// The number of peers: what is no number refused (Java's message), 0 taken; 12 taken too (Java takes one digit only).
expect(['abc', '0', '99999999999', '12', '4', '-1', '', '2147483647', '2147483648'].map(gnutellaPeerCountOfText)).toEqual([
undefined,
0,
undefined,
12,
4,
undefined,
undefined,
2_147_483_647,
undefined,
]);
expect([at('take a number of peers that is none').dialogs, at('take 12 peers').dialogs]).toEqual([['Es sind nur Zahlen erlaubt!'], ['Es sind nur Zahlen erlaubt!']]);
program.setMaximumKnownPeers(0);
check('take 0 peers');
check("take a number beyond Java's int");
program.setMaximumKnownPeers(12);
expect(program.maximumKnownPeers).toBe(12);
program.setMaximumKnownPeers(4);
check('take 4 peers');
program.reset();
run(1000);
check('leave the network');
expect(tables().peers).toEqual([]);
// A file added beside the window: its table shows it at once (Java's not).
folder.writeTextFile(`${GNUTELLA_FOLDER}/neu.txt`, 'neu');
check('a file added beside the window');
expect([at('a file added beside the window').files.length, tables().files.length]).toEqual([1, 2]);
});
});
describe('the Gnutella program beyond the steps', () => {
it('splits a result as Java’s table does', () => {
expect(['10.0.0.2/Lied der Woche.txt: 8 B', '10.0.0.2/a:b.txt: 3 B', '//x/y', 'kein'].map(gnutellaResultRow)).toEqual([
['10.0.0.2', 'Lied der Woche.txt', ' 8 B'],
['10.0.0.2', 'a:b.txt', ' 3 B'],
['x', 'y', ''],
['kein', '', ''],
]);
});
it('keeps the peers it knows when the number shrinks, and takes no more', () => {
const { engine, run } = setUp();
const program = engine.gnutella(WEB)!;
program.setMaximumKnownPeers(1);
program.join('192.168.0.31');
run(3000);
engine.gnutella(CLIENT)!.join('192.168.0.30');
run(3000);
expect(program.peers).toEqual(['192.168.0.31']);
program.setMaximumKnownPeers(0);
expect(program.peers).toEqual(['192.168.0.31']);
});
it("cancels only the program's own requests: a query it passes on still brings its hits back", () => {
const { engine, run } = setUp();
engine.gnutella(WEB)!.join('192.168.0.31');
run(3000);
engine.gnutella(CLIENT)!.join('192.168.0.30');
run(3000);
engine.gnutella(CLIENT)!.search('notiz');
// .30 has the query (after about 330 ms) and has sent it on to .31 (about 390 ms) when it cancels its own requests
// (its join's connection): the hits on their way back through it still come.
run(450);
engine.gnutella(WEB)!.cancelSearch();
run(3000);
expect(engine.gnutella(CLIENT)!.results).toEqual(['192.168.0.31/notizen.txt: 5 B']);
});
it('shows the files of /peer2peer, not its folders, as Java lists them', () => {
const { engine, files } = setUp();
files.get(ECHO)!.createDirectory(`${GNUTELLA_FOLDER}/ordner`);
expect(engine.gnutella(ECHO)!.fileRows()).toEqual([['notizen.txt', 'txt']]);
expect(engine.gnutella(CLIENT)!.fileRows()).toEqual([]);
});
it('cancels the downloads too, and a cancel without requests changes nothing', () => {
const { engine, files, run } = setUp();
const program = engine.gnutella(CLIENT)!;
program.cancelSearch();
program.join('192.168.0.31');
run(3000);
program.search('notiz');
run(3000);
expect(program.results).toEqual(['192.168.0.31/notizen.txt: 5 B']);
program.download(0);
program.cancelSearch();
run(3000);
expect(files.get(CLIENT)!.contains(`${GNUTELLA_FOLDER}/notizen.txt`)).toBe(false);
program.download(0);
run(3000);
expect(files.get(CLIENT)!.textFile(`${GNUTELLA_FOLDER}/notizen.txt`)).toBe('Notiz');
});
});