Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions apps/sim/executor/execution/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,81 @@ describe('ExecutionEngine', () => {
expect(result.executionState?.finalOutputResolvedSecretTraceProvenance?.entries).toEqual([])
})

/**
* A subflow sentinel stores its aggregate without provenance, and a loop that ran no
* iterations has none to merge. Treating that absence as a verdict marked the run
* unvouchable, which withheld the user's own final output from their execution log on every
* later view.
*/
it('derives final output provenance when the last block state carries none', async () => {
const node = createMockNode('loop-1', 'loop')
const registry = new ResolvedSecretTraceRegistry([
{ name: 'TOKEN', plaintext: 'secret-value-1234', encryptedValue: 'ciphertext' },
])
registry.recordResolved('TOKEN', 'secret-value-1234')
const context = createMockContext({
decisions: { router: new Map(), condition: new Map() },
resolvedSecretTraceRegistry: registry,
})
const nodeOrchestrator = createMockNodeOrchestrator()
vi.mocked(nodeOrchestrator.executeNode).mockResolvedValue({
nodeId: node.id,
output: { results: ['secret-value-1234'] },
isFinalOutput: true,
})
vi.mocked(nodeOrchestrator.handleNodeCompletion).mockImplementation(
(_ctx, nodeId, output) => {
context.blockStates.set(nodeId, { output, executed: true, executionTime: 1 })
}
)

const engine = new ExecutionEngine(
context,
createMockDAG([node]),
createMockEdgeManager(),
nodeOrchestrator
)
const result = await engine.run(node.id)

const provenance = result.executionState?.finalOutputResolvedSecretTraceProvenance
expect(provenance?.complete).toBe(true)
expect(provenance?.entries).toEqual([{ name: 'TOKEN', encryptedValue: 'ciphertext' }])
})

/** Deriving must not weaken the guarantee: a latched registry still exports incomplete. */
it('keeps the final output envelope incomplete when the registry latched', async () => {
const node = createMockNode('loop-1', 'loop')
const registry = new ResolvedSecretTraceRegistry([
{ name: 'TOKEN', plaintext: 'secret-value-1234', encryptedValue: 'ciphertext' },
])
registry.markIncomplete('unspecified')
const context = createMockContext({
decisions: { router: new Map(), condition: new Map() },
resolvedSecretTraceRegistry: registry,
})
const nodeOrchestrator = createMockNodeOrchestrator()
vi.mocked(nodeOrchestrator.executeNode).mockResolvedValue({
nodeId: node.id,
output: { results: ['secret-value-1234'] },
isFinalOutput: true,
})
vi.mocked(nodeOrchestrator.handleNodeCompletion).mockImplementation(
(_ctx, nodeId, output) => {
context.blockStates.set(nodeId, { output, executed: true, executionTime: 1 })
}
)

const engine = new ExecutionEngine(
context,
createMockDAG([node]),
createMockEdgeManager(),
nodeOrchestrator
)
const result = await engine.run(node.id)

expect(result.executionState?.finalOutputResolvedSecretTraceProvenance?.complete).toBe(false)
})

it('should not fall back to starter blocks for terminal resume snapshots', async () => {
const startNode = createMockNode('start', 'starter')
const dag = createMockDAG([startNode])
Expand Down
36 changes: 21 additions & 15 deletions apps/sim/executor/execution/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,25 +505,31 @@ export class ExecutionEngine {
this.context.finalOutputResolvedSecretTraceProvenance = state.resolvedSecretTraceProvenance
return
}
/**
* A block state without provenance is an absence of a shortcut, not a verdict. Several state
* writers legitimately store an output without one — a subflow sentinel aggregating iteration
* results is the common case, and a loop that ran no iterations has nothing to merge — so
* stamping an incomplete envelope here declared the run unvouchable whenever the last block
* was one of them. Every other consumer of a provenance-less block state falls back to the run
* registry; deriving does the same, against the value actually being described.
*/
this.deriveFinalOutputProvenance()
}

if (this.context.resolvedSecretTraceRegistry) {
this.context.finalOutputResolvedSecretTraceProvenance = {
version: 1,
complete: false,
entries: [],
}
}
/**
* Derives the final-output envelope from the run registry. Fails closed on its own terms: a
* latched registry exports an incomplete envelope, which is the genuinely unvouchable case.
*/
private deriveFinalOutputProvenance(): void {
const registry = this.context.resolvedSecretTraceRegistry
if (!registry) return
this.context.finalOutputResolvedSecretTraceProvenance =
registry.exportCommittedProvenanceForValue(this.finalOutput)
}

private ensureFinalOutputProvenance(): void {
if (
Object.hasOwn(this.context, 'finalOutputResolvedSecretTraceProvenance') ||
!this.context.resolvedSecretTraceRegistry
) {
return
}
this.context.finalOutputResolvedSecretTraceProvenance =
this.context.resolvedSecretTraceRegistry.exportCommittedProvenanceForValue(this.finalOutput)
if (Object.hasOwn(this.context, 'finalOutputResolvedSecretTraceProvenance')) return
this.deriveFinalOutputProvenance()
}

private buildPausedResult(startTime: number): ExecutionResult {
Expand Down
Loading