Skip to content

Commit 15dd811

Browse files
fix(tests): reserve the run name, hold Run for unsaved edits, cancel judges on close
- A test named "run" collided with the static run endpoint, so its detail page got a 405; the name is now reserved. - Run is disabled while the open editor holds edits the server has not saved (including a refused save), so a run never uses the previous source. - toMatchRubric model calls are aborted when the sandbox run ends, so a stopped test no longer keeps calling or billing the judge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EHGBgHrtePpi7KMrEfy41R
1 parent ca247e0 commit 15dd811

6 files changed

Lines changed: 31 additions & 8 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/tests/components/test-detail.tsx‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useState } from 'react'
3+
import { useEffect, useState } from 'react'
44
import { toast } from '@sim/emcn'
55
import { Columns2, Eye, Pencil, PlayOutline, ShieldCheck } from '@sim/emcn/icons'
66
import { getErrorMessage } from '@sim/utils/errors'
@@ -19,6 +19,7 @@ import {
1919
useWorkflowTest,
2020
} from '@/hooks/queries/workflow-tests'
2121
import { useAddressedWorkspaceFileRecord } from '@/hooks/queries/workspace-files'
22+
import { testRunSelectionKey, useTestRunSelectionStore } from '@/stores/workflow-tests/store'
2223

2324
interface TestTarget {
2425
workspaceId: string
@@ -74,12 +75,15 @@ interface TestRunActionInput extends TestTarget {
7475
export function useTestRunAction({ workspaceId, name, running }: TestRunActionInput) {
7576
const canEdit = useUserPermissionsContext().canEdit === true
7677
const runTests = useRunWorkflowTests(workspaceId)
78+
const unsaved = useTestRunSelectionStore(
79+
(state) => state.unsavedTests[testRunSelectionKey(workspaceId, name)] === true
80+
)
7781
const busy = running || runTests.isPending
7882
return {
7983
id: 'run',
8084
icon: PlayOutline,
81-
text: busy ? 'Running…' : 'Run',
82-
disabled: !canEdit || busy,
85+
text: busy ? 'Running…' : unsaved ? 'Unsaved' : 'Run',
86+
disabled: !canEdit || busy || unsaved,
8387
onSelect: () =>
8488
runTests.mutate(
8589
{ version: VERSION, names: [name] },
@@ -107,6 +111,9 @@ export function TestView({ workspaceId, name, previewMode }: TestViewProps) {
107111
const query = useWorkflowTest(workspaceId, name)
108112
const refreshTest = useRefreshWorkflowTest(workspaceId, name)
109113
const [saveError, setSaveError] = useState<string | null>(null)
114+
const setUnsaved = useTestRunSelectionStore((state) => state.setUnsaved)
115+
const testKey = testRunSelectionKey(workspaceId, name)
116+
useEffect(() => () => setUnsaved(testKey, false), [setUnsaved, testKey])
110117
const detail = query.data
111118
const file = useAddressedWorkspaceFileRecord(workspaceId, detail?.test.fileId ?? '', {
112119
enabled: Boolean(detail),
@@ -142,6 +149,7 @@ export function TestView({ workspaceId, name, previewMode }: TestViewProps) {
142149
preview={<TestDashboard workspaceId={workspaceId} name={name} detail={detail} />}
143150
sourceSide='end'
144151
enableFind
152+
onDirtyChange={(dirty) => setUnsaved(testKey, dirty)}
145153
onSaveError={(saveFailure) =>
146154
setSaveError(getErrorMessage(saveFailure, 'The test file was not saved'))
147155
}

‎apps/sim/lib/api/contracts/mothership-tests.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { z } from 'zod'
22

33
const scope = z.object({ workspaceId: z.string().min(1).max(100).optional() })
4-
/** A test file name: `tests/<name>.test.js`. */
5-
export const TEST_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,79}$/
4+
/** A test file name: `tests/<name>.test.js`. `run` is taken by the run endpoint beside the names. */
5+
export const TEST_NAME_PATTERN = /^(?!run$)[a-z0-9][a-z0-9-]{0,79}$/
66

77
const nameSchema = z
88
.string()
99
.trim()
1010
.regex(
1111
TEST_NAME_PATTERN,
12-
'Use a lowercase name of letters, numbers, and dashes, like billing-routing'
12+
'Use a lowercase name of letters, numbers, and dashes, like billing-routing (not "run")'
1313
)
1414
.describe('Test file name: tests/<name>.test.js, lowercase letters, numbers, and dashes.')
1515
const versionSchema = z

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface JudgeRubricInput {
3232
actorUserId: string
3333
value: string
3434
rubric: string
35+
abortSignal: AbortSignal
3536
}
3637

3738
/** One pass/fail model call for `toMatchRubric`, billed to the workspace like a wand call. */
@@ -65,6 +66,7 @@ export async function judgeRubric(input: JudgeRubricInput): Promise<JudgeVerdict
6566
},
6667
temperature: 0,
6768
workspaceId: input.workspaceId,
69+
abortSignal: input.abortSignal,
6870
})
6971
if (!('content' in response) || typeof response.content !== 'string') {
7072
throw new Error('The judge did not return a verdict')

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function assertTestName(name: string): void {
2424
if (!TEST_NAME_PATTERN.test(name)) {
2525
throw new OrchestrationError(
2626
'validation',
27-
`"${testFilePath(name)}" is not a valid test file. Use tests/<name>.test.js with a lowercase name of letters, numbers, and dashes, like tests/billing-routing.test.js`
27+
`"${testFilePath(name)}" is not a valid test file. Use tests/<name>.test.js with a lowercase name of letters, numbers, and dashes other than "run", like tests/billing-routing.test.js`
2828
)
2929
}
3030
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,8 @@ export class WorkflowTestSession {
524524
readonly enteredWorkflowIds = new Set<string>()
525525
/** Set when the sandbox run ends; a start still awaiting its lookups then starts nothing. */
526526
private closed = false
527+
/** Stops `toMatchRubric` model calls still in flight when the sandbox run ends. */
528+
private readonly judgeAbort = new AbortController()
527529

528530
constructor(readonly config: WorkflowTestSessionConfig) {}
529531

@@ -586,6 +588,7 @@ export class WorkflowTestSession {
586588

587589
close(): void {
588590
this.closed = true
591+
this.judgeAbort.abort(new Error('The test run ended'))
589592
this.cancel([...this.runs.keys()])
590593
}
591594

@@ -595,6 +598,7 @@ export class WorkflowTestSession {
595598
actorUserId: requirePrincipalSubjectUserId(this.config.principal),
596599
value: args.value,
597600
rubric: args.rubric,
601+
abortSignal: this.judgeAbort.signal,
598602
})
599603
}
600604

‎apps/sim/stores/workflow-tests/store.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,29 @@ import { devtools } from 'zustand/middleware'
88
*/
99
interface TestRunSelectionState {
1010
selectedRunIds: Record<string, string>
11+
/** Tests whose open editor holds edits the server has not saved; Run waits for them. */
12+
unsavedTests: Record<string, true>
1113
selectRun: (testKey: string, runId: string | null) => void
14+
setUnsaved: (testKey: string, unsaved: boolean) => void
1215
reset: () => void
1316
}
1417

1518
export const useTestRunSelectionStore = create<TestRunSelectionState>()(
1619
devtools(
1720
(set) => ({
1821
selectedRunIds: {},
22+
unsavedTests: {},
1923
selectRun: (testKey, runId) =>
2024
set((state) => {
2125
const { [testKey]: _previous, ...rest } = state.selectedRunIds
2226
return { selectedRunIds: runId === null ? rest : { ...rest, [testKey]: runId } }
2327
}),
24-
reset: () => set({ selectedRunIds: {} }),
28+
setUnsaved: (testKey, unsaved) =>
29+
set((state) => {
30+
const { [testKey]: _previous, ...rest } = state.unsavedTests
31+
return { unsavedTests: unsaved ? { ...rest, [testKey]: true } : rest }
32+
}),
33+
reset: () => set({ selectedRunIds: {}, unsavedTests: {} }),
2534
}),
2635
{ name: 'test-run-selection' }
2736
)

0 commit comments

Comments
 (0)