Skip to content

Commit b5ed03e

Browse files
fix(tests): keep test sources out of file tabs, refresh after runs, reopen tests
- File-edit tool results mark a non-tab file `fileTab: false`, and the browser skips promoting it. - Idle test pages poll every 15s so runs started elsewhere appear; a Mothership run returns its tests as resource changes. - open_resource accepts test resources through an authorized read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EHGBgHrtePpi7KMrEfy41R
1 parent 83a1daf commit b5ed03e

9 files changed

Lines changed: 66 additions & 16 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isCurrentBrowserToolName } from '@sim/browser-protocol'
22
import { isTerminalToolName } from '@sim/terminal-protocol'
3+
import { toRecordOrNull } from '@sim/utils/object'
34
import { isDesktopApp } from '@/lib/desktop'
45
import {
56
MothershipStreamV1ToolPhase,
@@ -97,12 +98,12 @@ function runToolResultSideEffects(ctx: StreamLoopContext, node: ToolNode, replay
9798
invalidateResourceQueries(deps.queryClient, deps.workspaceId, resource.type, resource.id)
9899
}
99100

100-
if (!replay && (name === ApplyFileEdit.id || name === PrepareFileEdit.id) && isSuccess) {
101-
const out = output as Record<string, unknown> | undefined
102-
const editData =
103-
out && typeof out.data === 'object' && out.data !== null
104-
? (out.data as Record<string, unknown>)
105-
: undefined
101+
const isFileEdit = name === ApplyFileEdit.id || name === PrepareFileEdit.id
102+
const editData = isFileEdit ? toRecordOrNull(toRecordOrNull(output)?.data) : null
103+
/** A file another resource owns (a test's source) never opens as a file tab. */
104+
const editedFileIsTab = editData?.fileTab !== false
105+
106+
if (!replay && isFileEdit && isSuccess && editedFileIsTab) {
106107
const editedFileId =
107108
(typeof editData?.id === 'string' ? editData.id : undefined) ??
108109
deps.previewSessionRef.current?.fileId
@@ -139,7 +140,9 @@ function runToolResultSideEffects(ctx: StreamLoopContext, node: ToolNode, replay
139140
if (name === PrepareFileEdit.id) {
140141
deps.removePreviewSessionImmediate(node.id)
141142
}
142-
const fileResource = extractedResources.find((r) => r.type === 'file')
143+
const fileResource = editedFileIsTab
144+
? extractedResources.find((r) => r.type === 'file')
145+
: undefined
143146
if (fileResource) {
144147
deps.promoteFileResource(fileResource.id, fileResource.title)
145148
deps.onResourceEventRef.current?.(

‎apps/sim/hooks/queries/workflow-tests.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import {
1111
export type WorkflowTestVersion = 'draft' | 'deployed'
1212

1313
const WORKFLOW_TEST_STALE_TIME = 15_000
14-
/** While a run is in flight; 20 reads a minute stays inside the routes' per-user limit of 30. */
14+
/** While a run is in flight, so its progress and result appear as they land. */
1515
const WORKFLOW_TEST_RUNNING_REFETCH_INTERVAL = 3_000
16+
/** Otherwise, so a run started elsewhere (Sim, another person, the API) shows up. */
17+
const WORKFLOW_TEST_IDLE_REFETCH_INTERVAL = 15_000
1618

1719
export const workflowTestKeys = {
1820
all: ['workflow-tests'] as const,
@@ -43,7 +45,7 @@ export function useWorkflowTests(workspaceId: string, options?: { enabled?: bool
4345
refetchInterval: (query) =>
4446
query.state.data?.tests.some((test) => test.status === 'running')
4547
? WORKFLOW_TEST_RUNNING_REFETCH_INTERVAL
46-
: false,
48+
: WORKFLOW_TEST_IDLE_REFETCH_INTERVAL,
4749
})
4850
}
4951

@@ -62,7 +64,7 @@ export function useWorkflowTest(workspaceId: string, name: string) {
6264
refetchInterval: (query) =>
6365
query.state.data?.latestRun?.status === 'running'
6466
? WORKFLOW_TEST_RUNNING_REFETCH_INTERVAL
65-
: false,
67+
: WORKFLOW_TEST_IDLE_REFETCH_INTERVAL,
6668
})
6769
}
6870

‎apps/sim/lib/api/contracts/mothership-resource-tools.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const openResourceInputSchema = z.strictObject({
1010
resources: z
1111
.array(
1212
z.strictObject({
13-
type: z.enum(['workflow', 'table', 'knowledgebase', 'file', 'dashboard', 'log']),
13+
type: z.enum(['workflow', 'table', 'knowledgebase', 'file', 'dashboard', 'log', 'test']),
1414
id: z.string().trim().min(1),
1515
viewId: z
1616
.string()
@@ -26,7 +26,7 @@ export const openResourceInputSchema = z.strictObject({
2626
export const openResourceOutputSchema = z.object({
2727
resources: z.array(
2828
z.object({
29-
type: z.enum(['workflow', 'table', 'knowledgebase', 'file', 'dashboard', 'log']),
29+
type: z.enum(['workflow', 'table', 'knowledgebase', 'file', 'dashboard', 'log', 'test']),
3030
id: z.string(),
3131
title: z.string(),
3232
viewId: z.string().optional(),

‎apps/sim/lib/mothership/resources/file-tabs.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { db } from '@sim/db'
22
import { workspaceFiles } from '@sim/db/schema'
3+
import { toStringOrNull } from '@sim/utils/coerce'
34
import { and, inArray, notInArray } from 'drizzle-orm'
5+
import type { BaseServerTool } from '@/lib/mothership/tools/server/base-tool'
46

57
/** Row contexts that open as a file tab: workspace files and chat uploads. */
68
const FILE_TAB_CONTEXTS = ['workspace', 'mothership']
@@ -23,3 +25,21 @@ export async function findNonTabFileIds(fileIds: string[]): Promise<Set<string>>
2325
)
2426
return new Set(rows.map((row) => row.id))
2527
}
28+
29+
/**
30+
* Marks a file-editing tool's result `fileTab: false` when the file it touched is not a file tab,
31+
* so the browser, which promotes edited files to tabs from the result, leaves it alone.
32+
*/
33+
export function withFileTabFlag<TArgs, TResult extends { data?: Record<string, unknown> }>(
34+
tool: BaseServerTool<TArgs, TResult>
35+
): BaseServerTool<TArgs, TResult> {
36+
return {
37+
...tool,
38+
async execute(args, context) {
39+
const result = await tool.execute(args, context)
40+
const id = toStringOrNull(result.data?.id)
41+
if (id === null || !(await findNonTabFileIds([id])).has(id)) return result
42+
return { ...result, data: { ...result.data, fileTab: false } }
43+
},
44+
}
45+
}

‎apps/sim/lib/mothership/tools/server/open-resource.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { executeDashboardUseCase } from '@/lib/mothership/application/execute-da
1515
import { executeCopilotFileUseCase } from '@/lib/mothership/application/execute-file-use-case'
1616
import { executeCopilotKnowledgeUseCase } from '@/lib/mothership/application/execute-knowledge-use-case'
1717
import { executeCopilotTableUseCase } from '@/lib/mothership/application/execute-table-use-case'
18+
import { executeWorkflowTestUseCase } from '@/lib/mothership/application/execute-workflow-test-use-case'
1819
import { executeCopilotWorkflowUseCase } from '@/lib/mothership/application/execute-workflow-use-case'
1920
import {
2021
COPILOT_APPLICATION_DELEGATION_TTL_MS,
@@ -26,6 +27,7 @@ import {
2627
} from '@/lib/mothership/tools/server/base-tool'
2728
import { readTableDefinitionUseCase } from '@/lib/table/application/tables'
2829
import { readTableViewUseCase } from '@/lib/table/application/views'
30+
import { getWorkflowTestDetail } from '@/lib/workflow-tests/application/tests'
2931
import { readWorkflowMetadata } from '@/lib/workflows/application/read-workflow'
3032
import { readWorkspaceFileMetadata } from '@/lib/workspace-files/application/read-workspace-file-metadata'
3133

@@ -96,6 +98,14 @@ export const openResourceServerTool: BaseServerTool<OpenResourceInput, OpenResou
9698
resources.push({ ...base, title: dashboard.name })
9799
break
98100
}
101+
case 'test': {
102+
const { test } = await executeWorkflowTestUseCase(context, getWorkflowTestDetail, {
103+
workspaceId,
104+
name: resource.id,
105+
})
106+
resources.push({ ...base, title: test.title })
107+
break
108+
}
99109
case 'file': {
100110
const { file } = await executeCopilotFileUseCase(
101111
context,

‎apps/sim/lib/mothership/tools/server/router.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
GenerateImage,
1010
GenerateVideo,
1111
} from '@/lib/mothership/generated/tool-catalog-v1'
12+
import { withFileTabFlag } from '@/lib/mothership/resources/file-tabs'
1213
import { copilotToolCanWrite } from '@/lib/mothership/tools/permissions'
1314
import {
1415
assertServerToolNotAborted,
@@ -74,8 +75,8 @@ const baseServerToolRegistry: Record<string, BaseServerTool> = {
7475
[readDocumentServerTool.name]: readDocumentServerTool,
7576
// The streamed file-writing pair: prepare opens the write (live preview),
7677
// apply continues it. The preview machinery keys off these exact names.
77-
[workspaceFileServerTool.name]: workspaceFileServerTool,
78-
[editContentServerTool.name]: editContentServerTool,
78+
[workspaceFileServerTool.name]: withFileTabFlag(workspaceFileServerTool),
79+
[editContentServerTool.name]: withFileTabFlag(editContentServerTool),
7980
[generateImageServerTool.name]: generateImageServerTool,
8081
[generateVideoServerTool.name]: generateVideoServerTool,
8182
[generateAudioServerTool.name]: generateAudioServerTool,

‎apps/sim/lib/mothership/tools/server/tests.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,12 @@ export const testsServerTool: BaseServerTool = {
143143
}
144144
)
145145
if (current.every((run) => run.status !== 'running')) {
146+
const resources: ResourceChange[] = current.map((run) => ({
147+
op: 'upsert',
148+
resource: { type: 'test', workspaceId, id: run.name, title: run.title },
149+
}))
146150
return {
151+
resources,
147152
version: input.version,
148153
results: current.map((run) => ({
149154
name: run.name,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ export const readWorkflowTestRuns = defineAuthorizedWorkspaceUseCase({
390390
runs: rows.map((row) => ({
391391
...presentRun(row, row.testSourceHash, facts),
392392
name: row.testName,
393+
title: row.testTitle,
393394
report: row.report as TestReport | null,
394395
})),
395396
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,16 +437,24 @@ export async function getWorkflowTestRunById(
437437
export async function listWorkflowTestRunsById(
438438
runIds: string[],
439439
workspaceId: string
440-
): Promise<Array<WorkflowTestRunRow & { testName: string; testSourceHash: string }>> {
440+
): Promise<
441+
Array<WorkflowTestRunRow & { testName: string; testTitle: string; testSourceHash: string }>
442+
> {
441443
if (runIds.length === 0) return []
442444
const rows = await db
443445
.select({
444446
run: workflowTestRun,
445447
testName: workflowTest.name,
448+
testTitle: workflowTest.title,
446449
testSourceHash: workflowTest.sourceHash,
447450
})
448451
.from(workflowTestRun)
449452
.innerJoin(workflowTest, eq(workflowTest.id, workflowTestRun.testId))
450453
.where(and(inArray(workflowTestRun.id, runIds), eq(workflowTestRun.workspaceId, workspaceId)))
451-
return rows.map(({ run, testName, testSourceHash }) => ({ ...run, testName, testSourceHash }))
454+
return rows.map(({ run, testName, testTitle, testSourceHash }) => ({
455+
...run,
456+
testName,
457+
testTitle,
458+
testSourceHash,
459+
}))
452460
}

0 commit comments

Comments
 (0)