@astrale-os/cli 1.0.0-beta.12 → 1.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/astrale.js +15432 -14745
- package/dist/types/admin/contract.d.ts +26 -0
- package/dist/types/admin/instance/client.d.ts +2 -4
- package/dist/types/admin/instance/model.d.ts +3 -0
- package/package.json +1 -1
- package/src/admin/.spec/architecture.md +12 -5
- package/src/admin/__tests__/fixture.ts +19 -95
- package/src/admin/catalog/.spec/api.d.ts +0 -2
- package/src/admin/catalog/.spec/architecture.md +5 -4
- package/src/admin/catalog/__tests__/client.test.ts +136 -33
- package/src/admin/catalog/client.ts +38 -61
- package/src/admin/contract.ts +46 -0
- package/src/admin/instance/.spec/api.d.ts +4 -8
- package/src/admin/instance/.spec/architecture.md +7 -7
- package/src/admin/instance/__tests__/client.test.ts +138 -31
- package/src/admin/instance/client.ts +32 -37
- package/src/admin/instance/model.ts +3 -0
- package/src/commands/__tests__/call.test.ts +34 -0
- package/src/commands/__tests__/read-commands.test.ts +27 -0
- package/src/commands/__tests__/token-ttl.test.ts +49 -4
- package/src/commands/call.ts +28 -3
- package/src/commands/query.ts +8 -2
- package/src/commands/token.ts +34 -20
- package/src/commands/ui/__tests__/commands.test.ts +129 -0
- package/src/commands/ui/add.ts +51 -0
- package/src/commands/ui/doctor.ts +13 -0
- package/src/commands/ui/init.ts +38 -0
- package/src/commands/ui/list.ts +26 -0
- package/src/commands/ui/preset-apply.ts +19 -0
- package/src/commands/ui/preset-list.ts +12 -0
- package/src/commands/ui/shared.ts +25 -0
- package/src/lib/__tests__/binary.test.ts +16 -1
- package/src/lib/binary.ts +22 -5
- package/src/lib/proc.ts +7 -2
- package/src/program/.spec/api.d.ts +1 -0
- package/src/program/__tests__/program.test.ts +32 -2
- package/src/program/build.ts +23 -1
- package/src/program/command.ts +1 -0
- package/src/program/registry.ts +2 -1
- package/src/ui/.spec/api.d.ts +14 -0
- package/src/ui/.spec/architecture.md +10 -0
- package/src/ui/.spec/laws.ts +36 -0
- package/src/ui/.spec/layout.ts +16 -0
- package/src/ui/__tests__/ui.test.ts +542 -0
- package/src/ui/index.ts +13 -0
- package/src/ui/lock.ts +87 -0
- package/src/ui/model.ts +83 -0
- package/src/ui/operations.ts +539 -0
- package/src/ui/project.ts +146 -0
- package/src/ui/release.ts +267 -0
- package/src/ui/runner.ts +18 -0
- package/studio/server/agent/harness/gateway/token.test.ts +1 -1
- package/studio/server/agent/harness/gateway/token.ts +3 -3
- package/dist/types/admin/binding.d.ts +0 -19
- package/src/admin/__tests__/binding.test.ts +0 -31
- package/src/admin/binding.ts +0 -98
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import type { ClientSession } from '@astrale-os/sdk/client/session'
|
|
2
2
|
import type { Node } from '@astrale-os/sdk/graph/node'
|
|
3
|
-
import type { ResolvedClass } from '@astrale-os/sdk/schema'
|
|
4
3
|
|
|
5
4
|
import { Path } from '@astrale-os/sdk/graph/path'
|
|
6
5
|
import { Query } from '@astrale-os/sdk/query'
|
|
7
6
|
|
|
8
|
-
import {
|
|
9
|
-
bindAdmin,
|
|
10
|
-
invokeAdminMethod,
|
|
11
|
-
requireAdminBinding,
|
|
12
|
-
requireAdminClass,
|
|
13
|
-
requireAdminCore,
|
|
14
|
-
requireAdminProperty,
|
|
15
|
-
type AdminBinding,
|
|
16
|
-
} from '../binding'
|
|
7
|
+
import { AdminContract, callAdminMethod } from '../contract'
|
|
17
8
|
import { readAllNodes, type AdminGraphApi } from '../graph'
|
|
18
9
|
import {
|
|
19
10
|
AdminDomainNotFoundError,
|
|
@@ -38,31 +29,21 @@ export interface AdminCatalogApi {
|
|
|
38
29
|
}
|
|
39
30
|
|
|
40
31
|
export interface AdminCatalogDependencies {
|
|
41
|
-
readonly bind?: (session: ClientSession) => Promise<AdminBinding>
|
|
42
32
|
readonly operationId?: (kind: 'publish' | 'configure-default') => string
|
|
43
33
|
}
|
|
44
34
|
|
|
45
|
-
/**
|
|
35
|
+
/** Connect the Domain catalog journey without schema discovery or reflection. */
|
|
46
36
|
export async function connectAdminCatalog(
|
|
47
37
|
context: AdminCatalogContext,
|
|
48
38
|
dependencies: AdminCatalogDependencies = {},
|
|
49
39
|
): Promise<AdminCatalogApi> {
|
|
50
|
-
const binding = requireAdminBinding(await (dependencies.bind ?? bindAdmin)(context.session))
|
|
51
|
-
const Domain = requireAdminClass(binding, 'Domain', 'node')
|
|
52
|
-
const Fleet = requireAdminClass(binding, 'Fleet', 'node')
|
|
53
|
-
const fleetInstallsByDefault = requireAdminClass(
|
|
54
|
-
binding,
|
|
55
|
-
'fleet_installs_domain_by_default',
|
|
56
|
-
'edge',
|
|
57
|
-
)
|
|
58
|
-
const fleet = requireAdminCore(binding, 'fleet')
|
|
59
40
|
const operationId = dependencies.operationId ?? defaultOperationId
|
|
60
41
|
|
|
61
42
|
const list = async (): Promise<DomainInfo[]> => {
|
|
62
43
|
const [nodes, defaultsPage] = await Promise.all([
|
|
63
44
|
readAllNodes(
|
|
64
45
|
context.graph,
|
|
65
|
-
Query.from({ nodes: [Domain] }).select({
|
|
46
|
+
Query.from({ nodes: [AdminContract.classes.Domain] }).select({
|
|
66
47
|
kind: 'nodes',
|
|
67
48
|
projection: { kind: 'value' },
|
|
68
49
|
}),
|
|
@@ -72,16 +53,20 @@ export async function connectAdminCatalog(
|
|
|
72
53
|
maximumPages: MAXIMUM_PAGES,
|
|
73
54
|
},
|
|
74
55
|
),
|
|
75
|
-
context.graph.neighbors(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
56
|
+
context.graph.neighbors(
|
|
57
|
+
AdminContract.fleet,
|
|
58
|
+
AdminContract.edges.fleetInstallsDomainByDefault,
|
|
59
|
+
{
|
|
60
|
+
direction: 'outgoing',
|
|
61
|
+
page: { size: PAGE_SIZE },
|
|
62
|
+
},
|
|
63
|
+
),
|
|
79
64
|
])
|
|
80
65
|
const defaults = await defaultsPage.collect({ maximumPages: MAXIMUM_PAGES })
|
|
81
66
|
if (defaults.cursor !== null)
|
|
82
67
|
throw new TypeError('Admin default Domain catalog exceeded its bound.')
|
|
83
68
|
const defaultIds = new Set(defaults.nodes.map((node) => String(node.id)))
|
|
84
|
-
return nodes.map((node) => domainFromNode(
|
|
69
|
+
return nodes.map((node) => domainFromNode(node, defaultIds.has(String(node.id))))
|
|
85
70
|
}
|
|
86
71
|
|
|
87
72
|
const requireDomain = async (identifier: string): Promise<DomainInfo> => {
|
|
@@ -110,7 +95,7 @@ export async function connectAdminCatalog(
|
|
|
110
95
|
let entry = existing
|
|
111
96
|
if (registryChanged) {
|
|
112
97
|
entry = domainFromSummary(
|
|
113
|
-
await
|
|
98
|
+
await callAdminMethod(context.session, AdminContract.fleet, 'publishDomain', {
|
|
114
99
|
operationId: operationId('publish'),
|
|
115
100
|
origin: input.origin,
|
|
116
101
|
name: input.name,
|
|
@@ -127,17 +112,10 @@ export async function connectAdminCatalog(
|
|
|
127
112
|
(entry.installByDefault ?? false) !== input.installByDefault
|
|
128
113
|
if (defaultChanged) {
|
|
129
114
|
entry = domainFromSummary(
|
|
130
|
-
await
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
'configureDefault',
|
|
135
|
-
Path.parse(entry.id),
|
|
136
|
-
{
|
|
137
|
-
operationId: operationId('configure-default'),
|
|
138
|
-
enabled: input.installByDefault,
|
|
139
|
-
},
|
|
140
|
-
),
|
|
115
|
+
await callAdminMethod(context.session, Path.parse(entry.id), 'configureDefault', {
|
|
116
|
+
operationId: operationId('configure-default'),
|
|
117
|
+
enabled: input.installByDefault,
|
|
118
|
+
}),
|
|
141
119
|
input.installByDefault === true,
|
|
142
120
|
)
|
|
143
121
|
}
|
|
@@ -150,29 +128,23 @@ export async function connectAdminCatalog(
|
|
|
150
128
|
})
|
|
151
129
|
}
|
|
152
130
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
function domainFromNode(
|
|
156
|
-
definition: DynamicDefinition,
|
|
157
|
-
node: Node,
|
|
158
|
-
installByDefault: boolean,
|
|
159
|
-
): DomainInfo {
|
|
131
|
+
function domainFromNode(node: Node, installByDefault: boolean): DomainInfo {
|
|
160
132
|
return Object.freeze({
|
|
161
133
|
id: Path.id(node.id).raw,
|
|
162
|
-
origin: requiredProperty(
|
|
163
|
-
name: requiredProperty(
|
|
164
|
-
url: requiredProperty(
|
|
165
|
-
...optionalProperty(
|
|
134
|
+
origin: requiredProperty(node, 'origin'),
|
|
135
|
+
name: requiredProperty(node, 'name'),
|
|
136
|
+
url: requiredProperty(node, 'discoveryUrl'),
|
|
137
|
+
...optionalProperty(node, 'description'),
|
|
166
138
|
...(installByDefault ? { installByDefault: true } : {}),
|
|
167
|
-
createdAt: requiredProperty(
|
|
168
|
-
updatedAt: requiredProperty(
|
|
139
|
+
createdAt: requiredProperty(node, 'createdAt'),
|
|
140
|
+
updatedAt: requiredProperty(node, 'updatedAt'),
|
|
169
141
|
})
|
|
170
142
|
}
|
|
171
143
|
|
|
172
144
|
function domainFromSummary(input: unknown, installByDefault: boolean): DomainInfo {
|
|
173
145
|
const value = record(input, 'Admin Domain summary')
|
|
174
146
|
return Object.freeze({
|
|
175
|
-
id:
|
|
147
|
+
id: requiredNodePath(value.id, 'Admin Domain id'),
|
|
176
148
|
origin: requiredString(value.origin, 'Admin Domain origin'),
|
|
177
149
|
name: requiredString(value.name, 'Admin Domain name'),
|
|
178
150
|
url: requiredString(value.discoveryUrl, 'Admin Domain discovery URL'),
|
|
@@ -185,19 +157,15 @@ function domainFromSummary(input: unknown, installByDefault: boolean): DomainInf
|
|
|
185
157
|
})
|
|
186
158
|
}
|
|
187
159
|
|
|
188
|
-
function requiredProperty(
|
|
189
|
-
return requiredString(
|
|
190
|
-
node.props[requireAdminProperty(definition, name).key],
|
|
191
|
-
`Admin Domain.${name}`,
|
|
192
|
-
)
|
|
160
|
+
function requiredProperty(node: Node, name: keyof typeof AdminContract.properties.domain): string {
|
|
161
|
+
return requiredString(node.props[AdminContract.properties.domain[name]], `Admin Domain.${name}`)
|
|
193
162
|
}
|
|
194
163
|
|
|
195
164
|
function optionalProperty(
|
|
196
|
-
definition: DynamicDefinition,
|
|
197
165
|
node: Node,
|
|
198
|
-
name:
|
|
166
|
+
name: keyof typeof AdminContract.properties.domain,
|
|
199
167
|
): Readonly<Record<string, string>> {
|
|
200
|
-
const value = node.props[
|
|
168
|
+
const value = node.props[AdminContract.properties.domain[name]]
|
|
201
169
|
return value === undefined ? {} : { [name]: requiredString(value, `Admin Domain.${name}`) }
|
|
202
170
|
}
|
|
203
171
|
|
|
@@ -206,6 +174,15 @@ function requiredString(input: unknown, label: string): string {
|
|
|
206
174
|
return input
|
|
207
175
|
}
|
|
208
176
|
|
|
177
|
+
function requiredNodePath(input: unknown, label: string): string {
|
|
178
|
+
const value = requiredString(input, label)
|
|
179
|
+
try {
|
|
180
|
+
return Path.parse(value).raw
|
|
181
|
+
} catch {
|
|
182
|
+
throw new TypeError(`${label} is invalid.`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
209
186
|
function record(input: unknown, label: string): Readonly<Record<string, unknown>> {
|
|
210
187
|
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
211
188
|
throw new TypeError(`${label} is invalid.`)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Input } from '@astrale-os/sdk/client'
|
|
2
|
+
import type { ClientSession } from '@astrale-os/sdk/client/session'
|
|
3
|
+
import type { ClassRef } from '@astrale-os/sdk/schema'
|
|
4
|
+
|
|
5
|
+
import { call } from '@astrale-os/sdk/client'
|
|
6
|
+
import { Path } from '@astrale-os/sdk/graph/path'
|
|
7
|
+
import { K, PropertyKey } from '@astrale-os/sdk/schema'
|
|
8
|
+
|
|
9
|
+
const origin = 'admin.astrale.ai'
|
|
10
|
+
|
|
11
|
+
function classRef(name: string): ClassRef {
|
|
12
|
+
return Object.freeze({ origin, kind: 'class', name }) as ClassRef
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const Domain = classRef('Domain')
|
|
16
|
+
|
|
17
|
+
export const AdminContract = Object.freeze({
|
|
18
|
+
origin,
|
|
19
|
+
fleet: Path.parse('/:admin.astrale.ai:core.fleet'),
|
|
20
|
+
classes: Object.freeze({
|
|
21
|
+
Domain,
|
|
22
|
+
}),
|
|
23
|
+
edges: Object.freeze({
|
|
24
|
+
fleetInstallsDomainByDefault: classRef('fleet_installs_domain_by_default'),
|
|
25
|
+
}),
|
|
26
|
+
properties: Object.freeze({
|
|
27
|
+
domain: Object.freeze({
|
|
28
|
+
origin: PropertyKey.of(Domain, 'origin'),
|
|
29
|
+
name: K.classes.Named.properties.name.key,
|
|
30
|
+
discoveryUrl: PropertyKey.of(Domain, 'discoveryUrl'),
|
|
31
|
+
description: K.classes.Descriptable.properties.description.key,
|
|
32
|
+
createdAt: K.classes.Timestamped.properties.createdAt.key,
|
|
33
|
+
updatedAt: K.classes.Timestamped.properties.updatedAt.key,
|
|
34
|
+
}),
|
|
35
|
+
}),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/** Invoke one stable Admin instance Method without schema discovery or reflection. */
|
|
39
|
+
export function callAdminMethod(
|
|
40
|
+
session: ClientSession,
|
|
41
|
+
receiver: Path,
|
|
42
|
+
method: string,
|
|
43
|
+
input: Input,
|
|
44
|
+
): Promise<unknown> {
|
|
45
|
+
return session.call(call(Path.instanceMethod(receiver, method), input))
|
|
46
|
+
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import type { ClientSession } from '@astrale-os/sdk/client/session'
|
|
2
|
-
import type { DomainBinding } from '@astrale-os/shell'
|
|
3
|
-
|
|
4
|
-
import type { AdminGraphApi } from '../../graph/.spec/api.js'
|
|
5
2
|
|
|
6
3
|
export type InstanceState = 'provisioning' | 'ready' | 'deleting' | 'failed' | 'deleted'
|
|
7
4
|
|
|
@@ -11,10 +8,11 @@ export interface InstanceInfo {
|
|
|
11
8
|
readonly url: string
|
|
12
9
|
readonly hostId?: string
|
|
13
10
|
readonly region?: string
|
|
14
|
-
readonly state
|
|
11
|
+
readonly state: InstanceState
|
|
15
12
|
readonly phase?: string
|
|
16
13
|
readonly error?: string | null
|
|
17
14
|
readonly createdAt?: string
|
|
15
|
+
readonly updatedAt?: string
|
|
18
16
|
readonly organizationId?: string
|
|
19
17
|
}
|
|
20
18
|
|
|
@@ -33,12 +31,11 @@ export interface DomainInstallReceipt {
|
|
|
33
31
|
|
|
34
32
|
export interface AdminInstanceContext {
|
|
35
33
|
readonly session: ClientSession
|
|
36
|
-
readonly graph: AdminGraphApi
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
export interface AdminInstanceApi {
|
|
40
37
|
list(): Promise<OwnedInstanceInfo[]>
|
|
41
|
-
create(slug: string
|
|
38
|
+
create(slug: string): Promise<InstanceInfo>
|
|
42
39
|
status(identifier: string): Promise<InstanceInfo>
|
|
43
40
|
delete(identifier: string): Promise<InstanceInfo>
|
|
44
41
|
installDomain(identifier: string, domain: string): Promise<DomainInstallReceipt>
|
|
@@ -51,7 +48,6 @@ export class AdminInstanceNotFoundError extends Error {
|
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
export interface AdminInstanceDependencies {
|
|
54
|
-
readonly bind?: (session: ClientSession) => Promise<DomainBinding>
|
|
55
51
|
readonly operationId?: (kind: 'create' | 'status' | 'delete' | 'install-domain') => string
|
|
56
52
|
}
|
|
57
53
|
|
|
@@ -63,4 +59,4 @@ export function findOwnedInstance(
|
|
|
63
59
|
instances: readonly OwnedInstanceInfo[],
|
|
64
60
|
identifier: string,
|
|
65
61
|
): OwnedInstanceInfo | undefined
|
|
66
|
-
export function
|
|
62
|
+
export function formatInstanceState(info: InstanceInfo): string
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# CLI Admin Instance adapter
|
|
2
2
|
|
|
3
|
-
The adapter
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The adapter calls the stable singleton Fleet receiver directly. Listing and automatic creation are
|
|
4
|
+
one Admin Method call each; connection performs no schema discovery, introspection, or graph read.
|
|
5
|
+
Every returned summary is decoded locally before it reaches a command.
|
|
6
6
|
|
|
7
|
-
Automatic creation invokes `Fleet.createInstance
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
Automatic creation invokes `Fleet.createInstance`. Status, deletion, and Domain installation first
|
|
8
|
+
resolve a caller-visible Instance through Fleet and then invoke that exact Instance receiver. The
|
|
9
|
+
adapter creates internal operation IDs but preserves the existing CLI projection and
|
|
10
|
+
selection-required experience.
|
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { adminSession } from '../../__tests__/fixture'
|
|
4
4
|
import { connectAdminInstances } from '../client'
|
|
5
5
|
|
|
6
|
-
function fixture(input: {
|
|
7
|
-
invoke?: (method: { owner: string; name: string }, receiver: unknown, input: unknown) => unknown
|
|
8
|
-
}) {
|
|
6
|
+
function fixture(input: { invoke?: (target: string, input: unknown) => unknown }) {
|
|
9
7
|
const calls: Array<{
|
|
10
|
-
|
|
11
|
-
receiver: string
|
|
8
|
+
target: string
|
|
12
9
|
value: unknown
|
|
13
10
|
}> = []
|
|
14
|
-
const remote = adminSession((
|
|
15
|
-
calls.push({
|
|
16
|
-
return input.invoke?.(
|
|
11
|
+
const remote = adminSession((target, value) => {
|
|
12
|
+
calls.push({ target, value })
|
|
13
|
+
return input.invoke?.(target, value)
|
|
17
14
|
})
|
|
18
|
-
const binding = adminBinding()
|
|
19
15
|
return {
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
call: remote.call,
|
|
17
|
+
reflection: remote.reflection,
|
|
22
18
|
calls,
|
|
23
19
|
connect: () =>
|
|
24
20
|
connectAdminInstances(
|
|
25
21
|
{ session: remote.session },
|
|
26
22
|
{
|
|
27
|
-
bind: async () => binding,
|
|
28
23
|
operationId: (kind) => `cli.instance.${kind}:test`,
|
|
29
24
|
},
|
|
30
25
|
),
|
|
@@ -34,13 +29,15 @@ function fixture(input: {
|
|
|
34
29
|
describe('V2 Admin Instance adapter', () => {
|
|
35
30
|
test('lists caller-visible Instances through the Admin-owned Fleet operation', async () => {
|
|
36
31
|
const contract = fixture({
|
|
37
|
-
invoke: (
|
|
38
|
-
|
|
32
|
+
invoke: (target) =>
|
|
33
|
+
target.endsWith('::listInstances')
|
|
39
34
|
? [
|
|
40
35
|
{
|
|
41
36
|
id: '@instance-node',
|
|
42
37
|
slug: 'demo',
|
|
43
38
|
url: 'https://demo.eu.astrale.ai',
|
|
39
|
+
hostId: '@host-node',
|
|
40
|
+
region: 'fr-par',
|
|
44
41
|
state: 'ready',
|
|
45
42
|
createdAt: '2026-08-12T00:00:00.000Z',
|
|
46
43
|
updatedAt: '2026-08-12T00:00:00.000Z',
|
|
@@ -54,17 +51,20 @@ describe('V2 Admin Instance adapter', () => {
|
|
|
54
51
|
id: '@instance-node',
|
|
55
52
|
slug: 'demo',
|
|
56
53
|
url: 'https://demo.eu.astrale.ai',
|
|
54
|
+
hostId: '@host-node',
|
|
55
|
+
region: 'fr-par',
|
|
57
56
|
state: 'ready',
|
|
58
57
|
createdAt: '2026-08-12T00:00:00.000Z',
|
|
58
|
+
updatedAt: '2026-08-12T00:00:00.000Z',
|
|
59
59
|
},
|
|
60
60
|
])
|
|
61
61
|
expect(contract.calls).toEqual([
|
|
62
62
|
{
|
|
63
|
-
|
|
64
|
-
receiver: '/:admin.astrale.ai:core.fleet::listInstances',
|
|
63
|
+
target: '/:admin.astrale.ai:core.fleet::listInstances',
|
|
65
64
|
value: {},
|
|
66
65
|
},
|
|
67
66
|
])
|
|
67
|
+
expect(contract.reflection).not.toHaveBeenCalled()
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
test('delegates default placement to Fleet without reading Host inventory', async () => {
|
|
@@ -87,8 +87,7 @@ describe('V2 Admin Instance adapter', () => {
|
|
|
87
87
|
})
|
|
88
88
|
expect(contract.calls).toEqual([
|
|
89
89
|
{
|
|
90
|
-
|
|
91
|
-
receiver: '/:admin.astrale.ai:core.fleet::createInstance',
|
|
90
|
+
target: '/:admin.astrale.ai:core.fleet::createInstance',
|
|
92
91
|
value: { operationId: 'cli.instance.create:test', slug: 'demo' },
|
|
93
92
|
},
|
|
94
93
|
])
|
|
@@ -96,8 +95,8 @@ describe('V2 Admin Instance adapter', () => {
|
|
|
96
95
|
|
|
97
96
|
test('refreshes and deletes through the resolved Instance receiver', async () => {
|
|
98
97
|
const contract = fixture({
|
|
99
|
-
invoke: (
|
|
100
|
-
|
|
98
|
+
invoke: (target) =>
|
|
99
|
+
target.endsWith('::listInstances')
|
|
101
100
|
? [
|
|
102
101
|
{
|
|
103
102
|
id: '@instance-node',
|
|
@@ -112,25 +111,34 @@ describe('V2 Admin Instance adapter', () => {
|
|
|
112
111
|
id: '@instance-node',
|
|
113
112
|
slug: 'demo',
|
|
114
113
|
url: 'https://demo.eu.astrale.ai',
|
|
115
|
-
state:
|
|
114
|
+
state: target.endsWith('::delete') ? 'deleted' : 'ready',
|
|
115
|
+
createdAt: '2026-08-12T00:00:00.000Z',
|
|
116
|
+
updatedAt: '2026-08-12T00:00:00.000Z',
|
|
116
117
|
},
|
|
117
118
|
})
|
|
118
119
|
const api = await contract.connect()
|
|
119
120
|
|
|
120
121
|
await expect(api.status('demo')).resolves.toMatchObject({ state: 'ready' })
|
|
121
122
|
await expect(api.delete('@instance-node')).resolves.toMatchObject({ state: 'deleted' })
|
|
122
|
-
expect(contract.calls
|
|
123
|
-
{
|
|
124
|
-
{
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
expect(contract.calls).toEqual([
|
|
124
|
+
{ target: '/:admin.astrale.ai:core.fleet::listInstances', value: {} },
|
|
125
|
+
{
|
|
126
|
+
target: '@instance-node::status',
|
|
127
|
+
value: { operationId: 'cli.instance.status:test' },
|
|
128
|
+
},
|
|
129
|
+
{ target: '/:admin.astrale.ai:core.fleet::listInstances', value: {} },
|
|
130
|
+
{
|
|
131
|
+
target: '@instance-node::delete',
|
|
132
|
+
value: { operationId: 'cli.instance.delete:test' },
|
|
133
|
+
},
|
|
127
134
|
])
|
|
135
|
+
expect(contract.reflection).not.toHaveBeenCalled()
|
|
128
136
|
})
|
|
129
137
|
|
|
130
138
|
test('installs a resolved catalog Domain through Instance.installDomain', async () => {
|
|
131
139
|
const contract = fixture({
|
|
132
|
-
invoke: (
|
|
133
|
-
|
|
140
|
+
invoke: (target) =>
|
|
141
|
+
target.endsWith('::listInstances')
|
|
134
142
|
? [
|
|
135
143
|
{
|
|
136
144
|
id: '@instance-node',
|
|
@@ -158,9 +166,108 @@ describe('V2 Admin Instance adapter', () => {
|
|
|
158
166
|
ok: true,
|
|
159
167
|
})
|
|
160
168
|
expect(contract.calls.at(-1)).toEqual({
|
|
161
|
-
|
|
162
|
-
receiver: '@instance-node::installDomain',
|
|
169
|
+
target: '@instance-node::installDomain',
|
|
163
170
|
value: { operationId: 'cli.instance.install-domain:test', domain: '@crm-domain' },
|
|
164
171
|
})
|
|
172
|
+
expect(contract.reflection).not.toHaveBeenCalled()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('connects without network I/O and list performs exactly one call', async () => {
|
|
176
|
+
const contract = fixture({ invoke: () => [] })
|
|
177
|
+
|
|
178
|
+
const api = await contract.connect()
|
|
179
|
+
expect(contract.call).not.toHaveBeenCalled()
|
|
180
|
+
expect(contract.reflection).not.toHaveBeenCalled()
|
|
181
|
+
|
|
182
|
+
await expect(api.list()).resolves.toEqual([])
|
|
183
|
+
expect(contract.call).toHaveBeenCalledTimes(1)
|
|
184
|
+
expect(contract.reflection).not.toHaveBeenCalled()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('rejects malformed inventory, lifecycle, Method, and install outputs', async () => {
|
|
188
|
+
await expect((await fixture({ invoke: () => ({}) }).connect()).list()).rejects.toThrow(
|
|
189
|
+
'Admin Instance inventory is invalid.',
|
|
190
|
+
)
|
|
191
|
+
await expect(
|
|
192
|
+
(
|
|
193
|
+
await fixture({
|
|
194
|
+
invoke: () => [{ id: '@instance-node', slug: 'demo', state: 'mystery' }],
|
|
195
|
+
}).connect()
|
|
196
|
+
).list(),
|
|
197
|
+
).rejects.toThrow('Admin Instance state is invalid.')
|
|
198
|
+
await expect(
|
|
199
|
+
(
|
|
200
|
+
await fixture({
|
|
201
|
+
invoke: () => [{ id: 'not-a-path', slug: 'demo', state: 'ready' }],
|
|
202
|
+
}).connect()
|
|
203
|
+
).list(),
|
|
204
|
+
).rejects.toThrow('Admin Instance id is invalid.')
|
|
205
|
+
await expect(
|
|
206
|
+
(
|
|
207
|
+
await fixture({
|
|
208
|
+
invoke: () => [
|
|
209
|
+
{ id: '@instance-node', slug: 'demo', hostId: 'not-a-path', state: 'ready' },
|
|
210
|
+
],
|
|
211
|
+
}).connect()
|
|
212
|
+
).list(),
|
|
213
|
+
).rejects.toThrow('Admin Host id is invalid.')
|
|
214
|
+
|
|
215
|
+
const malformedStatus = fixture({
|
|
216
|
+
invoke: (target) =>
|
|
217
|
+
target.endsWith('::listInstances')
|
|
218
|
+
? [
|
|
219
|
+
{
|
|
220
|
+
id: '@instance-node',
|
|
221
|
+
slug: 'demo',
|
|
222
|
+
state: 'ready',
|
|
223
|
+
createdAt: '2026-08-12T00:00:00.000Z',
|
|
224
|
+
updatedAt: '2026-08-12T00:00:00.000Z',
|
|
225
|
+
},
|
|
226
|
+
]
|
|
227
|
+
: {
|
|
228
|
+
id: '@instance-node',
|
|
229
|
+
slug: 'demo',
|
|
230
|
+
state: 'ready',
|
|
231
|
+
failure: {},
|
|
232
|
+
createdAt: '2026-08-12T00:00:00.000Z',
|
|
233
|
+
updatedAt: '2026-08-12T00:00:00.000Z',
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
await expect((await malformedStatus.connect()).status('demo')).rejects.toThrow(
|
|
237
|
+
'Admin failure message is invalid.',
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
const malformedInstall = fixture({
|
|
241
|
+
invoke: (target) =>
|
|
242
|
+
target.endsWith('::listInstances')
|
|
243
|
+
? [
|
|
244
|
+
{
|
|
245
|
+
id: '@instance-node',
|
|
246
|
+
slug: 'demo',
|
|
247
|
+
state: 'ready',
|
|
248
|
+
createdAt: '2026-08-12T00:00:00.000Z',
|
|
249
|
+
updatedAt: '2026-08-12T00:00:00.000Z',
|
|
250
|
+
},
|
|
251
|
+
]
|
|
252
|
+
: {
|
|
253
|
+
domain: '@crm-domain',
|
|
254
|
+
instance: '@instance-node',
|
|
255
|
+
origin: 'crm.acme.dev',
|
|
256
|
+
ok: 'yes',
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
await expect(
|
|
260
|
+
(await malformedInstall.connect()).installDomain('demo', '@crm-domain'),
|
|
261
|
+
).rejects.toThrow('Admin Domain install outcome is invalid.')
|
|
262
|
+
|
|
263
|
+
const malformedInstallPath = fixture({
|
|
264
|
+
invoke: (target) =>
|
|
265
|
+
target.endsWith('::listInstances')
|
|
266
|
+
? [{ id: '@instance-node', slug: 'demo', state: 'ready' }]
|
|
267
|
+
: { domain: 'not-a-path', instance: '@instance-node', origin: 'crm.acme.dev', ok: true },
|
|
268
|
+
})
|
|
269
|
+
await expect(
|
|
270
|
+
(await malformedInstallPath.connect()).installDomain('demo', '@crm-domain'),
|
|
271
|
+
).rejects.toThrow('Admin Domain reference is invalid.')
|
|
165
272
|
})
|
|
166
273
|
})
|