sync: update from private repo (5b6df2f)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-10 08:40:41 +00:00
parent dfc5950117
commit 5502478636
43 changed files with 6452 additions and 18 deletions
+167
View File
@@ -0,0 +1,167 @@
/**
* console-protocol.ts is a type-only module describing the Console WS wire
* protocol. These tests act as a wire-format regression guard:
* - compile-time: object literals must `satisfies` each message type, so a
* field rename (e.g. acting_user_id → actingUserId) breaks this file
* - runtime: each message survives a JSON round-trip losslessly (the actual
* transport is JSON text frames, see src/bridge/console-ws-api.ts)
* - malformed input: shows how consumers must defend against junk frames
*/
import { describe, it, expect } from 'vitest';
import type {
AttachMessage,
ReplayBeginMessage,
ReplayEndMessage,
ResizeMessage,
NoticeMessage,
CloseMessage,
ServerTextMessage,
ClientTextMessage,
AnyTextMessage,
SessionCloseReason,
NoticeSeverity,
} from './console-protocol.js';
/** Minimal runtime validator mirroring what a WS consumer must do. */
function parseTextFrame(raw: string): AnyTextMessage | null {
let obj: unknown;
try {
obj = JSON.parse(raw);
} catch {
return null;
}
if (typeof obj !== 'object' || obj === null) return null;
const type = (obj as Record<string, unknown>)['type'];
if (typeof type !== 'string') return null;
const known = ['attach', 'replay_begin', 'replay_end', 'resize', 'notice', 'close'];
if (!known.includes(type)) return null;
return obj as AnyTextMessage;
}
function roundTrip<T extends AnyTextMessage>(msg: T): T {
return JSON.parse(JSON.stringify(msg)) as T;
}
describe('ssh/console-protocol wire shapes', () => {
it('attach message round-trips with snake_case wire fields intact', () => {
const msg = {
type: 'attach',
acting_user_id: 'user-42',
can_write: true,
connection_id: 'conn-7',
cols: 120,
rows: 40,
} satisfies AttachMessage;
const back = roundTrip(msg);
expect(back).toEqual(msg);
// wire field names are part of the protocol contract
expect(Object.keys(back).sort()).toEqual([
'acting_user_id',
'can_write',
'cols',
'connection_id',
'rows',
'type',
]);
});
it('replay_begin / replay_end round-trip', () => {
const begin = { type: 'replay_begin', bytes: 8192 } satisfies ReplayBeginMessage;
const end = { type: 'replay_end' } satisfies ReplayEndMessage;
expect(roundTrip(begin)).toEqual(begin);
expect(roundTrip(end)).toEqual(end);
});
it('resize message (client → server) round-trips', () => {
const msg = { type: 'resize', cols: 80, rows: 24 } satisfies ResizeMessage;
expect(roundTrip(msg)).toEqual(msg);
});
it('notice message round-trips for every severity', () => {
const severities = ['info', 'warn', 'error'] satisfies NoticeSeverity[];
for (const severity of severities) {
const msg = { type: 'notice', severity, msg: 'something happened' } satisfies NoticeMessage;
expect(roundTrip(msg)).toEqual(msg);
}
});
it('close message round-trips for every documented close reason', () => {
const reasons = [
'idle_timeout',
'duration_cap',
'host_disconnect',
'maintenance',
'admin_kill',
'connection_change',
'session_cap_evict',
'worker_shutdown',
'access_revoked',
] satisfies SessionCloseReason[];
expect(new Set(reasons).size).toBe(reasons.length);
for (const reason of reasons) {
const msg = { type: 'close', reason } satisfies CloseMessage;
expect(roundTrip(msg)).toEqual(msg);
}
});
it('discriminated union narrows on type', () => {
const msgs: ServerTextMessage[] = [
{ type: 'attach', acting_user_id: 'u', can_write: false, connection_id: 'c', cols: 1, rows: 1 },
{ type: 'replay_begin', bytes: 0 },
{ type: 'replay_end' },
{ type: 'notice', severity: 'warn', msg: 'read-only' },
{ type: 'close', reason: 'admin_kill' },
];
const seen: string[] = [];
for (const m of msgs) {
switch (m.type) {
case 'attach':
seen.push(`attach:${m.connection_id}`);
break;
case 'replay_begin':
seen.push(`replay_begin:${m.bytes}`);
break;
case 'replay_end':
seen.push('replay_end');
break;
case 'notice':
seen.push(`notice:${m.severity}`);
break;
case 'close':
seen.push(`close:${m.reason}`);
break;
default: {
// exhaustiveness guard — adding a new ServerTextMessage variant
// without handling it here is a compile error
const _never: never = m;
void _never;
}
}
}
expect(seen).toEqual([
'attach:c',
'replay_begin:0',
'replay_end',
'notice:warn',
'close:admin_kill',
]);
const client: ClientTextMessage = { type: 'resize', cols: 100, rows: 30 };
expect(client.type).toBe('resize');
});
it('malformed frames are rejected by a defensive parser', () => {
expect(parseTextFrame('not json at all')).toBeNull();
expect(parseTextFrame('')).toBeNull();
expect(parseTextFrame('null')).toBeNull();
expect(parseTextFrame('42')).toBeNull();
expect(parseTextFrame('"resize"')).toBeNull();
expect(parseTextFrame('[]')).toBeNull();
expect(parseTextFrame('{}')).toBeNull(); // missing type
expect(parseTextFrame('{"type":123}')).toBeNull(); // non-string type
expect(parseTextFrame('{"type":"selfdestruct"}')).toBeNull(); // unknown type
// valid frame passes
const ok = parseTextFrame('{"type":"resize","cols":80,"rows":24}');
expect(ok).toEqual({ type: 'resize', cols: 80, rows: 24 });
});
});
+123
View File
@@ -0,0 +1,123 @@
/**
* Startup recovery for stale (pending) SSH audit rows.
* Complements the smoke tests in audit-repo.test.ts with detail-merge,
* idempotency, and ordering coverage.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { runMigrations } from '../db/migrate.js';
import { createAuditRepo } from './audit-repo.js';
import { reconcileStaleSshAudit } from './recovery.js';
const validKey = 'a'.repeat(64);
function bootstrapDb(): Database.Database {
process.env.MCP_ENCRYPTION_KEY = validKey;
const db = new Database(':memory:');
db.pragma('foreign_keys = ON');
db.exec(`CREATE TABLE users (id TEXT PRIMARY KEY);`);
db.exec(`CREATE TABLE jobs (id TEXT PRIMARY KEY, wait_reason TEXT);`);
db.exec(`CREATE TABLE local_tasks (id INTEGER PRIMARY KEY AUTOINCREMENT);`);
runMigrations(db);
return db;
}
describe('ssh/recovery reconcileStaleSshAudit', () => {
let db: Database.Database;
beforeEach(() => {
db = bootstrapDb();
});
afterEach(() => {
db.close();
delete process.env.MCP_ENCRYPTION_KEY;
});
it('returns zero counts on an empty audit log', () => {
const result = reconcileStaleSshAudit(db);
expect(result).toEqual({ reconciledCount: 0, ids: [] });
});
it('marks every pending row aborted with the stale_reason detail', () => {
const repo = createAuditRepo(db);
const id1 = repo.begin({ action: 'ssh.exec', connectionId: 'c1' });
const id2 = repo.begin({ action: 'ssh.upload', connectionId: 'c2' });
const result = reconcileStaleSshAudit(db);
expect(result.reconciledCount).toBe(2);
expect(result.ids.sort()).toEqual([id1, id2].sort());
for (const id of [id1, id2]) {
const row = repo.getById(id);
expect(row?.outcome).toBe('aborted');
expect(row?.completedAt).not.toBeNull();
expect(row?.detail).toMatchObject({
stale_reason: 'orchestrator_restart',
});
expect(String(row?.detail?.['detail'])).toMatch(/outcome is unknown/);
}
});
it('merges stale_reason into existing detail without dropping prior keys', () => {
const repo = createAuditRepo(db);
const id = repo.begin({
action: 'ssh.exec',
connectionId: 'c1',
detail: { command: 'systemctl restart app', host: 'web-1' },
});
reconcileStaleSshAudit(db);
const row = repo.getById(id);
expect(row?.detail).toMatchObject({
command: 'systemctl restart app',
host: 'web-1',
stale_reason: 'orchestrator_restart',
});
});
it('does not touch rows that already reached a terminal outcome', () => {
const repo = createAuditRepo(db);
const doneId = repo.begin({ action: 'ssh.exec', connectionId: 'c1' });
repo.complete(doneId, 'success', { exit_code: 0 });
const failedId = repo.beginAndComplete({ action: 'ssh.exec', connectionId: 'c1' }, 'failed');
const pendingId = repo.begin({ action: 'ssh.exec', connectionId: 'c1' });
const result = reconcileStaleSshAudit(db);
expect(result.reconciledCount).toBe(1);
expect(result.ids).toEqual([pendingId]);
expect(repo.getById(doneId)?.outcome).toBe('success');
expect(repo.getById(doneId)?.detail).toEqual({ exit_code: 0 });
expect(repo.getById(failedId)?.outcome).toBe('failed');
});
it('is idempotent — a second run finds nothing to reconcile', () => {
const repo = createAuditRepo(db);
repo.begin({ action: 'ssh.exec', connectionId: 'c1' });
expect(reconcileStaleSshAudit(db).reconciledCount).toBe(1);
expect(reconcileStaleSshAudit(db)).toEqual({ reconciledCount: 0, ids: [] });
});
it('returns ids in started_at ascending order (oldest first)', () => {
const repo = createAuditRepo(db);
const newer = repo.begin({
action: 'ssh.exec',
connectionId: 'c1',
startedAt: '2026-06-10T12:00:00.000Z',
});
const oldest = repo.begin({
action: 'ssh.exec',
connectionId: 'c1',
startedAt: '2026-06-01T00:00:00.000Z',
});
const middle = repo.begin({
action: 'ssh.exec',
connectionId: 'c1',
startedAt: '2026-06-05T08:30:00.000Z',
});
const result = reconcileStaleSshAudit(db);
expect(result.ids).toEqual([oldest, middle, newer]);
});
});