@owlmeans/viable-common 0.0.11 → 0.0.13
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 +68 -1
- package/agent-meta/manifest.json +23 -0
- package/agent-meta/skills/agent-presentation/SKILL.md +62 -0
- package/agent-meta/skills/viable-common/SKILL.md +180 -0
- package/build/agent/index.d.ts +1 -0
- package/build/agent/index.d.ts.map +1 -1
- package/build/agent/index.js +1 -0
- package/build/agent/index.js.map +1 -1
- package/build/agent/presentation.d.ts +102 -0
- package/build/agent/presentation.d.ts.map +1 -0
- package/build/agent/presentation.js +303 -0
- package/build/agent/presentation.js.map +1 -0
- package/build/blueprint/case.js +3 -3
- package/build/connect/consts.d.ts +93 -5
- package/build/connect/consts.d.ts.map +1 -1
- package/build/connect/consts.js +90 -1
- package/build/connect/consts.js.map +1 -1
- package/build/connect/entrypoints.d.ts +208 -4
- package/build/connect/entrypoints.d.ts.map +1 -1
- package/build/connect/entrypoints.js +61 -33
- package/build/connect/entrypoints.js.map +1 -1
- package/build/connect/errors.d.ts +20 -0
- package/build/connect/errors.d.ts.map +1 -1
- package/build/connect/errors.js +39 -0
- package/build/connect/errors.js.map +1 -1
- package/build/connect/ops.d.ts +46 -2
- package/build/connect/ops.d.ts.map +1 -1
- package/build/connect/references.d.ts +60 -1
- package/build/connect/references.d.ts.map +1 -1
- package/build/connect/references.js +16 -0
- package/build/connect/references.js.map +1 -1
- package/build/connect/schemas.d.ts +17 -2
- package/build/connect/schemas.d.ts.map +1 -1
- package/build/connect/schemas.js +91 -7
- package/build/connect/schemas.js.map +1 -1
- package/build/connect/types.d.ts +125 -1
- package/build/connect/types.d.ts.map +1 -1
- package/build/convert/consts.d.ts +539 -0
- package/build/convert/consts.d.ts.map +1 -0
- package/build/convert/consts.js +630 -0
- package/build/convert/consts.js.map +1 -0
- package/build/convert/helpers.d.ts +91 -0
- package/build/convert/helpers.d.ts.map +1 -0
- package/build/convert/helpers.js +301 -0
- package/build/convert/helpers.js.map +1 -0
- package/build/convert/index.d.ts +11 -0
- package/build/convert/index.d.ts.map +1 -0
- package/build/convert/index.js +4 -0
- package/build/convert/index.js.map +1 -0
- package/build/convert/schemas.d.ts +62 -0
- package/build/convert/schemas.d.ts.map +1 -0
- package/build/convert/schemas.js +626 -0
- package/build/convert/schemas.js.map +1 -0
- package/build/convert/types.d.ts +683 -0
- package/build/convert/types.d.ts.map +1 -0
- package/build/convert/types.js +2 -0
- package/build/convert/types.js.map +1 -0
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/skills/catalogue.d.ts.map +1 -1
- package/build/skills/catalogue.js +76 -34
- package/build/skills/catalogue.js.map +1 -1
- package/build/slot/consts.d.ts +73 -2
- package/build/slot/consts.d.ts.map +1 -1
- package/build/slot/consts.js +105 -5
- package/build/slot/consts.js.map +1 -1
- package/build/slot/schemas.d.ts +9 -1
- package/build/slot/schemas.d.ts.map +1 -1
- package/build/slot/schemas.js +18 -0
- package/build/slot/schemas.js.map +1 -1
- package/build/slot/types.d.ts +71 -0
- package/build/slot/types.d.ts.map +1 -1
- package/package.json +15 -7
- package/src/agent/index.ts +1 -0
- package/src/agent/presentation.ts +335 -0
- package/src/blueprint/case.ts +3 -3
- package/src/connect/consts.ts +93 -1
- package/src/connect/entrypoints.ts +113 -40
- package/src/connect/errors.ts +44 -0
- package/src/connect/ops.ts +50 -2
- package/src/connect/references.ts +53 -5
- package/src/connect/schemas.ts +102 -13
- package/src/connect/types.ts +130 -2
- package/src/convert/consts.ts +672 -0
- package/src/convert/helpers.ts +340 -0
- package/src/convert/index.ts +10 -0
- package/src/convert/schemas.ts +663 -0
- package/src/convert/types.ts +741 -0
- package/src/index.ts +1 -0
- package/src/skills/catalogue.ts +76 -34
- package/src/slot/consts.ts +112 -6
- package/src/slot/schemas.ts +20 -1
- package/src/slot/types.ts +80 -0
- package/tests/connect-convert.spec.ts +162 -0
- package/tests/connect-entrypoints.spec.ts +12 -4
- package/tests/connect-errors.spec.ts +22 -0
- package/tests/convert.spec.ts +318 -0
- package/tests/presentation.spec.ts +85 -0
- package/tests/skills.spec.ts +47 -0
- /package/tests/{design.test.ts → design.spec.ts} +0 -0
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from './topology/index.js'
|
|
|
16
16
|
export * from './blueprint/index.js'
|
|
17
17
|
export * from './slot/index.js'
|
|
18
18
|
export * from './connect/index.js'
|
|
19
|
+
export * from './convert/index.js'
|
|
19
20
|
export * from './scaffold/index.js'
|
|
20
21
|
export * from './design/index.js'
|
|
21
22
|
export * from './branding.js'
|
package/src/skills/catalogue.ts
CHANGED
|
@@ -108,7 +108,8 @@ them, and each has a sentinel comment marking where a new line goes:
|
|
|
108
108
|
|
|
109
109
|
- \`sources/common/src/consts.ts\` — the \`app\` alias tree.
|
|
110
110
|
- \`sources/common/src/entrypoints.ts\` — the shared immutable protocol tree (\`appEntrypoints\`).
|
|
111
|
-
- \`sources/api/src/entrypoints.ts\` — server bindings
|
|
111
|
+
- \`sources/api/src/entrypoints.ts\` — server bindings: \`bind\` + one \`handlers<Context>()\` wrap
|
|
112
|
+
per handler.
|
|
112
113
|
- \`sources/web/src/entrypoints.ts\` — client bindings (\`bindAll\` + \`bindScreen\`).
|
|
113
114
|
- \`sources/worker/src/entrypoints.ts\` — job bindings (\`bind\`).
|
|
114
115
|
- \`sources/web/src/nav.ts\` — the navigation registry: one line per screen, which
|
|
@@ -702,7 +703,7 @@ It declares the alias and the hooks that READ it, and nothing else:
|
|
|
702
703
|
import { useStoreList, useStoreModel } from '@owlmeans/client'
|
|
703
704
|
import { useContext } from '@owlmeans/web-client'
|
|
704
705
|
import type { Criteria } from '@owlmeans/resource'
|
|
705
|
-
import type { Task } from 'project-common/models/task/task
|
|
706
|
+
import type { Task } from 'project-common/models/task/task'
|
|
706
707
|
|
|
707
708
|
export const TASK_STATE = 'task-state'
|
|
708
709
|
|
|
@@ -982,7 +983,7 @@ Write this instead, and let the route handler import the function directly:
|
|
|
982
983
|
export const getSummary = async (ctx: BasicContext<BasicConfig>, guestId: string) => { ... }
|
|
983
984
|
|
|
984
985
|
// RIGHT — sources/api/src/app/dashboard/activity.ts
|
|
985
|
-
import { getSummary } from 'project-backend/models/dashboard/activity
|
|
986
|
+
import { getSummary } from 'project-backend/models/dashboard/activity'
|
|
986
987
|
const summary = await getSummary(ctx, guestId)
|
|
987
988
|
|
|
988
989
|
Do not create a service just to hold functions.
|
|
@@ -1101,37 +1102,43 @@ There is NO express here. \`express\`, \`cors\` and \`@types/express\` are not i
|
|
|
1101
1102
|
no \`app.get(...)\`, no \`req\`/\`res\` of a web server, no \`next\`, and no middleware. Anything
|
|
1102
1103
|
written for express fails to resolve.
|
|
1103
1104
|
|
|
1104
|
-
A handler is a
|
|
1105
|
-
|
|
1105
|
+
A handler is a PLAIN exported async function that RETURNS its result — named exports only, one
|
|
1106
|
+
file per entity: \`sources/api/src/app/<entity>/<action>.ts\`. It NEVER imports \`handlers\` and
|
|
1107
|
+
NEVER calls \`handlers<Context>()\`; that belongs only in the file that BINDS it:
|
|
1108
|
+
|
|
1109
|
+
// sources/api/src/app/task/create.ts — the handler module
|
|
1110
|
+
export const handleTaskCreate = async (payload: TaskCreatePayload, ctx: Context) => {
|
|
1111
|
+
const tasks = getTaskResource(ctx)
|
|
1112
|
+
return await tasks.create(payload)
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
sources/api/src/entrypoints.ts wraps that export EXACTLY ONCE, choosing the helper by what the
|
|
1116
|
+
protocol carries — the request BODY → \`api.body\`, route PARAMS (the ':' segments of the declared
|
|
1117
|
+
path) → \`api.params\`, anything else (query, headers, nothing at all) → \`api.request\`:
|
|
1106
1118
|
|
|
1107
1119
|
import { handlers } from '@owlmeans/server-api'
|
|
1108
1120
|
import { bind } from '@owlmeans/server-entrypoint'
|
|
1109
1121
|
import { protocols } from 'project-common/entrypoints'
|
|
1122
|
+
import { handleTaskCreate } from '@/app/task/create.js'
|
|
1110
1123
|
const api = handlers<Context>()
|
|
1111
1124
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
return await tasks.create(payload)
|
|
1116
|
-
})
|
|
1125
|
+
export const entrypoints = [
|
|
1126
|
+
bind(protocols.api.task.create, api.body(protocols.api.task.create, handleTaskCreate)),
|
|
1127
|
+
]
|
|
1117
1128
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1129
|
+
WRONG — the handler module ALSO calls \`handlers()\` and wraps itself, so the export bound above is
|
|
1130
|
+
wrapped a SECOND time. \`tsc\` catches it: \`TS2345 "Argument of type 'BoundEntrypointHandler<…>' is
|
|
1131
|
+
not assignable"\`. Left unfixed, the build stays clean anyway if nothing runs \`tsc\` on this
|
|
1132
|
+
package, and every request to the route then fails with \`TypeError: handler is not a function\`
|
|
1133
|
+
(\`execute is not a function\` for \`.request\`) — an endpoint that "completed" and answers nothing:
|
|
1122
1134
|
|
|
1123
|
-
//
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1135
|
+
// WRONG — sources/api/src/app/task/create.ts
|
|
1136
|
+
const api = handlers<Context>()
|
|
1137
|
+
export const handleTaskCreate = api.body(protocols.api.task.create, async (payload, ctx) => {
|
|
1138
|
+
const tasks = getTaskResource(ctx)
|
|
1139
|
+
return await tasks.create(payload)
|
|
1127
1140
|
})
|
|
1128
1141
|
|
|
1129
|
-
export const entrypoints = [
|
|
1130
|
-
bind(protocols.api.task.create, createTask),
|
|
1131
|
-
bind(protocols.api.task.get, getTask),
|
|
1132
|
-
bind(protocols.api.task.list, listTasks),
|
|
1133
|
-
]
|
|
1134
|
-
|
|
1135
1142
|
- The SECOND argument is the OwlMeans context. Reach every resource through it — never
|
|
1136
1143
|
import a database connection and never write raw SQL in a handler.
|
|
1137
1144
|
- RETURN the value. \`res.json(...)\`, \`res.status(...)\`, \`res.send(...)\` do not exist; a
|
|
@@ -1141,9 +1148,9 @@ typed helper from \`@owlmeans/server-api\`. Pick by what the handler reads:
|
|
|
1141
1148
|
200 carrying an error object instead.
|
|
1142
1149
|
- Never read a token, never check a role, never look at an \`Authorization\` header. Access is
|
|
1143
1150
|
declared on the protocol (\`guards\` / \`gate\` options) and enforced before the handler runs.
|
|
1144
|
-
- A handler is inert until
|
|
1145
|
-
\`sources/api/src/entrypoints.ts\` binds it to its protocol. Without that line the
|
|
1146
|
-
answers 404 and nothing reports an error.
|
|
1151
|
+
- A handler is inert until the \`bind(protocols.api.<name>, api.body/params/request(protocols.api.<name>, handler))\`
|
|
1152
|
+
line in \`sources/api/src/entrypoints.ts\` binds it to its protocol. Without that line the
|
|
1153
|
+
endpoint answers 404 and nothing reports an error.
|
|
1147
1154
|
- Handlers are ENTITY-SCOPED: \`sources/api/src/app/<entity>/<action>.ts\`, named exports only.
|
|
1148
1155
|
The directory is what keeps two entities' \`list\` apart — the file name carries no marker.
|
|
1149
1156
|
`),
|
|
@@ -1169,6 +1176,21 @@ says before rewriting anything.
|
|
|
1169
1176
|
its \`.js\` suffix — \`'../../resources/task/task.js'\`. Adding, removing or changing the
|
|
1170
1177
|
extension on the alias fixes nothing and the same error comes back under the other spelling;
|
|
1171
1178
|
two attempts that differ only in the suffix mean you are in this case.
|
|
1179
|
+
- **"Cannot find module 'project-common/…' or 'project-backend/…'" is almost always the
|
|
1180
|
+
extension, not the path.** These two are PACKAGES, resolved through their published
|
|
1181
|
+
\`exports\` map, and a deep import into either carries NO suffix — no \`.js\`, no \`.ts\`, no
|
|
1182
|
+
\`.type\`. \`project-backend/models/enquiry/enquiry.js\` resolves to a file
|
|
1183
|
+
(\`build/models/enquiry/enquiry.js.js\`) that can never exist; drop the extension entirely.
|
|
1184
|
+
This is unrelated to the \`sources/backend/\` bullet above it: that one is the SAME package
|
|
1185
|
+
importing itself and keeps \`.js\`; this one is a DIFFERENT package importing it by name.
|
|
1186
|
+
- **\`TS2345\` naming \`BoundEntrypointHandler\` at a \`bind(...)\` line in
|
|
1187
|
+
\`sources/api/src/entrypoints.ts\` or \`sources/worker/src/entrypoints.ts\` is a handler wrapped
|
|
1188
|
+
TWICE** — the handler module already exported a bound handler, and this line wraps it again
|
|
1189
|
+
with \`.body\`/\`.params\`/\`.request\`. Bind the already-bound export DIRECTLY
|
|
1190
|
+
(\`bind(protocol, handler)\`, no wrapper) — never cast around the type error, and never touch the
|
|
1191
|
+
handler module. Left as a type error, this is also the runtime fingerprint of the same defect:
|
|
1192
|
+
a story reported \`completed\` whose every request to that route fails with
|
|
1193
|
+
\`TypeError: handler is not a function\` (\`execute is not a function\` for \`.request\`).
|
|
1172
1194
|
- "Cannot find module" for \`postgres\`, \`@/lib/db.js\` or \`@/db/schema/...\` inside a
|
|
1173
1195
|
\`resources/**\` file means the file was written against the REMOVED Drizzle-DDL layout.
|
|
1174
1196
|
Rewrite it as an OwlMeans resource: an AJV schema on \`resource.schema\`, built with
|
|
@@ -1203,8 +1225,8 @@ says before rewriting anything.
|
|
|
1203
1225
|
\`@reduxjs/toolkit\`, \`react-redux\`, \`@/state/store\` or \`@/lib/fetch\` means the file was
|
|
1204
1226
|
written against the REMOVED stack. None of those packages are installed and none will be.
|
|
1205
1227
|
Rewrite the file against entrypoints: \`useNavigate\` from \`@owlmeans/client\` for navigation,
|
|
1206
|
-
\`ctx.entrypoint(protocol).call(...)\` for a backend call, a
|
|
1207
|
-
|
|
1228
|
+
\`ctx.entrypoint(protocol).call(...)\` for a backend call, a plain exported handler wrapped ONCE
|
|
1229
|
+
where it is bound for an endpoint (see "Writing an endpoint handler"), and the state hooks
|
|
1208
1230
|
(\`useStoreModel\`/\`useStoreList\` over a state resource) for client state. Do NOT install the
|
|
1209
1231
|
package and do NOT create the missing module.
|
|
1210
1232
|
- A missing export from a \`*.ts\` module — a selector, an action creator, a thunk, a
|
|
@@ -1389,9 +1411,29 @@ The job's name IS its entrypoint alias. Declare \`app.job.<name>\` in \`consts.t
|
|
|
1389
1411
|
sentinel, and the protocol in \`entrypoints.ts\` with \`job()\` from \`@owlmeans/route\`.
|
|
1390
1412
|
|
|
1391
1413
|
## 3. The processor — \`sources/worker/src/jobs/<name>.ts\`
|
|
1392
|
-
A
|
|
1393
|
-
|
|
1394
|
-
|
|
1414
|
+
A PLAIN exported async function — named exports only, exactly like an endpoint handler. It never
|
|
1415
|
+
imports \`handlers\` and never calls \`handlers<Context>()\`; that belongs only in
|
|
1416
|
+
\`sources/worker/src/entrypoints.ts\`, which wraps it EXACTLY ONCE when binding it:
|
|
1417
|
+
|
|
1418
|
+
// sources/worker/src/jobs/report-build.ts — the processor module
|
|
1419
|
+
export const handleReportBuildJob = async (req: ReportBuildRequest, ctx: Context) => { ... }
|
|
1420
|
+
|
|
1421
|
+
// sources/worker/src/entrypoints.ts — the ONLY wrap
|
|
1422
|
+
import * as jobs from '@/jobs/index.js'
|
|
1423
|
+
const worker = handlers<Context>()
|
|
1424
|
+
bind(protocols.job.reportBuild, worker.request(protocols.job.reportBuild, jobs.handleReportBuildJob))
|
|
1425
|
+
|
|
1426
|
+
WRONG — the processor module ALSO calls \`handlers()\` and wraps itself, so the export bound above
|
|
1427
|
+
is wrapped a SECOND time. \`tsc\` catches it (\`TS2345 "BoundEntrypointHandler<…> is not
|
|
1428
|
+
assignable"\`); left unfixed, every enqueued job then fails with \`TypeError: handler is not a
|
|
1429
|
+
function\`:
|
|
1430
|
+
|
|
1431
|
+
// WRONG — sources/worker/src/jobs/report-build.ts
|
|
1432
|
+
const worker = handlers<Context>()
|
|
1433
|
+
export const handleReportBuildJob = worker.request(protocols.job.reportBuild, async (req, ctx) => { ... })
|
|
1434
|
+
|
|
1435
|
+
It RETURNS its result; throwing a \`ResilientError\` subclass is how a refusal is reported, and the
|
|
1436
|
+
class survives the broker.
|
|
1395
1437
|
|
|
1396
1438
|
Two rules with no equivalent on the HTTP side:
|
|
1397
1439
|
- **Call \`job.touch()\` inside every long loop.** The broker judges liveness by the lock, and
|
|
@@ -1400,8 +1442,8 @@ Two rules with no equivalent on the HTTP side:
|
|
|
1400
1442
|
- **A processor must be safe to run twice.** Skip what a previous attempt recorded, or delete
|
|
1401
1443
|
what it created, and say in a comment which of the two this one does.
|
|
1402
1444
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1445
|
+
Enqueue from an endpoint with the same typed call used for HTTP:
|
|
1446
|
+
\`context.entrypoint(appEntrypoints.job.<name>).call({ body: data })\`.
|
|
1405
1447
|
`),
|
|
1406
1448
|
|
|
1407
1449
|
skill(ViableSkill.TargetAgents, 'LLM agents inside the application', `
|
package/src/slot/consts.ts
CHANGED
|
@@ -41,6 +41,14 @@ export enum SlotCommandType {
|
|
|
41
41
|
|
|
42
42
|
export enum SlotGitCommand {
|
|
43
43
|
Ensure = 'ensure',
|
|
44
|
+
/**
|
|
45
|
+
* Bring a remote repository's tree onto a volume that already exists.
|
|
46
|
+
*
|
|
47
|
+
* NOT `git clone`: a slot's directory is never empty — provisioning leaves its own metadata
|
|
48
|
+
* there — and `git clone` refuses a non-empty destination. The executor therefore initializes,
|
|
49
|
+
* adds the remote, fetches at a bounded depth and checks the branch out over the tree.
|
|
50
|
+
*/
|
|
51
|
+
Clone = 'clone',
|
|
44
52
|
Status = 'status',
|
|
45
53
|
Commit = 'commit',
|
|
46
54
|
Log = 'log',
|
|
@@ -66,6 +74,34 @@ export enum SlotFileCommand {
|
|
|
66
74
|
DeleteFile = 'deleteFile',
|
|
67
75
|
FindFilesWithEnvVars = 'findFilesWithEnvVars',
|
|
68
76
|
GetRootPath = 'getRootPath',
|
|
77
|
+
/**
|
|
78
|
+
* List a tree with a size and a binary flag per entry, bounded by a caller-given limit.
|
|
79
|
+
*
|
|
80
|
+
* The reason it exists rather than being composed out of {@link SlotFileCommand.GetSourceList}
|
|
81
|
+
* plus a read per file: a foreign repository can hold a hundred thousand files, and one round
|
|
82
|
+
* trip each — over a signed HTTP call to a pod, or over a connector on somebody's laptop — is
|
|
83
|
+
* not a slower version of the same thing, it is a walk that never finishes.
|
|
84
|
+
*/
|
|
85
|
+
StatTree = 'statTree',
|
|
86
|
+
/**
|
|
87
|
+
* Read the first N bytes of one file.
|
|
88
|
+
*
|
|
89
|
+
* A census classifies a file from its head; reading whole files to do it would hold a 1 MB
|
|
90
|
+
* export in memory to look at its first line. The answer is text, so a binary head comes back
|
|
91
|
+
* as whatever decoding produced — which is exactly the signal the entropy classification wants.
|
|
92
|
+
*/
|
|
93
|
+
ReadHead = 'readHead',
|
|
94
|
+
/**
|
|
95
|
+
* Move everything in the project root into a subdirectory, keeping a named few in place.
|
|
96
|
+
*
|
|
97
|
+
* One command rather than a listing plus a move per file, because it must be ATOMIC from the
|
|
98
|
+
* caller's point of view: a relocation interrupted half way leaves a tree that is neither the
|
|
99
|
+
* origin nor a target, and nothing downstream can tell which files already moved. Binary-safe —
|
|
100
|
+
* it moves paths and never reads contents.
|
|
101
|
+
*/
|
|
102
|
+
Relocate = 'relocate',
|
|
103
|
+
/** Delete a directory and everything under it. The purge of a relocated origin. */
|
|
104
|
+
RemoveTree = 'removeTree',
|
|
69
105
|
}
|
|
70
106
|
|
|
71
107
|
export enum SlotShellCommand {
|
|
@@ -228,6 +264,37 @@ export const COMMAND_DEADLINES: Record<string, number> = {
|
|
|
228
264
|
|
|
229
265
|
export const DEFAULT_COMMAND_DEADLINE = 60_000
|
|
230
266
|
|
|
267
|
+
/**
|
|
268
|
+
* EXECUTOR-side ceilings for the git commands whose real duration is not a git command's.
|
|
269
|
+
*
|
|
270
|
+
* Everything git does inside a slot is local and takes seconds — except a clone, which is a
|
|
271
|
+
* network fetch of somebody else's repository and is measured in minutes. Bounding it by the
|
|
272
|
+
* generic git deadline made a conversion's very first step fail on any repository large enough
|
|
273
|
+
* to be worth converting.
|
|
274
|
+
*/
|
|
275
|
+
export const GIT_COMMAND_DEADLINES: Partial<Record<SlotGitCommand, number>> = {
|
|
276
|
+
[SlotGitCommand.Clone]: 600_000,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** What every other git command gets. The value the type-level branch used to hard-code. */
|
|
280
|
+
export const DEFAULT_GIT_COMMAND_DEADLINE = 60_000
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* EXECUTOR-side ceilings for the file commands that walk or move a whole tree.
|
|
284
|
+
*
|
|
285
|
+
* The other file commands are one path each and finish in milliseconds; these three are bounded
|
|
286
|
+
* by how big the tree is, which for an origin project is set by whoever wrote it.
|
|
287
|
+
*/
|
|
288
|
+
export const FILE_COMMAND_DEADLINES: Partial<Record<SlotFileCommand, number>> = {
|
|
289
|
+
[SlotFileCommand.StatTree]: 120_000,
|
|
290
|
+
[SlotFileCommand.Relocate]: 300_000,
|
|
291
|
+
[SlotFileCommand.RemoveTree]: 120_000,
|
|
292
|
+
[SlotFileCommand.ReadHead]: 30_000,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** What every other file command gets. The value the type-level branch used to hard-code. */
|
|
296
|
+
export const DEFAULT_FILE_COMMAND_DEADLINE = 30_000
|
|
297
|
+
|
|
231
298
|
/**
|
|
232
299
|
* CALLER-side ceilings per command type: how long the asker waits for an answer.
|
|
233
300
|
*
|
|
@@ -266,6 +333,33 @@ export const SHELL_COMMAND_TIMEOUTS: Record<string, number> = {
|
|
|
266
333
|
[SlotShellCommand.Integrity]: 25_000, // executor deadline (15s) + margin
|
|
267
334
|
}
|
|
268
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Per-COMMAND caller ceilings for git and for files.
|
|
338
|
+
*
|
|
339
|
+
* Each is its executor deadline plus the same 40 s margin: the caller's bound must outlast the
|
|
340
|
+
* executor's, or a command that failed cleanly inside its own deadline reaches the caller as a
|
|
341
|
+
* timeout and the real reason — the one the executor took the trouble to produce — is lost.
|
|
342
|
+
*/
|
|
343
|
+
export const GIT_COMMAND_TIMEOUTS: Partial<Record<SlotGitCommand, number>> = {
|
|
344
|
+
[SlotGitCommand.Clone]: 640_000,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export const FILE_COMMAND_TIMEOUTS: Partial<Record<SlotFileCommand, number>> = {
|
|
348
|
+
[SlotFileCommand.StatTree]: 160_000,
|
|
349
|
+
[SlotFileCommand.Relocate]: 340_000,
|
|
350
|
+
[SlotFileCommand.RemoveTree]: 160_000,
|
|
351
|
+
[SlotFileCommand.ReadHead]: 70_000,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** The per-command caller bound for one command, or nothing where its type's default is right. */
|
|
355
|
+
const perCommandTimeout = (type: SlotCommandType, command: string): number | undefined => {
|
|
356
|
+
switch (type) {
|
|
357
|
+
case SlotCommandType.Shell: return SHELL_COMMAND_TIMEOUTS[command]
|
|
358
|
+
case SlotCommandType.Git: return GIT_COMMAND_TIMEOUTS[command as SlotGitCommand]
|
|
359
|
+
default: return FILE_COMMAND_TIMEOUTS[command as SlotFileCommand]
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
269
363
|
/**
|
|
270
364
|
* Resolve the caller-side bound for one command.
|
|
271
365
|
*
|
|
@@ -276,12 +370,24 @@ export const SHELL_COMMAND_TIMEOUTS: Record<string, number> = {
|
|
|
276
370
|
export const commandTimeout = (
|
|
277
371
|
type: SlotCommandType, command: string, override?: number
|
|
278
372
|
): number => override
|
|
279
|
-
?? (type
|
|
373
|
+
?? perCommandTimeout(type, command)
|
|
280
374
|
?? COMMAND_TIMEOUTS[type]
|
|
281
375
|
?? DEFAULT_COMMAND_TIMEOUT
|
|
282
376
|
|
|
283
|
-
/**
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
377
|
+
/**
|
|
378
|
+
* Resolve the executor-side deadline for one command.
|
|
379
|
+
*
|
|
380
|
+
* Per-command first, per-type second. It was per-type only, which is how a clone inherited the
|
|
381
|
+
* bound of a `git status` — the same shape as the shell table above, and added for the same
|
|
382
|
+
* reason.
|
|
383
|
+
*/
|
|
384
|
+
export const commandDeadline = (type: SlotCommandType, command: string): number => {
|
|
385
|
+
switch (type) {
|
|
386
|
+
case SlotCommandType.Shell:
|
|
387
|
+
return COMMAND_DEADLINES[command] ?? DEFAULT_COMMAND_DEADLINE
|
|
388
|
+
case SlotCommandType.Git:
|
|
389
|
+
return GIT_COMMAND_DEADLINES[command as SlotGitCommand] ?? DEFAULT_GIT_COMMAND_DEADLINE
|
|
390
|
+
default:
|
|
391
|
+
return FILE_COMMAND_DEADLINES[command as SlotFileCommand] ?? DEFAULT_FILE_COMMAND_DEADLINE
|
|
392
|
+
}
|
|
393
|
+
}
|
package/src/slot/schemas.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JSONSchemaType } from 'ajv'
|
|
2
2
|
import { SlotCommandType, SlotFileCommand, SlotGitCommand, SlotShellCommand } from './consts.js'
|
|
3
|
-
import type { SlotCommandPayload } from './types.js'
|
|
3
|
+
import type { SlotCommandPayload, SlotGitCloneArgs } from './types.js'
|
|
4
4
|
|
|
5
5
|
export const SlotCommandPayloadSchema = {
|
|
6
6
|
type: 'object',
|
|
@@ -25,3 +25,22 @@ export const SlotCommandPayloadSchema = {
|
|
|
25
25
|
required: ['type', 'command'],
|
|
26
26
|
additionalProperties: false
|
|
27
27
|
} as JSONSchemaType<SlotCommandPayload>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The clone command's arguments, as a signed body carries them.
|
|
31
|
+
*
|
|
32
|
+
* `token` is declared because it is SENT — a body field the schema does not declare is stripped
|
|
33
|
+
* by validation and then verified against a signature the sender made over the whole thing, which
|
|
34
|
+
* the guard reports as a 401 rather than as the validation gap it is.
|
|
35
|
+
*/
|
|
36
|
+
export const SlotGitCloneArgsSchema = {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
remoteUrl: { type: 'string', minLength: 1, maxLength: 2048 },
|
|
40
|
+
branch: { type: 'string', maxLength: 256, nullable: true },
|
|
41
|
+
depth: { type: 'number', minimum: 0, nullable: true },
|
|
42
|
+
token: { type: 'string', maxLength: 4096, nullable: true },
|
|
43
|
+
},
|
|
44
|
+
required: ['remoteUrl'],
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
} as unknown as JSONSchemaType<SlotGitCloneArgs>
|
package/src/slot/types.ts
CHANGED
|
@@ -75,6 +75,86 @@ export interface SlotGitLogResult {
|
|
|
75
75
|
commits: SlotGitCommitInfo[]
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Bring a remote repository onto the volume.
|
|
80
|
+
*
|
|
81
|
+
* `token` is a per-invocation credential and never reaches the tree: the executor passes it
|
|
82
|
+
* through the process environment for the one fetch, exactly as push and pull already do, so it
|
|
83
|
+
* appears in no `.git/config`, no remote URL and no argv anything can observe.
|
|
84
|
+
*/
|
|
85
|
+
export interface SlotGitCloneArgs {
|
|
86
|
+
remoteUrl: string
|
|
87
|
+
/** The branch to check out. Absent means whatever the remote calls its default. */
|
|
88
|
+
branch?: string
|
|
89
|
+
/** Fetch depth; 0 means the whole history. Defaults to a shallow fetch. */
|
|
90
|
+
depth?: number
|
|
91
|
+
token?: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SlotGitCloneResult {
|
|
95
|
+
cloned: boolean
|
|
96
|
+
/** The branch actually checked out — the resolved default, when none was asked for. */
|
|
97
|
+
branch: string
|
|
98
|
+
head: SlotGitCommitInfo | null
|
|
99
|
+
/** Executor diagnostics, when it has any worth reporting. */
|
|
100
|
+
result?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One entry of a {@link SlotFileCommand.StatTree} listing.
|
|
105
|
+
*
|
|
106
|
+
* The shape both a publisher and a connector answer with, so a census walks a pod's volume and a
|
|
107
|
+
* developer's own directory through one contract. `binary` is the executor's own verdict — the
|
|
108
|
+
* caller has not read the file and cannot form one.
|
|
109
|
+
*/
|
|
110
|
+
export interface FileStat {
|
|
111
|
+
path: string
|
|
112
|
+
bytes: number
|
|
113
|
+
binary: boolean
|
|
114
|
+
modifiedAt?: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface StatTreeArgs {
|
|
118
|
+
/** Project-relative directory to walk. Absent means the whole project. */
|
|
119
|
+
dir?: string
|
|
120
|
+
/** Entries to return before the answer reports itself truncated. */
|
|
121
|
+
limit?: number
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* `total` is what the walk SAW, `entries` what it returned.
|
|
126
|
+
*
|
|
127
|
+
* The two differ on a truncated walk, and the difference is what tells a caller its picture of the
|
|
128
|
+
* tree is partial — a bounded listing that reported only what it returned would be indistinguish-
|
|
129
|
+
* able from a small repository.
|
|
130
|
+
*/
|
|
131
|
+
export interface StatTreeResult {
|
|
132
|
+
entries: FileStat[]
|
|
133
|
+
truncated: boolean
|
|
134
|
+
total: number
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ReadHeadArgs {
|
|
138
|
+
path: string
|
|
139
|
+
bytes: number
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Move everything in the project root under `dir`, leaving the named entries where they are. */
|
|
143
|
+
export interface RelocateArgs {
|
|
144
|
+
dir: string
|
|
145
|
+
/** Root-relative names that stay put — the slot's own metadata and markers. */
|
|
146
|
+
keep?: string[]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface RelocateResult {
|
|
150
|
+
moved: number
|
|
151
|
+
kept: string[]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface RemoveTreeArgs {
|
|
155
|
+
dir: string
|
|
156
|
+
}
|
|
157
|
+
|
|
78
158
|
/** Arguments a shell command may carry. */
|
|
79
159
|
export interface SlotShellArgs {
|
|
80
160
|
subproject?: SubProject
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import Ajv from 'ajv'
|
|
3
|
+
import addFormats from 'ajv-formats'
|
|
4
|
+
import { RouteMethod, route } from '@owlmeans/route'
|
|
5
|
+
import { aliasOf, openProtocol, protocols } from '@owlmeans/entrypoint'
|
|
6
|
+
import { connect, ConnectInquiryKind, ConnectJobKind, ConnectOpKind } from '../src/connect/consts.js'
|
|
7
|
+
import { connectProtocols } from '../src/connect/entrypoints.js'
|
|
8
|
+
import { connectRef } from '../src/connect/references.js'
|
|
9
|
+
import {
|
|
10
|
+
ConnectCapabilitiesSchema, ConnectConvertCreateBodySchema, ConnectConvertProceedBodySchema,
|
|
11
|
+
ConnectInquiryParamsSchema, ConnectPipelineResumeBodySchema, InquiryAnswerSchema
|
|
12
|
+
} from '../src/connect/schemas.js'
|
|
13
|
+
import { ConnectExecutor } from '../src/connect/consts.js'
|
|
14
|
+
import { InquiryKind } from '@owlmeans/llm-common'
|
|
15
|
+
import { ConversionDecision, OriginKind } from '../src/convert/consts.js'
|
|
16
|
+
|
|
17
|
+
const compiler = () => addFormats(new Ajv({ strict: false }))
|
|
18
|
+
|
|
19
|
+
const entrypoints = () => connectProtocols({
|
|
20
|
+
guard: 'test-guard',
|
|
21
|
+
gate: { alias: 'test-gate', params: ['id'] },
|
|
22
|
+
localLlm: { alias: 'test-paid-llm', params: ['id'] },
|
|
23
|
+
updateBase: openProtocol(route('test:update:base', '/update')),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const entrypointOf = (alias: string) => {
|
|
27
|
+
const found = protocols(entrypoints()).find(item => item.alias === alias)
|
|
28
|
+
expect(found, alias).toBeDefined()
|
|
29
|
+
|
|
30
|
+
return found!
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The immutable declaration a route model wraps — path, method and parent as written. */
|
|
34
|
+
const routeOf = (alias: string) => entrypointOf(alias).route.route
|
|
35
|
+
|
|
36
|
+
describe('viable-common - the conversion additions to the connector contract', () => {
|
|
37
|
+
test('every new schema compiles', () => {
|
|
38
|
+
const ajv = compiler()
|
|
39
|
+
const schemas: [string, object][] = [
|
|
40
|
+
['InquiryAnswerSchema', InquiryAnswerSchema],
|
|
41
|
+
['ConnectInquiryParamsSchema', ConnectInquiryParamsSchema],
|
|
42
|
+
['ConnectConvertCreateBodySchema', ConnectConvertCreateBodySchema],
|
|
43
|
+
['ConnectConvertProceedBodySchema', ConnectConvertProceedBodySchema],
|
|
44
|
+
['ConnectPipelineResumeBodySchema', ConnectPipelineResumeBodySchema],
|
|
45
|
+
['ConnectCapabilitiesSchema', ConnectCapabilitiesSchema],
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
for (const [name, schema] of schemas) {
|
|
49
|
+
expect(() => ajv.compile(schema as never), name).not.toThrow()
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('an answer carries one value or several, and declining is a value too', () => {
|
|
54
|
+
const validate = compiler().compile(InquiryAnswerSchema as never)
|
|
55
|
+
|
|
56
|
+
expect(validate({ inquiryId: 'q1', value: 'yes' })).toBe(true)
|
|
57
|
+
expect(validate({ inquiryId: 'q1', value: ['a', 'b'] })).toBe(true)
|
|
58
|
+
expect(validate({ inquiryId: 'q1', declined: true })).toBe(true)
|
|
59
|
+
expect(validate({ inquiryId: 'q1', text: 'because the origin has two admin roles' })).toBe(true)
|
|
60
|
+
expect(validate({ value: 'orphaned' })).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a resume carries the answers the parked run asked for', () => {
|
|
64
|
+
const validate = compiler().compile(ConnectPipelineResumeBodySchema as never)
|
|
65
|
+
|
|
66
|
+
expect(validate({ answers: { q1: { inquiryId: 'q1', value: 'yes' } } })).toBe(true)
|
|
67
|
+
expect(validate({})).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('the convert bodies accept an origin and a decision', () => {
|
|
71
|
+
const ajv = compiler()
|
|
72
|
+
const create = ajv.compile(ConnectConvertCreateBodySchema as never)
|
|
73
|
+
const proceed = ajv.compile(ConnectConvertProceedBodySchema as never)
|
|
74
|
+
|
|
75
|
+
expect(create({
|
|
76
|
+
name: 'Deskflow',
|
|
77
|
+
origin: { kind: OriginKind.Github, repoUrl: 'https://github.com/acme/deskflow' },
|
|
78
|
+
})).toBe(true)
|
|
79
|
+
// A local target the connector is already attached to fetches nothing.
|
|
80
|
+
expect(create({})).toBe(true)
|
|
81
|
+
expect(proceed({ decision: ConversionDecision.Extract })).toBe(true)
|
|
82
|
+
expect(proceed({ decision: 'teleport' })).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('capabilities accept an executor kind this platform has never heard of', () => {
|
|
86
|
+
// The SDK and the platform ship separately: a connector built against a newer package must not
|
|
87
|
+
// be refused a session because it advertises one more capability than this platform knows.
|
|
88
|
+
const validate = compiler().compile(ConnectCapabilitiesSchema as never)
|
|
89
|
+
|
|
90
|
+
expect(validate({
|
|
91
|
+
harness: 'claude-code',
|
|
92
|
+
tiers: {},
|
|
93
|
+
subagents: true,
|
|
94
|
+
effortControl: true,
|
|
95
|
+
executors: [ConnectExecutor.Files, ConnectExecutor.Human, 'telepathy'],
|
|
96
|
+
})).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('the conversion and generated-file routes are declared, at their paths and methods', () => {
|
|
100
|
+
const expected: [string, string, RouteMethod][] = [
|
|
101
|
+
[connect.convert.create, '/convert', RouteMethod.POST],
|
|
102
|
+
[connect.convert.check, '/convert/:id/check', RouteMethod.GET],
|
|
103
|
+
[connect.convert.start, '/convert/:id/start', RouteMethod.POST],
|
|
104
|
+
[connect.convert.proceed, '/convert/:id/proceed', RouteMethod.POST],
|
|
105
|
+
[connect.convert.cancel, '/convert/:id/cancel', RouteMethod.POST],
|
|
106
|
+
[connect.convert.status, '/convert/:id', RouteMethod.GET],
|
|
107
|
+
[connect.convert.purge, '/convert/:id/purge', RouteMethod.POST],
|
|
108
|
+
[connect.inquiry.answer, '/project/:id/inquiry/:inquiryId', RouteMethod.POST],
|
|
109
|
+
[connect.project.converterLlm, '/project/:id/converter-llm', RouteMethod.POST],
|
|
110
|
+
[connect.files.list, '/project/:id/files', RouteMethod.GET],
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
for (const [alias, path, method] of expected) {
|
|
114
|
+
const route = routeOf(alias)
|
|
115
|
+
expect(route.path, alias).toBe(path)
|
|
116
|
+
expect(route.method, alias).toBe(method)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('every conversion route has the typed reference used by connector adapters', () => {
|
|
121
|
+
expect(connectRef.convert.create.alias).toBe(connect.convert.create)
|
|
122
|
+
expect(connectRef.convert.check.alias).toBe(connect.convert.check)
|
|
123
|
+
expect(connectRef.convert.start.alias).toBe(connect.convert.start)
|
|
124
|
+
expect(connectRef.convert.proceed.alias).toBe(connect.convert.proceed)
|
|
125
|
+
expect(connectRef.convert.cancel.alias).toBe(connect.convert.cancel)
|
|
126
|
+
expect(connectRef.convert.status.alias).toBe(connect.convert.status)
|
|
127
|
+
expect(connectRef.convert.purge.alias).toBe(connect.convert.purge)
|
|
128
|
+
expect(connectRef.inquiry.answer.alias).toBe(connect.inquiry.answer)
|
|
129
|
+
expect(connectRef.project.converterLlm.alias).toBe(connect.project.converterLlm)
|
|
130
|
+
expect(connectRef.files.list.alias).toBe(connect.files.list)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('the conversion routes hang under the connector base and carry no paid gate', () => {
|
|
134
|
+
// Delegated inference is the DEFAULT for a conversion, not an experimental capability — so
|
|
135
|
+
// unlike `project.llm`, nothing here sits behind the local-LLM gate.
|
|
136
|
+
expect(entrypointOf(connect.project.llm).gate?.alias).toBe('test-paid-llm')
|
|
137
|
+
|
|
138
|
+
const gated = [...Object.values(connect.convert), connect.inquiry.answer,
|
|
139
|
+
connect.project.converterLlm]
|
|
140
|
+
for (const alias of gated) {
|
|
141
|
+
expect(entrypointOf(alias).gate, alias).toBeUndefined()
|
|
142
|
+
expect(aliasOf(routeOf(alias).parent!), alias).toBe(connect.base)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('the vocabulary a conversion adds to the connector is declared', () => {
|
|
147
|
+
expect(ConnectOpKind.Inquiry).toBe('inquiry')
|
|
148
|
+
expect(ConnectExecutor.Human).toBe('human')
|
|
149
|
+
expect(Object.values(ConnectInquiryKind)).toEqual(['choice', 'text', 'confirm'])
|
|
150
|
+
// The platform's mapper is a widening rather than a translation table, which is only true
|
|
151
|
+
// while the two enums stay byte-identical — `manager-api` does not depend on the model
|
|
152
|
+
// runtime, so nothing else in the stack would notice one of them gaining a fourth shape.
|
|
153
|
+
expect(Object.values(ConnectInquiryKind)).toEqual(Object.values(InquiryKind))
|
|
154
|
+
// One job kind per stage: each stage is its own pipeline run, looked up by the alias its kind
|
|
155
|
+
// names, so a single `convert` kind would match whichever run row happened to be newest.
|
|
156
|
+
expect(ConnectJobKind.ConvertIntake).toBe('convert-intake')
|
|
157
|
+
expect(ConnectJobKind.ConvertAnalysis).toBe('convert-analysis')
|
|
158
|
+
expect(ConnectJobKind.ConvertExtraction).toBe('convert-extraction')
|
|
159
|
+
expect(ConnectJobKind.ConvertImplementation).toBe('convert-implementation')
|
|
160
|
+
expect(ConnectJobKind.ConvertPurge).toBe('convert-purge')
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { protocols } from '@owlmeans/entrypoint'
|
|
2
|
+
import { openProtocol, protocols } from '@owlmeans/entrypoint'
|
|
3
3
|
import { DEFAULT_GUARD } from '@owlmeans/auth-common'
|
|
4
|
+
import { route } from '@owlmeans/route'
|
|
4
5
|
import { connect } from '../src/connect/consts.js'
|
|
5
6
|
import { connectProtocols } from '../src/connect/entrypoints.js'
|
|
6
7
|
|
|
7
8
|
describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
8
|
-
const tree = connectProtocols({
|
|
9
|
+
const tree = connectProtocols({
|
|
10
|
+
guard: DEFAULT_GUARD,
|
|
11
|
+
updateBase: openProtocol(route('manager:update', '/update')),
|
|
12
|
+
})
|
|
9
13
|
|
|
10
14
|
test('exports named immutable declarations and a complete flat materialization view', () => {
|
|
11
15
|
expect(tree.base.alias).toBe(connect.base)
|
|
12
16
|
expect(tree.session.open.alias).toBe(connect.session.open)
|
|
13
17
|
expect(tree.project.llm.alias).toBe(connect.project.llm)
|
|
14
|
-
expect(
|
|
15
|
-
expect(
|
|
18
|
+
expect(tree.project.converterLlm.alias).toBe(connect.project.converterLlm)
|
|
19
|
+
expect(tree.convert.proceed.alias).toBe(connect.convert.proceed)
|
|
20
|
+
expect(tree.inquiry.answer.alias).toBe(connect.inquiry.answer)
|
|
21
|
+
expect(tree.files.list.alias).toBe(connect.files.list)
|
|
22
|
+
expect(protocols(tree)).toHaveLength(38)
|
|
23
|
+
expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(38)
|
|
16
24
|
})
|
|
17
25
|
})
|