mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
1099 lines
42 KiB
TypeScript
1099 lines
42 KiB
TypeScript
// @vitest-environment node
|
|
import { and, eq } from 'drizzle-orm';
|
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
import { getTestDB } from '../../../core/getTestDB';
|
|
import { agentDocuments, agents, documents, users } from '../../../schemas';
|
|
import {
|
|
AGENT_SKILL_TEMPLATE_ID,
|
|
DOCUMENT_FOLDER_TYPE,
|
|
SKILL_BUNDLE_FILE_TYPE,
|
|
SKILL_INDEX_FILE_TYPE,
|
|
} from '../../../schemas/file';
|
|
import type { LobeChatDatabase } from '../../../type';
|
|
import {
|
|
AgentDocumentModel,
|
|
DocumentLoadFormat,
|
|
DocumentLoadPosition,
|
|
DocumentLoadRule,
|
|
PolicyLoad,
|
|
} from '../agentDocument';
|
|
|
|
const userId = 'agent-document-test-user';
|
|
const otherUserId = 'other-agent-document-test-user';
|
|
|
|
const agentId = 'agent-document-test-agent';
|
|
const secondAgentId = 'agent-document-test-agent-2';
|
|
const otherAgentId = 'other-agent-document-test-agent';
|
|
|
|
let agentDocumentModel: AgentDocumentModel;
|
|
let otherAgentDocumentModel: AgentDocumentModel;
|
|
const serverDB: LobeChatDatabase = await getTestDB();
|
|
|
|
beforeEach(async () => {
|
|
await serverDB.delete(users);
|
|
|
|
await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
|
|
await serverDB.insert(agents).values([
|
|
{ id: agentId, userId },
|
|
{ id: secondAgentId, userId },
|
|
{ id: otherAgentId, userId: otherUserId },
|
|
]);
|
|
|
|
agentDocumentModel = new AgentDocumentModel(serverDB, userId);
|
|
otherAgentDocumentModel = new AgentDocumentModel(serverDB, otherUserId);
|
|
});
|
|
|
|
describe('AgentDocumentModel', () => {
|
|
describe('associate', () => {
|
|
it('should link an existing document to an agent and return the new id', async () => {
|
|
// Create a document in the documents table directly
|
|
const [doc] = await serverDB
|
|
.insert(documents)
|
|
.values({
|
|
content: 'crawled content',
|
|
fileType: 'article',
|
|
filename: 'page.html',
|
|
source: 'https://example.com',
|
|
sourceType: 'web',
|
|
title: 'Example Page',
|
|
totalCharCount: 15,
|
|
totalLineCount: 1,
|
|
userId,
|
|
})
|
|
.returning();
|
|
|
|
const result = await agentDocumentModel.associate({ agentId, documentId: doc!.id });
|
|
|
|
expect(result.id).toBeDefined();
|
|
expect(result.id).not.toBe('');
|
|
|
|
// Verify the agentDocuments row was created
|
|
const [row] = await serverDB
|
|
.select()
|
|
.from(agentDocuments)
|
|
.where(eq(agentDocuments.id, result.id));
|
|
|
|
expect(row).toBeDefined();
|
|
expect(row?.agentId).toBe(agentId);
|
|
expect(row?.documentId).toBe(doc!.id);
|
|
expect(row?.userId).toBe(userId);
|
|
expect(row?.policyLoad).toBe(PolicyLoad.PROGRESSIVE);
|
|
});
|
|
|
|
it('should be idempotent (onConflictDoNothing)', async () => {
|
|
const [doc] = await serverDB
|
|
.insert(documents)
|
|
.values({
|
|
content: 'content',
|
|
fileType: 'article',
|
|
filename: 'dup.html',
|
|
source: 'https://example.com/dup',
|
|
sourceType: 'web',
|
|
title: 'Dup Page',
|
|
totalCharCount: 7,
|
|
totalLineCount: 1,
|
|
userId,
|
|
})
|
|
.returning();
|
|
|
|
const first = await agentDocumentModel.associate({ agentId, documentId: doc!.id });
|
|
const second = await agentDocumentModel.associate({ agentId, documentId: doc!.id });
|
|
|
|
expect(first.id).toBeDefined();
|
|
// Second call should not throw, id may be undefined due to onConflictDoNothing
|
|
expect(second).toBeDefined();
|
|
});
|
|
|
|
it('should not create documents row — only the link', async () => {
|
|
const [doc] = await serverDB
|
|
.insert(documents)
|
|
.values({
|
|
content: 'existing',
|
|
fileType: 'article',
|
|
filename: 'existing.html',
|
|
source: 'https://example.com/existing',
|
|
sourceType: 'web',
|
|
title: 'Existing',
|
|
totalCharCount: 8,
|
|
totalLineCount: 1,
|
|
userId,
|
|
})
|
|
.returning();
|
|
|
|
const countBefore = await serverDB
|
|
.select()
|
|
.from(documents)
|
|
.where(eq(documents.userId, userId));
|
|
await agentDocumentModel.associate({ agentId, documentId: doc!.id });
|
|
const countAfter = await serverDB
|
|
.select()
|
|
.from(documents)
|
|
.where(eq(documents.userId, userId));
|
|
|
|
expect(countAfter.length).toBe(countBefore.length);
|
|
});
|
|
|
|
it('should allow associating documents when a live sibling already owns the same filename', async () => {
|
|
const existing = await agentDocumentModel.create(agentId, 'associated.md', 'managed');
|
|
const [doc] = await serverDB
|
|
.insert(documents)
|
|
.values({
|
|
content: 'existing',
|
|
fileType: 'article',
|
|
filename: 'associated.md',
|
|
source: 'https://example.com/associated',
|
|
sourceType: 'web',
|
|
title: 'Associated',
|
|
totalCharCount: 8,
|
|
totalLineCount: 1,
|
|
userId,
|
|
})
|
|
.returning();
|
|
|
|
const associated = await agentDocumentModel.associate({ agentId, documentId: doc!.id });
|
|
const matched = await agentDocumentModel.listByParentAndFilename(
|
|
agentId,
|
|
null,
|
|
'associated.md',
|
|
);
|
|
|
|
expect(associated.id).toBeDefined();
|
|
expect(matched.map((item) => item.documentId)).toEqual([existing.documentId, doc!.id]);
|
|
});
|
|
});
|
|
|
|
describe('create', () => {
|
|
it('creates ordinary agent documents with agent source attribution by default', async () => {
|
|
const created = await agentDocumentModel.create(agentId, 'brief', 'content');
|
|
|
|
expect(created.sourceType).toBe('agent');
|
|
expect(created.source).toBe(`agent-document://${agentId}/brief`);
|
|
});
|
|
|
|
it('allows trusted callers to set document source attribution', async () => {
|
|
const created = await agentDocumentModel.create(agentId, 'skill-a', 'content', {
|
|
source: 'agent-signal:skill-management',
|
|
sourceType: 'agent-signal',
|
|
});
|
|
|
|
expect(created.sourceType).toBe('agent-signal');
|
|
expect(created.source).toBe('agent-signal:skill-management');
|
|
});
|
|
|
|
/**
|
|
* @example
|
|
* Higher-level services can compose multiple agent document writes in one transaction.
|
|
*/
|
|
it('rolls back createWithTx when the caller transaction fails', async () => {
|
|
let createdAgentDocumentId: string | undefined;
|
|
let createdDocumentId: string | undefined;
|
|
|
|
await expect(
|
|
serverDB.transaction(async (trx) => {
|
|
const created = await agentDocumentModel.createWithTx(
|
|
trx,
|
|
agentId,
|
|
'rollback-note',
|
|
'content',
|
|
);
|
|
createdAgentDocumentId = created.id;
|
|
createdDocumentId = created.documentId;
|
|
|
|
throw new Error('Intentional rollback');
|
|
}),
|
|
).rejects.toThrow('Intentional rollback');
|
|
|
|
if (createdAgentDocumentId) {
|
|
const [binding] = await serverDB
|
|
.select()
|
|
.from(agentDocuments)
|
|
.where(eq(agentDocuments.id, createdAgentDocumentId));
|
|
|
|
expect(binding).toBeUndefined();
|
|
}
|
|
|
|
if (createdDocumentId) {
|
|
const [doc] = await serverDB
|
|
.select()
|
|
.from(documents)
|
|
.where(eq(documents.id, createdDocumentId));
|
|
|
|
expect(doc).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('should create an agent document with normalized policy and linked document row', async () => {
|
|
const result = await agentDocumentModel.create(agentId, 'identity.md', 'line1\nline2', {
|
|
loadPosition: DocumentLoadPosition.BEFORE_SYSTEM,
|
|
loadRules: { maxTokens: 1024, priority: 2, rule: DocumentLoadRule.ALWAYS },
|
|
metadata: { description: 'Identity policy', domain: 'ops' },
|
|
templateId: 'claw',
|
|
});
|
|
|
|
expect(result.agentId).toBe(agentId);
|
|
expect(result.filename).toBe('identity.md');
|
|
expect(result.title).toBe('identity');
|
|
expect(result.content).toBe('line1\nline2');
|
|
expect(result.policy?.context?.position).toBe(DocumentLoadPosition.BEFORE_SYSTEM);
|
|
expect(result.policy?.context?.maxTokens).toBe(1024);
|
|
expect(result.policy?.context?.priority).toBe(2);
|
|
expect(result.policyLoadFormat).toBe(DocumentLoadFormat.RAW);
|
|
expect(result.policyLoadRule).toBe(DocumentLoadRule.ALWAYS);
|
|
|
|
const [doc] = await serverDB
|
|
.select()
|
|
.from(documents)
|
|
.where(eq(documents.id, result.documentId));
|
|
|
|
expect(doc).toBeDefined();
|
|
expect(doc?.title).toBe('identity');
|
|
expect(doc?.description).toBe('Identity policy');
|
|
expect(doc?.source).toBe(`agent-document://${agentId}/${encodeURIComponent('identity.md')}`);
|
|
expect(doc?.totalCharCount).toBe('line1\nline2'.length);
|
|
expect(doc?.totalLineCount).toBe(2);
|
|
});
|
|
|
|
it('should use default policy values when optional args are omitted', async () => {
|
|
const result = await agentDocumentModel.create(agentId, 'quick-note.txt', 'hello');
|
|
|
|
expect(result.policy?.context?.position).toBe(DocumentLoadPosition.BEFORE_FIRST_USER);
|
|
expect(result.policy?.context?.rule).toBe(DocumentLoadRule.ALWAYS);
|
|
expect(result.policyLoadFormat).toBe(DocumentLoadFormat.RAW);
|
|
expect(result.policyLoad).toBe(PolicyLoad.PROGRESSIVE);
|
|
expect(result.accessShared).toBe(0);
|
|
expect(result.accessPublic).toBe(0);
|
|
});
|
|
|
|
it('should allow duplicate live sibling filenames at the database boundary', async () => {
|
|
const first = await agentDocumentModel.create(agentId, 'duplicate.md', 'first', {
|
|
createdAt: new Date('2024-01-01T00:00:00.000Z'),
|
|
});
|
|
const second = await agentDocumentModel.create(agentId, 'duplicate.md', 'second', {
|
|
createdAt: new Date('2024-01-02T00:00:00.000Z'),
|
|
});
|
|
|
|
const matched = await agentDocumentModel.listByParentAndFilename(
|
|
agentId,
|
|
null,
|
|
'duplicate.md',
|
|
);
|
|
|
|
expect(second.documentId).not.toBe(first.documentId);
|
|
expect(matched.map((item) => item.documentId)).toEqual([first.documentId, second.documentId]);
|
|
});
|
|
|
|
it('should allow different agents to use the same root filename', async () => {
|
|
const first = await agentDocumentModel.create(agentId, 'shared.md', 'first');
|
|
const second = await agentDocumentModel.create(secondAgentId, 'shared.md', 'second');
|
|
|
|
expect(first.agentId).toBe(agentId);
|
|
expect(second.agentId).toBe(secondAgentId);
|
|
expect(first.documentId).not.toBe(second.documentId);
|
|
});
|
|
|
|
it('should allow recreating a filename after the previous sibling is soft deleted', async () => {
|
|
const first = await agentDocumentModel.create(agentId, 'recreated.md', 'first');
|
|
|
|
await agentDocumentModel.delete(first.id, 'replace');
|
|
|
|
const second = await agentDocumentModel.create(agentId, 'recreated.md', 'second');
|
|
|
|
expect(second.id).not.toBe(first.id);
|
|
expect(second.content).toBe('second');
|
|
});
|
|
|
|
it('should allow managed mount documents to reuse storage filenames', async () => {
|
|
const first = await agentDocumentModel.create(agentId, 'skills', '', {
|
|
metadata: { mount: { namespace: 'topic', role: 'root' } },
|
|
});
|
|
const second = await agentDocumentModel.create(agentId, 'skills', '', {
|
|
metadata: { mount: { namespace: 'agent', role: 'root' } },
|
|
});
|
|
|
|
expect(first.documentId).not.toBe(second.documentId);
|
|
});
|
|
});
|
|
|
|
describe('findById and findByFilename', () => {
|
|
it('should isolate records by user', async () => {
|
|
const ownDoc = await agentDocumentModel.create(agentId, 'own.md', 'own content');
|
|
const otherDoc = await otherAgentDocumentModel.create(
|
|
otherAgentId,
|
|
'other.md',
|
|
'other content',
|
|
);
|
|
|
|
const ownResult = await agentDocumentModel.findById(ownDoc.id);
|
|
const otherResult = await agentDocumentModel.findById(otherDoc.id);
|
|
|
|
expect(ownResult?.id).toBe(ownDoc.id);
|
|
expect(otherResult).toBeUndefined();
|
|
|
|
const byFilename = await agentDocumentModel.findByFilename(agentId, 'own.md');
|
|
expect(byFilename?.id).toBe(ownDoc.id);
|
|
});
|
|
|
|
it('should find current-agent records by underlying document ids', async () => {
|
|
const ownDoc = await agentDocumentModel.create(agentId, 'own.md', 'own content');
|
|
const secondAgentDoc = await agentDocumentModel.create(
|
|
secondAgentId,
|
|
'second.md',
|
|
'second content',
|
|
);
|
|
const otherUserDoc = await otherAgentDocumentModel.create(
|
|
otherAgentId,
|
|
'other.md',
|
|
'other content',
|
|
);
|
|
|
|
const result = await agentDocumentModel.findByDocumentIds(agentId, [
|
|
ownDoc.documentId,
|
|
secondAgentDoc.documentId,
|
|
otherUserDoc.documentId,
|
|
'missing-document',
|
|
]);
|
|
|
|
expect(result.map((doc) => doc.id)).toEqual([ownDoc.id]);
|
|
});
|
|
});
|
|
|
|
describe('update and upsert', () => {
|
|
it('should update content, metadata and policy projections', async () => {
|
|
const created = await agentDocumentModel.create(agentId, 'policy.md', 'old', {
|
|
loadPosition: DocumentLoadPosition.BEFORE_FIRST_USER,
|
|
loadRules: { maxTokens: 100, priority: 8 },
|
|
metadata: { description: 'old desc', topic: 'old' },
|
|
});
|
|
|
|
await agentDocumentModel.update(created.id, {
|
|
content: 'new\ncontent',
|
|
loadPosition: DocumentLoadPosition.AFTER_KNOWLEDGE,
|
|
loadRules: { maxTokens: 500, priority: 1 },
|
|
metadata: { description: 'new desc', topic: 'new' },
|
|
policy: { context: { policyLoadFormat: DocumentLoadFormat.FILE } },
|
|
});
|
|
|
|
const updated = await agentDocumentModel.findById(created.id);
|
|
expect(updated?.content).toBe('new\ncontent');
|
|
expect(updated?.metadata).toMatchObject({ description: 'new desc', topic: 'new' });
|
|
expect(updated?.policy?.context?.position).toBe(DocumentLoadPosition.AFTER_KNOWLEDGE);
|
|
expect(updated?.policy?.context?.maxTokens).toBe(500);
|
|
expect(updated?.policy?.context?.priority).toBe(1);
|
|
expect(updated?.policyLoadFormat).toBe(DocumentLoadFormat.FILE);
|
|
expect(updated?.policyLoadPosition).toBe(DocumentLoadPosition.AFTER_KNOWLEDGE);
|
|
|
|
const [updatedDoc] = await serverDB
|
|
.select()
|
|
.from(documents)
|
|
.where(eq(documents.id, created.documentId));
|
|
|
|
expect(updatedDoc?.totalCharCount).toBe('new\ncontent'.length);
|
|
expect(updatedDoc?.totalLineCount).toBe(2);
|
|
expect(updatedDoc?.description).toBe('new desc');
|
|
});
|
|
|
|
it('should upsert by creating a new document when filename does not exist', async () => {
|
|
const result = await agentDocumentModel.upsert(agentId, 'new-upsert.md', 'fresh', {
|
|
loadPosition: DocumentLoadPosition.BEFORE_SYSTEM,
|
|
loadRules: { priority: 5 },
|
|
templateId: 'claw',
|
|
});
|
|
|
|
expect(result.filename).toBe('new-upsert.md');
|
|
expect(result.content).toBe('fresh');
|
|
expect(result.templateId).toBe('claw');
|
|
expect(result.policy?.context?.position).toBe(DocumentLoadPosition.BEFORE_SYSTEM);
|
|
});
|
|
|
|
it('should upsert by filename and merge metadata on updates', async () => {
|
|
const first = await agentDocumentModel.upsert(agentId, 'policy-upsert.md', 'v1', {
|
|
loadPosition: DocumentLoadPosition.BEFORE_FIRST_USER,
|
|
loadRules: { priority: 9 },
|
|
metadata: { a: 1, description: 'v1' },
|
|
});
|
|
|
|
const second = await agentDocumentModel.upsert(agentId, 'policy-upsert.md', 'v2', {
|
|
loadRules: { priority: 1, maxTokens: 900 },
|
|
metadata: { b: 2, description: 'v2' },
|
|
});
|
|
|
|
expect(second.id).toBe(first.id);
|
|
expect(second.content).toBe('v2');
|
|
expect(second.metadata).toMatchObject({ a: 1, b: 2, description: 'v2' });
|
|
expect(second.policy?.context?.priority).toBe(9);
|
|
expect(second.policy?.context?.maxTokens).toBe(900);
|
|
});
|
|
});
|
|
|
|
describe('rename and copy', () => {
|
|
it('should rename and preserve human-readable filename/source', async () => {
|
|
const created = await agentDocumentModel.create(agentId, 'old-name.md', 'hello');
|
|
|
|
const renamed = await agentDocumentModel.rename(created.id, 'New Name');
|
|
|
|
expect(renamed?.title).toBe('New Name');
|
|
expect(renamed?.filename).toBe('New Name');
|
|
|
|
const [doc] = await serverDB
|
|
.select()
|
|
.from(documents)
|
|
.where(eq(documents.id, created.documentId));
|
|
|
|
expect(doc?.source).toBe(`agent-document://${agentId}/${encodeURIComponent('New Name')}`);
|
|
});
|
|
|
|
it('uses the new title verbatim as filename when renaming', async () => {
|
|
const created = await agentDocumentModel.create(agentId, 'identity.md', 'hello');
|
|
|
|
const renamed = await agentDocumentModel.rename(created.id, 'IDENTITY 2');
|
|
|
|
expect(renamed?.filename).toBe('IDENTITY 2');
|
|
});
|
|
|
|
it('should move path metadata without changing agent document identity', async () => {
|
|
const folder = await agentDocumentModel.create(agentId, 'folder', '', {
|
|
fileType: DOCUMENT_FOLDER_TYPE,
|
|
title: 'folder',
|
|
});
|
|
const created = await agentDocumentModel.create(agentId, 'old.md', 'hello');
|
|
|
|
const moved = await agentDocumentModel.movePath(created.id, {
|
|
filename: 'new.md',
|
|
parentId: folder.documentId,
|
|
});
|
|
|
|
expect(moved?.id).toBe(created.id);
|
|
expect(moved?.documentId).toBe(created.documentId);
|
|
expect(moved?.filename).toBe('new.md');
|
|
expect(moved?.parentId).toBe(folder.documentId);
|
|
|
|
const [doc] = await serverDB
|
|
.select()
|
|
.from(documents)
|
|
.where(eq(documents.id, created.documentId));
|
|
|
|
expect(doc?.source).toBe(`agent-document://${agentId}/${encodeURIComponent('new.md')}`);
|
|
});
|
|
|
|
it('should allow moving a document over an existing live sibling filename', async () => {
|
|
const folder = await agentDocumentModel.create(agentId, 'move-folder', '', {
|
|
fileType: DOCUMENT_FOLDER_TYPE,
|
|
title: 'move-folder',
|
|
});
|
|
const source = await agentDocumentModel.create(agentId, 'source.md', 'source');
|
|
const target = await agentDocumentModel.create(agentId, 'target.md', 'target', {
|
|
parentId: folder.documentId,
|
|
});
|
|
|
|
const moved = await agentDocumentModel.movePath(source.id, {
|
|
filename: 'target.md',
|
|
parentId: folder.documentId,
|
|
});
|
|
const matched = await agentDocumentModel.listByParentAndFilename(
|
|
agentId,
|
|
folder.documentId,
|
|
'target.md',
|
|
);
|
|
|
|
expect(moved?.id).toBe(source.id);
|
|
expect(matched.map((item) => item.documentId)).toEqual([
|
|
source.documentId,
|
|
target.documentId,
|
|
]);
|
|
});
|
|
|
|
it('should copy into a new record and keep policy/template metadata', async () => {
|
|
const created = await agentDocumentModel.create(agentId, 'copy-source.md', 'copy me', {
|
|
loadPosition: DocumentLoadPosition.BEFORE_SYSTEM,
|
|
loadRules: { maxTokens: 200, priority: 3 },
|
|
metadata: { description: 'source desc', domain: 'A' },
|
|
templateId: 'claw',
|
|
});
|
|
|
|
const copied = await agentDocumentModel.copy(created.id, 'Copied Title');
|
|
|
|
expect(copied).toBeDefined();
|
|
expect(copied?.id).not.toBe(created.id);
|
|
expect(copied?.documentId).not.toBe(created.documentId);
|
|
expect(copied?.filename).toBe('Copied Title');
|
|
expect(copied?.templateId).toBe('claw');
|
|
expect(copied?.policy?.context?.maxTokens).toBe(200);
|
|
expect(copied?.metadata).toMatchObject({ description: 'source desc', domain: 'A' });
|
|
});
|
|
|
|
it('should preserve policyLoad when copying a document', async () => {
|
|
const created = await agentDocumentModel.create(agentId, 'always-doc.md', 'content', {
|
|
policyLoad: PolicyLoad.ALWAYS,
|
|
});
|
|
|
|
const copied = await agentDocumentModel.copy(created.id, 'Always Copy');
|
|
|
|
expect(copied?.policyLoad).toBe(PolicyLoad.ALWAYS);
|
|
});
|
|
});
|
|
|
|
describe('convertAgentDocumentToSkillIndex and updateDocumentIdentity', () => {
|
|
it('converts an ordinary agent document binding into a skill index while preserving ids', async () => {
|
|
const source = await agentDocumentModel.create(agentId, 'workflow-note', '# Workflow', {
|
|
metadata: { agentSignal: { hintIsSkill: true } },
|
|
});
|
|
const bundle = await agentDocumentModel.create(agentId, 'workflow-note', '', {
|
|
fileType: 'skills/bundle',
|
|
policyLoad: PolicyLoad.DISABLED,
|
|
source: 'agent-signal:skill-management',
|
|
sourceType: 'agent-signal',
|
|
});
|
|
|
|
const converted = await agentDocumentModel.convertAgentDocumentToSkillIndex({
|
|
agentDocumentId: source.id,
|
|
content: '---\nname: workflow-note\ndescription: Workflow note\n---\n# Workflow',
|
|
editorData: { root: { children: [], type: 'root' } },
|
|
filename: 'workflow-note',
|
|
metadata: {
|
|
agentSignal: { hintIsSkill: true },
|
|
skill: { frontmatter: { description: 'Workflow note', name: 'workflow-note' } },
|
|
},
|
|
parentId: bundle.documentId,
|
|
source: 'agent-signal:skill-management',
|
|
sourceType: 'agent-signal',
|
|
title: 'Workflow Note',
|
|
});
|
|
|
|
expect(converted?.id).toBe(source.id);
|
|
expect(converted?.documentId).toBe(source.documentId);
|
|
expect(converted?.fileType).toBe('skills/index');
|
|
expect(converted?.filename).toBe('workflow-note');
|
|
expect(converted?.parentId).toBe(bundle.documentId);
|
|
expect(converted?.policyLoad).toBe(PolicyLoad.DISABLED);
|
|
expect(converted?.sourceType).toBe('agent-signal');
|
|
expect(converted?.source).toBe('agent-signal:skill-management');
|
|
expect(converted?.templateId).toBe('agent-skill');
|
|
expect(converted?.title).toBe('Workflow Note');
|
|
expect(converted?.metadata).toMatchObject({
|
|
agentSignal: { hintIsSkill: true },
|
|
skill: { frontmatter: { description: 'Workflow note', name: 'workflow-note' } },
|
|
});
|
|
|
|
const [doc] = await serverDB
|
|
.select()
|
|
.from(documents)
|
|
.where(eq(documents.id, source.documentId));
|
|
|
|
expect(doc?.description).toBe('Workflow note');
|
|
expect(doc?.totalCharCount).toBe(
|
|
'---\nname: workflow-note\ndescription: Workflow note\n---\n# Workflow'.length,
|
|
);
|
|
expect(doc?.totalLineCount).toBe(5);
|
|
});
|
|
|
|
/**
|
|
* @example
|
|
* Skill creation can convert an existing source document and still roll back as one aggregate.
|
|
*/
|
|
it('rolls back convertAgentDocumentToSkillIndexWithTx when the caller transaction fails', async () => {
|
|
const source = await agentDocumentModel.create(agentId, 'workflow-note', '# Workflow', {
|
|
metadata: { agentSignal: { hintIsSkill: true } },
|
|
});
|
|
const bundle = await agentDocumentModel.create(agentId, 'workflow-note', '', {
|
|
fileType: 'skills/bundle',
|
|
policyLoad: PolicyLoad.DISABLED,
|
|
source: 'agent-signal:skill-management',
|
|
sourceType: 'agent-signal',
|
|
});
|
|
|
|
await expect(
|
|
serverDB.transaction(async (trx) => {
|
|
await agentDocumentModel.convertAgentDocumentToSkillIndexWithTx(trx, {
|
|
agentDocumentId: source.id,
|
|
content: '---\nname: workflow-note\ndescription: Workflow note\n---\n# Workflow',
|
|
filename: 'SKILL.md',
|
|
metadata: {
|
|
agentSignal: { hintIsSkill: true },
|
|
skill: { frontmatter: { description: 'Workflow note', name: 'workflow-note' } },
|
|
},
|
|
parentId: bundle.documentId,
|
|
source: 'agent-signal:skill-management',
|
|
sourceType: 'agent-signal',
|
|
title: 'SKILL.md',
|
|
});
|
|
|
|
throw new Error('Intentional rollback');
|
|
}),
|
|
).rejects.toThrow('Intentional rollback');
|
|
|
|
const unchanged = await agentDocumentModel.findById(source.id);
|
|
|
|
expect(unchanged).toMatchObject({
|
|
documentId: source.documentId,
|
|
fileType: 'agent/document',
|
|
filename: 'workflow-note',
|
|
parentId: null,
|
|
policyLoad: PolicyLoad.PROGRESSIVE,
|
|
sourceType: 'agent',
|
|
});
|
|
});
|
|
|
|
it('updates backing document identity fields without changing the agent document binding', async () => {
|
|
const folder = await agentDocumentModel.create(agentId, 'skills', '', {
|
|
fileType: DOCUMENT_FOLDER_TYPE,
|
|
title: 'skills',
|
|
});
|
|
const created = await agentDocumentModel.create(agentId, 'old-name', 'content');
|
|
|
|
const updated = await agentDocumentModel.updateDocumentIdentity(created.id, {
|
|
filename: 'new-name',
|
|
metadata: { skill: { frontmatter: { description: 'New', name: 'new-name' } } },
|
|
parentId: folder.documentId,
|
|
title: 'New Name',
|
|
});
|
|
|
|
expect(updated?.id).toBe(created.id);
|
|
expect(updated?.documentId).toBe(created.documentId);
|
|
expect(updated?.filename).toBe('new-name');
|
|
expect(updated?.parentId).toBe(folder.documentId);
|
|
expect(updated?.title).toBe('New Name');
|
|
expect(updated?.metadata).toMatchObject({
|
|
skill: { frontmatter: { description: 'New', name: 'new-name' } },
|
|
});
|
|
|
|
const [doc] = await serverDB
|
|
.select()
|
|
.from(documents)
|
|
.where(eq(documents.id, created.documentId));
|
|
|
|
expect(doc?.description).toBe('New');
|
|
});
|
|
|
|
it('returns the existing binding when document identity update has no fields', async () => {
|
|
const created = await agentDocumentModel.create(agentId, 'unchanged', 'content');
|
|
|
|
const updated = await agentDocumentModel.updateDocumentIdentity(created.id, {});
|
|
|
|
expect(updated).toMatchObject({
|
|
documentId: created.documentId,
|
|
filename: 'unchanged',
|
|
id: created.id,
|
|
title: 'unchanged',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('findByAgent and findByTemplate', () => {
|
|
it('should return matched docs with parsed loadRules', async () => {
|
|
await agentDocumentModel.create(agentId, 'a.md', 'A', {
|
|
loadRules: { maxTokens: 100, priority: 2 },
|
|
});
|
|
await agentDocumentModel.create(agentId, 'b.md', 'B', {
|
|
loadRules: { maxTokens: 50, priority: 1 },
|
|
});
|
|
await agentDocumentModel.create(agentId, 'c.md', 'C', {
|
|
loadRules: { priority: 9 },
|
|
templateId: 'claw',
|
|
});
|
|
await agentDocumentModel.create(agentId, 'd.md', 'D', {
|
|
loadRules: { priority: 8 },
|
|
templateId: 'claw',
|
|
});
|
|
await agentDocumentModel.create(secondAgentId, 'e.md', 'E', {
|
|
loadRules: { priority: 7 },
|
|
templateId: 'claw',
|
|
});
|
|
|
|
const byAgent = await agentDocumentModel.findByAgent(agentId);
|
|
expect(byAgent).toHaveLength(4);
|
|
expect(byAgent.every((item) => item.agentId === agentId)).toBe(true);
|
|
expect(byAgent[0].loadRules).toBeDefined();
|
|
|
|
const byTemplate = await agentDocumentModel.findByTemplate(agentId, 'claw');
|
|
expect(byTemplate).toHaveLength(2);
|
|
expect(byTemplate.every((item) => item.templateId === 'claw')).toBe(true);
|
|
});
|
|
|
|
it('should return only skill-managed docs for skill registry assembly', async () => {
|
|
const bundle = await agentDocumentModel.create(agentId, 'bug-triage', 'bundle body', {
|
|
fileType: SKILL_BUNDLE_FILE_TYPE,
|
|
templateId: AGENT_SKILL_TEMPLATE_ID,
|
|
});
|
|
await agentDocumentModel.create(agentId, 'SKILL.md', 'skill body', {
|
|
fileType: SKILL_INDEX_FILE_TYPE,
|
|
parentId: bundle.documentId,
|
|
templateId: AGENT_SKILL_TEMPLATE_ID,
|
|
});
|
|
await agentDocumentModel.create(agentId, 'ordinary.md', 'ordinary body');
|
|
await agentDocumentModel.create(agentId, 'web-page', 'web body', {
|
|
fileType: 'article',
|
|
sourceType: 'web',
|
|
});
|
|
|
|
const result = await agentDocumentModel.findSkillDocsByAgent(agentId);
|
|
|
|
expect(result.map((item) => item.filename).sort()).toEqual(['SKILL.md', 'bug-triage']);
|
|
expect(result.every((item) => item.category === 'skill')).toBe(true);
|
|
});
|
|
|
|
it('should omit progressive document content for chat context hydration', async () => {
|
|
await agentDocumentModel.create(agentId, 'always.md', 'always body', {
|
|
editorData: { root: { children: [{ text: 'always body' }] } },
|
|
policyLoad: PolicyLoad.ALWAYS,
|
|
});
|
|
await agentDocumentModel.create(agentId, 'progressive.md', 'progressive body', {
|
|
editorData: { root: { children: [{ text: 'progressive body' }] } },
|
|
policyLoad: PolicyLoad.PROGRESSIVE,
|
|
});
|
|
await agentDocumentModel.create(agentId, 'web-page', 'web body', {
|
|
fileType: 'article',
|
|
policyLoad: PolicyLoad.PROGRESSIVE,
|
|
sourceType: 'web',
|
|
});
|
|
|
|
const result = await agentDocumentModel.findContextByAgent(agentId);
|
|
const byFilename = Object.fromEntries(result.map((item) => [item.filename, item]));
|
|
|
|
expect(byFilename['always.md']?.content).toBe('always body');
|
|
expect(byFilename['always.md']?.contentCharCount).toBe('always body'.length);
|
|
expect(byFilename['always.md']?.editorData).toEqual({
|
|
root: { children: [{ text: 'always body' }] },
|
|
});
|
|
expect(byFilename['progressive.md']?.content).toBe('');
|
|
expect(byFilename['progressive.md']?.contentCharCount).toBe('progressive body'.length);
|
|
expect(byFilename['progressive.md']?.editorData).toBeNull();
|
|
expect(byFilename['web-page']?.content).toBe('');
|
|
expect(byFilename['web-page']?.contentCharCount).toBe('web body'.length);
|
|
});
|
|
});
|
|
|
|
describe('hasByAgent', () => {
|
|
it('should return whether a user has visible documents for the agent', async () => {
|
|
expect(await agentDocumentModel.hasByAgent(agentId)).toBe(false);
|
|
|
|
const created = await agentDocumentModel.create(agentId, 'exists.md', 'A');
|
|
await agentDocumentModel.create(secondAgentId, 'other-agent.md', 'B');
|
|
|
|
expect(await agentDocumentModel.hasByAgent(agentId)).toBe(true);
|
|
expect(await agentDocumentModel.hasByAgent(secondAgentId)).toBe(true);
|
|
|
|
await agentDocumentModel.delete(created.id);
|
|
|
|
expect(await agentDocumentModel.hasByAgent(agentId)).toBe(false);
|
|
});
|
|
|
|
it('should keep existence checks isolated by user', async () => {
|
|
await otherAgentDocumentModel.create(otherAgentId, 'other-user.md', 'A');
|
|
|
|
expect(await agentDocumentModel.hasByAgent(otherAgentId)).toBe(false);
|
|
expect(await otherAgentDocumentModel.hasByAgent(otherAgentId)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('updateToolLoadRule and loadable queries', () => {
|
|
it('should apply tool load rule and exclude manual docs from loadable results', async () => {
|
|
const alwaysDoc = await agentDocumentModel.create(agentId, 'always.md', 'always', {
|
|
loadPosition: DocumentLoadPosition.BEFORE_FIRST_USER,
|
|
loadRules: { priority: 2 },
|
|
});
|
|
const manualDoc = await agentDocumentModel.create(agentId, 'manual.md', 'manual', {
|
|
loadPosition: DocumentLoadPosition.BEFORE_FIRST_USER,
|
|
loadRules: { priority: 1 },
|
|
});
|
|
|
|
const updated = await agentDocumentModel.updateToolLoadRule(manualDoc.id, {
|
|
keywordMatchMode: 'all',
|
|
keywords: ['urgent', 'risk'],
|
|
maxDocuments: 3,
|
|
maxTokens: 600,
|
|
mode: 'manual',
|
|
pinnedDocumentIds: [alwaysDoc.id],
|
|
policyLoadFormat: 'file',
|
|
priority: 10,
|
|
regexp: '\\burgent\\b',
|
|
rule: DocumentLoadRule.BY_KEYWORDS,
|
|
timeRange: { from: '2026-01-01T00:00:00.000Z', to: '2026-12-31T23:59:59.000Z' },
|
|
});
|
|
|
|
expect(updated?.policyLoad).toBe(PolicyLoad.DISABLED);
|
|
expect(updated?.policyLoadFormat).toBe(DocumentLoadFormat.FILE);
|
|
expect(updated?.policy?.context?.maxDocuments).toBe(3);
|
|
expect(updated?.policy?.context?.rule).toBe(DocumentLoadRule.BY_KEYWORDS);
|
|
expect(updated?.policy?.context?.keywords).toEqual(['urgent', 'risk']);
|
|
expect(updated?.policy?.context?.keywordMatchMode).toBe('all');
|
|
expect(updated?.policy?.context?.regexp).toBe('\\burgent\\b');
|
|
expect(updated?.policy?.context?.timeRange).toEqual({
|
|
from: '2026-01-01T00:00:00.000Z',
|
|
to: '2026-12-31T23:59:59.000Z',
|
|
});
|
|
expect(updated?.policy?.context?.pinnedDocumentIds).toEqual([alwaysDoc.id]);
|
|
|
|
const loadable = await agentDocumentModel.getLoadableDocuments(agentId);
|
|
expect(loadable).toHaveLength(1);
|
|
expect(loadable[0].id).toBe(alwaysDoc.id);
|
|
|
|
const injectable = await agentDocumentModel.getInjectableDocuments(agentId);
|
|
expect(injectable.map((d) => d.id)).toEqual([alwaysDoc.id]);
|
|
|
|
const context = await agentDocumentModel.getAgentContext(agentId);
|
|
expect(context).toContain('--- always.md ---');
|
|
expect(context).not.toContain('--- manual.md ---');
|
|
});
|
|
|
|
it('should preserve progressive policyLoad when updating load rule without mode', async () => {
|
|
const doc = await agentDocumentModel.create(agentId, 'progressive.md', 'content');
|
|
expect(doc.policyLoad).toBe(PolicyLoad.PROGRESSIVE);
|
|
|
|
const updated = await agentDocumentModel.updateToolLoadRule(doc.id, {
|
|
rule: 'by-keywords',
|
|
keywords: ['test'],
|
|
});
|
|
|
|
expect(updated?.policyLoad).toBe(PolicyLoad.PROGRESSIVE);
|
|
expect(updated?.policy?.context?.keywords).toEqual(['test']);
|
|
expect(updated?.policyLoadRule).toBe(DocumentLoadRule.BY_KEYWORDS);
|
|
});
|
|
|
|
it('should group docs by position and sort by priority ascending', async () => {
|
|
await agentDocumentModel.create(agentId, 'p2.md', 'p2', {
|
|
loadPosition: DocumentLoadPosition.BEFORE_KNOWLEDGE,
|
|
loadRules: { priority: 2 },
|
|
});
|
|
await agentDocumentModel.create(agentId, 'p1.md', 'p1', {
|
|
loadPosition: DocumentLoadPosition.BEFORE_KNOWLEDGE,
|
|
loadRules: { priority: 1 },
|
|
});
|
|
|
|
const grouped = await agentDocumentModel.getDocumentsByPosition(agentId);
|
|
const docsAtPosition = grouped.get(DocumentLoadPosition.BEFORE_KNOWLEDGE) || [];
|
|
|
|
expect(docsAtPosition).toHaveLength(2);
|
|
expect(docsAtPosition[0].filename).toBe('p1.md');
|
|
expect(docsAtPosition[1].filename).toBe('p2.md');
|
|
});
|
|
});
|
|
|
|
describe('delete', () => {
|
|
it('should soft delete a single document while preserving linked documents row', async () => {
|
|
const created = await agentDocumentModel.create(agentId, 'delete-me.md', 'delete me');
|
|
|
|
await agentDocumentModel.delete(created.id, 'cleanup');
|
|
|
|
const visible = await agentDocumentModel.findById(created.id);
|
|
expect(visible).toBeUndefined();
|
|
|
|
const [rawAgentDoc] = await serverDB
|
|
.select()
|
|
.from(agentDocuments)
|
|
.where(eq(agentDocuments.id, created.id));
|
|
|
|
expect(rawAgentDoc?.deletedAt).toBeInstanceOf(Date);
|
|
expect(rawAgentDoc?.deletedByUserId).toBe(userId);
|
|
expect(rawAgentDoc?.deletedByAgentId).toBeNull();
|
|
expect(rawAgentDoc?.deleteReason).toBe('cleanup');
|
|
expect(rawAgentDoc?.policyLoad).toBe(PolicyLoad.DISABLED);
|
|
|
|
const [rawDoc] = await serverDB
|
|
.select()
|
|
.from(documents)
|
|
.where(eq(documents.id, created.documentId));
|
|
|
|
expect(rawDoc).toBeDefined();
|
|
});
|
|
|
|
it('should restore a deleted document even when a live sibling has the same filename', async () => {
|
|
const first = await agentDocumentModel.create(agentId, 'restore-conflict.md', 'first');
|
|
|
|
await agentDocumentModel.delete(first.id, 'replace');
|
|
const second = await agentDocumentModel.create(agentId, 'restore-conflict.md', 'second');
|
|
|
|
await agentDocumentModel.restore(first.id);
|
|
const matched = await agentDocumentModel.listByParentAndFilename(
|
|
agentId,
|
|
null,
|
|
'restore-conflict.md',
|
|
);
|
|
|
|
expect(matched.map((item) => item.documentId)).toEqual([first.documentId, second.documentId]);
|
|
});
|
|
|
|
it('should return empty string from getAgentContext when no loadable docs exist', async () => {
|
|
const context = await agentDocumentModel.getAgentContext(agentId);
|
|
expect(context).toBe('');
|
|
});
|
|
|
|
it('should soft delete by agent and by template', async () => {
|
|
const templateDoc = await agentDocumentModel.create(agentId, 'template-a.md', 'A', {
|
|
templateId: 'claw',
|
|
});
|
|
const otherTemplateDoc = await agentDocumentModel.create(agentId, 'template-b.md', 'B', {
|
|
templateId: 'other',
|
|
});
|
|
const secondAgentDoc = await agentDocumentModel.create(secondAgentId, 'agent-2.md', 'C');
|
|
|
|
await agentDocumentModel.deleteByTemplate(agentId, 'claw', 'template cleanup');
|
|
|
|
const clawVisible = await agentDocumentModel.findById(templateDoc.id);
|
|
const otherTemplateVisible = await agentDocumentModel.findById(otherTemplateDoc.id);
|
|
expect(clawVisible).toBeUndefined();
|
|
expect(otherTemplateVisible).toBeDefined();
|
|
|
|
await agentDocumentModel.deleteByAgent(secondAgentId, 'agent cleanup');
|
|
const secondAgentVisible = await agentDocumentModel.findById(secondAgentDoc.id);
|
|
expect(secondAgentVisible).toBeUndefined();
|
|
const [secondAgentRow] = await serverDB
|
|
.select()
|
|
.from(agentDocuments)
|
|
.where(and(eq(agentDocuments.id, secondAgentDoc.id), eq(agentDocuments.userId, userId)));
|
|
expect(secondAgentRow?.deletedByAgentId).toBe(secondAgentId);
|
|
expect(secondAgentRow?.deletedByUserId).toBeNull();
|
|
|
|
const [otherTemplateRow] = await serverDB
|
|
.select()
|
|
.from(agentDocuments)
|
|
.where(and(eq(agentDocuments.id, otherTemplateDoc.id), eq(agentDocuments.userId, userId)));
|
|
expect(otherTemplateRow?.deletedAt).toBeNull();
|
|
});
|
|
|
|
it('should support include-deleted lookups and deleted-only child listings', async () => {
|
|
const folder = await agentDocumentModel.create(agentId, 'notes', '', {
|
|
fileType: DOCUMENT_FOLDER_TYPE,
|
|
title: 'notes',
|
|
});
|
|
const visibleChild = await agentDocumentModel.create(agentId, 'visible.md', 'visible', {
|
|
parentId: folder.documentId,
|
|
});
|
|
const deletedChild = await agentDocumentModel.create(agentId, 'deleted.md', 'deleted', {
|
|
parentId: folder.documentId,
|
|
});
|
|
|
|
await agentDocumentModel.delete(deletedChild.id, 'trash it');
|
|
|
|
expect(await agentDocumentModel.findById(deletedChild.id)).toBeUndefined();
|
|
expect(
|
|
await agentDocumentModel.findById(deletedChild.id, {
|
|
includeDeleted: true,
|
|
}),
|
|
).toMatchObject({ id: deletedChild.id });
|
|
|
|
const liveChildren = await agentDocumentModel.listByParent(agentId, folder.documentId);
|
|
const allChildren = await agentDocumentModel.listByParent(agentId, folder.documentId, {
|
|
includeDeleted: true,
|
|
});
|
|
const deletedChildren = await agentDocumentModel.listByParent(agentId, folder.documentId, {
|
|
deletedOnly: true,
|
|
});
|
|
|
|
expect(liveChildren.map((item) => item.id)).toEqual([visibleChild.id]);
|
|
expect(allChildren.map((item) => item.id).sort()).toEqual(
|
|
[visibleChild.id, deletedChild.id].sort(),
|
|
);
|
|
expect(deletedChildren.map((item) => item.id)).toEqual([deletedChild.id]);
|
|
|
|
const deletedByPath = await agentDocumentModel.findByParentAndFilename(
|
|
agentId,
|
|
folder.documentId,
|
|
'deleted.md',
|
|
{
|
|
includeDeleted: true,
|
|
},
|
|
);
|
|
expect(deletedByPath?.id).toBe(deletedChild.id);
|
|
|
|
const liveByPath = await agentDocumentModel.listByParentAndFilename(
|
|
agentId,
|
|
folder.documentId,
|
|
'visible.md',
|
|
{
|
|
limit: 1,
|
|
},
|
|
);
|
|
expect(liveByPath.map((item) => item.id)).toEqual([visibleChild.id]);
|
|
});
|
|
|
|
it('should soft-delete, restore, and permanently delete a subtree by root document id', async () => {
|
|
const rootFolder = await agentDocumentModel.create(agentId, 'workspace', '', {
|
|
fileType: DOCUMENT_FOLDER_TYPE,
|
|
title: 'workspace',
|
|
});
|
|
const nestedFolder = await agentDocumentModel.create(agentId, 'drafts', '', {
|
|
fileType: DOCUMENT_FOLDER_TYPE,
|
|
parentId: rootFolder.documentId,
|
|
title: 'drafts',
|
|
});
|
|
const nestedFile = await agentDocumentModel.create(agentId, 'plan.md', 'v1', {
|
|
parentId: nestedFolder.documentId,
|
|
});
|
|
const siblingFile = await agentDocumentModel.create(agentId, 'keep.md', 'keep me');
|
|
|
|
await agentDocumentModel.deleteSubtreeByDocumentId(
|
|
agentId,
|
|
rootFolder.documentId,
|
|
'recursive cleanup',
|
|
);
|
|
|
|
expect(await agentDocumentModel.findById(rootFolder.id)).toBeUndefined();
|
|
expect(await agentDocumentModel.findById(nestedFolder.id)).toBeUndefined();
|
|
expect(await agentDocumentModel.findById(nestedFile.id)).toBeUndefined();
|
|
expect(await agentDocumentModel.findById(siblingFile.id)).toBeDefined();
|
|
|
|
const deletedTree = await agentDocumentModel.listSubtreeByDocumentId(
|
|
agentId,
|
|
rootFolder.documentId,
|
|
{
|
|
includeDeleted: true,
|
|
},
|
|
);
|
|
expect(deletedTree.map((item) => item.id).sort()).toEqual(
|
|
[rootFolder.id, nestedFolder.id, nestedFile.id].sort(),
|
|
);
|
|
|
|
const trashItems = await agentDocumentModel.listDeletedByAgent(agentId);
|
|
expect(trashItems.map((item) => item.id).sort()).toEqual(
|
|
[rootFolder.id, nestedFolder.id, nestedFile.id].sort(),
|
|
);
|
|
|
|
await agentDocumentModel.restoreSubtreeByDocumentId(agentId, rootFolder.documentId);
|
|
|
|
const restoredTree = await agentDocumentModel.listSubtreeByDocumentId(
|
|
agentId,
|
|
rootFolder.documentId,
|
|
);
|
|
expect(restoredTree.map((item) => item.id).sort()).toEqual(
|
|
[rootFolder.id, nestedFolder.id, nestedFile.id].sort(),
|
|
);
|
|
expect(await agentDocumentModel.listDeletedByAgent(agentId)).toEqual([]);
|
|
|
|
await agentDocumentModel.deleteSubtreeByDocumentId(
|
|
agentId,
|
|
rootFolder.documentId,
|
|
'recursive cleanup',
|
|
);
|
|
await agentDocumentModel.permanentlyDeleteSubtreeByDocumentId(agentId, rootFolder.documentId);
|
|
|
|
expect(
|
|
await agentDocumentModel.findByDocumentId(agentId, rootFolder.documentId, {
|
|
includeDeleted: true,
|
|
}),
|
|
).toBeUndefined();
|
|
expect(
|
|
await agentDocumentModel.findByDocumentId(agentId, nestedFolder.documentId, {
|
|
includeDeleted: true,
|
|
}),
|
|
).toBeUndefined();
|
|
expect(
|
|
await agentDocumentModel.findByDocumentId(agentId, nestedFile.documentId, {
|
|
includeDeleted: true,
|
|
}),
|
|
).toBeUndefined();
|
|
expect(await agentDocumentModel.findById(siblingFile.id)).toBeDefined();
|
|
|
|
const remainingRows = await serverDB
|
|
.select()
|
|
.from(documents)
|
|
.where(eq(documents.userId, userId));
|
|
|
|
expect(remainingRows.map((item) => item.id)).toContain(siblingFile.documentId);
|
|
expect(remainingRows.map((item) => item.id)).not.toContain(rootFolder.documentId);
|
|
expect(remainingRows.map((item) => item.id)).not.toContain(nestedFolder.documentId);
|
|
expect(remainingRows.map((item) => item.id)).not.toContain(nestedFile.documentId);
|
|
});
|
|
});
|
|
});
|