From c06fd7840e966d0a19108bd2ddfd954c3b1f3d92 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 21 Aug 2026 08:54:12 -0400 Subject: [PATCH] Open the source of a mapped-realm card from Edit Template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `identifyCard` reports a module in canonical form, so for a realm reached through a registered prefix it is an identifier rather than a URL. Edit Template parsed it as a URL, which throws — selecting any card whose type lives in the base, catalog or skills realm and asking to edit its template did nothing. Pass the identifier through instead. `updateCodePath` already accepts one and resolves it, so nothing else has to change; the parse was the whole bug. An audit of the other consumers found no second instance: of the nineteen `identify` / `identifyCard` call sites in the host, this was the only one feeding a module into `new URL`. The one other place that parses a code ref's module, `serializers/code-ref.ts`, guards on the identifier being URL-shaped first. Co-Authored-By: Claude Opus 5 --- .../operator-mode/preview-panel/index.gts | 8 +++-- .../tests/acceptance/code-submode-test.ts | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/host/app/components/operator-mode/preview-panel/index.gts b/packages/host/app/components/operator-mode/preview-panel/index.gts index 5ad553f2113..c9b280b8222 100644 --- a/packages/host/app/components/operator-mode/preview-panel/index.gts +++ b/packages/host/app/components/operator-mode/preview-panel/index.gts @@ -26,6 +26,7 @@ import { isCardInstance, isFileDefInstance, isResolvedCodeRef, + rri, cardDefFormats, fileDefFormats, fieldDefFormats, @@ -133,10 +134,13 @@ export default class PreviewPanel extends Component { private editTemplate = () => { const type = identifyCard(this.args.card.constructor as any); if (type && isResolvedCodeRef(type)) { - const gtsFileUrl = type.module.endsWith('.gts') + // `identifyCard` reports a module in canonical form, which for a + // mapped realm is a prefix identifier rather than a URL — parsing it + // here would throw. `updateCodePath` resolves an identifier itself. + const gtsFile = type.module.endsWith('.gts') ? type.module : `${type.module}.gts`; - this.operatorModeStateService.updateCodePath(new URL(gtsFileUrl)); + this.operatorModeStateService.updateCodePath(rri(gtsFile)); } }; diff --git a/packages/host/tests/acceptance/code-submode-test.ts b/packages/host/tests/acceptance/code-submode-test.ts index 809e5266aa7..a9096d1d224 100644 --- a/packages/host/tests/acceptance/code-submode-test.ts +++ b/packages/host/tests/acceptance/code-submode-test.ts @@ -1,5 +1,6 @@ import { click, + find, waitFor, fillIn, triggerEvent, @@ -2911,5 +2912,40 @@ module('Acceptance | code submode tests', function (_hooks) { .dom('[data-test-card-url-bar-input]') .hasValue(`${testRealmURL}person.gts`); }); + + test('Edit Template opens the source of a card whose type lives in a mapped realm', async function (assert) { + // `person-entry` is a Spec, so its type resolves to the base realm — a + // prefix identifier rather than a URL. The button used to parse that + // module as a URL, which throws for any mapped realm, so the source + // never opened. + await visitOperatorMode({ + stacks: [ + [ + { + id: `${testRealmURL}person-entry`, + format: 'isolated', + }, + ], + ], + submode: 'code', + codePath: `${testRealmURL}person-entry.json`, + }); + + await waitFor('[data-test-card-resource-loaded]'); + await click('[data-test-edit-template-button]'); + + await waitFor('[data-test-card-url-bar-input]'); + let codePath = ( + find('[data-test-card-url-bar-input]') as HTMLInputElement + ).value; + assert.true( + codePath.endsWith('spec.gts'), + `the base realm's spec source opened (got ${codePath})`, + ); + assert.false( + codePath.startsWith('@'), + 'the identifier was resolved to a URL rather than passed through raw', + ); + }); }); });