@pikku/core 0.12.78 → 0.12.80
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 +234 -0
- package/dist/dev/hot-reload.js +1 -6
- package/dist/ecosystem.d.ts +27 -0
- package/dist/ecosystem.js +26 -0
- package/dist/errors/error-handler.js +4 -2
- package/dist/function/function-runner.js +20 -42
- package/dist/function/functions.types.d.ts +9 -5
- package/dist/index.d.ts +4 -9
- package/dist/index.js +3 -8
- package/dist/middleware/auth-cookie.d.ts +0 -4
- package/dist/middleware/auth-cookie.js +38 -2
- package/dist/middleware/cors.js +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.js +14 -3
- package/dist/permissions.d.ts +8 -10
- package/dist/permissions.js +0 -11
- package/dist/pikku-state.js +12 -5
- package/dist/schema.js +35 -1
- package/dist/services/ai-run-state-service.d.ts +7 -1
- package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
- package/dist/services/in-memory-ai-run-state-service.js +2 -1
- package/dist/services/in-memory-workflow-service.d.ts +10 -0
- package/dist/services/in-memory-workflow-service.js +25 -0
- package/dist/services/local-content-request-handler.d.ts +21 -0
- package/dist/services/local-content-request-handler.js +72 -53
- package/dist/services/local-content.d.ts +6 -0
- package/dist/services/local-content.js +14 -1
- package/dist/services/workflow-service.d.ts +4 -2
- package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
- package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
- package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
- package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/channel-store-tests.js +72 -0
- package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/credential-service-tests.js +109 -0
- package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/deployment-service-tests.js +21 -0
- package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
- package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/secret-service-tests.js +80 -0
- package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/session-store-tests.js +43 -0
- package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
- package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-service-tests.js +150 -0
- package/dist/testing/service-tests.d.ts +6 -0
- package/dist/testing/service-tests.js +26 -791
- package/dist/types/core.types.d.ts +11 -1
- package/dist/types/state.types.d.ts +1 -1
- package/dist/utils/node-host-resolver.d.ts +12 -0
- package/dist/utils/node-host-resolver.js +16 -0
- package/dist/utils/safe-fetch.d.ts +18 -0
- package/dist/utils/safe-fetch.js +167 -29
- package/dist/wirings/actor-flow/run-conversation.js +1 -6
- package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
- package/dist/wirings/ai-agent/agent-rpc.js +53 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
- package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
- package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
- package/dist/wirings/ai-agent/voice-input.js +1 -6
- package/dist/wirings/ai-agent/voice-output.js +2 -12
- package/dist/wirings/channel/channel-common.js +1 -0
- package/dist/wirings/channel/channel-handler.js +3 -5
- package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
- package/dist/wirings/channel/channel-rpc-service.js +0 -8
- package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
- package/dist/wirings/channel/channel-rpc.types.js +8 -0
- package/dist/wirings/channel/channel-runner.d.ts +1 -3
- package/dist/wirings/channel/channel-runner.js +16 -8
- package/dist/wirings/channel/channel.types.d.ts +2 -0
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
- package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
- package/dist/wirings/cli/cli-runner.js +4 -2
- package/dist/wirings/cli/cli.types.d.ts +0 -8
- package/dist/wirings/cli/command-parser.js +13 -0
- package/dist/wirings/gateway/gateway-runner.js +41 -5
- package/dist/wirings/http/http-routes.js +2 -0
- package/dist/wirings/http/http-runner.d.ts +0 -10
- package/dist/wirings/http/http-runner.js +6 -13
- package/dist/wirings/http/http.types.d.ts +0 -10
- package/dist/wirings/http/index.d.ts +1 -1
- package/dist/wirings/http/index.js +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
- package/dist/wirings/mcp/mcp-runner.js +0 -6
- package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
- package/dist/wirings/rpc/rpc-runner.js +53 -86
- package/dist/wirings/rpc/rpc-types.d.ts +3 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -0
- package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
- package/dist/wirings/trigger/trigger-runner.js +1 -0
- package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
- package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
- package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
- package/dist/wirings/workflow/feature.js +7 -4
- package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
- package/dist/wirings/workflow/graph/graph-runner.js +8 -7
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
- package/dist/wirings/workflow/index.d.ts +7 -2
- package/dist/wirings/workflow/index.js +5 -1
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
- package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +44 -111
- package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
- package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
- package/dist/wirings/workflow/workflow-approval.js +114 -0
- package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
- package/dist/wirings/workflow/workflow-constants.js +35 -0
- package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
- package/dist/wirings/workflow/workflow-errors.js +112 -0
- package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
- package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
- package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
- package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
- package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
- package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
- package/dist/wirings/workflow/workflow-recovery.js +101 -0
- package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
- package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
- package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
- package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
- package/dist/wirings/workflow/workflow-suspend.js +33 -0
- package/dist/wirings/workflow/workflow.types.d.ts +7 -0
- package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
- package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
- package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
- package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
- package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
- package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
- package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
- package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
- package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
- package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
- package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
- package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
- package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
- package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
- package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
- package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
- package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
- package/knowledge/decisions/internals/index.md +38 -26
- package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
- package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
- package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
- package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
- package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
- package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
- package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
- package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
- package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
- package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
- package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
- package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
- package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
- package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
- package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
- package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
- package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
- package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
- package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
- package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
- package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
- package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
- package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
- package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
- package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
- package/knowledge/decisions/security/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
- package/knowledge/decisions/security/index.md +7 -0
- package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
- package/knowledge/questions/index.md +2 -1
- package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
- package/package.json +14 -3
- package/scripts/generate-api-report.d.mts +1 -0
- package/scripts/generate-api-report.mjs +89 -0
- package/scripts/generate-api-report.mts +218 -0
- package/src/api-report.test.ts +69 -0
- package/src/column-form.ts +1 -4
- package/src/crypto-utils.test.ts +15 -2
- package/src/dev/hot-reload.ts +2 -7
- package/src/ecosystem.ts +40 -0
- package/src/errors/error-handler.ts +5 -3
- package/src/errors/error.test.ts +4 -1
- package/src/function/function-runner.test.ts +1 -1
- package/src/function/function-runner.ts +28 -39
- package/src/function/functions.types.ts +15 -9
- package/src/handle-error.ts +1 -1
- package/src/index.ts +3 -22
- package/src/middleware/auth-cookie.test.ts +50 -0
- package/src/middleware/auth-cookie.ts +38 -2
- package/src/middleware/cors.ts +2 -1
- package/src/middleware/index.ts +1 -1
- package/src/middleware/remote-auth.test.ts +43 -0
- package/src/middleware/remote-auth.ts +17 -3
- package/src/middleware-runner.ts +4 -2
- package/src/no-any-casts.test.ts +57 -0
- package/src/permissions.test.ts +3 -1
- package/src/permissions.ts +22 -24
- package/src/pikku-state.ts +17 -6
- package/src/public-surface.json +578 -0
- package/src/public-surface.json.README +25 -0
- package/src/public-surface.test.ts +105 -0
- package/src/removed-legacy-exports.test.ts +62 -0
- package/src/schema.test.ts +78 -0
- package/src/schema.ts +36 -1
- package/src/services/ai-run-state-service.ts +7 -1
- package/src/services/audit-service.ts +2 -2
- package/src/services/in-memory-ai-run-state-service.ts +3 -2
- package/src/services/in-memory-workflow-service.ts +25 -0
- package/src/services/index.ts +1 -4
- package/src/services/local-content-request-handler.test.ts +43 -5
- package/src/services/local-content-request-handler.ts +103 -74
- package/src/services/local-content.ts +15 -1
- package/src/services/local-email-service.ts +5 -1
- package/src/services/system-role-guard.test.ts +4 -1
- package/src/services/workflow-service.ts +9 -2
- package/src/side-effects-are-declared.test.ts +84 -0
- package/src/source-files-stay-composable.test.ts +41 -0
- package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
- package/src/testing/service-tests/channel-store-tests.ts +98 -0
- package/src/testing/service-tests/credential-service-tests.ts +143 -0
- package/src/testing/service-tests/deployment-service-tests.ts +33 -0
- package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
- package/src/testing/service-tests/secret-service-tests.ts +105 -0
- package/src/testing/service-tests/session-store-tests.ts +62 -0
- package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
- package/src/testing/service-tests/workflow-service-tests.ts +308 -0
- package/src/testing/service-tests.ts +31 -1111
- package/src/types/core.types.ts +15 -1
- package/src/types/state.types.ts +1 -1
- package/src/utils/node-host-resolver.ts +20 -0
- package/src/utils/safe-fetch.test.ts +143 -1
- package/src/utils/safe-fetch.ts +200 -25
- package/src/wirings/actor-flow/run-conversation.ts +1 -6
- package/src/wirings/ai-agent/agent-rpc.ts +120 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
- package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
- package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
- package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
- package/src/wirings/ai-agent/voice-input.ts +1 -6
- package/src/wirings/ai-agent/voice-output.ts +2 -12
- package/src/wirings/channel/channel-common.ts +5 -2
- package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
- package/src/wirings/channel/channel-handler.ts +7 -7
- package/src/wirings/channel/channel-rpc-service.ts +0 -14
- package/src/wirings/channel/channel-rpc.test.ts +25 -4
- package/src/wirings/channel/channel-rpc.types.ts +14 -0
- package/src/wirings/channel/channel-runner.ts +33 -20
- package/src/wirings/channel/channel.types.ts +2 -0
- package/src/wirings/channel/local/local-channel-runner.ts +1 -1
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
- package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
- package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
- package/src/wirings/cli/cli-runner.ts +7 -4
- package/src/wirings/cli/cli.types.ts +0 -39
- package/src/wirings/cli/command-parser.test.ts +19 -0
- package/src/wirings/cli/command-parser.ts +17 -1
- package/src/wirings/gateway/gateway-authorization.test.ts +131 -0
- package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
- package/src/wirings/gateway/gateway-runner.ts +62 -22
- package/src/wirings/http/http-routes.ts +4 -1
- package/src/wirings/http/http-runner.ts +11 -26
- package/src/wirings/http/http.types.ts +0 -15
- package/src/wirings/http/index.ts +1 -7
- package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
- package/src/wirings/http/web-request.ts +1 -1
- package/src/wirings/mcp/mcp-runner.ts +2 -16
- package/src/wirings/persona/persona-environments.test.ts +14 -3
- package/src/wirings/persona/persona.test.ts +13 -3
- package/src/wirings/persona/validate-personas.ts +5 -1
- package/src/wirings/queue/queue-runner.ts +1 -1
- package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
- package/src/wirings/rpc/rpc-runner.ts +58 -136
- package/src/wirings/rpc/rpc-types.ts +7 -0
- package/src/wirings/rpc/wire-addon.ts +3 -1
- package/src/wirings/secret/validate-secret-definitions.test.ts +69 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
- package/src/wirings/trigger/trigger-runner.ts +7 -5
- package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
- package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
- package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
- package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
- package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
- package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
- package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
- package/src/wirings/workflow/feature.ts +7 -4
- package/src/wirings/workflow/graph/graph-node.ts +1 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
- package/src/wirings/workflow/graph/graph-runner.ts +13 -16
- package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
- package/src/wirings/workflow/index.ts +12 -4
- package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
- package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +208 -630
- package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
- package/src/wirings/workflow/workflow-approval.ts +185 -0
- package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
- package/src/wirings/workflow/workflow-constants.ts +47 -0
- package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
- package/src/wirings/workflow/workflow-dispatch-relay.test.ts +128 -0
- package/src/wirings/workflow/workflow-errors.ts +128 -0
- package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
- package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
- package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
- package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
- package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
- package/src/wirings/workflow/workflow-recovery.ts +162 -0
- package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
- package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
- package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
- package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
- package/src/wirings/workflow/workflow-suspend.ts +61 -0
- package/src/wirings/workflow/workflow.types.ts +7 -0
- package/src/wirings-stay-decoupled.test.ts +123 -0
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/src/internal.ts +0 -10
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { InMemoryWorkflowService } from '../../services/in-memory-workflow-service.js'
|
|
5
|
+
import { pikkuState } from '../../pikku-state.js'
|
|
6
|
+
import { runWorkflowGraph } from './graph/graph-runner.js'
|
|
7
|
+
import { assertWorkflowRunOwner } from './workflow-run-ownership.js'
|
|
8
|
+
import { WorkflowStepFunctionMismatchError } from './workflow-errors.js'
|
|
9
|
+
import { ForbiddenError } from '../../errors/errors.js'
|
|
10
|
+
|
|
11
|
+
const silentLogger = { error() {}, info() {}, warn() {}, debug() {} }
|
|
12
|
+
|
|
13
|
+
const seedSingletons = () =>
|
|
14
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
15
|
+
logger: silentLogger,
|
|
16
|
+
// `resumeWorkflow` hands the run back to the orchestrator queue; the tests
|
|
17
|
+
// here assert on what happened before that hand-off.
|
|
18
|
+
queueService: { add: async () => 'job-1' },
|
|
19
|
+
} as any)
|
|
20
|
+
|
|
21
|
+
describe('a graph run starts at an entry node the graph declared', () => {
|
|
22
|
+
const meta = {
|
|
23
|
+
name: 'payout',
|
|
24
|
+
pikkuFuncId: 'payout',
|
|
25
|
+
source: 'graph' as const,
|
|
26
|
+
entryNodeIds: ['validate'],
|
|
27
|
+
graphHash: 'payout-hash',
|
|
28
|
+
nodes: {
|
|
29
|
+
validate: { nodeId: 'validate', rpcName: 'validatePayout' },
|
|
30
|
+
// Reachable only after validation, but its input is a literal — so
|
|
31
|
+
// `areDependenciesSatisfied` alone would happily start the run here.
|
|
32
|
+
transfer: {
|
|
33
|
+
nodeId: 'transfer',
|
|
34
|
+
rpcName: 'transferFunds',
|
|
35
|
+
input: { amount: 1000 },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('a startNode outside entryNodeIds is rejected', async () => {
|
|
41
|
+
seedSingletons()
|
|
42
|
+
const ws = new InMemoryWorkflowService()
|
|
43
|
+
|
|
44
|
+
await assert.rejects(
|
|
45
|
+
runWorkflowGraph(
|
|
46
|
+
ws,
|
|
47
|
+
'payout',
|
|
48
|
+
{},
|
|
49
|
+
{ rpcWithWire: async () => ({}) },
|
|
50
|
+
true,
|
|
51
|
+
'transfer',
|
|
52
|
+
undefined,
|
|
53
|
+
meta
|
|
54
|
+
),
|
|
55
|
+
/'transfer' is not an entry node/
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('a declared entry node still starts the run', async () => {
|
|
60
|
+
seedSingletons()
|
|
61
|
+
const ws = new InMemoryWorkflowService()
|
|
62
|
+
|
|
63
|
+
const { runId } = await runWorkflowGraph(
|
|
64
|
+
ws,
|
|
65
|
+
'payout',
|
|
66
|
+
{},
|
|
67
|
+
{ rpcWithWire: async () => ({}) },
|
|
68
|
+
true,
|
|
69
|
+
'validate',
|
|
70
|
+
undefined,
|
|
71
|
+
meta
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
assert.ok(runId, 'the graph ran from the entry node it declared')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('a step runs the function the workflow dispatched it with', () => {
|
|
79
|
+
test('a queue message naming another function is rejected', async () => {
|
|
80
|
+
seedSingletons()
|
|
81
|
+
const ws = new InMemoryWorkflowService()
|
|
82
|
+
const runId = await ws.createRun('flow', {}, false, 'hash', {
|
|
83
|
+
type: 'queue',
|
|
84
|
+
})
|
|
85
|
+
await ws.insertStepState(runId, 'notify', 'sendReceipt', {})
|
|
86
|
+
|
|
87
|
+
const invoked: string[] = []
|
|
88
|
+
await assert.rejects(
|
|
89
|
+
ws.executeWorkflowStep(runId, 'notify', 'deleteAllUsers', {}, {
|
|
90
|
+
rpcWithWire: async (rpcName: string) => {
|
|
91
|
+
invoked.push(rpcName)
|
|
92
|
+
return {}
|
|
93
|
+
},
|
|
94
|
+
} as any),
|
|
95
|
+
WorkflowStepFunctionMismatchError
|
|
96
|
+
)
|
|
97
|
+
assert.deepEqual(invoked, [], 'nothing was invoked')
|
|
98
|
+
|
|
99
|
+
const stepState = await ws.getStepState(runId, 'notify')
|
|
100
|
+
assert.equal(
|
|
101
|
+
stepState.status,
|
|
102
|
+
'pending',
|
|
103
|
+
'the forged message left the step untouched'
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('the dispatched function still runs', async () => {
|
|
108
|
+
seedSingletons()
|
|
109
|
+
const ws = new InMemoryWorkflowService()
|
|
110
|
+
const runId = await ws.createRun('flow', {}, false, 'hash', {
|
|
111
|
+
type: 'queue',
|
|
112
|
+
})
|
|
113
|
+
await ws.insertStepState(runId, 'notify', 'sendReceipt', {})
|
|
114
|
+
|
|
115
|
+
const invoked: string[] = []
|
|
116
|
+
await ws.executeWorkflowStep(runId, 'notify', 'sendReceipt', {}, {
|
|
117
|
+
rpcWithWire: async (rpcName: string) => {
|
|
118
|
+
invoked.push(rpcName)
|
|
119
|
+
return {}
|
|
120
|
+
},
|
|
121
|
+
} as any)
|
|
122
|
+
|
|
123
|
+
assert.deepEqual(invoked, ['sendReceipt'])
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('workflow run ownership', () => {
|
|
128
|
+
test('a run started by a session is readable only by that session', () => {
|
|
129
|
+
assert.throws(
|
|
130
|
+
() =>
|
|
131
|
+
assertWorkflowRunOwner(
|
|
132
|
+
{ type: 'http', pikkuUserId: 'user-a' },
|
|
133
|
+
{
|
|
134
|
+
userId: 'user-b',
|
|
135
|
+
}
|
|
136
|
+
),
|
|
137
|
+
ForbiddenError
|
|
138
|
+
)
|
|
139
|
+
assert.throws(
|
|
140
|
+
() =>
|
|
141
|
+
assertWorkflowRunOwner(
|
|
142
|
+
{ type: 'http', pikkuUserId: 'user-a' },
|
|
143
|
+
undefined
|
|
144
|
+
),
|
|
145
|
+
ForbiddenError
|
|
146
|
+
)
|
|
147
|
+
assert.doesNotThrow(() =>
|
|
148
|
+
assertWorkflowRunOwner(
|
|
149
|
+
{ type: 'http', pikkuUserId: 'user-a' },
|
|
150
|
+
{
|
|
151
|
+
userId: 'user-a',
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('a run with no recorded owner has no ownership to enforce', () => {
|
|
158
|
+
assert.doesNotThrow(() =>
|
|
159
|
+
assertWorkflowRunOwner({ type: 'trigger' }, { userId: 'user-b' })
|
|
160
|
+
)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('approveStep refuses a caller who does not own the run', async () => {
|
|
164
|
+
seedSingletons()
|
|
165
|
+
const ws = new InMemoryWorkflowService()
|
|
166
|
+
const runId = await ws.createRun('flow', {}, false, 'hash', {
|
|
167
|
+
type: 'http',
|
|
168
|
+
pikkuUserId: 'owner',
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
await assert.rejects(
|
|
172
|
+
ws.approveStep(
|
|
173
|
+
runId,
|
|
174
|
+
'release-funds',
|
|
175
|
+
{ ok: true },
|
|
176
|
+
{
|
|
177
|
+
userId: 'attacker',
|
|
178
|
+
}
|
|
179
|
+
),
|
|
180
|
+
ForbiddenError
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const state = await ws.getRunState(runId)
|
|
184
|
+
assert.deepEqual(
|
|
185
|
+
Object.keys(state),
|
|
186
|
+
[],
|
|
187
|
+
'no decision was recorded for the run'
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('approveStep accepts the run owner', async () => {
|
|
192
|
+
seedSingletons()
|
|
193
|
+
const ws = new InMemoryWorkflowService()
|
|
194
|
+
const runId = await ws.createRun('flow', {}, false, 'hash', {
|
|
195
|
+
type: 'http',
|
|
196
|
+
pikkuUserId: 'owner',
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
await ws.approveStep(
|
|
200
|
+
runId,
|
|
201
|
+
'release-funds',
|
|
202
|
+
{ ok: true },
|
|
203
|
+
{
|
|
204
|
+
userId: 'owner',
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const state = await ws.getRunState(runId)
|
|
209
|
+
assert.equal(
|
|
210
|
+
Object.keys(state).length,
|
|
211
|
+
1,
|
|
212
|
+
'the owner’s decision was recorded'
|
|
213
|
+
)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { PikkuRawWire, SerializedError } from '../../types/core.types.js'
|
|
2
|
+
import type {
|
|
3
|
+
CoreWorkflow,
|
|
4
|
+
PikkuWorkflowWire,
|
|
5
|
+
StepState,
|
|
6
|
+
WorkflowRun,
|
|
7
|
+
WorkflowStatus,
|
|
8
|
+
WorkflowStepOptions,
|
|
9
|
+
} from './workflow.types.js'
|
|
10
|
+
|
|
11
|
+
export interface RunLifecycleContext {
|
|
12
|
+
runId: string
|
|
13
|
+
run: WorkflowRun
|
|
14
|
+
workflowMeta: any
|
|
15
|
+
workflow: CoreWorkflow
|
|
16
|
+
wire: PikkuRawWire
|
|
17
|
+
packageName: string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The subset of the workflow service a step executor is allowed to call back
|
|
22
|
+
* into.
|
|
23
|
+
*/
|
|
24
|
+
export interface WorkflowRunEngine {
|
|
25
|
+
inlineStep(
|
|
26
|
+
runId: string,
|
|
27
|
+
logicalStepName: string,
|
|
28
|
+
fn: Function,
|
|
29
|
+
stepOptions?: WorkflowStepOptions,
|
|
30
|
+
data?: any,
|
|
31
|
+
funcName?: string
|
|
32
|
+
): Promise<any>
|
|
33
|
+
updateRunStatus(
|
|
34
|
+
runId: string,
|
|
35
|
+
status: WorkflowStatus,
|
|
36
|
+
output?: any,
|
|
37
|
+
error?: SerializedError
|
|
38
|
+
): Promise<void>
|
|
39
|
+
onChildWorkflowFailed(run: WorkflowRun, error: unknown): Promise<void>
|
|
40
|
+
verifyStepName(stepName: unknown): void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hooks a host may install to decorate runs it did not start. Used by the
|
|
45
|
+
* scenario service to attach its own per-run state.
|
|
46
|
+
*/
|
|
47
|
+
export interface WorkflowRunExtension {
|
|
48
|
+
attachRunContext(
|
|
49
|
+
runId: string,
|
|
50
|
+
workflowMeta: any,
|
|
51
|
+
options?: Record<string, any>
|
|
52
|
+
): Promise<void>
|
|
53
|
+
detachRunContext(runId: string): void
|
|
54
|
+
decorateRunWire(
|
|
55
|
+
wire: PikkuRawWire,
|
|
56
|
+
context: {
|
|
57
|
+
runId: string
|
|
58
|
+
workflowMeta: any
|
|
59
|
+
workflowWire: PikkuWorkflowWire
|
|
60
|
+
}
|
|
61
|
+
): void
|
|
62
|
+
decorateWorkflowWire(
|
|
63
|
+
workflowWire: PikkuWorkflowWire,
|
|
64
|
+
context: {
|
|
65
|
+
name: string
|
|
66
|
+
runId: string
|
|
67
|
+
rpcService: any
|
|
68
|
+
addonNamespace?: string | null
|
|
69
|
+
}
|
|
70
|
+
): void
|
|
71
|
+
onBeforeRunFunc(context: RunLifecycleContext): Promise<void>
|
|
72
|
+
onAfterRunFunc(
|
|
73
|
+
context: RunLifecycleContext,
|
|
74
|
+
outcome: 'completed' | 'failed' | 'interrupted',
|
|
75
|
+
failure: unknown
|
|
76
|
+
): Promise<void>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Per-run bookkeeping held only for the lifetime of an in-process execution.
|
|
81
|
+
*/
|
|
82
|
+
export type RunContext = {
|
|
83
|
+
activeExecutions: number
|
|
84
|
+
inline?: boolean
|
|
85
|
+
ordinals: Map<string, number>
|
|
86
|
+
lastStep?: string
|
|
87
|
+
replay?: {
|
|
88
|
+
steps?: Map<string, StepState>
|
|
89
|
+
run?: WorkflowRun
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ForbiddenError } from '../../errors/errors.js'
|
|
2
|
+
import { addError } from '../../errors/error-handler.js'
|
|
3
|
+
import type { CoreUserSession } from '../../types/core.types.js'
|
|
4
|
+
import type { WorkflowRunWire } from './workflow.types.js'
|
|
5
|
+
|
|
6
|
+
export class WorkflowRunForbiddenError extends ForbiddenError {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('Not authorized to access this workflow run')
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
addError(WorkflowRunForbiddenError, {
|
|
12
|
+
status: 403,
|
|
13
|
+
message: 'Not authorized to access this workflow run.',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A run started through a session records that session's user as its owner, and
|
|
18
|
+
* only that user may read it or answer its approval gates.
|
|
19
|
+
*
|
|
20
|
+
* A run with no recorded owner — started by a trigger, a scheduler, or a route
|
|
21
|
+
* wired without auth — has nobody to compare a caller against, so ownership is
|
|
22
|
+
* not a control that exists for it. Gate those with `auth` or `permissions` on
|
|
23
|
+
* the entrypoint instead.
|
|
24
|
+
*/
|
|
25
|
+
export const assertWorkflowRunOwner = (
|
|
26
|
+
wire: WorkflowRunWire | undefined,
|
|
27
|
+
session: CoreUserSession | undefined
|
|
28
|
+
): void => {
|
|
29
|
+
const owner = wire?.pikkuUserId
|
|
30
|
+
if (!owner) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if (!session?.userId || session.userId !== owner) {
|
|
34
|
+
throw new WorkflowRunForbiddenError()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { WorkflowSuspendedException } from './workflow-errors.js'
|
|
2
|
+
import type { ApprovalStore } from './workflow-approval.js'
|
|
3
|
+
import type { StepState } from './workflow.types.js'
|
|
4
|
+
|
|
5
|
+
/** The durable step name a suspension point is recorded under. */
|
|
6
|
+
export const suspendStepNameFor = (reason: string): string =>
|
|
7
|
+
`__workflow_suspend:${reason}`
|
|
8
|
+
|
|
9
|
+
/** What the suspend gate needs from the workflow service. */
|
|
10
|
+
export type SuspendStore = Pick<
|
|
11
|
+
ApprovalStore,
|
|
12
|
+
'getStepState' | 'insertStepState' | 'setStepRunning' | 'setStepResult'
|
|
13
|
+
>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Record a suspension point and unwind the run.
|
|
17
|
+
*
|
|
18
|
+
* A suspension that has already succeeded returns instead of throwing, so a
|
|
19
|
+
* replay walks past a gate the run has already passed through.
|
|
20
|
+
*/
|
|
21
|
+
export const recordSuspension = async (
|
|
22
|
+
store: SuspendStore,
|
|
23
|
+
runId: string,
|
|
24
|
+
reason: string,
|
|
25
|
+
stepName: string,
|
|
26
|
+
fromStepName: string | undefined
|
|
27
|
+
): Promise<void> => {
|
|
28
|
+
const insert = () =>
|
|
29
|
+
store.insertStepState(
|
|
30
|
+
runId,
|
|
31
|
+
stepName,
|
|
32
|
+
'pikkuWorkflowSuspend',
|
|
33
|
+
{ reason },
|
|
34
|
+
undefined,
|
|
35
|
+
fromStepName
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
let stepState: StepState
|
|
39
|
+
try {
|
|
40
|
+
stepState = await store.getStepState(runId, stepName)
|
|
41
|
+
} catch {
|
|
42
|
+
stepState = await insert()
|
|
43
|
+
}
|
|
44
|
+
if (!stepState.stepId) {
|
|
45
|
+
stepState = await insert()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (stepState.status === 'succeeded') {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (stepState.status === 'pending') {
|
|
53
|
+
await store.setStepRunning(stepState.stepId)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await store.setStepResult(stepState.stepId, {
|
|
57
|
+
reason,
|
|
58
|
+
suspendedAt: new Date().toISOString(),
|
|
59
|
+
})
|
|
60
|
+
throw new WorkflowSuspendedException(runId, reason)
|
|
61
|
+
}
|
|
@@ -120,6 +120,13 @@ export interface WorkflowRun {
|
|
|
120
120
|
export interface StepState {
|
|
121
121
|
stepId: string
|
|
122
122
|
status: StepStatus
|
|
123
|
+
/**
|
|
124
|
+
* The function the workflow dispatched this step with, recorded so a worker
|
|
125
|
+
* can reject a queue message naming anything else. `null` is a step with no
|
|
126
|
+
* function of its own (inline work); `undefined` is a store that never
|
|
127
|
+
* recorded one, and cannot be compared against.
|
|
128
|
+
*/
|
|
129
|
+
rpcName?: string | null
|
|
123
130
|
result?: any
|
|
124
131
|
error?: SerializedError
|
|
125
132
|
attemptCount: number
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { join, dirname, resolve, relative, sep } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const srcRoot = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const wiringsRoot = join(srcRoot, 'wirings')
|
|
9
|
+
|
|
10
|
+
/** Type-only statements are erased, so they couple nothing at runtime. */
|
|
11
|
+
const TYPE_ONLY = /^\s*(?:import|export)\s+type\b/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Which wire may reach which, at runtime.
|
|
15
|
+
*
|
|
16
|
+
* A wire should be self-contained apart from what is genuinely common, so every
|
|
17
|
+
* entry here is a deliberate crossover point rather than an accident. Adding one
|
|
18
|
+
* is a visible diff — and worth asking whether the dependency could be
|
|
19
|
+
* type-only, which couples nothing at runtime.
|
|
20
|
+
*/
|
|
21
|
+
const ALLOWED: Record<string, string[]> = {
|
|
22
|
+
// A channel is an HTTP upgrade, so it builds on the HTTP wire.
|
|
23
|
+
channel: ['http'],
|
|
24
|
+
// `remote` on a wire with no peer — one leaf module, not the channel runtime.
|
|
25
|
+
http: ['channel'],
|
|
26
|
+
cli: ['channel'],
|
|
27
|
+
// An agent streams over a channel and is invoked over rpc.
|
|
28
|
+
'ai-agent': ['channel', 'rpc'],
|
|
29
|
+
// `wire.rpc.agent` — the facade lives with the agent runtime it delegates to.
|
|
30
|
+
rpc: ['ai-agent'],
|
|
31
|
+
gateway: ['http'],
|
|
32
|
+
mcp: ['rpc'],
|
|
33
|
+
trigger: ['rpc'],
|
|
34
|
+
// A workflow step is dispatched to a queue, invoked over rpc, or slept on.
|
|
35
|
+
workflow: ['queue', 'rpc', 'scheduler'],
|
|
36
|
+
// A virtual user drives a scenario, which is a workflow, as a persona.
|
|
37
|
+
'virtual-user': ['role', 'workflow'],
|
|
38
|
+
persona: ['virtual-user'],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sourceFiles = (directory: string): string[] =>
|
|
42
|
+
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
43
|
+
const entryPath = join(directory, entry.name)
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
return entry.name === 'dist' ? [] : sourceFiles(entryPath)
|
|
46
|
+
}
|
|
47
|
+
return entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')
|
|
48
|
+
? [entryPath]
|
|
49
|
+
: []
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/** Every wire each wire reaches at runtime, following value imports only. */
|
|
53
|
+
const crossWireEdges = (): Map<string, Set<string>> => {
|
|
54
|
+
const edges = new Map<string, Set<string>>()
|
|
55
|
+
|
|
56
|
+
for (const file of sourceFiles(wiringsRoot)) {
|
|
57
|
+
const from = relative(wiringsRoot, file).split(sep)[0]!
|
|
58
|
+
|
|
59
|
+
for (const line of readFileSync(file, 'utf-8').split('\n')) {
|
|
60
|
+
if (TYPE_ONLY.test(line)) continue
|
|
61
|
+
const specifier = line.match(/\bfrom\s+'(\.[^']+)'/)?.[1]
|
|
62
|
+
if (!specifier) continue
|
|
63
|
+
|
|
64
|
+
const target = resolve(dirname(file), specifier)
|
|
65
|
+
if (!target.startsWith(wiringsRoot + sep)) continue
|
|
66
|
+
|
|
67
|
+
const to = relative(wiringsRoot, target).split(sep)[0]!
|
|
68
|
+
if (to === from) continue
|
|
69
|
+
if (!edges.has(from)) edges.set(from, new Set())
|
|
70
|
+
edges.get(from)!.add(to)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return edges
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('wirings stay decoupled', () => {
|
|
78
|
+
test('no wire reaches a wire it is not declared to', () => {
|
|
79
|
+
const undeclared = [...crossWireEdges()]
|
|
80
|
+
.flatMap(([from, tos]) =>
|
|
81
|
+
[...tos]
|
|
82
|
+
.filter((to) => !(ALLOWED[from] ?? []).includes(to))
|
|
83
|
+
.map((to) => `${from} -> ${to}`)
|
|
84
|
+
)
|
|
85
|
+
.sort()
|
|
86
|
+
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
undeclared,
|
|
89
|
+
[],
|
|
90
|
+
'a wire gained a runtime dependency on another wire:\n' +
|
|
91
|
+
`${undeclared.join('\n')}\n` +
|
|
92
|
+
'Add it to ALLOWED only if it is genuinely necessary — a type-only ' +
|
|
93
|
+
'import couples nothing.'
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('every declared crossover still exists', () => {
|
|
98
|
+
const actual = crossWireEdges()
|
|
99
|
+
const stale = Object.entries(ALLOWED)
|
|
100
|
+
.flatMap(([from, tos]) =>
|
|
101
|
+
tos
|
|
102
|
+
.filter((to) => !actual.get(from)?.has(to))
|
|
103
|
+
.map((to) => `${from} -> ${to}`)
|
|
104
|
+
)
|
|
105
|
+
.sort()
|
|
106
|
+
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
stale,
|
|
109
|
+
[],
|
|
110
|
+
`these crossovers are declared but no longer real — delete them:\n${stale.join('\n')}`
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('the walker finds the edges it is meant to', () => {
|
|
115
|
+
// Guards both tests above: a walker that resolved nothing would pass
|
|
116
|
+
// whatever the wirings imported.
|
|
117
|
+
const edges = crossWireEdges()
|
|
118
|
+
assert.ok(
|
|
119
|
+
edges.get('channel')?.has('http'),
|
|
120
|
+
`expected channel -> http; walker found ${edges.size} wires with edges`
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
})
|