Add interactive SSH portfolio features
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { validateConfig } from '../src/config.js';
|
||||
|
||||
const valid = () => ({
|
||||
theme: { primary: 'cyan', accent: 'yellow', muted: 'brightBlack' },
|
||||
host: { port: 2222, notificationPort: 7777, hostKeyPath: 'data/host.key' },
|
||||
adminPublicKeys: []
|
||||
});
|
||||
|
||||
test('accepts a valid configuration', () => assert.equal(validateConfig(valid()).host.port, 2222));
|
||||
test('rejects invalid ports with an actionable path', () => {
|
||||
const config = valid(); config.host.port = 70000;
|
||||
assert.throws(() => validateConfig(config), /host\.port/);
|
||||
});
|
||||
test('rejects unknown colors with an actionable path', () => {
|
||||
const config = valid(); config.theme.primary = 'orange';
|
||||
assert.throws(() => validateConfig(config), /theme\.primary/);
|
||||
});
|
||||
test('accepts key blobs and OpenSSH public-key lines', () => {
|
||||
const config = valid(); config.adminPublicKeys = ['QUJDRA==', 'ssh-ed25519 QUJDRA== owner'];
|
||||
assert.doesNotThrow(() => validateConfig(config));
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { safeName, validatePost } from '../src/content-policy.js';
|
||||
|
||||
test('bulletin posts normalize whitespace and reject terminal controls', () => {
|
||||
assert.deepEqual(validatePost(' hello world '), { ok: true, value: 'hello world' });
|
||||
assert.match(validatePost('hello\u001b[2J').reason, /unsupported/);
|
||||
});
|
||||
|
||||
test('bulletin posts reject emoji and excessive length', () => {
|
||||
assert.match(validatePost('hello 😀').reason, /Emoji/);
|
||||
assert.match(validatePost('x'.repeat(181)).reason, /limited/);
|
||||
});
|
||||
|
||||
test('names are reduced to safe display characters', () => assert.equal(safeName('\u001b[31m bob!!'), '31mbob'));
|
||||
@@ -0,0 +1,19 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { PortfolioDatabase } from '../src/database.js';
|
||||
|
||||
test('database rate-limits posts and stores leaderboard scores', t => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ssh-portfolio-'));
|
||||
const db = new PortfolioDatabase(path.join(directory, 'test.sqlite'));
|
||||
t.after(() => { db.close(); fs.rmSync(directory, { recursive: true, force: true }); });
|
||||
assert.equal(db.addBoardPost({ author: 'visitor', body: 'hello', ip: '127.0.0.1', now: 100000 }).ok, true);
|
||||
assert.match(db.addBoardPost({ author: 'visitor', body: 'again', ip: '127.0.0.1', now: 100001 }).reason, /wait/);
|
||||
assert.equal(db.listBoardPosts()[0].body, 'hello');
|
||||
db.addScore('flappy', 'visitor', 3);
|
||||
db.addScore('flappy', 'visitor', 7);
|
||||
assert.equal(db.getLeaderboard('flappy')[0].score, 7);
|
||||
assert.equal(db.getPlayerBest('flappy', 'visitor'), 7);
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { FlappyGame, FlappyService } from '../src/commands/flappy.js';
|
||||
import { PongGame, PongService } from '../src/commands/pong.js';
|
||||
|
||||
test('flappy engine advances deterministically', () => {
|
||||
const game = new FlappyGame(() => 0);
|
||||
game.tick(true);
|
||||
assert.equal(game.y, 4.35);
|
||||
for (let i = 0; i < 4; i++) game.tick(false);
|
||||
assert.ok(game.y > 3 && game.y < 4, 'one flap should create a modest arc');
|
||||
assert.match(game.render(), /Score: 0/);
|
||||
});
|
||||
|
||||
test('pong engine moves paddles and ball', () => {
|
||||
const game = new PongGame(false); const x = game.ballX;
|
||||
game.tick('left', 'up');
|
||||
assert.equal(game.leftY, 3);
|
||||
assert.equal(game.ballX, x - 1);
|
||||
});
|
||||
|
||||
test('pong bot can miss when its reaction is delayed', () => {
|
||||
const game = new PongGame(true, () => 0);
|
||||
game.ballY = 0; game.rightY = 6;
|
||||
game.tick('left', 'stay');
|
||||
assert.equal(game.rightY, 6);
|
||||
});
|
||||
|
||||
test('live flappy consumes Space and exits on q', () => {
|
||||
let handler; let finished = ''; let writes = 0;
|
||||
const service = new FlappyService({ addScore() {}, getLeaderboard() { return []; }, getPlayerBest() { return 0; } });
|
||||
const ctx = { username: 'visitor', channel: { destroyed: false, write() { writes++; } }, tui: {
|
||||
setInputHandler(value) { handler = value; }, finishLive(value) { finished = value; }
|
||||
} };
|
||||
assert.equal(service.start(ctx), '__LIVE__');
|
||||
assert.equal(typeof handler, 'function');
|
||||
const initialWrites = writes; handler(' ');
|
||||
assert.equal(writes, initialWrites, 'key repeat must not advance extra physics frames');
|
||||
handler('q');
|
||||
assert.match(finished, /exited/);
|
||||
});
|
||||
|
||||
test('live flappy accepts restart after game over', () => {
|
||||
let handler; let writes = 0;
|
||||
const service = new FlappyService({ addScore() {}, getLeaderboard() { return []; }, getPlayerBest() { return 0; } });
|
||||
const ctx = { username: 'visitor', channel: { destroyed: false, write() { writes++; } }, tui: {
|
||||
setInputHandler(value) { handler = value; }, finishLive() {}
|
||||
} };
|
||||
service.start(ctx);
|
||||
const before = writes;
|
||||
const state = service.live.get(ctx);
|
||||
state.game.over = true;
|
||||
handler('r');
|
||||
assert.notEqual(service.live.get(ctx).game, state.game);
|
||||
assert.ok(writes > before);
|
||||
handler('q');
|
||||
});
|
||||
|
||||
test('live pong consumes arrow keys and exits on q', () => {
|
||||
let handler; let finished = '';
|
||||
const service = new PongService({ addScore() {}, getLeaderboard() { return []; }, getPlayerBest() { return 0; } });
|
||||
const ctx = { username: 'visitor', channel: { destroyed: false, write() {} }, tui: {
|
||||
setInputHandler(value) { handler = value; }, finishLive(value) { finished = value; }
|
||||
} };
|
||||
assert.equal(service.start(ctx, 'bot'), '__LIVE__');
|
||||
handler('\x1b[A'); handler('\x1b[B'); handler('q');
|
||||
assert.match(finished, /left/);
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { pages, pageMap } from '../src/pages/index.js';
|
||||
|
||||
test('page registry resolves aliases to their canonical module', () => {
|
||||
for (const page of pages) for (const alias of page.aliases || []) assert.equal(pageMap.get(alias), page);
|
||||
});
|
||||
|
||||
test('page registry is ordered and contains unique canonical pages', () => {
|
||||
const canonical = [...new Set(pageMap.values())];
|
||||
assert.deepEqual(canonical.map(page => page.name), pages.slice().sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.name.localeCompare(b.name)).map(page => page.name));
|
||||
});
|
||||
Reference in New Issue
Block a user