|
1 | 1 | import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' |
2 | 2 | import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' |
| 3 | + |
| 4 | +const workflowMetadataMocks = vi.hoisted(() => ({ |
| 5 | + buildAPIUrl: vi.fn((path: string) => new URL(path, 'https://sim.local')), |
| 6 | + buildExecutorDelegationHeaders: vi.fn(), |
| 7 | +})) |
| 8 | + |
| 9 | +vi.mock('@/executor/utils/http', () => ({ |
| 10 | + buildAPIUrl: workflowMetadataMocks.buildAPIUrl, |
| 11 | + buildExecutorDelegationHeaders: workflowMetadataMocks.buildExecutorDelegationHeaders, |
| 12 | +})) |
| 13 | + |
3 | 14 | import { |
4 | 15 | calculateCost, |
5 | 16 | describeModelLevel, |
@@ -1841,6 +1852,105 @@ describe('prepareToolExecution invoker identity hand-off', () => { |
1841 | 1852 | }) |
1842 | 1853 | }) |
1843 | 1854 |
|
| 1855 | +describe('workflow executor metadata delegation', () => { |
| 1856 | + const workflowBlock = { |
| 1857 | + type: 'workflow', |
| 1858 | + name: 'Workflow', |
| 1859 | + description: 'Execute a workflow', |
| 1860 | + inputs: {}, |
| 1861 | + subBlocks: [], |
| 1862 | + tools: { access: ['workflow_executor'] }, |
| 1863 | + } |
| 1864 | + const workflowTool = { |
| 1865 | + id: 'workflow_executor', |
| 1866 | + name: 'Workflow Executor', |
| 1867 | + description: 'Execute another workflow', |
| 1868 | + params: { |
| 1869 | + workflowId: { |
| 1870 | + type: 'string' as const, |
| 1871 | + required: true, |
| 1872 | + visibility: 'user-only' as const, |
| 1873 | + }, |
| 1874 | + }, |
| 1875 | + } |
| 1876 | + |
| 1877 | + beforeEach(() => { |
| 1878 | + vi.clearAllMocks() |
| 1879 | + workflowMetadataMocks.buildExecutorDelegationHeaders.mockResolvedValue({ |
| 1880 | + 'Content-Type': 'application/json', |
| 1881 | + Authorization: 'Bearer delegated-token', |
| 1882 | + }) |
| 1883 | + }) |
| 1884 | + |
| 1885 | + afterEach(() => { |
| 1886 | + vi.unstubAllGlobals() |
| 1887 | + }) |
| 1888 | + |
| 1889 | + it('binds workflow metadata reads to the target workflow and trusted execution subject', async () => { |
| 1890 | + const fetchMock = vi |
| 1891 | + .fn() |
| 1892 | + .mockResolvedValue( |
| 1893 | + new Response( |
| 1894 | + JSON.stringify({ data: { name: 'Child Workflow', description: 'Child description' } }), |
| 1895 | + { status: 200, headers: { 'Content-Type': 'application/json' } } |
| 1896 | + ) |
| 1897 | + ) |
| 1898 | + vi.stubGlobal('fetch', fetchMock) |
| 1899 | + |
| 1900 | + const result = await transformBlockTool( |
| 1901 | + { type: 'workflow', params: { workflowId: 'child-workflow' } }, |
| 1902 | + { |
| 1903 | + getAllBlocks: () => [workflowBlock], |
| 1904 | + getTool: () => workflowTool, |
| 1905 | + enrichmentContext: { |
| 1906 | + workflowId: 'parent-workflow', |
| 1907 | + workspaceId: 'workspace-1', |
| 1908 | + executionId: 'execution-1', |
| 1909 | + userId: 'user-1', |
| 1910 | + }, |
| 1911 | + } |
| 1912 | + ) |
| 1913 | + |
| 1914 | + expect(workflowMetadataMocks.buildExecutorDelegationHeaders).toHaveBeenCalledWith({ |
| 1915 | + subjectUserId: 'user-1', |
| 1916 | + workflowId: 'child-workflow', |
| 1917 | + executionId: 'execution-1', |
| 1918 | + }) |
| 1919 | + expect(fetchMock).toHaveBeenCalledWith('https://sim.local/api/workflows/child-workflow', { |
| 1920 | + headers: { |
| 1921 | + 'Content-Type': 'application/json', |
| 1922 | + Authorization: 'Bearer delegated-token', |
| 1923 | + }, |
| 1924 | + }) |
| 1925 | + expect(result).toMatchObject({ |
| 1926 | + id: 'workflow_executor_child-workflow', |
| 1927 | + name: 'Child Workflow', |
| 1928 | + description: 'Child description', |
| 1929 | + }) |
| 1930 | + }) |
| 1931 | + |
| 1932 | + it('does not issue an actorless fallback token without a trusted execution subject', async () => { |
| 1933 | + const fetchMock = vi.fn() |
| 1934 | + vi.stubGlobal('fetch', fetchMock) |
| 1935 | + |
| 1936 | + const result = await transformBlockTool( |
| 1937 | + { type: 'workflow', params: { workflowId: 'child-workflow' } }, |
| 1938 | + { |
| 1939 | + getAllBlocks: () => [workflowBlock], |
| 1940 | + getTool: () => workflowTool, |
| 1941 | + } |
| 1942 | + ) |
| 1943 | + |
| 1944 | + expect(workflowMetadataMocks.buildExecutorDelegationHeaders).not.toHaveBeenCalled() |
| 1945 | + expect(fetchMock).not.toHaveBeenCalled() |
| 1946 | + expect(result).toMatchObject({ |
| 1947 | + id: 'workflow_executor_child-workflow', |
| 1948 | + name: 'Workflow Executor', |
| 1949 | + description: 'Execute another workflow', |
| 1950 | + }) |
| 1951 | + }) |
| 1952 | +}) |
| 1953 | + |
1844 | 1954 | /** |
1845 | 1955 | * The agent block's tuning-level fields accept variable and environment references, so any |
1846 | 1956 | * message that echoes a caller-supplied level can otherwise carry whatever that reference |
|
0 commit comments