@platforma-sdk/block-tools 2.13.0 → 2.14.1
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/cli.js +12 -12
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +728 -648
- package/dist/cli.mjs.map +1 -1
- package/dist/cmd/build-kind-manifest.d.ts +7 -0
- package/dist/config-CtXMWNgm.js +3 -0
- package/dist/config-CtXMWNgm.js.map +1 -0
- package/dist/{config-CiC347sB.mjs → config-Dx45aFJ_.mjs} +792 -456
- package/dist/config-Dx45aFJ_.mjs.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +15 -15
- package/dist/structure/engine/api.d.ts +1 -1
- package/dist/structure/rules/kind-package-json.d.ts +3 -0
- package/dist/structure/rules/kind.d.ts +1 -0
- package/dist/v2/build_kind_dist.d.ts +25 -0
- package/dist/v2/index.d.ts +4 -0
- package/dist/v2/kind/resolve-refs.d.ts +63 -0
- package/dist/v2/kind/version-match.d.ts +23 -0
- package/dist/v2/model/block_description.d.ts +8 -1
- package/dist/v2/publish-block.d.ts +23 -0
- package/dist/v2/registry/index.d.ts +2 -0
- package/dist/v2/registry/kind_resolver.d.ts +121 -0
- package/dist/v2/registry/registry.d.ts +23 -0
- package/dist/v2/registry/registry_reader.d.ts +34 -1
- package/dist/v2/registry/schema_kinds.d.ts +632 -0
- package/dist/v2/registry/schema_public.d.ts +72 -0
- package/package.json +7 -7
- package/src/cli.test.ts +2 -1
- package/src/cli.ts +2 -0
- package/src/cmd/build-kind-manifest.ts +41 -0
- package/src/cmd/publish.ts +10 -1
- package/src/structure/__tests__/model-package-json.snapshot.test.ts +1 -0
- package/src/structure/engine/api.ts +1 -1
- package/src/structure/engine/ctx.ts +1 -0
- package/src/structure/engine/discovery.ts +16 -2
- package/src/structure/rules/block-package-json.ts +18 -0
- package/src/structure/rules/kind-package-json.ts +117 -0
- package/src/structure/rules/kind.ts +29 -0
- package/src/structure/rules/model-package-json.ts +8 -0
- package/src/structure/rules/model.ts +15 -13
- package/src/structure/rules/root-pnpm-workspace.ts +1 -0
- package/src/structure/rules/shared/pascal-case.ts +1 -1
- package/src/structure/structure-definition.ts +2 -0
- package/src/structure/templates/static/kind/.oxfmtrc.json +4 -0
- package/src/structure/templates/static/kind/.oxlintrc.json +3 -0
- package/src/structure/templates/static/kind/src/index.ts +49 -0
- package/src/structure/templates/static/kind/tsconfig.json +10 -0
- package/src/structure/templates/text/model/src/index.tpl.ts +18 -0
- package/src/v2/build_dist.ts +35 -0
- package/src/v2/build_kind_dist.ts +104 -0
- package/src/v2/from_pack_v2.test.ts +2 -2
- package/src/v2/index.ts +4 -0
- package/src/v2/kind/resolve-refs.ts +150 -0
- package/src/v2/kind/version-match.test.ts +60 -0
- package/src/v2/kind/version-match.ts +55 -0
- package/src/v2/model/block_description.ts +15 -4
- package/src/v2/publish-block.ts +60 -0
- package/src/v2/registry/index.ts +2 -0
- package/src/v2/registry/kind_resolver.test.ts +123 -0
- package/src/v2/registry/kind_resolver.ts +190 -0
- package/src/v2/registry/registry.test.ts +247 -1
- package/src/v2/registry/registry.ts +227 -4
- package/src/v2/registry/registry_reader.ts +68 -0
- package/src/v2/registry/schema_kinds.test.ts +65 -0
- package/src/v2/registry/schema_kinds.ts +169 -0
- package/dist/config-BmJWc8sR.js +0 -3
- package/dist/config-BmJWc8sR.js.map +0 -1
- package/dist/config-CiC347sB.mjs.map +0 -1
- package/src/structure/templates/static/model/src/index.ts +0 -13
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { BlockPackId } from "@milaboratories/pl-model-middle-layer";
|
|
2
|
+
import { AnyChannel, StableChannel } from "@milaboratories/pl-model-middle-layer";
|
|
3
|
+
import * as semver from "semver";
|
|
4
|
+
import type { KindOverview } from "./schema_kinds";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* PURE, IO-free block-kind resolution core.
|
|
8
|
+
*
|
|
9
|
+
* Given a parsed {@link KindOverview} projection (fetched by the reader) and a
|
|
10
|
+
* version selector, this module answers "which concrete implementing block
|
|
11
|
+
* satisfies `{name}@{selector}`?" with a discriminated {@link KindResolution}.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is fed literals in unit tests — no `FolderReader`, no cache,
|
|
14
|
+
* no `semver` use beyond range math. This is the net-new, correctness-critical
|
|
15
|
+
* seam (implementation-path §6): selector→range translation and channel
|
|
16
|
+
* selection each have edges worth isolating and testing.
|
|
17
|
+
*
|
|
18
|
+
* Naming: this module never parses the `{name}@{version}` *reference* string
|
|
19
|
+
* (that codec is `parseKindRef`, block_kind_ref.ts) nor the npm-name→path split
|
|
20
|
+
* (`npmNameToKindPath`, schema_kinds.ts). It consumes an already-split selector.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The three version-selector tiers of the redefined-semver scheme
|
|
25
|
+
* (01-kind-and-lifecycle.md:63):
|
|
26
|
+
* - `exact` — `@X.Y.Z`, pin to one version.
|
|
27
|
+
* - `patch` — `~X.Y.Z`, behavior frozen (patch floats).
|
|
28
|
+
* - `minor` — `^X.Y.Z`, behavior floats (minor floats).
|
|
29
|
+
*/
|
|
30
|
+
export type SelectorOp = "exact" | "patch" | "minor";
|
|
31
|
+
|
|
32
|
+
export interface Selector {
|
|
33
|
+
op: SelectorOp;
|
|
34
|
+
version: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Split a raw selector string into its {@link Selector} operator + version.
|
|
39
|
+
*
|
|
40
|
+
* The leading character encodes the tier: `~`→patch, `^`→minor, `@`→exact.
|
|
41
|
+
* A bare version (no recognized leading operator) is treated as `exact`. Note
|
|
42
|
+
* that `@` as a *leading* char is redundant with the bare form and accepted
|
|
43
|
+
* only because the reference form is `{name}@{version}` — after `parseKindRef`
|
|
44
|
+
* splits on the final `@`, the version half is normally bare or `~`/`^`-prefixed.
|
|
45
|
+
*/
|
|
46
|
+
export function parseSelector(raw: string): Selector {
|
|
47
|
+
const s = raw.trim();
|
|
48
|
+
switch (s[0]) {
|
|
49
|
+
case "~":
|
|
50
|
+
return { op: "patch", version: s.slice(1) };
|
|
51
|
+
case "^":
|
|
52
|
+
return { op: "minor", version: s.slice(1) };
|
|
53
|
+
case "@":
|
|
54
|
+
return { op: "exact", version: s.slice(1) };
|
|
55
|
+
default:
|
|
56
|
+
return { op: "exact", version: s };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Map a {@link Selector} to a semver range string:
|
|
62
|
+
* - `exact` — `=X.Y.Z`
|
|
63
|
+
* - `patch` — `>=X.Y.Z <X.(Y+1).0-0`, behavior frozen, params float
|
|
64
|
+
* - `minor` — `>=X.Y.Z <(X+1).0.0-0`, behavior floats, never crosses a params-break
|
|
65
|
+
*
|
|
66
|
+
* The kind scheme redefines the *meaning* of major/minor/patch, not the range
|
|
67
|
+
* arithmetic — so the arithmetic is written out here rather than delegated to
|
|
68
|
+
* npm's `~`/`^` operators, which are not major-agnostic. Below 1.0.0 npm expands
|
|
69
|
+
* both `^0.2.3` and `~0.2.3` to `>=0.2.3 <0.3.0-0`, treating a `0.x` minor as the
|
|
70
|
+
* breaking boundary per ecosystem convention. Under the kind scheme a
|
|
71
|
+
* params-break is a MAJOR bump at every major including zero, and nothing
|
|
72
|
+
* guarantees a kind starts at `1.0.0`, so borrowing `^` would silently collapse
|
|
73
|
+
* `patch` and `minor` into one tier for any `0.x` kind — erasing the distinction
|
|
74
|
+
* the whole scheme rests on.
|
|
75
|
+
*
|
|
76
|
+
* For `X >= 1` these forms are equivalent to `~`/`^`, down to npm's `-0`
|
|
77
|
+
* upper-bound suffix that keeps prereleases of the boundary out of range.
|
|
78
|
+
*
|
|
79
|
+
* @throws if `sel.version` is not a valid semver version. {@link parseSelector}
|
|
80
|
+
* does not validate it; the model-side `parseKindSelector` does.
|
|
81
|
+
*/
|
|
82
|
+
export function selectorToRange(sel: Selector): string {
|
|
83
|
+
if (sel.op === "exact") return `=${sel.version}`;
|
|
84
|
+
const major = semver.major(sel.version);
|
|
85
|
+
const minor = semver.minor(sel.version);
|
|
86
|
+
const upperBound = sel.op === "patch" ? `${major}.${minor + 1}.0` : `${major + 1}.0.0`;
|
|
87
|
+
return `>=${sel.version} <${upperBound}-0`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Outcome of resolving a kind reference against its overview projection.
|
|
92
|
+
*
|
|
93
|
+
* The three failure reasons are distinct on purpose (the caller maps each to a
|
|
94
|
+
* different user-facing error):
|
|
95
|
+
* - `no-matching-kind-version` — the selector satisfies zero kind versions
|
|
96
|
+
* (`maxSatisfying` returned `null`).
|
|
97
|
+
* - `no-implementation` — a kind version matched, but it has no implementing
|
|
98
|
+
* block at all.
|
|
99
|
+
* - `no-stable-implementation` — implementing blocks exist for the matched
|
|
100
|
+
* kind version, but none is on the `stable` channel and `allowUnstable` is
|
|
101
|
+
* off.
|
|
102
|
+
*/
|
|
103
|
+
export type KindResolution =
|
|
104
|
+
| { ok: true; blockId: BlockPackId; channel: string }
|
|
105
|
+
| { ok: false; reason: "no-matching-kind-version" }
|
|
106
|
+
| { ok: false; reason: "no-implementation" }
|
|
107
|
+
| { ok: false; reason: "no-stable-implementation" };
|
|
108
|
+
|
|
109
|
+
export type KindResolutionReason = Extract<KindResolution, { ok: false }>["reason"];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Typed error the reader/facade throw when {@link resolveKind} fails, carrying
|
|
113
|
+
* the discriminated {@link KindResolutionReason} and the offending reference so
|
|
114
|
+
* a caller can map it to a spec-level error without re-deriving the reason.
|
|
115
|
+
*/
|
|
116
|
+
export class KindResolutionError extends Error {
|
|
117
|
+
constructor(
|
|
118
|
+
public readonly reason: KindResolutionReason,
|
|
119
|
+
public readonly ref: string,
|
|
120
|
+
) {
|
|
121
|
+
super(`Cannot resolve block kind "${ref}": ${reason}`);
|
|
122
|
+
this.name = "KindResolutionError";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Newest implementing block for `(kindVersion, channel)` from the flat
|
|
128
|
+
* `implementers` list — the RMW source of truth. Newest by *block* version via
|
|
129
|
+
* `semver.gt`. Under `allowUnstable` every implementer of the kind version is
|
|
130
|
+
* eligible regardless of channel membership (the `AnyChannel` view); otherwise
|
|
131
|
+
* only implementers published to `channel` count.
|
|
132
|
+
*/
|
|
133
|
+
function pickNewestImplementer(
|
|
134
|
+
implementers: KindOverview["implementers"],
|
|
135
|
+
kindVersion: string,
|
|
136
|
+
channel: string,
|
|
137
|
+
allowUnstable: boolean,
|
|
138
|
+
): BlockPackId | undefined {
|
|
139
|
+
const candidates = implementers.filter(
|
|
140
|
+
(i) => semver.eq(i.kindVersion, kindVersion) && (allowUnstable || i.channels.includes(channel)),
|
|
141
|
+
);
|
|
142
|
+
if (candidates.length === 0) return undefined;
|
|
143
|
+
return candidates.reduce((a, b) => (semver.gt(b.id.version, a.id.version) ? b : a)).id;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve a kind reference to a concrete implementing block.
|
|
148
|
+
*
|
|
149
|
+
* 1. Pick the newest kind version satisfying the selector via
|
|
150
|
+
* `semver.maxSatisfying` over the overview's `kindVersions`.
|
|
151
|
+
* 2. Select the target channel: `stable` by default, `any` when `allowUnstable`
|
|
152
|
+
* is set (the apply-time flag).
|
|
153
|
+
* 3. Pick the newest implementing block in that `(version, channel)` via
|
|
154
|
+
* `semver.gt`, reading the flat `implementers` source of truth and falling
|
|
155
|
+
* back to the derived `latestByChannel` projection for older overviews.
|
|
156
|
+
*
|
|
157
|
+
* @param overview parsed & normalized kind projection (reader-supplied).
|
|
158
|
+
* @param selector either a raw selector string (`^1.2.3`, `~1.2.3`, `1.2.3`)
|
|
159
|
+
* or a pre-parsed {@link Selector}.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveKind(
|
|
162
|
+
overview: KindOverview,
|
|
163
|
+
selector: string | Selector,
|
|
164
|
+
opt: { allowUnstable: boolean },
|
|
165
|
+
): KindResolution {
|
|
166
|
+
const sel = typeof selector === "string" ? parseSelector(selector) : selector;
|
|
167
|
+
const range = selectorToRange(sel);
|
|
168
|
+
|
|
169
|
+
const versions = overview.kindVersions.map((v) => v.kindVersion);
|
|
170
|
+
const picked = semver.maxSatisfying(versions, range);
|
|
171
|
+
if (picked === null) return { ok: false, reason: "no-matching-kind-version" };
|
|
172
|
+
|
|
173
|
+
const entry = overview.kindVersions.find((v) => semver.eq(v.kindVersion, picked));
|
|
174
|
+
const channel = opt.allowUnstable ? AnyChannel : StableChannel;
|
|
175
|
+
|
|
176
|
+
const blockId =
|
|
177
|
+
pickNewestImplementer(overview.implementers, picked, channel, opt.allowUnstable) ??
|
|
178
|
+
entry?.latestByChannel[channel];
|
|
179
|
+
if (blockId) return { ok: true, blockId, channel };
|
|
180
|
+
|
|
181
|
+
// Nothing in the target channel. Distinguish "no implementer at all" from
|
|
182
|
+
// "implementers exist but none stable". When `allowUnstable` is set the target
|
|
183
|
+
// channel is `any`, so an empty result here always means zero implementers.
|
|
184
|
+
const hasAnyImpl =
|
|
185
|
+
overview.implementers.some((i) => semver.eq(i.kindVersion, picked)) ||
|
|
186
|
+
(entry !== undefined && Object.keys(entry.latestByChannel).length > 0);
|
|
187
|
+
|
|
188
|
+
if (!hasAnyImpl) return { ok: false, reason: "no-implementation" };
|
|
189
|
+
return { ok: false, reason: "no-stable-implementation" };
|
|
190
|
+
}
|
|
@@ -4,9 +4,15 @@ import { randomUUID } from "crypto";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import fsp from "fs/promises";
|
|
6
6
|
import { BlockRegistryV2 } from "./registry";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
UpdateSuggestions,
|
|
9
|
+
BlockPackManifest,
|
|
10
|
+
StableChannel,
|
|
11
|
+
} from "@milaboratories/pl-model-middle-layer";
|
|
8
12
|
import { inferUpdateSuggestions } from "./registry_reader";
|
|
9
13
|
import { OverviewSnapshotsPrefix } from "./schema_internal";
|
|
14
|
+
import { KindOverview, kindOverviewPath, npmNameToKindPath } from "./schema_kinds";
|
|
15
|
+
import { resolveKind } from "./kind_resolver";
|
|
10
16
|
|
|
11
17
|
type TestStorageInstance = {
|
|
12
18
|
storage: RegistryStorage;
|
|
@@ -260,6 +266,246 @@ test.each(testStorages)(
|
|
|
260
266
|
},
|
|
261
267
|
);
|
|
262
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Fixture for the kind-projection test: a block version whose description
|
|
271
|
+
* carries a `{name}@{version}` kind reference. Only the id and that reference
|
|
272
|
+
* matter to the reconciler's kind pass.
|
|
273
|
+
*/
|
|
274
|
+
const kindTestKindNpmName = "@platforma-open/kindorg.kindpkg.kind";
|
|
275
|
+
/** A second, unrelated kind — the migration target in the kind-change test. */
|
|
276
|
+
const kindTestOtherKindNpmName = "@platforma-open/kindorg.otherpkg.kind";
|
|
277
|
+
|
|
278
|
+
function kindImplementingManifest(
|
|
279
|
+
version: string,
|
|
280
|
+
kindVersion: string,
|
|
281
|
+
kindNpmName: string = kindTestKindNpmName,
|
|
282
|
+
): BlockPackManifest {
|
|
283
|
+
return BlockPackManifest.parse({
|
|
284
|
+
schema: "v2",
|
|
285
|
+
description: {
|
|
286
|
+
id: { organization: "testorg", name: "kindblk", version },
|
|
287
|
+
title: "Kind implementing block",
|
|
288
|
+
summary: "Test package",
|
|
289
|
+
kind: `${kindNpmName}@${kindVersion}`,
|
|
290
|
+
components: {
|
|
291
|
+
workflow: { type: "workflow-v1", main: { type: "relative", path: "workflow.json" } },
|
|
292
|
+
model: { type: "relative", path: "model.json" },
|
|
293
|
+
ui: { type: "relative", path: "ui.json" },
|
|
294
|
+
},
|
|
295
|
+
meta: {
|
|
296
|
+
title: "Kind implementing block",
|
|
297
|
+
description: "Test package description",
|
|
298
|
+
organization: { name: "Test Organization", url: "https://test.com" },
|
|
299
|
+
tags: [],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
files: [
|
|
303
|
+
{
|
|
304
|
+
name: "model.json",
|
|
305
|
+
size: 13,
|
|
306
|
+
sha256: "6FD977DB9B2AFE87A9CEEE48432881299A6AAF83D935FBBE83007660287F9C2E",
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const kindTestFileReader = async (fileName: string) => {
|
|
313
|
+
if (fileName === "model.json") return Buffer.from('{"test":true}');
|
|
314
|
+
throw new Error(`Unknown file: ${fileName}`);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Channel membership is per *block version* (`v2/{org}/{name}/{version}/channels/{channel}`),
|
|
319
|
+
* and every writer of a channel marker drops an update seed for that same
|
|
320
|
+
* version. So a `normal` (read-modify-write) reconcile pass always re-reads the
|
|
321
|
+
* versions whose membership changed, and the per-entry `channels` snapshot
|
|
322
|
+
* stored in `kinds/{org}/{name}/overview.json` cannot go stale behind it.
|
|
323
|
+
*
|
|
324
|
+
* This test pins that invariant on both sides of a stable-channel handover:
|
|
325
|
+
* adding `stable` to a newer version (both versions stable — marking is not a
|
|
326
|
+
* moving pointer, so the older one keeps its own membership) and then removing
|
|
327
|
+
* `stable` from the older one. Either step must leave every entry's snapshot
|
|
328
|
+
* equal to the markers actually present in storage, and stable resolution must
|
|
329
|
+
* follow. Regression guard: any future change that mutates a marker without
|
|
330
|
+
* seeding that exact version — or that turns `stable` into a single moving
|
|
331
|
+
* pointer — leaves the kind index asserting a version is stable when it is not.
|
|
332
|
+
*/
|
|
333
|
+
test.each(testStorages)(
|
|
334
|
+
"channel handover keeps kind overview channel snapshots correct in normal mode with $name",
|
|
335
|
+
async ({ storageProvider }) => {
|
|
336
|
+
const { storage, teardown } = storageProvider();
|
|
337
|
+
const registry = new BlockRegistryV2(storage, undefined, { skipSnapshotCreation: true });
|
|
338
|
+
|
|
339
|
+
const ovPath = kindOverviewPath(npmNameToKindPath(kindTestKindNpmName));
|
|
340
|
+
const readKindOverview = async (): Promise<KindOverview> => {
|
|
341
|
+
const content = await storage.getFile(ovPath);
|
|
342
|
+
expect(content).toBeDefined();
|
|
343
|
+
return KindOverview.parse(JSON.parse(content!.toString()));
|
|
344
|
+
};
|
|
345
|
+
const channelsOf = (overview: KindOverview, version: string): string[] => {
|
|
346
|
+
const entry = overview.implementers.find((i) => i.id.version === version);
|
|
347
|
+
expect(entry, `no implementer entry for ${version}`).toBeDefined();
|
|
348
|
+
return [...entry!.channels].sort();
|
|
349
|
+
};
|
|
350
|
+
const stableResolution = (overview: KindOverview) =>
|
|
351
|
+
resolveKind(overview, "1.0.0", { allowUnstable: false });
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
// Two versions of one block, both implementing kind 1.0.0.
|
|
355
|
+
const a = kindImplementingManifest("1.0.0", "1.0.0");
|
|
356
|
+
const b = kindImplementingManifest("2.0.0", "1.0.0");
|
|
357
|
+
await registry.publishPackage(a, kindTestFileReader);
|
|
358
|
+
await registry.publishPackage(b, kindTestFileReader);
|
|
359
|
+
|
|
360
|
+
// A stable, reconcile.
|
|
361
|
+
await registry.addPackageToChannel(a.description.id, StableChannel);
|
|
362
|
+
await registry.updateIfNeeded("normal");
|
|
363
|
+
|
|
364
|
+
let overview = await readKindOverview();
|
|
365
|
+
expect(channelsOf(overview, "1.0.0")).toEqual([StableChannel]);
|
|
366
|
+
expect(channelsOf(overview, "2.0.0")).toEqual([]);
|
|
367
|
+
expect(stableResolution(overview)).toEqual({
|
|
368
|
+
ok: true,
|
|
369
|
+
blockId: a.description.id,
|
|
370
|
+
channel: StableChannel,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// B stable, reconcile in normal mode. A keeps its own marker (marking is
|
|
374
|
+
// additive per version), so both entries must read stable and resolution
|
|
375
|
+
// must move to the newer version.
|
|
376
|
+
await registry.addPackageToChannel(b.description.id, StableChannel);
|
|
377
|
+
await registry.updateIfNeeded("normal");
|
|
378
|
+
|
|
379
|
+
overview = await readKindOverview();
|
|
380
|
+
expect(channelsOf(overview, "1.0.0")).toEqual([StableChannel]);
|
|
381
|
+
expect(channelsOf(overview, "2.0.0")).toEqual([StableChannel]);
|
|
382
|
+
expect(await storage.listFiles(`v2/testorg/kindblk/1.0.0/channels/`)).toEqual([
|
|
383
|
+
StableChannel,
|
|
384
|
+
]);
|
|
385
|
+
expect(stableResolution(overview)).toEqual({
|
|
386
|
+
ok: true,
|
|
387
|
+
blockId: b.description.id,
|
|
388
|
+
channel: StableChannel,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Now the losing side of a real handover: drop stable from A. The version
|
|
392
|
+
// losing membership is the one seeded, so a normal pass must clear its
|
|
393
|
+
// snapshot rather than carry the stale `["stable"]`.
|
|
394
|
+
await registry.removePackageFromChannel(a.description.id, StableChannel);
|
|
395
|
+
await registry.updateIfNeeded("normal");
|
|
396
|
+
|
|
397
|
+
overview = await readKindOverview();
|
|
398
|
+
expect(channelsOf(overview, "1.0.0")).toEqual([]);
|
|
399
|
+
expect(channelsOf(overview, "2.0.0")).toEqual([StableChannel]);
|
|
400
|
+
expect(await storage.listFiles(`v2/testorg/kindblk/1.0.0/channels/`)).toEqual([]);
|
|
401
|
+
expect(stableResolution(overview)).toEqual({
|
|
402
|
+
ok: true,
|
|
403
|
+
blockId: b.description.id,
|
|
404
|
+
channel: StableChannel,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Dropping the last stable marker must make the kind unresolvable on the
|
|
408
|
+
// stable channel — not silently fall back to a no-longer-stable version.
|
|
409
|
+
await registry.removePackageFromChannel(b.description.id, StableChannel);
|
|
410
|
+
await registry.updateIfNeeded("normal");
|
|
411
|
+
|
|
412
|
+
overview = await readKindOverview();
|
|
413
|
+
expect(channelsOf(overview, "1.0.0")).toEqual([]);
|
|
414
|
+
expect(channelsOf(overview, "2.0.0")).toEqual([]);
|
|
415
|
+
expect(stableResolution(overview)).toEqual({
|
|
416
|
+
ok: false,
|
|
417
|
+
reason: "no-stable-implementation",
|
|
418
|
+
});
|
|
419
|
+
expect(resolveKind(overview, "1.0.0", { allowUnstable: true })).toEqual({
|
|
420
|
+
ok: true,
|
|
421
|
+
blockId: b.description.id,
|
|
422
|
+
channel: "any",
|
|
423
|
+
});
|
|
424
|
+
} finally {
|
|
425
|
+
await teardown();
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* A block version can change which kind it implements: `publishPackage` overwrites
|
|
432
|
+
* same-version content (unlike `publishKind`, which is source-hash guarded), so a
|
|
433
|
+
* re-publish can move `{org}:{name}:{version}` from kind A to kind B.
|
|
434
|
+
*
|
|
435
|
+
* A `normal` pass only ever reads the CURRENT manifest, which names B — so nothing
|
|
436
|
+
* in the fresh scan points at A. Without an explicit visit A keeps listing the block
|
|
437
|
+
* as its implementer, and `resolveKind(A)` answers with a block whose manifest
|
|
438
|
+
* implements B: a mismatch that surfaces only at apply time, as rejected params.
|
|
439
|
+
* The reconciler recovers the old ref from the package overview it already loaded.
|
|
440
|
+
*
|
|
441
|
+
* Both halves are pinned here: A survives, minus the migrated version, while it
|
|
442
|
+
* still has an implementer — and is deleted once the last one leaves.
|
|
443
|
+
*/
|
|
444
|
+
test.each(testStorages)(
|
|
445
|
+
"changing a block's kind clears its old kind overview in normal mode with $name",
|
|
446
|
+
async ({ storageProvider }) => {
|
|
447
|
+
const { storage, teardown } = storageProvider();
|
|
448
|
+
const registry = new BlockRegistryV2(storage, undefined, { skipSnapshotCreation: true });
|
|
449
|
+
|
|
450
|
+
const ovPathA = kindOverviewPath(npmNameToKindPath(kindTestKindNpmName));
|
|
451
|
+
const ovPathB = kindOverviewPath(npmNameToKindPath(kindTestOtherKindNpmName));
|
|
452
|
+
const readOverview = async (ovPath: string): Promise<KindOverview | undefined> => {
|
|
453
|
+
const content = await storage.getFile(ovPath);
|
|
454
|
+
return content === undefined ? undefined : KindOverview.parse(JSON.parse(content.toString()));
|
|
455
|
+
};
|
|
456
|
+
const implementerVersions = (overview: KindOverview | undefined): string[] =>
|
|
457
|
+
(overview?.implementers ?? []).map((i) => i.id.version).sort();
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
// Two versions of one block, both implementing kind A, both stable.
|
|
461
|
+
const a1 = kindImplementingManifest("1.0.0", "1.0.0");
|
|
462
|
+
const a2 = kindImplementingManifest("2.0.0", "1.0.0");
|
|
463
|
+
await registry.publishPackage(a1, kindTestFileReader);
|
|
464
|
+
await registry.publishPackage(a2, kindTestFileReader);
|
|
465
|
+
await registry.addPackageToChannel(a1.description.id, StableChannel);
|
|
466
|
+
await registry.addPackageToChannel(a2.description.id, StableChannel);
|
|
467
|
+
await registry.updateIfNeeded("normal");
|
|
468
|
+
|
|
469
|
+
expect(implementerVersions(await readOverview(ovPathA))).toEqual(["1.0.0", "2.0.0"]);
|
|
470
|
+
expect(await readOverview(ovPathB)).toBeUndefined();
|
|
471
|
+
|
|
472
|
+
// Re-publish 1.0.0 against kind B — same block id, different kind.
|
|
473
|
+
const b1 = kindImplementingManifest("1.0.0", "1.0.0", kindTestOtherKindNpmName);
|
|
474
|
+
await registry.publishPackage(b1, kindTestFileReader);
|
|
475
|
+
await registry.updateIfNeeded("normal");
|
|
476
|
+
|
|
477
|
+
const overviewA = await readOverview(ovPathA);
|
|
478
|
+
const overviewB = await readOverview(ovPathB);
|
|
479
|
+
expect(implementerVersions(overviewA)).toEqual(["2.0.0"]);
|
|
480
|
+
expect(implementerVersions(overviewB)).toEqual(["1.0.0"]);
|
|
481
|
+
// Each kind must resolve to the block that actually implements it now.
|
|
482
|
+
expect(resolveKind(overviewA!, "1.0.0", { allowUnstable: false })).toEqual({
|
|
483
|
+
ok: true,
|
|
484
|
+
blockId: a2.description.id,
|
|
485
|
+
channel: StableChannel,
|
|
486
|
+
});
|
|
487
|
+
expect(resolveKind(overviewB!, "1.0.0", { allowUnstable: false })).toEqual({
|
|
488
|
+
ok: true,
|
|
489
|
+
blockId: b1.description.id,
|
|
490
|
+
channel: StableChannel,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Migrate the last implementer off A: its overview is orphaned and removed.
|
|
494
|
+
const b2 = kindImplementingManifest("2.0.0", "1.0.0", kindTestOtherKindNpmName);
|
|
495
|
+
await registry.publishPackage(b2, kindTestFileReader);
|
|
496
|
+
await registry.updateIfNeeded("normal");
|
|
497
|
+
|
|
498
|
+
expect(await readOverview(ovPathA)).toBeUndefined();
|
|
499
|
+
expect(implementerVersions(await readOverview(ovPathB))).toEqual(["1.0.0", "2.0.0"]);
|
|
500
|
+
expect(
|
|
501
|
+
resolveKind((await readOverview(ovPathB))!, "1.0.0", { allowUnstable: false }),
|
|
502
|
+
).toEqual({ ok: true, blockId: b2.description.id, channel: StableChannel });
|
|
503
|
+
} finally {
|
|
504
|
+
await teardown();
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
);
|
|
508
|
+
|
|
263
509
|
test.each([
|
|
264
510
|
{
|
|
265
511
|
name: "test1",
|