@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.
Files changed (34) hide show
  1. package/dist/index.js +195 -3
  2. package/dist/organization-model/index.js +195 -3
  3. package/package.json +1 -1
  4. package/src/__tests__/template-foundations-compatibility.test.ts +95 -14
  5. package/src/auth/multi-tenancy/types.ts +2 -1
  6. package/src/execution/engine/__tests__/fixtures/test-agents.ts +4 -4
  7. package/src/execution/engine/index.ts +5 -19
  8. package/src/execution/engine/tools/platform/index.ts +9 -33
  9. package/src/execution/engine/tools/registry.ts +109 -2
  10. package/src/execution/engine/tools/tool-maps.ts +88 -0
  11. package/src/organization-model/README.md +19 -4
  12. package/src/organization-model/__tests__/graph.test.ts +612 -0
  13. package/src/organization-model/__tests__/resolve.test.ts +208 -0
  14. package/src/organization-model/defaults.ts +1 -1
  15. package/src/organization-model/organization-graph.mdx +262 -0
  16. package/src/organization-model/organization-model.mdx +257 -0
  17. package/src/organization-model/resolve.ts +26 -2
  18. package/src/organization-model/schema.ts +203 -1
  19. package/src/platform/constants/versions.ts +1 -1
  20. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +24 -0
  21. package/src/platform/registry/__tests__/resource-registry.test.ts +63 -0
  22. package/src/platform/registry/resource-registry.ts +98 -10
  23. package/src/projects/api-schemas.ts +2 -1
  24. package/src/reference/_generated/contracts.md +1044 -0
  25. package/src/reference/glossary.md +88 -0
  26. package/src/server.ts +2 -3
  27. package/src/execution/engine/tools/platform/resource-invocation/__tests__/edge-cases.test.ts +0 -507
  28. package/src/execution/engine/tools/platform/resource-invocation/__tests__/resource-invocation-service.test.ts +0 -500
  29. package/src/execution/engine/tools/platform/resource-invocation/__tests__/tool.test.ts +0 -555
  30. package/src/execution/engine/tools/platform/resource-invocation/dynamic-tool.ts +0 -94
  31. package/src/execution/engine/tools/platform/resource-invocation/index.ts +0 -14
  32. package/src/execution/engine/tools/platform/resource-invocation/resource-invocation-service.ts +0 -147
  33. package/src/execution/engine/tools/platform/resource-invocation/tool.ts +0 -115
  34. 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
- // Resource invocation tool factory (uses runtime service registry)
206
- export { createResourceInvocationTool } from './execution/engine/tools/platform/resource-invocation'
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 {
@@ -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
- })