From 9e767272e26816528def967688d83614dadcb4bf Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Mon, 13 Jul 2026 18:47:36 +0800 Subject: [PATCH 1/6] Fix catalog remix leaving base-realm module refs unresolvable in the install destination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit planInstanceInstall/resolveTargetCodeRef checked base-realm membership against the symbolic https://cardstack.com/base/ URL, but a resolved codeRef.module is the base realm's real backing URL, so a base-realm instance (e.g. a Skill) was wrongly treated as needing to be copied into the install destination — producing an adoptsFrom.module relative to the new install folder instead of the actual base realm module. Also restores the @cardstack/catalog/ virtual-network mapping for live test runs specifically (resolvedCatalogRealmURL is nulled for environment === 'test', which breaks module resolution for content that imports catalog-realm commands, like the Listing card class). Co-Authored-By: Claude Sonnet 5 --- packages/host/app/config/environment.ts | 1 + packages/host/app/services/network.ts | 19 +++++++++++++++++++ packages/host/config/environment.js | 8 ++++++++ packages/runtime-common/catalog.ts | 25 +++++++++++++++++++------ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/packages/host/app/config/environment.ts b/packages/host/app/config/environment.ts index 64cf75d6cd1..bc0d266c752 100644 --- a/packages/host/app/config/environment.ts +++ b/packages/host/app/config/environment.ts @@ -37,6 +37,7 @@ export default config as { realmServerURL: string; resolvedBaseRealmURL: string; resolvedCatalogRealmURL: string | undefined; + rawCatalogRealmURL: string | undefined; resolvedSkillsRealmURL: string; resolvedOpenRouterRealmURL: string | undefined; resolvedTestRealmURL: string; diff --git a/packages/host/app/services/network.ts b/packages/host/app/services/network.ts index b67128941cb..fc307218a61 100644 --- a/packages/host/app/services/network.ts +++ b/packages/host/app/services/network.ts @@ -103,6 +103,25 @@ export default class NetworkService extends Service { resolvedTestRealmURL, ); } + // environment.js nulls out resolvedCatalogRealmURL for + // `environment === 'test'`, since the mocked qunit suite has no real + // catalog realm to fetch from. Live tests (testem-live.js) are the + // exception — they run against a real, running catalog realm — so + // restore the @cardstack/catalog/ mapping using the always-populated + // rawCatalogRealmURL when a live-test run is detected at runtime. + let isLiveTest = new URLSearchParams(window.location.search).has( + 'liveTest', + ); + if ( + isLiveTest && + !config.resolvedCatalogRealmURL && + config.rawCatalogRealmURL + ) { + virtualNetwork.addRealmMapping( + '@cardstack/catalog/', + config.rawCatalogRealmURL, + ); + } } return virtualNetwork; } diff --git a/packages/host/config/environment.js b/packages/host/config/environment.js index 4e7d56cf033..b38a26c242f 100644 --- a/packages/host/config/environment.js +++ b/packages/host/config/environment.js @@ -166,6 +166,14 @@ module.exports = function (environment) { resolvedCatalogRealmURL: skipCatalog ? undefined : process.env.RESOLVED_CATALOG_REALM_URL || defaults.catalogRealmURL, + // Same value as resolvedCatalogRealmURL, but never nulled out by the + // `environment === 'test'` block below. Live tests (testem-live.js) run + // against a real catalog realm, unlike the mocked qunit suite, so + // NetworkService falls back to this to restore the @cardstack/catalog/ + // mapping specifically for live-test runs. + rawCatalogRealmURL: skipCatalog + ? undefined + : process.env.RESOLVED_CATALOG_REALM_URL || defaults.catalogRealmURL, resolvedSkillsRealmURL: process.env.RESOLVED_SKILLS_REALM_URL || defaults.skillsRealmURL, resolvedOpenRouterRealmURL: diff --git a/packages/runtime-common/catalog.ts b/packages/runtime-common/catalog.ts index ab3df314423..e66e99e49db 100644 --- a/packages/runtime-common/catalog.ts +++ b/packages/runtime-common/catalog.ts @@ -7,7 +7,7 @@ import type { CardDef } from '@cardstack/base/card-api'; import { RealmPaths, join } from './paths.ts'; import type { ResolvedCodeRef } from './code-ref.ts'; import { resolveAdoptedCodeRef } from './code-ref.ts'; -import { realmURL } from './constants.ts'; +import { baseRealmRRI, realmURL } from './constants.ts'; import { logger } from './log.ts'; import type { LocalPath } from './paths.ts'; import { rri } from './realm-identifiers.ts'; @@ -23,7 +23,20 @@ export interface Listing extends CardDef { skills: any[]; } -const baseRealmPath = new RealmPaths(new URL('https://cardstack.com/base/')); +// A resolved codeRef.module is typically the base realm's real backing URL +// (e.g. `https://localhost:4201/base/skill`), not a literal +// `https://cardstack.com/base/` string — VirtualNetwork.unresolveURL() maps +// a real URL back to its canonical RRI-prefix form (`@cardstack/base/skill`) +// via the registered realm mapping, which is what actually identifies base +// realm membership post-RRI-refactor. Without this, a base-realm module +// gets wrongly treated as something that needs to be copied into the +// install destination. +function isInBaseRealm( + module: RealmResourceIdentifier, + virtualNetwork: VirtualNetwork, +): boolean { + return virtualNetwork.unresolveURL(module).startsWith(baseRealmRRI); +} // sourceCodeRef -- (installs module) --> targetCodeRef // sourceCodeRef: code ref of the code from the source realm @@ -222,7 +235,7 @@ function resolveTargetCodeRef( resolver: ListingPathResolver, virtualNetwork: VirtualNetwork, ): ResolvedCodeRef { - if (baseRealmPath.inRealm(codeRef.module)) { + if (isInBaseRealm(codeRef.module, virtualNetwork)) { return codeRef; } else { let moduleURL = virtualNetwork.toURL(codeRef.module); @@ -249,7 +262,7 @@ export function planModuleInstall( }; }); let modulesCopy = codeRefs.flatMap((sourceCodeRef: ResolvedCodeRef) => { - if (baseRealmPath.inRealm(sourceCodeRef.module)) { + if (isInBaseRealm(sourceCodeRef.module, virtualNetwork)) { return []; } let targetCodeRef = resolveTargetCodeRef( @@ -276,10 +289,10 @@ export function planInstanceInstall( for (let instance of instances) { let sourceCodeRef = resolveAdoptedCodeRef(instance, virtualNetwork); let lid = resolver.local(virtualNetwork.toURL(instance.id).href); - if (baseRealmPath.inRealm(rri(instance.id))) { + if (isInBaseRealm(rri(instance.id), virtualNetwork)) { throw new Error('Cannot install instance from base realm'); } - if (!baseRealmPath.inRealm(sourceCodeRef.module)) { + if (!isInBaseRealm(sourceCodeRef.module, virtualNetwork)) { let targetCodeRef = resolveTargetCodeRef( sourceCodeRef, resolver, From 7e4e84efc65492f5ab487b580ad2cbe1f9bf79f1 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Mon, 13 Jul 2026 19:24:37 +0800 Subject: [PATCH 2/6] Simplify the live-test catalog realm fallback to reuse the existing realmURL param MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit live-test.js already passes the catalog realm's real address into every live-test browser session via the realmURL query param (used to discover which realm to load test files from) — no need for a parallel rawCatalogRealmURL build-time config field duplicating the same skipCatalog logic. NetworkService now reads realmURL directly, the same way live-test.js's own config/environment shim already does. Co-Authored-By: Claude Sonnet 5 --- packages/host/app/config/environment.ts | 1 - packages/host/app/services/network.ts | 20 ++++++++------------ packages/host/config/environment.js | 8 -------- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/packages/host/app/config/environment.ts b/packages/host/app/config/environment.ts index bc0d266c752..64cf75d6cd1 100644 --- a/packages/host/app/config/environment.ts +++ b/packages/host/app/config/environment.ts @@ -37,7 +37,6 @@ export default config as { realmServerURL: string; resolvedBaseRealmURL: string; resolvedCatalogRealmURL: string | undefined; - rawCatalogRealmURL: string | undefined; resolvedSkillsRealmURL: string; resolvedOpenRouterRealmURL: string | undefined; resolvedTestRealmURL: string; diff --git a/packages/host/app/services/network.ts b/packages/host/app/services/network.ts index fc307218a61..69d3937d0bc 100644 --- a/packages/host/app/services/network.ts +++ b/packages/host/app/services/network.ts @@ -106,21 +106,17 @@ export default class NetworkService extends Service { // environment.js nulls out resolvedCatalogRealmURL for // `environment === 'test'`, since the mocked qunit suite has no real // catalog realm to fetch from. Live tests (testem-live.js) are the - // exception — they run against a real, running catalog realm — so - // restore the @cardstack/catalog/ mapping using the always-populated - // rawCatalogRealmURL when a live-test run is detected at runtime. - let isLiveTest = new URLSearchParams(window.location.search).has( - 'liveTest', - ); + // exception — they run against a real, running catalog realm, whose + // address is already passed in via the same `realmURL` query param + // live-test.js uses to discover which realm's test files to load. + let liveTestParams = new URLSearchParams(window.location.search); + let liveTestRealmURL = liveTestParams.get('realmURL'); if ( - isLiveTest && + liveTestParams.has('liveTest') && !config.resolvedCatalogRealmURL && - config.rawCatalogRealmURL + liveTestRealmURL ) { - virtualNetwork.addRealmMapping( - '@cardstack/catalog/', - config.rawCatalogRealmURL, - ); + virtualNetwork.addRealmMapping('@cardstack/catalog/', liveTestRealmURL); } } return virtualNetwork; diff --git a/packages/host/config/environment.js b/packages/host/config/environment.js index b38a26c242f..4e7d56cf033 100644 --- a/packages/host/config/environment.js +++ b/packages/host/config/environment.js @@ -166,14 +166,6 @@ module.exports = function (environment) { resolvedCatalogRealmURL: skipCatalog ? undefined : process.env.RESOLVED_CATALOG_REALM_URL || defaults.catalogRealmURL, - // Same value as resolvedCatalogRealmURL, but never nulled out by the - // `environment === 'test'` block below. Live tests (testem-live.js) run - // against a real catalog realm, unlike the mocked qunit suite, so - // NetworkService falls back to this to restore the @cardstack/catalog/ - // mapping specifically for live-test runs. - rawCatalogRealmURL: skipCatalog - ? undefined - : process.env.RESOLVED_CATALOG_REALM_URL || defaults.catalogRealmURL, resolvedSkillsRealmURL: process.env.RESOLVED_SKILLS_REALM_URL || defaults.skillsRealmURL, resolvedOpenRouterRealmURL: From 1839007d379980fd3aae0c5a608aa2b7a314a1a1 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Mon, 13 Jul 2026 19:32:34 +0800 Subject: [PATCH 3/6] Move the live-test catalog realm fix entirely into live-test.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit network.ts already reads config.resolvedCatalogRealmURL normally — the gap was that live-test.js only patched a spread copy of the config module for its own bootstrapping loader, never the shared module object that every test's own app instance (via setupApplicationTest) imports normally. Mutating that shared object directly means every later-created app instance sees the real catalog realm address with no production runtime code touched at all. Co-Authored-By: Claude Sonnet 5 --- packages/host/app/services/network.ts | 15 --------------- packages/host/tests/live-test.js | 7 +++++++ 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/host/app/services/network.ts b/packages/host/app/services/network.ts index 69d3937d0bc..b67128941cb 100644 --- a/packages/host/app/services/network.ts +++ b/packages/host/app/services/network.ts @@ -103,21 +103,6 @@ export default class NetworkService extends Service { resolvedTestRealmURL, ); } - // environment.js nulls out resolvedCatalogRealmURL for - // `environment === 'test'`, since the mocked qunit suite has no real - // catalog realm to fetch from. Live tests (testem-live.js) are the - // exception — they run against a real, running catalog realm, whose - // address is already passed in via the same `realmURL` query param - // live-test.js uses to discover which realm's test files to load. - let liveTestParams = new URLSearchParams(window.location.search); - let liveTestRealmURL = liveTestParams.get('realmURL'); - if ( - liveTestParams.has('liveTest') && - !config.resolvedCatalogRealmURL && - liveTestRealmURL - ) { - virtualNetwork.addRealmMapping('@cardstack/catalog/', liveTestRealmURL); - } } return virtualNetwork; } diff --git a/packages/host/tests/live-test.js b/packages/host/tests/live-test.js index 89c28308934..551961f1ccc 100644 --- a/packages/host/tests/live-test.js +++ b/packages/host/tests/live-test.js @@ -67,6 +67,13 @@ export async function loadRealmTests(application) { import('@cardstack/host/config/environment'), ]); + // Mutate the shared config module object directly (not just the copy + // handed to loaderInstance's shim below) so every app instance created + // later by individual tests' setupApplicationTest — which import + // '@cardstack/host/config/environment' as a normal static import, not + // through this loader — also sees the real catalog realm address. + hostConfigEnvironment.default.resolvedCatalogRealmURL = realmURL; + const loaderInstance = application.buildInstance({ rootElement: '#ember-testing-loader', }); From 5391732817b56202caf353c12db45085e8b7b515 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Mon, 13 Jul 2026 21:44:49 +0800 Subject: [PATCH 4/6] Preserve the literal base-realm URL check alongside the RRI-prefix check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isInBaseRealm only normalized via VirtualNetwork.unresolveURL(), which requires a registered realm mapping to recognize a base-realm module — but a module ref built without one (the existing plan-install unit test uses a bare VirtualNetwork with no mappings, and this is also how base refs appear in serialized cards/specs) stays in the literal https://cardstack.com/base/ form and was wrongly treated as needing to be copied into the install destination. Check the literal form first, matching what the RealmPaths-based check did before. Co-Authored-By: Claude Sonnet 5 --- packages/runtime-common/catalog.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/runtime-common/catalog.ts b/packages/runtime-common/catalog.ts index e66e99e49db..6c86e891d04 100644 --- a/packages/runtime-common/catalog.ts +++ b/packages/runtime-common/catalog.ts @@ -7,7 +7,7 @@ import type { CardDef } from '@cardstack/base/card-api'; import { RealmPaths, join } from './paths.ts'; import type { ResolvedCodeRef } from './code-ref.ts'; import { resolveAdoptedCodeRef } from './code-ref.ts'; -import { baseRealmRRI, realmURL } from './constants.ts'; +import { baseRealm, baseRealmRRI, realmURL } from './constants.ts'; import { logger } from './log.ts'; import type { LocalPath } from './paths.ts'; import { rri } from './realm-identifiers.ts'; @@ -23,18 +23,23 @@ export interface Listing extends CardDef { skills: any[]; } -// A resolved codeRef.module is typically the base realm's real backing URL -// (e.g. `https://localhost:4201/base/skill`), not a literal -// `https://cardstack.com/base/` string — VirtualNetwork.unresolveURL() maps +// A codeRef.module for a base-realm class may show up in either form: +// the literal symbolic `https://cardstack.com/base/` URL (e.g. when built +// directly, without going through a VirtualNetwork with real mappings +// registered), or the base realm's real backing URL (e.g. +// `https://localhost:4201/base/skill`) once resolved through one that has +// them. VirtualNetwork.unresolveURL() only normalizes the latter — it maps // a real URL back to its canonical RRI-prefix form (`@cardstack/base/skill`) -// via the registered realm mapping, which is what actually identifies base -// realm membership post-RRI-refactor. Without this, a base-realm module -// gets wrongly treated as something that needs to be copied into the -// install destination. +// via the registered realm mapping — so check the literal symbolic form +// first. Without both, a base-realm module can get wrongly treated as +// something that needs to be copied into the install destination. function isInBaseRealm( module: RealmResourceIdentifier, virtualNetwork: VirtualNetwork, ): boolean { + if (module.startsWith(baseRealm.url)) { + return true; + } return virtualNetwork.unresolveURL(module).startsWith(baseRealmRRI); } From dfe284b9d7e9bea05d312dbc22bbb53687f02a45 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Tue, 14 Jul 2026 21:18:02 +0800 Subject: [PATCH 5/6] Fix isInBaseRealm doc comment to accurately describe unresolveURL's contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit unresolveURL() can canonicalize either the literal symbolic base-realm URL or its real backing URL — the deciding factor is whether the VirtualNetwork has the base realm's mappings registered, not which URL form it's given. Co-Authored-By: Claude Sonnet 5 --- packages/runtime-common/catalog.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/runtime-common/catalog.ts b/packages/runtime-common/catalog.ts index 6c86e891d04..108e2c1ccc1 100644 --- a/packages/runtime-common/catalog.ts +++ b/packages/runtime-common/catalog.ts @@ -23,16 +23,16 @@ export interface Listing extends CardDef { skills: any[]; } -// A codeRef.module for a base-realm class may show up in either form: -// the literal symbolic `https://cardstack.com/base/` URL (e.g. when built -// directly, without going through a VirtualNetwork with real mappings -// registered), or the base realm's real backing URL (e.g. -// `https://localhost:4201/base/skill`) once resolved through one that has -// them. VirtualNetwork.unresolveURL() only normalizes the latter — it maps -// a real URL back to its canonical RRI-prefix form (`@cardstack/base/skill`) -// via the registered realm mapping — so check the literal symbolic form -// first. Without both, a base-realm module can get wrongly treated as -// something that needs to be copied into the install destination. +// A codeRef.module for a base-realm class may show up in either form: the +// literal symbolic `https://cardstack.com/base/` URL, or the base realm's +// real backing URL (e.g. `https://localhost:4201/base/skill`). +// VirtualNetwork.unresolveURL() can canonicalize either form to the +// `@cardstack/base/` RRI prefix, but only when the base realm's URL and +// realm mappings are actually registered on that VirtualNetwork instance — +// a bare VirtualNetwork (e.g. the plan-install unit test) leaves both +// forms unchanged, so the literal-URL check below is still needed. +// Without it, a base-realm module can get wrongly treated as something +// that needs to be copied into the install destination. function isInBaseRealm( module: RealmResourceIdentifier, virtualNetwork: VirtualNetwork, From 0ac8c85126a21347ca1d8f497742e8d21a7ae56b Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Wed, 15 Jul 2026 18:47:12 +0800 Subject: [PATCH 6/6] Canonicalize once in isInBaseRealm instead of matching multiple URL forms The planner now unresolves the incoming module ref to RRI form once and compares against the @cardstack/base/ prefix, dropping the literal-URL branch. That branch existed only for the plan-install unit test's bare VirtualNetwork; the test now registers the base realm mapping the same way the host's network service does at construction. Co-Authored-By: Claude Fable 5 --- packages/host/tests/unit/plan-install-test.ts | 4 +++ packages/runtime-common/catalog.ts | 25 ++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/host/tests/unit/plan-install-test.ts b/packages/host/tests/unit/plan-install-test.ts index c70ace04df8..bd8863fa35a 100644 --- a/packages/host/tests/unit/plan-install-test.ts +++ b/packages/host/tests/unit/plan-install-test.ts @@ -21,6 +21,10 @@ const baseRealmURL = new URL('https://cardstack.com/base/'); const foreignRealmURL = new URL('https://localhost:4201/user1/personal-realm/'); const virtualNetwork = new VirtualNetwork(); +// The install planner canonicalizes module refs through the virtual +// network's realm mappings, so register the base realm prefix the same way +// the host's network service does at construction. +virtualNetwork.addRealmMapping('@cardstack/base/', baseRealmURL.href); module('Unit | Catalog | Install Plan Builder', function () { test('when listing name is not provided, just provides uuid (in this case uuid="xyz")', function (assert) { diff --git a/packages/runtime-common/catalog.ts b/packages/runtime-common/catalog.ts index 108e2c1ccc1..5a83b8716e0 100644 --- a/packages/runtime-common/catalog.ts +++ b/packages/runtime-common/catalog.ts @@ -7,7 +7,7 @@ import type { CardDef } from '@cardstack/base/card-api'; import { RealmPaths, join } from './paths.ts'; import type { ResolvedCodeRef } from './code-ref.ts'; import { resolveAdoptedCodeRef } from './code-ref.ts'; -import { baseRealm, baseRealmRRI, realmURL } from './constants.ts'; +import { baseRealmRRI, realmURL } from './constants.ts'; import { logger } from './log.ts'; import type { LocalPath } from './paths.ts'; import { rri } from './realm-identifiers.ts'; @@ -23,23 +23,20 @@ export interface Listing extends CardDef { skills: any[]; } -// A codeRef.module for a base-realm class may show up in either form: the -// literal symbolic `https://cardstack.com/base/` URL, or the base realm's -// real backing URL (e.g. `https://localhost:4201/base/skill`). -// VirtualNetwork.unresolveURL() can canonicalize either form to the -// `@cardstack/base/` RRI prefix, but only when the base realm's URL and -// realm mappings are actually registered on that VirtualNetwork instance — -// a bare VirtualNetwork (e.g. the plan-install unit test) leaves both -// forms unchanged, so the literal-URL check below is still needed. -// Without it, a base-realm module can get wrongly treated as something -// that needs to be copied into the install destination. +// A codeRef.module for a base-realm class may reach the planner as the +// literal symbolic `https://cardstack.com/base/` URL or as the base realm's +// real backing URL (e.g. `https://localhost:4201/base/skill`), so +// canonicalize once to RRI form and compare against the `@cardstack/base/` +// prefix. This requires the base realm's URL and realm mappings to be +// registered on the VirtualNetwork the caller hands us — every production +// VirtualNetwork registers them at construction (see the host's network +// service), and tests must do the same. The durable end-state is for refs +// to already be canonical before they reach this layer, at which point the +// unresolveURL call collapses to a no-op prefix check. function isInBaseRealm( module: RealmResourceIdentifier, virtualNetwork: VirtualNetwork, ): boolean { - if (module.startsWith(baseRealm.url)) { - return true; - } return virtualNetwork.unresolveURL(module).startsWith(baseRealmRRI); }