Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
8de88ee
feat(objectql, plugin-email): email template names held per namespace…
claude Nov 10, 2026
cef0ef9
test(objectql, plugin-email): pin the email template narrowing, the p…
claude Nov 10, 2026
c88476f
test(dogfood): the flow-family boot pins read the email template's na…
claude Nov 10, 2026
4312637
fix(metadata-protocol): the exact qualifier reads a bundle addition a…
claude Nov 10, 2026
8bbcc56
test(core, cli): the resolver and the stored-references report read t…
claude Nov 10, 2026
e37cebe
docs(changeset): a code package's bundle addition is still judged aga…
claude Nov 10, 2026
f1189c7
test(plugin-email): the platform-requested list pin loads core at mod…
claude Nov 10, 2026
3ccfb7a
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
3465aaa
fix(metadata-protocol): the exact qualifier keeps a pair's item on it…
claude Nov 10, 2026
a52f89e
docs(changeset): the email template change declares the ?package= nar…
claude Nov 10, 2026
a4326c9
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
50e1ef9
fix(plugin-email, service-automation, service-messaging): an email te…
claude Nov 10, 2026
a4ca293
fix(metadata-protocol, cli): a pair member row two holders could own …
claude Nov 10, 2026
1484ab5
test(plugin-email): read the last protocol request without Array.prot…
claude Nov 10, 2026
1fabc5d
feat(metadata): the in-memory store keeps one slot per (package, name…
claude Nov 10, 2026
f6bbafc
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
4c176c5
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
249a6a4
fix(metadata-protocol, cli)!: a package-less row of a name two packag…
claude Nov 10, 2026
9102aca
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
14bbef3
fix(service-automation, metadata-protocol, objectql, cli): contract r…
claude Nov 10, 2026
376b80e
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
9ea252e
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
c79a4e6
test(metadata-protocol, rest, runtime): the getItem-reading doubles r…
claude Nov 10, 2026
c5d0e37
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
d808486
fix(cli): the stored-references hint names no tracker id in its print…
claude Nov 11, 2026
ee4bcab
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 11, 2026
7136e33
fix(metadata-protocol): a row hydrated into the registry with no pack…
claude Nov 11, 2026
4668d9e
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
460df4a
test(objectql): the package-less row is written as the store writes i…
claude Nov 11, 2026
248d471
fix(metadata): a bundled type's unscoped loader reads keep every memb…
claude Nov 11, 2026
26e2548
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
61dfabf
docs(changeset): declare the loader half — rollback and delete by nam…
claude Nov 11, 2026
f9f589d
fix(metadata): MetadataManager's plural reads keep a bundled type's m…
claude Nov 11, 2026
f779531
fix(metadata): a bundled type's save writes its own member's row, and…
claude Nov 11, 2026
12ee9e5
docs(changeset): the manager's plural reads per member, and the membe…
claude Nov 11, 2026
cfb8f30
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
0f93f8f
fix(metadata): the boot priming pass never writes an item back into t…
claude Nov 11, 2026
74a7e4c
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
e8c7447
docs(metadata): MetadataUnregisterOptions keeps its own doc comment (…
claude Nov 11, 2026
fefaa51
test(metadata): the R5-A control seeds the database loader from a rea…
claude Nov 11, 2026
2990c4c
docs(metadata): the boot write-back note is its own changeset line; o…
claude Nov 11, 2026
74d2e76
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
7594637
test(metadata): database-loader.test.ts pays the metadata-fs load at …
claude Nov 11, 2026
6ed3bea
docs(metadata): keep database-loader.ts out of round 7's diff
claude Nov 11, 2026
17cfbf8
fix(metadata): the boot pass binds a legacy row's empty package colum…
claude Nov 11, 2026
e2ab2d3
wip(automation): flow names per namespace — slot-served /automation a…
claude Nov 12, 2026
cea1e0f
wip(test): teach the doubles and the family pins that a flow reads by…
claude Nov 12, 2026
58586bf
wip(runtime): read the registry only for a packaged flow caller, fail…
claude Nov 12, 2026
e4725f6
wip(dogfood): pin the package-less save of a shared flow name as the …
claude Nov 12, 2026
b32464f
wip: changeset for flow names per namespace; pin the co-owner refusal…
claude Nov 12, 2026
0e5eb45
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 12, 2026
290d918
test: the core narrowed-set pin reads the flow as narrowed; pay the m…
claude Nov 12, 2026
27abd45
Merge branch 'main' into claude/project-thread-3giljf
objectstack-fleet[bot] Nov 12, 2026
0ecd1ff
Merge remote-tracking branch 'origin/claude/project-thread-3giljf' in…
claude Nov 12, 2026
2dbc914
test: the published-door double reads the flow by package; the instal…
claude Nov 12, 2026
f845bef
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 12, 2026
a187fb1
fix(runtime): a flow name two namespaces hold is resolved per package…
claude Nov 12, 2026
2c65035
test(runtime): pay the metadata load at collection in the list_action…
claude Nov 12, 2026
731ac15
fix(service-automation,runtime): a run's records follow its slot key …
claude Nov 12, 2026
1e89878
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 12, 2026
6bc8374
fix(service-automation): the boot binds every run row that names no p…
claude Nov 12, 2026
59ff6ff
wip(#32613): start from #32507 head
claude Nov 12, 2026
5c65dab
wip(#32613): fix
claude Nov 12, 2026
2b2319e
wip(#32613): pins
claude Nov 12, 2026
98ca854
wip(#32613): changeset
claude Nov 12, 2026
c3b042e
wip(#32613): narrowing arm
claude Nov 12, 2026
b0c90e9
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 12, 2026
824aab8
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 12, 2026
3b36a90
wip(metadata-protocol): judge a sealed type's declared override again…
claude Nov 12, 2026
c5da110
wip(objectql): pin the action's exact two-holder answer at every face
claude Nov 12, 2026
04c58fb
wip(changeset): the action override row and its FROM → TO
claude Nov 12, 2026
b51125a
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 13, 2026
756a743
test(objectql): one of the two holders disabled, then re-enabled: the…
claude Nov 13, 2026
95e7f6b
docs(changeset): the measured action lists and the one-holder-disable…
claude Nov 13, 2026
b777b1b
docs(changeset): the disabled-holder sentence names the measured case
claude Nov 13, 2026
c0b7cb1
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 13, 2026
616dbbd
fix(metadata-protocol): scope the two-holder judgment to flow and act…
claude Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/32613-two-holder-override-slots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@objectstack/metadata-protocol': patch
---

fix(metadata-protocol)!: when two packages ship one flow or action name and each has its declared `.custom` override, every read, the lists and the boot pull use each override in its own package's slot, in any registration order (ADR-0150 D4)

Clause-②: no (narrowing) — the env-wide lists stop serving the extra entries they served before: for a name two or more packages ship, each with a declared `.custom` override, the entry under the override's pair slot (stamped with the name's first shipper) is no longer listed. For an action name, the package that registered second no longer serves its shipped action in place of its own declared override: ADR-0150 D4 binds each `.custom` override to the package it customizes (rows "registry overlay slot" and R16), so the decline it replaces was never the contract. First-party callers that read those entries: 0.
Metadata-protocol: none

<!-- adr-0087: not-required (no-migration-prescription) no key of any schema is added, removed, renamed or re-shaped and no stored row changes shape: what moves is which entries the env-wide lists serve for a name two packages ship, which stored row an action's address serves and which stored row the boot pull arms first, so there is nothing for `objectstack migrate meta` to rewrite -->

**BREAKING** for a caller that read the env-wide flow or action list (`GET /meta/flow`, `GET /meta/action`, `getMetaItemsForExecution`) and used an entry stamped with one package that carried another package's declared override. Such an entry is no longer listed. Read each package's override from its own entry, or by its package (`?package=ID`). Nothing in the workspace reads those entries.

Two packages ship a flow or an action of the same name. Each has a declared override in its own `.custom` pair. Before this fix, three places got this wrong.

- **The lists.** These are the served list (`GET /meta/flow`, `GET /meta/action`, naming no package) and the execution view (`getMetaItemsForExecution`, which the automation engine arms from). They listed each override a second time, under its pair's slot. The name's first registered shipper stamped its own package on that extra entry. For a flow, alpha first gave `alpha:OVR-A` twice, `alpha:OVR-B` and `beta:OVR-B`. Beta first gave `alpha:OVR-A`, `beta:OVR-A` and `beta:OVR-B` twice. So one package's override was listed under the other package. An action listed the first package's override twice the same way. Now each override is listed once, as its own package's item: exactly `alpha:OVR-A` and `beta:OVR-B`, in either order. With the first registered package disabled, the lists also kept the disabled package's override; now they list the enabled package's override alone, whichever package is disabled. When only one package ships the name, the pair's slot was already dropped, so that case is unchanged.
- **An action's override.** The reads judged an action's stored row as a declared override against the name's first registered shipper only. So the second package's declared override was declined at its own address (`?package=`, plain and layered), in its served-overrides list, in the served list and in the execution view, all of which served that package's shipped action instead. Alpha first, the lists gave `alpha:OVR-A` twice and `beta:SHIPPED-beta`. Beta first, they gave `alpha:SHIPPED-alpha` and `beta:OVR-B` twice. With the first package disabled, the enabled package's override was still declined. Now each package's address and both lists serve its own override: exactly `alpha:OVR-A` and `beta:OVR-B`, in either order. A flow was already judged against every shipper.
- **The boot pull.** The hydration decides which stored row holds a flow name's bare registry slot. It judged a row as a declared override against the first holder only. So the second holder's override counted as a plain stored row. If a package-less row was read after it, that row took the slot and the override was lost. The automation boot pull then armed that holder's shipped flow until `kernel:ready` re-armed from the protocol's view. A record change in that window ran the wrong body. Now a row is judged against every package that ships the name, and each override is armed in its own package's slot from the first pull.

Only flow and action move. For the six sealed types (page, app, dataset, doc, book, mapping), a second holder's declared override is still judged against the name's first registered shipper, and for every other overlay type the env-wide list keeps the pair's entry, as before. Those, and what a registry reader gets for each holder of an action name, are carried to #33131.

No door refuses a request it answered before, and no key, value or stored shape changes. The flow's by-name reads (`?package=`) already answered each package's own override and are unchanged.
36 changes: 27 additions & 9 deletions packages/metadata-protocol/src/protocol-lock-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,19 +151,26 @@ export function declinesStoredRow(
this: Pick<LockStateHost, 'engine' | 'isDeclaredCodeDatasource' | 'isShippedFlowName' | 'isUnservedSealedOverlay' | 'packagedArtifactOwner' | 'shippedArtifactsOf'>,
type: string, name: unknown, row?: ServedOverrideRow): boolean {
const installed = installedManifestsOfEngine(this.engine);
// [ADR-0150 D4, #31696, #32613] Whether `row` is the declared override of
// a package that ships `(type, name)`, judged against EVERY such package:
// two packages' items of one name are two items, each with its own
// declared override, so the read serves each in its package's slot —
// never only the override of whichever shipper the by-name lookup happens
// to find first (the first holder to register), which made the answer
// depend on registration order (red line 2).
// Used for a flow and an action only (the six sealed types below keep the
// single-owner judgment).
// No row, no override: the shippers are not read for a name-only question.
const servedOverrideOfAShipper = (shippedType: string, shippedName: string): boolean => row !== undefined && row !== null
&& (isServedOverrideRowOf(row, this.packagedArtifactOwner({ type: shippedType, name: shippedName }), installed)
|| [...shippingPackageIds(this.shippedArtifactsOf(shippedType, shippedName))]
.some((shipper) => isServedOverrideRowOf(row, shipper, installed)));
if (this.isShippedFlowName(type, name)) {
// [#28412, ADR-0146 D9 item 1] …except the one row that IS the
// packaged flow now: the shipping package's declared override, judged
// on the row's own `package_id` and body by the one served-override
// predicate. Every other stored row of the name stays declined.
// [ADR-0150 D4, #31696] Judged against EVERY package that ships the
// name: two packages' flows of one name are two items, each with its
// own declared override, so the read serves each in its package's
// slot — never only the override of whichever shipper the by-name
// lookup happens to find first.
if (isServedOverrideRowOf(row, this.packagedArtifactOwner({ type: 'flow', name: name as string }), installed)) return false;
return ![...shippingPackageIds(this.shippedArtifactsOf('flow', name as string))]
.some((shipper) => isServedOverrideRowOf(row, shipper, installed));
return !servedOverrideOfAShipper('flow', name as string);
}
if (typeof name !== 'string' || name === '') return false;
if (this.isDeclaredCodeDatasource(type, name)) return true;
Expand All @@ -177,7 +184,18 @@ export function declinesStoredRow(
// way: its declared override is served over the base's name from its
// row, so the layered read's `effective` and a by-name read answered by
// the metadata service see it as the registry's readers do.
return !((servesSealedOverrideRow(type) || canonicalMetaType(type) === 'action')
// [#32613] …for an `action`, against every package that ships the name,
// as the flow's above: with two holders of an action name, the second
// holder's declared override was judged against the first's package and
// declined at its own address, in the served list and in the execution
// view, so which holder's override ran depended on which package
// registered first.
if (canonicalMetaType(type) === 'action') return !servedOverrideOfAShipper(type, name);
// The six sealed types keep the single-owner judgment (the name's first
// registered shipper) here: moving them to every shipper changes what a
// read naming no package answers for each of them, which is measured and
// pinned on its own follow-up card (#33131), not by this change.
return !(servesSealedOverrideRow(type)
&& isServedOverrideRowOf(row, this.packagedArtifactOwner({ type, name }), installed));
}

Expand Down
46 changes: 40 additions & 6 deletions packages/metadata-protocol/src/protocol-meta-list-reads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
isActionWithheldByDisabledPackage,
isItemOfDisabledPackage,
} from './protocol-authoring-refusals.js';
import { soleShippingPackage } from './protocol-environment-rows.js';
import { shippingPackageIds } from './protocol-environment-rows.js';
import type { ProtocolHost } from './protocol-host.js';
import { canonicalizeMetaRequestType } from './protocol-meta-type-schema.js';
import { mergePackageAwareOverlay, servedViewExpansion, withoutServedRowName } from './protocol-overlay-merge.js';
Expand Down Expand Up @@ -92,6 +92,15 @@ function decorateListedItem(type: string, item: unknown): unknown {
return { ...served, name: (item as { name: string }).name, ...(_diagnostics !== undefined ? { _diagnostics } : {}) };
}

/**
* [#32613] The types whose env-wide list drops a two-shipper override's pair
* slot: the two whose answer, for a name two or more packages ship, each with
* its declared override, is measured and pinned (each override once, in its
* own package's slot, in either registration order). Any other type of such a
* name keeps its pair's entry, as before.
*/
const TWO_HOLDER_PAIR_SLOT_TYPES: ReadonlySet<string> = new Set(['flow', 'action']);

/** The body of `ObjectStackProtocolImplementation#readFlattenedMetaItems`; the method's docblock there is its contract. */
export async function readFlattenedMetaItems(host: Pick<ProtocolHost, 'artifactLockLayerAt' | 'baseServesBundleMember' | 'convertStoredItem' | 'declinesStoredRow' | 'disabledActionsOf' | 'engine' | 'environmentId' | 'expandStoredViewContainers' | 'findEnvironmentRows' | 'flowRegistrationsOf' | 'foldObjectExtendersFromRegistry' | 'getServicesRegistry' | 'governServedObject' | 'hydrateOverlayIntoRegistry' | 'isStoredEntryOfDeclinedName' | 'isUnservedSealedOverlay' | 'isWritablePackage' | 'lookupArtifactItem' | 'namesWithOwnStoredRow' | 'readActiveOverlayRows' | 'rethrowUnlessMetadataStoreUnprovisioned' | 'shippedArtifactsOf' | 'shippingPackagesOf' | 'shipsBundleName' | 'storedOverlayEntries'>, request: { type: string; packageId?: string; names?: readonly string[]; previewDrafts?: boolean },
audience: 'served' | 'execution',
Expand Down Expand Up @@ -780,19 +789,44 @@ export async function readFlattenedMetaItems(host: Pick<ProtocolHost, 'artifactL
// ({@link baseServesBundleMember}), so a locale the customer adds in
// `.custom` is listed and executed (the email loader reads this list).
// A dependent's member under the name is still dropped here.
//
// [#32613, ADR-0150 D4] …and a name two or more packages ship drops the
// pair's own slot of a row that declares one shipper's override: the
// merges above have served that row in its shipper's slot, under its
// shipper's provenance, so its pair's slot is the same row a second time.
// Kept, it was enveloped by the name's first shipper
// ({@link lookupArtifactItem} with the pair's id finds no own artifact), so
// the list served one holder's override under another holder's package,
// by registration order (red line 2). Judged by the one served-override
// predicate on the item's own binding (the pair, its `package_id` column)
// and body, against each shipper. Every other item of such a name is kept
// as before (ADR-0048 §3.3), and a name one package ships is narrowed
// above as before, which already drops the pair's slot.
// Only for `flow` and `action` ({@link TWO_HOLDER_PAIR_SLOT_TYPES}): any
// other type of such a name keeps its pair's entry, as before.
if (servedOverride && !packageId && request.type !== 'package') {
const typeShipping = host.shippingPackagesOf(request.type);
const isBundle = ITEM_KEY_DISCRIMINATORS[request.type] !== undefined;
const soleShipperOf = new Map<string, string | undefined>();
const shippersOf = new Map<string, ReadonlySet<string>>();
items = (items as any[]).filter((it) => {
const itemName = it?.name;
const bound = boundPackageOf(it?._packageId);
if (typeof itemName !== 'string' || bound === undefined) return true;
if (!soleShipperOf.has(itemName)) {
soleShipperOf.set(itemName, soleShippingPackage(host.shippedArtifactsOf(request.type, itemName, typeShipping)));
if (!shippersOf.has(itemName)) {
shippersOf.set(itemName, shippingPackageIds(host.shippedArtifactsOf(request.type, itemName, typeShipping)));
}
const shippers = shippersOf.get(itemName)!;
if (shippers.has(bound)) return true;
if (shippers.size > 1) {
// Scoped to the two types whose two-holder answer is measured
// and pinned (flow, action); every other type keeps the item
// as before, carried to its own follow-up card (#33131).
if (!TWO_HOLDER_PAIR_SLOT_TYPES.has(PLURAL_TO_SINGULAR[request.type] ?? request.type)) return true;
return ![...shippers].some((shipper) =>
isServedOverrideRowOf({ type: request.type, package_id: bound, metadata: it }, shipper, installed));
}
const soleShipper = soleShipperOf.get(itemName);
if (soleShipper === undefined || soleShipper === bound) return true;
if (shippers.size === 0) return true;
const [soleShipper] = shippers;
return isBundle && host.baseServesBundleMember(request.type, itemName, soleShipper, it, typeShipping);
});
}
Expand Down
15 changes: 12 additions & 3 deletions packages/metadata-protocol/src/protocol-registry-hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared';
import { expandViewContainer, isAggregatedViewContainer } from '@objectstack/spec/ui';
import { bindStoredRow } from './stored-row-binding.js';
import { incomingTakesStoredFlowSlot } from './stored-flow-slot.js';
import { shippingPackageIds } from './protocol-environment-rows.js';
import { OBJECT_EXTENSION_TYPE } from './protocol-object-extension-item.js';
import { canonicalMetaType } from './protocol-meta-type-schema.js';
import { remainingRowsPhrase } from './protocol-authoring-refusals.js';
Expand Down Expand Up @@ -537,7 +538,7 @@ export class ProtocolRegistryHydrationFacet {
* anything the static map does not carry, so a runtime kind the platform
* has never heard of can never trip this gate.
*/
static async hydrateOverlayIntoRegistry(host: Pick<ProtocolHost, 'anotherPackageShips' | 'declinesStoredRow' | 'engine' | 'hydrateExpandedViewItems' | 'lookupArtifactItem' | 'packagedArtifactOwner' | 'registerStoredObjectExtension' | 'restoreArtifactRegistryView'>, type: string,
static async hydrateOverlayIntoRegistry(host: Pick<ProtocolHost, 'anotherPackageShips' | 'declinesStoredRow' | 'engine' | 'hydrateExpandedViewItems' | 'lookupArtifactItem' | 'packagedArtifactOwner' | 'registerStoredObjectExtension' | 'restoreArtifactRegistryView' | 'shippedArtifactsOf'>, type: string,
data: unknown,
options: HydrationOptions,): Promise<boolean> {
// [#9111] See the header: the mint door asserts, the caller folds.
Expand Down Expand Up @@ -663,8 +664,16 @@ export class ProtocolRegistryHydrationFacet {
const held = typeof registry.getItem === 'function'
? registry.getItem(type, String((data as any).name))
: undefined;
const owner = host.packagedArtifactOwner({ type, name: String((data as any).name) });
if (!incomingTakesStoredFlowSlot(held, registered, owner, installedManifestsOf(registry))) return true;
// [#32613, ADR-0150 D4] …judged against EVERY package that ships
// the name, each by its own pair, never only the bare-name owner
// (the first holder to register): a second holder's declared
// override is its package's override too, and a package-less row
// read after it no longer drops it from the boot pull.
const name = String((data as any).name);
const shippers = new Set<string>(shippingPackageIds(host.shippedArtifactsOf(type, name)));
const owner = host.packagedArtifactOwner({ type, name });
if (typeof owner === 'string' && owner !== '') shippers.add(owner);
if (!incomingTakesStoredFlowSlot(held, registered, shippers, installedManifestsOf(registry))) return true;
}
registry.registerItem(type, registered, 'name' as any);
await host.hydrateExpandedViewItems(type, data, options);
Expand Down
43 changes: 43 additions & 0 deletions packages/metadata-protocol/src/stored-flow-slot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,46 @@ describe('[#28717] the declared override keeps the bare slot of its shipped flow
expect(shadowedStoredRowsOf({ ...held })).toEqual([]);
});
});

/**
* [#32613, ADR-0150 D4] A name two packages ship, each with its declared
* override in its own pair: an override is judged against every shipper, so
* neither holder's override is ever read as a plain stored row.
*/
describe('[#32613] two packages ship the name: each holder\'s declared override is an override', () => {
const OTHER = 'com.acme.other';
const SHIPPERS = [BASE, OTHER];
const otherOverride = () =>
bindStoredRow(body('OTHER OVERRIDE', { overrides: { package: OTHER, basedOn: '1.0.0' } }), `${OTHER}.custom`);

it('the second holder\'s override keeps the slot over a package-less row read after it, and carries it', () => {
const held = otherOverride();
expect(incomingTakesStoredFlowSlot(held, legacy(), SHIPPERS, NOTHING_INSTALLED)).toBe(false);
expect(labels(shadowedStoredRowsOf(held))).toEqual(['LEGACY']);
});

it('FROM: judged against the first holder alone, the package-less row took the slot and dropped it', () => {
const held = otherOverride();
const incoming = legacy();
expect(incomingTakesStoredFlowSlot(held, incoming, BASE, NOTHING_INSTALLED)).toBe(true);
expect(shadowedStoredRowsOf(incoming)).toEqual([]);
});

it('two holders\' overrides: whichever is read last takes the slot and carries the other, in either order', () => {
for (const [first, second] of [[override, otherOverride], [otherOverride, override]] as const) {
const held = first();
const incoming = second();
expect(incomingTakesStoredFlowSlot(held, incoming, SHIPPERS, NOTHING_INSTALLED)).toBe(true);
expect(labels(shadowedStoredRowsOf(incoming))).toEqual([(held as { label: string }).label]);
// …and a package-less row read after both keeps neither from the slot's body.
expect(incomingTakesStoredFlowSlot(incoming, legacy(), SHIPPERS, NOTHING_INSTALLED)).toBe(false);
expect(labels([incoming, ...shadowedStoredRowsOf(incoming)]).sort())
.toEqual([(incoming as { label: string }).label, (held as { label: string }).label, 'LEGACY'].sort());
}
});

it('CONTROL: a pair row declaring a package that does not ship the name is no override', () => {
const stranger = bindStoredRow(body('STRANGER', { overrides: { package: 'com.acme.none', basedOn: '1.0.0' } }), 'com.acme.none.custom');
expect(incomingTakesStoredFlowSlot(stranger, legacy(), SHIPPERS, NOTHING_INSTALLED)).toBe(true);
});
});
Loading
Loading