@astrale-os/cli 0.8.1-alpha.6 → 1.0.0-beta.0
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/README.md +18 -5
- package/dist/astrale.js +2661 -2714
- package/dist/public/connect-core.js +2019 -3050
- package/dist/public/keys/index.js +1851 -2885
- package/dist/public/paths/index.js +1830 -2872
- package/dist/types/connection/auth.d.ts +3 -0
- package/dist/types/lib/instance.d.ts +10 -0
- package/package.json +9 -9
- package/src/commands/__tests__/domain-install-operation.test.ts +121 -0
- package/src/commands/__tests__/domain-install-owned.test.ts +66 -0
- package/src/commands/__tests__/domain-uninstall.test.ts +53 -0
- package/src/commands/__tests__/install-direct.test.ts +3 -2
- package/src/commands/__tests__/install-identity-override.test.ts +14 -3
- package/src/commands/__tests__/instance-bookmark.test.ts +66 -1
- package/src/commands/__tests__/instance-list-rows.test.ts +1 -0
- package/src/commands/__tests__/instance-use.test.ts +67 -0
- package/src/commands/__tests__/view-build.test.ts +58 -0
- package/src/commands/domain/install.ts +84 -20
- package/src/commands/domain/uninstall.ts +128 -0
- package/src/commands/instance/active.ts +13 -1
- package/src/commands/instance/bookmark.ts +26 -3
- package/src/commands/instance/list.ts +18 -4
- package/src/commands/instance/use.ts +54 -7
- package/src/commands/view.ts +28 -16
- package/src/connection/.spec/architecture.md +5 -0
- package/src/connection/.spec/laws/connection.ts +20 -0
- package/src/connection/.spec/layout.ts +1 -0
- package/src/connection/__tests__/auth.test.ts +27 -1
- package/src/connection/__tests__/ca-fetch.test.ts +8 -1
- package/src/connection/__tests__/errors.test.ts +483 -33
- package/src/connection/__tests__/exchange.test.ts +46 -5
- package/src/connection/__tests__/reasons.test.ts +78 -0
- package/src/connection/auth.ts +11 -9
- package/src/connection/command.ts +9 -1
- package/src/connection/errors.ts +149 -159
- package/src/connection/exchange.ts +14 -2
- package/src/connection/index.ts +1 -1
- package/src/connection/reasons.ts +193 -0
- package/src/lib/__tests__/instance.test.ts +51 -1
- package/src/lib/__tests__/view-assets.test.ts +33 -1
- package/src/lib/__tests__/view-server.test.ts +68 -0
- package/src/lib/ca-fetch.ts +9 -3
- package/src/lib/instance.ts +31 -0
- package/src/lib/view/assets.ts +16 -2
- package/src/program/__tests__/program.test.ts +2 -1
- package/src/program/build.ts +2 -1
- package/studio/client/dist/assets/{elk-api-D0cBetPW.js → elk-api-D2xgMJvi.js} +1 -1
- package/studio/client/dist/assets/{index-BQnJ5sgd.css → index-BMdnsIJA.css} +1 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +8 -0
- package/studio/client/dist/assets/index-LGSWRrk8.js +81 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +8 -9
- package/studio/server/agent/prompts/anchors.test.ts +74 -0
- package/studio/server/agent/prompts/anchors.ts +85 -15
- package/studio/server/agent/prompts/system.test.ts +12 -0
- package/studio/server/agent/prompts/system.ts +6 -6
- package/studio/server/api.ts +1 -5
- package/studio/server/cache.ts +5 -2
- package/studio/server/domain.test.ts +67 -0
- package/studio/server/domain.ts +29 -6
- package/studio/server/index.ts +1 -3
- package/studio/server/introspect/anatomy-extras.test.ts +104 -1
- package/studio/server/introspect/anatomy-extras.ts +340 -8
- package/studio/server/introspect/anatomy.test.ts +33 -0
- package/studio/server/introspect/anatomy.ts +22 -7
- package/studio/server/introspect/bundle.ts +7 -1
- package/studio/server/introspect/canonical-schema.test.ts +395 -0
- package/studio/server/introspect/canonical-schema.ts +751 -0
- package/studio/server/introspect/core-extractor.ts +30 -10
- package/studio/server/introspect/core.ts +4 -2
- package/studio/server/introspect/diff.test.ts +124 -0
- package/studio/server/introspect/diff.ts +252 -23
- package/studio/server/introspect/extractor.ts +46 -14
- package/studio/server/introspect/overlay-tsmorph.test.ts +164 -1
- package/studio/server/introspect/overlay-tsmorph.ts +381 -106
- package/studio/server/introspect/overlay.test.ts +72 -0
- package/studio/server/introspect/overlay.ts +16 -6
- package/studio/server/introspect/runtime.test.ts +217 -0
- package/studio/server/introspect/runtime.ts +18 -6
- package/studio/server/introspect/schema-refs.test.ts +100 -0
- package/studio/server/introspect/schema-refs.ts +23 -2
- package/studio/server/state/baseline.test.ts +51 -0
- package/studio/server/state/baseline.ts +31 -2
- package/studio/server/state/create.test.ts +37 -0
- package/studio/server/state/create.ts +21 -15
- package/studio/server/state/instance.test.ts +63 -0
- package/studio/server/state/instance.ts +65 -29
- package/studio/server/state/views.test.ts +209 -9
- package/studio/server/state/views.ts +176 -69
- package/studio/server/watch.test.ts +36 -0
- package/studio/server/watch.ts +24 -16
- package/studio/server/workspace-watch.ts +8 -3
- package/studio/shared/types.ts +164 -33
- package/viewer/dist/main.js +57 -57
- package/studio/client/dist/assets/index-Dspir4w7.js +0 -81
- package/studio/client/dist/assets/index-bVD2KJgz.js +0 -8
- package/studio/server/view-dev-server.test.ts +0 -111
- package/studio/server/view-dev-server.ts +0 -372
|
@@ -15,7 +15,8 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
15
15
|
import { join } from 'node:path'
|
|
16
16
|
|
|
17
17
|
import { getBundle } from '../cache'
|
|
18
|
-
import { registerDomain } from '../domain'
|
|
18
|
+
import { isDomainDir, registerDomain } from '../domain'
|
|
19
|
+
import { findSchemaDefinition } from '../introspect/anatomy-extras'
|
|
19
20
|
import { bootDomain } from '../lifecycle'
|
|
20
21
|
import { stoppers, workspaceRoot, workspaceSchemaDirName } from '../workspace-state'
|
|
21
22
|
|
|
@@ -87,7 +88,8 @@ export async function createDomain(
|
|
|
87
88
|
...(instance ? ['--instance', instance] : []),
|
|
88
89
|
]
|
|
89
90
|
const scaffold = await run('npx', scaffoldArgs, root)
|
|
90
|
-
|
|
91
|
+
const schemaDirName = workspaceSchemaDirName()
|
|
92
|
+
if (!isDomainDir(dir, schemaDirName)) {
|
|
91
93
|
return {
|
|
92
94
|
ok: false,
|
|
93
95
|
error: 'Scaffolding did not produce a domain. See the log.',
|
|
@@ -97,7 +99,7 @@ export async function createDomain(
|
|
|
97
99
|
|
|
98
100
|
// 1b. Flag the placeholder origin for the agent (we ask for the name only, so the
|
|
99
101
|
// origin is `<name>.example.dev` until someone — or the agent — sets the real one).
|
|
100
|
-
annotateOrigin(dir)
|
|
102
|
+
annotateOrigin(dir, schemaDirName)
|
|
101
103
|
|
|
102
104
|
// 2. Install deps so the domain is fully introspectable + deployable. Best-effort:
|
|
103
105
|
// a scaffolded-but-uninstalled domain still loads (static fallback), so a failed
|
|
@@ -106,11 +108,11 @@ export async function createDomain(
|
|
|
106
108
|
|
|
107
109
|
// 3. Register + boot with deps present. The live watcher may have already booted a
|
|
108
110
|
// deps-less fallback for this dir mid-install — stop it and boot fresh.
|
|
109
|
-
const handle = registerDomain(dir,
|
|
111
|
+
const handle = registerDomain(dir, schemaDirName)
|
|
110
112
|
if (!handle) {
|
|
111
113
|
return {
|
|
112
114
|
ok: false,
|
|
113
|
-
error: 'Scaffold incomplete —
|
|
115
|
+
error: 'Scaffold incomplete — config, composition entry, or schema index is missing.',
|
|
114
116
|
output: combine(scaffold, install),
|
|
115
117
|
}
|
|
116
118
|
}
|
|
@@ -129,27 +131,31 @@ function combine(a: { output: string }, b: { output: string }): string {
|
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
/**
|
|
132
|
-
* Drop an agent-actionable marker on the origin line of the scaffolded
|
|
133
|
-
* schema/index.ts
|
|
134
|
+
* Drop an agent-actionable marker on the origin line of the scaffolded schema.
|
|
135
|
+
* The public schema/index.ts is now commonly a barrel, so the definition is
|
|
136
|
+
* located statically below it. We only ask for the NAME, so the origin starts as the
|
|
134
137
|
* `<name>.example.dev` placeholder — this comment tells a human (or the studio's
|
|
135
138
|
* agent) it's a placeholder to change, and that the studio re-parses the literal
|
|
136
139
|
* as the source of truth so the rename takes effect on save. Best-effort: the
|
|
137
140
|
* template already explains the origin, so a parse miss is harmless.
|
|
138
141
|
*/
|
|
139
|
-
function annotateOrigin(dir: string): void {
|
|
140
|
-
const
|
|
142
|
+
export function annotateOrigin(dir: string, schemaDirName = 'schema'): void {
|
|
143
|
+
const definition = findSchemaDefinition(dir, schemaDirName)
|
|
144
|
+
if (!definition) return
|
|
145
|
+
const file = definition.file
|
|
141
146
|
try {
|
|
142
147
|
const src = readFileSync(file, 'utf8')
|
|
143
148
|
if (src.includes('ORIGIN —')) return // already annotated (idempotent)
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const pad =
|
|
149
|
+
const lines = src.split('\n')
|
|
150
|
+
const index = Math.max(0, definition.line - 1)
|
|
151
|
+
const pad = lines[index]?.match(/^[ \t]*/)?.[0] ?? ''
|
|
147
152
|
const note =
|
|
148
153
|
`${pad}// ORIGIN — the domain's permanent identity in the graph. It was set from the name as a\n` +
|
|
149
|
-
`${pad}// PLACEHOLDER
|
|
154
|
+
`${pad}// PLACEHOLDER; change the literal or constant used here (e.g. to "crm.acme.dev"), ideally BEFORE\n` +
|
|
150
155
|
`${pad}// the first deploy (the origin is hard to change once installed). The studio parses this\n` +
|
|
151
|
-
`${pad}//
|
|
152
|
-
|
|
156
|
+
`${pad}// schema statically as the source of truth, so the rename refreshes on save.\n`
|
|
157
|
+
lines.splice(index, 0, note.trimEnd())
|
|
158
|
+
writeFileSync(file, lines.join('\n'))
|
|
153
159
|
} catch {
|
|
154
160
|
/* best-effort */
|
|
155
161
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { schemaHashOf } from '../introspect/hash'
|
|
4
|
+
import {
|
|
5
|
+
installedBundleRootHash,
|
|
6
|
+
installedDomainCommand,
|
|
7
|
+
installedDomainProbeResult,
|
|
8
|
+
} from './instance'
|
|
9
|
+
|
|
10
|
+
describe('installed Domain introspection', () => {
|
|
11
|
+
test('uses the public bundle introspection command for the selected instance', () => {
|
|
12
|
+
expect(installedDomainCommand('issues.astrale.ai', 'staging')).toEqual([
|
|
13
|
+
'astrale',
|
|
14
|
+
'introspect',
|
|
15
|
+
'issues.astrale.ai',
|
|
16
|
+
'--bundle',
|
|
17
|
+
'--json',
|
|
18
|
+
'-i',
|
|
19
|
+
'staging',
|
|
20
|
+
])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('hashes the installed Bundle root with the same deterministic hash as local schema', () => {
|
|
24
|
+
const root = {
|
|
25
|
+
version: 'v1',
|
|
26
|
+
domain: 'issues.astrale.ai',
|
|
27
|
+
classes: { Issue: { properties: { title: { type: 'string' } } } },
|
|
28
|
+
}
|
|
29
|
+
const probe = installedDomainProbeResult(
|
|
30
|
+
JSON.stringify({
|
|
31
|
+
state: 'ready',
|
|
32
|
+
bundle: { format: 'astrale.dsl.bundle', version: 'v1', root, closure: [] },
|
|
33
|
+
}),
|
|
34
|
+
'',
|
|
35
|
+
0,
|
|
36
|
+
)
|
|
37
|
+
expect(probe.state).toBe('installed')
|
|
38
|
+
expect(installedBundleRootHash(probe)).toBe(schemaHashOf(root))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('does not mistake the removed Domain Node shape for installation introspection', () => {
|
|
42
|
+
expect(
|
|
43
|
+
installedDomainProbeResult(
|
|
44
|
+
JSON.stringify({ path: '/issues.astrale.ai', props: { schema: '{"domain":"old"}' } }),
|
|
45
|
+
'',
|
|
46
|
+
0,
|
|
47
|
+
),
|
|
48
|
+
).toEqual({ state: 'unknown', root: null })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('recognizes the current CLI not-installed diagnostic', () => {
|
|
52
|
+
expect(
|
|
53
|
+
installedDomainProbeResult(
|
|
54
|
+
'',
|
|
55
|
+
JSON.stringify({
|
|
56
|
+
error: 'DOMAIN_NOT_INSTALLED',
|
|
57
|
+
message: 'Domain issues.astrale.ai is not installed on this Kernel.',
|
|
58
|
+
}),
|
|
59
|
+
1,
|
|
60
|
+
),
|
|
61
|
+
).toEqual({ state: 'not-installed', root: null })
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* The ACTIVE instance is GLOBAL (not per-domain) and the `astrale` CLI owns it —
|
|
5
5
|
* we never keep our own copy: `listInstances` reads `astrale instance list`,
|
|
6
6
|
* `setActiveInstance` runs `astrale instance use`. Install + drift are GROUND
|
|
7
|
-
* TRUTH, queried from the target instance (`astrale
|
|
8
|
-
* record — so a deploy done outside the studio (CLI/terminal) is still
|
|
7
|
+
* TRUTH, queried from the target instance (`astrale introspect <origin>`) — NOT
|
|
8
|
+
* a local record — so a deploy done outside the studio (CLI/terminal) is still
|
|
9
|
+
* seen.
|
|
9
10
|
*
|
|
10
11
|
* Deploy (`pnpm prod`) = the managed astrale adapter: deploy + auto-install on
|
|
11
12
|
* the configured instance; we capture the printed service URL.
|
|
@@ -117,18 +118,70 @@ export function lastDeploy(root: string): DeployRecord | null {
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
/**
|
|
120
|
-
* Ask the target instance for
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* authed / unknown instance) ⇒ unknown (never falsely
|
|
121
|
+
* Ask the target instance for its public installation introspection. Installed
|
|
122
|
+
* ⇒ the Kernel returns the admitted Bundle, whose root is the exact schema value
|
|
123
|
+
* hashed locally; `DOMAIN_NOT_INSTALLED` ⇒ not installed; anything else
|
|
124
|
+
* (offline / not authed / unknown instance) ⇒ unknown (never falsely
|
|
125
|
+
* "not installed").
|
|
124
126
|
*/
|
|
127
|
+
export function installedDomainCommand(origin: string, instance: string): string[] {
|
|
128
|
+
return ['astrale', 'introspect', origin, '--bundle', '--json', '-i', instance]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type InstalledDomainProbe =
|
|
132
|
+
| { state: 'installed'; root: unknown }
|
|
133
|
+
| { state: 'not-installed' | 'unknown'; root: null }
|
|
134
|
+
|
|
135
|
+
function jsonObject(text: string): Record<string, unknown> | null {
|
|
136
|
+
try {
|
|
137
|
+
const value = JSON.parse(text)
|
|
138
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null
|
|
139
|
+
} catch {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function installedDomainProbeResult(
|
|
145
|
+
stdout: string,
|
|
146
|
+
stderr: string,
|
|
147
|
+
exitCode: number,
|
|
148
|
+
): InstalledDomainProbe {
|
|
149
|
+
const output = jsonObject(stdout)
|
|
150
|
+
const diagnostic = jsonObject(stderr)
|
|
151
|
+
const error = output?.error ?? diagnostic?.error
|
|
152
|
+
if (
|
|
153
|
+
error === 'DOMAIN_NOT_INSTALLED' ||
|
|
154
|
+
error === 'NOT_FOUND' ||
|
|
155
|
+
/\b(?:DOMAIN_NOT_INSTALLED|NOT_FOUND)\b/.test(`${stdout}\n${stderr}`)
|
|
156
|
+
) {
|
|
157
|
+
return { state: 'not-installed', root: null }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (exitCode === 0) {
|
|
161
|
+
const bundle = output?.bundle
|
|
162
|
+
if (
|
|
163
|
+
bundle !== null &&
|
|
164
|
+
typeof bundle === 'object' &&
|
|
165
|
+
!Array.isArray(bundle) &&
|
|
166
|
+
Object.prototype.hasOwnProperty.call(bundle, 'root')
|
|
167
|
+
) {
|
|
168
|
+
return { state: 'installed', root: (bundle as { root: unknown }).root }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { state: 'unknown', root: null }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function installedBundleRootHash(probe: InstalledDomainProbe): string | null {
|
|
175
|
+
return probe.state === 'installed' ? schemaHashOf(probe.root) : null
|
|
176
|
+
}
|
|
177
|
+
|
|
125
178
|
async function getInstalledDomain(
|
|
126
179
|
origin: string,
|
|
127
180
|
instance: string,
|
|
128
181
|
timeoutMs = 8000,
|
|
129
|
-
): Promise<
|
|
182
|
+
): Promise<InstalledDomainProbe> {
|
|
130
183
|
try {
|
|
131
|
-
const proc = Bun.spawn(
|
|
184
|
+
const proc = Bun.spawn(installedDomainCommand(origin, instance), {
|
|
132
185
|
stdout: 'pipe',
|
|
133
186
|
stderr: 'pipe',
|
|
134
187
|
})
|
|
@@ -144,30 +197,13 @@ async function getInstalledDomain(
|
|
|
144
197
|
new Response(proc.stdout).text(),
|
|
145
198
|
new Response(proc.stderr).text(),
|
|
146
199
|
])
|
|
147
|
-
await proc.exited
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
parsed = JSON.parse(out)
|
|
151
|
-
} catch {
|
|
152
|
-
/* non-JSON */
|
|
153
|
-
}
|
|
154
|
-
if (parsed?.error === 'NOT_FOUND' || /\bNOT_FOUND\b/.test(`${out}\n${err}`))
|
|
155
|
-
return { state: 'not-installed', schema: null }
|
|
156
|
-
if (parsed?.path && parsed?.props) {
|
|
157
|
-
let schema: unknown = null
|
|
158
|
-
try {
|
|
159
|
-
schema = parsed.props.schema ? JSON.parse(parsed.props.schema) : null
|
|
160
|
-
} catch {
|
|
161
|
-
/* schema absent/unparseable — still installed */
|
|
162
|
-
}
|
|
163
|
-
return { state: 'installed', schema }
|
|
164
|
-
}
|
|
165
|
-
return { state: 'unknown', schema: null }
|
|
200
|
+
const code = await proc.exited
|
|
201
|
+
return installedDomainProbeResult(out, err, code)
|
|
166
202
|
} finally {
|
|
167
203
|
clearTimeout(timer)
|
|
168
204
|
}
|
|
169
205
|
} catch {
|
|
170
|
-
return { state: 'unknown',
|
|
206
|
+
return { state: 'unknown', root: null }
|
|
171
207
|
}
|
|
172
208
|
}
|
|
173
209
|
|
|
@@ -187,7 +223,7 @@ export async function instanceStatus(
|
|
|
187
223
|
const probe = await getInstalledDomain(origin, deployTarget)
|
|
188
224
|
if (probe.state === 'installed') {
|
|
189
225
|
install = 'installed'
|
|
190
|
-
installedHash =
|
|
226
|
+
installedHash = installedBundleRootHash(probe)
|
|
191
227
|
drift =
|
|
192
228
|
installedHash && localHash
|
|
193
229
|
? installedHash === localHash
|
|
@@ -1,24 +1,224 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
RememberedViewTarget,
|
|
5
|
+
StudioSchemaBundle,
|
|
6
|
+
ViewInfo,
|
|
7
|
+
ViewTargetCandidate,
|
|
8
|
+
} from '../../shared/types'
|
|
4
9
|
|
|
5
10
|
import {
|
|
6
11
|
conciseCliFailure,
|
|
7
|
-
|
|
12
|
+
readyViewSession,
|
|
8
13
|
reconcileRememberedTarget,
|
|
9
14
|
targetDefinition,
|
|
10
15
|
targetFromRow,
|
|
16
|
+
viewDefinitionBindings,
|
|
17
|
+
viewSessionArgs,
|
|
11
18
|
} from './views'
|
|
12
19
|
|
|
13
|
-
describe('
|
|
14
|
-
test('
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
'
|
|
20
|
+
describe('view session command', () => {
|
|
21
|
+
test('opens the installed ViewPath without rejected placement overrides', () => {
|
|
22
|
+
const args = viewSessionArgs('issues.astrale.ai', 'issue-detail', 'staging', '@issue-1')
|
|
23
|
+
expect(args).toEqual([
|
|
24
|
+
'view',
|
|
25
|
+
'/:issues.astrale.ai:view.issue-detail',
|
|
26
|
+
'--target',
|
|
27
|
+
'@issue-1',
|
|
28
|
+
'--no-open',
|
|
29
|
+
'--json',
|
|
30
|
+
'-i',
|
|
31
|
+
'staging',
|
|
32
|
+
])
|
|
33
|
+
expect(args).not.toContain('--view-url')
|
|
34
|
+
expect(args).not.toContain('--handshake')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('omits --target for a standalone view', () => {
|
|
38
|
+
expect(viewSessionArgs('issues.astrale.ai', 'dashboard', 'local')).toEqual([
|
|
39
|
+
'view',
|
|
40
|
+
'/:issues.astrale.ai:view.dashboard',
|
|
41
|
+
'--no-open',
|
|
42
|
+
'--json',
|
|
43
|
+
'-i',
|
|
44
|
+
'local',
|
|
45
|
+
])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('preserves canonical interface View target coordinates', () => {
|
|
49
|
+
const view = { slug: 'users', kind: 'unknown' } satisfies ViewInfo
|
|
50
|
+
const bundle = {
|
|
51
|
+
ir: {
|
|
52
|
+
views: {
|
|
53
|
+
users: {
|
|
54
|
+
name: 'users',
|
|
55
|
+
auth: 'required',
|
|
56
|
+
target: {
|
|
57
|
+
kind: 'definition',
|
|
58
|
+
definitions: [{ origin: 'kernel.astrale.ai', kind: 'interface', name: 'Identity' }],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
} as unknown as StudioSchemaBundle
|
|
64
|
+
|
|
65
|
+
expect(viewDefinitionBindings('shell.astrale.ai', view, bundle)).toEqual([
|
|
66
|
+
{
|
|
67
|
+
className: 'Identity',
|
|
68
|
+
classOrigin: 'kernel.astrale.ai',
|
|
69
|
+
kind: 'interface',
|
|
70
|
+
},
|
|
71
|
+
])
|
|
72
|
+
expect(targetDefinition('Identity', 'kernel.astrale.ai', 'interface')).toBe(
|
|
73
|
+
'/:kernel.astrale.ai:interface.Identity',
|
|
20
74
|
)
|
|
21
75
|
})
|
|
76
|
+
|
|
77
|
+
test('preserves every exact canonical homonym and ignores short-name metadata', () => {
|
|
78
|
+
const view = {
|
|
79
|
+
slug: 'named',
|
|
80
|
+
kind: 'unknown',
|
|
81
|
+
viewFor: 'WrongLegacyTarget',
|
|
82
|
+
} satisfies ViewInfo
|
|
83
|
+
const bundle = {
|
|
84
|
+
ir: {
|
|
85
|
+
views: {
|
|
86
|
+
named: {
|
|
87
|
+
name: 'named',
|
|
88
|
+
auth: 'required',
|
|
89
|
+
target: {
|
|
90
|
+
kind: 'definition',
|
|
91
|
+
definitions: [
|
|
92
|
+
{ origin: 'directory.example.dev', kind: 'interface', name: 'Named' },
|
|
93
|
+
{ origin: 'people.example.dev', kind: 'interface', name: 'Named' },
|
|
94
|
+
{ origin: 'catalog.example.dev', kind: 'class', name: 'Named' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
} as unknown as StudioSchemaBundle
|
|
101
|
+
|
|
102
|
+
expect(viewDefinitionBindings('shell.astrale.ai', view, bundle)).toEqual([
|
|
103
|
+
{ className: 'Named', classOrigin: 'directory.example.dev', kind: 'interface' },
|
|
104
|
+
{ className: 'Named', classOrigin: 'people.example.dev', kind: 'interface' },
|
|
105
|
+
{ className: 'Named', classOrigin: 'catalog.example.dev', kind: 'class' },
|
|
106
|
+
])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('treats an exact Domain target as authoritative over legacy viewFor', () => {
|
|
110
|
+
const view = { slug: 'home', kind: 'unknown', viewFor: 'User' } satisfies ViewInfo
|
|
111
|
+
const bundle = {
|
|
112
|
+
ir: {
|
|
113
|
+
views: {
|
|
114
|
+
home: { name: 'home', auth: 'required', target: { kind: 'domain' } },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
} as unknown as StudioSchemaBundle
|
|
118
|
+
|
|
119
|
+
expect(viewDefinitionBindings('shell.astrale.ai', view, bundle)).toEqual([])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('uses every exact fallback candidate across origin and definition-kind collisions', () => {
|
|
123
|
+
const view = { slug: 'legacy-named', kind: 'unknown', viewFor: 'Named' } satisfies ViewInfo
|
|
124
|
+
const bundle = {
|
|
125
|
+
ir: {
|
|
126
|
+
domain: 'shell.astrale.ai',
|
|
127
|
+
views: {},
|
|
128
|
+
classes: {
|
|
129
|
+
Named: {
|
|
130
|
+
origin: 'shell.astrale.ai',
|
|
131
|
+
ref: { origin: 'shell.astrale.ai', kind: 'class', name: 'Named' },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
interfaces: {
|
|
135
|
+
Named: {
|
|
136
|
+
origin: 'shell.astrale.ai',
|
|
137
|
+
ref: { origin: 'shell.astrale.ai', kind: 'interface', name: 'Named' },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
imports: {
|
|
141
|
+
Named: { origin: 'legacy.invalid', definition: 'class' },
|
|
142
|
+
},
|
|
143
|
+
importsByKey: {
|
|
144
|
+
'directory.example.dev:interface.Named': {
|
|
145
|
+
origin: 'directory.example.dev',
|
|
146
|
+
definition: 'interface',
|
|
147
|
+
},
|
|
148
|
+
'catalog.example.dev:class.Named': {
|
|
149
|
+
origin: 'catalog.example.dev',
|
|
150
|
+
definition: 'class',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
} as unknown as StudioSchemaBundle
|
|
155
|
+
|
|
156
|
+
expect(viewDefinitionBindings('shell.astrale.ai', view, bundle)).toEqual([
|
|
157
|
+
{ className: 'Named', classOrigin: 'shell.astrale.ai', kind: 'class' },
|
|
158
|
+
{ className: 'Named', classOrigin: 'shell.astrale.ai', kind: 'interface' },
|
|
159
|
+
{ className: 'Named', classOrigin: 'directory.example.dev', kind: 'interface' },
|
|
160
|
+
{ className: 'Named', classOrigin: 'catalog.example.dev', kind: 'class' },
|
|
161
|
+
])
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('uses the short-name import only for legacy bundles without an exact index', () => {
|
|
165
|
+
const view = { slug: 'legacy-users', kind: 'unknown', viewFor: 'User' } satisfies ViewInfo
|
|
166
|
+
const legacy = {
|
|
167
|
+
ir: {
|
|
168
|
+
domain: 'shell.astrale.ai',
|
|
169
|
+
views: {},
|
|
170
|
+
classes: {},
|
|
171
|
+
interfaces: {},
|
|
172
|
+
imports: {
|
|
173
|
+
User: { origin: 'accounts.example.dev', definition: 'interface' },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
} as unknown as StudioSchemaBundle
|
|
177
|
+
const exact = {
|
|
178
|
+
ir: {
|
|
179
|
+
...legacy.ir,
|
|
180
|
+
importsByKey: {},
|
|
181
|
+
},
|
|
182
|
+
} as unknown as StudioSchemaBundle
|
|
183
|
+
|
|
184
|
+
expect(viewDefinitionBindings('shell.astrale.ai', view, legacy)).toEqual([
|
|
185
|
+
{ className: 'User', classOrigin: 'accounts.example.dev', kind: 'interface' },
|
|
186
|
+
])
|
|
187
|
+
expect(viewDefinitionBindings('shell.astrale.ai', view, exact)).toEqual([])
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('reads the verified placement URL from the current CLI session route', () => {
|
|
191
|
+
expect(
|
|
192
|
+
readyViewSession(
|
|
193
|
+
{
|
|
194
|
+
session: {
|
|
195
|
+
id: 'v-a1b2',
|
|
196
|
+
pageUrl: 'http://127.0.0.1:4419/s/nonce/',
|
|
197
|
+
view: { route: { href: 'https://issues.example/ui/issue' } },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
null,
|
|
201
|
+
),
|
|
202
|
+
).toEqual({
|
|
203
|
+
status: 'ready',
|
|
204
|
+
sessionId: 'v-a1b2',
|
|
205
|
+
pageUrl: 'http://127.0.0.1:4419/s/nonce/',
|
|
206
|
+
viewUrl: 'https://issues.example/ui/issue',
|
|
207
|
+
target: null,
|
|
208
|
+
})
|
|
209
|
+
expect(
|
|
210
|
+
readyViewSession(
|
|
211
|
+
{
|
|
212
|
+
session: {
|
|
213
|
+
id: 'v-old',
|
|
214
|
+
pageUrl: 'http://127.0.0.1:4419/s/old/',
|
|
215
|
+
view: { url: 'https://legacy.invalid/view' },
|
|
216
|
+
},
|
|
217
|
+
} as never,
|
|
218
|
+
null,
|
|
219
|
+
),
|
|
220
|
+
).toBeNull()
|
|
221
|
+
})
|
|
22
222
|
})
|
|
23
223
|
|
|
24
224
|
test('reduces CLI stack output to the actionable kernel failure', () => {
|