@astrale-os/cli 0.4.0-alpha.13 → 0.6.0-alpha.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/LICENSE +202 -0
- package/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +27 -0
- package/dist/astrale.js +24396 -9632
- package/package.json +24 -22
- package/src/command.ts +2 -0
- package/src/commands/__tests__/admin-instance.test.ts +3 -2
- package/src/commands/__tests__/domain-list.test.ts +6 -2
- package/src/commands/__tests__/help-contract.test.ts +27 -14
- package/src/commands/__tests__/install-identity-override.test.ts +2 -2
- package/src/commands/__tests__/ls.test.ts +1 -1
- package/src/commands/__tests__/read-commands.test.ts +201 -0
- package/src/commands/__tests__/view.test.ts +100 -0
- package/src/commands/call.ts +27 -44
- package/src/commands/describe.ts +57 -58
- package/src/commands/domain/install.ts +9 -9
- package/src/commands/domain/list.ts +2 -2
- package/src/commands/domain/publish.ts +3 -3
- package/src/commands/get.ts +48 -23
- package/src/commands/identity/register.ts +27 -33
- package/src/commands/instance/active.ts +2 -2
- package/src/commands/instance/create.ts +1 -1
- package/src/commands/instance/delete.ts +3 -3
- package/src/commands/instance/list.ts +8 -3
- package/src/commands/instance/status.ts +4 -3
- package/src/commands/instance/use.ts +2 -2
- package/src/commands/logs.ts +8 -8
- package/src/commands/ls.ts +77 -55
- package/src/commands/mutate.ts +191 -0
- package/src/commands/query.ts +307 -20
- package/src/commands/session/analyze.ts +50 -0
- package/src/commands/session/list.ts +36 -0
- package/src/commands/token.ts +4 -7
- package/src/commands/view-serve.ts +26 -0
- package/src/commands/view.ts +614 -0
- package/src/connect-core.test.ts +42 -0
- package/src/connect-core.ts +53 -0
- package/src/kernel/__tests__/auth.test.ts +1 -0
- package/src/kernel/__tests__/expand.test.ts +123 -0
- package/src/kernel/client.ts +33 -37
- package/src/kernel/expand.ts +55 -61
- package/src/kernel/graph.ts +96 -0
- package/src/kernel/index.ts +18 -3
- package/src/kernel/options.ts +1 -1
- package/src/kernel/run.ts +0 -13
- package/src/lib/__tests__/instance-target.test.ts +34 -0
- package/src/lib/__tests__/table.test.ts +1 -1
- package/src/lib/__tests__/view-open-intent.test.ts +308 -0
- package/src/lib/__tests__/view-snapshot.test.ts +77 -0
- package/src/lib/admin-domain.ts +8 -4
- package/src/lib/admin-instance.ts +5 -1
- package/src/lib/config.ts +1 -0
- package/src/lib/domain-identity.ts +1 -1
- package/src/lib/instance-target.ts +5 -0
- package/src/lib/instance.ts +11 -0
- package/src/lib/log.ts +12 -2
- package/src/lib/login-flow.ts +41 -4
- package/src/lib/provision-instance.ts +2 -2
- package/src/lib/self.ts +1 -3
- package/src/lib/view/open-intent.ts +97 -0
- package/src/lib/view/resolve.ts +104 -0
- package/src/lib/view/server.ts +294 -0
- package/src/lib/view/session.ts +123 -0
- package/src/lib/view/snapshot.ts +101 -0
- package/src/program.ts +16 -3
- package/src/registry.ts +1 -1
- package/src/setup/render.ts +1 -3
- package/src/setup/steps/instance.ts +2 -2
- package/src/telemetry/__tests__/gate.test.ts +63 -0
- package/src/telemetry/__tests__/recorder.test.ts +110 -0
- package/src/telemetry/__tests__/redact.test.ts +79 -0
- package/src/telemetry/__tests__/session.test.ts +166 -0
- package/src/telemetry/__tests__/trigger.test.ts +49 -0
- package/src/telemetry/adapters/__tests__/claude-code.test.ts +89 -0
- package/src/telemetry/adapters/__tests__/codex.test.ts +138 -0
- package/src/telemetry/adapters/__tests__/index.test.ts +88 -0
- package/src/telemetry/adapters/claude-code.ts +90 -0
- package/src/telemetry/adapters/codex.ts +160 -0
- package/src/telemetry/adapters/index.ts +45 -0
- package/src/telemetry/adapters/types.ts +21 -0
- package/src/telemetry/analyze.ts +227 -0
- package/src/telemetry/gate.ts +79 -0
- package/src/telemetry/recorder.ts +57 -0
- package/src/telemetry/redact.ts +53 -0
- package/src/telemetry/session.ts +88 -0
- package/src/telemetry/settings.ts +26 -0
- package/src/telemetry/store.ts +91 -0
- package/src/telemetry/trigger.ts +119 -0
- package/src/telemetry/types.ts +64 -0
- package/studio/client/dist/assets/index-CyN5G8IA.js +109 -0
- package/studio/client/dist/assets/index-DKKMHBBC.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/server/agent/ask.ts +7 -2
- package/studio/server/agent/claude.ts +15 -3
- package/studio/server/agent/runner.ts +4 -1
- package/studio/server/agent/session-id.ts +13 -0
- package/studio/server/api.ts +50 -32
- package/studio/server/cache.ts +17 -6
- package/studio/server/client-package.test.ts +147 -0
- package/studio/server/client-package.ts +242 -0
- package/studio/server/index.ts +13 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -0
- package/studio/server/introspect/anatomy-extras.ts +48 -49
- package/studio/server/introspect/anatomy.ts +7 -7
- package/studio/server/introspect/overlay-tsmorph.test.ts +104 -0
- package/studio/server/introspect/overlay-tsmorph.ts +230 -72
- package/studio/server/state/harness-gateway.ts +12 -3
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/views.test.ts +90 -0
- package/studio/server/state/views.ts +396 -99
- package/studio/server/state/visibility.ts +10 -6
- package/studio/server/view-dev-server.test.ts +111 -0
- package/studio/server/view-dev-server.ts +372 -0
- package/studio/shared/types.ts +57 -10
- package/viewer/dist/index.html +93 -0
- package/viewer/dist/main.js +71 -0
- package/src/kernel/__tests__/remote-routing.test.ts +0 -70
- package/src/kernel/remote-routing.ts +0 -88
- package/studio/client/dist/assets/index-DOwzZAEK.css +0 -1
- package/studio/client/dist/assets/index-wtU0Zxhy.js +0 -183
- package/studio/tsconfig.json +0 -23
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-alpha.0",
|
|
4
4
|
"description": "Astrale CLI — connect to existing Astrale kernels",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astrale",
|
|
7
7
|
"cli"
|
|
8
8
|
],
|
|
9
|
-
"license": "
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
10
|
"author": "Astrale",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
@@ -23,33 +23,23 @@
|
|
|
23
23
|
"studio/server",
|
|
24
24
|
"studio/shared",
|
|
25
25
|
"studio/client/dist",
|
|
26
|
-
"studio/package.json"
|
|
26
|
+
"studio/package.json",
|
|
27
|
+
"viewer/dist",
|
|
28
|
+
"THIRD-PARTY-NOTICES.md"
|
|
27
29
|
],
|
|
28
30
|
"type": "module",
|
|
29
31
|
"exports": {
|
|
30
32
|
"./keys": "./src/lib/keys.ts",
|
|
31
|
-
"./paths": "./src/lib/paths.ts"
|
|
33
|
+
"./paths": "./src/lib/paths.ts",
|
|
34
|
+
"./connect-core": "./src/connect-core.ts"
|
|
32
35
|
},
|
|
33
36
|
"publishConfig": {
|
|
34
37
|
"access": "public",
|
|
35
38
|
"registry": "https://registry.npmjs.org"
|
|
36
39
|
},
|
|
37
|
-
"scripts": {
|
|
38
|
-
"preinstall": "node .check-workspace.cjs",
|
|
39
|
-
"prepare": "husky",
|
|
40
|
-
"build": "bun scripts/build.ts",
|
|
41
|
-
"prepack": "bun scripts/build.ts",
|
|
42
|
-
"typecheck": "tsgo --noEmit",
|
|
43
|
-
"lint": "oxlint .",
|
|
44
|
-
"lint:fix": "oxlint --fix .",
|
|
45
|
-
"format": "pnpm exec oxfmt --write .",
|
|
46
|
-
"format:check": "pnpm exec oxfmt --check .",
|
|
47
|
-
"test": "bun test src",
|
|
48
|
-
"test:watch": "bun test --watch src"
|
|
49
|
-
},
|
|
50
40
|
"dependencies": {
|
|
51
|
-
"@astrale-os/kernel-client": ">=0.
|
|
52
|
-
"@astrale-os/kernel-core": ">=0.
|
|
41
|
+
"@astrale-os/kernel-client": ">=0.3.1 <1.0.0",
|
|
42
|
+
"@astrale-os/kernel-core": ">=0.7.0 <1.0.0",
|
|
53
43
|
"@inquirer/prompts": "^8.5.2",
|
|
54
44
|
"chalk": "^5.6.2",
|
|
55
45
|
"chokidar": "^4.0.3",
|
|
@@ -62,7 +52,8 @@
|
|
|
62
52
|
},
|
|
63
53
|
"devDependencies": {
|
|
64
54
|
"@astrale-os/ox": ">=0.1.0 <1.0.0",
|
|
65
|
-
"@astrale/
|
|
55
|
+
"@astrale-os/shell": ">=0.3.5 <1.0.0",
|
|
56
|
+
"@astrale/commitlint-config": "npm:@jsr/astrale__commitlint-config@~2.0.1",
|
|
66
57
|
"@commitlint/cli": "~20.3.1",
|
|
67
58
|
"@commitlint/config-conventional": "~20.3.1",
|
|
68
59
|
"@types/bun": "^1.1.16",
|
|
@@ -70,6 +61,7 @@
|
|
|
70
61
|
"@typescript/native-preview": "latest",
|
|
71
62
|
"husky": "~9.1.7",
|
|
72
63
|
"lint-staged": "~16.2.7",
|
|
64
|
+
"msgpackr": "^1.12.1",
|
|
73
65
|
"oxfmt": "0.52.0",
|
|
74
66
|
"oxlint": "latest",
|
|
75
67
|
"typescript": "~6.0.1-rc"
|
|
@@ -86,5 +78,15 @@
|
|
|
86
78
|
"engines": {
|
|
87
79
|
"node": ">=20"
|
|
88
80
|
},
|
|
89
|
-
"
|
|
90
|
-
|
|
81
|
+
"scripts": {
|
|
82
|
+
"preinstall": "node .check-workspace.cjs",
|
|
83
|
+
"build": "bun scripts/build.ts",
|
|
84
|
+
"typecheck": "tsgo --noEmit && tsgo --noEmit -p viewer",
|
|
85
|
+
"lint": "oxlint .",
|
|
86
|
+
"lint:fix": "oxlint --fix .",
|
|
87
|
+
"format": "pnpm exec oxfmt --write .",
|
|
88
|
+
"format:check": "pnpm exec oxfmt --check .",
|
|
89
|
+
"test": "bun test src",
|
|
90
|
+
"test:watch": "bun test --watch src"
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/command.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type CommandDefinition = {
|
|
|
16
16
|
description: string
|
|
17
17
|
/** One-line summary shown in the parent's command list (Commander `.summary()`). */
|
|
18
18
|
summary?: string
|
|
19
|
+
/** Hide from help listings (internal entries, e.g. `__view-serve`). */
|
|
20
|
+
hidden?: boolean
|
|
19
21
|
aliases?: string[]
|
|
20
22
|
arguments?: CommandArgument[]
|
|
21
23
|
options?: CommandOption[]
|
|
@@ -3,12 +3,13 @@ import { describe, expect, test } from 'bun:test'
|
|
|
3
3
|
import type { IdentityStore } from '../../lib/identity'
|
|
4
4
|
|
|
5
5
|
import { AuthError } from '../../errors'
|
|
6
|
-
import { ADMIN_INSTANCE } from '../../lib/admin-instance'
|
|
6
|
+
import { ADMIN_INSTANCE, adminInstanceMethod } from '../../lib/admin-instance'
|
|
7
7
|
import { assertAlphaCreateIdentity } from '../instance/create'
|
|
8
8
|
|
|
9
9
|
describe('admin-backed instance commands', () => {
|
|
10
10
|
test('target the merged Instance class', () => {
|
|
11
|
-
expect(ADMIN_INSTANCE).toBe('
|
|
11
|
+
expect(ADMIN_INSTANCE).toBe('/:admin.astrale.ai:class.Instance')
|
|
12
|
+
expect(adminInstanceMethod('list')).toBe('/:admin.astrale.ai:class.Instance:list')
|
|
12
13
|
})
|
|
13
14
|
|
|
14
15
|
test('instance create preflight points fresh installs at WorkOS login', () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import type
|
|
4
|
-
|
|
3
|
+
import { ADMIN_DOMAIN, adminDomainMethod, type DomainInfo } from '../../lib/admin-domain'
|
|
5
4
|
import { byDefaultThenName, domainProjection, type DomainRow } from '../domain/list'
|
|
6
5
|
|
|
7
6
|
const strip = (s: string): string => s.replace(/\[[0-9;]*m/g, '')
|
|
@@ -18,6 +17,11 @@ function entry(over: Partial<DomainInfo>): DomainInfo {
|
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
describe('domain list — ordering', () => {
|
|
20
|
+
test('uses the semantic static method path', () => {
|
|
21
|
+
expect(ADMIN_DOMAIN).toBe('/:admin.astrale.ai:class.DomainEntry')
|
|
22
|
+
expect(adminDomainMethod('list')).toBe('/:admin.astrale.ai:class.DomainEntry:list')
|
|
23
|
+
})
|
|
24
|
+
|
|
21
25
|
test('install-by-default sorts first, then alphabetical by origin', () => {
|
|
22
26
|
const rows = [
|
|
23
27
|
entry({ origin: 'zeta.astrale.ai' }),
|
|
@@ -6,10 +6,7 @@ import { join } from 'node:path'
|
|
|
6
6
|
|
|
7
7
|
import { buildProgram } from '../../program'
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
// "never drifts". These tests hold that claim to account: the version is
|
|
11
|
-
// single-sourced from package.json, no internal `§` spec anchors leak into
|
|
12
|
-
// rendered help, and the workspace skill mirror stays byte-identical.
|
|
9
|
+
// Help output is the public CLI contract: version, spec anchors, and skill mirror stay in sync.
|
|
13
10
|
|
|
14
11
|
const cliRoot = join(import.meta.dir, '../../..')
|
|
15
12
|
|
|
@@ -28,9 +25,7 @@ describe('help contract — version is single-sourced', () => {
|
|
|
28
25
|
readFileSync(join(cliRoot, '.release-please-manifest.json'), 'utf8'),
|
|
29
26
|
) as Record<string, string>
|
|
30
27
|
|
|
31
|
-
//
|
|
32
|
-
// rendered version equals BOTH JSON sources catches a re-hardcoded literal
|
|
33
|
-
// and a manifest/package.json divergence (release-please bumps both).
|
|
28
|
+
// Program version must follow both package.json and release-please metadata.
|
|
34
29
|
expect(program.version()).toBe(pkg.version)
|
|
35
30
|
expect(manifest['.']).toBe(pkg.version)
|
|
36
31
|
})
|
|
@@ -92,13 +87,7 @@ describe('help contract — connect-only command surface', () => {
|
|
|
92
87
|
const program = await buildProgram()
|
|
93
88
|
const names = allCommands(program).map((command) => command.name())
|
|
94
89
|
|
|
95
|
-
//
|
|
96
|
-
// runtime management (the historical `astrale logs`, removed with
|
|
97
|
-
// start/stop; the historical `astrale domain`, local domain wiring) and each
|
|
98
|
-
// has since been RECLAIMED for a connect-side meaning through the admin
|
|
99
|
-
// control plane — `astrale logs <service>` tails a MANAGED service's buffer;
|
|
100
|
-
// `astrale domain publish` registers an installable domain in the admin's
|
|
101
|
-
// catalog and `astrale domain install <url>` mounts one on an instance.
|
|
90
|
+
// `logs` and `domain` have managed/admin meanings, so only retired local-runtime verbs are absent.
|
|
102
91
|
for (const removed of [
|
|
103
92
|
'init',
|
|
104
93
|
'start',
|
|
@@ -116,6 +105,30 @@ describe('help contract — connect-only command surface', () => {
|
|
|
116
105
|
})
|
|
117
106
|
})
|
|
118
107
|
|
|
108
|
+
describe('help contract — read command split', () => {
|
|
109
|
+
test('get is point-read only and query owns structured read flags', async () => {
|
|
110
|
+
const program = await buildProgram()
|
|
111
|
+
const get = allCommands(program).find((command) => command.name() === 'get')
|
|
112
|
+
const query = allCommands(program).find((command) => command.name() === 'query')
|
|
113
|
+
const getHelp = get?.helpInformation() ?? ''
|
|
114
|
+
const queryHelp = query?.helpInformation() ?? ''
|
|
115
|
+
|
|
116
|
+
expect(getHelp).toContain('Usage: astrale get [options] <path>')
|
|
117
|
+
expect(getHelp).toContain('-l, --long')
|
|
118
|
+
expect(getHelp).not.toContain('--depth')
|
|
119
|
+
expect(getHelp).not.toContain('--children')
|
|
120
|
+
expect(getHelp).not.toContain('--edges')
|
|
121
|
+
expect(getHelp).not.toContain('--graph')
|
|
122
|
+
|
|
123
|
+
expect(queryHelp).toContain('Usage: astrale query [options] [paths...]')
|
|
124
|
+
expect(queryHelp).toContain('--depth <n>')
|
|
125
|
+
expect(queryHelp).toContain('--children <json>')
|
|
126
|
+
expect(queryHelp).toContain('--edges <json>')
|
|
127
|
+
expect(queryHelp).toContain('--ast <json>')
|
|
128
|
+
expect(queryHelp).toContain('--cypher <query>')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
119
132
|
describe('help contract — skill is single-source, not duplicated', () => {
|
|
120
133
|
const canonical = join(cliRoot, 'skills/astrale-cli/SKILL.md')
|
|
121
134
|
// Workspace mirror lives in the superrepo, outside this submodule. Absent
|
|
@@ -11,7 +11,7 @@ describe('admin-path target classification', () => {
|
|
|
11
11
|
test('a bare origin installs by catalog origin (the unique registry key)', () => {
|
|
12
12
|
expect(domainRefFromTarget('crm.acme.dev')).toEqual({ origin: 'crm.acme.dev' })
|
|
13
13
|
// A host:port that is not a url is still an origin, not a url.
|
|
14
|
-
expect(domainRefFromTarget('
|
|
14
|
+
expect(domainRefFromTarget('example.astrale.ai')).toEqual({ origin: 'example.astrale.ai' })
|
|
15
15
|
})
|
|
16
16
|
})
|
|
17
17
|
|
|
@@ -23,7 +23,7 @@ describe('identity-override detection', () => {
|
|
|
23
23
|
|
|
24
24
|
test('origin differing from the serving host is an override', () => {
|
|
25
25
|
// The spec §5 attack shape: a fork on workers.dev claiming a well-known origin.
|
|
26
|
-
expect(isIdentityOverride('
|
|
26
|
+
expect(isIdentityOverride('shell.astrale.ai', 'crm.workers.dev')).toBe(true)
|
|
27
27
|
// The scaffold default also aliases until the placeholder origin is edited.
|
|
28
28
|
expect(isIdentityOverride('hldom.example.dev', 'hldom-example-dev.acme.workers.dev')).toBe(true)
|
|
29
29
|
})
|
|
@@ -7,7 +7,7 @@ import { basename, classNameOf } from '../ls'
|
|
|
7
7
|
// `path` (absolute) and `class` (serialized ClassPath).
|
|
8
8
|
describe('ls — display projection (slug-bug regression)', () => {
|
|
9
9
|
test('basename derives the display name from the absolute path', () => {
|
|
10
|
-
expect(basename('/
|
|
10
|
+
expect(basename('/example.astrale.ai')).toBe('example.astrale.ai')
|
|
11
11
|
expect(basename('/kernel.astrale.ai')).toBe('kernel.astrale.ai')
|
|
12
12
|
expect(basename('/')).toBe('/')
|
|
13
13
|
expect(basename(undefined)).toBe('')
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
type Wire = { nodes: unknown[]; edges: unknown[]; roots: string[]; next?: Record<string, unknown> }
|
|
4
|
+
type RunOpts = {
|
|
5
|
+
opts: Record<string, unknown>
|
|
6
|
+
fn: (ctx: { client: { call: typeof clientCallMock } }) => Promise<unknown>
|
|
7
|
+
format?: (result: unknown, opts: Record<string, unknown>, isRaw: boolean) => void | Promise<void>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class ExitError extends Error {
|
|
11
|
+
constructor(readonly code: string | number | null | undefined) {
|
|
12
|
+
super('process.exit(' + String(code) + ')')
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const emptyWire = (): Wire => ({ nodes: [], edges: [], roots: [] })
|
|
17
|
+
|
|
18
|
+
let stdout = ''
|
|
19
|
+
let errors: string[] = []
|
|
20
|
+
let originalStdoutWrite: typeof process.stdout.write
|
|
21
|
+
let originalConsoleError: typeof console.error
|
|
22
|
+
let originalExit: typeof process.exit
|
|
23
|
+
let queryAsts: unknown[] = []
|
|
24
|
+
let getPaths: string[] = []
|
|
25
|
+
let queryResult: { wire: Wire } = { wire: emptyWire() }
|
|
26
|
+
let getResult: unknown = null
|
|
27
|
+
let clientCallResult: unknown = { rows: [] }
|
|
28
|
+
|
|
29
|
+
const clientCallMock = mock(async () => clientCallResult)
|
|
30
|
+
const queryMock = mock(async (ast: unknown) => {
|
|
31
|
+
queryAsts.push(ast)
|
|
32
|
+
return queryResult
|
|
33
|
+
})
|
|
34
|
+
const getMock = mock(async (path: string) => {
|
|
35
|
+
getPaths.push(path)
|
|
36
|
+
return getResult
|
|
37
|
+
})
|
|
38
|
+
const bindGraphMock = mock(() => ({ query: queryMock, get: getMock }))
|
|
39
|
+
const expandSelfInPathMock = mock(async (path: string) => {
|
|
40
|
+
if (path === '@self') {
|
|
41
|
+
return {
|
|
42
|
+
path: '@expanded-self',
|
|
43
|
+
meta: { original: '@self', expanded: '@expanded-self', selfId: 'expanded-self' },
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { path, meta: undefined }
|
|
47
|
+
})
|
|
48
|
+
const withSelfHintMock = mock(async (fn: () => Promise<unknown>) => fn())
|
|
49
|
+
const runKernelCommandMock = mock(async (run: RunOpts) => {
|
|
50
|
+
const result = await run.fn({ client: { call: clientCallMock } })
|
|
51
|
+
if (run.format) await run.format(result, run.opts, true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
mock.module('../../kernel', () => ({
|
|
55
|
+
bindGraph: bindGraphMock,
|
|
56
|
+
expandSelfInPath: expandSelfInPathMock,
|
|
57
|
+
runKernelCommand: runKernelCommandMock,
|
|
58
|
+
withSelfHint: withSelfHintMock,
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
stdout = ''
|
|
63
|
+
errors = []
|
|
64
|
+
queryAsts = []
|
|
65
|
+
getPaths = []
|
|
66
|
+
queryResult = { wire: emptyWire() }
|
|
67
|
+
getResult = null
|
|
68
|
+
clientCallResult = { rows: [] }
|
|
69
|
+
clientCallMock.mockClear()
|
|
70
|
+
queryMock.mockClear()
|
|
71
|
+
getMock.mockClear()
|
|
72
|
+
bindGraphMock.mockClear()
|
|
73
|
+
expandSelfInPathMock.mockClear()
|
|
74
|
+
withSelfHintMock.mockClear()
|
|
75
|
+
runKernelCommandMock.mockClear()
|
|
76
|
+
|
|
77
|
+
originalStdoutWrite = process.stdout.write.bind(process.stdout)
|
|
78
|
+
originalConsoleError = console.error
|
|
79
|
+
originalExit = process.exit
|
|
80
|
+
process.stdout.write = ((chunk: string | Uint8Array) => {
|
|
81
|
+
stdout += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')
|
|
82
|
+
return true
|
|
83
|
+
}) as typeof process.stdout.write
|
|
84
|
+
console.error = ((...args: unknown[]) => {
|
|
85
|
+
errors.push(args.map(String).join(' '))
|
|
86
|
+
}) as typeof console.error
|
|
87
|
+
process.exit = ((code?: string | number | null) => {
|
|
88
|
+
throw new ExitError(code)
|
|
89
|
+
}) as typeof process.exit
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
process.stdout.write = originalStdoutWrite
|
|
94
|
+
console.error = originalConsoleError
|
|
95
|
+
process.exit = originalExit
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('query command', () => {
|
|
99
|
+
test('lowers roots and selectors to the expected QueryASTInput', async () => {
|
|
100
|
+
const { queryCommand } = await import('../query')
|
|
101
|
+
|
|
102
|
+
await queryCommand(['/root', '@self'], {
|
|
103
|
+
json: true,
|
|
104
|
+
depth: '2',
|
|
105
|
+
children:
|
|
106
|
+
'{"classes":["/:kernel.astrale.ai:class.Folder"],"limit":50,"cursor":"child-cursor","order":{"by":"id","dir":"asc"}}',
|
|
107
|
+
edges:
|
|
108
|
+
'[{"as":"perm","classes":["/:kernel.astrale.ai:class.has_perm"],"direction":"out","limit":10,"cursor":"edge-cursor","order":{"by":"id","dir":"desc"}},{"direction":"both"}]',
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
expect(queryAsts).toEqual([
|
|
112
|
+
{
|
|
113
|
+
version: 1,
|
|
114
|
+
from: ['/root', '@expanded-self'],
|
|
115
|
+
steps: [
|
|
116
|
+
{
|
|
117
|
+
expand: {
|
|
118
|
+
edge: 'has_parent',
|
|
119
|
+
dir: 'in',
|
|
120
|
+
depth: 2,
|
|
121
|
+
filter: { class: ['/:kernel.astrale.ai:class.Folder'] },
|
|
122
|
+
page: { limit: 50, cursor: 'child-cursor' },
|
|
123
|
+
order: { by: 'id', dir: 'asc' },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
expand: {
|
|
128
|
+
edge: ['/:kernel.astrale.ai:class.has_perm'],
|
|
129
|
+
dir: 'out',
|
|
130
|
+
as: 'perm',
|
|
131
|
+
page: { limit: 10, cursor: 'edge-cursor' },
|
|
132
|
+
order: { by: 'id', dir: 'desc' },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{ expand: { dir: 'both', as: 'e1' } },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
])
|
|
139
|
+
expect(expandSelfInPathMock.mock.calls.map((call) => call[0])).toEqual(['/root', '@self'])
|
|
140
|
+
expect(withSelfHintMock).toHaveBeenCalledTimes(1)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('rejects an invalid children selector before dispatch', async () => {
|
|
144
|
+
const { queryCommand } = await import('../query')
|
|
145
|
+
|
|
146
|
+
await expect(queryCommand(['/'], { json: true, children: '{"bogus":true}' })).rejects.toEqual(
|
|
147
|
+
new ExitError(1),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
expect(errors.join('\n')).toContain('--children invalid selector')
|
|
151
|
+
expect(queryMock).not.toHaveBeenCalled()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('rejects mixing --ast with positional roots', async () => {
|
|
155
|
+
const { queryCommand } = await import('../query')
|
|
156
|
+
|
|
157
|
+
await expect(
|
|
158
|
+
queryCommand(['/'], { json: true, ast: '{"version":1,"from":["/"]}' }),
|
|
159
|
+
).rejects.toEqual(new ExitError(1))
|
|
160
|
+
|
|
161
|
+
expect(errors.join('\n')).toContain(
|
|
162
|
+
'--ast cannot be used with positional roots or --depth/--children/--edges',
|
|
163
|
+
)
|
|
164
|
+
expect(queryMock).not.toHaveBeenCalled()
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('get command', () => {
|
|
169
|
+
test('dispatches a point read and prints the flat wire projection', async () => {
|
|
170
|
+
const { getCommand } = await import('../get')
|
|
171
|
+
const row = {
|
|
172
|
+
id: 'n1',
|
|
173
|
+
class: '/:demo:class.Widget',
|
|
174
|
+
path: '/demo/widget',
|
|
175
|
+
props: { name: 'Widget' },
|
|
176
|
+
__labels: ['Node', 'Widget'],
|
|
177
|
+
classId: 'class-1',
|
|
178
|
+
}
|
|
179
|
+
getResult = row
|
|
180
|
+
|
|
181
|
+
await getCommand('/demo/widget', { json: true })
|
|
182
|
+
|
|
183
|
+
expect(getPaths).toEqual(['/demo/widget'])
|
|
184
|
+
expect(JSON.parse(stdout)).toEqual({
|
|
185
|
+
id: 'n1',
|
|
186
|
+
class: '/:demo:class.Widget',
|
|
187
|
+
path: '/demo/widget',
|
|
188
|
+
props: { name: 'Widget' },
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('errors with exit 1 when no node resolves', async () => {
|
|
193
|
+
const { getCommand } = await import('../get')
|
|
194
|
+
getResult = null
|
|
195
|
+
|
|
196
|
+
await expect(getCommand('/missing', { json: true })).rejects.toEqual(new ExitError(1))
|
|
197
|
+
|
|
198
|
+
expect(getPaths).toEqual(['/missing'])
|
|
199
|
+
expect(errors.join('\n')).toContain('node "/missing" not found or not visible')
|
|
200
|
+
})
|
|
201
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
const getMock = mock(async (_path: string) => ({ id: 'issue-id' }))
|
|
4
|
+
const clientCallMock = mock(async (): Promise<unknown> => {
|
|
5
|
+
throw new Error('View:resolve must not run for a bare --view-url')
|
|
6
|
+
})
|
|
7
|
+
const abMock = mock(async (_args: string[]) => ({
|
|
8
|
+
ok: true,
|
|
9
|
+
data: { snapshot: '- button "Ready" [ref=e1]' },
|
|
10
|
+
error: null,
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
mock.module('../../kernel', () => ({
|
|
14
|
+
bindGraph: () => ({ get: getMock }),
|
|
15
|
+
expandSelfInPath: async (path: string) => ({ path }),
|
|
16
|
+
resolveKernelTarget: mock(),
|
|
17
|
+
withKernelClient: async (
|
|
18
|
+
_opts: unknown,
|
|
19
|
+
run: (ctx: { client: { call: typeof clientCallMock } }) => Promise<unknown>,
|
|
20
|
+
) => run({ client: { call: clientCallMock } }),
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
mock.module('../../lib/browser', () => ({
|
|
24
|
+
ab: abMock,
|
|
25
|
+
AGENT_BROWSER_REPO: 'vercel-labs/agent-browser',
|
|
26
|
+
BROWSER_DIR: '/tmp/astrale-browser',
|
|
27
|
+
findAgentBrowser: mock(async () => '/usr/bin/agent-browser'),
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
getMock.mockClear()
|
|
32
|
+
clientCallMock.mockClear()
|
|
33
|
+
abMock.mockClear()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('view session resolution', () => {
|
|
37
|
+
test('uses a bare --view-url target only as shell context', async () => {
|
|
38
|
+
const { resolveSession } = await import('../view')
|
|
39
|
+
const targetPath = '/users/user-1/issue-1'
|
|
40
|
+
|
|
41
|
+
const result = await resolveSession(undefined, {
|
|
42
|
+
viewUrl: 'http://127.0.0.1:5173/ui/issues',
|
|
43
|
+
handshake: 'shell',
|
|
44
|
+
target: targetPath,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(getMock).toHaveBeenCalledWith(targetPath)
|
|
48
|
+
expect(clientCallMock).not.toHaveBeenCalled()
|
|
49
|
+
expect(result).toEqual({
|
|
50
|
+
view: {
|
|
51
|
+
url: 'http://127.0.0.1:5173/ui/issues',
|
|
52
|
+
functionId: 'dev-view',
|
|
53
|
+
handshake: 'shell',
|
|
54
|
+
name: 'dev',
|
|
55
|
+
},
|
|
56
|
+
target: { id: 'issue-id', path: targetPath },
|
|
57
|
+
candidates: [],
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('lists every candidate without requiring a view selection', async () => {
|
|
62
|
+
const { resolveSession } = await import('../view')
|
|
63
|
+
const targetPath = '/domains/ai-gateway.astrale.ai/core/gemini-3-5-flash'
|
|
64
|
+
const candidates = [
|
|
65
|
+
{
|
|
66
|
+
id: 'chat-id',
|
|
67
|
+
path: '/domains/ai-gateway.astrale.ai/views/chat',
|
|
68
|
+
url: 'https://ai-gateway.astrale.ai/ui/chat',
|
|
69
|
+
origin: 'class' as const,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'model-id',
|
|
73
|
+
path: '/domains/ai-gateway.astrale.ai/views/model',
|
|
74
|
+
url: 'https://ai-gateway.astrale.ai/ui/model',
|
|
75
|
+
origin: 'class' as const,
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
clientCallMock.mockImplementationOnce(async () => candidates)
|
|
79
|
+
|
|
80
|
+
const result = await resolveSession(targetPath, { list: true })
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
target: { id: 'issue-id', path: targetPath },
|
|
84
|
+
candidates,
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('view capture timing', () => {
|
|
90
|
+
test('settles the view before taking a screenshot', async () => {
|
|
91
|
+
const { runSnapshotExtras } = await import('../view')
|
|
92
|
+
|
|
93
|
+
await runSnapshotExtras({ screenshot: '/tmp/view.png' })
|
|
94
|
+
|
|
95
|
+
const commands = abMock.mock.calls.map(([args]) => args[0])
|
|
96
|
+
expect(commands.at(-1)).toBe('screenshot')
|
|
97
|
+
expect(commands.slice(0, -1).every((command) => command === 'snapshot')).toBe(true)
|
|
98
|
+
expect(commands.length).toBeGreaterThan(2)
|
|
99
|
+
})
|
|
100
|
+
})
|