@astrale-os/cli 1.0.0-beta.11 → 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.
Files changed (56) hide show
  1. package/dist/astrale.js +15432 -14745
  2. package/dist/types/admin/contract.d.ts +26 -0
  3. package/dist/types/admin/instance/client.d.ts +2 -4
  4. package/dist/types/admin/instance/model.d.ts +3 -0
  5. package/package.json +1 -1
  6. package/src/admin/.spec/architecture.md +12 -5
  7. package/src/admin/__tests__/fixture.ts +19 -95
  8. package/src/admin/catalog/.spec/api.d.ts +0 -2
  9. package/src/admin/catalog/.spec/architecture.md +5 -4
  10. package/src/admin/catalog/__tests__/client.test.ts +136 -33
  11. package/src/admin/catalog/client.ts +38 -61
  12. package/src/admin/contract.ts +46 -0
  13. package/src/admin/instance/.spec/api.d.ts +4 -8
  14. package/src/admin/instance/.spec/architecture.md +7 -7
  15. package/src/admin/instance/__tests__/client.test.ts +138 -31
  16. package/src/admin/instance/client.ts +32 -37
  17. package/src/admin/instance/model.ts +3 -0
  18. package/src/commands/__tests__/call.test.ts +34 -0
  19. package/src/commands/__tests__/read-commands.test.ts +27 -0
  20. package/src/commands/__tests__/token-ttl.test.ts +49 -4
  21. package/src/commands/call.ts +28 -3
  22. package/src/commands/query.ts +8 -2
  23. package/src/commands/token.ts +34 -20
  24. package/src/commands/ui/__tests__/commands.test.ts +129 -0
  25. package/src/commands/ui/add.ts +51 -0
  26. package/src/commands/ui/doctor.ts +13 -0
  27. package/src/commands/ui/init.ts +38 -0
  28. package/src/commands/ui/list.ts +26 -0
  29. package/src/commands/ui/preset-apply.ts +19 -0
  30. package/src/commands/ui/preset-list.ts +12 -0
  31. package/src/commands/ui/shared.ts +25 -0
  32. package/src/lib/__tests__/binary.test.ts +16 -1
  33. package/src/lib/binary.ts +22 -5
  34. package/src/lib/proc.ts +7 -2
  35. package/src/program/.spec/api.d.ts +1 -0
  36. package/src/program/__tests__/program.test.ts +32 -2
  37. package/src/program/build.ts +23 -1
  38. package/src/program/command.ts +1 -0
  39. package/src/program/registry.ts +2 -1
  40. package/src/ui/.spec/api.d.ts +14 -0
  41. package/src/ui/.spec/architecture.md +10 -0
  42. package/src/ui/.spec/laws.ts +36 -0
  43. package/src/ui/.spec/layout.ts +16 -0
  44. package/src/ui/__tests__/ui.test.ts +542 -0
  45. package/src/ui/index.ts +13 -0
  46. package/src/ui/lock.ts +87 -0
  47. package/src/ui/model.ts +83 -0
  48. package/src/ui/operations.ts +539 -0
  49. package/src/ui/project.ts +146 -0
  50. package/src/ui/release.ts +267 -0
  51. package/src/ui/runner.ts +18 -0
  52. package/studio/server/agent/harness/gateway/token.test.ts +1 -1
  53. package/studio/server/agent/harness/gateway/token.ts +3 -3
  54. package/dist/types/admin/binding.d.ts +0 -19
  55. package/src/admin/__tests__/binding.test.ts +0 -31
  56. 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
- /** Bind the discovered Admin revision and expose its Domain catalog journey. */
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(fleet, fleetInstallsByDefault, {
76
- direction: 'outgoing',
77
- page: { size: PAGE_SIZE },
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(Domain, node, defaultIds.has(String(node.id))))
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 invokeAdminMethod(context.session, binding, Fleet, 'publishDomain', fleet, {
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 invokeAdminMethod(
131
- context.session,
132
- binding,
133
- Domain,
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
- type DynamicDefinition = ResolvedClass<'node'>
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(definition, node, 'origin'),
163
- name: requiredProperty(definition, node, 'name'),
164
- url: requiredProperty(definition, node, 'discoveryUrl'),
165
- ...optionalProperty(definition, node, 'description'),
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(definition, node, 'createdAt'),
168
- updatedAt: requiredProperty(definition, node, 'updatedAt'),
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: requiredString(value.id, 'Admin Domain 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(definition: DynamicDefinition, node: Node, name: string): string {
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: string,
166
+ name: keyof typeof AdminContract.properties.domain,
199
167
  ): Readonly<Record<string, string>> {
200
- const value = node.props[requireAdminProperty(definition, name).key]
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?: InstanceState
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, hostId?: string): Promise<InstanceInfo>
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 formatInstanceLocation(info: InstanceInfo): string
62
+ export function formatInstanceState(info: InstanceInfo): string
@@ -1,10 +1,10 @@
1
1
  # CLI Admin Instance adapter
2
2
 
3
- The adapter discovers and binds the configured Admin Domain, proves its origin, and resolves the
4
- singleton Fleet receiver. Policy-visible `Instance` and `Host` Nodes come from bounded Graph queries;
5
- Host location is joined through `instance_runs_on_host`.
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`; explicit `--host-id` resolves one visible,
8
- non-reserved Host and invokes `Host.createInstance`. Status, deletion, and Domain installation
9
- invoke the exact Instance receiver. The adapter creates internal operation IDs but preserves the
10
- existing CLI projection and selection-required experience.
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 { adminBinding, adminSession } from '../../__tests__/fixture'
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
- method: { owner: string; name: string }
11
- receiver: string
8
+ target: string
12
9
  value: unknown
13
10
  }> = []
14
- const remote = adminSession((method, receiver, value) => {
15
- calls.push({ method, receiver: String(receiver), value })
16
- return input.invoke?.(method, receiver, value)
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
- binding,
21
- invoke: remote.invoke,
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: (method) =>
38
- method.name === 'listInstances'
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
- method: { owner: 'Fleet', name: 'listInstances' },
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
- method: { owner: 'Fleet', name: 'createInstance' },
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: (method) =>
100
- method.name === 'listInstances'
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: method.name === 'delete' ? 'deleted' : 'ready',
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.map(({ method }) => method)).toEqual([
123
- { owner: 'Fleet', name: 'listInstances' },
124
- { owner: 'Instance', name: 'status' },
125
- { owner: 'Fleet', name: 'listInstances' },
126
- { owner: 'Instance', name: 'delete' },
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: (method) =>
133
- method.name === 'listInstances'
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
- method: { owner: 'Instance', name: 'installDomain' },
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
  })