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
12 changes: 2 additions & 10 deletions apps/sim/app/api/v2/workflows/[workflowId]/deploy/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
v2RouteMocks,
} from '@sim/testing'
import { createPersonalApiKeyPrincipal } from '@sim/testing/factories/principal.factory'
import { createEmptyWorkflowLintReport } from '@sim/testing/factories/workflow-lint.factory'
import { createRouteContext } from '@sim/testing/helpers/http'
import { auditMock } from '@sim/testing/mocks/audit.mock'
import { createMockRequest } from '@sim/testing/mocks/request.mock'
Expand Down Expand Up @@ -43,13 +44,7 @@ const sideEffectWarning =
'Deployment activation completed, and post-activation notifications are queued.'

const unquotedRowJson = {
sources: [],
sinks: [],
orphanBlocks: [],
emptyOutgoingPorts: [],
invalidBranchPorts: [],
invalidConnectionTargets: [],
fieldIssues: [],
...createEmptyWorkflowLintReport(),
unresolvedReferences: [
{
blockId: 'insert',
Expand All @@ -60,9 +55,6 @@ const unquotedRowJson = {
reason: 'unquoted-json-string: quote it',
},
],
tableFieldIssues: [],
manualRunIssue: null,
notes: [],
}

describe('POST /api/v2/workflows/[workflowId]/deploy', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
v2RateLimiterModuleMock,
v2RouteMocks,
} from '@sim/testing'
import { createEmptyWorkflowLintReport } from '@sim/testing/factories/workflow-lint.factory'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'

Expand Down Expand Up @@ -41,20 +42,7 @@ const DROPPED_INPUT = {
error: 'Invalid credential ID',
}

/** An empty report, with every field the contract publishes. */
const LINT = {
sources: [],
sinks: [],
orphanBlocks: [],
emptyOutgoingPorts: [],
invalidBranchPorts: [],
invalidConnectionTargets: [],
fieldIssues: [],
unresolvedReferences: [],
tableFieldIssues: [],
manualRunIssue: null,
notes: [],
}
const LINT = createEmptyWorkflowLintReport()

const auth = {
principal: { kind: 'personal_api_key' as const, userId: 'user-1', keyId: 'personal-key-1' },
Expand Down
15 changes: 2 additions & 13 deletions apps/sim/app/api/v2/workflows/[workflowId]/state/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
v2RateLimiterModuleMock,
v2RouteMocks,
} from '@sim/testing'
import { createEmptyWorkflowLintReport } from '@sim/testing/factories/workflow-lint.factory'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'

Expand Down Expand Up @@ -63,19 +64,7 @@ function putRequest(body: unknown) {
})
}

const EMPTY_LINT = {
sources: [],
sinks: [],
orphanBlocks: [],
emptyOutgoingPorts: [],
invalidBranchPorts: [],
invalidConnectionTargets: [],
fieldIssues: [],
unresolvedReferences: [],
tableFieldIssues: [],
manualRunIssue: null,
notes: [],
}
const EMPTY_LINT = createEmptyWorkflowLintReport()

describe('/api/v2/workflows/[workflowId]/state', () => {
beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1076,7 +1076,7 @@ export function useWorkflowExecution() {
selectedOutputs = chatStore.getState().getSelectedWorkflowOutput(activeWorkflowId)
}

let startBlockId: string | undefined
let startBlockId: string
let finalWorkflowInput = workflowInput

if (isExecutingFromChat) {
Expand Down Expand Up @@ -1128,18 +1128,6 @@ export function useWorkflowExecution() {
}
}

if (!startBlockId) {
const error = new WorkflowValidationError(
'No valid trigger block found to start execution',
'validation',
'validation',
'Workflow Validation'
)
logger.error('No startBlockId found after trigger search')
if (activeWorkflowId) finishOwnedExecution(activeWorkflowId, persistenceExecution)
throw error
}

logger.info('Final execution setup:', {
startBlockId,
isExecutingFromChat,
Expand Down Expand Up @@ -1316,7 +1304,7 @@ export function useWorkflowExecution() {
}

// Add trigger block to executed blocks so downstream blocks can use run-from-block
if (data.success && startBlockId) {
if (data.success) {
executedBlockIds.add(startBlockId)
}

Expand Down
1 change: 1 addition & 0 deletions apps/sim/executor/utils/run-from-block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ describe('validateRunFromBlock', () => {
subflowType: 'loop',
}),
])
dag.loopConfigs.set('loop-1', { id: 'loop-1', nodes: ['A'], iterations: 2, loopType: 'for' })
const executedBlocks = new Set(['A'])

const result = validateRunFromBlock('A', dag, executedBlocks)
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/executor/utils/run-from-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export function validateRunFromBlock(
}

if (node) {
if (node.metadata.isLoopNode) return insideSubflow(node.metadata.subflowId, 'loop')
// A branch-clone id (`block₍0₎`) is in no subflow config, so findOutermostContainer misses it.
if (node.metadata.isParallelBranch) return insideSubflow(node.metadata.subflowId, 'parallel')

if (node.metadata.isSentinel) {
Expand Down
16 changes: 2 additions & 14 deletions apps/sim/lib/api/contracts/v2/workflow-graph.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createEmptyWorkflowLintReport } from '@sim/testing/factories/workflow-lint.factory'
import { describe, expect, it } from 'vitest'
import {
v2ApplyWorkflowOperationsDataSchema,
Expand Down Expand Up @@ -92,20 +93,7 @@ describe('v2WorkflowGraphSchema', () => {
})
})

/** An empty report carrying every field the lint schema requires. */
const EMPTY_LINT = {
sources: [],
sinks: [],
orphanBlocks: [],
emptyOutgoingPorts: [],
invalidBranchPorts: [],
invalidConnectionTargets: [],
fieldIssues: [],
unresolvedReferences: [],
tableFieldIssues: [],
manualRunIssue: null,
notes: [],
}
const EMPTY_LINT = createEmptyWorkflowLintReport()

/** A report exercising every finding kind the lint schema publishes. */
const FULL_LINT = {
Expand Down
15 changes: 2 additions & 13 deletions apps/sim/lib/mothership/agent-cli/engines/lint-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
createSessionPrincipal,
createWorkspaceApiKeyPrincipal,
} from '@sim/testing/factories/principal.factory'
import { createEmptyWorkflowLintReport } from '@sim/testing/factories/workflow-lint.factory'
import {
secretsUseCasesMock,
secretsUseCasesMockFns,
Expand Down Expand Up @@ -82,19 +83,7 @@ describe('Mothership workflow lint scope', () => {
})
mocks.permission.mockResolvedValue('read')
mocks.snapshot.mockResolvedValue({ workflowRecord: { id: 'wf-1' }, normalizedData: graph })
mocks.report.mockResolvedValue({
sources: [],
sinks: [],
orphanBlocks: [],
emptyOutgoingPorts: [],
invalidBranchPorts: [],
invalidConnectionTargets: [],
fieldIssues: [],
unresolvedReferences: [],
tableFieldIssues: [],
manualRunIssue: null,
notes: [],
})
mocks.report.mockResolvedValue(createEmptyWorkflowLintReport())
mocks.secrets.mockResolvedValue({ secrets: [{ envKey: 'WORKFLOW_KEY' }], nextCursorKeys: null })
})

Expand Down
30 changes: 12 additions & 18 deletions apps/sim/lib/mothership/agent-cli/engines/universal-grep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ const PLATFORM_SCOPES: ReadonlySet<Scope> = new Set<Scope>(['blocks', 'tools'])

interface GrepRuntime extends AgentCliRuntime {
reportIncomplete(message: string): void
reportNonText(): void
}

/**
Expand Down Expand Up @@ -121,6 +120,11 @@ export interface GrepMaterialized {
label: string
id: string
text: string | null
/**
* Skipped unread because it has no text to extract: a note, not an incomplete search.
* Still returned, so `--in` naming it is not an unknown selector.
*/
nonText?: true
}

interface Page {
Expand Down Expand Up @@ -389,13 +393,11 @@ async function fetchAll(
if (scope !== 'files') {
return mapConcurrent(entries, FETCH_CONCURRENCY, (item) => FETCHERS[scope](runtime, item))
}
// Skipped files keep their identity, so `--in` naming one is not an unknown selector.
const skipped: GrepMaterialized[] = []
const readable: IndexEntry[] = []
for (const item of entries) {
if (lacksText(item.raw)) {
runtime.reportNonText()
skipped.push({ scope, id: item.id, label: item.label, text: null })
skipped.push({ scope, id: item.id, label: item.label, text: null, nonText: true })
} else readable.push(item)
}
if (readable.length > MAX_GREP_FILES) {
Expand Down Expand Up @@ -589,15 +591,10 @@ export interface GrepSource {
registry?: AgentCliRuntime['resolvedSecretTraceRegistry']
/** The only scopes this corpus holds, and the refusal for a search that names another. */
scopeLimit?: { scopes: readonly Scope[]; refusal: string }
/**
* `reportNonText` counts a file skipped unread because it has no text to extract: a note,
* not an incomplete search. The skipped file is still returned with null text.
*/
materialize(
scopes: Scope[],
nameFilter: string | undefined,
reportIncomplete: (message: string) => void,
reportNonText: () => void
reportIncomplete: (message: string) => void
): Promise<GrepMaterialized[]>
}

Expand Down Expand Up @@ -652,10 +649,9 @@ export async function executeGrep(positionals: string[], flags: AgentCliFlags, s
issueCount++
if (issues.length < 10) issues.push(clip(message))
}
let nonTextCount = 0
const reportNonText = () => {
nonTextCount++
}
const candidates = await source.materialize(searched, nameFilter, reportIncomplete)
source.signal?.throwIfAborted()
const nonTextCount = candidates.filter((candidate) => candidate.nonText).length
const finish = (stdout: string) => {
const skipped = nonTextCount
? `Skipped ${nonTextCount} file${nonTextCount === 1 ? '' : 's'} with no searchable text (images, media, archives, and other binary formats).`
Expand All @@ -669,8 +665,6 @@ export async function executeGrep(positionals: string[], flags: AgentCliFlags, s
stderr: [incomplete, skipped].filter(Boolean).join('\n'),
}
}
const candidates = await source.materialize(searched, nameFilter, reportIncomplete, reportNonText)
source.signal?.throwIfAborted()
/**
* A resource nothing in the searched worlds answers to is a wrong selector, not a
* search with no hits — a silent "No matches" would hide the misspelling.
Expand Down Expand Up @@ -758,8 +752,8 @@ export const universalGrepCommand: AgentCliEngine = {
executeGrep(positionals, flags, {
signal: runtime.signal,
registry: runtime.resolvedSecretTraceRegistry,
async materialize(scopes, nameFilter, reportIncomplete, reportNonText) {
const bounded = gatedRuntime({ ...runtime, reportIncomplete, reportNonText })
async materialize(scopes, nameFilter, reportIncomplete) {
const bounded = gatedRuntime({ ...runtime, reportIncomplete })
return nameFilter
? materializeWithin(bounded, scopes, nameFilter)
: (await settleAll(scopes.map((scope) => materializeScope(bounded, scope)))).flat()
Expand Down
6 changes: 2 additions & 4 deletions apps/sim/lib/mothership/agent-cli/project-file-grep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ export function grepProjectFiles(
scopes: ['files'],
refusal: 'Project grep supports only --scope files. Search workspace resources separately.',
},
async materialize(_scopes, nameFilter, reportIncomplete, reportNonText) {
// Kept so `--in` naming an unread non-text file is not an unknown selector.
async materialize(_scopes, nameFilter, reportIncomplete) {
const skipped: GrepMaterialized[] = []
const selectedFiles: {
item: Awaited<ReturnType<typeof listProjectFiles.execute>>['files'][number]
Expand Down Expand Up @@ -72,8 +71,7 @@ export function grepProjectFiles(
type: observedMediaType(item.type, item.name),
})
) {
reportNonText()
skipped.push({ scope: 'files', id: item.id, label, text: null })
skipped.push({ scope: 'files', id: item.id, label, text: null, nonText: true })
continue
}
if (selectedFiles.length >= MAX_GREP_FILES) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@ describe('applyWorkflowOperations', () => {
})

/**
* An agent piping the apply response through a projection of `warnings`
* never saw lint findings, which lived only under `lint`.
* A caller projecting the response down to `warnings` still learns lint
* findings exist; the findings stay in `lint`.
*/
it('points at lint findings from warnings, on commit and on dry run', async () => {
const orphan = { blockId: 'block-1', blockName: 'Start', blockType: 'starter' }
Expand All @@ -281,9 +281,9 @@ describe('applyWorkflowOperations', () => {
principal: sessionPrincipal,
input: { workflowId: 'workflow-1', operations, dryRun },
})
expect(result.warnings).toEqual([
'Workflow lint found issues (Blocks with no incoming edge: 2). The findings are in `lint`.',
])
expect(result.warnings).toHaveLength(1)
expect(result.warnings[0]).toContain('Blocks with no incoming edge: 2')
expect(result.warnings[0]).toContain('in `lint`')
}
})

Expand Down
15 changes: 2 additions & 13 deletions apps/sim/lib/workflows/application/deployments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createSessionPrincipal,
createWorkspaceApiKeyPrincipal,
} from '@sim/testing/factories/principal.factory'
import { createEmptyWorkflowLintReport } from '@sim/testing/factories/workflow-lint.factory'
import { auditMock, auditMockFns } from '@sim/testing/mocks/audit.mock'
import { realtimeNotifyMock } from '@sim/testing/mocks/realtime-notify.mock'
import { workflowAuthzMockFns } from '@sim/testing/mocks/workflow-authz.mock'
Expand Down Expand Up @@ -83,19 +84,7 @@ const context = {
billedAccountUserId: 'billing-owner-1',
}

const cleanLint = {
sources: [],
sinks: [],
orphanBlocks: [],
emptyOutgoingPorts: [],
invalidBranchPorts: [],
invalidConnectionTargets: [],
fieldIssues: [],
unresolvedReferences: [],
tableFieldIssues: [],
manualRunIssue: null,
notes: [],
}
const cleanLint = createEmptyWorkflowLintReport()

const adminPrincipals: Array<{ principal: Principal; actorUserId: string }> = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ describe('Copilot workflow run application commands', () => {
expect(message).toContain('execution source-execution-1')
expect(message).toContain('run_workflow_until_block')
expect(message).toContain('stopAfterBlockId "agent-1"')
expect(message).toContain('executionId')
expect(message).toContain('or pass the executionId of a run that executed "Fetch" (fetch-1)')
})

it('names the container a nested block sits in and points at it', async () => {
Expand All @@ -378,7 +378,7 @@ describe('Copilot workflow run application commands', () => {

expect(message).toContain('inside loop "Each row" (loop-1)')
expect(message).toContain('block id "loop-1"')
expect(message).toContain('run_workflow')
expect(message).toContain('or run the whole workflow with run_workflow.')
})

it('lists the blocks the workflow does have when the id is unknown', async () => {
Expand Down
Loading
Loading