@astrale-os/cli 1.0.0-beta.1 → 1.0.0-beta.11
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 +23 -12
- package/dist/astrale.js +48048 -56798
- package/dist/public/connect-core.js +2011 -30765
- package/dist/public/keys/index.js +884 -29483
- package/dist/public/paths/index.js +13101 -41459
- package/dist/types/admin/binding.d.ts +19 -0
- package/dist/types/admin/instance/client.d.ts +22 -0
- package/dist/types/admin/instance/index.d.ts +2 -0
- package/dist/types/admin/instance/model.d.ts +5 -12
- 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 +7 -59
- package/dist/types/lib/failure-debug.d.ts +1 -0
- package/dist/types/lib/idp.d.ts +26 -168
- 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/meta.d.ts +2 -2
- 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 +91 -0
- package/dist/types/lib/validation.d.ts +4 -1
- package/package.json +16 -19
- package/src/__tests__/fixtures/publication.ts +20 -0
- package/src/admin/__tests__/binding.test.ts +31 -0
- package/src/admin/__tests__/fixture.ts +101 -0
- package/src/admin/binding.ts +98 -0
- package/src/admin/catalog/.spec/api.d.ts +2 -2
- package/src/admin/catalog/__tests__/client.test.ts +20 -49
- package/src/admin/catalog/client.ts +36 -32
- package/src/admin/catalog/model.ts +3 -4
- 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 +2 -2
- package/src/admin/instance/__tests__/client.test.ts +39 -164
- package/src/admin/instance/client.ts +35 -124
- package/src/admin/instance/index.ts +1 -2
- package/src/admin/instance/model.ts +11 -20
- package/src/commands/__tests__/admin-instance.test.ts +11 -8
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call-describe.test.ts +46 -33
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +12 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +6 -2
- package/src/commands/__tests__/domain-list.test.ts +63 -4
- package/src/commands/__tests__/install-identity-override.test.ts +19 -19
- 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__/read-commands.test.ts +35 -10
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +48 -19
- package/src/commands/call-describe.ts +60 -68
- package/src/commands/call.ts +43 -56
- package/src/commands/domain/install.ts +13 -17
- package/src/commands/domain/list.ts +18 -13
- package/src/commands/domain/publish.ts +14 -11
- package/src/commands/domain/uninstall.ts +2 -2
- package/src/commands/get.ts +14 -15
- package/src/commands/identity/__tests__/register.test.ts +3 -3
- package/src/commands/identity/register.ts +5 -4
- 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 +9 -16
- package/src/commands/logs.ts +24 -25
- package/src/commands/mutate.ts +6 -5
- package/src/commands/query.ts +27 -30
- package/src/commands/studio.ts +20 -0
- package/src/commands/token.ts +8 -7
- package/src/commands/update.ts +38 -14
- package/src/commands/view.ts +7 -6
- package/src/connection/.spec/api.d.ts +7 -8
- package/src/connection/.spec/architecture.md +5 -4
- 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__/credential.test.ts +20 -1
- package/src/connection/__tests__/errors.test.ts +43 -32
- package/src/connection/__tests__/exchange.test.ts +36 -6
- 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 +3 -3
- package/src/connection/__tests__/target.test.ts +1 -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 +52 -27
- 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 +19 -8
- 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__/registration.test.ts +1 -1
- package/src/identity/registration.ts +3 -3
- 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__/command-dx.test.ts +1 -1
- package/src/lib/__tests__/domain-publication.test.ts +138 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +36 -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__/instance.test.ts +6 -0
- package/src/lib/__tests__/log-errors.test.ts +64 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +113 -8
- package/src/lib/__tests__/view-open-intent.test.ts +0 -2
- package/src/lib/__tests__/view-server.test.ts +18 -3
- package/src/lib/__tests__/view-session.test.ts +1 -1
- package/src/lib/admin-instance.ts +14 -8
- package/src/lib/admin-target.ts +21 -2
- package/src/lib/command-dx.ts +9 -9
- package/src/lib/domain-publication.ts +104 -0
- package/src/lib/failure-debug.ts +26 -0
- package/src/lib/idp.ts +39 -4
- package/src/lib/instance-candidates.ts +2 -2
- package/src/lib/instance-target.ts +3 -14
- package/src/lib/instance.ts +22 -12
- package/src/lib/log.ts +16 -9
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/provision-instance.ts +34 -57
- package/src/lib/update.ts +124 -30
- package/src/lib/validation.ts +2 -2
- package/src/lib/view/resolve.ts +1 -1
- package/src/lib/view/server.ts +17 -3
- package/src/program/__tests__/program.test.ts +20 -4
- package/src/program/build.ts +2 -2
- package/src/setup/__tests__/instance-step.test.ts +27 -10
- package/src/setup/steps/instance.ts +22 -15
- package/studio/client/dist/assets/elk-api-DjmKsHXc.js +1 -0
- package/studio/client/dist/assets/index-7YHdeCxp.js +81 -0
- package/studio/client/dist/assets/index-C0FAnwNw.css +2 -0
- package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
- package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +1 -0
- package/studio/client/dist/assets/schema-studio-CABpTfvH.js +8 -0
- package/studio/client/dist/index.html +3 -2
- package/studio/package.json +10 -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/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- package/studio/server/agent/prompts/anchors.test.ts +32 -36
- package/studio/server/agent/prompts/anchors.ts +20 -49
- package/studio/server/agent/prompts/system.test.ts +3 -2
- package/studio/server/agent/prompts/system.ts +6 -5
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/routes.test.ts +1 -1
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/coordinator.test.ts +1 -1
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +43 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api-agent-loadout.test.ts +1 -1
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +122 -0
- package/studio/server/cache.ts +162 -11
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +50 -40
- package/studio/server/domain.ts +83 -52
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/index.ts +1 -1
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/routes.ts +114 -0
- package/studio/server/introspect/anatomy/views.ts +53 -0
- package/studio/server/introspect/anatomy-extras.test.ts +106 -35
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.test.ts +22 -14
- package/studio/server/introspect/anatomy.ts +52 -28
- package/studio/server/introspect/bundle.ts +15 -9
- package/studio/server/introspect/canonical-schema.test.ts +93 -309
- package/studio/server/introspect/canonical-schema.ts +257 -425
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/core-extractor.ts +18 -127
- package/studio/server/introspect/core.ts +1 -2
- package/studio/server/introspect/diff.test.ts +18 -18
- package/studio/server/introspect/diff.ts +44 -90
- package/studio/server/introspect/extractor.ts +23 -116
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/overlay.test.ts +26 -54
- package/studio/server/introspect/overlay.ts +10 -37
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +83 -143
- package/studio/server/introspect/runtime.ts +40 -11
- package/studio/server/introspect/schema-ir-json.test.ts +70 -0
- package/studio/server/introspect/schema-ir-json.ts +151 -0
- 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/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +161 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +46 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +330 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +223 -7
- package/studio/server/state/baseline.ts +148 -34
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -11
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/views/target.test.ts +78 -0
- package/studio/server/views/target.ts +222 -0
- package/studio/server/watch.test.ts +11 -11
- package/studio/server/watch.ts +9 -5
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/server/workspace-watch.ts +2 -2
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +393 -0
- package/studio/shared/contracts/surface.test.ts +317 -0
- package/studio/shared/contracts/workspace.ts +265 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +34 -0
- package/studio/shared/schema/identity.ts +68 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +40 -61
- package/.check-workspace.cjs +0 -40
- package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
- package/studio/client/dist/assets/elk-api-D2xgMJvi.js +0 -1
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.test.ts +0 -286
- package/studio/server/state/views.ts +0 -535
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
DEFAULT_ADMIN_TARGET_NAME,
|
|
7
7
|
DEFAULT_ADMIN_DOMAIN_ISSUER,
|
|
8
8
|
DEFAULT_ADMIN_TARGET_URL,
|
|
9
|
+
defaultAdminTargetForChannel,
|
|
9
10
|
resolveAdminTargetFromStore,
|
|
10
11
|
} from '../admin-target'
|
|
11
12
|
import { DEFAULT_CONFIG, type AstraleConfig } from '../config'
|
|
@@ -32,6 +33,17 @@ const instances: InstanceStore = {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
describe('resolveAdminTargetFromStore', () => {
|
|
36
|
+
test('derives hosted Admin defaults from the release channel', () => {
|
|
37
|
+
expect(defaultAdminTargetForChannel('beta')).toEqual({
|
|
38
|
+
url: 'https://admin.eu.beta.astrale.ai/api',
|
|
39
|
+
domainIssuer: 'https://admin.beta.astrale.ai',
|
|
40
|
+
})
|
|
41
|
+
expect(defaultAdminTargetForChannel('stable')).toEqual({
|
|
42
|
+
url: 'https://admin.eu.astrale.ai/api',
|
|
43
|
+
domainIssuer: 'https://admin.astrale.ai',
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
35
47
|
test('uses hosted admin default without active instance coupling', () => {
|
|
36
48
|
expect(resolveAdminTargetFromStore({}, DEFAULT_CONFIG, instances)).toEqual({
|
|
37
49
|
name: DEFAULT_ADMIN_TARGET_NAME,
|
|
@@ -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
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { publication } from '@astrale-os/sdk/publication'
|
|
2
|
+
import { defineSchema } from '@astrale-os/sdk/schema'
|
|
3
|
+
import { describe, expect, test } from 'bun:test'
|
|
4
|
+
|
|
5
|
+
import { releaseFor } from '../../__tests__/fixtures/publication'
|
|
6
|
+
import { domainPublicationUrl, fetchDomainPublication } from '../domain-publication'
|
|
7
|
+
|
|
8
|
+
const ROOT = 'https://publication-reader.example.dev'
|
|
9
|
+
const PUBLICATION_URL = `${ROOT}/.well-known/astrale/domain.json`
|
|
10
|
+
const MAXIMUM_PUBLICATION_BYTES = 1024 * 1024
|
|
11
|
+
const schema = defineSchema('publication-reader.example.dev', {})
|
|
12
|
+
const deployed = releaseFor(schema, ROOT).publication
|
|
13
|
+
|
|
14
|
+
describe('Domain Publication reader', () => {
|
|
15
|
+
test('fetches only the canonical path with redirects disabled', async () => {
|
|
16
|
+
const requests: Array<{ readonly url: string; readonly redirect?: RequestRedirect }> = []
|
|
17
|
+
const fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
18
|
+
requests.push({ url: String(input), redirect: init?.redirect })
|
|
19
|
+
return Response.json(deployed)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await expect(fetchDomainPublication(`${ROOT}/`, undefined, fetch)).resolves.toEqual(deployed)
|
|
23
|
+
expect(requests).toEqual([{ url: PUBLICATION_URL, redirect: 'error' }])
|
|
24
|
+
})
|
|
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
|
+
|
|
31
|
+
test('propagates redirect rejection instead of accepting another target', async () => {
|
|
32
|
+
const fetch = async (_input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
33
|
+
expect(init?.redirect).toBe('error')
|
|
34
|
+
throw new TypeError('redirect rejected')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await expect(fetchDomainPublication(ROOT, undefined, fetch)).rejects.toThrow(
|
|
38
|
+
'redirect rejected',
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('stops at one MiB and cancels an over-limit response stream', async () => {
|
|
43
|
+
let pulls = 0
|
|
44
|
+
let cancelled = false
|
|
45
|
+
const body = new ReadableStream<Uint8Array>(
|
|
46
|
+
{
|
|
47
|
+
pull(controller) {
|
|
48
|
+
pulls += 1
|
|
49
|
+
if (pulls === 1) {
|
|
50
|
+
controller.enqueue(new Uint8Array(MAXIMUM_PUBLICATION_BYTES))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (pulls === 2) {
|
|
54
|
+
controller.enqueue(Uint8Array.of(0))
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
controller.error(new Error('reader consumed beyond its byte limit'))
|
|
58
|
+
},
|
|
59
|
+
cancel() {
|
|
60
|
+
cancelled = true
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{ highWaterMark: 0 },
|
|
64
|
+
)
|
|
65
|
+
const fetch = async (): Promise<Response> => new Response(body)
|
|
66
|
+
|
|
67
|
+
await expect(fetchDomainPublication(ROOT, undefined, fetch)).rejects.toThrow(
|
|
68
|
+
`exceeded ${MAXIMUM_PUBLICATION_BYTES} bytes`,
|
|
69
|
+
)
|
|
70
|
+
expect(pulls).toBe(2)
|
|
71
|
+
expect(cancelled).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('rejects an oversized Content-Length before pulling and cancels the body', async () => {
|
|
75
|
+
let pulls = 0
|
|
76
|
+
let cancelled = false
|
|
77
|
+
const body = new ReadableStream<Uint8Array>(
|
|
78
|
+
{
|
|
79
|
+
pull() {
|
|
80
|
+
pulls += 1
|
|
81
|
+
},
|
|
82
|
+
cancel() {
|
|
83
|
+
cancelled = true
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{ highWaterMark: 0 },
|
|
87
|
+
)
|
|
88
|
+
const fetch = async (): Promise<Response> =>
|
|
89
|
+
new Response(body, {
|
|
90
|
+
headers: { 'content-length': String(MAXIMUM_PUBLICATION_BYTES + 1) },
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
await expect(fetchDomainPublication(ROOT, undefined, fetch)).rejects.toThrow(
|
|
94
|
+
`exceeded ${MAXIMUM_PUBLICATION_BYTES} bytes`,
|
|
95
|
+
)
|
|
96
|
+
expect(pulls).toBe(0)
|
|
97
|
+
expect(cancelled).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test.each([
|
|
101
|
+
['invalid UTF-8', Uint8Array.of(0xc3, 0x28), TypeError],
|
|
102
|
+
['invalid JSON', new TextEncoder().encode('{'), SyntaxError],
|
|
103
|
+
['invalid Publication', new TextEncoder().encode('{}'), publication.PublicationError],
|
|
104
|
+
] as const)('rejects %s before returning a Publication', async (_label, bytes, Cause) => {
|
|
105
|
+
const fetch = async (): Promise<Response> => new Response(bytes)
|
|
106
|
+
const error = await rejection(fetchDomainPublication(ROOT, undefined, fetch))
|
|
107
|
+
|
|
108
|
+
expect(error.message).toBe(`GET ${PUBLICATION_URL} returned an invalid Domain Publication`)
|
|
109
|
+
expect(error.cause).toBeInstanceOf(Cause)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test.each([
|
|
113
|
+
`${ROOT}/nested`,
|
|
114
|
+
`${ROOT}?deployment=current`,
|
|
115
|
+
`${ROOT}#fragment`,
|
|
116
|
+
'https://user:secret@publication-reader.example.dev',
|
|
117
|
+
'ftp://publication-reader.example.dev',
|
|
118
|
+
])('rejects a non-origin deployment URL before fetch: %s', async (input) => {
|
|
119
|
+
let called = false
|
|
120
|
+
const fetch = async (): Promise<Response> => {
|
|
121
|
+
called = true
|
|
122
|
+
return Response.json(deployed)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await expect(fetchDomainPublication(input, undefined, fetch)).rejects.toBeInstanceOf(TypeError)
|
|
126
|
+
expect(called).toBe(false)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
async function rejection(input: Promise<unknown>): Promise<Error> {
|
|
131
|
+
try {
|
|
132
|
+
await input
|
|
133
|
+
} catch (cause) {
|
|
134
|
+
if (cause instanceof Error) return cause
|
|
135
|
+
throw new Error('Expected an Error rejection.', { cause })
|
|
136
|
+
}
|
|
137
|
+
throw new Error('Expected the promise to reject.')
|
|
138
|
+
}
|
|
@@ -68,11 +68,29 @@ describe('ensureFreshSession', () => {
|
|
|
68
68
|
expect(result.orgHintCalls).toBe(1)
|
|
69
69
|
expect(server.refreshCount()).toBe(1)
|
|
70
70
|
const session = await readSession()
|
|
71
|
+
expect(session.clientId).toBe('c_1')
|
|
71
72
|
expect(session.refresh_token).toBe('rt-1')
|
|
72
73
|
expect((session.tokens as Record<string, { access_token: string }>)[AUD].access_token).toBe(
|
|
73
74
|
result.token as string,
|
|
74
75
|
)
|
|
75
76
|
expect(server.lastOrganizationId()).toBe('org_1')
|
|
77
|
+
expect(server.lastClientId()).toBe('c_1')
|
|
78
|
+
} finally {
|
|
79
|
+
await server.stop()
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('refreshes with the client that issued the session instead of the IdP default', async () => {
|
|
84
|
+
const server = rotationServer()
|
|
85
|
+
try {
|
|
86
|
+
await writeIdpConfig(server.url)
|
|
87
|
+
await writeSession({ clientId: 'c_session', expires_at: past() })
|
|
88
|
+
|
|
89
|
+
const result = await runDriver('ensure', { DRIVER_AUDIENCE: AUD })
|
|
90
|
+
|
|
91
|
+
expect(result.ok).toBe(true)
|
|
92
|
+
expect(server.lastClientId()).toBe('c_session')
|
|
93
|
+
expect((await readSession()).clientId).toBe('c_session')
|
|
76
94
|
} finally {
|
|
77
95
|
await server.stop()
|
|
78
96
|
}
|
|
@@ -273,6 +291,7 @@ function rotationServer(opts?: {
|
|
|
273
291
|
}): {
|
|
274
292
|
url: string
|
|
275
293
|
refreshCount: () => number
|
|
294
|
+
lastClientId: () => string | undefined
|
|
276
295
|
lastOrganizationId: () => string | undefined
|
|
277
296
|
stop: () => Promise<void>
|
|
278
297
|
} {
|
|
@@ -280,6 +299,7 @@ function rotationServer(opts?: {
|
|
|
280
299
|
// refresh token is invalid_grant.
|
|
281
300
|
let current = 'rt-0'
|
|
282
301
|
let count = 0
|
|
302
|
+
let lastClientId: string | undefined
|
|
283
303
|
let lastOrgId: string | undefined
|
|
284
304
|
const server = Bun.serve({
|
|
285
305
|
port: 0,
|
|
@@ -300,6 +320,7 @@ function rotationServer(opts?: {
|
|
|
300
320
|
if (opts?.delayMs) await new Promise((r) => setTimeout(r, opts.delayMs))
|
|
301
321
|
count += 1
|
|
302
322
|
current = `rt-${count}`
|
|
323
|
+
lastClientId = form.get('client_id') ?? undefined
|
|
303
324
|
lastOrgId = form.get('organization_id') ?? undefined
|
|
304
325
|
return Response.json({
|
|
305
326
|
access_token: unsignedJwt({ aud: AUD, exp: futureEpoch() }),
|
|
@@ -312,6 +333,7 @@ function rotationServer(opts?: {
|
|
|
312
333
|
return {
|
|
313
334
|
url: `http://${server.hostname}:${server.port}`,
|
|
314
335
|
refreshCount: () => count,
|
|
336
|
+
lastClientId: () => lastClientId,
|
|
315
337
|
lastOrganizationId: () => lastOrgId,
|
|
316
338
|
stop: () => server.stop(true),
|
|
317
339
|
}
|
|
@@ -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', () => {
|
|
@@ -100,6 +130,7 @@ describe('IdP schemas and token helpers', () => {
|
|
|
100
130
|
idp: 'workos',
|
|
101
131
|
issuer: 'https://example.authkit.app',
|
|
102
132
|
subject: 'user_123',
|
|
133
|
+
clientId: 'client_123',
|
|
103
134
|
access_token: 'opaque-token',
|
|
104
135
|
refresh_token: 'refresh-token',
|
|
105
136
|
token_type: 'Bearer',
|
|
@@ -110,6 +141,7 @@ describe('IdP schemas and token helpers', () => {
|
|
|
110
141
|
})
|
|
111
142
|
|
|
112
143
|
expect(session.claims?.email).toBe('alice@example.com')
|
|
144
|
+
expect(session.clientId).toBe('client_123')
|
|
113
145
|
expect(session.refresh_token).toBe('refresh-token')
|
|
114
146
|
})
|
|
115
147
|
|
|
@@ -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 () => {
|
|
@@ -74,6 +74,12 @@ describe('InstanceStoreSchema', () => {
|
|
|
74
74
|
expect(normalizeInstanceKernelUrl('https://testmarc.eu.astrale.ai/')).toBe(
|
|
75
75
|
'https://testmarc.eu.astrale.ai/api',
|
|
76
76
|
)
|
|
77
|
+
expect(normalizeInstanceKernelUrl('https://testmarc.eu.beta.astrale.ai')).toBe(
|
|
78
|
+
'https://testmarc.eu.beta.astrale.ai/api',
|
|
79
|
+
)
|
|
80
|
+
expect(normalizeInstanceKernelUrl('https://shell.beta.astrale.ai')).toBe(
|
|
81
|
+
'https://shell.beta.astrale.ai',
|
|
82
|
+
)
|
|
77
83
|
})
|
|
78
84
|
|
|
79
85
|
test('does not rewrite explicit kernel or non-managed URLs', () => {
|
|
@@ -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
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { readFileSync } from 'node:fs'
|
|
3
|
-
import { join } from 'node:path'
|
|
2
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
3
|
+
import { join, relative } from 'node:path'
|
|
4
4
|
import { Project } from 'ts-morph'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -19,6 +19,18 @@ import { Project } from 'ts-morph'
|
|
|
19
19
|
|
|
20
20
|
const CLI_ROOT = new URL('../../../', import.meta.url).pathname
|
|
21
21
|
|
|
22
|
+
interface PackageManifest {
|
|
23
|
+
dependencies?: Record<string, string>
|
|
24
|
+
scripts?: Record<string, string>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cliPackage = JSON.parse(
|
|
28
|
+
readFileSync(join(CLI_ROOT, 'package.json'), 'utf8'),
|
|
29
|
+
) as PackageManifest
|
|
30
|
+
const studioPackage = JSON.parse(
|
|
31
|
+
readFileSync(join(CLI_ROOT, 'studio/package.json'), 'utf8'),
|
|
32
|
+
) as PackageManifest
|
|
33
|
+
|
|
22
34
|
/** Base package of a bare specifier: `@scope/pkg/sub` → `@scope/pkg`, `pkg/sub` → `pkg`. */
|
|
23
35
|
function basePackage(spec: string): string {
|
|
24
36
|
const parts = spec.split('/')
|
|
@@ -29,26 +41,33 @@ function isBuiltin(spec: string): boolean {
|
|
|
29
41
|
return spec.startsWith('node:') || spec === 'bun' || spec.startsWith('bun:')
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
function runtimeFiles(directory: string): string[] {
|
|
45
|
+
return readdirSync(directory).flatMap((entry) => {
|
|
46
|
+
const path = join(directory, entry)
|
|
47
|
+
if (statSync(path).isDirectory()) return runtimeFiles(path)
|
|
48
|
+
return /\.(?:ts|tsx)$/.test(path) && !/\.test\.(?:ts|tsx)$/.test(path) ? [path] : []
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const studioPath = (path: string) => relative(CLI_ROOT, path).replaceAll('\\', '/')
|
|
53
|
+
|
|
32
54
|
describe('studio server runtime deps', () => {
|
|
33
|
-
const deps = new Set(
|
|
34
|
-
Object.keys(
|
|
35
|
-
(
|
|
36
|
-
JSON.parse(readFileSync(join(CLI_ROOT, 'package.json'), 'utf8')) as {
|
|
37
|
-
dependencies?: Record<string, string>
|
|
38
|
-
}
|
|
39
|
-
).dependencies ?? {},
|
|
40
|
-
),
|
|
41
|
-
)
|
|
55
|
+
const deps = new Set(Object.keys(cliPackage.dependencies ?? {}))
|
|
42
56
|
|
|
43
57
|
const project = new Project({ skipAddingFilesFromTsConfig: true })
|
|
44
58
|
project.addSourceFilesAtPaths([
|
|
45
59
|
join(CLI_ROOT, 'studio/server/**/*.ts'),
|
|
60
|
+
join(CLI_ROOT, 'studio/server/**/*.tsx'),
|
|
46
61
|
join(CLI_ROOT, 'studio/shared/**/*.ts'),
|
|
62
|
+
join(CLI_ROOT, 'studio/shared/**/*.tsx'),
|
|
47
63
|
])
|
|
64
|
+
const runtimeSourceFiles = project
|
|
65
|
+
.getSourceFiles()
|
|
66
|
+
.filter((sourceFile) => !/\.test\.(?:ts|tsx)$/.test(sourceFile.getBaseName()))
|
|
48
67
|
|
|
49
68
|
// getImportStringLiterals covers static imports, re-exports, and dynamic import().
|
|
50
69
|
const offenders: Record<string, string[]> = {}
|
|
51
|
-
for (const sf of
|
|
70
|
+
for (const sf of runtimeSourceFiles) {
|
|
52
71
|
const specs = sf.getImportStringLiterals().map((l) => l.getLiteralValue())
|
|
53
72
|
for (const spec of specs) {
|
|
54
73
|
if (spec.startsWith('.') || spec.startsWith('/') || isBuiltin(spec)) continue
|
|
@@ -66,9 +85,27 @@ describe('studio server runtime deps', () => {
|
|
|
66
85
|
).toEqual({})
|
|
67
86
|
})
|
|
68
87
|
|
|
69
|
-
test('
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
test('parses every shipped Studio server and shared runtime source', () => {
|
|
89
|
+
const expected = [
|
|
90
|
+
...runtimeFiles(join(CLI_ROOT, 'studio/server')),
|
|
91
|
+
...runtimeFiles(join(CLI_ROOT, 'studio/shared')),
|
|
92
|
+
]
|
|
93
|
+
.map(studioPath)
|
|
94
|
+
.sort()
|
|
95
|
+
const parsed = runtimeSourceFiles
|
|
96
|
+
.map((sourceFile) => studioPath(sourceFile.getFilePath()))
|
|
97
|
+
.sort()
|
|
98
|
+
|
|
99
|
+
expect(parsed).toEqual(expected)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('studio test gate', () => {
|
|
104
|
+
test('the root and Studio suites include all client, server, and shared tests', () => {
|
|
105
|
+
expect(cliPackage.scripts?.test).toBe(
|
|
106
|
+
'node scripts/qualification/source-boundary.mjs --target && bun test src studio/client/src studio/server studio/shared && node --test scripts/*.test.mjs',
|
|
107
|
+
)
|
|
108
|
+
expect(cliPackage.scripts?.['test:studio']).toBe('pnpm --dir studio test')
|
|
109
|
+
expect(studioPackage.scripts?.test).toBe('bun test client/src server shared')
|
|
73
110
|
})
|
|
74
111
|
})
|