Builds the deferred integration tests from docs/backlog/feature-test-coverage- deferred.md that needed real infra (a test Postgres + real Redis) which the repo lacked. Runs against an isolated, auto-created docmost_test database and Redis logical DB 15 — never the dev data. Harness (apps/server/test/integration/, run via new `pnpm --filter server test:int` => jest --config test/jest-integration.json; default unit `jest` is untouched and excludes these via the *.int-spec.ts name + rootDir): - db.ts: buildTestDb() mirrors database.module.ts exactly (PostgresJSDialect, CamelCasePlugin, bigint to:20/from:[20,1700] parsing) + minimal seed helpers. - global-setup.ts: DROP/CREATE docmost_test, CREATE EXTENSION vector, migrate to latest via Kysely Migrator (fails loud on any errored migration). - global-teardown.ts: closes the pool. Coverage (5 suites, 16 tests, all green against live PG+Redis): - WorkspaceRepo.updateSetting: jsonb-merge persists htmlEmbed without clobbering sibling ai/sharing namespaces (the kill-switch write half). - AiAgentRoleRepo: soft-delete exclusion, cross-workspace tenant isolation, duplicate (name,workspace) -> 23505, name reusable after softDelete (partial unique index WHERE deleted_at IS NULL), same name across workspaces allowed. - page_template_references: deleting either source or referenced page cascades the link row (onDelete cascade) — real FK, not mocked. - PublicShareWorkspaceLimiter vs REAL Redis: real ioredis EVAL of the sliding- window Lua — max boundary (3 admit / 4th deny), re-admit after the window slides, same-ms distinct members. Catches Lua bugs a FakeRedis cannot. - AiChatRepo.findByCreator: role-badge join (enabled->badge; soft-deleted or disabled role -> null). Review: APPROVE; applied its two hardening suggestions (fail loud on errored migration result even without a top-level error; TEST_REDIS_URL override + ping preflight). tsc clean; unit run excludes int-spec (verified). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
97 lines
3.0 KiB
TypeScript
97 lines
3.0 KiB
TypeScript
import { Kysely } from 'kysely';
|
|
import { AiChatRepo } from '@docmost/db/repos/ai-chat/ai-chat.repo';
|
|
import { AiAgentRoleRepo } from '@docmost/db/repos/ai-agent-roles/ai-agent-roles.repo';
|
|
import {
|
|
getTestDb,
|
|
destroyTestDb,
|
|
createWorkspace,
|
|
createUser,
|
|
createRole,
|
|
createChat,
|
|
} from './db';
|
|
|
|
/**
|
|
* E (stretch) — AiChatRepo.findByCreator role-badge LEFT JOIN. The badge
|
|
* (roleName/roleEmoji) is populated ONLY when the bound role is live AND
|
|
* enabled; a soft-deleted or disabled role resolves to NULL, matching the
|
|
* stream's resolveRoleForRequest downgrade. Real SQL join, not a mock.
|
|
*/
|
|
describe('AiChatRepo.findByCreator role-badge join [integration]', () => {
|
|
let db: Kysely<any>;
|
|
let repo: AiChatRepo;
|
|
let roleRepo: AiAgentRoleRepo;
|
|
let workspaceId: string;
|
|
let creatorId: string;
|
|
|
|
beforeAll(async () => {
|
|
db = getTestDb();
|
|
repo = new AiChatRepo(db as any);
|
|
roleRepo = new AiAgentRoleRepo(db as any);
|
|
workspaceId = (await createWorkspace(db)).id;
|
|
creatorId = (await createUser(db, workspaceId)).id;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await destroyTestDb();
|
|
});
|
|
|
|
async function badgeFor(chatId: string) {
|
|
const { items } = await repo.findByCreator(creatorId, workspaceId, {
|
|
limit: 50,
|
|
} as any);
|
|
const row = items.find((c: any) => c.id === chatId);
|
|
expect(row).toBeDefined();
|
|
return { roleName: (row as any).roleName, roleEmoji: (row as any).roleEmoji };
|
|
}
|
|
|
|
it('enabled role -> roleName/roleEmoji populated', async () => {
|
|
const role = await createRole(db, {
|
|
workspaceId,
|
|
name: 'Proofreader',
|
|
emoji: '📝',
|
|
enabled: true,
|
|
});
|
|
const chat = await createChat(db, { workspaceId, creatorId, roleId: role.id });
|
|
|
|
const badge = await badgeFor(chat.id);
|
|
expect(badge.roleName).toBe('Proofreader');
|
|
expect(badge.roleEmoji).toBe('📝');
|
|
});
|
|
|
|
it('soft-deleted role -> badge NULL', async () => {
|
|
const role = await createRole(db, {
|
|
workspaceId,
|
|
name: 'Deleted Persona',
|
|
emoji: '🗑️',
|
|
enabled: true,
|
|
});
|
|
const chat = await createChat(db, { workspaceId, creatorId, roleId: role.id });
|
|
await roleRepo.softDelete(role.id, workspaceId);
|
|
|
|
const badge = await badgeFor(chat.id);
|
|
expect(badge.roleName).toBeNull();
|
|
expect(badge.roleEmoji).toBeNull();
|
|
});
|
|
|
|
it('disabled role -> badge NULL (mirrors resolveRoleForRequest downgrade)', async () => {
|
|
const role = await createRole(db, {
|
|
workspaceId,
|
|
name: 'Disabled Persona',
|
|
emoji: '🚫',
|
|
enabled: false,
|
|
});
|
|
const chat = await createChat(db, { workspaceId, creatorId, roleId: role.id });
|
|
|
|
const badge = await badgeFor(chat.id);
|
|
expect(badge.roleName).toBeNull();
|
|
expect(badge.roleEmoji).toBeNull();
|
|
});
|
|
|
|
it('chat with no role -> badge NULL', async () => {
|
|
const chat = await createChat(db, { workspaceId, creatorId, roleId: null });
|
|
const badge = await badgeFor(chat.id);
|
|
expect(badge.roleName).toBeNull();
|
|
expect(badge.roleEmoji).toBeNull();
|
|
});
|
|
});
|