Files
AI-Rendezvous/packages/core/test/core.test.ts
T
a.andreev fde1ae152f
deploy / deploy (push) Canceled after 0s
Canonical secrets policy for agents + best-effort redaction
- /security.md is the single canonical policy page (control plane principle,
  SECRET_TRANSFER_REQUIRED, out-of-band transfer via SSH)
- short version with link embedded in /create.md, room .md, llms.txt, landing,
  MCP create tool response; docs updated
- redactSecrets() applied on input to messages, questions, resolutions,
  contracts and room brief/goal/participant instructions (best-effort: PEM
  keys, JWTs, common token prefixes, password/token/secret assignments)
2026-09-06 21:49:15 +03:00

223 lines
9.6 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { RendezvousService, Store, RendezvousError, redactSecrets } from '../src/index.js';
function makeService(): { svc: RendezvousService; dbPath: string } {
const dbPath = `:memory:`;
const store = new Store(dbPath);
return { svc: new RendezvousService(store), dbPath };
}
function createTwoPartyRoom(svc: RendezvousService) {
return svc.createRoom(
{
title: '1C <-> app integration',
goal: 'Agree on exchange contract',
participants: [
{ role: 'windows-1c', knows: ['IIS', '1C'], needs_to_determine: ['frequency'] },
{ role: 'application', knows: ['app code'], needs_to_determine: ['contract'] },
],
},
'http://test.local',
);
}
test('createRoom returns invite urls and tokens for each participant', () => {
const { svc } = makeService();
const created = createTwoPartyRoom(svc);
assert.equal(created.status, 'open');
assert.equal(created.invite_urls.length, 2);
assert.match(created.invite_urls[0], /http:\/\/test\.local\/r\/.+\/.+/);
assert.notEqual(created.participants[0].token, created.participants[1].token);
assert.ok(created.participants[0].token.length >= 32, 'token must be long and random');
});
test('token of participant A cannot act as participant B context and vice versa works', () => {
const { svc } = makeService();
const created = createTwoPartyRoom(svc);
const roomId = created.room_id;
const tokenA = created.participants[0].token;
const tokenB = created.participants[1].token;
const viewA = svc.getRoomView(roomId, tokenA);
assert.equal(viewA.your_role, 'windows-1c');
const viewB = svc.getRoomView(roomId, tokenB);
assert.equal(viewB.your_role, 'application');
// wrong room id with a valid token -> forbidden/not found
assert.throws(() => svc.getRoomView('nosuchroom', tokenA), RendezvousError);
});
test('participant tokens are isolated: a token from another room is rejected', () => {
const { svc } = makeService();
const room1 = createTwoPartyRoom(svc);
const room2 = svc.createRoom(
{ title: 'other', participants: [{ role: 'x' }, { role: 'y' }] },
'http://test.local',
);
assert.throws(() => svc.getRoomView(room2.room_id, room1.participants[0].token), RendezvousError);
});
test('invalid token rejected, room id alone is not enough', () => {
const { svc } = makeService();
const created = createTwoPartyRoom(svc);
assert.throws(() => svc.authenticate(created.room_id, 'forged-token-123'), RendezvousError);
});
test('full negotiation scenario: messages -> questions -> contract -> agree -> final.md', () => {
const { svc } = makeService();
const created = createTwoPartyRoom(svc);
const roomId = created.room_id;
const [, B] = created.participants.map((p) => p.token);
// A states facts
svc.postMessage(roomId, created.participants[0].token, 'IIS has /exchange/hs endpoint, auth is basic.');
// B asks a blocking question addressed to A
const q = svc.openQuestion(roomId, B, 'What is the real exchange frequency? Check the 1C job config.', true, created.participants[0].id);
assert.equal(q.status, 'open');
assert.equal(q.blocking, true);
// A cannot finalize while blocking question open
svc.proposeContract(roomId, created.participants[0].token, '## Facts\ntodo');
const agreeWhileOpen = svc.agree(roomId, B);
assert.equal(agreeWhileOpen.room_status, 'open');
// A verifies and resolves
svc.postMessage(roomId, created.participants[0].token, 'Checked regagent job: every 15 minutes.');
svc.resolveQuestion(roomId, created.participants[0].token, q.id, 'Verified in 1C job config: every 15 min.');
// B proposes final contract, both agree -> agreed
svc.proposeContract(roomId, B, '## Facts\n- every 15 min\n## Interface\n- POST /exchange/hs');
svc.agree(roomId, B);
const final = svc.agree(roomId, created.participants[0].token);
assert.equal(final.room_status, 'agreed');
const view = svc.getRoomView(roomId, B);
assert.equal(view.room_status, 'agreed');
assert.ok(view.what_you_should_do_next.includes('final'));
// room is closed for writes
assert.throws(() => svc.postMessage(roomId, B, 'late message'), (e: RendezvousError) => e.code === 'conflict');
});
test('TTL cleanup deletes rooms, messages, tokens and artifacts', () => {
const { svc } = makeService();
const created = createTwoPartyRoom(svc);
const roomId = created.room_id;
const [, B] = created.participants.map((p) => p.token);
svc.postMessage(roomId, B, 'hello');
const q = svc.openQuestion(roomId, B, 'q?', true, created.participants[0].id);
svc.proposeContract(roomId, B, '## Facts\n- x');
// force-expire the room directly in storage
svc.store.db
.prepare('UPDATE rooms SET expires_at = ? WHERE id = ?')
.run(new Date(Date.now() - 1000).toISOString(), roomId);
const removed = svc.cleanupExpired();
assert.equal(removed, 1);
const count = (table: string) =>
(svc.store.db.prepare(`SELECT COUNT(*) AS c FROM ${table} WHERE room_id = ?`).get(roomId) as { c: number }).c;
assert.equal((svc.store.db.prepare('SELECT COUNT(*) AS c FROM rooms').get() as { c: number }).c, 0);
assert.equal(count('participants'), 0);
assert.equal(count('messages'), 0);
assert.equal(count('questions'), 0);
assert.equal(count('contract_revisions'), 0);
// everything is gone: token no longer authenticates
assert.throws(() => svc.getRoomView(roomId, B), RendezvousError);
assert.equal(svc.cleanupExpired(), 0, 'second run is a no-op');
});
test('limits: ttl > 24h rejected, oversize message rejected, 1 participant rejected', () => {
const { svc } = makeService();
assert.throws(
() => svc.createRoom({ title: 'x', ttl_hours: 25, participants: [{ role: 'a' }, { role: 'b' }] }, 'http://x'),
RendezvousError,
);
assert.throws(() => svc.createRoom({ title: 'x', participants: [{ role: 'a' }] }, 'http://x'), RendezvousError);
const created = createTwoPartyRoom(svc);
assert.throws(
() => svc.postMessage(created.room_id, created.participants[0].token, 'a'.repeat(33 * 1024)),
RendezvousError,
);
});
test('destroyRoom: allowed for participant and observer, rejected for strangers', () => {
const { svc } = makeService();
const created = createTwoPartyRoom(svc);
const roomId = created.room_id;
const tokenA = created.participants[0].token;
const observerToken = created.observer_url.split('/').pop()!;
// stranger
assert.throws(() => svc.destroyRoom(roomId, 'forged-token'), RendezvousError);
// observer may read and destroy
const view = svc.getObserverView(roomId, observerToken);
assert.ok(view.turn.length > 0);
svc.destroyRoom(roomId, observerToken);
assert.throws(() => svc.getRoomView(roomId, tokenA), RendezvousError, 'room is gone');
// participant may also destroy
const c2 = createTwoPartyRoom(svc);
svc.destroyRoom(c2.room_id, c2.participants[1].token);
assert.throws(() => svc.getRoomView(c2.room_id, c2.participants[0].token), RendezvousError);
});
test('join: creator auto-joined, invitee reports once and idempotently', () => {
const { svc } = makeService();
const created = createTwoPartyRoom(svc);
const [, tokenB] = created.participants.map((p) => p.token);
const viewA = svc.getRoomView(created.room_id, created.participants[0].token);
assert.ok(viewA.participants[0].joined_at, 'creator is joined from the start');
assert.equal(viewA.participants[1].joined_at, null);
// B's advice tells it to join first
const viewB = svc.getRoomView(created.room_id, tokenB);
assert.match(viewB.what_you_should_do_next, /join/);
const first = svc.join(created.room_id, tokenB).participants.find((p) => p.role === 'application')!;
const ts = first.joined_at!;
assert.ok(ts);
// idempotent: timestamp does not change
const again = svc.join(created.room_id, tokenB).participants.find((p) => p.role === 'application')!;
assert.equal(again.joined_at, ts);
});
test('redactSecrets strips obvious secret values (best-effort)', () => {
const input = [
'db password: hunter2secret123',
'token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N65IWDpmNfXQ',
'key sk-abcdefabcdefabcdefabcdef',
'-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAsecret\n-----END RSA PRIVATE KEY-----',
'ok: the db password lives in vault prod/db, deliver via scp',
].join('\n');
const out = redactSecrets(input);
assert.ok(!out.includes('hunter2secret123'), 'password value must be redacted');
assert.ok(!out.includes('eyJhbGciOi'), 'JWT must be redacted');
assert.ok(!out.includes('sk-abcdefabcdef'), 'prefixed token must be redacted');
assert.ok(!out.includes('MIIEpAIBAAKCAsecret'), 'private key must be redacted');
assert.ok(out.includes('[REDACTED'), 'redaction marker present');
// safe discussion is untouched
assert.ok(out.includes('lives in vault prod/db'), 'name/source discussion kept');
// and redaction is applied on the way into a room
const { svc } = makeService();
const created = createTwoPartyRoom(svc);
const msg = svc.postMessage(created.room_id, created.participants[0].token, 'api_key: supersecretvalue99');
assert.ok(!msg.content.includes('supersecretvalue99'));
assert.ok(msg.content.includes('[REDACTED'));
});
test('only addressee or author can resolve a question', () => {
const { svc } = makeService();
const created = createTwoPartyRoom(svc);
const [A, B] = created.participants.map((p) => p.token);
const q = svc.openQuestion(created.room_id, B, 'check logs', true, created.participants[0].id);
// a random third participant would be needed to test "other"; with 2
// participants the author/addressee rule reduces to both being allowed.
const resolved = svc.resolveQuestion(created.room_id, A, q.id, 'checked: ok');
assert.equal(resolved.status, 'resolved');
});