@@ -16,6 +16,7 @@ import {
1616} from '@sim/db/schema'
1717import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures'
1818import { sha256Hex } from '@sim/security/hash'
19+ import { createExecutorPrincipal } from '@sim/testing/factories/principal.factory'
1920import { generateId } from '@sim/utils/id'
2021import { isRecordLike } from '@sim/utils/object'
2122import { eq , inArray } from 'drizzle-orm'
@@ -33,6 +34,12 @@ vi.mock('@/lib/uploads/core/setup.server', () => ({
3334import { observeContentDelivery } from '@/lib/core/application'
3435import { encryptSecret } from '@/lib/core/security/encryption'
3536import type { DurableSecretProvenance } from '@/lib/execution/durable-secret-provenance'
37+ import {
38+ PRIVATE_TOOL_METADATA_REQUEST_HEADER ,
39+ RESOLVED_SECRET_PROVENANCE_FIELD ,
40+ RESOLVED_SECRET_PROVENANCE_METADATA_V1 ,
41+ } from '@/lib/execution/private-tool-metadata'
42+ import { listDocumentsOperation } from '@/lib/internal/knowledge/operations'
3643import {
3744 createKnowledgeAclFixtureIds ,
3845 seedKnowledgeAclFixture ,
@@ -51,6 +58,7 @@ import {
5158 readDetailedKnowledgeTagUsage ,
5259 readKnowledgeTagUsage ,
5360} from '@/lib/knowledge/application/tags'
61+ import * as documentService from '@/lib/knowledge/documents/service'
5462import {
5563 bindKnowledgeDocumentFieldSecretProvenance ,
5664 classifyKnowledgeDocumentMetadata ,
@@ -63,6 +71,7 @@ import { embeddingVectorValues } from '@/lib/knowledge/vector-columns'
6371import { createCopilotChatPrincipal } from '@/lib/mothership/auth/application-delegation'
6472import { createRegistryContentGate } from '@/lib/mothership/content-delivery/registry-gate'
6573import { uploadFile } from '@/lib/uploads/core/storage-service'
74+ import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection'
6675import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
6776
6877const SECRET = 'fixture-knowledge-token-41875'
@@ -95,6 +104,7 @@ async function seedDocument(
95104 content ?: string
96105 provenance ?: DurableSecretProvenance
97106 original ?: boolean
107+ contentHash ?: string | null
98108 } = { }
99109) {
100110 const content = options . content ?? 'page public content'
@@ -105,6 +115,7 @@ async function seedDocument(
105115 id,
106116 knowledgeBaseId : ids . knowledgeBaseId ,
107117 filename : options . name ?? 'page.txt' ,
118+ contentHash : options . contentHash ?? null ,
108119 fileUrl :
109120 options . original === false
110121 ? ''
@@ -229,6 +240,81 @@ afterAll(async () => {
229240} )
230241
231242describe ( 'knowledge content delivery' , ( ) => {
243+ it . each ( [ null , sha256Hex ( 'stable connector source' ) ] ) (
244+ 'keeps workflow list provenance complete for an unchanged document with content hash %s' ,
245+ async ( contentHash ) => {
246+ const doc = await seedDocument ( {
247+ name : `${ SECRET } .txt` ,
248+ contentHash,
249+ provenance : bindKnowledgeDocumentFieldSecretProvenance ( exact , 'filename' , `${ SECRET } .txt` ) ,
250+ } )
251+ const context = {
252+ principal : createExecutorPrincipal ( {
253+ subjectUserId : ids . aliceId ,
254+ workspaceId : ids . workspaceId ,
255+ audience : KNOWLEDGE_DELEGATION_AUDIENCE ,
256+ delegationContext : { kind : 'workflow_execution' as const , workflowId : generateId ( ) } ,
257+ } ) ,
258+ headers : new Headers ( {
259+ [ PRIVATE_TOOL_METADATA_REQUEST_HEADER ] : RESOLVED_SECRET_PROVENANCE_METADATA_V1 ,
260+ } ) ,
261+ }
262+ const result = await listDocumentsOperation (
263+ ids . knowledgeBaseId ,
264+ { limit : 50 , offset : 0 } ,
265+ context
266+ )
267+ const provenance = result . bodyFields ?. [ RESOLVED_SECRET_PROVENANCE_FIELD ]
268+ expect ( provenance ) . toMatchObject ( { complete : true } )
269+ expect ( result . body ) . toMatchObject ( {
270+ data : { documents : [ { id : doc . id , filename : `${ SECRET } .txt` } ] } ,
271+ } )
272+ expect ( JSON . stringify ( result . body ) ) . not . toContain ( 'contentHash' )
273+ const registry = new ResolvedSecretTraceRegistry ( [ ] , {
274+ userId : ids . aliceId ,
275+ workspaceId : ids . workspaceId ,
276+ } )
277+ expect ( await registry . importProvenance ( provenance , { trusted : true } ) ) . toBe ( true )
278+ const projected = projectResolvedSecretModelContent ( result . body , registry )
279+ expect ( projected . safe ) . toBe ( true )
280+ expect ( JSON . stringify ( projected ) ) . toContain ( '{{FIXTURE_TOKEN}}.txt' )
281+ expect ( JSON . stringify ( projected ) ) . not . toContain ( SECRET )
282+
283+ const read = documentService . getDocuments
284+ const replace = vi
285+ . spyOn ( documentService , 'getDocuments' )
286+ . mockImplementation ( async ( ...args ) => {
287+ const snapshot = await read ( ...args )
288+ await db . transaction ( async ( tx ) => {
289+ const [ replacement ] = await tx
290+ . update ( document )
291+ . set ( { contentHash : sha256Hex ( 'replacement connector source' ) } )
292+ . where ( eq ( document . id , doc . id ) )
293+ . returning ( )
294+ await replaceKnowledgeDocumentSecretProvenanceInTx (
295+ tx ,
296+ doc . id ,
297+ createKnowledgeDocumentSourceValue ( replacement ) ,
298+ bindKnowledgeDocumentFieldSecretProvenance ( exact , 'filename' , `${ SECRET } .txt` )
299+ )
300+ } )
301+ return snapshot
302+ } )
303+ try {
304+ const changed = await listDocumentsOperation (
305+ ids . knowledgeBaseId ,
306+ { limit : 50 , offset : 0 } ,
307+ context
308+ )
309+ expect ( changed . bodyFields ?. [ RESOLVED_SECRET_PROVENANCE_FIELD ] ) . toMatchObject ( {
310+ complete : false ,
311+ } )
312+ } finally {
313+ replace . mockRestore ( )
314+ }
315+ }
316+ )
317+
232318 it ( 'classifies a mixed metadata page without materializing multiple inline originals together' , async ( ) => {
233319 const content = 'inline source content ' . repeat ( 8192 )
234320 const legacy = await seedDocument ( { content } )
0 commit comments