@elevasis/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +195 -3
- package/dist/organization-model/index.js +195 -3
- package/package.json +1 -1
- package/src/__tests__/template-foundations-compatibility.test.ts +95 -14
- package/src/auth/multi-tenancy/types.ts +2 -1
- package/src/execution/engine/__tests__/fixtures/test-agents.ts +4 -4
- package/src/execution/engine/index.ts +5 -19
- package/src/execution/engine/tools/platform/index.ts +9 -33
- package/src/execution/engine/tools/registry.ts +109 -2
- package/src/execution/engine/tools/tool-maps.ts +88 -0
- package/src/organization-model/README.md +19 -4
- package/src/organization-model/__tests__/graph.test.ts +612 -0
- package/src/organization-model/__tests__/resolve.test.ts +208 -0
- package/src/organization-model/defaults.ts +1 -1
- package/src/organization-model/organization-graph.mdx +262 -0
- package/src/organization-model/organization-model.mdx +257 -0
- package/src/organization-model/resolve.ts +26 -2
- package/src/organization-model/schema.ts +203 -1
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/registry/__tests__/resource-registry.integration.test.ts +24 -0
- package/src/platform/registry/__tests__/resource-registry.test.ts +63 -0
- package/src/platform/registry/resource-registry.ts +98 -10
- package/src/projects/api-schemas.ts +2 -1
- package/src/reference/_generated/contracts.md +1044 -0
- package/src/reference/glossary.md +88 -0
- package/src/server.ts +2 -3
- package/src/execution/engine/tools/platform/resource-invocation/__tests__/edge-cases.test.ts +0 -507
- package/src/execution/engine/tools/platform/resource-invocation/__tests__/resource-invocation-service.test.ts +0 -500
- package/src/execution/engine/tools/platform/resource-invocation/__tests__/tool.test.ts +0 -555
- package/src/execution/engine/tools/platform/resource-invocation/dynamic-tool.ts +0 -94
- package/src/execution/engine/tools/platform/resource-invocation/index.ts +0 -14
- package/src/execution/engine/tools/platform/resource-invocation/resource-invocation-service.ts +0 -147
- package/src/execution/engine/tools/platform/resource-invocation/tool.ts +0 -115
- package/src/execution/engine/tools/platform/resource-invocation/types.ts +0 -31
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Glossary
|
|
3
|
+
description: Terminology disambiguation for Organization OS concepts used in the template scaffold, foundations, and published packages.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Glossary
|
|
7
|
+
|
|
8
|
+
For canonical (internal) definitions, see the Organization OS glossary in the monorepo architecture docs.
|
|
9
|
+
|
|
10
|
+
This condensed version covers every ambiguity-prone term a template consumer or agent is likely to encounter. Alphabetical within each section.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Terms
|
|
15
|
+
|
|
16
|
+
**accessFeatureKey** -- the `OrganizationModelFeatureKey` a `FeatureModule` declares so the provider knows which org-level gate to check. Required field on every `FeatureModule`. Distinct from the nav-item `featureKey` (see below). Shell keys like `crm` and `lead-gen` map to grouped org-model keys (`acquisition`) via `FEATURE_KEY_ALIASES` -- this aliasing is automatic; callers do not need to translate manually.
|
|
17
|
+
|
|
18
|
+
**AdminGuard** -- route-level admin wrapper from `@elevasis/ui/features/auth`. Wraps routes restricted to admin members. Must nest inside `ProtectedRoute`. Does not replace `requiresAdmin` on nav entries -- use both when both route access and nav visibility need admin enforcement.
|
|
19
|
+
|
|
20
|
+
**Contract** -- the publishable I/O boundary: Zod schemas in `foundations/types/index.ts` for workflow inputs/outputs, or the `FeatureModule` TypeScript shape for shell features. Distinct from "manifest": a contract is the structural definition; a manifest is a specific feature instance conforming to that shape.
|
|
21
|
+
|
|
22
|
+
**DeploymentSpec** -- the complete resource collection for one organization (`workflows`, `agents`, `triggers`, `integrations`, `relationships`, `externalResources`, `humanCheckpoints`). Defined in `@repo/core`. The `operations/src/index.ts` file in each external project exports one `DeploymentSpec`.
|
|
23
|
+
|
|
24
|
+
**Domain** -- a semantic business area in `OrganizationModel.domains`. Four defaults: `crm`, `lead-gen`, `delivery`, `operations`. Domains group entity IDs, surface IDs, resource IDs, and capability IDs into a coherent business area. Semantic, not navigational -- describes what business area a thing belongs to, not which access key gates it. Distinct from "feature": a domain describes business meaning; a feature key describes access and enablement.
|
|
25
|
+
|
|
26
|
+
**FEATURE_KEY_ALIASES** -- the alias map in `@elevasis/ui` that bridges shell feature-module keys to org-model grouped keys: `crm -> acquisition`, `lead-gen -> acquisition`, `projects -> delivery`. Used internally by `createFeatureAccessHook` so `isFeatureEnabled('crm')` resolves correctly against `MembershipFeatureConfig.features.acquisition`.
|
|
27
|
+
|
|
28
|
+
**FoundationLegacyFeatureKey** -- template-local union for feature keys defined in `foundations/config/organization-model.ts`. Distinct from the published `OrganizationModelFeatureKey` (seven-key union from `@elevasis/core`). Extension point for template-specific feature identity without modifying the published core union. `FoundationFeatureKey` combines both (`OrganizationModelFeatureKey | FoundationLegacyFeatureKey`). `FEATURE_KEY_ALIASES` maps between shell keys and org-model grouped keys at runtime.
|
|
29
|
+
|
|
30
|
+
**Feature** -- an overloaded term. Always qualify which layer is in scope:
|
|
31
|
+
|
|
32
|
+
- **Platform capability** -- a product area (Execution Engine, Workflows, Agents, etc.). Not one-to-one with shell features.
|
|
33
|
+
- **Shell FeatureModule** -- a manifest-backed UI feature registered with `ElevasisFeaturesProvider`. Seven manifest-backed: `lead-gen`, `crm`, `delivery`, `operations`, `monitoring`, `settings`, `seo`. Two utility (no manifest): `auth`, `dashboard`.
|
|
34
|
+
- **Organization-model feature key** (`OrganizationModelFeatureKey`) -- grouped access key. Seven values: `acquisition`, `delivery`, `operations`, `monitoring`, `settings`, `seo`, `calibration`. Controls org-level gating and membership overrides.
|
|
35
|
+
|
|
36
|
+
**featureKey** (nav-item) -- optional field on `FeatureNavEntry` and `FeatureNavLink` that gates a specific nav item's visibility independently of the module's `accessFeatureKey`. Resolved through `FEATURE_KEY_ALIASES`. May be more specific than the module's access key when a nested link needs a narrower gate.
|
|
37
|
+
|
|
38
|
+
**FeatureGuard** -- route-level feature gate from `@elevasis/ui/features/auth`. Blocks access to a route when the resolved org model has the relevant feature key disabled. Must nest inside `ProtectedRoute`. Distinct from `AdminGuard` (admin membership) and `ProtectedRoute` (authentication).
|
|
39
|
+
|
|
40
|
+
**FeatureModule** -- the manifest contract each shell feature provides to `ElevasisFeaturesProvider`. Defined in `@repo/ui`, NOT `@repo/core`. Fields: `key`, `accessFeatureKey` (required), `domainIds`, `capabilityIds`, `navEntry`, `sidebar`, `subshellRoutes`, `organizationGraph` (Operations-only). Template consumers build a local manifest array and pass it to `ElevasisFeaturesProvider` in `__root.tsx`.
|
|
41
|
+
|
|
42
|
+
**Foundations** -- the adapter layer in `foundations/` (two modules: `config/organization-model.ts` and `types/index.ts`). Source package with no build step. Depends only on `@elevasis/core` (npm) and `zod`. Never import `@repo/core` from foundations -- that would break standalone deployment.
|
|
43
|
+
|
|
44
|
+
**Manifest** -- a `FeatureModule` instance that declares what one shell feature contributes at runtime (nav, sidebar, subshell routes, access key, semantic references). The provider registers an array of manifests at startup and validates them against the resolved org model.
|
|
45
|
+
|
|
46
|
+
**MembershipFeatureConfig** -- per-member-per-org feature overrides stored in `org_memberships.config`. Six keys: `operations`, `monitoring`, `acquisition`, `delivery`, `calibration`, `seo`. Note: `settings` is absent -- see **Settings asymmetry** below.
|
|
47
|
+
|
|
48
|
+
**OrganizationModel** -- the top-level semantic contract for an organization. Published from `@elevasis/core/organization-model`. In the template, authored in `foundations/config/organization-model.ts` and exported as `canonicalOrganizationModel` (passed to `ElevasisFeaturesProvider`) and `organizationModel` (enriched shape for app-local use).
|
|
49
|
+
|
|
50
|
+
**OrganizationModelFeatureKey** -- seven values: `acquisition`, `delivery`, `operations`, `monitoring`, `settings`, `seo`, `calibration`. These appear in `OrganizationModel.features.enabled` and as `accessFeatureKey` on `FeatureModule` instances. `MembershipFeatureConfig` overrides six of them per member (no `settings` slot -- see **Settings asymmetry**).
|
|
51
|
+
|
|
52
|
+
**Provider / ElevasisFeaturesProvider** -- the runtime that registers manifests, resolves feature access against the org model, dispatches subshell routing via `FeatureShell`, and exposes resolved state through `useElevasisFeatures()`. Mounted in `__root.tsx`. Accepts `features`, optional `organizationModel`, and optional `appShellOverrides`.
|
|
53
|
+
|
|
54
|
+
**Resolved types (ResolvedFeatureModule, ResolvedShellNavItem)** -- provider output. `ResolvedFeatureModule` extends `FeatureModule` with `access` (enabled state) and `semantics` (merged domain, capability, and surface IDs). `ResolvedShellNavItem` extends `FeatureNavEntry` with `placement`, `source`, and `accessFeatureKey`. Both from `@elevasis/ui`.
|
|
55
|
+
|
|
56
|
+
**Resource** -- an entry in `OrganizationModel.resourceMappings` linking a deployable automation resource into the semantic model. At the registry layer, resources are `WorkflowDefinition` or `AgentDefinition` instances in a `DeploymentSpec`.
|
|
57
|
+
|
|
58
|
+
**Settings asymmetry** -- `settings` is a valid `OrganizationModelFeatureKey` (org-level, present in `OrganizationModel.features.enabled`). It is absent from `MembershipFeatureConfig` (six keys, no `settings`). The org can disable settings entirely, but per-member disabling is not supported. Settings visibility for individual members is controlled via `requiresAdmin` on the nav entry and `AdminGuard` on routes.
|
|
59
|
+
|
|
60
|
+
**Subshell / Sidebar** -- the feature-scoped UI region rendered when the current route matches a manifest's `subshellRoutes`. Each `FeatureModule.sidebar` is a `ComponentType`. Consumers customize by composing the feature's published sidebar primitives (`CrmSidebar`, `CrmSidebarMiddle`, etc.) and assigning their component to `manifest.sidebar`.
|
|
61
|
+
|
|
62
|
+
**Surface** -- a navigable view in `OrganizationModel.navigation.surfaces`. Identified by a dotted `id` (e.g., `crm.pipeline`). Has `path`, `surfaceType`, optional `featureKey` gate, and cross-reference arrays. Distinct from "page": a surface is the org-model declaration; a page is the React component rendered at the route.
|
|
63
|
+
|
|
64
|
+
**Topology** -- resource relationships (`triggers`, `uses`, `approval`) declared in `DeploymentSpec.relationships` and `HumanCheckpointDefinition.routesTo`. Used for Command View graph edges.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Package-Boundary Cheat Sheet
|
|
69
|
+
|
|
70
|
+
**`@elevasis/core`** (published npm)
|
|
71
|
+
|
|
72
|
+
- `OrganizationModel`, `OrganizationModelFeatureKey`, `OrganizationModelSurface`, `OrganizationModelResourceMapping`
|
|
73
|
+
- `resolveOrganizationModel`, `defineOrganizationModel`, `DEFAULT_ORGANIZATION_MODEL`
|
|
74
|
+
- `MembershipFeatureConfig`
|
|
75
|
+
|
|
76
|
+
**`@elevasis/ui`** (published npm)
|
|
77
|
+
|
|
78
|
+
- `FeatureModule`, `FeatureNavEntry`, `FeatureNavLink`
|
|
79
|
+
- `ResolvedFeatureModule`, `ResolvedShellNavItem`
|
|
80
|
+
- `FeatureGuard`, `AdminGuard`, `ProtectedRoute`
|
|
81
|
+
- `ElevasisFeaturesProvider`, `ElevasisCoreProvider`, `useElevasisFeatures`
|
|
82
|
+
- `FEATURE_KEY_ALIASES`, `createFeatureAccessHook`
|
|
83
|
+
|
|
84
|
+
**`foundations/`** (local source package -- not published)
|
|
85
|
+
|
|
86
|
+
- `canonicalOrganizationModel` -- passed to `ElevasisFeaturesProvider`
|
|
87
|
+
- `organizationModel` -- enriched shape for app-local code
|
|
88
|
+
- Workflow I/O schemas (`types/index.ts`)
|
package/src/server.ts
CHANGED
|
@@ -202,9 +202,8 @@ export { createLLMCallTool, type LLMCallToolConfig } from './execution/engine/to
|
|
|
202
202
|
// Workflow step helper (uses server-only adapters)
|
|
203
203
|
export { llmCall, type LLMCallOptions } from './execution/engine/workflow/helpers/server'
|
|
204
204
|
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
export type { ResourceDefinition } from './execution/engine/tools/platform/resource-invocation'
|
|
205
|
+
// Retained server-side resource invocation service types live under @repo/core/execution.
|
|
206
|
+
// Deployed SDK workers use platform.call() -> dispatcher for nested execution.
|
|
208
207
|
|
|
209
208
|
// Resilience utilities (timeout, circuit breaker, infrastructure errors)
|
|
210
209
|
export {
|
package/src/execution/engine/tools/platform/resource-invocation/__tests__/edge-cases.test.ts
DELETED
|
@@ -1,507 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
import { z } from 'zod'
|
|
3
|
-
import { ResourceInvocationService, createResourceInvocationService } from '../resource-invocation-service'
|
|
4
|
-
import type { ResourceRegistry } from '../../../../../../platform/registry/resource-registry'
|
|
5
|
-
import type { WorkflowDefinition } from '../../../../workflow/types'
|
|
6
|
-
import type { AgentDefinition } from '../../../../agent/core/types'
|
|
7
|
-
import type { ExecuteResourceCallback } from '../types'
|
|
8
|
-
import { createMockExecutionContext } from '../../../../test-utils/mocks'
|
|
9
|
-
|
|
10
|
-
describe('ResourceInvocationService Edge Cases', () => {
|
|
11
|
-
let mockRegistry: ResourceRegistry
|
|
12
|
-
let mockExecuteResource: ReturnType<typeof vi.fn>
|
|
13
|
-
let service: ResourceInvocationService
|
|
14
|
-
|
|
15
|
-
const mockWorkflowDefinition: WorkflowDefinition = {
|
|
16
|
-
config: {
|
|
17
|
-
resourceId: 'test-workflow',
|
|
18
|
-
name: 'Test Workflow',
|
|
19
|
-
description: 'Test workflow for edge cases',
|
|
20
|
-
version: '1.0.0',
|
|
21
|
-
type: 'workflow',
|
|
22
|
-
status: 'dev'
|
|
23
|
-
},
|
|
24
|
-
contract: {
|
|
25
|
-
inputSchema: z.object({
|
|
26
|
-
value: z.string()
|
|
27
|
-
}),
|
|
28
|
-
outputSchema: z.object({
|
|
29
|
-
result: z.string()
|
|
30
|
-
})
|
|
31
|
-
},
|
|
32
|
-
entryPoint: 'start',
|
|
33
|
-
steps: {}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const mockAgentDefinition: AgentDefinition = {
|
|
37
|
-
config: {
|
|
38
|
-
resourceId: 'test-agent',
|
|
39
|
-
name: 'Test Agent',
|
|
40
|
-
description: 'Test agent for edge cases',
|
|
41
|
-
version: '1.0.0',
|
|
42
|
-
type: 'agent',
|
|
43
|
-
status: 'dev'
|
|
44
|
-
},
|
|
45
|
-
contract: {
|
|
46
|
-
inputSchema: z.object({
|
|
47
|
-
query: z.string()
|
|
48
|
-
}),
|
|
49
|
-
outputSchema: z.object({
|
|
50
|
-
answer: z.string()
|
|
51
|
-
})
|
|
52
|
-
},
|
|
53
|
-
modelConfig: {
|
|
54
|
-
provider: 'openai',
|
|
55
|
-
model: 'gpt-4o',
|
|
56
|
-
temperature: 0.7
|
|
57
|
-
},
|
|
58
|
-
systemPrompt: 'You are a test agent',
|
|
59
|
-
tools: []
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
beforeEach(() => {
|
|
63
|
-
mockRegistry = {
|
|
64
|
-
getResourceDefinition: vi.fn(),
|
|
65
|
-
getRelationships: vi.fn().mockReturnValue(undefined)
|
|
66
|
-
} as unknown as ResourceRegistry
|
|
67
|
-
|
|
68
|
-
mockExecuteResource = vi.fn()
|
|
69
|
-
|
|
70
|
-
service = new ResourceInvocationService(mockRegistry, mockExecuteResource)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('concurrent invocations', () => {
|
|
74
|
-
it('handles concurrent invocations without state mixing', async () => {
|
|
75
|
-
// Setup: All invocations use the same resource definition
|
|
76
|
-
vi.mocked(mockRegistry.getResourceDefinition).mockReturnValue(mockWorkflowDefinition)
|
|
77
|
-
|
|
78
|
-
// Mock executor that returns different results based on input
|
|
79
|
-
mockExecuteResource.mockImplementation(async (request) => {
|
|
80
|
-
// Simulate async delay
|
|
81
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
success: true,
|
|
85
|
-
output: { result: `processed-${request.input.value}` },
|
|
86
|
-
executionId: `exec-${request.input.value}`,
|
|
87
|
-
resourceType: 'workflow'
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const mockContext = createMockExecutionContext({
|
|
92
|
-
organizationId: 'org-123',
|
|
93
|
-
organizationName: 'test-org'
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
// Launch 3 parallel calls with different inputs
|
|
97
|
-
const results = await Promise.all([
|
|
98
|
-
service.executeSync('test-workflow', { value: 'input-1' }, 'test-org', mockContext),
|
|
99
|
-
service.executeSync('test-workflow', { value: 'input-2' }, 'test-org', mockContext),
|
|
100
|
-
service.executeSync('test-workflow', { value: 'input-3' }, 'test-org', mockContext)
|
|
101
|
-
])
|
|
102
|
-
|
|
103
|
-
// Verify each returns its own result without interference
|
|
104
|
-
expect(results[0].success).toBe(true)
|
|
105
|
-
expect(results[0].output).toEqual({ result: 'processed-input-1' })
|
|
106
|
-
expect(results[0].executionId).toBe('exec-input-1')
|
|
107
|
-
|
|
108
|
-
expect(results[1].success).toBe(true)
|
|
109
|
-
expect(results[1].output).toEqual({ result: 'processed-input-2' })
|
|
110
|
-
expect(results[1].executionId).toBe('exec-input-2')
|
|
111
|
-
|
|
112
|
-
expect(results[2].success).toBe(true)
|
|
113
|
-
expect(results[2].output).toEqual({ result: 'processed-input-3' })
|
|
114
|
-
expect(results[2].executionId).toBe('exec-input-3')
|
|
115
|
-
|
|
116
|
-
// Verify all three calls were made
|
|
117
|
-
expect(mockExecuteResource).toHaveBeenCalledTimes(3)
|
|
118
|
-
})
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
describe('empty input validation', () => {
|
|
122
|
-
it('handles empty object input for resources with z.object({})', async () => {
|
|
123
|
-
// Create resource with z.object({}) schema (accepts any object)
|
|
124
|
-
const emptySchemaWorkflow: WorkflowDefinition = {
|
|
125
|
-
...mockWorkflowDefinition,
|
|
126
|
-
contract: {
|
|
127
|
-
inputSchema: z.object({}),
|
|
128
|
-
outputSchema: z.object({ status: z.string() })
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
vi.mocked(mockRegistry.getResourceDefinition).mockReturnValue(emptySchemaWorkflow)
|
|
133
|
-
|
|
134
|
-
mockExecuteResource.mockResolvedValue({
|
|
135
|
-
success: true,
|
|
136
|
-
output: { status: 'completed' },
|
|
137
|
-
executionId: 'exec-empty',
|
|
138
|
-
resourceType: 'workflow'
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
const mockContext = createMockExecutionContext({
|
|
142
|
-
organizationName: 'test-org'
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
// Verify empty {} input is valid
|
|
146
|
-
const result = await service.executeSync(
|
|
147
|
-
'empty-schema-workflow',
|
|
148
|
-
{},
|
|
149
|
-
'test-org',
|
|
150
|
-
mockContext
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
expect(result.success).toBe(true)
|
|
154
|
-
expect(result.output).toEqual({ status: 'completed' })
|
|
155
|
-
expect(mockExecuteResource).toHaveBeenCalledWith(
|
|
156
|
-
expect.objectContaining({
|
|
157
|
-
input: {}
|
|
158
|
-
}),
|
|
159
|
-
mockContext
|
|
160
|
-
)
|
|
161
|
-
})
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
describe('execution depth limits', () => {
|
|
165
|
-
it('allows execution at depth 4 (one below limit)', async () => {
|
|
166
|
-
vi.mocked(mockRegistry.getResourceDefinition).mockReturnValue(mockWorkflowDefinition)
|
|
167
|
-
|
|
168
|
-
mockExecuteResource.mockResolvedValue({
|
|
169
|
-
success: true,
|
|
170
|
-
output: { result: 'depth-4-success' },
|
|
171
|
-
executionId: 'exec-depth-4',
|
|
172
|
-
resourceType: 'workflow'
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
// Context with executionDepth: 4
|
|
176
|
-
const contextDepth4 = createMockExecutionContext({
|
|
177
|
-
organizationName: 'test-org',
|
|
178
|
-
executionDepth: 4
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
const result = await service.executeSync(
|
|
182
|
-
'test-workflow',
|
|
183
|
-
{ value: 'test' },
|
|
184
|
-
'test-org',
|
|
185
|
-
contextDepth4
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
// Should succeed (limit is 5)
|
|
189
|
-
expect(result.success).toBe(true)
|
|
190
|
-
expect(result.output).toEqual({ result: 'depth-4-success' })
|
|
191
|
-
expect(mockExecuteResource).toHaveBeenCalledWith(
|
|
192
|
-
expect.anything(),
|
|
193
|
-
contextDepth4
|
|
194
|
-
)
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('blocks execution at depth 5 (at limit)', async () => {
|
|
198
|
-
vi.mocked(mockRegistry.getResourceDefinition).mockReturnValue(mockWorkflowDefinition)
|
|
199
|
-
|
|
200
|
-
// Mock executor to reject at depth 5
|
|
201
|
-
mockExecuteResource.mockResolvedValue({
|
|
202
|
-
success: false,
|
|
203
|
-
output: null,
|
|
204
|
-
executionId: '',
|
|
205
|
-
resourceType: 'workflow',
|
|
206
|
-
error: 'Maximum execution depth exceeded'
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
// Context with executionDepth: 5
|
|
210
|
-
const contextDepth5 = createMockExecutionContext({
|
|
211
|
-
organizationName: 'test-org',
|
|
212
|
-
executionDepth: 5
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
const result = await service.executeSync(
|
|
216
|
-
'test-workflow',
|
|
217
|
-
{ value: 'test' },
|
|
218
|
-
'test-org',
|
|
219
|
-
contextDepth5
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
// Should fail with depth exceeded error
|
|
223
|
-
expect(result.success).toBe(false)
|
|
224
|
-
expect(result.error).toContain('Maximum execution depth exceeded')
|
|
225
|
-
})
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
describe('null vs undefined output semantics', () => {
|
|
229
|
-
it('distinguishes null output (fire-and-forget) from undefined', async () => {
|
|
230
|
-
// Resource with outputSchema: null (fire-and-forget pattern)
|
|
231
|
-
const fireAndForgetWorkflow: WorkflowDefinition = {
|
|
232
|
-
...mockWorkflowDefinition,
|
|
233
|
-
contract: {
|
|
234
|
-
inputSchema: z.object({ data: z.string() }),
|
|
235
|
-
outputSchema: null
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
vi.mocked(mockRegistry.getResourceDefinition).mockReturnValue(fireAndForgetWorkflow)
|
|
240
|
-
|
|
241
|
-
// Resource should return null (not undefined)
|
|
242
|
-
mockExecuteResource.mockResolvedValue({
|
|
243
|
-
success: true,
|
|
244
|
-
output: null,
|
|
245
|
-
executionId: 'exec-null',
|
|
246
|
-
resourceType: 'workflow'
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
const mockContext = createMockExecutionContext({
|
|
250
|
-
organizationName: 'test-org'
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
const result = await service.executeSync(
|
|
254
|
-
'fire-and-forget',
|
|
255
|
-
{ data: 'test' },
|
|
256
|
-
'test-org',
|
|
257
|
-
mockContext
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
// Verify output is explicitly null, not undefined
|
|
261
|
-
expect(result.success).toBe(true)
|
|
262
|
-
expect(result.output).toBeNull()
|
|
263
|
-
expect(result.output).not.toBeUndefined()
|
|
264
|
-
})
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
describe('complex nested schema validation', () => {
|
|
268
|
-
it('validates deeply nested input schemas', async () => {
|
|
269
|
-
// Resource with deeply nested schema
|
|
270
|
-
const nestedSchemaWorkflow: WorkflowDefinition = {
|
|
271
|
-
...mockWorkflowDefinition,
|
|
272
|
-
contract: {
|
|
273
|
-
inputSchema: z.object({
|
|
274
|
-
nested: z.object({
|
|
275
|
-
deep: z.object({
|
|
276
|
-
value: z.string().min(1)
|
|
277
|
-
})
|
|
278
|
-
})
|
|
279
|
-
}),
|
|
280
|
-
outputSchema: z.object({ result: z.string() })
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
vi.mocked(mockRegistry.getResourceDefinition).mockReturnValue(nestedSchemaWorkflow)
|
|
285
|
-
|
|
286
|
-
mockExecuteResource.mockResolvedValue({
|
|
287
|
-
success: true,
|
|
288
|
-
output: { result: 'nested-success' },
|
|
289
|
-
executionId: 'exec-nested',
|
|
290
|
-
resourceType: 'workflow'
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
const mockContext = createMockExecutionContext({
|
|
294
|
-
organizationName: 'test-org'
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
// Test valid deeply nested input
|
|
298
|
-
const validResult = await service.executeSync(
|
|
299
|
-
'nested-schema-workflow',
|
|
300
|
-
{
|
|
301
|
-
nested: {
|
|
302
|
-
deep: {
|
|
303
|
-
value: 'valid-value'
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
},
|
|
307
|
-
'test-org',
|
|
308
|
-
mockContext
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
expect(validResult.success).toBe(true)
|
|
312
|
-
expect(validResult.output).toEqual({ result: 'nested-success' })
|
|
313
|
-
|
|
314
|
-
// Test invalid deeply nested input (empty string violates min(1))
|
|
315
|
-
const invalidResult = await service.executeSync(
|
|
316
|
-
'nested-schema-workflow',
|
|
317
|
-
{
|
|
318
|
-
nested: {
|
|
319
|
-
deep: {
|
|
320
|
-
value: '' // Violates min(1)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
},
|
|
324
|
-
'test-org',
|
|
325
|
-
mockContext
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
expect(invalidResult.success).toBe(false)
|
|
329
|
-
expect(invalidResult.error).toContain('Input validation failed')
|
|
330
|
-
|
|
331
|
-
// Test missing nested field
|
|
332
|
-
const missingFieldResult = await service.executeSync(
|
|
333
|
-
'nested-schema-workflow',
|
|
334
|
-
{
|
|
335
|
-
nested: {
|
|
336
|
-
// Missing 'deep' field
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
'test-org',
|
|
340
|
-
mockContext
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
expect(missingFieldResult.success).toBe(false)
|
|
344
|
-
expect(missingFieldResult.error).toContain('Input validation failed')
|
|
345
|
-
})
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
describe('error preservation through chain', () => {
|
|
349
|
-
it('preserves error context through invocation chain', async () => {
|
|
350
|
-
vi.mocked(mockRegistry.getResourceDefinition).mockReturnValue(mockAgentDefinition)
|
|
351
|
-
|
|
352
|
-
// Executor returns error with specific message
|
|
353
|
-
const specificErrorMessage = 'Agent execution failed: Model API rate limit exceeded (429)'
|
|
354
|
-
mockExecuteResource.mockResolvedValue({
|
|
355
|
-
success: false,
|
|
356
|
-
output: null,
|
|
357
|
-
executionId: 'exec-error',
|
|
358
|
-
resourceType: 'agent',
|
|
359
|
-
error: specificErrorMessage
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
const mockContext = createMockExecutionContext({
|
|
363
|
-
organizationName: 'test-org'
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
const result = await service.executeSync(
|
|
367
|
-
'test-agent',
|
|
368
|
-
{ query: 'test query' },
|
|
369
|
-
'test-org',
|
|
370
|
-
mockContext
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
// Verify error message preserved in result
|
|
374
|
-
expect(result.success).toBe(false)
|
|
375
|
-
expect(result.error).toBe(specificErrorMessage)
|
|
376
|
-
expect(result.error).toContain('Model API rate limit exceeded')
|
|
377
|
-
expect(result.error).toContain('429')
|
|
378
|
-
})
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
describe('self-invocation prevention', () => {
|
|
382
|
-
it('handles invocation of same resourceId (no infinite loop)', async () => {
|
|
383
|
-
vi.mocked(mockRegistry.getResourceDefinition).mockReturnValue(mockAgentDefinition)
|
|
384
|
-
|
|
385
|
-
// Mock executor that checks for recursion depth
|
|
386
|
-
let invocationCount = 0
|
|
387
|
-
mockExecuteResource.mockImplementation(async (request, context) => {
|
|
388
|
-
invocationCount++
|
|
389
|
-
|
|
390
|
-
// Simulate depth limit enforcement in coordinator
|
|
391
|
-
if (context.executionDepth >= 5) {
|
|
392
|
-
return {
|
|
393
|
-
success: false,
|
|
394
|
-
output: null,
|
|
395
|
-
executionId: '',
|
|
396
|
-
resourceType: 'agent',
|
|
397
|
-
error: 'Maximum execution depth exceeded'
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
success: true,
|
|
403
|
-
output: { answer: `invocation-${invocationCount}` },
|
|
404
|
-
executionId: `exec-${invocationCount}`,
|
|
405
|
-
resourceType: 'agent'
|
|
406
|
-
}
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
// Parent agent trying to invoke itself
|
|
410
|
-
const parentContext = createMockExecutionContext({
|
|
411
|
-
organizationName: 'test-org',
|
|
412
|
-
resourceId: 'test-agent',
|
|
413
|
-
executionDepth: 5 // At depth limit
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
const result = await service.executeSync(
|
|
417
|
-
'test-agent', // Same as parent resourceId
|
|
418
|
-
{ query: 'recursive query' },
|
|
419
|
-
'test-org',
|
|
420
|
-
parentContext
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
// Depth limit should prevent infinite recursion
|
|
424
|
-
expect(result.success).toBe(false)
|
|
425
|
-
expect(result.error).toContain('Maximum execution depth exceeded')
|
|
426
|
-
expect(invocationCount).toBe(1) // Only one attempt made
|
|
427
|
-
})
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
describe('multiple organizations isolation', () => {
|
|
431
|
-
it('prevents cross-organization resource access', async () => {
|
|
432
|
-
// Setup: Registry returns resource for org-a, nothing for org-b
|
|
433
|
-
vi.mocked(mockRegistry.getResourceDefinition).mockImplementation((orgName, resourceId) => {
|
|
434
|
-
if (orgName === 'org-a' && resourceId === 'private-workflow') {
|
|
435
|
-
return mockWorkflowDefinition
|
|
436
|
-
}
|
|
437
|
-
return null
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
const contextOrgA = createMockExecutionContext({
|
|
441
|
-
organizationId: 'org-a-id',
|
|
442
|
-
organizationName: 'org-a'
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
const contextOrgB = createMockExecutionContext({
|
|
446
|
-
organizationId: 'org-b-id',
|
|
447
|
-
organizationName: 'org-b'
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
mockExecuteResource.mockResolvedValue({
|
|
451
|
-
success: true,
|
|
452
|
-
output: { result: 'success' },
|
|
453
|
-
executionId: 'exec-org-a',
|
|
454
|
-
resourceType: 'workflow'
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
// org-a should succeed
|
|
458
|
-
const resultOrgA = await service.executeSync(
|
|
459
|
-
'private-workflow',
|
|
460
|
-
{ value: 'test' },
|
|
461
|
-
'org-a',
|
|
462
|
-
contextOrgA
|
|
463
|
-
)
|
|
464
|
-
|
|
465
|
-
expect(resultOrgA.success).toBe(true)
|
|
466
|
-
expect(mockRegistry.getResourceDefinition).toHaveBeenCalledWith('org-a', 'private-workflow')
|
|
467
|
-
|
|
468
|
-
// org-b should fail (resource not found)
|
|
469
|
-
const resultOrgB = await service.executeSync(
|
|
470
|
-
'private-workflow',
|
|
471
|
-
{ value: 'test' },
|
|
472
|
-
'org-b',
|
|
473
|
-
contextOrgB
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
expect(resultOrgB.success).toBe(false)
|
|
477
|
-
expect(resultOrgB.error).toContain('Resource not found')
|
|
478
|
-
expect(mockRegistry.getResourceDefinition).toHaveBeenCalledWith('org-b', 'private-workflow')
|
|
479
|
-
})
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
describe('factory function edge cases', () => {
|
|
483
|
-
it('throws with null registry', () => {
|
|
484
|
-
expect(() => {
|
|
485
|
-
createResourceInvocationService(null as unknown as ResourceRegistry, mockExecuteResource)
|
|
486
|
-
}).toThrow('ResourceRegistry required')
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
it('throws with undefined registry', () => {
|
|
490
|
-
expect(() => {
|
|
491
|
-
createResourceInvocationService(undefined as unknown as ResourceRegistry, mockExecuteResource)
|
|
492
|
-
}).toThrow('ResourceRegistry required')
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
it('throws with null executeResource callback', () => {
|
|
496
|
-
expect(() => {
|
|
497
|
-
createResourceInvocationService(mockRegistry, null as unknown as ExecuteResourceCallback)
|
|
498
|
-
}).toThrow('executeResource callback required')
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
it('throws with undefined executeResource callback', () => {
|
|
502
|
-
expect(() => {
|
|
503
|
-
createResourceInvocationService(mockRegistry, undefined as unknown as ExecuteResourceCallback)
|
|
504
|
-
}).toThrow('executeResource callback required')
|
|
505
|
-
})
|
|
506
|
-
})
|
|
507
|
-
})
|