@astrale-os/cli 1.0.0-beta.3 → 1.0.0-beta.30
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 +80 -6
- package/dist/astrale.js +55471 -58674
- package/dist/public/connect-core.js +2158 -30706
- package/dist/public/keys/index.js +1016 -29541
- package/dist/public/paths/index.js +370 -28654
- package/dist/types/admin/contract.d.ts +26 -0
- package/dist/types/admin/instance/client.d.ts +20 -0
- package/dist/types/admin/instance/index.d.ts +2 -0
- package/dist/types/admin/instance/model.d.ts +6 -10
- package/dist/types/connection/auth.d.ts +3 -0
- package/dist/types/connection/call.d.ts +3 -0
- package/dist/types/connection/command.d.ts +27 -0
- package/dist/types/connection/credential.d.ts +14 -0
- package/dist/types/connection/errors.d.ts +1 -0
- package/dist/types/connection/exchange.d.ts +9 -0
- package/dist/types/connection/failure/classify.d.ts +2 -0
- package/dist/types/connection/failure/index.d.ts +5 -0
- package/dist/types/connection/failure/model.d.ts +25 -0
- package/dist/types/connection/failure/render.d.ts +3 -0
- package/dist/types/connection/index.d.ts +8 -0
- package/dist/types/connection/reasons.d.ts +37 -0
- package/dist/types/connection/self.d.ts +37 -0
- package/dist/types/connection/session.d.ts +31 -0
- package/dist/types/connection/target.d.ts +38 -0
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/lib/admin-instance.d.ts +17 -0
- package/dist/types/lib/admin-target.d.ts +11 -36
- package/dist/types/lib/config.d.ts +13 -59
- package/dist/types/lib/failure-debug.d.ts +1 -0
- package/dist/types/lib/idp.d.ts +25 -173
- package/dist/types/lib/instance-target.d.ts +1 -1
- package/dist/types/lib/instance.d.ts +30 -88
- package/dist/types/lib/log.d.ts +5 -3
- package/dist/types/lib/output.d.ts +1 -1
- package/dist/types/lib/proc.d.ts +43 -0
- package/dist/types/lib/self.d.ts +9 -0
- package/dist/types/lib/update.d.ts +109 -0
- package/dist/types/lib/validation.d.ts +4 -1
- package/dist/types/state/exchange-credentials.d.ts +6 -2
- package/dist/types/state/files.d.ts +2 -0
- package/dist/types/state/index.d.ts +3 -2
- package/dist/types/state/paths.d.ts +2 -0
- package/dist/types/state/session-routes.d.ts +10 -0
- package/package.json +16 -19
- package/src/__tests__/fixtures/publication.ts +20 -0
- package/src/admin/.spec/architecture.md +12 -5
- package/src/admin/__tests__/fixture.ts +25 -0
- package/src/admin/catalog/.spec/api.d.ts +1 -3
- package/src/admin/catalog/.spec/architecture.md +5 -4
- package/src/admin/catalog/__tests__/client.test.ts +143 -69
- package/src/admin/catalog/client.ts +40 -54
- package/src/admin/catalog/model.ts +3 -4
- package/src/admin/contract.ts +47 -0
- package/src/admin/graph/.spec/api.d.ts +4 -10
- package/src/admin/graph/nodes.ts +1 -1
- package/src/admin/instance/.spec/api.d.ts +5 -9
- package/src/admin/instance/.spec/architecture.md +7 -7
- package/src/admin/instance/__tests__/client.test.ts +157 -175
- package/src/admin/instance/client.ts +39 -128
- package/src/admin/instance/index.ts +1 -2
- package/src/admin/instance/model.ts +12 -18
- package/src/commands/__tests__/admin-instance.test.ts +11 -8
- package/src/commands/__tests__/call-describe.test.ts +46 -33
- package/src/commands/__tests__/call.test.ts +34 -0
- package/src/commands/__tests__/domain-install-operation.test.ts +30 -22
- package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
- package/src/commands/__tests__/domain-list.test.ts +5 -25
- package/src/commands/__tests__/install-direct.test.ts +115 -8
- package/src/commands/__tests__/install-identity-override.test.ts +6 -17
- package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
- package/src/commands/__tests__/instance-status.test.ts +139 -0
- package/src/commands/__tests__/introspect-parse.test.ts +80 -1
- package/src/commands/__tests__/logs.test.ts +250 -2
- package/src/commands/__tests__/read-commands.test.ts +118 -10
- package/src/commands/__tests__/token-ttl.test.ts +49 -4
- package/src/commands/__tests__/update.test.ts +25 -3
- package/src/commands/__tests__/view.test.ts +50 -2
- package/src/commands/auth/logout.ts +2 -1
- package/src/commands/browser.ts +29 -0
- package/src/commands/call-describe.ts +60 -68
- package/src/commands/call.ts +71 -59
- package/src/commands/domain/install.ts +73 -46
- package/src/commands/domain/list.ts +1 -1
- package/src/commands/domain/publish.ts +14 -11
- package/src/commands/domain/uninstall.ts +2 -2
- package/src/commands/get.ts +15 -16
- package/src/commands/identity/__tests__/register.test.ts +3 -3
- package/src/commands/identity/register.ts +8 -8
- package/src/commands/instance/create.ts +8 -16
- package/src/commands/instance/delete.ts +12 -9
- package/src/commands/instance/list.ts +2 -2
- package/src/commands/instance/status.ts +66 -20
- package/src/commands/instance/use.ts +16 -12
- package/src/commands/introspect.ts +53 -41
- package/src/commands/logs.ts +123 -36
- package/src/commands/mutate.ts +6 -5
- package/src/commands/query.ts +35 -32
- package/src/commands/session/analyze.ts +26 -4
- package/src/commands/token.ts +42 -27
- package/src/commands/ui/__tests__/commands.test.ts +152 -0
- package/src/commands/ui/add.ts +51 -0
- package/src/commands/ui/doctor.ts +21 -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/commands/update.ts +89 -48
- package/src/commands/view.ts +82 -37
- package/src/connection/.spec/api.d.ts +7 -8
- package/src/connection/.spec/architecture.md +14 -5
- package/src/connection/.spec/flows/session.ts +2 -2
- package/src/connection/.spec/laws/connection.ts +6 -2
- package/src/connection/.spec/layout.ts +1 -0
- package/src/connection/__tests__/auth.test.ts +1 -0
- package/src/connection/__tests__/credential.test.ts +21 -1
- package/src/connection/__tests__/errors.test.ts +27 -20
- package/src/connection/__tests__/exchange.test.ts +64 -11
- package/src/connection/__tests__/failure-fixtures.ts +39 -0
- package/src/connection/__tests__/failure-safety.test.ts +75 -0
- package/src/connection/__tests__/self.test.ts +31 -11
- package/src/connection/__tests__/session.test.ts +8 -4
- package/src/connection/__tests__/target.test.ts +2 -0
- package/src/connection/auth.ts +13 -0
- package/src/connection/call.ts +1 -1
- package/src/connection/credential.ts +2 -2
- package/src/connection/errors.ts +1 -355
- package/src/connection/exchange.ts +110 -61
- package/src/connection/failure/classify.ts +110 -0
- package/src/connection/failure/index.ts +18 -0
- package/src/connection/failure/model.ts +28 -0
- package/src/connection/failure/render.ts +116 -0
- package/src/connection/reasons.ts +16 -0
- package/src/connection/self.ts +39 -21
- package/src/connection/session.ts +27 -9
- package/src/errors.ts +3 -2
- package/src/graph/.spec/api.d.ts +12 -5
- package/src/graph/.spec/layout.ts +9 -1
- package/src/graph/__tests__/mutation.test.ts +2 -2
- package/src/graph/__tests__/query.test.ts +25 -6
- package/src/graph/class.ts +30 -0
- package/src/graph/index.ts +1 -0
- package/src/graph/query.ts +21 -32
- package/src/identity/.spec/api.d.ts +2 -2
- package/src/identity/__tests__/fixtures/registry-journey.ts +13 -1
- package/src/identity/__tests__/registration.test.ts +1 -1
- package/src/identity/__tests__/registry.test.ts +4 -0
- package/src/identity/registration.ts +3 -3
- package/src/identity/registry.ts +2 -0
- package/src/keys/__tests__/keys.test.ts +4 -0
- package/src/keys/credential.ts +1 -0
- package/src/lib/__tests__/admin-target.test.ts +12 -0
- package/src/lib/__tests__/binary.test.ts +16 -1
- package/src/lib/__tests__/browser-retention.test.ts +212 -0
- package/src/lib/__tests__/command-dx.test.ts +1 -1
- package/src/lib/__tests__/config.test.ts +27 -0
- package/src/lib/__tests__/domain-publication.test.ts +10 -16
- package/src/lib/__tests__/idp.test.ts +34 -4
- package/src/lib/__tests__/instance-candidates.test.ts +1 -3
- package/src/lib/__tests__/instance-target.test.ts +7 -39
- package/src/lib/__tests__/log-errors.test.ts +64 -0
- package/src/lib/__tests__/skills.test.ts +809 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
- package/src/lib/__tests__/update.test.ts +264 -11
- package/src/lib/__tests__/view-assets.test.ts +25 -0
- package/src/lib/__tests__/view-external-open-intent.test.ts +150 -0
- package/src/lib/__tests__/view-external-open-origins.test.ts +27 -0
- package/src/lib/__tests__/view-open-intent.test.ts +0 -2
- package/src/lib/__tests__/view-server.test.ts +26 -3
- package/src/lib/__tests__/view-session.test.ts +2 -1
- package/src/lib/admin-instance.ts +14 -8
- package/src/lib/admin-target.ts +21 -2
- package/src/lib/binary.ts +22 -5
- package/src/lib/browser-retention.ts +210 -0
- package/src/lib/command-dx.ts +9 -9
- package/src/lib/config.ts +12 -1
- package/src/lib/domain-publication.ts +9 -7
- package/src/lib/failure-debug.ts +26 -0
- package/src/lib/idp.ts +34 -3
- package/src/lib/instance-candidates.ts +2 -2
- package/src/lib/instance-target.ts +3 -14
- package/src/lib/instance.ts +16 -11
- package/src/lib/log.ts +16 -9
- package/src/lib/output.ts +1 -1
- package/src/lib/proc.ts +7 -2
- package/src/lib/provision-instance.ts +34 -57
- package/src/lib/skills/lock.ts +117 -0
- package/src/lib/skills/sync.ts +814 -0
- package/src/lib/skills.ts +1 -52
- package/src/lib/update.ts +298 -45
- package/src/lib/validation.ts +2 -2
- package/src/lib/view/assets.ts +11 -2
- package/src/lib/view/external-open-intent.ts +41 -0
- package/src/lib/view/external-open-origins.ts +37 -0
- package/src/lib/view/host-capabilities.ts +17 -0
- package/src/lib/view/resolve.ts +1 -1
- package/src/lib/view/server.ts +18 -3
- package/src/lib/view/session.ts +2 -0
- package/src/program/.spec/api.d.ts +1 -0
- package/src/program/__tests__/program.test.ts +163 -6
- package/src/program/argv.ts +14 -0
- package/src/program/build.ts +26 -4
- package/src/program/command.ts +1 -0
- package/src/program/index.ts +1 -0
- package/src/program/registry.ts +2 -1
- package/src/setup/__tests__/instance-step.test.ts +27 -10
- package/src/setup/steps/instance.ts +22 -15
- package/src/setup/steps/skills.ts +40 -38
- package/src/state/.spec/api.d.ts +21 -2
- package/src/state/.spec/architecture.md +18 -4
- package/src/state/.spec/layout.ts +1 -0
- package/src/state/__tests__/exchange-credentials.test.ts +88 -42
- package/src/state/__tests__/files.test.ts +24 -1
- package/src/state/__tests__/fixtures/session-route-process.ts +93 -0
- package/src/state/__tests__/paths.test.ts +1 -0
- package/src/state/__tests__/session-routes.test.ts +138 -0
- package/src/state/exchange-credentials.ts +22 -9
- package/src/state/files.ts +41 -0
- package/src/state/index.ts +3 -1
- package/src/state/paths.ts +3 -0
- package/src/state/session-routes.ts +34 -0
- package/src/telemetry/__tests__/analyze-log.test.ts +38 -0
- package/src/telemetry/__tests__/recorder.test.ts +1 -0
- package/src/telemetry/__tests__/retention.test.ts +267 -0
- package/src/telemetry/__tests__/settings.test.ts +102 -0
- package/src/telemetry/__tests__/store-scan.test.ts +128 -0
- package/src/telemetry/__tests__/trigger.test.ts +33 -4
- package/src/telemetry/analyze.ts +24 -2
- package/src/telemetry/recorder.ts +15 -4
- package/src/telemetry/retention.ts +176 -0
- package/src/telemetry/session.ts +18 -12
- package/src/telemetry/settings.ts +64 -11
- package/src/telemetry/store.ts +92 -9
- package/src/telemetry/trigger.ts +16 -28
- package/src/ui/.spec/api.d.ts +14 -0
- package/src/ui/.spec/architecture.md +14 -0
- package/src/ui/.spec/laws.ts +50 -0
- package/src/ui/.spec/layout.ts +16 -0
- package/src/ui/__tests__/ui.test.ts +1742 -0
- package/src/ui/index.ts +13 -0
- package/src/ui/lock.ts +89 -0
- package/src/ui/model.ts +83 -0
- package/src/ui/operations.ts +1044 -0
- package/src/ui/project.ts +333 -0
- package/src/ui/release.ts +398 -0
- package/src/ui/runner.ts +18 -0
- package/studio/client/dist/assets/elk-api-DjmKsHXc.js +1 -0
- package/studio/client/dist/assets/index-BFVFs_8x.js +81 -0
- package/studio/client/dist/assets/index-DuB9iUdu.css +2 -0
- package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
- package/studio/client/dist/assets/schema-studio--a0kX3QU.css +1 -0
- package/studio/client/dist/assets/schema-studio-DH6oEWME.js +8 -0
- package/studio/client/dist/index.html +4 -3
- package/studio/package.json +8 -14
- package/studio/server/agent/ask.test.ts +1 -1
- package/studio/server/agent/bridge/grant.test.ts +1 -1
- package/studio/server/agent/bridge/routes.test.ts +1 -1
- package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
- package/studio/server/agent/harness/gateway/token.test.ts +1 -1
- package/studio/server/agent/harness/gateway/token.ts +3 -3
- package/studio/server/agent/prompts/anchors.test.ts +41 -32
- package/studio/server/agent/prompts/anchors.ts +23 -51
- package/studio/server/agent/prompts/system.test.ts +4 -4
- package/studio/server/agent/prompts/system.ts +5 -5
- package/studio/server/agent/routes.test.ts +1 -1
- package/studio/server/agent/run/coordinator.test.ts +1 -1
- package/studio/server/agent/run/preparation.ts +1 -1
- package/studio/server/api/canvas.ts +0 -1
- package/studio/server/api/context.ts +1 -1
- package/studio/server/api/deployment.ts +1 -1
- package/studio/server/api/views.ts +2 -2
- package/studio/server/api/workspace.ts +1 -1
- package/studio/server/api-agent-loadout.test.ts +1 -1
- package/studio/server/api.test.ts +1 -1
- package/studio/server/cache.test.ts +24 -18
- package/studio/server/cache.ts +10 -54
- package/studio/server/cli-consumers.test.ts +31 -3
- package/studio/server/domain.test.ts +50 -68
- package/studio/server/domain.ts +82 -63
- package/studio/server/handoff/copy.ts +2 -2
- package/studio/server/index.ts +1 -1
- package/studio/server/instances/probe.test.ts +1 -1
- package/studio/server/introspect/anatomy/views/routes.ts +46 -37
- package/studio/server/introspect/anatomy/views.ts +10 -16
- package/studio/server/introspect/anatomy-extras.test.ts +29 -56
- package/studio/server/introspect/anatomy.test.ts +22 -14
- package/studio/server/introspect/anatomy.ts +6 -9
- package/studio/server/introspect/bundle.ts +0 -3
- package/studio/server/introspect/canonical-schema.test.ts +155 -424
- package/studio/server/introspect/canonical-schema.ts +248 -601
- package/studio/server/introspect/core.ts +14 -22
- package/studio/server/introspect/diff.test.ts +21 -9
- package/studio/server/introspect/diff.ts +19 -64
- package/studio/server/introspect/extractor.ts +23 -122
- package/studio/server/introspect/overlay-tsmorph.test.ts +116 -241
- package/studio/server/introspect/overlay-tsmorph.ts +0 -1
- package/studio/server/introspect/overlay.ts +3 -51
- package/studio/server/introspect/revision.test.ts +24 -28
- package/studio/server/introspect/revision.ts +11 -22
- package/studio/server/introspect/runtime.test.ts +77 -195
- package/studio/server/introspect/runtime.ts +5 -67
- package/studio/server/introspect/schema-ir-json.test.ts +81 -0
- package/studio/server/introspect/schema-ir-json.ts +39 -69
- package/studio/server/introspect/schema-refs.test.ts +43 -88
- package/studio/server/introspect/schema-refs.ts +14 -49
- package/studio/server/introspect/source-overlay/handlers.ts +116 -636
- package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
- package/studio/server/introspect/source-overlay/spans.ts +15 -45
- package/studio/server/lifecycle.ts +4 -1
- package/studio/server/state/baseline.test.ts +14 -9
- package/studio/server/state/baseline.ts +3 -5
- package/studio/server/state/documents.test.ts +69 -0
- package/studio/server/state/documents.ts +57 -10
- package/studio/server/state/visibility.ts +1 -5
- package/studio/server/views/model.ts +3 -3
- package/studio/server/views/target.test.ts +18 -76
- package/studio/server/views/target.ts +24 -50
- package/studio/server/watch.test.ts +11 -11
- package/studio/server/watch.ts +8 -4
- package/studio/server/workspace/updates.ts +23 -1
- package/studio/server/workspace-watch.ts +2 -2
- package/studio/shared/contracts/runtime.ts +4 -0
- package/studio/shared/contracts/schema.ts +44 -131
- package/studio/shared/contracts/surface.test.ts +15 -17
- package/studio/shared/contracts/workspace.ts +5 -11
- package/studio/shared/schema/identity.test.ts +20 -44
- package/studio/shared/schema/identity.ts +9 -24
- package/studio/tsconfig.json +2 -1
- package/tsconfig.json +1 -1
- package/viewer/dist/index.html +21 -0
- package/viewer/dist/main.js +41 -62
- package/.check-workspace.cjs +0 -40
- package/src/admin/binding.ts +0 -168
- package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
- package/studio/client/dist/assets/elk-api-lwhFo7vB.js +0 -1
- package/studio/client/dist/assets/index-B-c-smo9.js +0 -8
- package/studio/client/dist/assets/index-BckHuAWk.js +0 -81
- package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
- package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
- package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
- package/studio/server/introspect/core-extractor.ts +0 -139
- package/studio/server/introspect/overlay.test.ts +0 -72
- package/studio/server/introspect/source-overlay/annotations.ts +0 -14
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
symlinkSync,
|
|
8
|
+
utimesSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs'
|
|
11
|
+
import { hostname, tmpdir } from 'node:os'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
import type { BrowserRetentionBudget } from '../browser-retention'
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_MAX_CACHE_BYTES,
|
|
18
|
+
DEFAULT_MAX_PROFILE_AGE_DAYS,
|
|
19
|
+
browserRetentionBudget,
|
|
20
|
+
heldByLiveBrowser,
|
|
21
|
+
profileCacheBytes,
|
|
22
|
+
sweepBrowserProfiles,
|
|
23
|
+
} from '../browser-retention'
|
|
24
|
+
|
|
25
|
+
const DAY = 24 * 60 * 60 * 1000
|
|
26
|
+
const BUDGET: BrowserRetentionBudget = { maxCacheBytes: 10_000, maxProfileAgeMs: 30 * DAY }
|
|
27
|
+
|
|
28
|
+
let dir: string
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
dir = mkdtempSync(join(tmpdir(), 'astrale-browser-ret-'))
|
|
32
|
+
delete process.env.ASTRALE_BROWSER_MAX_CACHE_BYTES
|
|
33
|
+
delete process.env.ASTRALE_BROWSER_MAX_PROFILE_AGE_DAYS
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
rmSync(dir, { recursive: true, force: true })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
type Seed = { idleMs?: number; cacheBytes?: number; lockPid?: number }
|
|
41
|
+
|
|
42
|
+
/** A profile shaped like Chromium's: session state at the root and in Default/,
|
|
43
|
+
* cache in the subdirectories retention is allowed to delete. */
|
|
44
|
+
function seedProfile(name: string, { idleMs = 0, cacheBytes = 0, lockPid }: Seed): string {
|
|
45
|
+
const profile = join(dir, name)
|
|
46
|
+
mkdirSync(join(profile, 'Default'), { recursive: true })
|
|
47
|
+
writeFileSync(join(profile, 'Local State'), '{"os_crypt":{"encrypted_key":"x"}}')
|
|
48
|
+
writeFileSync(join(profile, 'Default', 'Cookies'), 'SQLite format 3\0cookie-data')
|
|
49
|
+
writeFileSync(join(profile, 'Default', 'Preferences'), '{"profile":{}}')
|
|
50
|
+
mkdirSync(join(profile, 'Default', 'Local Storage', 'leveldb'), { recursive: true })
|
|
51
|
+
writeFileSync(join(profile, 'Default', 'Local Storage', 'leveldb', '000003.log'), 'state')
|
|
52
|
+
|
|
53
|
+
if (cacheBytes > 0) {
|
|
54
|
+
mkdirSync(join(profile, 'Default', 'Cache', 'Cache_Data'), { recursive: true })
|
|
55
|
+
writeFileSync(
|
|
56
|
+
join(profile, 'Default', 'Cache', 'Cache_Data', 'f_000001'),
|
|
57
|
+
'x'.repeat(cacheBytes),
|
|
58
|
+
)
|
|
59
|
+
mkdirSync(join(profile, 'Default', 'Code Cache', 'js'), { recursive: true })
|
|
60
|
+
writeFileSync(join(profile, 'Default', 'Code Cache', 'js', 'index'), 'y'.repeat(cacheBytes))
|
|
61
|
+
}
|
|
62
|
+
if (lockPid !== undefined) {
|
|
63
|
+
symlinkSync(`${hostname()}-${lockPid}`, join(profile, 'SingletonLock'))
|
|
64
|
+
}
|
|
65
|
+
const when = new Date(Date.now() - idleMs)
|
|
66
|
+
utimesSync(profile, when, when)
|
|
67
|
+
return profile
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const sessionIntact = (name: string): boolean =>
|
|
71
|
+
existsSync(join(dir, name, 'Default', 'Cookies')) &&
|
|
72
|
+
existsSync(join(dir, name, 'Local State')) &&
|
|
73
|
+
existsSync(join(dir, name, 'Default', 'Preferences')) &&
|
|
74
|
+
existsSync(join(dir, name, 'Default', 'Local Storage', 'leveldb', '000003.log'))
|
|
75
|
+
|
|
76
|
+
const cacheGone = (name: string): boolean =>
|
|
77
|
+
!existsSync(join(dir, name, 'Default', 'Cache')) &&
|
|
78
|
+
!existsSync(join(dir, name, 'Default', 'Code Cache'))
|
|
79
|
+
|
|
80
|
+
describe('age rule', () => {
|
|
81
|
+
test('a dormant profile is removed outright', async () => {
|
|
82
|
+
seedProfile('dormant', { idleMs: 40 * DAY, cacheBytes: 100 })
|
|
83
|
+
const r = await sweepBrowserProfiles({ dir, budget: BUDGET })
|
|
84
|
+
expect(r.removed).toEqual(['dormant'])
|
|
85
|
+
expect(existsSync(join(dir, 'dormant'))).toBe(false)
|
|
86
|
+
expect(r.bytesFreed).toBeGreaterThan(0)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('a recently used profile is kept', async () => {
|
|
90
|
+
seedProfile('fresh', { idleMs: 2 * DAY, cacheBytes: 100 })
|
|
91
|
+
const r = await sweepBrowserProfiles({ dir, budget: BUDGET })
|
|
92
|
+
expect(r.removed).toEqual([])
|
|
93
|
+
expect(sessionIntact('fresh')).toBe(true)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('the age bound is configurable', async () => {
|
|
97
|
+
seedProfile('two-days', { idleMs: 2 * DAY })
|
|
98
|
+
const strict = { ...BUDGET, maxProfileAgeMs: DAY }
|
|
99
|
+
expect((await sweepBrowserProfiles({ dir, budget: strict })).removed).toEqual(['two-days'])
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('size rule', () => {
|
|
104
|
+
test('a profile under budget is left completely alone', async () => {
|
|
105
|
+
seedProfile('small', { idleMs: DAY, cacheBytes: 1_000 })
|
|
106
|
+
const r = await sweepBrowserProfiles({ dir, budget: BUDGET })
|
|
107
|
+
expect(r.purged).toEqual([])
|
|
108
|
+
expect(cacheGone('small')).toBe(false)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('a profile over budget loses its cache but keeps its session', async () => {
|
|
112
|
+
seedProfile('bloated', { idleMs: DAY, cacheBytes: 20_000 })
|
|
113
|
+
const r = await sweepBrowserProfiles({ dir, budget: BUDGET })
|
|
114
|
+
expect(r.purged).toEqual(['bloated'])
|
|
115
|
+
expect(cacheGone('bloated')).toBe(true)
|
|
116
|
+
// The entire point: the cookie survives, so the user stays signed in.
|
|
117
|
+
expect(sessionIntact('bloated')).toBe(true)
|
|
118
|
+
expect(existsSync(join(dir, 'bloated'))).toBe(true)
|
|
119
|
+
expect(r.bytesFreed).toBeGreaterThanOrEqual(40_000)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('the size bound counts only cache directories, not the whole profile', async () => {
|
|
123
|
+
const profile = seedProfile('measured', { idleMs: DAY, cacheBytes: 3_000 })
|
|
124
|
+
writeFileSync(join(profile, 'Default', 'History'), 'z'.repeat(50_000))
|
|
125
|
+
// 2 cache files of 3 KB each; History is not cache and must not count.
|
|
126
|
+
expect(await profileCacheBytes(profile)).toBe(6_000)
|
|
127
|
+
expect((await sweepBrowserProfiles({ dir, budget: BUDGET })).purged).toEqual([])
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('live-browser guard', () => {
|
|
132
|
+
test('a profile locked by a live process is never touched', async () => {
|
|
133
|
+
// Our own pid is unambiguously alive.
|
|
134
|
+
seedProfile('in-use', { idleMs: 40 * DAY, cacheBytes: 20_000, lockPid: process.pid })
|
|
135
|
+
const r = await sweepBrowserProfiles({ dir, budget: BUDGET })
|
|
136
|
+
expect(r.skipped).toEqual(['in-use'])
|
|
137
|
+
expect(r.removed).toEqual([])
|
|
138
|
+
expect(r.purged).toEqual([])
|
|
139
|
+
expect(existsSync(join(dir, 'in-use'))).toBe(true)
|
|
140
|
+
expect(cacheGone('in-use')).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('a stale lock from a dead process does not protect anything', async () => {
|
|
144
|
+
// Chromium leaves these behind after a crash; they must not pin disk forever.
|
|
145
|
+
seedProfile('crashed', { idleMs: 40 * DAY, lockPid: 999_999 })
|
|
146
|
+
const r = await sweepBrowserProfiles({ dir, budget: BUDGET })
|
|
147
|
+
expect(r.removed).toEqual(['crashed'])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('heldByLiveBrowser reads the symlink without resolving it', () => {
|
|
151
|
+
// The target names a file that does not exist — statting it would throw.
|
|
152
|
+
const live = seedProfile('live', { lockPid: process.pid })
|
|
153
|
+
const dead = seedProfile('dead', { lockPid: 999_999 })
|
|
154
|
+
const none = seedProfile('none', {})
|
|
155
|
+
expect(heldByLiveBrowser(live)).toBe(true)
|
|
156
|
+
expect(heldByLiveBrowser(dead)).toBe(false)
|
|
157
|
+
expect(heldByLiveBrowser(none)).toBe(false)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('sweep resilience', () => {
|
|
162
|
+
test('a missing browser directory is a no-op, not an error', async () => {
|
|
163
|
+
const gone = join(dir, 'does-not-exist')
|
|
164
|
+
const r = await sweepBrowserProfiles({ dir: gone, budget: BUDGET })
|
|
165
|
+
expect(r).toEqual({ removed: [], purged: [], skipped: [], bytesFreed: 0 })
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('stray files beside the profiles are ignored', async () => {
|
|
169
|
+
writeFileSync(join(dir, 'browser.json'), '{}')
|
|
170
|
+
seedProfile('real', { idleMs: 40 * DAY })
|
|
171
|
+
expect((await sweepBrowserProfiles({ dir, budget: BUDGET })).removed).toEqual(['real'])
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('several profiles are handled independently in one pass', async () => {
|
|
175
|
+
seedProfile('a-dormant', { idleMs: 40 * DAY })
|
|
176
|
+
seedProfile('b-bloated', { idleMs: DAY, cacheBytes: 20_000 })
|
|
177
|
+
seedProfile('c-fine', { idleMs: DAY, cacheBytes: 100 })
|
|
178
|
+
seedProfile('d-locked', { idleMs: 40 * DAY, lockPid: process.pid })
|
|
179
|
+
const r = await sweepBrowserProfiles({ dir, budget: BUDGET })
|
|
180
|
+
expect(r.removed).toEqual(['a-dormant'])
|
|
181
|
+
expect(r.purged).toEqual(['b-bloated'])
|
|
182
|
+
expect(r.skipped).toEqual(['d-locked'])
|
|
183
|
+
expect(sessionIntact('c-fine')).toBe(true)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('browserRetentionBudget', () => {
|
|
188
|
+
test('defaults when nothing is configured', async () => {
|
|
189
|
+
expect(await browserRetentionBudget()).toEqual({
|
|
190
|
+
maxCacheBytes: DEFAULT_MAX_CACHE_BYTES,
|
|
191
|
+
maxProfileAgeMs: DEFAULT_MAX_PROFILE_AGE_DAYS * DAY,
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('env overrides both bounds', async () => {
|
|
196
|
+
process.env.ASTRALE_BROWSER_MAX_CACHE_BYTES = '4096'
|
|
197
|
+
process.env.ASTRALE_BROWSER_MAX_PROFILE_AGE_DAYS = '3'
|
|
198
|
+
expect(await browserRetentionBudget()).toEqual({
|
|
199
|
+
maxCacheBytes: 4096,
|
|
200
|
+
maxProfileAgeMs: 3 * DAY,
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test.each([
|
|
205
|
+
['zero', '0'],
|
|
206
|
+
['negative', '-1'],
|
|
207
|
+
['not a number', 'lots'],
|
|
208
|
+
])('a %s env value falls back to the default rather than removing the cap', async (_l, value) => {
|
|
209
|
+
process.env.ASTRALE_BROWSER_MAX_CACHE_BYTES = value
|
|
210
|
+
expect((await browserRetentionBudget()).maxCacheBytes).toBe(DEFAULT_MAX_CACHE_BYTES)
|
|
211
|
+
})
|
|
212
|
+
})
|
|
@@ -16,7 +16,7 @@ function dx(
|
|
|
16
16
|
error: CommanderError,
|
|
17
17
|
argv: string[],
|
|
18
18
|
) {
|
|
19
|
-
const rendered = JSON.parse(stripAnsi(renderCommanderError(program, error, argv))) as {
|
|
19
|
+
const rendered = JSON.parse(stripAnsi(renderCommanderError(program, error, argv, true))) as {
|
|
20
20
|
error: string
|
|
21
21
|
message: string
|
|
22
22
|
detail: string
|
|
@@ -23,6 +23,33 @@ describe('AstraleConfigSchema', () => {
|
|
|
23
23
|
})
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
+
test('retention bounds survive a parse — a read/write cycle must not drop them', () => {
|
|
27
|
+
// zod strips unknown keys, so a bound declared only in its reader would be
|
|
28
|
+
// silently erased by any command that rewrites the config.
|
|
29
|
+
const result = AstraleConfigSchema.parse({
|
|
30
|
+
telemetry: { enabled: true, maxAgeDays: 7, maxBytes: 1_048_576 },
|
|
31
|
+
browser: { maxCacheBytes: 52_428_800, maxProfileAgeDays: 14 },
|
|
32
|
+
})
|
|
33
|
+
expect(result.telemetry).toEqual({ enabled: true, maxAgeDays: 7, maxBytes: 1_048_576 })
|
|
34
|
+
expect(result.browser).toEqual({ maxCacheBytes: 52_428_800, maxProfileAgeDays: 14 })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test.each([
|
|
38
|
+
['zero', 0],
|
|
39
|
+
['negative', -5],
|
|
40
|
+
['not a number', 'lots'],
|
|
41
|
+
['infinite', Number.POSITIVE_INFINITY],
|
|
42
|
+
])('a %s bound is dropped, and does not take the rest of the config with it', (_l, value) => {
|
|
43
|
+
const result = AstraleConfigSchema.parse({
|
|
44
|
+
issuer: 'https://test.astrale.ai',
|
|
45
|
+
telemetry: { enabled: true, maxAgeDays: value },
|
|
46
|
+
browser: { maxCacheBytes: value },
|
|
47
|
+
})
|
|
48
|
+
expect(result.telemetry.maxAgeDays).toBeUndefined()
|
|
49
|
+
expect(result.browser.maxCacheBytes).toBeUndefined()
|
|
50
|
+
expect(result.issuer).toBe('https://test.astrale.ai')
|
|
51
|
+
})
|
|
52
|
+
|
|
26
53
|
test('rejects non-url issuer', () => {
|
|
27
54
|
expect(() => AstraleConfigSchema.parse({ issuer: 'not-a-url' })).toThrow()
|
|
28
55
|
})
|
|
@@ -1,26 +1,15 @@
|
|
|
1
|
-
import { publication } from '@astrale-os/
|
|
2
|
-
import {
|
|
3
|
-
import { issuer } from '@astrale-os/sdk/auth'
|
|
4
|
-
import { createDeployment } from '@astrale-os/sdk/deployment'
|
|
5
|
-
import { defineSchema } from '@astrale-os/sdk/schema/v1'
|
|
1
|
+
import { publication } from '@astrale-os/sdk/publication'
|
|
2
|
+
import { defineSchema } from '@astrale-os/sdk/schema'
|
|
6
3
|
import { describe, expect, test } from 'bun:test'
|
|
7
4
|
|
|
8
|
-
import {
|
|
5
|
+
import { releaseFor } from '../../__tests__/fixtures/publication'
|
|
6
|
+
import { domainPublicationUrl, fetchDomainPublication } from '../domain-publication'
|
|
9
7
|
|
|
10
8
|
const ROOT = 'https://publication-reader.example.dev'
|
|
11
9
|
const PUBLICATION_URL = `${ROOT}/.well-known/astrale/domain.json`
|
|
12
10
|
const MAXIMUM_PUBLICATION_BYTES = 1024 * 1024
|
|
13
11
|
const schema = defineSchema('publication-reader.example.dev', {})
|
|
14
|
-
const
|
|
15
|
-
schema,
|
|
16
|
-
handlers: { functions: {}, classes: {}, interfaces: {} },
|
|
17
|
-
})
|
|
18
|
-
const deployed = createDeployment({
|
|
19
|
-
definition,
|
|
20
|
-
issuer: issuer.accept(ROOT),
|
|
21
|
-
bundleHref: `${ROOT}/domain.bundle.json`,
|
|
22
|
-
bindings: { callables: [] },
|
|
23
|
-
}).publication
|
|
12
|
+
const deployed = releaseFor(schema, ROOT).publication
|
|
24
13
|
|
|
25
14
|
describe('Domain Publication reader', () => {
|
|
26
15
|
test('fetches only the canonical path with redirects disabled', async () => {
|
|
@@ -34,6 +23,11 @@ describe('Domain Publication reader', () => {
|
|
|
34
23
|
expect(requests).toEqual([{ url: PUBLICATION_URL, redirect: 'error' }])
|
|
35
24
|
})
|
|
36
25
|
|
|
26
|
+
test('normalizes an origin and admits the exact canonical Publication URL', () => {
|
|
27
|
+
expect(domainPublicationUrl(ROOT).href).toBe(PUBLICATION_URL)
|
|
28
|
+
expect(domainPublicationUrl(PUBLICATION_URL).href).toBe(PUBLICATION_URL)
|
|
29
|
+
})
|
|
30
|
+
|
|
37
31
|
test('propagates redirect rejection instead of accepting another target', async () => {
|
|
38
32
|
const fetch = async (_input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
39
33
|
expect(init?.redirect).toBe('error')
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
IdpSessionSchema,
|
|
10
10
|
isSessionExpired,
|
|
11
11
|
issuerFromToken,
|
|
12
|
+
legacyBuiltinWorkosReplacement,
|
|
12
13
|
normalizeTokenResponse,
|
|
13
14
|
OAuthTokenError,
|
|
14
15
|
OidcMetadataSchema,
|
|
@@ -63,8 +64,11 @@ describe('IdP schemas and token helpers', () => {
|
|
|
63
64
|
expect(config?.client.token_request_format).toBe('json')
|
|
64
65
|
})
|
|
65
66
|
|
|
66
|
-
test('
|
|
67
|
-
|
|
67
|
+
test('builds the built-in WorkOS IdP with the public client id by default', () => {
|
|
68
|
+
const config = builtinIdpConfig('workos', undefined, {})
|
|
69
|
+
|
|
70
|
+
expect(config?.entry.builtIn).toBe(true)
|
|
71
|
+
expect(config?.client.client_id).toBe('client_01KC29HET5F3QAQ8GNTPZ7F320')
|
|
68
72
|
})
|
|
69
73
|
|
|
70
74
|
test('reads WorkOS client id from WORKOS_CLIENT_ID before VITE_WORKOS_CLIENT_ID', () => {
|
|
@@ -75,8 +79,34 @@ describe('IdP schemas and token helpers', () => {
|
|
|
75
79
|
}),
|
|
76
80
|
).toBe('client_primary')
|
|
77
81
|
expect(workosClientIdFromEnv({ VITE_WORKOS_CLIENT_ID: 'client_vite' })).toBe('client_vite')
|
|
78
|
-
expect(workosClientIdFromEnv({})).
|
|
79
|
-
expect(workosClientIdFromEnv({ WORKOS_CLIENT_ID: ' ' })).
|
|
82
|
+
expect(workosClientIdFromEnv({})).toBe('client_01KC29HET5F3QAQ8GNTPZ7F320')
|
|
83
|
+
expect(workosClientIdFromEnv({ WORKOS_CLIENT_ID: ' ' })).toBe(
|
|
84
|
+
'client_01KC29HET5F3QAQ8GNTPZ7F320',
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('replaces only the legacy persisted built-in WorkOS client', () => {
|
|
89
|
+
const legacy = builtinIdpConfig('workos', 'client_01KC29HEGD7B40TV2C4QZ436BG', {})!
|
|
90
|
+
const replacement = legacyBuiltinWorkosReplacement(legacy, undefined, {})
|
|
91
|
+
|
|
92
|
+
expect(replacement?.client.client_id).toBe('client_01KC29HET5F3QAQ8GNTPZ7F320')
|
|
93
|
+
expect(replacement?.metadata.jwks_uri).toBe(
|
|
94
|
+
'https://api.workos.com/sso/jwks/client_01KC29HET5F3QAQ8GNTPZ7F320',
|
|
95
|
+
)
|
|
96
|
+
expect(
|
|
97
|
+
legacyBuiltinWorkosReplacement(
|
|
98
|
+
{ ...legacy, entry: { ...legacy.entry, builtIn: false } },
|
|
99
|
+
undefined,
|
|
100
|
+
{},
|
|
101
|
+
),
|
|
102
|
+
).toBeUndefined()
|
|
103
|
+
expect(
|
|
104
|
+
legacyBuiltinWorkosReplacement(
|
|
105
|
+
{ ...legacy, client: { ...legacy.client, client_id: 'client_custom' } },
|
|
106
|
+
undefined,
|
|
107
|
+
{},
|
|
108
|
+
),
|
|
109
|
+
).toBeUndefined()
|
|
80
110
|
})
|
|
81
111
|
|
|
82
112
|
test('stores client secret env names, not secret material', () => {
|
|
@@ -18,7 +18,6 @@ const managedDemo: OwnedInstanceInfo = {
|
|
|
18
18
|
slug: 'demo',
|
|
19
19
|
url: 'https://demo.eu.astrale.ai',
|
|
20
20
|
state: 'ready',
|
|
21
|
-
region: 'eu',
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
describe('collectInstanceCandidates', () => {
|
|
@@ -138,11 +137,10 @@ describe('describeInstanceCandidate', () => {
|
|
|
138
137
|
slug: 'twin',
|
|
139
138
|
url: 'https://twin.eu.astrale.ai',
|
|
140
139
|
state: 'ready',
|
|
141
|
-
region: 'eu',
|
|
142
140
|
},
|
|
143
141
|
])
|
|
144
142
|
expect(describeInstanceCandidate(managed!)).toBe(
|
|
145
|
-
'twin (managed) https://twin.eu.astrale.ai/api
|
|
143
|
+
'twin (managed) https://twin.eu.astrale.ai/api',
|
|
146
144
|
)
|
|
147
145
|
})
|
|
148
146
|
})
|
|
@@ -3,6 +3,7 @@ import { describe, expect, test } from 'bun:test'
|
|
|
3
3
|
import type { AstraleConfig } from '../config'
|
|
4
4
|
import type { InstanceStore } from '../instance'
|
|
5
5
|
|
|
6
|
+
import { AdminInstanceNotFoundError } from '../../admin/instance/model'
|
|
6
7
|
import { AstraleError } from '../../errors'
|
|
7
8
|
import { DEFAULT_CONFIG } from '../config'
|
|
8
9
|
import {
|
|
@@ -112,6 +113,7 @@ describe('resolveInstanceTarget', () => {
|
|
|
112
113
|
id: 'node-1',
|
|
113
114
|
slug,
|
|
114
115
|
url: `https://${slug}.eu.astrale.ai`,
|
|
116
|
+
state: 'ready',
|
|
115
117
|
}),
|
|
116
118
|
},
|
|
117
119
|
),
|
|
@@ -124,9 +126,6 @@ describe('resolveInstanceTarget', () => {
|
|
|
124
126
|
})
|
|
125
127
|
|
|
126
128
|
test('unknown slugs surface the original instance-not-found error', async () => {
|
|
127
|
-
const notFound = new Error('Path not found: "/admin/instances/missing"')
|
|
128
|
-
notFound.name = 'NotFoundError'
|
|
129
|
-
|
|
130
129
|
await expect(
|
|
131
130
|
resolveInstanceTarget(
|
|
132
131
|
{ source: 'name', name: 'missing' },
|
|
@@ -134,7 +133,7 @@ describe('resolveInstanceTarget', () => {
|
|
|
134
133
|
config: DEFAULT_CONFIG,
|
|
135
134
|
instances: emptyStore,
|
|
136
135
|
managed: async () => {
|
|
137
|
-
throw
|
|
136
|
+
throw new AdminInstanceNotFoundError('missing')
|
|
138
137
|
},
|
|
139
138
|
},
|
|
140
139
|
),
|
|
@@ -168,14 +167,8 @@ describe('instance target helpers', () => {
|
|
|
168
167
|
expect(couldBeConfiguredAdminInstance('bryan', directAdminConfig)).toBe(false)
|
|
169
168
|
})
|
|
170
169
|
|
|
171
|
-
test('treats
|
|
172
|
-
|
|
173
|
-
data?: unknown
|
|
174
|
-
}
|
|
175
|
-
wrapped.name = 'KernelError'
|
|
176
|
-
wrapped.data = { type: 'NotFoundError' }
|
|
177
|
-
|
|
178
|
-
expect(isManagedInstanceNotFound(wrapped)).toBe(true)
|
|
170
|
+
test('treats the owner-scoped Admin error as managed lookup not-found', () => {
|
|
171
|
+
expect(isManagedInstanceNotFound(new AdminInstanceNotFoundError('admin'))).toBe(true)
|
|
179
172
|
})
|
|
180
173
|
|
|
181
174
|
test('does not hide non-not-found managed lookup failures', () => {
|
|
@@ -194,37 +187,12 @@ describe('instance target helpers', () => {
|
|
|
194
187
|
})
|
|
195
188
|
})
|
|
196
189
|
|
|
197
|
-
describe('isManagedInstanceNotFound:
|
|
198
|
-
test('
|
|
199
|
-
const e = new Error('NOT_FOUND: no instance node for "knowledge"')
|
|
200
|
-
e.name = 'InternalKernelError'
|
|
201
|
-
expect(isManagedInstanceNotFound(e)).toBe(true)
|
|
202
|
-
})
|
|
203
|
-
test('other InternalKernelError messages are not swallowed', () => {
|
|
190
|
+
describe('isManagedInstanceNotFound: unknown errors', () => {
|
|
191
|
+
test('does not infer a miss from an InternalKernelError message', () => {
|
|
204
192
|
const e = new Error('DISPATCH_FAILED: handler crashed')
|
|
205
193
|
e.name = 'InternalKernelError'
|
|
206
194
|
expect(isManagedInstanceNotFound(e)).toBe(false)
|
|
207
195
|
})
|
|
208
|
-
test('resolveNamedInstanceTarget maps the wrap to typed INSTANCE_NOT_FOUND', async () => {
|
|
209
|
-
const managed = async () => {
|
|
210
|
-
const e = new Error('NOT_FOUND: no instance node for "ghost"')
|
|
211
|
-
e.name = 'InternalKernelError'
|
|
212
|
-
throw e
|
|
213
|
-
}
|
|
214
|
-
const opts = {
|
|
215
|
-
config: DEFAULT_CONFIG,
|
|
216
|
-
instances: { instances: {} },
|
|
217
|
-
managed,
|
|
218
|
-
} as unknown as Parameters<typeof resolveInstanceTarget>[1]
|
|
219
|
-
let caught: unknown
|
|
220
|
-
try {
|
|
221
|
-
await resolveInstanceTarget({ source: 'name', name: 'ghost' }, opts)
|
|
222
|
-
} catch (e) {
|
|
223
|
-
caught = e
|
|
224
|
-
}
|
|
225
|
-
expect(caught).toBeInstanceOf(AstraleError)
|
|
226
|
-
expect((caught as AstraleError).code).toBe('INSTANCE_NOT_FOUND')
|
|
227
|
-
})
|
|
228
196
|
|
|
229
197
|
test('maps Admin token-exchange failure to INSTANCE_NOT_FOUND', async () => {
|
|
230
198
|
const managed = async () => {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { AstraleError } from '../../errors'
|
|
4
|
+
import { failInput, fatal } from '../log'
|
|
5
|
+
|
|
6
|
+
class ExitError extends Error {}
|
|
7
|
+
|
|
8
|
+
let stderr = ''
|
|
9
|
+
let exit: typeof process.exit
|
|
10
|
+
let write: typeof process.stderr.write
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
stderr = ''
|
|
14
|
+
exit = process.exit
|
|
15
|
+
write = process.stderr.write
|
|
16
|
+
process.exit = (() => {
|
|
17
|
+
throw new ExitError()
|
|
18
|
+
}) as typeof process.exit
|
|
19
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
20
|
+
stderr += String(chunk)
|
|
21
|
+
return true
|
|
22
|
+
}) as typeof process.stderr.write
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
process.exit = exit
|
|
27
|
+
process.stderr.write = write
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('terminal error safety', () => {
|
|
31
|
+
test('renders an empty aggregate as an honest non-blank unexpected failure', () => {
|
|
32
|
+
expect(() => fatal(new AggregateError([], ''), { json: true })).toThrow(ExitError)
|
|
33
|
+
expect(JSON.parse(stderr)).toEqual({
|
|
34
|
+
error: 'UNEXPECTED_ERROR',
|
|
35
|
+
message: 'The CLI encountered an unexpected internal failure.',
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('retains unknown failure evidence only under explicit debug output', () => {
|
|
40
|
+
const failure = new AggregateError([new Error('provider rejected organization scope')], '')
|
|
41
|
+
|
|
42
|
+
expect(() => fatal(failure, { json: true, debug: true })).toThrow(ExitError)
|
|
43
|
+
|
|
44
|
+
expect(stderr).toContain(
|
|
45
|
+
'{"error":"UNEXPECTED_ERROR","message":"The CLI encountered an unexpected internal failure."}',
|
|
46
|
+
)
|
|
47
|
+
expect(stderr).toContain('aggregate: Error: provider rejected organization scope')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('rejects blank admitted CLI diagnostics', () => {
|
|
51
|
+
expect(() => new AstraleError('INVALID_INPUT', ' ')).toThrow(TypeError)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('labels only expected admission failures as invalid input', () => {
|
|
55
|
+
expect(() => failInput(new TypeError('Authored value is invalid.'), { json: true })).toThrow(
|
|
56
|
+
ExitError,
|
|
57
|
+
)
|
|
58
|
+
expect(JSON.parse(stderr)).toMatchObject({ error: 'INVALID_INPUT' })
|
|
59
|
+
|
|
60
|
+
stderr = ''
|
|
61
|
+
expect(() => failInput(new AggregateError([], ''), { json: true })).toThrow(ExitError)
|
|
62
|
+
expect(JSON.parse(stderr)).toMatchObject({ error: 'UNEXPECTED_ERROR' })
|
|
63
|
+
})
|
|
64
|
+
})
|