@owlmeans/viable-common 0.0.39 → 0.0.40
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 +4 -4
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/agent-presentation/SKILL.md +1 -1
- package/agent-meta/skills/viable-common/SKILL.md +40 -19
- package/build/areas/index.d.ts +1 -0
- package/build/areas/index.d.ts.map +1 -1
- package/build/areas/index.js +1 -0
- package/build/areas/index.js.map +1 -1
- package/build/areas/tenancy.d.ts +55 -0
- package/build/areas/tenancy.d.ts.map +1 -0
- package/build/areas/tenancy.js +33 -0
- package/build/areas/tenancy.js.map +1 -0
- package/build/blueprint/types.d.ts +19 -1
- package/build/blueprint/types.d.ts.map +1 -1
- package/build/blueprint/types.js.map +1 -1
- package/build/connect/consts.d.ts +3 -15
- package/build/connect/consts.d.ts.map +1 -1
- package/build/connect/consts.js +3 -15
- package/build/connect/consts.js.map +1 -1
- package/build/connect/entrypoints.d.ts +33 -69
- package/build/connect/entrypoints.d.ts.map +1 -1
- package/build/connect/entrypoints.js +5 -25
- package/build/connect/entrypoints.js.map +1 -1
- package/build/connect/references.d.ts +1 -31
- package/build/connect/references.d.ts.map +1 -1
- package/build/connect/references.js +0 -7
- package/build/connect/references.js.map +1 -1
- package/build/connect/types.d.ts +1 -25
- package/build/connect/types.d.ts.map +1 -1
- package/build/dev/consts.d.ts +15 -0
- package/build/dev/consts.d.ts.map +1 -1
- package/build/dev/consts.js +16 -0
- package/build/dev/consts.js.map +1 -1
- package/build/dev/schemas.d.ts +7 -0
- package/build/dev/schemas.d.ts.map +1 -1
- package/build/dev/schemas.js +9 -1
- package/build/dev/schemas.js.map +1 -1
- package/build/dev/types.d.ts +15 -1
- package/build/dev/types.d.ts.map +1 -1
- package/build/planning/schemas.d.ts.map +1 -1
- package/build/planning/schemas.js +25 -0
- package/build/planning/schemas.js.map +1 -1
- package/build/planning/types.d.ts +9 -0
- package/build/planning/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/areas/index.ts +1 -0
- package/src/areas/tenancy.ts +69 -0
- package/src/blueprint/types.ts +19 -1
- package/src/connect/consts.ts +3 -16
- package/src/connect/entrypoints.ts +10 -60
- package/src/connect/references.ts +4 -26
- package/src/connect/types.ts +1 -22
- package/src/dev/consts.ts +16 -0
- package/src/dev/schemas.ts +13 -1
- package/src/dev/types.ts +15 -1
- package/src/planning/schemas.ts +25 -0
- package/src/planning/types.ts +9 -0
- package/tests/access-schema.spec.ts +37 -0
- package/tests/connect-convert.spec.ts +5 -11
- package/tests/connect-entrypoints.spec.ts +33 -14
- package/tests/planning.spec.ts +26 -0
- package/tests/tenancy.spec.ts +48 -0
package/src/connect/types.ts
CHANGED
|
@@ -186,8 +186,7 @@ export interface ConnectProfileSettingsView extends ConnectProfileSettings {
|
|
|
186
186
|
* work: a conversion reads somebody else's whole repository, which is the one case where handing
|
|
187
187
|
* the inference to the parent agent is the cheap default rather than the experimental option.
|
|
188
188
|
*
|
|
189
|
-
* OPTIONAL
|
|
190
|
-
* optionally: the two converter fields are answered by a platform build that does not exist yet,
|
|
189
|
+
* OPTIONAL: the two converter fields are answered by a platform build that does not exist yet,
|
|
191
190
|
* and this package is consumed by the platform through a workspace link rather than a published
|
|
192
191
|
* range — a required field here is a compile error in every handler that already returns this
|
|
193
192
|
* view. Tightened to required once the platform's conversion handlers fill them.
|
|
@@ -251,26 +250,6 @@ export interface ConnectProjectBranding {
|
|
|
251
250
|
*/
|
|
252
251
|
export interface ConnectProjectBrandingSave extends Partial<ConnectProjectBranding> {}
|
|
253
252
|
|
|
254
|
-
/** What the platform tells a connector about itself. */
|
|
255
|
-
export interface ConnectCapabilitiesView {
|
|
256
|
-
/** Tier → the platform role names that will be asked for at that tier. */
|
|
257
|
-
tiers: Record<ModelTier, string[]>
|
|
258
|
-
/** Whether this caller may open a local-LLM session. */
|
|
259
|
-
localLlm: boolean
|
|
260
|
-
/**
|
|
261
|
-
* The default mode for a new session, resolved from the caller's settings.
|
|
262
|
-
*
|
|
263
|
-
* `converterLlm` is OPTIONAL: an older platform does not send it, and a client that treated its
|
|
264
|
-
* absence as a value would pin every conversion to whatever its own default happened to be.
|
|
265
|
-
*/
|
|
266
|
-
defaults: { target: ConnectTarget, llm: ConnectLlm, converterLlm?: ConnectLlm }
|
|
267
|
-
limits: {
|
|
268
|
-
pullWaitMs: number
|
|
269
|
-
modelTaskTimeoutMs: number
|
|
270
|
-
maxPending: number
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
253
|
/** Attach an existing project to a session. */
|
|
275
254
|
export interface ConnectAttachBody {
|
|
276
255
|
projectId?: string
|
package/src/dev/consts.ts
CHANGED
|
@@ -10,6 +10,22 @@ export enum AccessLevel {
|
|
|
10
10
|
Admin = 'admin',
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Who holds a permission without anybody granting it — the default CLASS of its definition.
|
|
15
|
+
*
|
|
16
|
+
* Evaluated by the IAM when it issues claims, never copied onto rows, so a person who joins an
|
|
17
|
+
* organization later holds what its members hold with nothing written for them. `User` is every
|
|
18
|
+
* signed-in person of the application; `Member` and `Owner` are the members and the owners of the
|
|
19
|
+
* organization being acted in, and therefore mean anything only for an organization-bound
|
|
20
|
+
* permission. `None` is the ordinary permission: held only where it was granted.
|
|
21
|
+
*/
|
|
22
|
+
export enum PermissionDefault {
|
|
23
|
+
None = 'none',
|
|
24
|
+
User = 'user',
|
|
25
|
+
Member = 'member',
|
|
26
|
+
Owner = 'owner',
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
/**
|
|
14
30
|
* Alias prefixes of the entrypoints a generated project declares.
|
|
15
31
|
*
|
package/src/dev/schemas.ts
CHANGED
|
@@ -80,7 +80,10 @@ export const EntrypointRefListSchema: JSONSchemaType<{entries: EntrypointRef[]}>
|
|
|
80
80
|
additionalProperties: false
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
/** The part of an {@link AccessBlock} a model is asked for. */
|
|
84
|
+
type ModelAccessBlock = Omit<AccessBlock, 'defaults' | 'entityScoped'>
|
|
85
|
+
|
|
86
|
+
const ModelAccessBlockSchema: JSONSchemaType<ModelAccessBlock> = {
|
|
84
87
|
type: "object",
|
|
85
88
|
title: "AccessBlock",
|
|
86
89
|
description: "Object with permissions array and access level",
|
|
@@ -119,6 +122,15 @@ export const AccessBlockSchema: JSONSchemaType<AccessBlock> = {
|
|
|
119
122
|
additionalProperties: false
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The access block a model writes.
|
|
127
|
+
*
|
|
128
|
+
* It names only what a model is asked for: `defaults` and `entityScoped` are written by code and
|
|
129
|
+
* must never reach a prompt, which `tests/access-schema.spec.ts` pins byte for byte. A value it
|
|
130
|
+
* admits is still an {@link AccessBlock}, because both code-written keys are optional.
|
|
131
|
+
*/
|
|
132
|
+
export const AccessBlockSchema = ModelAccessBlockSchema as unknown as JSONSchemaType<AccessBlock>
|
|
133
|
+
|
|
122
134
|
export const AccessListSchema: JSONSchemaType<AccessList> = {
|
|
123
135
|
type: "object",
|
|
124
136
|
title: "AccessList",
|
package/src/dev/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccessLevel } from "./consts.js"
|
|
1
|
+
import type { AccessLevel, PermissionDefault } from "./consts.js"
|
|
2
2
|
|
|
3
3
|
export interface SourceFile {
|
|
4
4
|
path: string
|
|
@@ -33,6 +33,20 @@ export interface AccessBlock {
|
|
|
33
33
|
level: AccessLevel
|
|
34
34
|
/** Gate params safe for every newly signed-in user; a subset of permissions. */
|
|
35
35
|
defaultEnabledPermissions?: string[]
|
|
36
|
+
/**
|
|
37
|
+
* The default class of each permission this block names, keyed by the gate param with its
|
|
38
|
+
* `@<routeParam>` selector stripped — the name the definition is registered under.
|
|
39
|
+
*
|
|
40
|
+
* Written by CODE, never by a model: `AccessBlockSchema` does not carry it, so a model answer
|
|
41
|
+
* never holds one, and what decides it is the project's tenancy and the area, not a reading of
|
|
42
|
+
* the story.
|
|
43
|
+
*/
|
|
44
|
+
defaults?: Record<string, PermissionDefault>
|
|
45
|
+
/**
|
|
46
|
+
* Whether the permissions of this block are bound to the organization being acted in, rather than
|
|
47
|
+
* held everywhere. Written by code, like {@link AccessBlock.defaults}.
|
|
48
|
+
*/
|
|
49
|
+
entityScoped?: boolean
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
/**
|
package/src/planning/schemas.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { JSONSchemaType } from 'ajv'
|
|
2
2
|
import { CODE_MAX } from '@owlmeans/planning'
|
|
3
3
|
import { ProjectArea } from '../areas/consts.js'
|
|
4
|
+
import { TENANCY_QUOTE_MAX } from '../areas/tenancy.js'
|
|
4
5
|
import { StoryKind } from '../ba/consts.js'
|
|
5
6
|
import { ConnectLlm, ConnectTarget } from '../connect/consts.js'
|
|
6
7
|
import { ProjectOriginSchema } from '../convert/schemas.js'
|
|
@@ -40,6 +41,30 @@ export const ViableProjectFieldsSchema = {
|
|
|
40
41
|
required: ['at'],
|
|
41
42
|
additionalProperties: false,
|
|
42
43
|
},
|
|
44
|
+
// An absent `tenancy` is "never decided"; a recorded one always carries both flags, so a
|
|
45
|
+
// reader never has to guess what a missing flag meant.
|
|
46
|
+
tenancy: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
nullable: true,
|
|
49
|
+
properties: {
|
|
50
|
+
operators: { type: 'boolean' },
|
|
51
|
+
users: { type: 'boolean' },
|
|
52
|
+
quotes: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
nullable: true,
|
|
55
|
+
properties: {
|
|
56
|
+
operators: { type: 'string', minLength: 1, maxLength: TENANCY_QUOTE_MAX, nullable: true },
|
|
57
|
+
users: { type: 'string', minLength: 1, maxLength: TENANCY_QUOTE_MAX, nullable: true },
|
|
58
|
+
},
|
|
59
|
+
required: [],
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
},
|
|
62
|
+
by: { type: 'string', enum: ['model', 'owner'] },
|
|
63
|
+
at: { type: 'string', minLength: 1, maxLength: 32 },
|
|
64
|
+
},
|
|
65
|
+
required: ['operators', 'users', 'by', 'at'],
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
},
|
|
43
68
|
},
|
|
44
69
|
required: [],
|
|
45
70
|
additionalProperties: false,
|
package/src/planning/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Card, Project, Specification } from '@owlmeans/planning'
|
|
2
2
|
import type { ProjectArea } from '../areas/consts.js'
|
|
3
|
+
import type { ViableTenancyDecision } from '../areas/tenancy.js'
|
|
3
4
|
import type { StoryKind } from '../ba/consts.js'
|
|
4
5
|
import type { ConnectLlm, ConnectTarget } from '../connect/consts.js'
|
|
5
6
|
import type { ProjectOrigin } from '../convert/types.js'
|
|
@@ -42,6 +43,14 @@ export type ViableProjectFields = {
|
|
|
42
43
|
* "none" and no later run would ask again. `at` is when it was decided, an ISO timestamp.
|
|
43
44
|
*/
|
|
44
45
|
landing?: ViableLandingDecision
|
|
46
|
+
/**
|
|
47
|
+
* Whether the target splits its operators and its end users into tenant organizations.
|
|
48
|
+
*
|
|
49
|
+
* ABSENT means never decided and reads as a single-organization application, exactly as every
|
|
50
|
+
* project before tenancy. It reaches a run through the blueprint reference (`BlueprintRef.tenancy`)
|
|
51
|
+
* only when a flag is on.
|
|
52
|
+
*/
|
|
53
|
+
tenancy?: ViableTenancyDecision
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
/** The recorded landing-gate decision — see {@link ViableProjectFields.landing}. */
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { AccessBlockSchema, AccessLevel, AccessListSchema, PermissionDefault } from '../src/index.js'
|
|
4
|
+
import type { AccessBlock } from '../src/index.js'
|
|
5
|
+
|
|
6
|
+
const sha256 = (value: unknown): string =>
|
|
7
|
+
createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
8
|
+
|
|
9
|
+
// The model-facing schemas, byte for byte. They are rendered into the access prompts verbatim and
|
|
10
|
+
// handed to the model as its answer contract, so a change here changes what every model is asked —
|
|
11
|
+
// update these only together with the prompts that read them.
|
|
12
|
+
const ACCESS_BLOCK_SHA256 = '577c8406a73604128ff016a35fe918960d1316a1a24fe4737be31e41478c6ca8'
|
|
13
|
+
const ACCESS_LIST_SHA256 = '80b3ab836718ce69c97e9854f647f2b5ed794ec4b871e86ae94b0fad4bf68a50'
|
|
14
|
+
|
|
15
|
+
describe('viable-common - the access schema a model is asked for', () => {
|
|
16
|
+
test('names only the model-written keys, never the code-written defaults or binding', () => {
|
|
17
|
+
expect(Object.keys(AccessBlockSchema.properties ?? {})).toEqual(
|
|
18
|
+
['permissions', 'level', 'defaultEnabledPermissions']
|
|
19
|
+
)
|
|
20
|
+
expect(JSON.stringify(AccessListSchema)).not.toContain('entityScoped')
|
|
21
|
+
expect(JSON.stringify(AccessListSchema)).not.toContain('"defaults"')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('is byte-identical to the schema the access prompts were written against', () => {
|
|
25
|
+
expect(sha256(AccessBlockSchema)).toBe(ACCESS_BLOCK_SHA256)
|
|
26
|
+
expect(sha256(AccessListSchema)).toBe(ACCESS_LIST_SHA256)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('an access block still carries the code-written keys as data', () => {
|
|
30
|
+
const block: AccessBlock = {
|
|
31
|
+
permissions: ['enquiry--view@enquiryId'], level: AccessLevel.Permissioned,
|
|
32
|
+
defaults: { 'enquiry--view': PermissionDefault.Member }, entityScoped: true,
|
|
33
|
+
}
|
|
34
|
+
expect(block.defaults).toEqual({ 'enquiry--view': 'member' })
|
|
35
|
+
expect(Object.values(PermissionDefault)).toEqual(['none', 'user', 'member', 'owner'])
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import Ajv from 'ajv'
|
|
3
3
|
import addFormats from 'ajv-formats'
|
|
4
|
-
import { RouteMethod
|
|
5
|
-
import { aliasOf,
|
|
4
|
+
import { RouteMethod } from '@owlmeans/route'
|
|
5
|
+
import { aliasOf, protocols } from '@owlmeans/entrypoint'
|
|
6
6
|
import { connect, ConnectInquiryKind, ConnectOpKind } from '../src/connect/consts.js'
|
|
7
7
|
import { connectProtocols } from '../src/connect/entrypoints.js'
|
|
8
8
|
import { connectRef } from '../src/connect/references.js'
|
|
@@ -20,7 +20,6 @@ const entrypoints = () => connectProtocols({
|
|
|
20
20
|
guard: 'test-guard',
|
|
21
21
|
gate: { alias: 'test-gate', params: ['id'] },
|
|
22
22
|
localLlm: { alias: 'test-paid-llm', params: ['id'] },
|
|
23
|
-
updateBase: openProtocol(route('test:update:base', '/update')),
|
|
24
23
|
})
|
|
25
24
|
|
|
26
25
|
const entrypointOf = (alias: string) => {
|
|
@@ -102,11 +101,9 @@ describe('viable-common - the conversion additions to the connector contract', (
|
|
|
102
101
|
[connect.convert.check, '/convert/:id/check', RouteMethod.GET],
|
|
103
102
|
[connect.convert.start, '/convert/:id/start', RouteMethod.POST],
|
|
104
103
|
[connect.convert.proceed, '/convert/:id/proceed', RouteMethod.POST],
|
|
105
|
-
[connect.convert.cancel, '/convert/:id/cancel', RouteMethod.POST],
|
|
106
104
|
[connect.convert.status, '/convert/:id', RouteMethod.GET],
|
|
107
105
|
[connect.convert.purge, '/convert/:id/purge', RouteMethod.POST],
|
|
108
106
|
[connect.inquiry.answer, '/project/:id/inquiry/:inquiryId', RouteMethod.POST],
|
|
109
|
-
[connect.project.converterLlm, '/project/:id/converter-llm', RouteMethod.POST],
|
|
110
107
|
[connect.files.list, '/project/:id/files', RouteMethod.GET],
|
|
111
108
|
]
|
|
112
109
|
|
|
@@ -122,21 +119,18 @@ describe('viable-common - the conversion additions to the connector contract', (
|
|
|
122
119
|
expect(connectRef.convert.check.alias).toBe(connect.convert.check)
|
|
123
120
|
expect(connectRef.convert.start.alias).toBe(connect.convert.start)
|
|
124
121
|
expect(connectRef.convert.proceed.alias).toBe(connect.convert.proceed)
|
|
125
|
-
expect(connectRef.convert.cancel.alias).toBe(connect.convert.cancel)
|
|
126
122
|
expect(connectRef.convert.status.alias).toBe(connect.convert.status)
|
|
127
123
|
expect(connectRef.convert.purge.alias).toBe(connect.convert.purge)
|
|
128
124
|
expect(connectRef.inquiry.answer.alias).toBe(connect.inquiry.answer)
|
|
129
|
-
expect(connectRef.project.converterLlm.alias).toBe(connect.project.converterLlm)
|
|
130
125
|
expect(connectRef.files.list.alias).toBe(connect.files.list)
|
|
131
126
|
})
|
|
132
127
|
|
|
133
128
|
test('the conversion routes hang under the connector base and carry no paid gate', () => {
|
|
134
129
|
// Delegated inference is the DEFAULT for a conversion, not an experimental capability — so
|
|
135
|
-
// unlike
|
|
136
|
-
expect(entrypointOf(connect.
|
|
130
|
+
// unlike the delegated session, nothing here sits behind the local-LLM gate.
|
|
131
|
+
expect(entrypointOf(connect.session.openDelegated).gate?.alias).toBe('test-paid-llm')
|
|
137
132
|
|
|
138
|
-
const gated = [...Object.values(connect.convert), connect.inquiry.answer
|
|
139
|
-
connect.project.converterLlm]
|
|
133
|
+
const gated = [...Object.values(connect.convert), connect.inquiry.answer]
|
|
140
134
|
for (const alias of gated) {
|
|
141
135
|
expect(entrypointOf(alias).gate, alias).toBeUndefined()
|
|
142
136
|
expect(aliasOf(routeOf(alias).parent!), alias).toBe(connect.base)
|
|
@@ -1,30 +1,48 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import Ajv from 'ajv'
|
|
3
|
-
import { aliasOf,
|
|
3
|
+
import { aliasOf, protocols } from '@owlmeans/entrypoint'
|
|
4
4
|
import { DEFAULT_GUARD } from '@owlmeans/auth-common'
|
|
5
|
-
import {
|
|
5
|
+
import { RouteMethod, RouteProtocols } from '@owlmeans/route'
|
|
6
6
|
import { connect, CONNECT_BRANDING_GOOGLE_TAG_MAX } from '../src/connect/consts.js'
|
|
7
7
|
import { ConnectProjectBrandingSaveSchema } from '../src/connect/schemas.js'
|
|
8
8
|
import { connectProtocols } from '../src/connect/entrypoints.js'
|
|
9
9
|
import { connectRef } from '../src/connect/references.js'
|
|
10
10
|
|
|
11
11
|
describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
12
|
-
const tree = connectProtocols({
|
|
13
|
-
guard: DEFAULT_GUARD,
|
|
14
|
-
updateBase: openProtocol(route('manager:update', '/update')),
|
|
15
|
-
})
|
|
12
|
+
const tree = connectProtocols({ guard: DEFAULT_GUARD })
|
|
16
13
|
|
|
17
14
|
test('exports named immutable declarations and a complete flat materialization view', () => {
|
|
18
15
|
expect(tree.base.alias).toBe(connect.base)
|
|
19
16
|
expect(tree.session.open.alias).toBe(connect.session.open)
|
|
20
|
-
expect(tree.project.llm.alias).toBe(connect.project.llm)
|
|
21
|
-
expect(tree.project.converterLlm.alias).toBe(connect.project.converterLlm)
|
|
22
17
|
expect(tree.story.status.alias).toBe(connect.story.status)
|
|
23
18
|
expect(tree.convert.proceed.alias).toBe(connect.convert.proceed)
|
|
24
19
|
expect(tree.inquiry.answer.alias).toBe(connect.inquiry.answer)
|
|
25
20
|
expect(tree.files.list.alias).toBe(connect.files.list)
|
|
26
|
-
expect(protocols(tree)).toHaveLength(
|
|
27
|
-
expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(
|
|
21
|
+
expect(protocols(tree)).toHaveLength(26)
|
|
22
|
+
expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(26)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('declares exactly the routes a connector calls — every alias, reference and path, and no socket', () => {
|
|
26
|
+
const declared = protocols(tree)
|
|
27
|
+
const addresses = declared.filter(protocol => protocol.route.route.method != null)
|
|
28
|
+
.map(protocol => `${protocol.route.route.method!.toUpperCase()} ${protocol.route.route.path}`).sort()
|
|
29
|
+
|
|
30
|
+
expect(addresses).toEqual([
|
|
31
|
+
'GET /convert/:id', 'GET /convert/:id/check', 'GET /pipeline/:id/:runId', 'GET /project',
|
|
32
|
+
'GET /project/:id/branding', 'GET /project/:id/files', 'GET /project/:id/status',
|
|
33
|
+
'GET /project/:id/story/:storyId/status', 'GET /session/:sessionId/ops',
|
|
34
|
+
'POST /convert', 'POST /convert/:id/proceed', 'POST /convert/:id/purge', 'POST /convert/:id/start',
|
|
35
|
+
'POST /pipeline/:id/:runId/resume', 'POST /project', 'POST /project/:id/branding',
|
|
36
|
+
'POST /project/:id/confirm', 'POST /project/:id/inquiry/:inquiryId', 'POST /project/:id/modify',
|
|
37
|
+
'POST /project/:id/reinit', 'POST /project/attach', 'POST /session', 'POST /session/:sessionId/close',
|
|
38
|
+
'POST /session/:sessionId/ops/:opId', 'POST /session/delegated',
|
|
39
|
+
])
|
|
40
|
+
expect(declared.every(protocol => protocol.route.route.protocol !== RouteProtocols.SOCKET)).toBe(true)
|
|
41
|
+
// The alias table, the tree and the typed references name the same routes.
|
|
42
|
+
const leaves = (value: object): string[] => Object.values(value).flatMap(entry =>
|
|
43
|
+
typeof entry === 'string' ? [entry] : 'alias' in entry ? [entry.alias as string] : leaves(entry))
|
|
44
|
+
expect(leaves(connect).sort()).toEqual(declared.map(protocol => protocol.alias).sort())
|
|
45
|
+
expect(leaves(connectRef).sort()).toEqual(declared.map(protocol => protocol.alias).filter(alias => alias !== connect.base).sort())
|
|
28
46
|
})
|
|
29
47
|
|
|
30
48
|
test('reads and saves a project\'s branding at one path, under the owned base and no paid gate', () => {
|
|
@@ -32,7 +50,6 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
|
32
50
|
guard: DEFAULT_GUARD,
|
|
33
51
|
gate: { alias: 'test-gate', params: ['owner'] },
|
|
34
52
|
localLlm: { alias: 'test-paid-llm', params: ['cap'] },
|
|
35
|
-
updateBase: openProtocol(route('manager:update', '/update')),
|
|
36
53
|
})
|
|
37
54
|
const get = paid.project.branding.get
|
|
38
55
|
const save = paid.project.branding.save
|
|
@@ -45,9 +62,11 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
|
45
62
|
expect(save.route.route.method).toBe(RouteMethod.POST)
|
|
46
63
|
expect(aliasOf(get.route.route.parent)).toBe(connect.base)
|
|
47
64
|
expect(aliasOf(save.route.route.parent)).toBe(connect.base)
|
|
48
|
-
// The paid local-LLM gate sits on
|
|
49
|
-
// inherits only the base's guard and ownership gate.
|
|
50
|
-
expect(paid.
|
|
65
|
+
// The paid local-LLM gate sits on the delegated session alone; branding carries no gate of its
|
|
66
|
+
// own and inherits only the base's guard and ownership gate.
|
|
67
|
+
expect(paid.session.openDelegated.gate?.alias).toBe('test-paid-llm')
|
|
68
|
+
expect(protocols(paid).filter(protocol => protocol.gate?.alias === 'test-paid-llm'))
|
|
69
|
+
.toEqual([paid.session.openDelegated])
|
|
51
70
|
expect(get.gate).toBeUndefined()
|
|
52
71
|
expect(save.gate).toBeUndefined()
|
|
53
72
|
expect(connectRef.project.branding.save.alias).toBe(connect.project.branding.save)
|
package/tests/planning.spec.ts
CHANGED
|
@@ -276,6 +276,32 @@ describe('viable-common - the viable field schemas', () => {
|
|
|
276
276
|
expect(project({ landing: { story: 'US-ABC12' } })).toBe(false)
|
|
277
277
|
expect(project({ landing: { story: 'US-ABC12', at, reason: 'best fit' } })).toBe(false)
|
|
278
278
|
})
|
|
279
|
+
|
|
280
|
+
test('a project records a tenancy decision with both flags, who decided it and when', () => {
|
|
281
|
+
expect(project({ tenancy: { operators: false, users: true, by: 'model', at } })).toBe(true)
|
|
282
|
+
expect(project({
|
|
283
|
+
tenancy: {
|
|
284
|
+
operators: true, users: true, by: 'owner', at,
|
|
285
|
+
quotes: { operators: 'Each agency manages its own staff.', users: null },
|
|
286
|
+
},
|
|
287
|
+
})).toBe(true)
|
|
288
|
+
expect(project({ tenancy: null })).toBe(true)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('a project refuses a partial tenancy decision, an unknown decider and a stray key', () => {
|
|
292
|
+
expect(project({ tenancy: { users: true, by: 'model', at } })).toBe(false)
|
|
293
|
+
expect(project({ tenancy: { operators: true, users: false, at } })).toBe(false)
|
|
294
|
+
expect(project({ tenancy: { operators: true, users: false, by: 'model' } })).toBe(false)
|
|
295
|
+
expect(project({ tenancy: { operators: 'yes', users: false, by: 'model', at } })).toBe(false)
|
|
296
|
+
expect(project({ tenancy: { operators: true, users: false, by: 'agent', at } })).toBe(false)
|
|
297
|
+
expect(project({ tenancy: { operators: true, users: false, by: 'model', at, reason: 'b2b' } })).toBe(false)
|
|
298
|
+
expect(project({
|
|
299
|
+
tenancy: { operators: true, users: false, by: 'model', at, quotes: { staff: 'x' } },
|
|
300
|
+
})).toBe(false)
|
|
301
|
+
expect(project({
|
|
302
|
+
tenancy: { operators: true, users: false, by: 'model', at, quotes: { operators: 'x'.repeat(1025) } },
|
|
303
|
+
})).toBe(false)
|
|
304
|
+
})
|
|
279
305
|
})
|
|
280
306
|
|
|
281
307
|
describe('viable-common - the viable card helpers', () => {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { NO_TENANCY, ProjectArea, tenancyOf, tenantedArea } from '../src/index.js'
|
|
3
|
+
import { LandingGatePreference } from '../src/blueprint/index.js'
|
|
4
|
+
|
|
5
|
+
const experience = (tenancy: unknown) =>
|
|
6
|
+
({ experience: { landingGate: LandingGatePreference.Allow, tenancy: tenancy as never } })
|
|
7
|
+
|
|
8
|
+
describe('viable-common - the project tenancy', () => {
|
|
9
|
+
test('NO_TENANCY is both flags off and cannot be changed', () => {
|
|
10
|
+
expect(NO_TENANCY).toEqual({ operators: false, users: false })
|
|
11
|
+
expect(Object.isFrozen(NO_TENANCY)).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('tenancyOf answers NO_TENANCY for no blueprint, no layer, no key and an unreadable value', () => {
|
|
15
|
+
expect(tenancyOf()).toBe(NO_TENANCY)
|
|
16
|
+
expect(tenancyOf(null)).toBe(NO_TENANCY)
|
|
17
|
+
expect(tenancyOf({})).toBe(NO_TENANCY)
|
|
18
|
+
expect(tenancyOf({ experience: { landingGate: LandingGatePreference.Allow } })).toBe(NO_TENANCY)
|
|
19
|
+
expect(tenancyOf(experience(null))).toBe(NO_TENANCY)
|
|
20
|
+
expect(tenancyOf(experience('both'))).toBe(NO_TENANCY)
|
|
21
|
+
expect(tenancyOf(experience({ operators: 'yes', users: 1 }))).toBe(NO_TENANCY)
|
|
22
|
+
expect(tenancyOf(experience({ operators: false, users: false }))).toBe(NO_TENANCY)
|
|
23
|
+
expect(tenancyOf({ experience: 'flat' as never })).toBe(NO_TENANCY)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('tenancyOf reads a literal true per flag, and an absent flag as off', () => {
|
|
27
|
+
expect(tenancyOf(experience({ operators: true, users: false }))).toEqual({ operators: true, users: false })
|
|
28
|
+
expect(tenancyOf(experience({ users: true }))).toEqual({ operators: false, users: true })
|
|
29
|
+
const both = tenancyOf(experience({ operators: true, users: true }))
|
|
30
|
+
expect(both).toEqual({ operators: true, users: true })
|
|
31
|
+
expect(Object.isFrozen(both)).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('tenantedArea: user follows users, operator follows operators, guest and admin never', () => {
|
|
35
|
+
const all = { operators: true, users: true }
|
|
36
|
+
expect(tenantedArea(ProjectArea.User, all)).toBe(true)
|
|
37
|
+
expect(tenantedArea(ProjectArea.Operator, all)).toBe(true)
|
|
38
|
+
expect(tenantedArea(ProjectArea.Guest, all)).toBe(false)
|
|
39
|
+
expect(tenantedArea(ProjectArea.Admin, all)).toBe(false)
|
|
40
|
+
|
|
41
|
+
expect(tenantedArea(ProjectArea.User, { operators: true, users: false })).toBe(false)
|
|
42
|
+
expect(tenantedArea(ProjectArea.Operator, { operators: false, users: true })).toBe(false)
|
|
43
|
+
for (const area of Object.values(ProjectArea)) {
|
|
44
|
+
expect(tenantedArea(area, NO_TENANCY)).toBe(false)
|
|
45
|
+
}
|
|
46
|
+
expect(tenantedArea('partner' as ProjectArea, all)).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
})
|