@empyria/restate 0.4.17 → 0.4.20
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/AGENTS.md +5 -5
- package/README.md +7 -8
- package/lib/Admin.js +145 -123
- package/package.json +4 -4
- package/test/Admin.test.js +22 -18
package/AGENTS.md
CHANGED
|
@@ -79,11 +79,11 @@ may observe an already-mocked value. See `test/Admin.test.js`.
|
|
|
79
79
|
same address registers again, but only `force: true` updates its handler list. Check both against a live server when
|
|
80
80
|
touching them.
|
|
81
81
|
|
|
82
|
-
`setupRestate`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`
|
|
86
|
-
|
|
82
|
+
`setupRestate` takes ONE complete `serviceURL` (scheme and explicit port included): it is registered as given and the
|
|
83
|
+
endpoint listens on its port. There is deliberately no `host` or `port` option — never assemble a URL from parts, the
|
|
84
|
+
scheme and any mapping are the caller's to state. `parseServiceURL` validates it (unit tested).
|
|
85
|
+
`createRestateAdmin().setupRestate` must forward every option to `setupRestate` — it once dropped
|
|
86
|
+
`keepDeploymentOnShutdown`/`forceRegistration`/`registrationCallbackFn`.
|
|
87
87
|
|
|
88
88
|
`setupRestate` (in `lib/Admin.js`) is not unit tested: it binds real HTTP/2 + health-check
|
|
89
89
|
ports, installs process-wide `SIGTERM`/`SIGINT` handlers, and its returned `forceClose` calls
|
package/README.md
CHANGED
|
@@ -122,14 +122,14 @@ endpoint, then create jobs with either:
|
|
|
122
122
|
Cron expressions are `croner` patterns (five fields, or six with seconds for tests). The target handler is sent the
|
|
123
123
|
`payload` as its input, or no input when there is none.
|
|
124
124
|
|
|
125
|
-
###
|
|
125
|
+
### Serving and registering an endpoint
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
`setupRestate({ restateAdminURL, serviceURL: 'https://agent-service.internal:9080', services })` takes one complete URL, the
|
|
128
|
+
address Restate reaches the endpoint at. It is registered as given and the endpoint listens on its port, so the scheme and
|
|
129
|
+
an explicit port are required (there is no separate host or port option); the health endpoint listens on the next port
|
|
130
|
+
unless `healthPort` is set.
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
It serves, registers the deployment, and deletes it again on SIGINT/SIGTERM. Invocations in flight are
|
|
133
133
|
pinned to their deployment, so deleting it strands them: a restarted process cannot finish them and they can only be
|
|
134
134
|
killed. For a process restarted at the same address, pass `keepDeploymentOnShutdown: true` and `forceRegistration: true`:
|
|
135
135
|
Restate keeps the deployment ID when the same address registers again (checked against Restate 1.7), running
|
|
@@ -137,8 +137,7 @@ invocations resume on the new process, and `force` updates the services and hand
|
|
|
137
137
|
re-registration keeps the old handler list). Only compatible changes are safe this way: an invocation of a removed or
|
|
138
138
|
renamed handler cannot resume.
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
registration (a supervisor that registers a process's address once and restarts the process), use
|
|
140
|
+
To serve without registering: when something else owns registration (a supervisor that registers a process's address once and restarts the process), use
|
|
142
141
|
`serveEndpoint({port, services})` instead: it only serves. Invocations already running on a deployment are pinned to its
|
|
143
142
|
address, so the deployment must survive the process stopping and come back at the same address.
|
|
144
143
|
|
package/lib/Admin.js
CHANGED
|
@@ -550,7 +550,7 @@ export function sendMessageAsyncWithDiscovery({ restateAdminURL }) {
|
|
|
550
550
|
}
|
|
551
551
|
|
|
552
552
|
/**
|
|
553
|
-
* @typedef {import('node:http2').Http2Server & {deploymentId: string, services: string[], forceClose: () => void}} RestateServer
|
|
553
|
+
* @typedef {import('node:http2').Http2Server & {deploymentId: string|undefined, services: string[]|undefined, forceClose: () => void}} RestateServer
|
|
554
554
|
*/
|
|
555
555
|
|
|
556
556
|
/**
|
|
@@ -578,6 +578,34 @@ export function defineObject(def) {
|
|
|
578
578
|
return restate.object(def)
|
|
579
579
|
}
|
|
580
580
|
|
|
581
|
+
/**
|
|
582
|
+
* The health check `setupRestate` uses unless given a `healthCheckFn`: `GET /health`, `/health/liveness` and
|
|
583
|
+
* `/health/readiness` answer 200 once the deployment is registered and 503 before, anything else 404.
|
|
584
|
+
* @param {import('node:http').IncomingMessage} req
|
|
585
|
+
* @param {import('node:http').ServerResponse} res
|
|
586
|
+
* @param {{isHealthy: boolean, deploymentId?: string, services?: string[]}} state
|
|
587
|
+
*/
|
|
588
|
+
export function defaultHealthCheck(req, res, { isHealthy, deploymentId, services }) {
|
|
589
|
+
if (
|
|
590
|
+
(req.url === '/health' ||
|
|
591
|
+
req.url === '/health/liveness' ||
|
|
592
|
+
req.url === '/health/readiness') &&
|
|
593
|
+
req.method === 'GET'
|
|
594
|
+
) {
|
|
595
|
+
res.writeHead(isHealthy ? 200 : 503, { 'Content-Type': 'application/json' })
|
|
596
|
+
res.end(
|
|
597
|
+
JSON.stringify({
|
|
598
|
+
status: isHealthy ? 'ok' : 'unavailable',
|
|
599
|
+
deployment: deploymentId,
|
|
600
|
+
services,
|
|
601
|
+
}),
|
|
602
|
+
)
|
|
603
|
+
} else {
|
|
604
|
+
res.writeHead(404)
|
|
605
|
+
res.end()
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
581
609
|
/**
|
|
582
610
|
* Starts the HTTP/2 Restate endpoint, a health-check HTTP server, registers
|
|
583
611
|
* the deployment with the admin API, and wires SIGTERM/SIGINT for graceful
|
|
@@ -592,16 +620,17 @@ export function defineObject(def) {
|
|
|
592
620
|
* that was removed or renamed cannot resume.
|
|
593
621
|
* @param {Object} params
|
|
594
622
|
* @param {string} params.restateAdminURL
|
|
595
|
-
* @param {
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
* any proxy/port mapping are the caller's to state, never guessed here.
|
|
599
|
-
* @param {string} [params.host] @deprecated Use `serviceURL`. Only when `serviceURL` is absent: registers as
|
|
600
|
-
* `http://<host>:<port>` (plain HTTP is assumed).
|
|
623
|
+
* @param {string} params.serviceURL Complete URL Restate uses to reach this endpoint, scheme and port included
|
|
624
|
+
* (e.g. `https://agent-service.internal:9080`). It is registered as given, and the endpoint listens on its port, so
|
|
625
|
+
* the port must be explicit. Scheme and port are the caller's to state, never guessed here.
|
|
601
626
|
* @param {number} [params.healthPort] - Defaults to `port + 1`.
|
|
602
627
|
* @param {Parameters<typeof restate.createEndpointHandler>[0]['services']} [params.services]
|
|
603
628
|
* @param {() => void} [params.shutdownCallbackFn]
|
|
604
629
|
* @param {(deploymentId: string) => void} [params.registrationCallbackFn]
|
|
630
|
+
* @param {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, state: {isHealthy: boolean, deploymentId?: string, services?: string[]}) => void | Promise<void>} [params.healthCheckFn]
|
|
631
|
+
* Takes over every request to the health port: routing, status code, headers and body are the function's to set and
|
|
632
|
+
* it must end the response. `state.isHealthy` is true once the deployment is registered. Without it, `/health`,
|
|
633
|
+
* `/health/liveness` and `/health/readiness` answer 200 or 503 with a small JSON body, healthy once registered.
|
|
605
634
|
* @param {boolean} [params.keepDeploymentOnShutdown=false] Leave the deployment registered on SIGINT/SIGTERM.
|
|
606
635
|
* @param {boolean} [params.forceRegistration=false] Register with `force: true`, updating an existing deployment at
|
|
607
636
|
* the same address in place.
|
|
@@ -610,15 +639,17 @@ export function defineObject(def) {
|
|
|
610
639
|
export async function setupRestate({
|
|
611
640
|
restateAdminURL,
|
|
612
641
|
serviceURL: serviceURLParam,
|
|
613
|
-
|
|
614
|
-
port,
|
|
615
|
-
healthPort = port + 1,
|
|
642
|
+
healthPort: healthPortParam,
|
|
616
643
|
services = [],
|
|
617
644
|
shutdownCallbackFn,
|
|
618
645
|
registrationCallbackFn,
|
|
646
|
+
healthCheckFn,
|
|
619
647
|
keepDeploymentOnShutdown = false,
|
|
620
648
|
forceRegistration = false,
|
|
621
649
|
}) {
|
|
650
|
+
const { serviceURL, port } = parseServiceURL(serviceURLParam)
|
|
651
|
+
const healthPort = healthPortParam ?? port + 1
|
|
652
|
+
|
|
622
653
|
const handler = restate.createEndpointHandler({ services })
|
|
623
654
|
|
|
624
655
|
const server = http2.createServer(handler)
|
|
@@ -626,25 +657,20 @@ export async function setupRestate({
|
|
|
626
657
|
// --- Health server ---
|
|
627
658
|
let isHealthy = false
|
|
628
659
|
|
|
660
|
+
const checkFn = healthCheckFn ?? defaultHealthCheck
|
|
629
661
|
const healthServer = createHttpServer((req, res) => {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
req.method === 'GET'
|
|
635
|
-
) {
|
|
636
|
-
const status = isHealthy ? 200 : 503
|
|
637
|
-
const body = JSON.stringify({
|
|
638
|
-
status: isHealthy ? 'ok' : 'unavailable',
|
|
639
|
-
deployment: server.deploymentId,
|
|
640
|
-
services: server.services,
|
|
641
|
-
})
|
|
642
|
-
res.writeHead(status, { 'Content-Type': 'application/json' })
|
|
643
|
-
res.end(body)
|
|
644
|
-
} else {
|
|
645
|
-
res.writeHead(404)
|
|
646
|
-
res.end()
|
|
662
|
+
const state = {
|
|
663
|
+
isHealthy,
|
|
664
|
+
deploymentId: server.deploymentId,
|
|
665
|
+
services: server.services,
|
|
647
666
|
}
|
|
667
|
+
Promise.resolve()
|
|
668
|
+
.then(() => checkFn(req, res, state))
|
|
669
|
+
.catch((err) => {
|
|
670
|
+
console.error('healthCheckFn failed', err)
|
|
671
|
+
if (!res.headersSent) res.writeHead(500)
|
|
672
|
+
res.end()
|
|
673
|
+
})
|
|
648
674
|
})
|
|
649
675
|
|
|
650
676
|
healthServer.listen(healthPort, () => console.log(`Health endpoint on :${healthPort}/health`))
|
|
@@ -689,125 +715,121 @@ export async function setupRestate({
|
|
|
689
715
|
|
|
690
716
|
server.listen(port, () => console.log(`Server running on ${port}`))
|
|
691
717
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
718
|
+
/**
|
|
719
|
+
* @param {Object} params
|
|
720
|
+
* @param {string} params.restateAdminURL
|
|
721
|
+
* @param {string} params.serviceURL
|
|
722
|
+
* @param {number} [params.attempts=5]
|
|
723
|
+
* @param {number} [params.baseDelayMs=3000]
|
|
724
|
+
* @returns {Promise<RegisterDeploymentResponse>}
|
|
725
|
+
*/
|
|
726
|
+
async function registerDeploymentWithRetry({
|
|
727
|
+
restateAdminURL,
|
|
728
|
+
serviceURL,
|
|
729
|
+
attempts = 5,
|
|
730
|
+
baseDelayMs = 3_000,
|
|
731
|
+
}) {
|
|
732
|
+
let lastErr
|
|
733
|
+
|
|
734
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
735
|
+
try {
|
|
736
|
+
const result = await registerDeployment({
|
|
737
|
+
restateAdminURL,
|
|
738
|
+
serviceURL,
|
|
739
|
+
force: forceRegistration,
|
|
740
|
+
})
|
|
741
|
+
|
|
711
742
|
try {
|
|
712
|
-
|
|
713
|
-
restateAdminURL,
|
|
714
|
-
serviceURL,
|
|
715
|
-
force: forceRegistration,
|
|
716
|
-
})
|
|
717
|
-
|
|
718
|
-
try {
|
|
719
|
-
registrationCallbackFn?.(result.id)
|
|
720
|
-
} catch (err) {
|
|
721
|
-
console.error(
|
|
722
|
-
`Error in registration callback for deployment ${result.id}: ${
|
|
723
|
-
err instanceof Error ? err.message : String(err)
|
|
724
|
-
}`,
|
|
725
|
-
)
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
return result
|
|
743
|
+
registrationCallbackFn?.(result.id)
|
|
729
744
|
} catch (err) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
if (attempt < attempts) {
|
|
738
|
-
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), DELAY_CAP)
|
|
739
|
-
console.log(
|
|
740
|
-
`Registration attempt ${attempt}/${attempts} failed, retrying in ${delay}ms: ${
|
|
741
|
-
err instanceof Error ? err.message : String(err)
|
|
742
|
-
}`,
|
|
743
|
-
)
|
|
744
|
-
await sleep(delay)
|
|
745
|
-
}
|
|
745
|
+
console.error(
|
|
746
|
+
`Error in registration callback for deployment ${result.id}: ${
|
|
747
|
+
err instanceof Error ? err.message : String(err)
|
|
748
|
+
}`,
|
|
749
|
+
)
|
|
746
750
|
}
|
|
747
|
-
}
|
|
748
751
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
}`,
|
|
753
|
-
)
|
|
754
|
-
}
|
|
755
|
-
const deployment = await registerDeploymentWithRetry({
|
|
756
|
-
restateAdminURL,
|
|
757
|
-
serviceURL,
|
|
758
|
-
}).catch(console.error)
|
|
752
|
+
return result
|
|
753
|
+
} catch (err) {
|
|
754
|
+
lastErr = err
|
|
759
755
|
|
|
760
|
-
|
|
756
|
+
// Deterministic — retrying won't help. Bail immediately.
|
|
757
|
+
if (err instanceof Error && err.message.includes('META0004')) {
|
|
758
|
+
throw err
|
|
759
|
+
}
|
|
761
760
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
761
|
+
if (attempt < attempts) {
|
|
762
|
+
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), DELAY_CAP)
|
|
763
|
+
console.log(
|
|
764
|
+
`Registration attempt ${attempt}/${attempts} failed, retrying in ${delay}ms: ${
|
|
765
|
+
err instanceof Error ? err.message : String(err)
|
|
766
|
+
}`,
|
|
767
|
+
)
|
|
768
|
+
await sleep(delay)
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
throw new Error(
|
|
774
|
+
`Failed to register deployment after ${attempts} attempts: ${
|
|
775
|
+
lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
776
|
+
}`,
|
|
777
|
+
)
|
|
769
778
|
}
|
|
779
|
+
const deployment = await registerDeploymentWithRetry({
|
|
780
|
+
restateAdminURL,
|
|
781
|
+
serviceURL,
|
|
782
|
+
}).catch(console.error)
|
|
783
|
+
|
|
784
|
+
isHealthy = !!deployment // Only healthy once registered
|
|
770
785
|
|
|
771
786
|
return Object.assign(server, {
|
|
772
|
-
deploymentId:
|
|
773
|
-
services:
|
|
787
|
+
deploymentId: deployment?.id,
|
|
788
|
+
services: deployment?.services?.map((s) => s.name),
|
|
774
789
|
forceClose,
|
|
775
790
|
})
|
|
776
791
|
}
|
|
777
792
|
|
|
778
793
|
/**
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
* @
|
|
782
|
-
* @
|
|
783
|
-
*
|
|
794
|
+
* Validates the URL `setupRestate` registers with Restate and returns it with the port the endpoint listens on.
|
|
795
|
+
* @param {string} serviceURL Complete http(s) URL including an explicit port
|
|
796
|
+
* @returns {{serviceURL: string, port: number}} `serviceURL` unchanged
|
|
797
|
+
* @throws {Error} If it is missing, not an absolute http(s) URL, or has no explicit port (the scheme's default would
|
|
798
|
+
* be a guess)
|
|
784
799
|
*/
|
|
785
|
-
export function
|
|
786
|
-
if (serviceURL) {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
800
|
+
export function parseServiceURL(serviceURL) {
|
|
801
|
+
if (!serviceURL) {
|
|
802
|
+
throw new Error(
|
|
803
|
+
'setupRestate needs `serviceURL`: the complete URL (scheme and port) Restate reaches this endpoint at',
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
let url
|
|
807
|
+
try {
|
|
808
|
+
url = new URL(serviceURL)
|
|
809
|
+
} catch {
|
|
810
|
+
throw new Error(
|
|
811
|
+
`serviceURL must be a complete URL including http:// or https://, got '${serviceURL}'`,
|
|
812
|
+
)
|
|
813
|
+
}
|
|
814
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
815
|
+
throw new Error(`serviceURL must use http or https, got '${serviceURL}'`)
|
|
816
|
+
}
|
|
817
|
+
if (!url.port) {
|
|
818
|
+
throw new Error(
|
|
819
|
+
`serviceURL '${serviceURL}' has no explicit port: the endpoint listens on the port in the URL`,
|
|
820
|
+
)
|
|
799
821
|
}
|
|
800
|
-
return
|
|
822
|
+
return { serviceURL, port: Number(url.port) }
|
|
801
823
|
}
|
|
802
824
|
|
|
803
825
|
/**
|
|
804
|
-
* Whether `setupRestate`'s shutdown removes the deployment: only
|
|
805
|
-
* caller keeps it for the next start.
|
|
826
|
+
* Whether `setupRestate`'s shutdown removes the deployment: only one that registered (`deploymentId` is undefined when registration
|
|
827
|
+
* failed), and not when the caller keeps it for the next start.
|
|
806
828
|
* @param {{deploymentId: string|undefined, keepDeploymentOnShutdown?: boolean}} params
|
|
807
829
|
* @returns {boolean}
|
|
808
830
|
*/
|
|
809
831
|
export function shouldDeleteOnShutdown({ deploymentId, keepDeploymentOnShutdown = false }) {
|
|
810
|
-
return Boolean(deploymentId) &&
|
|
832
|
+
return Boolean(deploymentId) && !keepDeploymentOnShutdown
|
|
811
833
|
}
|
|
812
834
|
|
|
813
835
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empyria/restate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.20",
|
|
4
4
|
"description": "Restate.dev helpers for the Empyria nanoservice framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Imre Fazekas <imre.fazekas@icloud.com>",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"croner": "10.0.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@ai-sdk/openai-compatible": "3.0.
|
|
40
|
-
"@ai-sdk/provider": "4.0.
|
|
39
|
+
"@ai-sdk/openai-compatible": "3.0.65",
|
|
40
|
+
"@ai-sdk/provider": "4.0.24",
|
|
41
41
|
"@restatedev/restate-server": "1.7.13",
|
|
42
|
-
"ai": "7.0.
|
|
42
|
+
"ai": "7.0.130",
|
|
43
43
|
"oxfmt": "0.72.0",
|
|
44
44
|
"oxlint": "1.87.0"
|
|
45
45
|
},
|
package/test/Admin.test.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
sendMessageAsyncWithDiscovery,
|
|
22
22
|
registerDeployment,
|
|
23
23
|
shouldDeleteOnShutdown,
|
|
24
|
-
|
|
24
|
+
parseServiceURL,
|
|
25
25
|
sendMessageAsynSafe,
|
|
26
26
|
defineWorkflow,
|
|
27
27
|
defineService,
|
|
@@ -543,28 +543,32 @@ describe('registerDeployment', () => {
|
|
|
543
543
|
})
|
|
544
544
|
})
|
|
545
545
|
|
|
546
|
-
describe('
|
|
547
|
-
test('
|
|
548
|
-
expect(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
546
|
+
describe('parseServiceURL', () => {
|
|
547
|
+
test('returns the URL unchanged with the port it carries', () => {
|
|
548
|
+
expect(parseServiceURL('http://agent-service:9080')).toEqual({
|
|
549
|
+
serviceURL: 'http://agent-service:9080',
|
|
550
|
+
port: 9080,
|
|
551
|
+
})
|
|
552
|
+
expect(parseServiceURL('https://svc.example.com:9443/base')).toEqual({
|
|
553
|
+
serviceURL: 'https://svc.example.com:9443/base',
|
|
554
|
+
port: 9443,
|
|
555
|
+
})
|
|
555
556
|
})
|
|
556
557
|
|
|
557
|
-
test('
|
|
558
|
-
expect(
|
|
558
|
+
test('requires a serviceURL', () => {
|
|
559
|
+
expect(() => parseServiceURL(undefined)).toThrow(/needs `serviceURL`/)
|
|
560
|
+
expect(() => parseServiceURL('')).toThrow(/needs `serviceURL`/)
|
|
559
561
|
})
|
|
560
562
|
|
|
561
|
-
test('
|
|
562
|
-
expect(
|
|
563
|
+
test('rejects a URL without a scheme or with a non-http scheme', () => {
|
|
564
|
+
expect(() => parseServiceURL('agent-service:9080')).toThrow(/http/)
|
|
565
|
+
expect(() => parseServiceURL('not a url')).toThrow(/complete URL/)
|
|
566
|
+
expect(() => parseServiceURL('ftp://svc:21')).toThrow(/http or https/)
|
|
563
567
|
})
|
|
564
568
|
|
|
565
|
-
test('
|
|
566
|
-
expect(() =>
|
|
567
|
-
expect(() =>
|
|
569
|
+
test('refuses to guess the scheme default when the URL has no explicit port', () => {
|
|
570
|
+
expect(() => parseServiceURL('https://svc.example.com')).toThrow(/no explicit port/)
|
|
571
|
+
expect(() => parseServiceURL('http://svc:80')).toThrow(/no explicit port/)
|
|
568
572
|
})
|
|
569
573
|
})
|
|
570
574
|
|
|
@@ -580,7 +584,7 @@ describe('shouldDeleteOnShutdown', () => {
|
|
|
580
584
|
})
|
|
581
585
|
|
|
582
586
|
test('never deletes without a real deployment', () => {
|
|
583
|
-
for (const deploymentId of [undefined, ''
|
|
587
|
+
for (const deploymentId of [undefined, '']) {
|
|
584
588
|
expect(shouldDeleteOnShutdown({ deploymentId })).toBe(false)
|
|
585
589
|
}
|
|
586
590
|
})
|