fix: preserve knowledge indexing state

This commit is contained in:
lofyer
2026-08-13 04:47:44 +08:00
parent e3b5702767
commit 0c46afba59
4 changed files with 772 additions and 91 deletions
+176 -6
View File
@@ -86,7 +86,7 @@ describe('KnowledgeDatabase', () => {
const inspection = new DatabaseSync(path)
expect(
inspection.prepare('PRAGMA user_version').get()
).toEqual({ user_version: 9 })
).toEqual({ user_version: 10 })
expect(
inspection
.prepare('SELECT version FROM schema_migrations ORDER BY version')
@@ -100,7 +100,8 @@ describe('KnowledgeDatabase', () => {
{ version: 6 },
{ version: 7 },
{ version: 8 },
{ version: 9 }
{ version: 9 },
{ version: 10 }
])
inspection.close()
@@ -192,7 +193,7 @@ describe('KnowledgeDatabase', () => {
})
})
it('upgrades an existing v1 database through knowledge schema v9', async () => {
it('upgrades an existing v1 database through knowledge schema v10', async () => {
const { database, path } = await createDatabase()
const knowledgeBase = database.createKnowledgeBase({
name: 'Version one data',
@@ -208,7 +209,8 @@ describe('KnowledgeDatabase', () => {
DROP TABLE embedding_index_state;
DROP TABLE chunk_embeddings;
DROP TABLE knowledge_tasks;
DELETE FROM schema_migrations WHERE version IN (2, 3, 4, 5, 6, 7, 8, 9);
DELETE FROM schema_migrations
WHERE version IN (2, 3, 4, 5, 6, 7, 8, 9, 10);
PRAGMA user_version = 1;
`)
downgrade.close()
@@ -218,7 +220,7 @@ describe('KnowledgeDatabase', () => {
upgraded.initialize()
const inspection = new DatabaseSync(path)
expect(inspection.prepare('PRAGMA user_version').get()).toEqual({
user_version: 9
user_version: 10
})
expect(
inspection
@@ -325,7 +327,7 @@ describe('KnowledgeDatabase', () => {
WHERE new.enabled = 1 AND new.role <> 'parent';
END;
INSERT INTO chunks_fts(chunks_fts) VALUES ('rebuild');
DELETE FROM schema_migrations WHERE version = 9;
DELETE FROM schema_migrations WHERE version IN (9, 10);
PRAGMA user_version = 8;
`)
downgrade
@@ -478,6 +480,38 @@ describe('KnowledgeDatabase', () => {
).toMatchObject({ status: 'interrupted', progress: 30 })
})
it('bounds task status text from runtime failures', async () => {
const { database } = await createDatabase()
const library = database.createKnowledgeBase({
name: 'Bounded task status',
storageMode: 'reference'
})
const task = database.createKnowledgeTask({
libraryId: library.id,
documentName: library.name,
scope: 'library',
kind: 'library-rebuild'
})
expect(
database.updateKnowledgeTask(task.id, {
status: 'failed',
message: 'm'.repeat(2_000),
error: {
message: 'e'.repeat(2_000),
remedy: 'r'.repeat(2_000)
}
})
).toMatchObject({
status: 'failed',
message: 'm'.repeat(1_000),
error: {
message: 'e'.repeat(1_000),
remedy: 'r'.repeat(1_000)
}
})
})
it('lists every active task in addition to the terminal history limit', async () => {
const { database } = await createDatabase()
const library = database.createKnowledgeBase({
@@ -779,6 +813,63 @@ describe('KnowledgeDatabase', () => {
expect(database.deleteEntity(other.id)).toBe(true)
})
it('finds graph identities beyond the ordinary list limit', async () => {
const { database } = await createDatabase()
const knowledgeBase = database.createKnowledgeBase({
name: 'Large graph identity',
storageMode: 'reference'
})
let expectedEntityId = ''
const targetEntityIds: string[] = []
for (let index = 0; index < 501; index += 1) {
const entity = database.createEntity({
knowledgeBaseId: knowledgeBase.id,
name: `Entity ${String(index).padStart(3, '0')}`,
type: 'CONCEPT',
locked: false
})
targetEntityIds.push(entity.id)
if (index === 500) {
expectedEntityId = entity.id
}
}
const source = database.createEntity({
knowledgeBaseId: knowledgeBase.id,
name: 'Relation Source',
type: 'CONCEPT',
locked: false
})
let expectedRelationId = ''
for (let index = 0; index < 501; index += 1) {
const relation = database.createRelation({
knowledgeBaseId: knowledgeBase.id,
sourceEntityId: source.id,
targetEntityId: targetEntityIds[index]!,
type: 'RELATED_TO',
locked: false
})
if (index === 500) {
expectedRelationId = relation.id
}
}
expect(
database.findEntityByCanonicalName(
knowledgeBase.id,
'CONCEPT',
'Entity 500'
)?.id
).toBe(expectedEntityId)
expect(
database.findRelationByIdentity(
knowledgeBase.id,
source.id,
targetEntityIds[500]!,
'RELATED_TO'
)?.id
).toBe(expectedRelationId)
})
it('persists controlled ontology updates and marks graph rebuild state', async () => {
const { database } = await createDatabase()
const library = database.createKnowledgeBase({
@@ -1229,6 +1320,12 @@ describe('KnowledgeDatabase', () => {
}
]
)
const concurrentReplacementId =
database.beginDocumentEmbeddingReplacement(
document.id,
'openai-compatible',
'embed-v1'
)
expect(
database.vectorSearch({
@@ -1244,6 +1341,9 @@ describe('KnowledgeDatabase', () => {
'openai-compatible',
'embed-v1'
)
database.discardDocumentEmbeddingReplacement(
concurrentReplacementId
)
expect(
database.vectorSearch({
knowledgeBaseId: knowledgeBase.id,
@@ -1254,6 +1354,55 @@ describe('KnowledgeDatabase', () => {
).toBe(item.id)
})
it('rejects a staged embedding replacement after chunk content changes', async () => {
const { database } = await createDatabase()
const knowledgeBase = database.createKnowledgeBase({
name: 'Stale replacement',
storageMode: 'reference'
})
const seeded = seedDocument(
database,
knowledgeBase.id,
'stale-replacement'
)
const document =
database.getEmbeddingIndexDocument(seeded.documentId)!
const item = document.items[0]!
const replacementId =
database.beginDocumentEmbeddingReplacement(
document.id,
'openai-compatible',
'embed-v1'
)
database.appendDocumentEmbeddingBatch(
replacementId,
document.id,
'openai-compatible',
'embed-v1',
[{
chunkId: item.id,
contentChecksum: item.contentChecksum!,
vector: [1, 0]
}]
)
database.updateChunk({
knowledgeBaseId: knowledgeBase.id,
documentId: document.id,
chunkId: item.id,
content: 'newer content'
})
expect(() =>
database.finishDocumentEmbeddingReplacement(
replacementId,
document.id,
'openai-compatible',
'embed-v1'
)
).toThrow('content changed')
database.discardDocumentEmbeddingReplacement(replacementId)
})
it('persists embedding index jobs independently by knowledge base', async () => {
const created = await createDatabase()
let database = created.database
@@ -1442,6 +1591,20 @@ describe('KnowledgeDatabase', () => {
0
).map((chunk) => chunk.id)
).toEqual(['parent-chunk'])
const graphEntity = database.createEntity({
knowledgeBaseId: library.id,
name: 'Chunk evidence',
type: 'CONCEPT',
locked: false
})
const evidence = database.createEvidence({
knowledgeBaseId: library.id,
entityId: graphEntity.id,
documentId: document.id,
chunkId: 'child-chunk',
quote: 'recallable child text',
source: 'rules'
})
database.updateChunk({
knowledgeBaseId: library.id,
@@ -1452,6 +1615,12 @@ describe('KnowledgeDatabase', () => {
expect(
database.search({ knowledgeBaseId: library.id, query: 'recallable' })
).toEqual([])
expect(database.listEvidence(library.id)).toEqual([
expect.objectContaining({ id: evidence.id })
])
expect(
database.graphSearch(library.id, 'Chunk evidence')
).toEqual([])
database.updateChunk({
knowledgeBaseId: library.id,
documentId: document.id,
@@ -1462,6 +1631,7 @@ describe('KnowledgeDatabase', () => {
expect(
database.search({ knowledgeBaseId: library.id, query: '人工修正' })
).toHaveLength(1)
expect(database.listEvidence(library.id)).toEqual([])
expect(
database.listChunksPage({
knowledgeBaseId: library.id,