@stigmer/server 3.17.0 → 3.18.0
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/dist/boot/compose.d.ts +12 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +4 -2
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/iampolicy/constants.d.ts +3 -3
- package/dist/domain/iampolicy/constants.js +3 -3
- package/dist/domain/oauthapp/refresolution/refresolution.js +1 -1
- package/dist/domain/oauthapp/refresolution.js +1 -1
- package/dist/domain/platform/controller.d.ts +8 -1
- package/dist/domain/platform/controller.d.ts.map +1 -1
- package/dist/domain/platform/controller.js +2 -3
- package/dist/domain/platform/controller.js.map +1 -1
- package/dist/domain/plugin/constants.d.ts +7 -1
- package/dist/domain/plugin/constants.d.ts.map +1 -1
- package/dist/domain/plugin/constants.js +7 -1
- package/dist/domain/plugin/constants.js.map +1 -1
- package/dist/domain/plugin/materialize/plan.d.ts.map +1 -1
- package/dist/domain/plugin/materialize/plan.js +16 -1
- package/dist/domain/plugin/materialize/plan.js.map +1 -1
- package/dist/domain/plugin/members.d.ts +46 -0
- package/dist/domain/plugin/members.d.ts.map +1 -1
- package/dist/domain/plugin/members.js +31 -8
- package/dist/domain/plugin/members.js.map +1 -1
- package/dist/domain/plugin/push.d.ts +5 -3
- package/dist/domain/plugin/push.d.ts.map +1 -1
- package/dist/domain/plugin/push.js +58 -14
- package/dist/domain/plugin/push.js.map +1 -1
- package/dist/pipeline/apiresource-labels.d.ts +20 -1
- package/dist/pipeline/apiresource-labels.d.ts.map +1 -1
- package/dist/pipeline/apiresource-labels.js +17 -0
- package/dist/pipeline/apiresource-labels.js.map +1 -1
- package/dist/store/interface.d.ts +6 -2
- package/dist/store/interface.d.ts.map +1 -1
- package/dist/store/interface.js.map +1 -1
- package/package.json +5 -5
- package/src/boot/compose.ts +16 -2
- package/src/domain/iampolicy/constants.ts +3 -3
- package/src/domain/oauthapp/__tests__/oauthapp.test.ts +2 -2
- package/src/domain/oauthapp/__tests__/refresolution.test.ts +2 -2
- package/src/domain/oauthapp/refresolution/refresolution.ts +1 -1
- package/src/domain/oauthapp/refresolution.ts +1 -1
- package/src/domain/platform/__tests__/platform.test.ts +47 -0
- package/src/domain/platform/controller.ts +10 -4
- package/src/domain/plugin/__tests__/materialize.test.ts +42 -47
- package/src/domain/plugin/__tests__/members.test.ts +149 -0
- package/src/domain/plugin/__tests__/plugin.test.ts +219 -8
- package/src/domain/plugin/constants.ts +7 -1
- package/src/domain/plugin/materialize/plan.ts +20 -1
- package/src/domain/plugin/members.ts +76 -7
- package/src/domain/plugin/push.ts +80 -20
- package/src/pipeline/apiresource-labels.ts +26 -1
- package/src/sandbox/__tests__/local-process.integration.test.ts +20 -6
- package/src/store/interface.ts +6 -2
|
@@ -330,8 +330,8 @@ describe("oauthapp domain (encryption enabled)", () => {
|
|
|
330
330
|
|
|
331
331
|
it("blocks through the unique-slug fallback (ref pinned to a foreign org)", async () => {
|
|
332
332
|
const app = await ts.command.create(appInput());
|
|
333
|
-
//
|
|
334
|
-
// the user's own org — resolution reaches it via unique slug (#584).
|
|
333
|
+
// A public server's posture: ref pinned to `org: stigmer`, app applied
|
|
334
|
+
// in the user's own org — resolution reaches it via unique slug (#584).
|
|
335
335
|
await seedReferencingMcpServer(
|
|
336
336
|
ts.server.store,
|
|
337
337
|
"mcps_01reffallback",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pins the OAuthApp ref-resolution ladder against Go's
|
|
3
3
|
* refresolution_test.go table: exact (org, slug) wins; a UNIQUE slug-only
|
|
4
|
-
* match is honored across orgs (
|
|
4
|
+
* match is honored across orgs (a public server's `org: stigmer` ref, #584);
|
|
5
5
|
* ambiguity resolves to nothing; an empty slug is the DCR/manual-token
|
|
6
6
|
* arm, not a lookup.
|
|
7
7
|
*/
|
|
@@ -58,7 +58,7 @@ describe("resolveOAuthAppRef", () => {
|
|
|
58
58
|
expect(app?.metadata?.id).toBe("oaa_github_beta");
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
it("honors a UNIQUE slug-only match when the ref's org has no app (
|
|
61
|
+
it("honors a UNIQUE slug-only match when the ref's org has no app (a public server's ref, #584)", async () => {
|
|
62
62
|
// Two orgs hold 'github' and neither is 'stigmer': ambiguous → nothing.
|
|
63
63
|
expect(await resolveOAuthAppRef(store, ref("stigmer", "github"), silentLogger)).toBeUndefined();
|
|
64
64
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* 1. An exact (org, slug) match wins — unique by the create pipeline's
|
|
19
19
|
* duplicate check.
|
|
20
20
|
* 2. Otherwise a slug-only match is honored when it is UNIQUE. This lets
|
|
21
|
-
* a
|
|
21
|
+
* a deployment satisfy a public server's ref pinned to
|
|
22
22
|
* `org: stigmer` with an OAuthApp applied in the user's own org (#584).
|
|
23
23
|
* 3. Two or more slug matches with no exact hit resolve to nothing, with
|
|
24
24
|
* a WARN naming the candidate orgs: ambiguity is never silently
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* 1. An exact (org, slug) match wins. Uniqueness is guaranteed by the
|
|
19
19
|
* create pipeline's duplicate check.
|
|
20
20
|
* 2. Otherwise a slug-only match is honored when it is UNIQUE — what
|
|
21
|
-
* lets a
|
|
21
|
+
* lets a deployment satisfy a public server's ref pinned to
|
|
22
22
|
* `org: stigmer` with an OAuthApp applied in the user's own org.
|
|
23
23
|
* 3. Two or more slug matches with no exact hit resolve to nothing, with
|
|
24
24
|
* a WARN naming the candidate orgs: ambiguity is never silently
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* execution id;
|
|
16
16
|
* - getRunnerBootstrapConfig echoes the configured Temporal coordinates
|
|
17
17
|
* with the token fields empty (minting a proxy token is cloud-only);
|
|
18
|
+
* - getServerInfo reports the build stamp by default and the release a
|
|
19
|
+
* library consumer states through ComposeOptions.version otherwise
|
|
20
|
+
* (stigmer#1168: unbundled, the stamp is always "dev");
|
|
18
21
|
* - getLicenseStatus answers `absent` with no claims and no key when no
|
|
19
22
|
* provider is composed, and the composed provider's report verbatim
|
|
20
23
|
* when one is — both stamped with the ONE instant the controller
|
|
@@ -229,6 +232,48 @@ describe("platform domain (composed server)", () => {
|
|
|
229
232
|
});
|
|
230
233
|
});
|
|
231
234
|
|
|
235
|
+
describe("platform domain (composed with a stated version)", () => {
|
|
236
|
+
let server: ComposedServer;
|
|
237
|
+
let client: PlatformClient;
|
|
238
|
+
let dir: string;
|
|
239
|
+
|
|
240
|
+
beforeAll(async () => {
|
|
241
|
+
dir = mkdtempSync(path.join(tmpdir(), "platform-domain-version-test-"));
|
|
242
|
+
vi.stubEnv("STIGMER_ENCRYPTION_KEY", ENC_KEY.toString("base64"));
|
|
243
|
+
server = await composeServer({
|
|
244
|
+
config: loadConfig({
|
|
245
|
+
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
246
|
+
DB_PATH: path.join(dir, "stigmer.db"),
|
|
247
|
+
ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
|
|
248
|
+
}),
|
|
249
|
+
logger: silentLogger,
|
|
250
|
+
version: "3.99.0",
|
|
251
|
+
portOverride: 0,
|
|
252
|
+
host: "127.0.0.1",
|
|
253
|
+
});
|
|
254
|
+
const port = await server.start();
|
|
255
|
+
client = createClient(
|
|
256
|
+
PlatformQueryController,
|
|
257
|
+
createGrpcTransport({ baseUrl: `http://127.0.0.1:${port}` }),
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
afterAll(async () => {
|
|
262
|
+
await server.shutdown();
|
|
263
|
+
rmSync(dir, { recursive: true, force: true });
|
|
264
|
+
vi.unstubAllEnvs();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("getServerInfo reports the composition's stated release, not the build stamp", async () => {
|
|
268
|
+
// A library consumer (the cloud composition) runs no bundle, so the
|
|
269
|
+
// define is never stamped for it; ComposeOptions.version is how it
|
|
270
|
+
// states the release it installed. Edition is untouched by it.
|
|
271
|
+
const info = await client.getServerInfo({});
|
|
272
|
+
expect(info.version).toBe("3.99.0");
|
|
273
|
+
expect(info.edition).toBe(ServerEdition.oss);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
232
277
|
describe("platform domain (keyless runner-token service)", () => {
|
|
233
278
|
it("a keyless service answers not-minted — fail-soft, never an error", async () => {
|
|
234
279
|
const transport = createRouterTransport((router) => {
|
|
@@ -239,6 +284,7 @@ describe("platform domain (keyless runner-token service)", () => {
|
|
|
239
284
|
RunnerAuthService.create(undefined),
|
|
240
285
|
),
|
|
241
286
|
edition: ServerEdition.oss,
|
|
287
|
+
version: "dev",
|
|
242
288
|
licenseStatus: ABSENT_LICENSE_STATUS,
|
|
243
289
|
now: () => new Date(),
|
|
244
290
|
logger: silentLogger,
|
|
@@ -273,6 +319,7 @@ describe("platform domain (license status)", () => {
|
|
|
273
319
|
RunnerAuthService.create(undefined),
|
|
274
320
|
),
|
|
275
321
|
edition: ServerEdition.enterprise,
|
|
322
|
+
version: "dev",
|
|
276
323
|
licenseStatus: provider,
|
|
277
324
|
now: () => instant,
|
|
278
325
|
logger: silentLogger,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* getRunnerBootstrapConfig and getLicenseStatus shapes; getRunnerScopedToken
|
|
8
8
|
* is deliberately excluded there — its arms are exercised mid-execution)
|
|
9
9
|
* and __tests__/platform.test.ts (the fail-soft mint matrix, the license
|
|
10
|
-
* status arms, the #15 composed-server pattern).
|
|
10
|
+
* status arms, the stated-version arm, the #15 composed-server pattern).
|
|
11
11
|
*
|
|
12
12
|
* getLicenseStatus is the one arm every edition answers from a driver
|
|
13
13
|
* point: a server's license is a fact about the server, like its edition,
|
|
@@ -75,7 +75,6 @@ import type {
|
|
|
75
75
|
RunnerScopedTokenRequest,
|
|
76
76
|
} from "../../runnerauth/runner-credential-provider.js";
|
|
77
77
|
import { TOKEN_TYPE_EXECUTION_SCOPED } from "../../runnerauth/runnerauth.js";
|
|
78
|
-
import { SERVER_VERSION } from "./version.js";
|
|
79
78
|
|
|
80
79
|
export interface PlatformControllerDeps {
|
|
81
80
|
/**
|
|
@@ -100,6 +99,13 @@ export interface PlatformControllerDeps {
|
|
|
100
99
|
* cloud composition answers `cloud` without forking this controller.
|
|
101
100
|
*/
|
|
102
101
|
readonly edition: ServerEdition;
|
|
102
|
+
/**
|
|
103
|
+
* The release reported beside the edition. The composition root resolves
|
|
104
|
+
* it (ComposeOptions.version, else SERVER_VERSION), so this controller
|
|
105
|
+
* never reads the build stamp itself: a library consumer with no bundle
|
|
106
|
+
* states its installed release and answers exactly as a bundle would.
|
|
107
|
+
*/
|
|
108
|
+
readonly version: string;
|
|
103
109
|
/**
|
|
104
110
|
* What license this server holds (extensions/license-status.ts). The
|
|
105
111
|
* composition root installs the built-in `absent` provider when no unit
|
|
@@ -130,11 +136,11 @@ export function registerPlatformServices(
|
|
|
130
136
|
});
|
|
131
137
|
}
|
|
132
138
|
|
|
133
|
-
/** Go GetServerInfo: the server edition and
|
|
139
|
+
/** Go GetServerInfo: the server edition and release version. */
|
|
134
140
|
function getServerInfo(deps: PlatformControllerDeps): GetServerInfoOutput {
|
|
135
141
|
return create(GetServerInfoOutputSchema, {
|
|
136
142
|
edition: deps.edition,
|
|
137
|
-
version:
|
|
143
|
+
version: deps.version,
|
|
138
144
|
});
|
|
139
145
|
}
|
|
140
146
|
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* SKILL.md with the plugin's labels on the request, a version the tag
|
|
9
9
|
* pattern rejects becoming a warning, an overlay that redefines the
|
|
10
10
|
* transport refused, the composed agent declaring its tools' variables
|
|
11
|
-
* (OAuth targets excluded, an authored agent left as written), and
|
|
12
|
-
*
|
|
11
|
+
* (OAuth targets excluded, an authored agent left as written), and each
|
|
12
|
+
* planned member's system flag read from its overlay and never from a
|
|
13
|
+
* skill. The member rules themselves (the slug decision, convergence, the
|
|
14
|
+
* dropped set) are members.test.ts's.
|
|
13
15
|
*/
|
|
14
16
|
import { describe, expect, it } from "vitest";
|
|
15
17
|
|
|
@@ -37,6 +39,7 @@ import { openArchive } from "../../../archive/open.js";
|
|
|
37
39
|
import {
|
|
38
40
|
PLUGIN_LABEL,
|
|
39
41
|
PLUGIN_VERSION_LABEL,
|
|
42
|
+
SYSTEM_LABEL,
|
|
40
43
|
} from "../../../pipeline/apiresource-labels.js";
|
|
41
44
|
import {
|
|
42
45
|
DEFAULT_AGENT_INSTRUCTIONS_VERSION,
|
|
@@ -45,8 +48,6 @@ import {
|
|
|
45
48
|
import type { PluginIdentity } from "../materialize/identity.js";
|
|
46
49
|
import { McpServerOverlayError } from "../materialize/mcp-servers.js";
|
|
47
50
|
import { planMaterialization } from "../materialize/plan.js";
|
|
48
|
-
import { droppedMembers, membersConverge } from "../members.js";
|
|
49
|
-
import type { Member } from "../members.js";
|
|
50
51
|
|
|
51
52
|
const IDENTITY: PluginIdentity = {
|
|
52
53
|
org: "acme",
|
|
@@ -231,7 +232,10 @@ describe("planMaterialization", () => {
|
|
|
231
232
|
|
|
232
233
|
it("declares the servers' variables on the composed agent so a session asks for them", () => {
|
|
233
234
|
expect(plan.agent!.resource.spec?.env).toEqual({
|
|
234
|
-
GITHUB_TOKEN: expect.objectContaining({
|
|
235
|
+
GITHUB_TOKEN: expect.objectContaining({
|
|
236
|
+
isSecret: true,
|
|
237
|
+
optional: false,
|
|
238
|
+
}),
|
|
235
239
|
});
|
|
236
240
|
});
|
|
237
241
|
|
|
@@ -259,7 +263,9 @@ describe("planMaterialization", () => {
|
|
|
259
263
|
);
|
|
260
264
|
// The server still declares it (the connect flow reads it there); the
|
|
261
265
|
// agent does not, because the value comes from a managed environment.
|
|
262
|
-
expect(withOAuth.mcpServers[0]!.resource.spec?.env).toHaveProperty(
|
|
266
|
+
expect(withOAuth.mcpServers[0]!.resource.spec?.env).toHaveProperty(
|
|
267
|
+
"GITHUB_TOKEN",
|
|
268
|
+
);
|
|
263
269
|
expect(withOAuth.agent!.resource.spec?.env).toEqual({});
|
|
264
270
|
});
|
|
265
271
|
|
|
@@ -313,50 +319,39 @@ describe("planMaterialization", () => {
|
|
|
313
319
|
});
|
|
314
320
|
});
|
|
315
321
|
|
|
316
|
-
describe("members:
|
|
317
|
-
const
|
|
318
|
-
const stored: Member[] = [
|
|
319
|
-
{
|
|
320
|
-
kind: ApiResourceKind.skill,
|
|
321
|
-
id: "skl_1",
|
|
322
|
-
slug: "thermo-review",
|
|
323
|
-
name: "thermo-review",
|
|
324
|
-
version: digest,
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
kind: ApiResourceKind.agent,
|
|
328
|
-
id: "agt_1",
|
|
329
|
-
slug: "thermos",
|
|
330
|
-
name: "thermos",
|
|
331
|
-
version: digest,
|
|
332
|
-
},
|
|
333
|
-
];
|
|
334
|
-
const planned = [
|
|
335
|
-
{
|
|
336
|
-
kind: ApiResourceKind.skill,
|
|
337
|
-
slug: "thermo-review",
|
|
338
|
-
name: "thermo-review",
|
|
339
|
-
},
|
|
340
|
-
{ kind: ApiResourceKind.agent, slug: "thermos", name: "thermos" },
|
|
341
|
-
];
|
|
322
|
+
describe("planned members: the system flag", () => {
|
|
323
|
+
const pkg = read(thermos);
|
|
342
324
|
|
|
343
|
-
it("
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
325
|
+
it("reads system content from the overlay's labels, never from a skill's request", () => {
|
|
326
|
+
const authored = create(AgentSchema, {
|
|
327
|
+
metadata: { name: "thermos", labels: { [SYSTEM_LABEL]: "true" } },
|
|
328
|
+
spec: create(AgentSpecSchema, { instructions: "You read thermostats." }),
|
|
329
|
+
});
|
|
330
|
+
const plan = planMaterialization(
|
|
331
|
+
pkg,
|
|
332
|
+
inMemoryPluginFiles(thermos),
|
|
333
|
+
{
|
|
334
|
+
agent: { path: "ai.stigmer/agent.yaml", resource: authored },
|
|
335
|
+
workflows: [],
|
|
336
|
+
mcpServers: [],
|
|
337
|
+
},
|
|
338
|
+
IDENTITY,
|
|
339
|
+
"",
|
|
340
|
+
);
|
|
341
|
+
const byKind = new Map(plan.members.map((m) => [m.kind, m.system]));
|
|
342
|
+
expect(byKind.get(ApiResourceKind.agent)).toBe(true);
|
|
343
|
+
expect(byKind.get(ApiResourceKind.skill)).toBe(false);
|
|
344
|
+
expect(byKind.get(ApiResourceKind.mcp_server)).toBe(false);
|
|
354
345
|
});
|
|
355
346
|
|
|
356
|
-
it("
|
|
357
|
-
|
|
358
|
-
|
|
347
|
+
it("plans no system content when no overlay declares it", () => {
|
|
348
|
+
const plan = planMaterialization(
|
|
349
|
+
pkg,
|
|
350
|
+
inMemoryPluginFiles(thermos),
|
|
351
|
+
{ workflows: [], mcpServers: [] },
|
|
352
|
+
IDENTITY,
|
|
353
|
+
"",
|
|
359
354
|
);
|
|
360
|
-
expect(
|
|
355
|
+
expect(plan.members.every((m) => m.system === false)).toBe(true);
|
|
361
356
|
});
|
|
362
357
|
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the member rules on plain values, no store: the slug decision
|
|
3
|
+
* (`judgeSlug`) in each of its five arms — free, ours, adopt when system
|
|
4
|
+
* content meets system content, refused for an unmanaged row whenever
|
|
5
|
+
* either side lacks the label, refused for another plugin's member
|
|
6
|
+
* whatever labels either side carries — and the convergence and drop rules
|
|
7
|
+
* over stored and planned members. The same decision through a composed
|
|
8
|
+
* server, with a real authorizer, is plugin.test.ts's.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
13
|
+
|
|
14
|
+
import { droppedMembers, judgeSlug, membersConverge } from "../members.js";
|
|
15
|
+
import type { Member, PlannedMember, SlugHolder } from "../members.js";
|
|
16
|
+
|
|
17
|
+
const DIGEST = "d".repeat(64);
|
|
18
|
+
const OURS = "plg_ours";
|
|
19
|
+
const OTHER = "plg_other";
|
|
20
|
+
|
|
21
|
+
function stored(overrides: Partial<Member> = {}): Member {
|
|
22
|
+
return {
|
|
23
|
+
kind: ApiResourceKind.agent,
|
|
24
|
+
id: "agt_1",
|
|
25
|
+
slug: "assistant",
|
|
26
|
+
name: "assistant",
|
|
27
|
+
version: DIGEST,
|
|
28
|
+
system: false,
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function planned(overrides: Partial<PlannedMember> = {}): PlannedMember {
|
|
34
|
+
return {
|
|
35
|
+
kind: ApiResourceKind.agent,
|
|
36
|
+
slug: "assistant",
|
|
37
|
+
name: "assistant",
|
|
38
|
+
system: false,
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function heldBy(
|
|
44
|
+
byPlugin: string | undefined,
|
|
45
|
+
holder: Member = stored(),
|
|
46
|
+
): SlugHolder {
|
|
47
|
+
return { held: true, byPlugin, holder };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("judgeSlug", () => {
|
|
51
|
+
it("is free when nothing holds the slug", () => {
|
|
52
|
+
expect(judgeSlug({ held: false }, planned(), OURS)).toEqual({
|
|
53
|
+
kind: "free",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("is ours when this plugin already holds it, whatever the labels", () => {
|
|
58
|
+
const holder = stored({ system: true });
|
|
59
|
+
expect(judgeSlug(heldBy(OURS, holder), planned(), OURS)).toEqual({
|
|
60
|
+
kind: "ours",
|
|
61
|
+
holder,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("adopts an unmanaged row only when both sides are system content", () => {
|
|
66
|
+
const holder = stored({ system: true });
|
|
67
|
+
expect(
|
|
68
|
+
judgeSlug(heldBy(undefined, holder), planned({ system: true }), OURS),
|
|
69
|
+
).toEqual({ kind: "adopt", holder });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("refuses an unmanaged row when either side lacks the system label", () => {
|
|
73
|
+
const userRow = stored({ system: false });
|
|
74
|
+
expect(
|
|
75
|
+
judgeSlug(heldBy(undefined, userRow), planned({ system: true }), OURS),
|
|
76
|
+
).toEqual({ kind: "held-unmanaged", holder: userRow });
|
|
77
|
+
|
|
78
|
+
const systemRow = stored({ system: true });
|
|
79
|
+
expect(
|
|
80
|
+
judgeSlug(heldBy(undefined, systemRow), planned({ system: false }), OURS),
|
|
81
|
+
).toEqual({ kind: "held-unmanaged", holder: systemRow });
|
|
82
|
+
|
|
83
|
+
expect(
|
|
84
|
+
judgeSlug(heldBy(undefined, userRow), planned({ system: false }), OURS),
|
|
85
|
+
).toEqual({ kind: "held-unmanaged", holder: userRow });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("never transfers another plugin's member, even system content to system content", () => {
|
|
89
|
+
const holder = stored({ system: true });
|
|
90
|
+
expect(
|
|
91
|
+
judgeSlug(heldBy(OTHER, holder), planned({ system: true }), OURS),
|
|
92
|
+
).toEqual({ kind: "held-by-plugin", holder, pluginId: OTHER });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("members: convergence and drops", () => {
|
|
97
|
+
const stored: Member[] = [
|
|
98
|
+
{
|
|
99
|
+
kind: ApiResourceKind.skill,
|
|
100
|
+
id: "skl_1",
|
|
101
|
+
slug: "thermo-review",
|
|
102
|
+
name: "thermo-review",
|
|
103
|
+
version: DIGEST,
|
|
104
|
+
system: false,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
kind: ApiResourceKind.agent,
|
|
108
|
+
id: "agt_1",
|
|
109
|
+
slug: "thermos",
|
|
110
|
+
name: "thermos",
|
|
111
|
+
version: DIGEST,
|
|
112
|
+
system: false,
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
const planned: PlannedMember[] = [
|
|
116
|
+
{
|
|
117
|
+
kind: ApiResourceKind.skill,
|
|
118
|
+
slug: "thermo-review",
|
|
119
|
+
name: "thermo-review",
|
|
120
|
+
system: false,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
kind: ApiResourceKind.agent,
|
|
124
|
+
slug: "thermos",
|
|
125
|
+
name: "thermos",
|
|
126
|
+
system: false,
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
it("converges only when every planned member is present and stamped with the digest", () => {
|
|
131
|
+
expect(membersConverge(stored, planned, DIGEST)).toBe(true);
|
|
132
|
+
expect(membersConverge(stored, planned, "e".repeat(64))).toBe(false);
|
|
133
|
+
expect(membersConverge(stored.slice(1), planned, DIGEST)).toBe(false);
|
|
134
|
+
expect(
|
|
135
|
+
membersConverge(
|
|
136
|
+
[...stored, { ...stored[0]!, slug: "extra", id: "skl_2" }],
|
|
137
|
+
planned,
|
|
138
|
+
DIGEST,
|
|
139
|
+
),
|
|
140
|
+
).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("drops the members the plan no longer names", () => {
|
|
144
|
+
expect(droppedMembers(stored, planned.slice(1)).map((m) => m.slug)).toEqual(
|
|
145
|
+
["thermo-review"],
|
|
146
|
+
);
|
|
147
|
+
expect(droppedMembers(stored, planned)).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
});
|