test: cover features since 053a9c0d + repair test tooling

Add ~330 tests across server (Jest), client (Vitest), editor-ext (Vitest)
and packages/mcp (node:test) for the gitmost features added since
053a9c0d: AI chat, AI agent roles, public-share assistant, MCP per-user
auth, HTML embed, page templates/embed, realtime tree, tree
expand/collapse, and the AI-settings UI.

Test-tooling fixes (prerequisite, were silently hiding coverage):
- Repair 3 page-template specs broken by the 11-arg TransclusionService
  constructor; they never compiled, so template access-control / content
  -leak / unsync-strip coverage was fictitious.
- Build @docmost/editor-ext before server tests via a `pretest` hook;
  the stale dist omitted the new HtmlEmbed/PageEmbed exports (TS2305).
- Let jest resolve the .tsx email templates: add `tsx` to
  moduleFileExtensions and widen the ts-jest transform to (t|j)sx?.

Behaviour-preserving "extract pure core" refactors that the tests drive:
- server: resolveShareAssistantRequest + uiMessageTextLength
  (public-share controller), decideBasicGate + mapAuthResultToResponse
  (mcp), buildErrorAssistantRecord (ai-chat), jsonbObject export (roles).
- client: render-raw-html + shouldExecute/canEdit, decide-embed-state,
  page-embed picker utils, tree-socket reducers, open/close branch maps,
  isEndpointConfigured/resolveKeyField; buildTreeWithChildren now treats
  a permission-trimmed orphan as a root instead of crashing.

Deferred (need a test DB or HTTP harness, documented in the specs):
repo-level Postgres integration tests and the public-share XFF E2E.
Pre-existing DI/lib0-ESM suite failures are untouched and out of scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-20 23:40:40 +03:00
parent 692c0abe13
commit 90d3fab483
56 changed files with 5668 additions and 447 deletions

View File

@@ -0,0 +1,111 @@
import { WorkspaceService } from './workspace.service';
/**
* Exercises the REAL WorkspaceService.update htmlEmbed-toggle persistence at the
* service seam: an update carrying `htmlEmbed` must call
* `workspaceRepo.updateSetting(workspaceId, 'htmlEmbed', value, trx)`, and an
* update WITHOUT it must not touch that setting. The repo, db transaction, and
* audit service are mocked; `executeTx` runs the callback against a fake trx.
*
* DEFERRED (DB-only): the "does not clobber sibling settings" guarantee is a
* jsonb merge property of `updateSetting`'s SQL and needs a real Postgres to
* assert. This spec only asserts the service-level CALL SHAPE.
*/
describe('WorkspaceService.update — htmlEmbed toggle persistence (real code)', () => {
function buildService(opts: { settingsBefore?: Record<string, any> }) {
const updateSetting = jest.fn().mockResolvedValue(undefined);
const updateWorkspace = jest.fn().mockResolvedValue(undefined);
const workspaceRepo = {
// First call: read settingsBefore. Second call: return the updated
// workspace (must include a licenseKey because update() destructures it).
findById: jest
.fn()
.mockResolvedValueOnce({ id: 'w1', settings: opts.settingsBefore ?? {} })
.mockResolvedValueOnce({ id: 'w1', name: 'WS', licenseKey: null }),
updateSetting,
updateWorkspace,
};
// Fake kysely db: only .transaction().execute(cb) is used on this path.
const db = {
transaction: jest.fn(() => ({
execute: jest.fn(async (cb: any) => cb({ __trx: true })),
})),
};
const auditService = { log: jest.fn() };
const service = new WorkspaceService(
workspaceRepo as any, // workspaceRepo
{} as any, // spaceService
{} as any, // spaceMemberService
{} as any, // groupRepo
{} as any, // groupUserRepo
{} as any, // userRepo
{} as any, // environmentService
{} as any, // domainService
{} as any, // licenseCheckService
{} as any, // shareRepo
{} as any, // watcherRepo
{} as any, // favoriteRepo
db as any, // db (InjectKysely)
{} as any, // attachmentQueue
{} as any, // billingQueue
{} as any, // aiQueue
auditService as any, // auditService
{} as any, // userSessionRepo
);
return { service, workspaceRepo, updateSetting, auditService };
}
it('persists htmlEmbed:true via updateSetting with the htmlEmbed key', async () => {
const { service, updateSetting } = buildService({});
await service.update('w1', { htmlEmbed: true } as any);
expect(updateSetting).toHaveBeenCalledTimes(1);
expect(updateSetting).toHaveBeenCalledWith(
'w1',
'htmlEmbed',
true,
expect.anything(), // the transaction handle
);
});
it('persists htmlEmbed:false (explicit disable is not dropped)', async () => {
const { service, updateSetting } = buildService({
settingsBefore: { htmlEmbed: true },
});
await service.update('w1', { htmlEmbed: false } as any);
expect(updateSetting).toHaveBeenCalledWith(
'w1',
'htmlEmbed',
false,
expect.anything(),
);
});
it('does NOT call updateSetting when htmlEmbed is undefined in the dto', async () => {
const { service, updateSetting } = buildService({});
await service.update('w1', { name: 'New name' } as any);
expect(updateSetting).not.toHaveBeenCalled();
});
it('audits the htmlEmbed change (before/after) when the value actually changes', async () => {
const { service, auditService } = buildService({
settingsBefore: { htmlEmbed: false },
});
await service.update('w1', { htmlEmbed: true } as any);
expect(auditService.log).toHaveBeenCalledTimes(1);
const logged = auditService.log.mock.calls[0][0];
expect(logged.changes.before.htmlEmbed).toBe(false);
expect(logged.changes.after.htmlEmbed).toBe(true);
});
});