@toa.io/extensions.introspection 1.0.0-alpha.262
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/CHANGELOG.md +61 -0
- package/components/introspection.edges/manifest.toa.yaml +48 -0
- package/components/introspection.edges/operations/lib/types.d.ts +28 -0
- package/components/introspection.edges/operations/lib/types.js +3 -0
- package/components/introspection.edges/operations/lib/types.js.map +1 -0
- package/components/introspection.edges/operations/merge.d.ts +9 -0
- package/components/introspection.edges/operations/merge.js +25 -0
- package/components/introspection.edges/operations/merge.js.map +1 -0
- package/components/introspection.edges/source/lib/types.ts +33 -0
- package/components/introspection.edges/source/merge.ts +26 -0
- package/components/introspection.edges/tsconfig.json +9 -0
- package/components/introspection.nodes/manifest.toa.yaml +49 -0
- package/components/introspection.nodes/operations/lib/types.d.ts +16 -0
- package/components/introspection.nodes/operations/lib/types.js +3 -0
- package/components/introspection.nodes/operations/lib/types.js.map +1 -0
- package/components/introspection.nodes/operations/merge.d.ts +9 -0
- package/components/introspection.nodes/operations/merge.js +21 -0
- package/components/introspection.nodes/operations/merge.js.map +1 -0
- package/components/introspection.nodes/source/lib/types.ts +18 -0
- package/components/introspection.nodes/source/merge.ts +21 -0
- package/components/introspection.nodes/tsconfig.json +9 -0
- package/cucumber.js +8 -0
- package/features/calls.feature +123 -0
- package/features/nodes.feature +70 -0
- package/features/samples.feature +75 -0
- package/features/shutdown.feature +37 -0
- package/features/site/_app/immutable/asset.js +1 -0
- package/features/site/index.html +6 -0
- package/features/steps/Map.ts +151 -0
- package/features/steps/UI.ts +88 -0
- package/features/steps/components/probe.discreet/manifest.toa.yaml +12 -0
- package/features/steps/components/probe.discreet/operations/ping.js +7 -0
- package/features/steps/components/probe.quiet/manifest.toa.yaml +11 -0
- package/features/steps/components/probe.quiet/operations/ping.js +7 -0
- package/features/steps/components/probe.source/manifest.toa.yaml +27 -0
- package/features/steps/components/probe.source/operations/crash.js +7 -0
- package/features/steps/components/probe.source/operations/fail.js +12 -0
- package/features/steps/components/probe.source/operations/relay.js +7 -0
- package/features/steps/components/probe.target/manifest.toa.yaml +24 -0
- package/features/steps/components/probe.target/operations/compute.js +7 -0
- package/features/steps/components/probe.target/operations/count.js +9 -0
- package/features/steps/config.ts +20 -0
- package/features/steps/tsconfig.json +9 -0
- package/features/ui.feature +85 -0
- package/package.json +31 -0
- package/readme.md +141 -0
- package/schemas/annotation.cos.yaml +16 -0
- package/schemas/declaration.cos.yaml +2 -0
- package/source/Composition.ts +61 -0
- package/source/Explorer.ts +16 -0
- package/source/Factory.ts +119 -0
- package/source/Reporter.ts +210 -0
- package/source/Tenant.ts +47 -0
- package/source/UI.ts +197 -0
- package/source/annotation.ts +83 -0
- package/source/const.ts +32 -0
- package/source/describe.ts +68 -0
- package/source/extension.ts +45 -0
- package/source/index.ts +7 -0
- package/source/keys.ts +0 -0
- package/source/manifest.ts +19 -0
- package/source/model.ts +82 -0
- package/source/sample.ts +49 -0
- package/source/schemas.ts +10 -0
- package/transpiled/Composition.d.ts +21 -0
- package/transpiled/Composition.js +48 -0
- package/transpiled/Composition.js.map +1 -0
- package/transpiled/Explorer.d.ts +9 -0
- package/transpiled/Explorer.js +19 -0
- package/transpiled/Explorer.js.map +1 -0
- package/transpiled/Factory.d.ts +22 -0
- package/transpiled/Factory.js +93 -0
- package/transpiled/Factory.js.map +1 -0
- package/transpiled/Reporter.d.ts +44 -0
- package/transpiled/Reporter.js +187 -0
- package/transpiled/Reporter.js.map +1 -0
- package/transpiled/Tenant.d.ts +18 -0
- package/transpiled/Tenant.js +40 -0
- package/transpiled/Tenant.js.map +1 -0
- package/transpiled/UI.d.ts +25 -0
- package/transpiled/UI.js +186 -0
- package/transpiled/UI.js.map +1 -0
- package/transpiled/annotation.d.ts +40 -0
- package/transpiled/annotation.js +46 -0
- package/transpiled/annotation.js.map +1 -0
- package/transpiled/const.d.ts +23 -0
- package/transpiled/const.js +27 -0
- package/transpiled/const.js.map +1 -0
- package/transpiled/contracts.d.ts +51 -0
- package/transpiled/contracts.js +53 -0
- package/transpiled/contracts.js.map +1 -0
- package/transpiled/describe.d.ts +9 -0
- package/transpiled/describe.js +64 -0
- package/transpiled/describe.js.map +1 -0
- package/transpiled/extension.d.ts +11 -0
- package/transpiled/extension.js +64 -0
- package/transpiled/extension.js.map +1 -0
- package/transpiled/index.d.ts +6 -0
- package/transpiled/index.js +25 -0
- package/transpiled/index.js.map +1 -0
- package/transpiled/keys.d.ts +9 -0
- package/transpiled/keys.js +0 -0
- package/transpiled/keys.js.map +1 -0
- package/transpiled/manifest.d.ts +9 -0
- package/transpiled/manifest.js +43 -0
- package/transpiled/manifest.js.map +1 -0
- package/transpiled/model.d.ts +71 -0
- package/transpiled/model.js +3 -0
- package/transpiled/model.js.map +1 -0
- package/transpiled/sample.d.ts +7 -0
- package/transpiled/sample.js +44 -0
- package/transpiled/sample.js.map +1 -0
- package/transpiled/schemas.d.ts +4 -0
- package/transpiled/schemas.js +13 -0
- package/transpiled/schemas.js.map +1 -0
- package/tsconfig.json +12 -0
- package/ui/dist/_app/env.js +1 -0
- package/ui/dist/_app/immutable/assets/0.fg2cP6bO.css +2 -0
- package/ui/dist/_app/immutable/assets/2.BghF56l_.css +1 -0
- package/ui/dist/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
- package/ui/dist/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/ui/dist/_app/immutable/chunks/CIvRtvVA.js +1 -0
- package/ui/dist/_app/immutable/chunks/D1jNriZ8.js +1 -0
- package/ui/dist/_app/immutable/chunks/DINmginf.js +1 -0
- package/ui/dist/_app/immutable/chunks/DKZCAkrF.js +1 -0
- package/ui/dist/_app/immutable/chunks/DLG2JvyF.js +6 -0
- package/ui/dist/_app/immutable/chunks/DSi-FjwI.js +2 -0
- package/ui/dist/_app/immutable/chunks/DdoZuyU1.js +1 -0
- package/ui/dist/_app/immutable/chunks/O9IjE_TH.js +1 -0
- package/ui/dist/_app/immutable/chunks/PUjOYRcs.js +3 -0
- package/ui/dist/_app/immutable/chunks/QKD4Mgck.js +5 -0
- package/ui/dist/_app/immutable/chunks/Ux-r8C9U.js +1 -0
- package/ui/dist/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/ui/dist/_app/immutable/entry/app.CTY34L35.js +2 -0
- package/ui/dist/_app/immutable/entry/start.Bq2Gn1Ax.js +1 -0
- package/ui/dist/_app/immutable/nodes/0.lOXtBRjQ.js +1 -0
- package/ui/dist/_app/immutable/nodes/1.r8ESZdgM.js +1 -0
- package/ui/dist/_app/immutable/nodes/2.DuVj0loz.js +8 -0
- package/ui/dist/_app/immutable/nodes/3.DhdkQ2Q1.js +1 -0
- package/ui/dist/_app/immutable/nodes/4.DUAEZdRr.js +1 -0
- package/ui/dist/_app/immutable/nodes/5.CIVn_xgq.js +1 -0
- package/ui/dist/_app/version.json +1 -0
- package/ui/dist/index.html +48 -0
- package/ui/dist/robots.txt +3 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Feature: Payload samples
|
|
2
|
+
|
|
3
|
+
Samples are real user data, so they are captured only when the context and the
|
|
4
|
+
component both allow it. Either level can veto.
|
|
5
|
+
|
|
6
|
+
Scenario: Nothing is captured by default
|
|
7
|
+
When the `probe.target.compute` is called with:
|
|
8
|
+
"""yaml
|
|
9
|
+
input:
|
|
10
|
+
a: 4
|
|
11
|
+
b: 5
|
|
12
|
+
"""
|
|
13
|
+
Then the map contains an edge:
|
|
14
|
+
"""yaml
|
|
15
|
+
dst:
|
|
16
|
+
namespace: probe
|
|
17
|
+
component: target
|
|
18
|
+
operation: compute
|
|
19
|
+
"""
|
|
20
|
+
And the map contains no edge:
|
|
21
|
+
"""yaml
|
|
22
|
+
dst:
|
|
23
|
+
namespace: probe
|
|
24
|
+
component: target
|
|
25
|
+
operation: compute
|
|
26
|
+
sample:
|
|
27
|
+
input:
|
|
28
|
+
a: 4
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@samples
|
|
32
|
+
Scenario: The context enables capturing
|
|
33
|
+
When the `probe.target.compute` is called with:
|
|
34
|
+
"""yaml
|
|
35
|
+
input:
|
|
36
|
+
a: 4
|
|
37
|
+
b: 5
|
|
38
|
+
"""
|
|
39
|
+
Then the map contains an edge:
|
|
40
|
+
"""yaml
|
|
41
|
+
dst:
|
|
42
|
+
namespace: probe
|
|
43
|
+
component: target
|
|
44
|
+
operation: compute
|
|
45
|
+
sample:
|
|
46
|
+
outcome: ok
|
|
47
|
+
input:
|
|
48
|
+
a: 4
|
|
49
|
+
b: 5
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@samples
|
|
53
|
+
Scenario: A component vetoes capturing for itself
|
|
54
|
+
When the `probe.discreet.ping` is called with:
|
|
55
|
+
"""yaml
|
|
56
|
+
input:
|
|
57
|
+
a: 7
|
|
58
|
+
"""
|
|
59
|
+
Then the map contains an edge:
|
|
60
|
+
"""yaml
|
|
61
|
+
dst:
|
|
62
|
+
namespace: probe
|
|
63
|
+
component: discreet
|
|
64
|
+
operation: ping
|
|
65
|
+
"""
|
|
66
|
+
And the map contains no edge:
|
|
67
|
+
"""yaml
|
|
68
|
+
dst:
|
|
69
|
+
namespace: probe
|
|
70
|
+
component: discreet
|
|
71
|
+
operation: ping
|
|
72
|
+
sample:
|
|
73
|
+
input:
|
|
74
|
+
a: 7
|
|
75
|
+
"""
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@shutdown
|
|
2
|
+
Feature: Graceful shutdown
|
|
3
|
+
|
|
4
|
+
A component reports on a timer, so whatever it observed since the last report
|
|
5
|
+
would be lost on a deploy. Stopping flushes it instead: the collector drains
|
|
6
|
+
its buffer while its remotes are still connected.
|
|
7
|
+
|
|
8
|
+
This suite runs with a flush period long enough that the timer cannot fire,
|
|
9
|
+
so nothing here reaches the map unless the shutdown does it.
|
|
10
|
+
|
|
11
|
+
Scenario: Observations are flushed on shutdown
|
|
12
|
+
When the `probe.source.relay` is called with:
|
|
13
|
+
"""yaml
|
|
14
|
+
input:
|
|
15
|
+
a: 2
|
|
16
|
+
b: 3
|
|
17
|
+
"""
|
|
18
|
+
And the components are stopped
|
|
19
|
+
Then the map contains an edge:
|
|
20
|
+
"""yaml
|
|
21
|
+
src:
|
|
22
|
+
namespace: probe
|
|
23
|
+
component: source
|
|
24
|
+
operation: relay
|
|
25
|
+
dst:
|
|
26
|
+
namespace: probe
|
|
27
|
+
component: target
|
|
28
|
+
operation: compute
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
Scenario: Component descriptions are flushed on shutdown
|
|
32
|
+
When the components are stopped
|
|
33
|
+
Then the map contains a node:
|
|
34
|
+
"""yaml
|
|
35
|
+
namespace: probe
|
|
36
|
+
component: target
|
|
37
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const hashed = true
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { readdirSync } from 'node:fs'
|
|
4
|
+
import { setTimeout } from 'node:timers/promises'
|
|
5
|
+
import { MongoClient } from 'mongodb'
|
|
6
|
+
import { after, before, binding, then, when } from 'cucumber-tsflow'
|
|
7
|
+
import { parse } from '@toa.io/yaml'
|
|
8
|
+
import { match } from '@toa.io/generic'
|
|
9
|
+
import * as boot from '@toa.io/boot'
|
|
10
|
+
import * as stage from '@toa.io/userland/stage'
|
|
11
|
+
import { Factory } from '../../source'
|
|
12
|
+
import type { Component, Connector, Request } from '@toa.io/core'
|
|
13
|
+
|
|
14
|
+
@binding()
|
|
15
|
+
export class Map {
|
|
16
|
+
private service: Connector | null = null
|
|
17
|
+
private composition: Connector | null = null
|
|
18
|
+
private remotes: Record<string, Component> = {}
|
|
19
|
+
|
|
20
|
+
@when('the `{word}` is called with:')
|
|
21
|
+
public async call (endpoint: string, yaml: string): Promise<void> {
|
|
22
|
+
const request = parse<Request>(yaml)
|
|
23
|
+
const [operation, component, namespace = 'default'] = endpoint.split('.').reverse()
|
|
24
|
+
|
|
25
|
+
// an operation may throw, and that is exactly what one of the scenarios is about
|
|
26
|
+
await this.invoke(`${namespace}.${component}`, operation, request).catch(() => undefined)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@then('the map contains a node:')
|
|
30
|
+
public async node (yaml: string): Promise<void> {
|
|
31
|
+
await this.eventually('introspection.nodes', parse(yaml))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@then('the map contains an edge:')
|
|
35
|
+
public async edge (yaml: string): Promise<void> {
|
|
36
|
+
await this.eventually('introspection.edges', parse(yaml))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@then('the map contains no node:')
|
|
40
|
+
public async noNode (yaml: string): Promise<void> {
|
|
41
|
+
await this.never('introspection.nodes', parse(yaml))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@then('the map contains no edge:')
|
|
45
|
+
public async noEdge (yaml: string): Promise<void> {
|
|
46
|
+
await this.never('introspection.edges', parse(yaml))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@when('the components are stopped')
|
|
50
|
+
public async stop (): Promise<void> {
|
|
51
|
+
await this.composition?.disconnect()
|
|
52
|
+
|
|
53
|
+
this.composition = null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@before('not @ui')
|
|
57
|
+
public async run (): Promise<void> {
|
|
58
|
+
await clean()
|
|
59
|
+
|
|
60
|
+
this.service = new Factory(boot).service()!
|
|
61
|
+
|
|
62
|
+
await this.service.connect()
|
|
63
|
+
|
|
64
|
+
// booted here rather than through the stage, so that a scenario can stop it on its own
|
|
65
|
+
this.composition = await boot.composition(components())
|
|
66
|
+
|
|
67
|
+
await this.composition.connect()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@after('not @ui')
|
|
71
|
+
public async shutdown (): Promise<void> {
|
|
72
|
+
this.remotes = {}
|
|
73
|
+
|
|
74
|
+
await this.composition?.disconnect()
|
|
75
|
+
await stage.shutdown()
|
|
76
|
+
await this.service?.disconnect()
|
|
77
|
+
|
|
78
|
+
this.composition = null
|
|
79
|
+
this.service = null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async invoke (id: string, operation: string, request: Request): Promise<unknown> {
|
|
83
|
+
this.remotes[id] ??= await stage.remote(id)
|
|
84
|
+
|
|
85
|
+
return await this.remotes[id].invoke(operation, request)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The map is eventually consistent: the collector buffers, flushes, and the task is queued. */
|
|
89
|
+
private async eventually (id: string, expected: object): Promise<void> {
|
|
90
|
+
const deadline = Date.now() + DEADLINE
|
|
91
|
+
|
|
92
|
+
let records: any[] = []
|
|
93
|
+
|
|
94
|
+
while (Date.now() < deadline) {
|
|
95
|
+
records = await this.list(id)
|
|
96
|
+
|
|
97
|
+
if (records.some((record) => match(record, expected)))
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
await setTimeout(POLL)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
assert.fail(`No record in '${id}' matches\n${JSON.stringify(expected, null, 2)}\n\n` +
|
|
104
|
+
`Present:\n${JSON.stringify(records, null, 2)}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Absence has to outlive a flush, otherwise it only proves the map is slow. */
|
|
108
|
+
private async never (id: string, expected: object): Promise<void> {
|
|
109
|
+
await setTimeout(SETTLE)
|
|
110
|
+
|
|
111
|
+
const records = await this.list(id)
|
|
112
|
+
const found = records.find((record) => match(record, expected))
|
|
113
|
+
|
|
114
|
+
assert.equal(found, undefined,
|
|
115
|
+
`Unexpected record in '${id}':\n${JSON.stringify(found, null, 2)}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async list (id: string): Promise<any[]> {
|
|
119
|
+
return await this.invoke(id, 'enumerate', { query: { limit: 100 } }) as any[]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function clean (): Promise<void> {
|
|
124
|
+
const client = new MongoClient(URL)
|
|
125
|
+
|
|
126
|
+
await client.connect()
|
|
127
|
+
|
|
128
|
+
const db = client.db(DB)
|
|
129
|
+
|
|
130
|
+
await Promise.all([
|
|
131
|
+
db.collection('introspection_nodes').deleteMany({}),
|
|
132
|
+
db.collection('introspection_edges').deleteMany({})
|
|
133
|
+
])
|
|
134
|
+
|
|
135
|
+
await client.close()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function components (): string[] {
|
|
139
|
+
const entries = readdirSync(ROOT, { withFileTypes: true })
|
|
140
|
+
|
|
141
|
+
return entries
|
|
142
|
+
.filter((entry) => entry.isDirectory())
|
|
143
|
+
.map((entry) => resolve(ROOT, entry.name))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const ROOT = resolve(__dirname, 'components')
|
|
147
|
+
const URL = 'mongodb://developer:secret@localhost:27017'
|
|
148
|
+
const DB = 'toa-dev'
|
|
149
|
+
const DEADLINE = 20_000
|
|
150
|
+
const POLL = 250
|
|
151
|
+
const SETTLE = 4_000
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import * as http from 'node:http'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { after, binding, given, then, when } from 'cucumber-tsflow'
|
|
5
|
+
import { UI } from '../../source/UI'
|
|
6
|
+
import { UI_PORT } from '../../source/const'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The scenarios are about the server, not about the page: it is pointed at a fixture
|
|
10
|
+
* directory, so that running them never needs a UI build.
|
|
11
|
+
*/
|
|
12
|
+
@binding()
|
|
13
|
+
export class Site {
|
|
14
|
+
private server: UI | null = null
|
|
15
|
+
private response: Response | null = null
|
|
16
|
+
|
|
17
|
+
@given('the UI is published')
|
|
18
|
+
public async publish (): Promise<void> {
|
|
19
|
+
this.server = new UI(UI_PORT, resolve(__dirname, '..', 'site'))
|
|
20
|
+
|
|
21
|
+
await this.server.connect()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@when('{string} is requested')
|
|
25
|
+
public async request (path: string): Promise<void> {
|
|
26
|
+
this.response = await get(path)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@then('the status is {int}')
|
|
30
|
+
public statusIs (status: number): void {
|
|
31
|
+
assert.equal(this.response?.status, status)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@then('the body contains {string}')
|
|
35
|
+
public bodyContains (text: string): void {
|
|
36
|
+
assert.ok(this.response?.body.includes(text),
|
|
37
|
+
`Expected the body to contain '${text}', got '${this.response?.body}'`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@then('the body is empty')
|
|
41
|
+
public bodyIsEmpty (): void {
|
|
42
|
+
assert.equal(this.response?.body, '')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@then('the {string} header is {string}')
|
|
46
|
+
public headerIs (name: string, value: string): void {
|
|
47
|
+
assert.equal(this.response?.headers[name], value)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@after()
|
|
51
|
+
public async shutdown (): Promise<void> {
|
|
52
|
+
await this.server?.disconnect()
|
|
53
|
+
|
|
54
|
+
this.server = null
|
|
55
|
+
this.response = null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Raw `http`, not `fetch`: the URL parser normalizes `%2e%2e` away, and one of the
|
|
61
|
+
* scenarios is about exactly that segment reaching the server.
|
|
62
|
+
*/
|
|
63
|
+
async function get (path: string): Promise<Response> {
|
|
64
|
+
return await new Promise((resolve, reject) => {
|
|
65
|
+
// `agent: false` — a keep-alive socket outlives the scenario that opened it
|
|
66
|
+
const request = http.get({ port: UI_PORT, path, agent: false }, (response) => {
|
|
67
|
+
let body = ''
|
|
68
|
+
|
|
69
|
+
response.setEncoding('utf8')
|
|
70
|
+
response.on('data', (chunk: string) => (body += chunk))
|
|
71
|
+
response.on('end', () => {
|
|
72
|
+
resolve({
|
|
73
|
+
status: response.statusCode!,
|
|
74
|
+
headers: response.headers as Record<string, string>,
|
|
75
|
+
body
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
request.on('error', reject)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface Response {
|
|
85
|
+
status: number
|
|
86
|
+
headers: Record<string, string>
|
|
87
|
+
body: string
|
|
88
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
namespace: probe
|
|
2
|
+
name: source
|
|
3
|
+
|
|
4
|
+
entity:
|
|
5
|
+
associated: true
|
|
6
|
+
storage: null
|
|
7
|
+
schema:
|
|
8
|
+
value:
|
|
9
|
+
type: integer
|
|
10
|
+
default: 0
|
|
11
|
+
secret: string
|
|
12
|
+
|
|
13
|
+
operations:
|
|
14
|
+
transit:
|
|
15
|
+
concurrency: retry
|
|
16
|
+
input:
|
|
17
|
+
value: .
|
|
18
|
+
secret: .
|
|
19
|
+
relay:
|
|
20
|
+
input:
|
|
21
|
+
a: number
|
|
22
|
+
b: number
|
|
23
|
+
output: number
|
|
24
|
+
crash: {}
|
|
25
|
+
fail:
|
|
26
|
+
errors:
|
|
27
|
+
- NOPE
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
namespace: probe
|
|
2
|
+
name: target
|
|
3
|
+
|
|
4
|
+
entity:
|
|
5
|
+
storage: null
|
|
6
|
+
schema:
|
|
7
|
+
counted:
|
|
8
|
+
type: integer
|
|
9
|
+
default: 0
|
|
10
|
+
|
|
11
|
+
operations:
|
|
12
|
+
compute:
|
|
13
|
+
input:
|
|
14
|
+
a: number
|
|
15
|
+
b: number
|
|
16
|
+
output: number
|
|
17
|
+
count:
|
|
18
|
+
concurrency: retry
|
|
19
|
+
input:
|
|
20
|
+
type: object
|
|
21
|
+
additionalProperties: true
|
|
22
|
+
|
|
23
|
+
receivers:
|
|
24
|
+
probe.source.created: count
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { setDefaultTimeout } from '@cucumber/cucumber'
|
|
2
|
+
import { encode } from '@toa.io/generic'
|
|
3
|
+
|
|
4
|
+
process.env.TOA_DEV = '1'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* The extension factory reads the environment once, and the bootloader caches
|
|
8
|
+
* factories per process — so the context level of the annotation is fixed for
|
|
9
|
+
* a whole run. `npm run features` therefore makes a pass per configuration.
|
|
10
|
+
*/
|
|
11
|
+
process.env.TOA_INTROSPECTION = encode({
|
|
12
|
+
samples: process.env.TOA_INTROSPECTION_SAMPLES === '1',
|
|
13
|
+
interval: Number(process.env.TOA_INTROSPECTION_INTERVAL ?? 1),
|
|
14
|
+
threshold: 64,
|
|
15
|
+
|
|
16
|
+
// `ui.feature` starts its own server; the explorer must not take the port first
|
|
17
|
+
ui: false
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
setDefaultTimeout(60 * 1000)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
@ui
|
|
2
|
+
Feature: Publishing the UI
|
|
3
|
+
|
|
4
|
+
The explorer serves the UI on its own port, out of the directory `ui` builds. The
|
|
5
|
+
mount path is baked into the bundle and forwarded by the ingress, so the server
|
|
6
|
+
routes relative to it.
|
|
7
|
+
|
|
8
|
+
Scenario: The root is the way in
|
|
9
|
+
Given the UI is published
|
|
10
|
+
When "/" is requested
|
|
11
|
+
Then the status is 302
|
|
12
|
+
And the "location" header is "/.introspection/"
|
|
13
|
+
|
|
14
|
+
Scenario: The UI answers at its mount path
|
|
15
|
+
Given the UI is published
|
|
16
|
+
When "/.introspection" is requested
|
|
17
|
+
Then the status is 200
|
|
18
|
+
And the body contains "the page"
|
|
19
|
+
|
|
20
|
+
Scenario: A trailing slash is the same resource
|
|
21
|
+
Given the UI is published
|
|
22
|
+
When "/.introspection/" is requested
|
|
23
|
+
Then the status is 200
|
|
24
|
+
And the body contains "the page"
|
|
25
|
+
|
|
26
|
+
Scenario: A query string does not change the resource
|
|
27
|
+
Given the UI is published
|
|
28
|
+
When "/.introspection?tab=edges" is requested
|
|
29
|
+
Then the status is 200
|
|
30
|
+
And the body contains "the page"
|
|
31
|
+
|
|
32
|
+
Scenario: An unknown route is the page
|
|
33
|
+
The client router knows what routes there are; this server does not.
|
|
34
|
+
|
|
35
|
+
Given the UI is published
|
|
36
|
+
When "/.introspection/whatever" is requested
|
|
37
|
+
Then the status is 200
|
|
38
|
+
And the body contains "the page"
|
|
39
|
+
|
|
40
|
+
Scenario: A route that looks like a file is still a route
|
|
41
|
+
A component is named `namespace.component`, so the address of one carries a dot and
|
|
42
|
+
an extension is no way to tell an asset from a route.
|
|
43
|
+
|
|
44
|
+
Given the UI is published
|
|
45
|
+
When "/.introspection/identity.passkeys/" is requested
|
|
46
|
+
Then the status is 200
|
|
47
|
+
And the body contains "the page"
|
|
48
|
+
|
|
49
|
+
Scenario: And with no trailing slash to give it away
|
|
50
|
+
Given the UI is published
|
|
51
|
+
When "/.introspection/identity.passkeys" is requested
|
|
52
|
+
Then the status is 200
|
|
53
|
+
And the body contains "the page"
|
|
54
|
+
|
|
55
|
+
Scenario: A hashed asset is served forever
|
|
56
|
+
Given the UI is published
|
|
57
|
+
When "/.introspection/_app/immutable/asset.js" is requested
|
|
58
|
+
Then the status is 200
|
|
59
|
+
And the "content-type" header is "text/javascript; charset=utf-8"
|
|
60
|
+
And the "cache-control" header is "public, max-age=31536000, immutable"
|
|
61
|
+
|
|
62
|
+
Scenario: The page itself is never cached
|
|
63
|
+
Given the UI is published
|
|
64
|
+
When "/.introspection/" is requested
|
|
65
|
+
Then the "cache-control" header is "no-cache"
|
|
66
|
+
|
|
67
|
+
Scenario: A missing asset is missing
|
|
68
|
+
Given the UI is published
|
|
69
|
+
When "/.introspection/_app/immutable/absent.js" is requested
|
|
70
|
+
Then the status is 404
|
|
71
|
+
|
|
72
|
+
Scenario: The path of the map API is not served by the UI
|
|
73
|
+
Given the UI is published
|
|
74
|
+
When "/introspection/nodes/" is requested
|
|
75
|
+
Then the status is 404
|
|
76
|
+
|
|
77
|
+
Scenario: A path that only looks like the mount path is absent
|
|
78
|
+
Given the UI is published
|
|
79
|
+
When "/.introspectionable" is requested
|
|
80
|
+
Then the status is 404
|
|
81
|
+
|
|
82
|
+
Scenario: Nothing outside the site is reachable
|
|
83
|
+
Given the UI is published
|
|
84
|
+
When "/.introspection/%2e%2e/%2e%2e/package.json" is requested
|
|
85
|
+
Then the status is 404
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toa.io/extensions.introspection",
|
|
3
|
+
"version": "1.0.0-alpha.262",
|
|
4
|
+
"description": "Toa Introspection",
|
|
5
|
+
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
|
+
"homepage": "https://github.com/toa-io/toa#readme",
|
|
7
|
+
"main": "transpiled/index.js",
|
|
8
|
+
"types": "transpiled/index.d.ts",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/toa-io/toa.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/toa-io/toa/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@toa.io/core": "1.0.0-alpha.262",
|
|
21
|
+
"@toa.io/generic": "1.0.0-alpha.254",
|
|
22
|
+
"@toa.io/schemas": "1.0.0-alpha.254",
|
|
23
|
+
"openspan": "1.0.0-alpha.254"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"transpile": "npx tsc && npx tsc -p ./components/introspection.nodes && npx tsc -p ./components/introspection.edges",
|
|
27
|
+
"build:ui": "npm --prefix ui ci && npm --prefix ui run build",
|
|
28
|
+
"prepack": "npm run build:ui",
|
|
29
|
+
"features": "npx cucumber-js --tags 'not @samples and not @shutdown' && TOA_INTROSPECTION_SAMPLES=1 npx cucumber-js --tags @samples && TOA_INTROSPECTION_INTERVAL=3600 npx cucumber-js --tags @shutdown"
|
|
30
|
+
}
|
|
31
|
+
}
|