Reviewers were asked to include or exclude learning trajectories while only seeing metadata — credit class, confidence, status. The decisions and their redacted context turns were already captured and stored, but no endpoint returned them, so the evidence was unreachable from the UI. Add GET /api/learning/trajectories/:id and an expandable row on the learning page that fetches on demand. Also widen SentMessageSchema to accept matrixEventId, imageOutcomeCredit and imageOutcomeAt. The learning loop added these to SentMessageRecord and they flow out of /api/messages, but the strict contract rejected them, breaking the dashboard. Enable generation with shadow mode off so eligible examples reach reply prompts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
105 lines
3.5 KiB
TypeScript
105 lines
3.5 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { RedditAccountsRepository } from '../src/database/account-repositories.js';
|
|
import { runMigrations } from '../src/database/client.js';
|
|
import { AccountAuthContext } from '../src/services/account-auth-context.js';
|
|
import { RedditAccountsService } from '../src/services/reddit-accounts.js';
|
|
|
|
const silentLogger = {
|
|
info: () => undefined,
|
|
warn: () => undefined,
|
|
error: () => undefined,
|
|
debug: () => undefined,
|
|
} as never;
|
|
|
|
describe('removing a Reddit account', () => {
|
|
let db: Database.Database;
|
|
let accounts: RedditAccountsRepository;
|
|
let auth: AccountAuthContext;
|
|
let stopped: string[];
|
|
let service: RedditAccountsService;
|
|
|
|
beforeEach(() => {
|
|
db = new Database(':memory:');
|
|
db.pragma('foreign_keys = ON');
|
|
runMigrations(db);
|
|
accounts = new RedditAccountsRepository(db);
|
|
auth = new AccountAuthContext(silentLogger);
|
|
stopped = [];
|
|
const runtimes = {
|
|
get: () => null,
|
|
stop: async (accountId: string) => { stopped.push(accountId); },
|
|
};
|
|
service = new RedditAccountsService(
|
|
accounts,
|
|
{ import: async () => { throw new Error('not used'); } } as never,
|
|
auth,
|
|
runtimes as never,
|
|
);
|
|
accounts.upsertVerified({
|
|
accountId: 't2_alice',
|
|
redditUsername: 'alice-bot',
|
|
matrixUserId: '@t2_alice:reddit.com',
|
|
cookiesJson: JSON.stringify([{ name: 'reddit_session', value: 'secret' }]),
|
|
matrixCredentialsJson: JSON.stringify({ accessToken: 'matrix-secret' }),
|
|
now: 100,
|
|
});
|
|
});
|
|
|
|
afterEach(() => db.close());
|
|
|
|
it('stops the runtime before dropping the account and its stored credentials', async () => {
|
|
const removed = await service.remove('t2_alice');
|
|
|
|
expect(stopped).toEqual(['t2_alice']);
|
|
expect(removed).toMatchObject({
|
|
accountId: 't2_alice',
|
|
redditUsername: 'alice-bot',
|
|
isRunning: false,
|
|
status: 'login-required',
|
|
});
|
|
expect(accounts.get('t2_alice')).toBeNull();
|
|
expect(service.list()).toEqual([]);
|
|
});
|
|
|
|
it('drops the authenticated session when the removed account held it', async () => {
|
|
auth.markAuthenticated({
|
|
accountId: 't2_alice',
|
|
redditUsername: 'alice-bot',
|
|
matrixUserId: '@t2_alice:reddit.com',
|
|
});
|
|
expect(auth.getSnapshot().accountId).toBe('t2_alice');
|
|
|
|
await service.remove('t2_alice');
|
|
|
|
expect(auth.getSnapshot()).toMatchObject({ status: 'login-required', accountId: null });
|
|
});
|
|
|
|
it('leaves another account\'s session alone', async () => {
|
|
accounts.upsertVerified({
|
|
accountId: 't2_bob',
|
|
redditUsername: 'bob-bot',
|
|
matrixUserId: '@t2_bob:reddit.com',
|
|
cookiesJson: JSON.stringify([{ name: 'reddit_session', value: 'bob-secret' }]),
|
|
matrixCredentialsJson: JSON.stringify({ accessToken: 'bob-matrix-secret' }),
|
|
now: 110,
|
|
});
|
|
auth.markAuthenticated({
|
|
accountId: 't2_bob',
|
|
redditUsername: 'bob-bot',
|
|
matrixUserId: '@t2_bob:reddit.com',
|
|
});
|
|
|
|
await service.remove('t2_alice');
|
|
|
|
expect(auth.getSnapshot()).toMatchObject({ status: 'authenticated', accountId: 't2_bob' });
|
|
expect(service.list().map((account) => account.accountId)).toEqual(['t2_bob']);
|
|
});
|
|
|
|
it('refuses to remove an account it does not know, without stopping anything', async () => {
|
|
await expect(service.remove('t2_missing')).rejects.toThrow('Reddit account not found');
|
|
expect(stopped).toEqual([]);
|
|
expect(accounts.get('t2_alice')).not.toBeNull();
|
|
});
|
|
});
|