Skip to content

Commit 2ab0a2c

Browse files
fix(tests): take a draft's timestamp from before the run loads it
Reading it after the load let an edit landing in between look tested; the snapshot from before the load can only err toward stale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9c939cb commit 2ab0a2c

3 files changed

Lines changed: 33 additions & 32 deletions

File tree

‎apps/sim/lib/workflow-tests/repository.ts‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export interface RanAgainstEntry {
142142
deploymentVersionId: string | null
143143
/** A `runWorkflow()` execution of it, whose snapshot is the workflow as it ran; null for a child. */
144144
executionId: string | null
145-
/** For a draft, the workflow's `updatedAt` when the run loaded it; a later edit makes the run stale. */
145+
/** For a draft, the workflow's `updatedAt` before the run loaded it; a later edit makes the run stale. */
146146
draftUpdatedAt: string | null
147147
}
148148

@@ -151,15 +151,13 @@ export type EnteredWorkflow = Omit<RanAgainstEntry, 'executionId'>
151151

152152
type ExecutedWorkflow = Omit<RanAgainstEntry, 'draftUpdatedAt'>
153153

154-
/** When a draft last changed; a test run reads it as it loads the draft. */
155-
export async function readDraftUpdatedAt(workflowId: string): Promise<string> {
156-
const [row] = await db
157-
.select({ updatedAt: workflow.updatedAt })
154+
/** When each of the workspace's drafts last changed; a test run reads them before it loads any. */
155+
export async function readDraftTimestamps(workspaceId: string): Promise<Map<string, string>> {
156+
const rows = await db
157+
.select({ id: workflow.id, updatedAt: workflow.updatedAt })
158158
.from(workflow)
159-
.where(eq(workflow.id, workflowId))
160-
.limit(1)
161-
if (!row) throw new Error(`Workflow ${workflowId} not found`)
162-
return row.updatedAt.toISOString()
159+
.where(and(eq(workflow.workspaceId, workspaceId), isNull(workflow.archivedAt)))
160+
return new Map(rows.map((row) => [row.id, row.updatedAt.toISOString()]))
163161
}
164162

165163
const enteredKey = (entry: { workflowId: string; deploymentVersionId: string | null }) =>

‎apps/sim/lib/workflow-tests/run-test-file.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export async function runWorkflowTestFile(
5151
)
5252
return {
5353
report: testReportSchema.parse(JSON.parse(Buffer.from(bytes).toString('utf-8'))),
54-
entered: await session.enteredWorkflows(),
54+
entered: session.enteredWorkflows(),
5555
}
5656
} finally {
5757
closeWorkflowTestSession(requestId)

‎apps/sim/lib/workflow-tests/session.ts‎

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
type ParkedMockCall,
1616
} from '@/lib/workflow-tests/mock-channel'
1717
import type { TestCaseStatus, TestEvent, TestTarget } from '@/lib/workflow-tests/protocol'
18-
import { type EnteredWorkflow, readDraftUpdatedAt } from '@/lib/workflow-tests/repository'
18+
import { type EnteredWorkflow, readDraftTimestamps } from '@/lib/workflow-tests/repository'
1919
import {
2020
runWorkflowForTest,
2121
type WorkflowTestRunResult,
@@ -251,10 +251,7 @@ class RunMatcher implements MockMatcher {
251251
constructor(
252252
targets: TestTarget[],
253253
private readonly loadChildBlocks: (workflowId: string) => Promise<Record<string, BlockState>>,
254-
private readonly onEnter: (
255-
workflowId: string,
256-
deploymentVersionId: string | null
257-
) => Promise<void>
254+
private readonly onEnter: (workflowId: string, deploymentVersionId: string | null) => void
258255
) {
259256
this.targets = [...targets].sort(
260257
(a, b) => Number(a.workflow === null) - Number(b.workflow === null)
@@ -276,7 +273,7 @@ class RunMatcher implements MockMatcher {
276273
this.entered = true
277274
this.outputRegistry = resolvedSecretTraceRegistry
278275
}
279-
await this.onEnter(workflowId, deploymentVersionId)
276+
this.onEnter(workflowId, deploymentVersionId)
280277
const workflowName = await this.workflowName(workflowId)
281278
for (const block of blocks) this.blocks.set(block.id, block)
282279
await this.learnWorkflow(
@@ -527,7 +524,7 @@ class TestWorkflowRun {
527524
export class WorkflowTestSession {
528525
private readonly runs = new Map<string, TestWorkflowRun>()
529526
/** Every workflow version this file's runs executed, children included, as each loaded it. */
530-
private readonly enteredVersions = new Map<string, Promise<EnteredWorkflow>>()
527+
private readonly enteredVersions = new Map<string, EnteredWorkflow>()
531528
/** Set when the sandbox run ends; a start still awaiting its lookups then starts nothing. */
532529
private closed = false
533530
/** Stops `toMatchRubric` model calls still in flight when the sandbox run ends. */
@@ -546,9 +543,10 @@ export class WorkflowTestSession {
546543
const blocks = await loadWorkflowBlocks(workflowId, workspaceId, version)
547544
const triggerBlockId = chooseTrigger(args.workflow, blocks, args.trigger)
548545
const targets = await resolveToolTargets(workspaceId, args.targets)
546+
const draftsBeforeLoad = version === 'draft' ? await readDraftTimestamps(workspaceId) : null
549547
if (this.closed) throw new Error('This workflow test run has ended')
550548
const matcher = new RunMatcher(targets, loadChildBlockNames, (id, deploymentVersionId) =>
551-
this.recordEntered(id, deploymentVersionId)
549+
this.recordEntered(id, deploymentVersionId, draftsBeforeLoad)
552550
)
553551
const channel = createMockChannel(matcher)
554552
const abort = new AbortController()
@@ -592,23 +590,28 @@ export class WorkflowTestSession {
592590
}
593591
}
594592

595-
/** The first entry of a draft keeps its timestamp, so an edit between runs leaves the file stale. */
596-
private async recordEntered(workflowId: string, deploymentVersionId: string | null) {
593+
/**
594+
* A draft records its timestamp from before the run loaded it, so any edit after that point,
595+
* including one the load may have missed, leaves the run stale. The first run to enter a
596+
* workflow keeps its entry, the earliest timestamp.
597+
*/
598+
private recordEntered(
599+
workflowId: string,
600+
deploymentVersionId: string | null,
601+
draftsBeforeLoad: Map<string, string> | null
602+
): void {
597603
const key = `${workflowId}:${deploymentVersionId ?? 'draft'}`
598-
let entry = this.enteredVersions.get(key)
599-
if (!entry) {
600-
entry = (async () => ({
601-
workflowId,
602-
deploymentVersionId,
603-
draftUpdatedAt: deploymentVersionId === null ? await readDraftUpdatedAt(workflowId) : null,
604-
}))()
605-
this.enteredVersions.set(key, entry)
606-
}
607-
await entry
604+
if (this.enteredVersions.has(key)) return
605+
this.enteredVersions.set(key, {
606+
workflowId,
607+
deploymentVersionId,
608+
draftUpdatedAt:
609+
deploymentVersionId === null ? (draftsBeforeLoad?.get(workflowId) ?? null) : null,
610+
})
608611
}
609612

610-
enteredWorkflows(): Promise<EnteredWorkflow[]> {
611-
return Promise.all(this.enteredVersions.values())
613+
enteredWorkflows(): EnteredWorkflow[] {
614+
return [...this.enteredVersions.values()]
612615
}
613616

614617
close(): void {

0 commit comments

Comments
 (0)