Skip to content

Commit 8e3e608

Browse files
authored
fix(knowledge): align document tag provenance selections with the serialized request (#6332)
* fix(knowledge): align document tag provenance selections with the serialized request The create/upsert document tools counted one provenance selection pair per parseDocumentTags entry, while the write route built targets from the serialized documentTagsData and dropped entries whose value is the empty string. A tag value that is truthy before coercion but stringifies to empty (`[]`, `[null]`, `{ toString: () => '' }`) was therefore counted by the tool and not by the route, and the bundle length check rejected the write with 400. Both sides now read one shared parser over the exact bytes that go on the wire, so their counts cannot diverge. * test(knowledge): use as const for the empty-stringifying tag fixture
1 parent eb245ad commit 8e3e608

5 files changed

Lines changed: 103 additions & 30 deletions

File tree

apps/sim/app/api/knowledge/secret-provenance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ import {
1919
importKnowledgePersistedResponseSecretProvenance,
2020
type KnowledgeDocumentSourceValue,
2121
type KnowledgeDocumentWriteSecretProvenance,
22-
parseKnowledgeDocumentTagProvenanceTargets,
2322
} from '@/lib/knowledge/secret-provenance'
2423
import {
2524
knowledgeDocumentContentSelectionKey,
2625
knowledgeDocumentFilenameSelectionKey,
2726
knowledgeDocumentTagNameSelectionKey,
2827
knowledgeDocumentTagValueSelectionKey,
28+
parseKnowledgeDocumentTagProvenanceTargets,
2929
} from '@/lib/knowledge/secret-provenance-selection'
3030
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
3131

apps/sim/lib/knowledge/secret-provenance-selection.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
1+
export interface KnowledgeDocumentTagProvenanceTarget {
2+
tagName: string
3+
value: unknown
4+
}
5+
6+
/**
7+
* Parses only tag entries that can causally contribute a persisted tag value. Both the tool that
8+
* builds the request selections and the route that builds the write targets read this one parser,
9+
* so their counts can never diverge.
10+
*/
11+
export function parseKnowledgeDocumentTagProvenanceTargets(
12+
documentTagsData: string | undefined
13+
): KnowledgeDocumentTagProvenanceTarget[] {
14+
if (!documentTagsData) return []
15+
try {
16+
const parsed: unknown = JSON.parse(documentTagsData)
17+
if (!Array.isArray(parsed)) return []
18+
return parsed.flatMap((candidate) => {
19+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return []
20+
const record = candidate as Record<string, unknown>
21+
const tagName = typeof record.tagName === 'string' ? record.tagName.trim() : ''
22+
if (!tagName || record.value === undefined || record.value === null || record.value === '') {
23+
return []
24+
}
25+
return [{ tagName, value: record.value }]
26+
})
27+
} catch {
28+
return []
29+
}
30+
}
31+
132
export function knowledgeDocumentFilenameSelectionKey(documentIndex: number): string {
233
return `document-filename:${documentIndex}`
334
}

apps/sim/lib/knowledge/secret-provenance.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -56,33 +56,6 @@ export interface KnowledgeDocumentWriteSecretProvenance {
5656
}[]
5757
}
5858

59-
interface KnowledgeDocumentTagProvenanceTarget {
60-
tagName: string
61-
value: unknown
62-
}
63-
64-
/** Parses only tag entries that can causally contribute a persisted tag value. */
65-
export function parseKnowledgeDocumentTagProvenanceTargets(
66-
documentTagsData: string | undefined
67-
): KnowledgeDocumentTagProvenanceTarget[] {
68-
if (!documentTagsData) return []
69-
try {
70-
const parsed: unknown = JSON.parse(documentTagsData)
71-
if (!Array.isArray(parsed)) return []
72-
return parsed.flatMap((candidate) => {
73-
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return []
74-
const record = candidate as Record<string, unknown>
75-
const tagName = typeof record.tagName === 'string' ? record.tagName.trim() : ''
76-
if (!tagName || record.value === undefined || record.value === null || record.value === '') {
77-
return []
78-
}
79-
return [{ tagName, value: record.value }]
80-
})
81-
} catch {
82-
return []
83-
}
84-
}
85-
8659
export type KnowledgeDocumentMetadataField = Exclude<
8760
keyof KnowledgeDocumentSourceValue,
8861
'fileUrl' | 'contentHash'
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import {
6+
knowledgeDocumentContentSelectionKey,
7+
knowledgeDocumentFilenameSelectionKey,
8+
knowledgeDocumentTagNameSelectionKey,
9+
knowledgeDocumentTagValueSelectionKey,
10+
parseKnowledgeDocumentTagProvenanceTargets,
11+
} from '@/lib/knowledge/secret-provenance-selection'
12+
import { selectKnowledgeDocumentWriteSecretProvenance } from '@/tools/knowledge/secret-provenance'
13+
import { formatDocumentTagsForAPI, parseDocumentTags } from '@/tools/shared/tags'
14+
15+
/** Mirrors the selection keys the knowledge write route derives from the serialized request body. */
16+
function serverSelectionKeys(documentTags: unknown): string[] {
17+
const { documentTagsData } = formatDocumentTagsForAPI(parseDocumentTags(documentTags))
18+
return [
19+
knowledgeDocumentFilenameSelectionKey(0),
20+
knowledgeDocumentContentSelectionKey(0),
21+
...parseKnowledgeDocumentTagProvenanceTargets(documentTagsData).flatMap((_tag, tagIndex) => [
22+
knowledgeDocumentTagNameSelectionKey(0, tagIndex),
23+
knowledgeDocumentTagValueSelectionKey(0, tagIndex),
24+
]),
25+
]
26+
}
27+
28+
const EMPTY_STRINGIFYING_TAG_VALUES = [
29+
['empty array', []],
30+
['array of null', [null]],
31+
['array of undefined', [undefined]],
32+
['object stringifying to empty', { toString: () => '' }],
33+
] as const
34+
35+
describe('selectKnowledgeDocumentWriteSecretProvenance', () => {
36+
it.each(EMPTY_STRINGIFYING_TAG_VALUES)(
37+
'agrees with the server target count for a tag value that is a %s',
38+
(_label, tagValue) => {
39+
const documentTags = [
40+
{ tagName: 'kept', value: 'value' },
41+
{ tagName: 'dropped', value: tagValue },
42+
]
43+
const selections = selectKnowledgeDocumentWriteSecretProvenance({
44+
name: 'doc.md',
45+
content: 'content',
46+
documentTags,
47+
})
48+
49+
expect(selections.map((selection) => selection.key)).toEqual(
50+
serverSelectionKeys(documentTags)
51+
)
52+
}
53+
)
54+
55+
it('keeps tags whose serialized value is non-empty', () => {
56+
const documentTags = { alpha: 'one', beta: 2, gamma: false }
57+
const selections = selectKnowledgeDocumentWriteSecretProvenance({
58+
name: 'doc.md',
59+
content: 'content',
60+
documentTags,
61+
})
62+
63+
expect(selections.map((selection) => selection.key)).toEqual(serverSelectionKeys(documentTags))
64+
expect(selections).toHaveLength(8)
65+
})
66+
})

apps/sim/tools/knowledge/secret-provenance.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import {
44
knowledgeDocumentFilenameSelectionKey,
55
knowledgeDocumentTagNameSelectionKey,
66
knowledgeDocumentTagValueSelectionKey,
7+
parseKnowledgeDocumentTagProvenanceTargets,
78
} from '@/lib/knowledge/secret-provenance-selection'
89
import { inferDocumentFileInfo } from '@/tools/knowledge/types'
9-
import { parseDocumentTags } from '@/tools/shared/tags'
10+
import { formatDocumentTagsForAPI, parseDocumentTags } from '@/tools/shared/tags'
1011

1112
/** Selects each causally independent persisted document field before request serialization. */
1213
export function selectKnowledgeDocumentWriteSecretProvenance(params: {
@@ -17,7 +18,9 @@ export function selectKnowledgeDocumentWriteSecretProvenance(params: {
1718
const name = typeof params.name === 'string' ? params.name.trim() : ''
1819
const content = typeof params.content === 'string' ? params.content.trim() : params.content
1920
const filename = inferDocumentFileInfo(name).filename
20-
const tags = parseDocumentTags(params.documentTags)
21+
const tags = parseKnowledgeDocumentTagProvenanceTargets(
22+
formatDocumentTagsForAPI(parseDocumentTags(params.documentTags)).documentTagsData
23+
)
2124

2225
return [
2326
{ key: knowledgeDocumentFilenameSelectionKey(0), value: filename },

0 commit comments

Comments
 (0)