@namzu/sdk 0.4.2 → 0.4.4
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/CHANGELOG.md +46 -0
- package/dist/advisory/context.test.d.ts +16 -0
- package/dist/advisory/context.test.d.ts.map +1 -0
- package/dist/advisory/context.test.js +92 -0
- package/dist/advisory/context.test.js.map +1 -0
- package/dist/advisory/evaluator.test.d.ts +34 -0
- package/dist/advisory/evaluator.test.d.ts.map +1 -0
- package/dist/advisory/evaluator.test.js +172 -0
- package/dist/advisory/evaluator.test.js.map +1 -0
- package/dist/advisory/executor.test.d.ts +35 -0
- package/dist/advisory/executor.test.d.ts.map +1 -0
- package/dist/advisory/executor.test.js +233 -0
- package/dist/advisory/executor.test.js.map +1 -0
- package/dist/advisory/registry.test.d.ts +16 -0
- package/dist/advisory/registry.test.d.ts.map +1 -0
- package/dist/advisory/registry.test.js +62 -0
- package/dist/advisory/registry.test.js.map +1 -0
- package/dist/bridge/a2a/agent-card.test.d.ts +24 -0
- package/dist/bridge/a2a/agent-card.test.d.ts.map +1 -0
- package/dist/bridge/a2a/agent-card.test.js +118 -0
- package/dist/bridge/a2a/agent-card.test.js.map +1 -0
- package/dist/bridge/a2a/mapper.test.d.ts +29 -0
- package/dist/bridge/a2a/mapper.test.d.ts.map +1 -0
- package/dist/bridge/a2a/mapper.test.js +265 -0
- package/dist/bridge/a2a/mapper.test.js.map +1 -0
- package/dist/bridge/a2a/message.test.d.ts +20 -0
- package/dist/bridge/a2a/message.test.d.ts.map +1 -0
- package/dist/bridge/a2a/message.test.js +116 -0
- package/dist/bridge/a2a/message.test.js.map +1 -0
- package/dist/bridge/a2a/task.test.d.ts +29 -0
- package/dist/bridge/a2a/task.test.d.ts.map +1 -0
- package/dist/bridge/a2a/task.test.js +198 -0
- package/dist/bridge/a2a/task.test.js.map +1 -0
- package/dist/bridge/mcp/connector/adapter.test.d.ts +27 -0
- package/dist/bridge/mcp/connector/adapter.test.d.ts.map +1 -0
- package/dist/bridge/mcp/connector/adapter.test.js +203 -0
- package/dist/bridge/mcp/connector/adapter.test.js.map +1 -0
- package/dist/bridge/sse/mapper.test.d.ts +27 -0
- package/dist/bridge/sse/mapper.test.d.ts.map +1 -0
- package/dist/bridge/sse/mapper.test.js +271 -0
- package/dist/bridge/sse/mapper.test.js.map +1 -0
- package/dist/bridge/tools/connector/adapter.d.ts +2 -2
- package/dist/bridge/tools/connector/adapter.test.d.ts +28 -0
- package/dist/bridge/tools/connector/adapter.test.d.ts.map +1 -0
- package/dist/bridge/tools/connector/adapter.test.js +182 -0
- package/dist/bridge/tools/connector/adapter.test.js.map +1 -0
- package/dist/bridge/tools/connector/definitions.test.d.ts +23 -0
- package/dist/bridge/tools/connector/definitions.test.d.ts.map +1 -0
- package/dist/bridge/tools/connector/definitions.test.js +158 -0
- package/dist/bridge/tools/connector/definitions.test.js.map +1 -0
- package/dist/bridge/tools/connector/router.test.d.ts +21 -0
- package/dist/bridge/tools/connector/router.test.d.ts.map +1 -0
- package/dist/bridge/tools/connector/router.test.js +139 -0
- package/dist/bridge/tools/connector/router.test.js.map +1 -0
- package/dist/bus/breaker.test.d.ts +41 -0
- package/dist/bus/breaker.test.d.ts.map +1 -0
- package/dist/bus/breaker.test.js +242 -0
- package/dist/bus/breaker.test.js.map +1 -0
- package/dist/bus/index.d.ts +3 -1
- package/dist/bus/index.d.ts.map +1 -1
- package/dist/bus/index.js +18 -11
- package/dist/bus/index.js.map +1 -1
- package/dist/bus/index.test.d.ts +25 -0
- package/dist/bus/index.test.d.ts.map +1 -0
- package/dist/bus/index.test.js +151 -0
- package/dist/bus/index.test.js.map +1 -0
- package/dist/bus/lock.test.d.ts +44 -0
- package/dist/bus/lock.test.d.ts.map +1 -0
- package/dist/bus/lock.test.js +226 -0
- package/dist/bus/lock.test.js.map +1 -0
- package/dist/bus/ownership.test.d.ts +26 -0
- package/dist/bus/ownership.test.d.ts.map +1 -0
- package/dist/bus/ownership.test.js +205 -0
- package/dist/bus/ownership.test.js.map +1 -0
- package/dist/config/runtime.d.ts +28 -28
- package/dist/connector/BaseConnector.test.d.ts +21 -0
- package/dist/connector/BaseConnector.test.d.ts.map +1 -0
- package/dist/connector/BaseConnector.test.js +108 -0
- package/dist/connector/BaseConnector.test.js.map +1 -0
- package/dist/connector/builtins/http.test.d.ts +30 -0
- package/dist/connector/builtins/http.test.d.ts.map +1 -0
- package/dist/connector/builtins/http.test.js +232 -0
- package/dist/connector/builtins/http.test.js.map +1 -0
- package/dist/connector/builtins/webhook.test.d.ts +20 -0
- package/dist/connector/builtins/webhook.test.d.ts.map +1 -0
- package/dist/connector/builtins/webhook.test.js +113 -0
- package/dist/connector/builtins/webhook.test.js.map +1 -0
- package/dist/connector/execution/factory.test.d.ts +16 -0
- package/dist/connector/execution/factory.test.d.ts.map +1 -0
- package/dist/connector/execution/factory.test.js +64 -0
- package/dist/connector/execution/factory.test.js.map +1 -0
- package/dist/connector/execution/remote.test.d.ts +16 -0
- package/dist/connector/execution/remote.test.d.ts.map +1 -0
- package/dist/connector/execution/remote.test.js +53 -0
- package/dist/connector/execution/remote.test.js.map +1 -0
- package/dist/connector/mcp/adapter.test.d.ts +34 -0
- package/dist/connector/mcp/adapter.test.d.ts.map +1 -0
- package/dist/connector/mcp/adapter.test.js +199 -0
- package/dist/connector/mcp/adapter.test.js.map +1 -0
- package/dist/probe/context.d.ts +8 -0
- package/dist/probe/context.d.ts.map +1 -0
- package/dist/probe/context.js +7 -0
- package/dist/probe/context.js.map +1 -0
- package/dist/probe/errors.d.ts +12 -0
- package/dist/probe/errors.d.ts.map +1 -0
- package/dist/probe/errors.js +21 -0
- package/dist/probe/errors.js.map +1 -0
- package/dist/probe/index.d.ts +5 -0
- package/dist/probe/index.d.ts.map +1 -0
- package/dist/probe/index.js +4 -0
- package/dist/probe/index.js.map +1 -0
- package/dist/probe/registry.d.ts +24 -0
- package/dist/probe/registry.d.ts.map +1 -0
- package/dist/probe/registry.js +228 -0
- package/dist/probe/registry.js.map +1 -0
- package/dist/probe/registry.test.d.ts +7 -0
- package/dist/probe/registry.test.d.ts.map +1 -0
- package/dist/probe/registry.test.js +310 -0
- package/dist/probe/registry.test.js.map +1 -0
- package/dist/provider/instrumentation.d.ts +9 -0
- package/dist/provider/instrumentation.d.ts.map +1 -0
- package/dist/provider/instrumentation.js +104 -0
- package/dist/provider/instrumentation.js.map +1 -0
- package/dist/provider/instrumentation.test.d.ts +2 -0
- package/dist/provider/instrumentation.test.d.ts.map +1 -0
- package/dist/provider/instrumentation.test.js +152 -0
- package/dist/provider/instrumentation.test.js.map +1 -0
- package/dist/public-runtime.d.ts +5 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +3 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/rag/chunking.test.d.ts +20 -0
- package/dist/rag/chunking.test.d.ts.map +1 -0
- package/dist/rag/chunking.test.js +92 -0
- package/dist/rag/chunking.test.js.map +1 -0
- package/dist/rag/context-assembler.test.d.ts +19 -0
- package/dist/rag/context-assembler.test.d.ts.map +1 -0
- package/dist/rag/context-assembler.test.js +98 -0
- package/dist/rag/context-assembler.test.js.map +1 -0
- package/dist/rag/embedding.test.d.ts +19 -0
- package/dist/rag/embedding.test.d.ts.map +1 -0
- package/dist/rag/embedding.test.js +115 -0
- package/dist/rag/embedding.test.js.map +1 -0
- package/dist/rag/ingestion.test.d.ts +22 -0
- package/dist/rag/ingestion.test.d.ts.map +1 -0
- package/dist/rag/ingestion.test.js +99 -0
- package/dist/rag/ingestion.test.js.map +1 -0
- package/dist/rag/knowledge-base.test.d.ts +17 -0
- package/dist/rag/knowledge-base.test.d.ts.map +1 -0
- package/dist/rag/knowledge-base.test.js +77 -0
- package/dist/rag/knowledge-base.test.js.map +1 -0
- package/dist/rag/rag-tool.test.d.ts +21 -0
- package/dist/rag/rag-tool.test.d.ts.map +1 -0
- package/dist/rag/rag-tool.test.js +149 -0
- package/dist/rag/rag-tool.test.js.map +1 -0
- package/dist/rag/retriever.test.d.ts +26 -0
- package/dist/rag/retriever.test.d.ts.map +1 -0
- package/dist/rag/retriever.test.js +180 -0
- package/dist/rag/retriever.test.js.map +1 -0
- package/dist/rag/vector-store.test.d.ts +38 -0
- package/dist/rag/vector-store.test.d.ts.map +1 -0
- package/dist/rag/vector-store.test.js +175 -0
- package/dist/rag/vector-store.test.js.map +1 -0
- package/dist/registry/ManagedRegistry.test.d.ts +21 -0
- package/dist/registry/ManagedRegistry.test.d.ts.map +1 -0
- package/dist/registry/ManagedRegistry.test.js +98 -0
- package/dist/registry/ManagedRegistry.test.js.map +1 -0
- package/dist/registry/Registry.test.d.ts +18 -0
- package/dist/registry/Registry.test.d.ts.map +1 -0
- package/dist/registry/Registry.test.js +79 -0
- package/dist/registry/Registry.test.js.map +1 -0
- package/dist/registry/agent/definitions.test.d.ts +15 -0
- package/dist/registry/agent/definitions.test.d.ts.map +1 -0
- package/dist/registry/agent/definitions.test.js +84 -0
- package/dist/registry/agent/definitions.test.js.map +1 -0
- package/dist/registry/connector/definitions.test.d.ts +13 -0
- package/dist/registry/connector/definitions.test.d.ts.map +1 -0
- package/dist/registry/connector/definitions.test.js +41 -0
- package/dist/registry/connector/definitions.test.js.map +1 -0
- package/dist/registry/connector/scoped.test.d.ts +21 -0
- package/dist/registry/connector/scoped.test.d.ts.map +1 -0
- package/dist/registry/connector/scoped.test.js +115 -0
- package/dist/registry/connector/scoped.test.js.map +1 -0
- package/dist/registry/plugin/index.test.d.ts +12 -0
- package/dist/registry/plugin/index.test.d.ts.map +1 -0
- package/dist/registry/plugin/index.test.js +69 -0
- package/dist/registry/plugin/index.test.js.map +1 -0
- package/dist/registry/tool/execute.test.d.ts +42 -0
- package/dist/registry/tool/execute.test.d.ts.map +1 -0
- package/dist/registry/tool/execute.test.js +281 -0
- package/dist/registry/tool/execute.test.js.map +1 -0
- package/dist/runtime/query/events.d.ts +3 -1
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +6 -1
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +3 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +30 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/iteration/phases/advisory.test.d.ts +42 -0
- package/dist/runtime/query/iteration/phases/advisory.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/advisory.test.js +334 -0
- package/dist/runtime/query/iteration/phases/advisory.test.js.map +1 -0
- package/dist/test-setup.d.ts +22 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +23 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/types/bus/index.d.ts +46 -2
- package/dist/types/bus/index.d.ts.map +1 -1
- package/dist/types/doctor/check.d.ts +41 -0
- package/dist/types/doctor/check.d.ts.map +1 -0
- package/dist/types/doctor/check.js +2 -0
- package/dist/types/doctor/check.js.map +1 -0
- package/dist/types/doctor/index.d.ts +2 -0
- package/dist/types/doctor/index.d.ts.map +1 -0
- package/dist/types/doctor/index.js +2 -0
- package/dist/types/doctor/index.js.map +1 -0
- package/dist/types/probe/event-kind.d.ts +6 -0
- package/dist/types/probe/event-kind.d.ts.map +1 -0
- package/dist/types/probe/event-kind.js +2 -0
- package/dist/types/probe/event-kind.js.map +1 -0
- package/dist/types/probe/event-of.d.ts +5 -0
- package/dist/types/probe/event-of.d.ts.map +1 -0
- package/dist/types/probe/event-of.js +2 -0
- package/dist/types/probe/event-of.js.map +1 -0
- package/dist/types/probe/index.d.ts +4 -0
- package/dist/types/probe/index.d.ts.map +1 -0
- package/dist/types/probe/index.js +2 -0
- package/dist/types/probe/index.js.map +1 -0
- package/dist/types/probe/registry.d.ts +27 -0
- package/dist/types/probe/registry.d.ts.map +1 -0
- package/dist/types/probe/registry.js +2 -0
- package/dist/types/probe/registry.js.map +1 -0
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +5 -0
- package/dist/utils/logger.js.map +1 -1
- package/dist/vault/instrumentation.d.ts +11 -0
- package/dist/vault/instrumentation.d.ts.map +1 -0
- package/dist/vault/instrumentation.js +32 -0
- package/dist/vault/instrumentation.js.map +1 -0
- package/dist/vault/instrumentation.test.d.ts +2 -0
- package/dist/vault/instrumentation.test.d.ts.map +1 -0
- package/dist/vault/instrumentation.test.js +80 -0
- package/dist/vault/instrumentation.test.js.map +1 -0
- package/package.json +4 -1
- package/src/advisory/context.test.ts +109 -0
- package/src/advisory/evaluator.test.ts +192 -0
- package/src/advisory/executor.test.ts +272 -0
- package/src/advisory/registry.test.ts +75 -0
- package/src/bridge/a2a/agent-card.test.ts +140 -0
- package/src/bridge/a2a/mapper.test.ts +293 -0
- package/src/bridge/a2a/message.test.ts +138 -0
- package/src/bridge/a2a/task.test.ts +235 -0
- package/src/bridge/mcp/connector/adapter.test.ts +230 -0
- package/src/bridge/sse/mapper.test.ts +422 -0
- package/src/bridge/tools/connector/adapter.test.ts +224 -0
- package/src/bridge/tools/connector/definitions.test.ts +183 -0
- package/src/bridge/tools/connector/router.test.ts +159 -0
- package/src/bus/breaker.test.ts +274 -0
- package/src/bus/index.test.ts +183 -0
- package/src/bus/index.ts +21 -10
- package/src/bus/lock.test.ts +265 -0
- package/src/bus/ownership.test.ts +243 -0
- package/src/connector/BaseConnector.test.ts +130 -0
- package/src/connector/builtins/http.test.ts +290 -0
- package/src/connector/builtins/webhook.test.ts +138 -0
- package/src/connector/execution/factory.test.ts +75 -0
- package/src/connector/execution/remote.test.ts +63 -0
- package/src/connector/mcp/adapter.test.ts +249 -0
- package/src/probe/context.ts +14 -0
- package/src/probe/errors.ts +27 -0
- package/src/probe/index.ts +4 -0
- package/src/probe/registry.test.ts +480 -0
- package/src/probe/registry.ts +276 -0
- package/src/provider/instrumentation.test.ts +192 -0
- package/src/provider/instrumentation.ts +139 -0
- package/src/public-runtime.ts +17 -0
- package/src/public-types.ts +3 -0
- package/src/rag/chunking.test.ts +107 -0
- package/src/rag/context-assembler.test.ts +114 -0
- package/src/rag/embedding.test.ts +130 -0
- package/src/rag/ingestion.test.ts +114 -0
- package/src/rag/knowledge-base.test.ts +106 -0
- package/src/rag/rag-tool.test.ts +167 -0
- package/src/rag/retriever.test.ts +210 -0
- package/src/rag/vector-store.test.ts +196 -0
- package/src/registry/ManagedRegistry.test.ts +118 -0
- package/src/registry/Registry.test.ts +91 -0
- package/src/registry/agent/definitions.test.ts +100 -0
- package/src/registry/connector/definitions.test.ts +51 -0
- package/src/registry/connector/scoped.test.ts +129 -0
- package/src/registry/plugin/index.test.ts +85 -0
- package/src/registry/tool/execute.test.ts +330 -0
- package/src/runtime/query/events.ts +6 -1
- package/src/runtime/query/executor.ts +34 -0
- package/src/runtime/query/iteration/phases/advisory.test.ts +412 -0
- package/src/test-setup.ts +24 -0
- package/src/types/bus/index.ts +54 -2
- package/src/types/doctor/check.ts +53 -0
- package/src/types/doctor/index.ts +9 -0
- package/src/types/probe/event-kind.ts +8 -0
- package/src/types/probe/event-of.ts +3 -0
- package/src/types/probe/index.ts +11 -0
- package/src/types/probe/registry.ts +36 -0
- package/src/utils/logger.ts +6 -1
- package/src/vault/instrumentation.test.ts +98 -0
- package/src/vault/instrumentation.ts +56 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 5):
|
|
3
|
+
*
|
|
4
|
+
* - `HttpConnector.connect` stores config + auth, strips trailing
|
|
5
|
+
* slashes from baseUrl, merges default headers with auth-derived
|
|
6
|
+
* headers.
|
|
7
|
+
* - Auth resolution (http only — webhook has its own):
|
|
8
|
+
* - `api_key` with `apiKey` + optional `headerName` (default
|
|
9
|
+
* `X-API-Key`).
|
|
10
|
+
* - `bearer` with `token` → `Authorization: Bearer <token>`.
|
|
11
|
+
* - `basic` with `username` + `password` → base64 encoded.
|
|
12
|
+
* - `none` / `oauth2` / `custom` → no headers.
|
|
13
|
+
* - Missing required credential fields throw a typed error string.
|
|
14
|
+
* - `disconnect` clears internal state.
|
|
15
|
+
* - `healthCheck` HEAD-fetches baseUrl with a 5s timeout; returns
|
|
16
|
+
* true iff `response.ok || response.status < 500`; false on any
|
|
17
|
+
* thrown fetch (e.g. timeout abort).
|
|
18
|
+
* - `execute("request", input)`:
|
|
19
|
+
* - `requireMethod` + `validateInput` run.
|
|
20
|
+
* - Builds URL from `baseUrl + path` + appends query params.
|
|
21
|
+
* - Sends default+input headers; auto-sets `Content-Type:
|
|
22
|
+
* application/json` when a body is present and no content-type
|
|
23
|
+
* was passed.
|
|
24
|
+
* - Parses response JSON when `content-type: application/json`,
|
|
25
|
+
* else text.
|
|
26
|
+
* - `success: true` iff status in [200, 400). Metadata includes
|
|
27
|
+
* status + statusText.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
31
|
+
|
|
32
|
+
import { HttpConnector } from './http.js'
|
|
33
|
+
|
|
34
|
+
function makeResponse(init: {
|
|
35
|
+
status?: number
|
|
36
|
+
statusText?: string
|
|
37
|
+
headers?: Record<string, string>
|
|
38
|
+
body?: unknown
|
|
39
|
+
}) {
|
|
40
|
+
const headers = new Headers(init.headers ?? { 'content-type': 'application/json' })
|
|
41
|
+
return {
|
|
42
|
+
ok: (init.status ?? 200) < 400,
|
|
43
|
+
status: init.status ?? 200,
|
|
44
|
+
statusText: init.statusText ?? 'OK',
|
|
45
|
+
headers,
|
|
46
|
+
json: async () => init.body,
|
|
47
|
+
text: async () => String(init.body ?? ''),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('HttpConnector', () => {
|
|
52
|
+
let fetchMock: ReturnType<typeof vi.fn>
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
fetchMock = vi.fn()
|
|
56
|
+
global.fetch = fetchMock as unknown as typeof fetch
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
vi.restoreAllMocks()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('connect + disconnect', () => {
|
|
64
|
+
it('strips trailing slashes from baseUrl', async () => {
|
|
65
|
+
const c = new HttpConnector()
|
|
66
|
+
await c.connect({ baseUrl: 'https://api.example.com//', timeoutMs: 30_000 })
|
|
67
|
+
// Follow-up request lands on the cleaned URL:
|
|
68
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: { ok: true } }))
|
|
69
|
+
await c.execute('request', { method: 'GET', path: 'x' })
|
|
70
|
+
expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/x', expect.any(Object))
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('disconnect clears internal state; execute after disconnect treats baseUrl as empty', async () => {
|
|
74
|
+
const c = new HttpConnector()
|
|
75
|
+
await c.connect({ baseUrl: 'https://api.example.com', timeoutMs: 30_000 })
|
|
76
|
+
await c.disconnect()
|
|
77
|
+
expect(await c.healthCheck()).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('auth resolution', () => {
|
|
82
|
+
it('api_key default header name = X-API-Key', async () => {
|
|
83
|
+
const c = new HttpConnector()
|
|
84
|
+
await c.connect(
|
|
85
|
+
{ baseUrl: 'https://api.example.com' },
|
|
86
|
+
{ type: 'api_key', credentials: { apiKey: 'secret' } },
|
|
87
|
+
)
|
|
88
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: {} }))
|
|
89
|
+
await c.execute('request', { method: 'GET', path: 'x' })
|
|
90
|
+
const headers = fetchMock.mock.calls[0]?.[1].headers as Record<string, string>
|
|
91
|
+
expect(headers['X-API-Key']).toBe('secret')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('api_key custom header name is honored', async () => {
|
|
95
|
+
const c = new HttpConnector()
|
|
96
|
+
await c.connect(
|
|
97
|
+
{ baseUrl: 'https://api.example.com' },
|
|
98
|
+
{ type: 'api_key', credentials: { apiKey: 'secret', headerName: 'X-Custom' } },
|
|
99
|
+
)
|
|
100
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: {} }))
|
|
101
|
+
await c.execute('request', { method: 'GET', path: 'x' })
|
|
102
|
+
const headers = fetchMock.mock.calls[0]?.[1].headers as Record<string, string>
|
|
103
|
+
expect(headers['X-Custom']).toBe('secret')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('bearer sets Authorization: Bearer <token>', async () => {
|
|
107
|
+
const c = new HttpConnector()
|
|
108
|
+
await c.connect(
|
|
109
|
+
{ baseUrl: 'https://api.example.com' },
|
|
110
|
+
{ type: 'bearer', credentials: { token: 'tkn' } },
|
|
111
|
+
)
|
|
112
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: {} }))
|
|
113
|
+
await c.execute('request', { method: 'GET', path: 'x' })
|
|
114
|
+
const headers = fetchMock.mock.calls[0]?.[1].headers as Record<string, string>
|
|
115
|
+
expect(headers.Authorization).toBe('Bearer tkn')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('basic encodes username:password as base64', async () => {
|
|
119
|
+
const c = new HttpConnector()
|
|
120
|
+
await c.connect(
|
|
121
|
+
{ baseUrl: 'https://api.example.com' },
|
|
122
|
+
{ type: 'basic', credentials: { username: 'alice', password: 'pw' } },
|
|
123
|
+
)
|
|
124
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: {} }))
|
|
125
|
+
await c.execute('request', { method: 'GET', path: 'x' })
|
|
126
|
+
const headers = fetchMock.mock.calls[0]?.[1].headers as Record<string, string>
|
|
127
|
+
expect(headers.Authorization).toBe(`Basic ${btoa('alice:pw')}`)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('none / oauth2 / custom add no auth headers', async () => {
|
|
131
|
+
for (const type of ['none', 'oauth2', 'custom'] as const) {
|
|
132
|
+
const c = new HttpConnector()
|
|
133
|
+
await c.connect({ baseUrl: 'https://api.example.com' }, { type, credentials: {} })
|
|
134
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: {} }))
|
|
135
|
+
await c.execute('request', { method: 'GET', path: 'x' })
|
|
136
|
+
const headers = fetchMock.mock.calls.at(-1)?.[1].headers as Record<string, string>
|
|
137
|
+
expect(headers.Authorization).toBeUndefined()
|
|
138
|
+
expect(headers['X-API-Key']).toBeUndefined()
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('api_key throws when apiKey is missing', async () => {
|
|
143
|
+
const c = new HttpConnector()
|
|
144
|
+
await expect(
|
|
145
|
+
c.connect({ baseUrl: 'https://api.example.com' }, { type: 'api_key', credentials: {} }),
|
|
146
|
+
).rejects.toThrow(/missing required credential "apiKey"/)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('bearer throws when token is missing', async () => {
|
|
150
|
+
const c = new HttpConnector()
|
|
151
|
+
await expect(
|
|
152
|
+
c.connect({ baseUrl: 'https://api.example.com' }, { type: 'bearer', credentials: {} }),
|
|
153
|
+
).rejects.toThrow(/missing required credential "token"/)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('basic throws when either username or password is missing', async () => {
|
|
157
|
+
const c = new HttpConnector()
|
|
158
|
+
await expect(
|
|
159
|
+
c.connect(
|
|
160
|
+
{ baseUrl: 'https://api.example.com' },
|
|
161
|
+
{ type: 'basic', credentials: { username: 'a' } },
|
|
162
|
+
),
|
|
163
|
+
).rejects.toThrow(/missing required credentials/)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('healthCheck', () => {
|
|
168
|
+
it('returns true for ok responses', async () => {
|
|
169
|
+
const c = new HttpConnector()
|
|
170
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
171
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200 }))
|
|
172
|
+
expect(await c.healthCheck()).toBe(true)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('returns true for 4xx (not-ok but < 500)', async () => {
|
|
176
|
+
const c = new HttpConnector()
|
|
177
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
178
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 404 }))
|
|
179
|
+
expect(await c.healthCheck()).toBe(true)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('returns false for 5xx', async () => {
|
|
183
|
+
const c = new HttpConnector()
|
|
184
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
185
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 503 }))
|
|
186
|
+
expect(await c.healthCheck()).toBe(false)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('returns false on thrown fetch', async () => {
|
|
190
|
+
const c = new HttpConnector()
|
|
191
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
192
|
+
fetchMock.mockRejectedValueOnce(new Error('timeout'))
|
|
193
|
+
expect(await c.healthCheck()).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('returns false when never connected', async () => {
|
|
197
|
+
const c = new HttpConnector()
|
|
198
|
+
expect(await c.healthCheck()).toBe(false)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('execute', () => {
|
|
203
|
+
it('returns success:true for 2xx + 3xx responses', async () => {
|
|
204
|
+
const c = new HttpConnector()
|
|
205
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
206
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: { ok: 1 } }))
|
|
207
|
+
const result = await c.execute('request', { method: 'GET', path: 'thing' })
|
|
208
|
+
expect(result.success).toBe(true)
|
|
209
|
+
expect(result.output).toMatchObject({ status: 200 })
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('returns success:false for 4xx / 5xx responses', async () => {
|
|
213
|
+
const c = new HttpConnector()
|
|
214
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
215
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 500, body: { err: 1 } }))
|
|
216
|
+
const result = await c.execute('request', { method: 'GET', path: 'thing' })
|
|
217
|
+
expect(result.success).toBe(false)
|
|
218
|
+
expect(result.metadata).toMatchObject({ status: 500 })
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('sets Content-Type: application/json when body is set and none provided', async () => {
|
|
222
|
+
const c = new HttpConnector()
|
|
223
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
224
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: {} }))
|
|
225
|
+
await c.execute('request', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
path: 'thing',
|
|
228
|
+
body: { k: 'v' },
|
|
229
|
+
})
|
|
230
|
+
const headers = fetchMock.mock.calls[0]?.[1].headers as Record<string, string>
|
|
231
|
+
expect(headers['Content-Type']).toBe('application/json')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('preserves caller-supplied Content-Type', async () => {
|
|
235
|
+
const c = new HttpConnector()
|
|
236
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
237
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: '' }))
|
|
238
|
+
await c.execute('request', {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
path: 'thing',
|
|
241
|
+
body: 'raw',
|
|
242
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
243
|
+
})
|
|
244
|
+
const headers = fetchMock.mock.calls[0]?.[1].headers as Record<string, string>
|
|
245
|
+
expect(headers['Content-Type']).toBe('text/plain')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('appends query params to the URL', async () => {
|
|
249
|
+
const c = new HttpConnector()
|
|
250
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
251
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: {} }))
|
|
252
|
+
await c.execute('request', {
|
|
253
|
+
method: 'GET',
|
|
254
|
+
path: 'thing',
|
|
255
|
+
query: { a: '1', b: '2' },
|
|
256
|
+
})
|
|
257
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe('https://api.example.com/thing?a=1&b=2')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('parses JSON response when content-type is json', async () => {
|
|
261
|
+
const c = new HttpConnector()
|
|
262
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
263
|
+
fetchMock.mockResolvedValueOnce(
|
|
264
|
+
makeResponse({
|
|
265
|
+
status: 200,
|
|
266
|
+
headers: { 'content-type': 'application/json' },
|
|
267
|
+
body: { ok: 1 },
|
|
268
|
+
}),
|
|
269
|
+
)
|
|
270
|
+
const result = await c.execute('request', { method: 'GET', path: 'x' })
|
|
271
|
+
expect((result.output as { body: unknown }).body).toEqual({ ok: 1 })
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('returns text body for non-JSON content-type', async () => {
|
|
275
|
+
const c = new HttpConnector()
|
|
276
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
277
|
+
fetchMock.mockResolvedValueOnce(
|
|
278
|
+
makeResponse({ status: 200, headers: { 'content-type': 'text/plain' }, body: 'hello' }),
|
|
279
|
+
)
|
|
280
|
+
const result = await c.execute('request', { method: 'GET', path: 'x' })
|
|
281
|
+
expect((result.output as { body: unknown }).body).toBe('hello')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('throws on invalid input (unknown method)', async () => {
|
|
285
|
+
const c = new HttpConnector()
|
|
286
|
+
await c.connect({ baseUrl: 'https://api.example.com' })
|
|
287
|
+
await expect(c.execute('not-a-method', {})).rejects.toThrow(/not found/)
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 5):
|
|
3
|
+
*
|
|
4
|
+
* - `WebhookConnector.connect` stores config + auth; merges default
|
|
5
|
+
* headers; sets `Authorization: Bearer <token>` iff auth is
|
|
6
|
+
* bearer with a token (other auth types are ignored here).
|
|
7
|
+
* - `disconnect` clears state.
|
|
8
|
+
* - `healthCheck` HEAD-fetches the configured url; true iff
|
|
9
|
+
* `ok || status < 500`; false on thrown fetch or empty url.
|
|
10
|
+
* - `execute("send", input)`:
|
|
11
|
+
* - Validates input via zod.
|
|
12
|
+
* - Posts JSON to `input.url ?? config.url`.
|
|
13
|
+
* - Always sets `Content-Type: application/json`.
|
|
14
|
+
* - When `config.secret` is set, computes HMAC-SHA256 over the
|
|
15
|
+
* stringified payload and sets `X-Webhook-Signature: sha256=<hex>`.
|
|
16
|
+
* - `success: true` iff `status in [200, 400)`.
|
|
17
|
+
* - `metadata.deliveredAt` is a recent timestamp.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createHmac } from 'node:crypto'
|
|
21
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
22
|
+
|
|
23
|
+
import { WebhookConnector } from './webhook.js'
|
|
24
|
+
|
|
25
|
+
function makeResponse(init: {
|
|
26
|
+
status?: number
|
|
27
|
+
headers?: Record<string, string>
|
|
28
|
+
body?: unknown
|
|
29
|
+
}) {
|
|
30
|
+
const headers = new Headers(init.headers ?? { 'content-type': 'application/json' })
|
|
31
|
+
return {
|
|
32
|
+
ok: (init.status ?? 200) < 400,
|
|
33
|
+
status: init.status ?? 200,
|
|
34
|
+
statusText: 'OK',
|
|
35
|
+
headers,
|
|
36
|
+
json: async () => init.body,
|
|
37
|
+
text: async () => String(init.body ?? ''),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('WebhookConnector', () => {
|
|
42
|
+
let fetchMock: ReturnType<typeof vi.fn>
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
fetchMock = vi.fn()
|
|
46
|
+
global.fetch = fetchMock as unknown as typeof fetch
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.restoreAllMocks()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('connect + disconnect round-trip state', async () => {
|
|
54
|
+
const c = new WebhookConnector()
|
|
55
|
+
await c.connect({ url: 'https://hook.example.com/x' })
|
|
56
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200 }))
|
|
57
|
+
expect(await c.healthCheck()).toBe(true)
|
|
58
|
+
await c.disconnect()
|
|
59
|
+
expect(await c.healthCheck()).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('bearer auth sets Authorization header on send', async () => {
|
|
63
|
+
const c = new WebhookConnector()
|
|
64
|
+
await c.connect(
|
|
65
|
+
{ url: 'https://hook.example.com' },
|
|
66
|
+
{ type: 'bearer', credentials: { token: 'tkn' } },
|
|
67
|
+
)
|
|
68
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: 'ok' }))
|
|
69
|
+
await c.execute('send', { payload: {} })
|
|
70
|
+
const headers = fetchMock.mock.calls[0]?.[1].headers as Record<string, string>
|
|
71
|
+
expect(headers.Authorization).toBe('Bearer tkn')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('send posts JSON to the configured URL', async () => {
|
|
75
|
+
const c = new WebhookConnector()
|
|
76
|
+
await c.connect({ url: 'https://hook.example.com' })
|
|
77
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: 'ok' }))
|
|
78
|
+
await c.execute('send', { payload: { k: 'v' } })
|
|
79
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
80
|
+
'https://hook.example.com',
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
method: 'POST',
|
|
83
|
+
body: JSON.stringify({ k: 'v' }),
|
|
84
|
+
}),
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('input.url overrides the configured URL', async () => {
|
|
89
|
+
const c = new WebhookConnector()
|
|
90
|
+
await c.connect({ url: 'https://default.example.com' })
|
|
91
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: 'ok' }))
|
|
92
|
+
await c.execute('send', { payload: {}, url: 'https://override.example.com' })
|
|
93
|
+
expect(fetchMock).toHaveBeenCalledWith('https://override.example.com', expect.any(Object))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('includes HMAC signature when secret is configured', async () => {
|
|
97
|
+
const secret = 's3cret'
|
|
98
|
+
const c = new WebhookConnector()
|
|
99
|
+
await c.connect({ url: 'https://hook.example.com', secret })
|
|
100
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: 'ok' }))
|
|
101
|
+
await c.execute('send', { payload: { k: 'v' } })
|
|
102
|
+
const headers = fetchMock.mock.calls[0]?.[1].headers as Record<string, string>
|
|
103
|
+
const expected = `sha256=${createHmac('sha256', secret)
|
|
104
|
+
.update(JSON.stringify({ k: 'v' }))
|
|
105
|
+
.digest('hex')}`
|
|
106
|
+
expect(headers['X-Webhook-Signature']).toBe(expected)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('omits HMAC signature when no secret is configured', async () => {
|
|
110
|
+
const c = new WebhookConnector()
|
|
111
|
+
await c.connect({ url: 'https://hook.example.com' })
|
|
112
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: 'ok' }))
|
|
113
|
+
await c.execute('send', { payload: {} })
|
|
114
|
+
const headers = fetchMock.mock.calls[0]?.[1].headers as Record<string, string>
|
|
115
|
+
expect(headers['X-Webhook-Signature']).toBeUndefined()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('success: true for 2xx; false for 4xx/5xx', async () => {
|
|
119
|
+
const c = new WebhookConnector()
|
|
120
|
+
await c.connect({ url: 'https://hook.example.com' })
|
|
121
|
+
|
|
122
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: 'ok' }))
|
|
123
|
+
expect((await c.execute('send', { payload: {} })).success).toBe(true)
|
|
124
|
+
|
|
125
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 500, body: 'err' }))
|
|
126
|
+
expect((await c.execute('send', { payload: {} })).success).toBe(false)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('metadata.deliveredAt is a recent timestamp', async () => {
|
|
130
|
+
const before = Date.now()
|
|
131
|
+
const c = new WebhookConnector()
|
|
132
|
+
await c.connect({ url: 'https://hook.example.com' })
|
|
133
|
+
fetchMock.mockResolvedValueOnce(makeResponse({ status: 200, body: 'ok' }))
|
|
134
|
+
const result = await c.execute('send', { payload: {} })
|
|
135
|
+
const delivered = (result.metadata as { deliveredAt: number }).deliveredAt
|
|
136
|
+
expect(delivered).toBeGreaterThanOrEqual(before)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 5):
|
|
3
|
+
*
|
|
4
|
+
* - `ExecutionContextFactory.create(config)` dispatches by
|
|
5
|
+
* `config.environment`:
|
|
6
|
+
* - 'local' → `LocalExecutionContext` with the forwarded fields.
|
|
7
|
+
* - 'remote' → `RemoteExecutionContext` with target + capabilities.
|
|
8
|
+
* - 'hybrid' → `HybridExecutionContext` with local + remotes +
|
|
9
|
+
* routingStrategy.
|
|
10
|
+
* - Unknown environment hits the exhaustive throw (unreachable via
|
|
11
|
+
* types).
|
|
12
|
+
* - The static `createLocal` / `createRemote` / `createHybrid`
|
|
13
|
+
* helpers directly return the appropriate subclass.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, it } from 'vitest'
|
|
17
|
+
|
|
18
|
+
import { LocalExecutionContext } from '../../execution/local.js'
|
|
19
|
+
import { ExecutionContextFactory } from './factory.js'
|
|
20
|
+
import { HybridExecutionContext } from './hybrid.js'
|
|
21
|
+
import { RemoteExecutionContext } from './remote.js'
|
|
22
|
+
|
|
23
|
+
describe('ExecutionContextFactory', () => {
|
|
24
|
+
it('creates a LocalExecutionContext for environment: local', () => {
|
|
25
|
+
const ctx = ExecutionContextFactory.create({
|
|
26
|
+
id: 'c1',
|
|
27
|
+
environment: 'local',
|
|
28
|
+
cwd: '/tmp',
|
|
29
|
+
fsAccess: true,
|
|
30
|
+
})
|
|
31
|
+
expect(ctx).toBeInstanceOf(LocalExecutionContext)
|
|
32
|
+
expect(ctx.id).toBe('c1')
|
|
33
|
+
expect(ctx.environment).toBe('local')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('creates a RemoteExecutionContext for environment: remote', () => {
|
|
37
|
+
const ctx = ExecutionContextFactory.create({
|
|
38
|
+
id: 'c2',
|
|
39
|
+
environment: 'remote',
|
|
40
|
+
target: { type: 'ssh', host: 'server.example.com' },
|
|
41
|
+
})
|
|
42
|
+
expect(ctx).toBeInstanceOf(RemoteExecutionContext)
|
|
43
|
+
expect(ctx.environment).toBe('remote')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('creates a HybridExecutionContext for environment: hybrid', () => {
|
|
47
|
+
const ctx = ExecutionContextFactory.create({
|
|
48
|
+
id: 'c3',
|
|
49
|
+
environment: 'hybrid',
|
|
50
|
+
local: { cwd: '/tmp', fsAccess: true },
|
|
51
|
+
remotes: [{ type: 'ssh', host: 'r1.example.com' }],
|
|
52
|
+
})
|
|
53
|
+
expect(ctx).toBeInstanceOf(HybridExecutionContext)
|
|
54
|
+
expect(ctx.environment).toBe('hybrid')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('createLocal / createRemote / createHybrid return the right subclass', () => {
|
|
58
|
+
expect(
|
|
59
|
+
ExecutionContextFactory.createLocal({ id: 'x', cwd: '/tmp', fsAccess: true }),
|
|
60
|
+
).toBeInstanceOf(LocalExecutionContext)
|
|
61
|
+
expect(
|
|
62
|
+
ExecutionContextFactory.createRemote({
|
|
63
|
+
id: 'y',
|
|
64
|
+
target: { type: 'ssh', host: 'h' },
|
|
65
|
+
}),
|
|
66
|
+
).toBeInstanceOf(RemoteExecutionContext)
|
|
67
|
+
expect(
|
|
68
|
+
ExecutionContextFactory.createHybrid({
|
|
69
|
+
id: 'z',
|
|
70
|
+
local: { cwd: '/tmp', fsAccess: true },
|
|
71
|
+
remotes: [],
|
|
72
|
+
}),
|
|
73
|
+
).toBeInstanceOf(HybridExecutionContext)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 5):
|
|
3
|
+
*
|
|
4
|
+
* - `RemoteExecutionContext` starts disconnected; `connect()` flips
|
|
5
|
+
* `connected = true` and emits `remote_connected`.
|
|
6
|
+
* - `disconnect()` is idempotent — the second call emits nothing
|
|
7
|
+
* and returns quietly.
|
|
8
|
+
* - `getTarget()` returns a COPY (mutating the returned object does
|
|
9
|
+
* not mutate internal state).
|
|
10
|
+
* - `initialize()` succeeds for supported target types; emits
|
|
11
|
+
* `context_initialized` + `context_ready`.
|
|
12
|
+
* - `isReady()` reflects post-initialize state.
|
|
13
|
+
* - `environment` is 'remote'.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, it } from 'vitest'
|
|
17
|
+
|
|
18
|
+
import type { RemoteTarget } from '../../types/connector/index.js'
|
|
19
|
+
|
|
20
|
+
import { RemoteExecutionContext } from './remote.js'
|
|
21
|
+
|
|
22
|
+
const target: RemoteTarget = { type: 'ssh', host: 'server.example.com', port: 22 }
|
|
23
|
+
|
|
24
|
+
describe('RemoteExecutionContext', () => {
|
|
25
|
+
it('environment is remote', () => {
|
|
26
|
+
const r = new RemoteExecutionContext({ id: 'r1', target })
|
|
27
|
+
expect(r.environment).toBe('remote')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('starts disconnected; connect flips to connected', async () => {
|
|
31
|
+
const r = new RemoteExecutionContext({ id: 'r1', target })
|
|
32
|
+
expect(r.isConnected()).toBe(false)
|
|
33
|
+
await r.connect()
|
|
34
|
+
expect(r.isConnected()).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('disconnect when not connected is a no-op', async () => {
|
|
38
|
+
const r = new RemoteExecutionContext({ id: 'r1', target })
|
|
39
|
+
await r.disconnect()
|
|
40
|
+
expect(r.isConnected()).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('disconnect flips back to disconnected', async () => {
|
|
44
|
+
const r = new RemoteExecutionContext({ id: 'r1', target })
|
|
45
|
+
await r.connect()
|
|
46
|
+
await r.disconnect()
|
|
47
|
+
expect(r.isConnected()).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('getTarget returns a copy (mutation does not leak)', () => {
|
|
51
|
+
const r = new RemoteExecutionContext({ id: 'r1', target })
|
|
52
|
+
const copy = r.getTarget()
|
|
53
|
+
copy.host = 'mutated.example.com'
|
|
54
|
+
expect(r.getTarget().host).toBe('server.example.com')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('initialize flips isReady to true', async () => {
|
|
58
|
+
const r = new RemoteExecutionContext({ id: 'r1', target })
|
|
59
|
+
expect(r.isReady()).toBe(false)
|
|
60
|
+
await r.initialize()
|
|
61
|
+
expect(r.isReady()).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
})
|