capdag 0.149.334 → 0.152.345
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/capdag.js +198 -115
- package/capdag.test.js +193 -47
- package/package.json +2 -2
package/capdag.js
CHANGED
|
@@ -3634,6 +3634,12 @@ class CartridgeInfo {
|
|
|
3634
3634
|
// Versions with platform-specific builds
|
|
3635
3635
|
this.versions = data.versions || {};
|
|
3636
3636
|
this.availableVersions = data.availableVersions || [];
|
|
3637
|
+
// Channel: 'release' or 'nightly'. Required — set by the registry
|
|
3638
|
+
// transformer when flattening the channel-partitioned registry.
|
|
3639
|
+
if (data.channel !== 'release' && data.channel !== 'nightly') {
|
|
3640
|
+
throw new Error(`CartridgeInfo ${data.id || '?'}: invalid or missing channel '${data.channel}'`);
|
|
3641
|
+
}
|
|
3642
|
+
this.channel = data.channel;
|
|
3637
3643
|
}
|
|
3638
3644
|
|
|
3639
3645
|
/** All caps flattened across all cap_groups, deduplicated by URN */
|
|
@@ -3682,7 +3688,9 @@ class CartridgeInfo {
|
|
|
3682
3688
|
}
|
|
3683
3689
|
|
|
3684
3690
|
/**
|
|
3685
|
-
* Cartridge suggestion for a missing cap
|
|
3691
|
+
* Cartridge suggestion for a missing cap. `channel` reports which
|
|
3692
|
+
* channel the suggesting cartridge lives in so consumers can render
|
|
3693
|
+
* the release/nightly distinction.
|
|
3686
3694
|
*/
|
|
3687
3695
|
class CartridgeSuggestion {
|
|
3688
3696
|
constructor(data) {
|
|
@@ -3694,21 +3702,31 @@ class CartridgeSuggestion {
|
|
|
3694
3702
|
this.latestVersion = data.latestVersion;
|
|
3695
3703
|
this.repoUrl = data.repoUrl;
|
|
3696
3704
|
this.pageUrl = data.pageUrl;
|
|
3705
|
+
if (data.channel !== 'release' && data.channel !== 'nightly') {
|
|
3706
|
+
throw new Error(`CartridgeSuggestion: invalid or missing channel '${data.channel}'`);
|
|
3707
|
+
}
|
|
3708
|
+
this.channel = data.channel;
|
|
3697
3709
|
}
|
|
3698
3710
|
}
|
|
3699
3711
|
|
|
3700
3712
|
/**
|
|
3701
|
-
* Cartridge registry cache entry
|
|
3713
|
+
* Cartridge registry cache entry. The cartridges map is keyed by
|
|
3714
|
+
* `<channel>:<id>` so the same id can independently coexist in both
|
|
3715
|
+
* channels.
|
|
3702
3716
|
*/
|
|
3703
3717
|
class CartridgeRepoCache {
|
|
3704
3718
|
constructor(repoUrl) {
|
|
3705
|
-
this.cartridges = new Map(); //
|
|
3706
|
-
this.capToCartridges = new Map(); // cap_urn -> [
|
|
3719
|
+
this.cartridges = new Map(); // "<channel>:<id>" -> CartridgeInfo
|
|
3720
|
+
this.capToCartridges = new Map(); // cap_urn -> [{channel, id}]
|
|
3707
3721
|
this.lastUpdated = Date.now();
|
|
3708
3722
|
this.repoUrl = repoUrl;
|
|
3709
3723
|
}
|
|
3710
3724
|
}
|
|
3711
3725
|
|
|
3726
|
+
function _cacheKey(channel, id) {
|
|
3727
|
+
return `${channel}:${id}`;
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3712
3730
|
/**
|
|
3713
3731
|
* Cartridge repository client - fetches and caches cartridge registry
|
|
3714
3732
|
*/
|
|
@@ -3719,52 +3737,74 @@ class CartridgeRepoClient {
|
|
|
3719
3737
|
}
|
|
3720
3738
|
|
|
3721
3739
|
/**
|
|
3722
|
-
* Fetch registry from a URL
|
|
3740
|
+
* Fetch a v5.0 channel-partitioned registry from a URL and flatten
|
|
3741
|
+
* to a list of `CartridgeInfo`, one per `(channel, id)` pair.
|
|
3723
3742
|
*/
|
|
3724
3743
|
async fetchRegistry(repoUrl) {
|
|
3725
3744
|
const response = await fetch(repoUrl);
|
|
3726
|
-
|
|
3745
|
+
if (response.status === 404) {
|
|
3746
|
+
// Manifest not published yet — return an empty list so the
|
|
3747
|
+
// caller's cache reflects "no cartridges available" without
|
|
3748
|
+
// poisoning future syncs.
|
|
3749
|
+
return [];
|
|
3750
|
+
}
|
|
3727
3751
|
if (!response.ok) {
|
|
3728
3752
|
throw new Error(`Cartridge registry request failed: HTTP ${response.status} from ${repoUrl}`);
|
|
3729
3753
|
}
|
|
3730
3754
|
|
|
3731
3755
|
const data = await response.json();
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3756
|
+
if (data.schemaVersion !== '5.0') {
|
|
3757
|
+
throw new Error(`Cartridge registry from ${repoUrl} has schemaVersion '${data.schemaVersion}'; required: 5.0`);
|
|
3758
|
+
}
|
|
3759
|
+
if (!data.channels || typeof data.channels !== 'object') {
|
|
3760
|
+
throw new Error(`Cartridge registry from ${repoUrl}: missing channels object`);
|
|
3735
3761
|
}
|
|
3736
3762
|
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3763
|
+
const out = [];
|
|
3764
|
+
for (const channel of ['release', 'nightly']) {
|
|
3765
|
+
const entry = data.channels[channel];
|
|
3766
|
+
if (!entry || typeof entry !== 'object' || !entry.cartridges || typeof entry.cartridges !== 'object') {
|
|
3767
|
+
throw new Error(`Cartridge registry from ${repoUrl}: channels.${channel}.cartridges must be an object`);
|
|
3768
|
+
}
|
|
3769
|
+
for (const [id, c] of Object.entries(entry.cartridges)) {
|
|
3770
|
+
out.push(new CartridgeInfo({
|
|
3771
|
+
...c,
|
|
3772
|
+
id,
|
|
3773
|
+
version: c.latestVersion,
|
|
3774
|
+
channel
|
|
3775
|
+
}));
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
return out;
|
|
3743
3779
|
}
|
|
3744
3780
|
|
|
3745
3781
|
/**
|
|
3746
3782
|
* Update cache from registry data.
|
|
3747
3783
|
*
|
|
3748
|
-
* The
|
|
3749
|
-
*
|
|
3750
|
-
*
|
|
3751
|
-
* (
|
|
3752
|
-
*
|
|
3753
|
-
* that fails to parse is a registry corruption:
|
|
3754
|
-
* silently keep the malformed string in the
|
|
3784
|
+
* The cartridges map is keyed by `<channel>:<id>` so the same id can
|
|
3785
|
+
* coexist in release and nightly with separate metadata/versions. The
|
|
3786
|
+
* cap-to-cartridges index keys on the *normalized* tagged-URN form
|
|
3787
|
+
* (parse via CapUrn.fromString, then take toString()) and stores
|
|
3788
|
+
* `{channel, id}` references so suggestions preserve channel
|
|
3789
|
+
* provenance. A cap URN that fails to parse is a registry corruption:
|
|
3790
|
+
* we throw rather than silently keep the malformed string in the
|
|
3791
|
+
* index.
|
|
3755
3792
|
*/
|
|
3756
3793
|
updateCache(repoUrl, cartridges) {
|
|
3757
3794
|
const cache = new CartridgeRepoCache(repoUrl);
|
|
3758
3795
|
|
|
3759
3796
|
for (const cartridge of cartridges) {
|
|
3760
|
-
cache.cartridges.set(cartridge.id, cartridge);
|
|
3797
|
+
cache.cartridges.set(_cacheKey(cartridge.channel, cartridge.id), cartridge);
|
|
3761
3798
|
|
|
3762
3799
|
for (const cap of cartridge.allCaps()) {
|
|
3763
3800
|
const normalized = CapUrn.fromString(cap.urn).toString();
|
|
3764
3801
|
if (!cache.capToCartridges.has(normalized)) {
|
|
3765
3802
|
cache.capToCartridges.set(normalized, []);
|
|
3766
3803
|
}
|
|
3767
|
-
cache.capToCartridges.get(normalized).push(
|
|
3804
|
+
cache.capToCartridges.get(normalized).push({
|
|
3805
|
+
channel: cartridge.channel,
|
|
3806
|
+
id: cartridge.id
|
|
3807
|
+
});
|
|
3768
3808
|
}
|
|
3769
3809
|
}
|
|
3770
3810
|
|
|
@@ -3812,8 +3852,9 @@ class CartridgeRepoClient {
|
|
|
3812
3852
|
* `capUrn` is parsed via CapUrn.fromString; the parsed-and-
|
|
3813
3853
|
* re-serialized form is the canonical key into the cap-to-cartridges
|
|
3814
3854
|
* index. Inside each candidate cartridge we walk its caps via
|
|
3815
|
-
* `allCaps()` and match each one with `isEquivalent
|
|
3816
|
-
*
|
|
3855
|
+
* `allCaps()` and match each one with `isEquivalent`. The `op` tag
|
|
3856
|
+
* has no functional role — only `in` and `out` predicates participate
|
|
3857
|
+
* in dispatch.
|
|
3817
3858
|
*/
|
|
3818
3859
|
getSuggestionsForCap(capUrn) {
|
|
3819
3860
|
const requested = CapUrn.fromString(capUrn);
|
|
@@ -3821,11 +3862,11 @@ class CartridgeRepoClient {
|
|
|
3821
3862
|
const suggestions = [];
|
|
3822
3863
|
|
|
3823
3864
|
for (const cache of this.caches.values()) {
|
|
3824
|
-
const
|
|
3825
|
-
if (!
|
|
3865
|
+
const refs = cache.capToCartridges.get(normalized);
|
|
3866
|
+
if (!refs) continue;
|
|
3826
3867
|
|
|
3827
|
-
for (const
|
|
3828
|
-
const cartridge = cache.cartridges.get(
|
|
3868
|
+
for (const ref of refs) {
|
|
3869
|
+
const cartridge = cache.cartridges.get(_cacheKey(ref.channel, ref.id));
|
|
3829
3870
|
if (!cartridge) continue;
|
|
3830
3871
|
|
|
3831
3872
|
const capInfo = cartridge.allCaps().find(c => {
|
|
@@ -3849,7 +3890,8 @@ class CartridgeRepoClient {
|
|
|
3849
3890
|
capTitle: capInfo.title,
|
|
3850
3891
|
latestVersion: cartridge.version,
|
|
3851
3892
|
repoUrl: cache.repoUrl,
|
|
3852
|
-
pageUrl: pageUrl
|
|
3893
|
+
pageUrl: pageUrl,
|
|
3894
|
+
channel: cartridge.channel
|
|
3853
3895
|
}));
|
|
3854
3896
|
}
|
|
3855
3897
|
}
|
|
@@ -3858,13 +3900,15 @@ class CartridgeRepoClient {
|
|
|
3858
3900
|
}
|
|
3859
3901
|
|
|
3860
3902
|
/**
|
|
3861
|
-
* Get all available cartridges from all repos
|
|
3903
|
+
* Get all available cartridges from all repos as
|
|
3904
|
+
* `[channel, id, cartridgeInfo]` tuples — the channel is first-class
|
|
3905
|
+
* so consumers don't have to look it up separately.
|
|
3862
3906
|
*/
|
|
3863
3907
|
getAllCartridges() {
|
|
3864
3908
|
const cartridges = [];
|
|
3865
3909
|
for (const cache of this.caches.values()) {
|
|
3866
|
-
for (const
|
|
3867
|
-
cartridges.push([
|
|
3910
|
+
for (const cartridgeInfo of cache.cartridges.values()) {
|
|
3911
|
+
cartridges.push([cartridgeInfo.channel, cartridgeInfo.id, cartridgeInfo]);
|
|
3868
3912
|
}
|
|
3869
3913
|
}
|
|
3870
3914
|
return cartridges;
|
|
@@ -3884,11 +3928,17 @@ class CartridgeRepoClient {
|
|
|
3884
3928
|
}
|
|
3885
3929
|
|
|
3886
3930
|
/**
|
|
3887
|
-
* Get cartridge info by
|
|
3931
|
+
* Get cartridge info by `(channel, id)`. Channel is required —
|
|
3932
|
+
* the same id can independently exist in both channels with
|
|
3933
|
+
* different metadata. Returns `null` when not found.
|
|
3888
3934
|
*/
|
|
3889
|
-
getCartridge(cartridgeId) {
|
|
3935
|
+
getCartridge(channel, cartridgeId) {
|
|
3936
|
+
if (channel !== 'release' && channel !== 'nightly') {
|
|
3937
|
+
throw new Error(`Invalid channel '${channel}' — must be 'release' or 'nightly'`);
|
|
3938
|
+
}
|
|
3939
|
+
const key = _cacheKey(channel, cartridgeId);
|
|
3890
3940
|
for (const cache of this.caches.values()) {
|
|
3891
|
-
const cartridge = cache.cartridges.get(
|
|
3941
|
+
const cartridge = cache.cartridges.get(key);
|
|
3892
3942
|
if (cartridge) {
|
|
3893
3943
|
return cartridge;
|
|
3894
3944
|
}
|
|
@@ -3916,51 +3966,74 @@ class CartridgeRepoClient {
|
|
|
3916
3966
|
/**
|
|
3917
3967
|
* Cartridge repository server - serves registry data with queries
|
|
3918
3968
|
*/
|
|
3969
|
+
/**
|
|
3970
|
+
* Distribution channel for a cartridge entry — mirrors capdag's
|
|
3971
|
+
* `CartridgeChannel` and the registry's `channels.<channel>` keys.
|
|
3972
|
+
*
|
|
3973
|
+
* `release` is the user-facing channel; `nightly` is the in-flight
|
|
3974
|
+
* channel. Always one of these two strings — no other values are valid.
|
|
3975
|
+
*/
|
|
3976
|
+
const CartridgeChannel = Object.freeze({
|
|
3977
|
+
Release: 'release',
|
|
3978
|
+
Nightly: 'nightly'
|
|
3979
|
+
});
|
|
3980
|
+
|
|
3981
|
+
function _validChannel(c) {
|
|
3982
|
+
return c === CartridgeChannel.Release || c === CartridgeChannel.Nightly;
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
/**
|
|
3986
|
+
* Reads a v5.0 channel-partitioned cartridge registry. Both `release`
|
|
3987
|
+
* and `nightly` channels are always present (possibly empty); every
|
|
3988
|
+
* `CartridgeInfo` returned carries the channel it came from so consumers
|
|
3989
|
+
* can render the release/nightly distinction without re-deriving.
|
|
3990
|
+
*/
|
|
3919
3991
|
class CartridgeRepoServer {
|
|
3920
3992
|
constructor(registry) {
|
|
3921
3993
|
this.registry = registry;
|
|
3922
3994
|
this.validateRegistry();
|
|
3923
3995
|
}
|
|
3924
3996
|
|
|
3925
|
-
/**
|
|
3926
|
-
* Validate registry schema
|
|
3927
|
-
*/
|
|
3928
3997
|
validateRegistry() {
|
|
3929
3998
|
if (!this.registry) {
|
|
3930
3999
|
throw new Error('Registry is required');
|
|
3931
4000
|
}
|
|
3932
|
-
if (this.registry.schemaVersion !== '
|
|
3933
|
-
throw new Error(`Unsupported registry schema version: ${this.registry.schemaVersion}. Required:
|
|
4001
|
+
if (this.registry.schemaVersion !== '5.0') {
|
|
4002
|
+
throw new Error(`Unsupported registry schema version: ${this.registry.schemaVersion}. Required: 5.0`);
|
|
4003
|
+
}
|
|
4004
|
+
const channels = this.registry.channels;
|
|
4005
|
+
if (!channels || typeof channels !== 'object') {
|
|
4006
|
+
throw new Error('Registry must have a channels object');
|
|
3934
4007
|
}
|
|
3935
|
-
|
|
3936
|
-
|
|
4008
|
+
for (const ch of [CartridgeChannel.Release, CartridgeChannel.Nightly]) {
|
|
4009
|
+
const entry = channels[ch];
|
|
4010
|
+
if (!entry || typeof entry !== 'object') {
|
|
4011
|
+
throw new Error(`Registry must have channels.${ch}`);
|
|
4012
|
+
}
|
|
4013
|
+
if (!entry.cartridges || typeof entry.cartridges !== 'object') {
|
|
4014
|
+
throw new Error(`Registry: channels.${ch}.cartridges must be an object`);
|
|
4015
|
+
}
|
|
3937
4016
|
}
|
|
3938
4017
|
}
|
|
3939
4018
|
|
|
3940
|
-
|
|
3941
|
-
* Validate version data has all required fields
|
|
3942
|
-
*/
|
|
3943
|
-
validateVersionData(id, version, versionData) {
|
|
4019
|
+
validateVersionData(channel, id, version, versionData) {
|
|
3944
4020
|
if (!Array.isArray(versionData.builds) || versionData.builds.length === 0) {
|
|
3945
|
-
throw new Error(`Cartridge ${id} v${version}: no builds`);
|
|
4021
|
+
throw new Error(`Cartridge ${id} (${channel}) v${version}: no builds`);
|
|
3946
4022
|
}
|
|
3947
4023
|
for (let i = 0; i < versionData.builds.length; i++) {
|
|
3948
4024
|
const build = versionData.builds[i];
|
|
3949
4025
|
if (!build.platform) {
|
|
3950
|
-
throw new Error(`Cartridge ${id} v${version}: build[${i}] missing platform`);
|
|
4026
|
+
throw new Error(`Cartridge ${id} (${channel}) v${version}: build[${i}] missing platform`);
|
|
3951
4027
|
}
|
|
3952
4028
|
if (!build.package || !build.package.name) {
|
|
3953
|
-
throw new Error(`Cartridge ${id} v${version}: build[${i}] (${build.platform}) missing package.name`);
|
|
4029
|
+
throw new Error(`Cartridge ${id} (${channel}) v${version}: build[${i}] (${build.platform}) missing package.name`);
|
|
3954
4030
|
}
|
|
3955
4031
|
if (!build.package.url) {
|
|
3956
|
-
throw new Error(`Cartridge ${id} v${version}: build[${i}] (${build.platform}) missing package.url`);
|
|
4032
|
+
throw new Error(`Cartridge ${id} (${channel}) v${version}: build[${i}] (${build.platform}) missing package.url`);
|
|
3957
4033
|
}
|
|
3958
4034
|
}
|
|
3959
4035
|
}
|
|
3960
4036
|
|
|
3961
|
-
/**
|
|
3962
|
-
* Compare version strings
|
|
3963
|
-
*/
|
|
3964
4037
|
compareVersions(a, b) {
|
|
3965
4038
|
const partsA = a.split('.').map(x => parseInt(x) || 0);
|
|
3966
4039
|
const partsB = b.split('.').map(x => parseInt(x) || 0);
|
|
@@ -3976,71 +4049,82 @@ class CartridgeRepoServer {
|
|
|
3976
4049
|
}
|
|
3977
4050
|
|
|
3978
4051
|
/**
|
|
3979
|
-
*
|
|
4052
|
+
* Convert one channel-entry into a flat CartridgeInfo. Throws if the
|
|
4053
|
+
* entry's `latestVersion` is not present in `versions` or if the
|
|
4054
|
+
* latest version's builds are malformed.
|
|
3980
4055
|
*/
|
|
3981
|
-
|
|
3982
|
-
const
|
|
3983
|
-
const
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
4056
|
+
_entryToCartridgeInfo(channel, id, cartridge) {
|
|
4057
|
+
const latestVersion = cartridge.latestVersion;
|
|
4058
|
+
const versionData = cartridge.versions[latestVersion];
|
|
4059
|
+
if (!versionData) {
|
|
4060
|
+
throw new Error(`Cartridge ${id} (${channel}): latestVersion ${latestVersion} not found in versions`);
|
|
4061
|
+
}
|
|
4062
|
+
this.validateVersionData(channel, id, latestVersion, versionData);
|
|
3988
4063
|
|
|
3989
|
-
|
|
3990
|
-
throw new Error(`Cartridge ${id}: latest version ${latestVersion} not found in versions`);
|
|
3991
|
-
}
|
|
4064
|
+
const availableVersions = Object.keys(cartridge.versions).sort((a, b) => this.compareVersions(b, a));
|
|
3992
4065
|
|
|
3993
|
-
|
|
3994
|
-
|
|
4066
|
+
if (!Array.isArray(cartridge.cap_groups)) {
|
|
4067
|
+
throw new Error(`Cartridge ${id} (${channel}): missing cap_groups array`);
|
|
4068
|
+
}
|
|
3995
4069
|
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4070
|
+
return {
|
|
4071
|
+
id,
|
|
4072
|
+
name: cartridge.name,
|
|
4073
|
+
version: latestVersion,
|
|
4074
|
+
description: cartridge.description,
|
|
4075
|
+
author: cartridge.author,
|
|
4076
|
+
pageUrl: cartridge.pageUrl || '',
|
|
4077
|
+
teamId: cartridge.teamId,
|
|
4078
|
+
signedAt: versionData.releaseDate,
|
|
4079
|
+
minAppVersion: versionData.minAppVersion || cartridge.minAppVersion,
|
|
4080
|
+
cap_groups: cartridge.cap_groups,
|
|
4081
|
+
categories: cartridge.categories,
|
|
4082
|
+
tags: cartridge.tags,
|
|
4083
|
+
versions: cartridge.versions,
|
|
4084
|
+
availableVersions,
|
|
4085
|
+
channel
|
|
4086
|
+
};
|
|
4087
|
+
}
|
|
4000
4088
|
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
return cartridge.cap_groups;
|
|
4014
|
-
})(),
|
|
4015
|
-
categories: cartridge.categories,
|
|
4016
|
-
tags: cartridge.tags,
|
|
4017
|
-
versions: cartridge.versions,
|
|
4018
|
-
availableVersions
|
|
4019
|
-
});
|
|
4089
|
+
/**
|
|
4090
|
+
* Walk both channels and emit a flat array of CartridgeInfo. Release
|
|
4091
|
+
* entries appear before nightly entries — UIs that paint in array
|
|
4092
|
+
* order get the user-facing channel at the top by default.
|
|
4093
|
+
*/
|
|
4094
|
+
transformToCartridgeArray() {
|
|
4095
|
+
const out = [];
|
|
4096
|
+
for (const channel of [CartridgeChannel.Release, CartridgeChannel.Nightly]) {
|
|
4097
|
+
const map = (this.registry.channels[channel].cartridges) || {};
|
|
4098
|
+
for (const [id, cartridge] of Object.entries(map)) {
|
|
4099
|
+
out.push(this._entryToCartridgeInfo(channel, id, cartridge));
|
|
4100
|
+
}
|
|
4020
4101
|
}
|
|
4021
|
-
|
|
4022
|
-
return cartridges;
|
|
4102
|
+
return out;
|
|
4023
4103
|
}
|
|
4024
4104
|
|
|
4025
4105
|
/**
|
|
4026
|
-
* Get all cartridges (API response format)
|
|
4106
|
+
* Get all cartridges (API response format) — both channels.
|
|
4027
4107
|
*/
|
|
4028
4108
|
getCartridges() {
|
|
4029
|
-
return {
|
|
4030
|
-
cartridges: this.transformToCartridgeArray()
|
|
4031
|
-
};
|
|
4109
|
+
return { cartridges: this.transformToCartridgeArray() };
|
|
4032
4110
|
}
|
|
4033
4111
|
|
|
4034
4112
|
/**
|
|
4035
|
-
* Get cartridge by
|
|
4113
|
+
* Get cartridge by `(channel, id)`. Channel is required because the
|
|
4114
|
+
* same id can independently exist in both channels. Returns
|
|
4115
|
+
* `undefined` if the cartridge isn't in the requested channel.
|
|
4036
4116
|
*/
|
|
4037
|
-
getCartridgeById(id) {
|
|
4038
|
-
|
|
4039
|
-
|
|
4117
|
+
getCartridgeById(channel, id) {
|
|
4118
|
+
if (!_validChannel(channel)) {
|
|
4119
|
+
throw new Error(`Invalid channel '${channel}' — must be 'release' or 'nightly'`);
|
|
4120
|
+
}
|
|
4121
|
+
const cartridge = this.registry.channels[channel].cartridges[id];
|
|
4122
|
+
if (!cartridge) return undefined;
|
|
4123
|
+
return this._entryToCartridgeInfo(channel, id, cartridge);
|
|
4040
4124
|
}
|
|
4041
4125
|
|
|
4042
4126
|
/**
|
|
4043
|
-
* Search cartridges by free-text query.
|
|
4127
|
+
* Search cartridges by free-text query across both channels.
|
|
4044
4128
|
*
|
|
4045
4129
|
* Matches against cartridge name, description, tags, and cap titles.
|
|
4046
4130
|
* Cap URN strings are not substring-matched: a cap URN is a tagged
|
|
@@ -4051,7 +4135,6 @@ class CartridgeRepoServer {
|
|
|
4051
4135
|
searchCartridges(query) {
|
|
4052
4136
|
const cartridges = this.transformToCartridgeArray();
|
|
4053
4137
|
const lowerQuery = query.toLowerCase();
|
|
4054
|
-
|
|
4055
4138
|
return cartridges.filter(p => {
|
|
4056
4139
|
const allCaps = (p.cap_groups || []).flatMap(g => g.caps || []);
|
|
4057
4140
|
return p.name.toLowerCase().includes(lowerQuery) ||
|
|
@@ -4062,7 +4145,7 @@ class CartridgeRepoServer {
|
|
|
4062
4145
|
}
|
|
4063
4146
|
|
|
4064
4147
|
/**
|
|
4065
|
-
* Get cartridges by category
|
|
4148
|
+
* Get cartridges by category — both channels.
|
|
4066
4149
|
*/
|
|
4067
4150
|
getCartridgesByCategory(category) {
|
|
4068
4151
|
const cartridges = this.transformToCartridgeArray();
|
|
@@ -4072,10 +4155,14 @@ class CartridgeRepoServer {
|
|
|
4072
4155
|
/**
|
|
4073
4156
|
* Get cartridges that provide a specific cap.
|
|
4074
4157
|
*
|
|
4075
|
-
*
|
|
4076
|
-
*
|
|
4077
|
-
*
|
|
4078
|
-
*
|
|
4158
|
+
* The request URN is parsed via CapUrn.fromString. Each declared
|
|
4159
|
+
* cartridge cap is parsed and matched with `conformsTo`: cap dispatch
|
|
4160
|
+
* is the partial-order question "does the declared cap conform to
|
|
4161
|
+
* (i.e. refine, equal, or be more specific than) the requested
|
|
4162
|
+
* pattern?". Only `in` and `out` tags are semantically meaningful —
|
|
4163
|
+
* no string comparison, no special role for the `op` tag. A malformed
|
|
4164
|
+
* input URN throws; a malformed declared URN in the registry also
|
|
4165
|
+
* throws (registry corruption is not a fallback condition).
|
|
4079
4166
|
*/
|
|
4080
4167
|
getCartridgesByCap(capUrn) {
|
|
4081
4168
|
const requested = CapUrn.fromString(capUrn);
|
|
@@ -4083,13 +4170,8 @@ class CartridgeRepoServer {
|
|
|
4083
4170
|
return cartridges.filter(p =>
|
|
4084
4171
|
(p.cap_groups || []).some(g =>
|
|
4085
4172
|
(g.caps || []).some(c => {
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
parsed = CapUrn.fromString(c.urn);
|
|
4089
|
-
} catch (_e) {
|
|
4090
|
-
return false;
|
|
4091
|
-
}
|
|
4092
|
-
return parsed.isEquivalent(requested);
|
|
4173
|
+
const declared = CapUrn.fromString(c.urn);
|
|
4174
|
+
return declared.conformsTo(requested);
|
|
4093
4175
|
})
|
|
4094
4176
|
)
|
|
4095
4177
|
);
|
|
@@ -5326,6 +5408,7 @@ module.exports = {
|
|
|
5326
5408
|
CartridgeRepoCache,
|
|
5327
5409
|
CartridgeRepoClient,
|
|
5328
5410
|
CartridgeRepoServer,
|
|
5411
|
+
CartridgeChannel,
|
|
5329
5412
|
// Machine notation
|
|
5330
5413
|
MachineSyntaxError,
|
|
5331
5414
|
MachineSyntaxErrorCodes,
|
package/capdag.test.js
CHANGED
|
@@ -1637,11 +1637,17 @@ function testJS_mediaSpecConstruction() {
|
|
|
1637
1637
|
// Cartridge Repository Tests (TEST320-TEST335)
|
|
1638
1638
|
// =============================================================================
|
|
1639
1639
|
|
|
1640
|
-
// Sample registry for testing
|
|
1640
|
+
// Sample registry for testing — v5.0 channel-partitioned schema.
|
|
1641
|
+
//
|
|
1642
|
+
// Both `release` and `nightly` are always present. We populate the
|
|
1643
|
+
// release channel with two cartridges (pdf + txt) for the bulk of the
|
|
1644
|
+
// tests, and add one nightly entry so isolation tests have something
|
|
1645
|
+
// to assert on. Tests that need an empty channel use literals inline.
|
|
1641
1646
|
const sampleRegistry = {
|
|
1642
|
-
schemaVersion: '
|
|
1647
|
+
schemaVersion: '5.0',
|
|
1643
1648
|
lastUpdated: '2026-02-07T16:48:28Z',
|
|
1644
|
-
|
|
1649
|
+
channels: {
|
|
1650
|
+
release: { cartridges: {
|
|
1645
1651
|
pdfcartridge: {
|
|
1646
1652
|
name: 'pdfcartridge',
|
|
1647
1653
|
description: 'PDF document processor',
|
|
@@ -1727,6 +1733,50 @@ const sampleRegistry = {
|
|
|
1727
1733
|
}
|
|
1728
1734
|
}
|
|
1729
1735
|
}
|
|
1736
|
+
} },
|
|
1737
|
+
nightly: { cartridges: {
|
|
1738
|
+
jsoncartridge: {
|
|
1739
|
+
name: 'jsoncartridge',
|
|
1740
|
+
description: 'JSON tooling — nightly only',
|
|
1741
|
+
author: 'test-author',
|
|
1742
|
+
pageUrl: 'https://example.com/json',
|
|
1743
|
+
teamId: 'P336JK947M',
|
|
1744
|
+
minAppVersion: '1.0.0',
|
|
1745
|
+
categories: ['data'],
|
|
1746
|
+
tags: ['json'],
|
|
1747
|
+
cap_groups: [
|
|
1748
|
+
{
|
|
1749
|
+
name: 'json-processing',
|
|
1750
|
+
adapter_urns: ['media:json'],
|
|
1751
|
+
caps: [
|
|
1752
|
+
{
|
|
1753
|
+
urn: 'cap:in="media:json";op=pretty;out="media:json;textable"',
|
|
1754
|
+
title: 'Pretty Print JSON',
|
|
1755
|
+
description: 'Format JSON',
|
|
1756
|
+
command: 'pretty'
|
|
1757
|
+
}
|
|
1758
|
+
]
|
|
1759
|
+
}
|
|
1760
|
+
],
|
|
1761
|
+
latestVersion: '0.1.0',
|
|
1762
|
+
versions: {
|
|
1763
|
+
'0.1.0': {
|
|
1764
|
+
releaseDate: '2026-04-01T00:00:00Z',
|
|
1765
|
+
changelog: ['Initial nightly'],
|
|
1766
|
+
minAppVersion: '1.0.0',
|
|
1767
|
+
builds: [{
|
|
1768
|
+
platform: 'darwin-arm64',
|
|
1769
|
+
package: {
|
|
1770
|
+
name: 'jsoncartridge-0.1.0.pkg',
|
|
1771
|
+
url: 'https://cartridges.machinefabric.com/nightly/jsoncartridge/0.1.0/jsoncartridge-0.1.0.pkg',
|
|
1772
|
+
sha256: 'deadbeef',
|
|
1773
|
+
size: 100000
|
|
1774
|
+
}
|
|
1775
|
+
}]
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
} }
|
|
1730
1780
|
}
|
|
1731
1781
|
};
|
|
1732
1782
|
|
|
@@ -1735,6 +1785,7 @@ function test320_cartridgeInfoConstruction() {
|
|
|
1735
1785
|
const data = {
|
|
1736
1786
|
id: 'testcartridge',
|
|
1737
1787
|
name: 'Test Cartridge',
|
|
1788
|
+
channel: 'release',
|
|
1738
1789
|
version: '1.0.0',
|
|
1739
1790
|
description: 'A test',
|
|
1740
1791
|
teamId: 'TEAM123',
|
|
@@ -1764,20 +1815,20 @@ function test320_cartridgeInfoConstruction() {
|
|
|
1764
1815
|
|
|
1765
1816
|
// TEST321: CartridgeInfo.is_signed() returns true when signature is present
|
|
1766
1817
|
function test321_cartridgeInfoIsSigned() {
|
|
1767
|
-
const signed = new CartridgeInfo({id: 'test', teamId: 'TEAM', signedAt: '2026-01-01', cap_groups: []});
|
|
1818
|
+
const signed = new CartridgeInfo({id: 'test', channel: 'release', teamId: 'TEAM', signedAt: '2026-01-01', cap_groups: []});
|
|
1768
1819
|
assert(signed.isSigned() === true, 'Cartridge with teamId and signedAt should be signed');
|
|
1769
1820
|
|
|
1770
|
-
const unsigned1 = new CartridgeInfo({id: 'test', teamId: '', signedAt: '2026-01-01', cap_groups: []});
|
|
1821
|
+
const unsigned1 = new CartridgeInfo({id: 'test', channel: 'release', teamId: '', signedAt: '2026-01-01', cap_groups: []});
|
|
1771
1822
|
assert(unsigned1.isSigned() === false, 'Cartridge without teamId should not be signed');
|
|
1772
1823
|
|
|
1773
|
-
const unsigned2 = new CartridgeInfo({id: 'test', teamId: 'TEAM', signedAt: '', cap_groups: []});
|
|
1824
|
+
const unsigned2 = new CartridgeInfo({id: 'test', channel: 'release', teamId: 'TEAM', signedAt: '', cap_groups: []});
|
|
1774
1825
|
assert(unsigned2.isSigned() === false, 'Cartridge without signedAt should not be signed');
|
|
1775
1826
|
}
|
|
1776
1827
|
|
|
1777
1828
|
// TEST322: CartridgeInfo.build_for_platform() returns the build matching the current platform
|
|
1778
1829
|
function test322_cartridgeInfoBuildForPlatform() {
|
|
1779
1830
|
const withBuilds = new CartridgeInfo({
|
|
1780
|
-
id: 'test', version: '1.0.0', cap_groups: [],
|
|
1831
|
+
id: 'test', channel: 'release', version: '1.0.0', cap_groups: [],
|
|
1781
1832
|
versions: {
|
|
1782
1833
|
'1.0.0': {
|
|
1783
1834
|
builds: [
|
|
@@ -1803,51 +1854,64 @@ function test322_cartridgeInfoBuildForPlatform() {
|
|
|
1803
1854
|
assert(platforms.includes('darwin-arm64'), 'Should include darwin-arm64');
|
|
1804
1855
|
assert(platforms.includes('linux-x86_64'), 'Should include linux-x86_64');
|
|
1805
1856
|
|
|
1806
|
-
const noBuilds = new CartridgeInfo({id: 'test', version: '1.0.0', cap_groups: [], versions: {}, availableVersions: []});
|
|
1857
|
+
const noBuilds = new CartridgeInfo({id: 'test', channel: 'release', version: '1.0.0', cap_groups: [], versions: {}, availableVersions: []});
|
|
1807
1858
|
assert(noBuilds.buildForPlatform('darwin-arm64') === null, 'Should return null when no versions');
|
|
1808
1859
|
assert(noBuilds.availablePlatforms().length === 0, 'Should have no platforms');
|
|
1809
1860
|
}
|
|
1810
1861
|
|
|
1811
|
-
// TEST323: CartridgeRepoServer
|
|
1862
|
+
// TEST323: CartridgeRepoServer requires schema 5.0 and rejects older.
|
|
1812
1863
|
function test323_cartridgeRepoServerValidateRegistry() {
|
|
1813
1864
|
// Valid registry
|
|
1814
1865
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
1815
|
-
assert(server.registry.schemaVersion === '
|
|
1866
|
+
assert(server.registry.schemaVersion === '5.0', 'Should accept v5.0 registry');
|
|
1816
1867
|
|
|
1817
|
-
// Invalid schema version
|
|
1868
|
+
// Invalid schema version (4.0 is no longer accepted)
|
|
1818
1869
|
let threw = false;
|
|
1819
1870
|
try {
|
|
1820
|
-
new CartridgeRepoServer({schemaVersion: '
|
|
1871
|
+
new CartridgeRepoServer({schemaVersion: '4.0', channels: {release: {cartridges: {}}, nightly: {cartridges: {}}}});
|
|
1821
1872
|
} catch (e) {
|
|
1822
1873
|
threw = true;
|
|
1823
|
-
assert(e.message.includes('
|
|
1874
|
+
assert(e.message.includes('5.0'), 'Should reject pre-5.0 schema and mention required version');
|
|
1824
1875
|
}
|
|
1825
|
-
assert(threw, 'Should throw for
|
|
1876
|
+
assert(threw, 'Should throw for v4.0 schema');
|
|
1826
1877
|
|
|
1827
|
-
// Missing
|
|
1878
|
+
// Missing channels object
|
|
1828
1879
|
threw = false;
|
|
1829
1880
|
try {
|
|
1830
|
-
new CartridgeRepoServer({schemaVersion: '
|
|
1881
|
+
new CartridgeRepoServer({schemaVersion: '5.0'});
|
|
1831
1882
|
} catch (e) {
|
|
1832
1883
|
threw = true;
|
|
1833
|
-
assert(e.message.includes('
|
|
1884
|
+
assert(e.message.includes('channels'), 'Should reject missing channels');
|
|
1834
1885
|
}
|
|
1835
|
-
assert(threw, 'Should throw for missing
|
|
1886
|
+
assert(threw, 'Should throw for missing channels');
|
|
1887
|
+
|
|
1888
|
+
// Missing one of the two required channels
|
|
1889
|
+
threw = false;
|
|
1890
|
+
try {
|
|
1891
|
+
new CartridgeRepoServer({schemaVersion: '5.0', channels: {release: {cartridges: {}}}});
|
|
1892
|
+
} catch (e) {
|
|
1893
|
+
threw = true;
|
|
1894
|
+
assert(e.message.includes('nightly'), 'Should require nightly channel');
|
|
1895
|
+
}
|
|
1896
|
+
assert(threw, 'Should throw when nightly channel is missing');
|
|
1836
1897
|
}
|
|
1837
1898
|
|
|
1838
|
-
// TEST324: CartridgeRepoServer
|
|
1899
|
+
// TEST324: CartridgeRepoServer walks both channels and emits a flat
|
|
1900
|
+
// CartridgeInfo array preserving channel provenance. Release entries
|
|
1901
|
+
// appear first.
|
|
1839
1902
|
function test324_cartridgeRepoServerTransformToArray() {
|
|
1840
1903
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
1841
1904
|
const cartridges = server.transformToCartridgeArray();
|
|
1842
1905
|
|
|
1843
1906
|
assert(Array.isArray(cartridges), 'Should return array');
|
|
1844
|
-
assert(cartridges.length ===
|
|
1907
|
+
assert(cartridges.length === 3, 'Should have 3 cartridges across both channels');
|
|
1845
1908
|
|
|
1846
1909
|
const pdf = cartridges.find(p => p.id === 'pdfcartridge');
|
|
1847
1910
|
assert(pdf !== undefined, 'Should include pdfcartridge');
|
|
1848
1911
|
assert(pdf.version === '0.81.5325', 'Should have latest version');
|
|
1849
1912
|
assert(pdf.teamId === 'P336JK947M', 'Should have teamId');
|
|
1850
1913
|
assert(pdf.signedAt === '2026-02-07T16:40:28Z', 'Should have signedAt from releaseDate');
|
|
1914
|
+
assert(pdf.channel === 'release', 'pdfcartridge should be in release channel');
|
|
1851
1915
|
assert(pdf.versions !== undefined, 'Should have versions');
|
|
1852
1916
|
assert(pdf.versions['0.81.5325'] !== undefined, 'Should have version data');
|
|
1853
1917
|
assert(pdf.versions['0.81.5325'].builds.length === 1, 'Should have 1 build');
|
|
@@ -1859,31 +1923,66 @@ function test324_cartridgeRepoServerTransformToArray() {
|
|
|
1859
1923
|
assert(Array.isArray(pdf.cap_groups), 'Should have cap_groups array');
|
|
1860
1924
|
assert(pdf.cap_groups.length === 1, 'Should have 1 cap_group');
|
|
1861
1925
|
assert(pdf.cap_groups[0].caps.length === 2, 'Should have 2 caps in the group');
|
|
1926
|
+
|
|
1927
|
+
const json = cartridges.find(p => p.id === 'jsoncartridge');
|
|
1928
|
+
assert(json !== undefined, 'Should include jsoncartridge from nightly channel');
|
|
1929
|
+
assert(json.channel === 'nightly', 'jsoncartridge should be in nightly channel');
|
|
1930
|
+
|
|
1931
|
+
// Release entries come before nightly so any UI that paints in
|
|
1932
|
+
// iteration order surfaces the user-facing channel first.
|
|
1933
|
+
const firstNightlyIdx = cartridges.findIndex(c => c.channel === 'nightly');
|
|
1934
|
+
const lastReleaseIdx = cartridges.map(c => c.channel).lastIndexOf('release');
|
|
1935
|
+
assert(firstNightlyIdx > lastReleaseIdx, 'Release entries must precede nightly entries');
|
|
1862
1936
|
}
|
|
1863
1937
|
|
|
1864
|
-
// TEST325: CartridgeRepoServer.
|
|
1938
|
+
// TEST325: CartridgeRepoServer.getCartridges() wraps the transformed
|
|
1939
|
+
// flat array (across both channels) in the response envelope.
|
|
1865
1940
|
function test325_cartridgeRepoServerGetCartridges() {
|
|
1866
1941
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
1867
1942
|
const response = server.getCartridges();
|
|
1868
1943
|
|
|
1869
1944
|
assert(response.cartridges !== undefined, 'Should have cartridges field');
|
|
1870
1945
|
assert(Array.isArray(response.cartridges), 'Cartridges should be array');
|
|
1871
|
-
assert(response.cartridges.length ===
|
|
1946
|
+
assert(response.cartridges.length === 3, 'Should have 3 cartridges total');
|
|
1947
|
+
assert(response.cartridges.every(c => c.channel === 'release' || c.channel === 'nightly'),
|
|
1948
|
+
'Every cartridge must carry a channel');
|
|
1872
1949
|
}
|
|
1873
1950
|
|
|
1874
|
-
// TEST326: CartridgeRepoServer.
|
|
1951
|
+
// TEST326: CartridgeRepoServer.getCartridgeById() requires (channel,
|
|
1952
|
+
// id). Same id looked up in the wrong channel must miss — channels are
|
|
1953
|
+
// independent namespaces.
|
|
1875
1954
|
function test326_cartridgeRepoServerGetCartridgeById() {
|
|
1876
1955
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
1877
1956
|
|
|
1878
|
-
const pdf = server.getCartridgeById('pdfcartridge');
|
|
1879
|
-
assert(pdf !== undefined, 'Should find pdfcartridge');
|
|
1957
|
+
const pdf = server.getCartridgeById('release', 'pdfcartridge');
|
|
1958
|
+
assert(pdf !== undefined, 'Should find pdfcartridge in release');
|
|
1880
1959
|
assert(pdf.id === 'pdfcartridge', 'Should have correct ID');
|
|
1960
|
+
assert(pdf.channel === 'release', 'Should report release channel');
|
|
1961
|
+
|
|
1962
|
+
const json = server.getCartridgeById('nightly', 'jsoncartridge');
|
|
1963
|
+
assert(json !== undefined, 'Should find jsoncartridge in nightly');
|
|
1964
|
+
assert(json.channel === 'nightly', 'Should report nightly channel');
|
|
1881
1965
|
|
|
1882
|
-
const
|
|
1966
|
+
const wrongChannel = server.getCartridgeById('nightly', 'pdfcartridge');
|
|
1967
|
+
assert(wrongChannel === undefined, 'pdf not in nightly — channels are independent');
|
|
1968
|
+
|
|
1969
|
+
const notFound = server.getCartridgeById('release', 'nonexistent');
|
|
1883
1970
|
assert(notFound === undefined, 'Should return undefined for missing cartridge');
|
|
1971
|
+
|
|
1972
|
+
let threw = false;
|
|
1973
|
+
try {
|
|
1974
|
+
server.getCartridgeById('staging', 'pdfcartridge');
|
|
1975
|
+
} catch (e) {
|
|
1976
|
+
threw = true;
|
|
1977
|
+
assert(e.message.includes('release') && e.message.includes('nightly'),
|
|
1978
|
+
'Should reject invalid channel value');
|
|
1979
|
+
}
|
|
1980
|
+
assert(threw, 'Should throw for invalid channel');
|
|
1884
1981
|
}
|
|
1885
1982
|
|
|
1886
|
-
// TEST327: CartridgeRepoServer.
|
|
1983
|
+
// TEST327: CartridgeRepoServer.searchCartridges() filters across both
|
|
1984
|
+
// channels by name/description/tags/cap titles. Cap URN strings are
|
|
1985
|
+
// not substring-matched.
|
|
1887
1986
|
function test327_cartridgeRepoServerSearchCartridges() {
|
|
1888
1987
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
1889
1988
|
|
|
@@ -1894,11 +1993,18 @@ function test327_cartridgeRepoServerSearchCartridges() {
|
|
|
1894
1993
|
const metadataResults = server.searchCartridges('metadata');
|
|
1895
1994
|
assert(metadataResults.length === 1, 'Should find cartridge by cap title');
|
|
1896
1995
|
|
|
1996
|
+
// Search must reach into the nightly channel too — channels are not
|
|
1997
|
+
// a search-time filter.
|
|
1998
|
+
const jsonResults = server.searchCartridges('json');
|
|
1999
|
+
assert(jsonResults.length === 1, 'Should reach nightly cartridges');
|
|
2000
|
+
assert(jsonResults[0].channel === 'nightly', 'Should report nightly channel');
|
|
2001
|
+
|
|
1897
2002
|
const noResults = server.searchCartridges('nonexistent');
|
|
1898
2003
|
assert(noResults.length === 0, 'Should return empty for no matches');
|
|
1899
2004
|
}
|
|
1900
2005
|
|
|
1901
|
-
// TEST328: CartridgeRepoServer.
|
|
2006
|
+
// TEST328: CartridgeRepoServer.getCartridgesByCategory() filters
|
|
2007
|
+
// cartridges by category across both channels.
|
|
1902
2008
|
function test328_cartridgeRepoServerGetByCategory() {
|
|
1903
2009
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
1904
2010
|
|
|
@@ -1909,9 +2015,15 @@ function test328_cartridgeRepoServerGetByCategory() {
|
|
|
1909
2015
|
const textCartridges = server.getCartridgesByCategory('text');
|
|
1910
2016
|
assert(textCartridges.length === 1, 'Should find 1 text cartridge');
|
|
1911
2017
|
assert(textCartridges[0].id === 'txtcartridge', 'Should be txtcartridge');
|
|
2018
|
+
|
|
2019
|
+
const dataCartridges = server.getCartridgesByCategory('data');
|
|
2020
|
+
assert(dataCartridges.length === 1, 'Category lookup should reach the nightly channel too');
|
|
2021
|
+
assert(dataCartridges[0].channel === 'nightly', 'Should be the nightly entry');
|
|
1912
2022
|
}
|
|
1913
2023
|
|
|
1914
|
-
// TEST329: CartridgeRepoServer.
|
|
2024
|
+
// TEST329: CartridgeRepoServer.getCartridgesByCap() parses the input
|
|
2025
|
+
// URN and matches each declared cap via `conformsTo`. Tag-order
|
|
2026
|
+
// differences resolve because matching is order-theoretic, not string.
|
|
1915
2027
|
function test329_cartridgeRepoServerGetByCap() {
|
|
1916
2028
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
1917
2029
|
|
|
@@ -1926,7 +2038,9 @@ function test329_cartridgeRepoServerGetByCap() {
|
|
|
1926
2038
|
assert(metadataCartridges.length === 1, 'Should find metadata cap');
|
|
1927
2039
|
}
|
|
1928
2040
|
|
|
1929
|
-
// TEST330: CartridgeRepoClient updates its local cache
|
|
2041
|
+
// TEST330: CartridgeRepoClient updates its local cache keyed by
|
|
2042
|
+
// "<channel>:<id>". The cache holds release and nightly entries
|
|
2043
|
+
// independently — the same id is allowed in both.
|
|
1930
2044
|
function test330_cartridgeRepoClientUpdateCache() {
|
|
1931
2045
|
const client = new CartridgeRepoClient(3600);
|
|
1932
2046
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
@@ -1936,11 +2050,14 @@ function test330_cartridgeRepoClientUpdateCache() {
|
|
|
1936
2050
|
|
|
1937
2051
|
const cache = client.caches.get('https://example.com/api/cartridges');
|
|
1938
2052
|
assert(cache !== undefined, 'Cache should exist');
|
|
1939
|
-
assert(cache.cartridges.size ===
|
|
2053
|
+
assert(cache.cartridges.size === 3, 'Should have 3 cartridges in cache (2 release + 1 nightly)');
|
|
2054
|
+
assert(cache.cartridges.has('release:pdfcartridge'), 'Should key by <channel>:<id>');
|
|
2055
|
+
assert(cache.cartridges.has('nightly:jsoncartridge'), 'Should hold nightly entry independently');
|
|
1940
2056
|
assert(cache.capToCartridges.size > 0, 'Should have cap mappings');
|
|
1941
2057
|
}
|
|
1942
2058
|
|
|
1943
|
-
// TEST331: CartridgeRepoClient.
|
|
2059
|
+
// TEST331: CartridgeRepoClient.getSuggestionsForCap() returns cartridge
|
|
2060
|
+
// suggestions with channel propagated onto each suggestion.
|
|
1944
2061
|
function test331_cartridgeRepoClientGetSuggestions() {
|
|
1945
2062
|
const client = new CartridgeRepoClient(3600);
|
|
1946
2063
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
@@ -1953,6 +2070,7 @@ function test331_cartridgeRepoClientGetSuggestions() {
|
|
|
1953
2070
|
|
|
1954
2071
|
assert(suggestions.length === 1, 'Should find 1 suggestion');
|
|
1955
2072
|
assert(suggestions[0].cartridgeId === 'pdfcartridge', 'Should suggest pdfcartridge');
|
|
2073
|
+
assert(suggestions[0].channel === 'release', 'Channel must propagate from cache');
|
|
1956
2074
|
// The returned capUrn is the canonical (normalized) form. Compare via
|
|
1957
2075
|
// tagged-URN equivalence rather than string equality so a tag-order
|
|
1958
2076
|
// difference between the request and the canonical form is tolerated.
|
|
@@ -1960,9 +2078,16 @@ function test331_cartridgeRepoClientGetSuggestions() {
|
|
|
1960
2078
|
const returned = CapUrn.fromString(suggestions[0].capUrn);
|
|
1961
2079
|
assert(returned.isEquivalent(requested), 'Should have equivalent cap URN');
|
|
1962
2080
|
assert(suggestions[0].capTitle === 'Disbind PDF', 'Should have cap title');
|
|
2081
|
+
|
|
2082
|
+
// Nightly cap should also surface a suggestion, with channel set to nightly.
|
|
2083
|
+
const prettyCap = 'cap:in="media:json";op=pretty;out="media:json;textable"';
|
|
2084
|
+
const nightlySuggestions = client.getSuggestionsForCap(prettyCap);
|
|
2085
|
+
assert(nightlySuggestions.length === 1, 'Should find nightly cap suggestion');
|
|
2086
|
+
assert(nightlySuggestions[0].channel === 'nightly', 'Should report nightly channel');
|
|
1963
2087
|
}
|
|
1964
2088
|
|
|
1965
|
-
// TEST332: CartridgeRepoClient.
|
|
2089
|
+
// TEST332: CartridgeRepoClient.getCartridge() requires (channel, id).
|
|
2090
|
+
// Same id in the wrong channel must miss.
|
|
1966
2091
|
function test332_cartridgeRepoClientGetCartridge() {
|
|
1967
2092
|
const client = new CartridgeRepoClient(3600);
|
|
1968
2093
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
@@ -1970,15 +2095,33 @@ function test332_cartridgeRepoClientGetCartridge() {
|
|
|
1970
2095
|
|
|
1971
2096
|
client.updateCache('https://example.com/api/cartridges', cartridges);
|
|
1972
2097
|
|
|
1973
|
-
const cartridge = client.getCartridge('pdfcartridge');
|
|
1974
|
-
assert(cartridge !== null, 'Should find cartridge');
|
|
2098
|
+
const cartridge = client.getCartridge('release', 'pdfcartridge');
|
|
2099
|
+
assert(cartridge !== null && cartridge !== undefined, 'Should find cartridge in release');
|
|
1975
2100
|
assert(cartridge.id === 'pdfcartridge', 'Should have correct ID');
|
|
2101
|
+
assert(cartridge.channel === 'release', 'Should report release channel');
|
|
2102
|
+
|
|
2103
|
+
const json = client.getCartridge('nightly', 'jsoncartridge');
|
|
2104
|
+
assert(json !== null && json !== undefined, 'Should find nightly entry');
|
|
2105
|
+
assert(json.channel === 'nightly', 'Should report nightly channel');
|
|
1976
2106
|
|
|
1977
|
-
const
|
|
1978
|
-
assert(
|
|
2107
|
+
const wrongChannel = client.getCartridge('nightly', 'pdfcartridge');
|
|
2108
|
+
assert(wrongChannel === undefined || wrongChannel === null,
|
|
2109
|
+
'Should miss when looking up release id in nightly channel');
|
|
2110
|
+
|
|
2111
|
+
const notFound = client.getCartridge('release', 'nonexistent');
|
|
2112
|
+
assert(notFound === undefined || notFound === null, 'Should miss for unknown id');
|
|
2113
|
+
|
|
2114
|
+
let threw = false;
|
|
2115
|
+
try {
|
|
2116
|
+
client.getCartridge('staging', 'pdfcartridge');
|
|
2117
|
+
} catch (e) {
|
|
2118
|
+
threw = true;
|
|
2119
|
+
}
|
|
2120
|
+
assert(threw, 'Should throw for invalid channel');
|
|
1979
2121
|
}
|
|
1980
2122
|
|
|
1981
|
-
// TEST333: CartridgeRepoClient.
|
|
2123
|
+
// TEST333: CartridgeRepoClient.getAllAvailableCaps() returns the set
|
|
2124
|
+
// of normalized URNs across both channels.
|
|
1982
2125
|
function test333_cartridgeRepoClientGetAllCaps() {
|
|
1983
2126
|
const client = new CartridgeRepoClient(3600);
|
|
1984
2127
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
@@ -1988,11 +2131,14 @@ function test333_cartridgeRepoClientGetAllCaps() {
|
|
|
1988
2131
|
|
|
1989
2132
|
const caps = client.getAllAvailableCaps();
|
|
1990
2133
|
assert(Array.isArray(caps), 'Should return array');
|
|
1991
|
-
|
|
2134
|
+
// 2 caps from pdfcartridge + 1 from txtcartridge + 1 from
|
|
2135
|
+
// jsoncartridge (nightly) = 4 unique caps.
|
|
2136
|
+
assert(caps.length === 4, `Should have 4 unique caps, got ${caps.length}`);
|
|
1992
2137
|
assert(caps.every(c => typeof c === 'string'), 'All caps should be strings');
|
|
1993
2138
|
}
|
|
1994
2139
|
|
|
1995
|
-
// TEST334: CartridgeRepoClient.
|
|
2140
|
+
// TEST334: CartridgeRepoClient.needsSync() returns true when cache is
|
|
2141
|
+
// empty / stale, false right after a fresh update.
|
|
1996
2142
|
function test334_cartridgeRepoClientNeedsSync() {
|
|
1997
2143
|
const client = new CartridgeRepoClient(1); // 1 second TTL
|
|
1998
2144
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
@@ -2008,12 +2154,10 @@ function test334_cartridgeRepoClientNeedsSync() {
|
|
|
2008
2154
|
|
|
2009
2155
|
// Should not need sync immediately
|
|
2010
2156
|
assert(client.needsSync(urls) === false, 'Should not need sync right after update');
|
|
2011
|
-
|
|
2012
|
-
// Wait for cache to expire (1 second)
|
|
2013
|
-
// Note: Can't test this synchronously, would need async test
|
|
2014
2157
|
}
|
|
2015
2158
|
|
|
2016
|
-
// TEST335:
|
|
2159
|
+
// TEST335: Round-trip: server produces a v5.0 response, client consumes
|
|
2160
|
+
// it, channel provenance is preserved end-to-end.
|
|
2017
2161
|
function test335_cartridgeRepoServerClientIntegration() {
|
|
2018
2162
|
// Server creates API response
|
|
2019
2163
|
const server = new CartridgeRepoServer(sampleRegistry);
|
|
@@ -2024,10 +2168,11 @@ function test335_cartridgeRepoServerClientIntegration() {
|
|
|
2024
2168
|
const cartridges = apiResponse.cartridges.map(p => new CartridgeInfo(p));
|
|
2025
2169
|
client.updateCache('https://example.com/api/cartridges', cartridges);
|
|
2026
2170
|
|
|
2027
|
-
// Client can find cartridge
|
|
2028
|
-
const cartridge = client.getCartridge('pdfcartridge');
|
|
2029
|
-
assert(cartridge !== null, 'Client should find cartridge from server data');
|
|
2171
|
+
// Client can find cartridge by (channel, id)
|
|
2172
|
+
const cartridge = client.getCartridge('release', 'pdfcartridge');
|
|
2173
|
+
assert(cartridge !== null && cartridge !== undefined, 'Client should find cartridge from server data');
|
|
2030
2174
|
assert(cartridge.isSigned(), 'Cartridge should be signed');
|
|
2175
|
+
assert(cartridge.channel === 'release', 'Should report release channel');
|
|
2031
2176
|
assert(cartridge.buildForPlatform('darwin-arm64') !== null, 'Cartridge should have darwin-arm64 build');
|
|
2032
2177
|
|
|
2033
2178
|
// Client can get suggestions
|
|
@@ -2035,6 +2180,7 @@ function test335_cartridgeRepoServerClientIntegration() {
|
|
|
2035
2180
|
const suggestions = client.getSuggestionsForCap(capUrn);
|
|
2036
2181
|
assert(suggestions.length === 1, 'Should get suggestions');
|
|
2037
2182
|
assert(suggestions[0].cartridgeId === 'pdfcartridge', 'Should suggest correct cartridge');
|
|
2183
|
+
assert(suggestions[0].channel === 'release', 'Suggestion should preserve channel');
|
|
2038
2184
|
|
|
2039
2185
|
// Server can search
|
|
2040
2186
|
const searchResults = server.searchCartridges('pdf');
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "Bahram Joharshamshiri",
|
|
3
3
|
"dependencies": {
|
|
4
4
|
"peggy": "^5.1.0",
|
|
5
|
-
"tagged-urn": "^0.
|
|
5
|
+
"tagged-urn": "^0.34.83"
|
|
6
6
|
},
|
|
7
7
|
"description": "JavaScript implementation of Cap URN (Capability Uniform Resource Names) with strict validation and matching",
|
|
8
8
|
"engines": {
|
|
@@ -40,5 +40,5 @@
|
|
|
40
40
|
"pretest": "npm run build:parser",
|
|
41
41
|
"test": "node capdag.test.js"
|
|
42
42
|
},
|
|
43
|
-
"version": "0.
|
|
43
|
+
"version": "0.152.345"
|
|
44
44
|
}
|