Repository: compiler-explorer/compiler-explorer (ts)

Issue:
[BUG]: Opt remarks display mismatched row numbers
### Describe the bug

![Image](https://github.com/user-attachments/assets/8d5e0fba-b29f-49dd-b25a-70f3100c33f1)

Since opt-remarks are now inline-text and take up line numbers, they line numbers for emitted opt-remarks are mixed up.

### Steps to reproduce

-

### Expected behavior

matching line numbers

### Reproduction link

https://www.compiler-explorer.com/z/4aWz7bYdc

### Screenshots

Not applicable

### Operating System

_No response_

### Browser version

_No response_

Pull request description:
Show opt remarks in a view zone instead of as editor content
Resolves #7662.

[View zones](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IViewZone.html) are rendered in the editor widget but don’t count as lines themselves. Monaco doesn’t automatically grow them to fit their content (and doesn’t extend the scroll area to fit them when the text is rendered without line breaks), so the height must be calculated manually.

Additionally, this changes the remarks to be *above* the line they are attached to. This is more intuitive in my opinion, but can be reverted again if you prefer.

Comparison:

before:
![opt_view_before](https://github.com/user-attachments/assets/7cd34349-4a97-4da8-b3c4-ec197542fb35)

after:
![opt_view](https://github.com/user-attachments/assets/c731ae02-b626-44d7-a9a2-77743747733d)


Patch:
diff --git a/CLAUDE.md b/CLAUDE.md
index fc2c78191..5db914d0b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -47,6 +47,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
   // Initialises the thing
   initialiseThing();
   ```
+- Avoid redundant function header comments that merely repeat the function name. For example:
+  ```
+  /**
+   * Sets up compiler change handling
+   */
+  function setupCompilerChangeHandling() {...}
+  ```
+  In this case, the function name already clearly states what it does.
+- Comments should provide additional context or explain "why" something is done, not just restate "what" is being done.
+- Only add function header comments when they provide meaningful information beyond what the function name and signature convey.
+- Use British English spellings for things like "initialise" and "colour", but only in new code. It's a preference not a hard requirement
 
 ## Testing Guidelines
 - Use Vitest for unit tests (compatible with Jest syntax)
diff --git a/etc/config/kotlin.amazon.properties b/etc/config/kotlin.amazon.properties
index 5ea01b1bb..d4d8ddef2 100644
--- a/etc/config/kotlin.amazon.properties
+++ b/etc/config/kotlin.amazon.properties
@@ -3,7 +3,7 @@ compilerType=kotlin
 versionFlag=-version
 objdumper=/opt/compiler-explorer/jdk-23.0.1/bin/javap
 instructionSet=java
-defaultCompiler=kotlinc2100
+defaultCompiler=kotlinc2121
 demangler=
 postProcess=
 options=
@@ -12,7 +12,7 @@ needsMulti=false
 supportsExecute=true
 interpreted=true
 
-group.kotlin.compilers=kotlinc1400:kotlinc1410:kotlinc1420:kotlinc1421:kotlinc1430:kotlinc1431:kotlinc1432:kotlinc1500:kotlinc1510:kotlinc1520:kotlinc1521:kotlinc1530:kotlinc1531:kotlinc1600:kotlinc1610:kotlinc1620:kotlinc1700:kotlinc1800:kotlinc1810:kotlinc1820:kotlinc1900:kotlinc1910:kotlinc1920:kotlinc2000:kotlinc2010:kotlinc2020:kotlinc2021:kotlinc2100
+group.kotlin.compilers=kotlinc1400:kotlinc1410:kotlinc1420:kotlinc1421:kotlinc1430:kotlinc1431:kotlinc1432:kotlinc1500:kotlinc1510:kotlinc1520:kotlinc1521:kotlinc1530:kotlinc1531:kotlinc1600:kotlinc1610:kotlinc1620:kotlinc1700:kotlinc1800:kotlinc1810:kotlinc1820:kotlinc1900:kotlinc1910:kotlinc1920:kotlinc2000:kotlinc2010:kotlinc2020:kotlinc2021:kotlinc2100:kotlinc2110:kotlinc2120:kotlinc2121
 group.kotlin.groupName=Kotlin
 group.kotlin.baseName=kotlinc
 group.kotlin.isSemVer=true
@@ -131,3 +131,31 @@ compiler.kotlinc2100.exe=/opt/compiler-explorer/kotlin-jvm-2.1.0/bin/kotlinc-jvm
 compiler.kotlinc2100.semver=2.1.0
 compiler.kotlinc2100.java_home=/opt/compiler-explorer/jdk-23.0.1
 compiler.kotlinc2100.runtime=/opt/compiler-explorer/jdk-23.0.1/bin/java
+compiler.kotlinc2110.exe=/opt/compiler-explorer/kotlin-jvm-2.1.10/bin/kotlinc-jvm
+compiler.kotlinc2110.semver=2.1.10
+compiler.kotlinc2110.java_home=/opt/compiler-explorer/jdk-23.0.1
+compiler.kotlinc2110.runtime=/opt/compiler-explorer/jdk-23.0.1/bin/java
+compiler.kotlinc2120.exe=/opt/compiler-explorer/kotlin-jvm-2.1.20/bin/kotlinc-jvm
+compiler.kotlinc2120.semver=2.1.20
+compiler.kotlinc2120.java_home=/opt/compiler-explorer/jdk-23.0.1
+compiler.kotlinc2120.runtime=/opt/compiler-explorer/jdk-23.0.1/bin/java
+compiler.kotlinc2121.exe=/opt/compiler-explorer/kotlin-jvm-2.1.21/bin/kotlinc-jvm
+compiler.kotlinc2121.semver=2.1.21
+compiler.kotlinc2121.java_home=/opt/compiler-explorer/jdk-23.0.1
+compiler.kotlinc2121.runtime=/opt/compiler-explorer/jdk-23.0.1/bin/java
+
+libs=kotlinx-coroutines-core-jvm
+
+libs.kotlinx-coroutines-core-jvm.name=kotlinx.coroutines
+libs.kotlinx-coroutines-core-jvm.versions=1102:190:181:173:164
+
+libs.kotlinx-coroutines-core-jvm.versions.1102.version=1.10.2
+libs.kotlinx-coroutines-core-jvm.versions.1102.path=/opt/compiler-explorer/kotlinx-coroutines-core-jvm-1.10.2/kotlinx-coroutines-core-jvm.jar
+libs.kotlinx-coroutines-core-jvm.versions.190.version=1.9.0
+libs.kotlinx-coroutines-core-jvm.versions.190.path=/opt/compiler-explorer/kotlinx-coroutines-core-jvm-1.9.0/kotlinx-coroutines-core-jvm.jar
+libs.kotlinx-coroutines-core-jvm.versions.181.version=1.8.1
+libs.kotlinx-coroutines-core-jvm.versions.181.path=/opt/compiler-explorer/kotlinx-coroutines-core-jvm-1.8.1/kotlinx-coroutines-core-jvm.jar
+libs.kotlinx-coroutines-core-jvm.versions.173.version=1.7.3
+libs.kotlinx-coroutines-core-jvm.versions.173.path=/opt/compiler-explorer/kotlinx-coroutines-core-jvm-1.7.3/kotlinx-coroutines-core-jvm.jar
+libs.kotlinx-coroutines-core-jvm.versions.164.version=1.6.4
+libs.kotlinx-coroutines-core-jvm.versions.164.path=/opt/compiler-explorer/kotlinx-coroutines-core-jvm-1.6.4/kotlinx-coroutines-core-jvm.jar
diff --git a/lib/buildenvsetup/ceconan.ts b/lib/buildenvsetup/ceconan.ts
index b6650a19f..0d105380f 100644
--- a/lib/buildenvsetup/ceconan.ts
+++ b/lib/buildenvsetup/ceconan.ts
@@ -253,6 +253,11 @@ export class BuildEnvSetupCeConanDirect extends BuildEnvSetupBase {
                 if (key === 'compiler.libcxx' && elem.settings['compiler'] === 'cshared') {
                     return true;
                 }
+                if (key === 'stdver') {
+                    // unless ABI breakage happens in future stdversions, assume they are all cxx11 and compatible
+                    //  or if not that the compiler.version has already made sure those won't be matched
+                    return true;
+                }
                 return val === elem.settings[key];
             });
         });
diff --git a/lib/options-handler.ts b/lib/options-handler.ts
index 6b477d350..fb8bf15fe 100755
--- a/lib/options-handler.ts
+++ b/lib/options-handler.ts
@@ -37,6 +37,7 @@ import type {Source} from '../types/source.interfaces.js';
 import type {ToolTypeKey} from '../types/tool.interfaces.js';
 import {AppArguments} from './app.interfaces.js';
 
+import {getRemoteId} from '../shared/remote-utils.js';
 import {logger} from './logger.js';
 import type {PropertyGetter, PropertyValue} from './properties.interfaces.js';
 import {CompilerProps} from './properties.js';
@@ -376,11 +377,6 @@ export class ClientOptionsHandler {
         return libraries;
     }
 
-    getRemoteId(remoteUrl: string, language: LanguageKey) {
-        const url = new URL(remoteUrl);
-        return url.host.replaceAll('.', '_') + '_' + language;
-    }
-
     libArrayToObject(libsArr: any[]) {
         const libs: Record<string, any> = {};
         for (const lib of libsArr) {
@@ -397,9 +393,9 @@ export class ClientOptionsHandler {
     }
 
     async getRemoteLibraries(language: LanguageKey, remoteUrl: string) {
-        const remoteId = this.getRemoteId(remoteUrl, language);
+        const remoteId = getRemoteId(remoteUrl, language);
         if (!this.remoteLibs[remoteId]) {
-            return new Promise(resolve => {
+            return await new Promise(resolve => {
                 const url = ClientOptionsHandler.getRemoteUrlForLibraries(remoteUrl, language);
                 logger.info(`Fetching remote libraries from ${url}`);
                 let fullData = '';
diff --git a/shared/remote-utils.ts b/shared/remote-utils.ts
new file mode 100644
index 000000000..23b5c083b
--- /dev/null
+++ b/shared/remote-utils.ts
@@ -0,0 +1,28 @@
+// Copyright (c) 2025, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+export function getRemoteId(remoteUrl: string, language: string): string {
+    const url: URL = new URL(remoteUrl);
+    return url.host.replace(/\./g, '_') + url.pathname.replace(/\//g, '_') + '_' + language;
+}
diff --git a/static/lib-utils.ts b/static/lib-utils.ts
index 88f111c35..f1a5b2b68 100644
--- a/static/lib-utils.ts
+++ b/static/lib-utils.ts
@@ -22,19 +22,15 @@
 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 // POSSIBILITY OF SUCH DAMAGE.
 
+import {getRemoteId} from '../shared/remote-utils.js';
 import {Remote} from './compiler.interfaces.js';
 import {LanguageLibs, Library} from './options.interfaces.js';
 import {options} from './options.js';
 
 const LIB_MATCH_RE = /([\w-]*)\.([\w-]*)/i;
 
-function getRemoteId(language: string, remoteUrl: string): string {
-    const url: URL = new URL(remoteUrl);
-    return url.host.replace(/\./g, '_') + '_' + language;
-}
-
 function getRemoteLibraries(language: string, remoteUrl: string): LanguageLibs {
-    const remoteId = getRemoteId(language, remoteUrl);
+    const remoteId = getRemoteId(remoteUrl, language);
     return options.remoteLibs[remoteId];
 }
 
@@ -78,7 +74,7 @@ export function getSupportedLibraries(
         }
         return allLibs;
     }
-    const allLibs = getRemoteLibraries(langId, remote.target);
+    const allLibs = getRemoteLibraries(langId, remote.target + remote.basePath);
     if (supportedLibrariesArr && supportedLibrariesArr.length > 0) {
         return copyAndFilterLibraries(allLibs, supportedLibrariesArr);
     }
diff --git a/static/panes/opt-view.ts b/static/panes/opt-view.ts
index a4d228e96..99454566d 100644
--- a/static/panes/opt-view.ts
+++ b/static/panes/opt-view.ts
@@ -38,14 +38,6 @@ import {Hub} from '../hub.js';
 import {extendConfig} from '../monaco-config.js';
 import {Toggles} from '../widgets/toggles.js';
 
-type OptClass = 'None' | 'Missed' | 'Passed' | 'Analysis';
-
-type OptviewLine = {
-    text: string;
-    srcLine: number;
-    optClass: OptClass;
-};
-
 export class Opt extends MonacoPane<monaco.editor.IStandaloneCodeEditor, OptState> {
     // Note: bool | undef here instead of just bool because of an issue with field initialization order
     private isCompilerSupported?: boolean;
@@ -54,13 +46,14 @@ export class Opt extends MonacoPane<monaco.editor.IStandaloneCodeEditor, OptStat
     private wrapButton: JQuery<HTMLElement>;
     private wrapTitle: JQuery<HTMLElement>;
 
-    // Keep optRemarks as state, to avoid triggerring a recompile when options change
+    // Keep optRemarks as state, to avoid triggering a recompile when options change
     private optRemarks: OptRemark[];
-    private srcAsOptview: OptviewLine[];
+    private optRemarkViewZoneIds: string[];
 
     constructor(hub: Hub, container: Container, state: OptState & MonacoPaneState) {
         super(hub, container, state);
         this.optRemarks = state.optOutput ?? [];
+        this.optRemarkViewZoneIds = [];
         this.eventHub.emit('optViewOpened', this.compilerInfo.compilerId);
     }
 
@@ -124,29 +117,19 @@ export class Opt extends MonacoPane<monaco.editor.IStandaloneCodeEditor, OptStat
         this.editor.onDidChangeCursorSelection(e => {
             cursorSelectionThrottledFunction(e);
         });
+
+        this.editor.onDidLayoutChange(({contentWidth}) => {
+            this.showOptRemarks(contentWidth);
+        });
     }
 
     override onCompileResult(id: number, compiler: CompilerInfo, result: CompilationResult) {
         if (this.compilerInfo.compilerId !== id || !this.isCompilerSupported) return;
 
-        const splitLines = (text: string): string[] => {
-            if (!text) return [];
-            const result = text.split(/\r?\n/);
-            if (result.length > 0 && result[result.length - 1] === '') return result.slice(0, -1);
-            return result;
-        };
-        const srcLines: string[] = result.source ? splitLines(result.source) : [];
-        this.srcAsOptview = [];
-
-        for (const i in srcLines) {
-            this.srcAsOptview.push({text: srcLines[i], srcLine: Number(i), optClass: 'None'});
-        }
-
         this.editor.setValue(unwrap(result.source));
-        if (result.optOutput) {
-            this.optRemarks = result.optOutput;
-            this.showOptRemarks();
-        }
+        this.optRemarks = result.optOutput ?? [];
+        this.showOptRemarks();
+
         // TODO: This is inelegant again. Previously took advantage of fourth argument for the compileResult event.
         const lang = compiler.lang === 'c++' ? 'cpp' : compiler.lang;
         const model = this.editor.getModel();
@@ -172,7 +155,7 @@ export class Opt extends MonacoPane<monaco.editor.IStandaloneCodeEditor, OptStat
         return 'Opt Viewer';
     }
 
-    showOptRemarks() {
+    showOptRemarks(width?: number) {
         const filters = this.filters.get();
         const includeMissed: boolean = filters['filter-missed'];
         const includePassed: boolean = filters['filter-passed'];
@@ -186,41 +169,32 @@ export class Opt extends MonacoPane<monaco.editor.IStandaloneCodeEditor, OptStat
                     (rem.optType === 'Analysis' && includeAnalysis))
             );
         });
-        const groupedRemarks = _(remarksToDisplay).groupBy(x => x.DebugLoc.Line);
-
-        const resLines = [...this.srcAsOptview];
-        for (const [key, value] of Object.entries(groupedRemarks)) {
-            const origLineNum = Number(key);
-            const curLineNum = resLines.findIndex(line => line.srcLine === origLineNum);
-            const contents = value.map(rem => ({
-                text: rem.displayString,
-                srcLine: -1,
-                optClass: rem.optType,
-            }));
-            resLines.splice(curLineNum, 0, ...contents);
-        }
 
-        const newText: string = resLines.reduce((accText, curSrcLine) => {
-            return accText + (curSrcLine.optClass === 'None' ? curSrcLine.text : '  ') + '\n';
-        }, '');
-        this.editor.setValue(newText);
-
-        const optDecorations: monaco.editor.IModelDeltaDecoration[] = [];
-        resLines.forEach((line, lineNum) => {
-            if (line.optClass !== 'None') {
-                optDecorations.push({
-                    range: new monaco.Range(lineNum + 1, 1, lineNum + 1, Number.POSITIVE_INFINITY),
-                    options: {
-                        isWholeLine: true,
-                        after: {
-                            content: line.text,
-                        },
-                        inlineClassName: 'opt-line.' + line.optClass.toLowerCase(),
-                    },
+        this.editor?.changeViewZones(accessor => {
+            const maxWidth = width ?? this.editor.getLayoutInfo().contentWidth;
+            this.optRemarkViewZoneIds.forEach(id => accessor.removeZone(id));
+            this.optRemarkViewZoneIds = remarksToDisplay.map(({displayString, optType, DebugLoc}) => {
+                const domNode = document.createElement('div');
+                domNode.classList.add('view-line', 'opt-line', optType.toLowerCase());
+                this.editor.applyFontInfo(domNode);
+                domNode.innerText = displayString;
+
+                // let the browser calculate the height that the text needs
+                domNode.style.visibility = 'hidden';
+                domNode.style.maxWidth = `${maxWidth}px`;
+                document.body.appendChild(domNode);
+                const heightInPx = domNode.clientHeight;
+                document.body.removeChild(domNode);
+                domNode.style.removeProperty('visibility');
+                domNode.style.removeProperty('max-width');
+
+                return accessor.addZone({
+                    domNode,
+                    heightInPx,
+                    afterLineNumber: DebugLoc.Line === 0 ? 0 : DebugLoc.Line - 1,
                 });
-            }
+            });
         });
-        this.editorDecorations.set(optDecorations);
     }
 
     override onCompiler(id: number, compiler: CompilerInfo | null, options: string, editorId: number, treeId: number) {
@@ -230,6 +204,8 @@ export class Opt extends MonacoPane<monaco.editor.IStandaloneCodeEditor, OptStat
             this.isCompilerSupported = compiler ? compiler.supportsOptOutput : false;
             if (!this.isCompilerSupported) {
                 this.editor.setValue('<OPT remarks are not supported for this compiler>');
+                this.optRemarks = [];
+                this.showOptRemarks();
             }
         }
     }
diff --git a/static/panes/stack-usage-view.ts b/static/panes/stack-usage-view.ts
index 55a76b3fe..47060cc80 100644
--- a/static/panes/stack-usage-view.ts
+++ b/static/panes/stack-usage-view.ts
@@ -87,9 +87,10 @@ export class StackUsage extends MonacoPane<monaco.editor.IStandaloneCodeEditor,
 
     override onCompileResult(id: number, compiler: CompilerInfo, result: CompilationResult) {
         if (this.compilerInfo.compilerId !== id || !this.isCompilerSupported) return;
-        this.editor.setValue(unwrap(result.source));
+        const source = unwrap(result.source);
+        this.editor.setValue(source);
         if (result.stackUsageOutput) {
-            this.showStackUsageResults(result.stackUsageOutput);
+            this.showStackUsageResults(result.stackUsageOutput, source);
         }
 
         // TODO: This is inelegant again. Previously took advantage of fourth argument for the compileResult event.
@@ -121,7 +122,7 @@ export class StackUsage extends MonacoPane<monaco.editor.IStandaloneCodeEditor,
         return '<Unimplemented>';
     }
 
-    showStackUsageResults(suEntries: suCodeEntry[], source?: string) {
+    showStackUsageResults(suEntries: suCodeEntry[], source: string) {
         const splitLines = (text: string): string[] => {
             if (!text) return [];
             const result = text.split(/\r?\n/);


Test patch:
diff --git a/cypress/e2e/remote-id.cy.ts b/cypress/e2e/remote-id.cy.ts
new file mode 100644
index 000000000..c043eca90
--- /dev/null
+++ b/cypress/e2e/remote-id.cy.ts
@@ -0,0 +1,9 @@
+import {runFrontendTest} from '../support/utils';
+
+describe('RemoteId testing', () => {
+    before(() => {
+        cy.visit('/');
+    });
+
+    runFrontendTest('remoteId');
+});
diff --git a/shared/url-testcases.ts b/shared/url-testcases.ts
new file mode 100644
index 000000000..dc3616891
--- /dev/null
+++ b/shared/url-testcases.ts
@@ -0,0 +1,32 @@
+export const UrlTestCases = [
+    {
+        remoteUrl: 'https://example.com/path/to/resource',
+        language: 'cpp',
+        expectedId: 'example_com_path_to_resource_cpp',
+    },
+    {
+        remoteUrl: 'https://example.com',
+        language: 'java',
+        expectedId: 'example_com__java',
+    },
+    {
+        remoteUrl: 'https://sub.domain.com/a/b/c/',
+        language: 'rust',
+        expectedId: 'sub_domain_com_a_b_c__rust',
+    },
+    {
+        remoteUrl: 'https://godbolt.org:443/gpu',
+        language: 'c++',
+        expectedId: 'godbolt_org_gpu_c++',
+    },
+    {
+        remoteUrl: 'https://godbolt.org:443/winprod',
+        language: 'c++',
+        expectedId: 'godbolt_org_winprod_c++',
+    },
+    {
+        remoteUrl: 'https://godbolt.org:443',
+        language: 'c++',
+        expectedId: 'godbolt_org__c++',
+    },
+];
diff --git a/static/tests/_all.ts b/static/tests/_all.ts
index 0f2f1ec18..6c9b96803 100644
--- a/static/tests/_all.ts
+++ b/static/tests/_all.ts
@@ -25,3 +25,4 @@
 import './frontend-testing';
 import './hello-world';
 import './motd';
+import './remote-id';
diff --git a/static/tests/remote-id.ts b/static/tests/remote-id.ts
new file mode 100644
index 000000000..3d86d6b78
--- /dev/null
+++ b/static/tests/remote-id.ts
@@ -0,0 +1,47 @@
+// Copyright (c) 2025, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+/// <reference path="../../node_modules/cypress/types/cypress-global-vars.d.ts" />
+
+import {getRemoteId} from '../../shared/remote-utils.js';
+
+import {UrlTestCases} from '../../shared/url-testcases.js';
+
+import {ITestable} from './frontend-testing.interfaces.js';
+
+class RemoteIdTests implements ITestable {
+    public readonly description: string = 'remoteId';
+
+    public async run() {
+        UrlTestCases.forEach(testCase => {
+            if (getRemoteId(testCase.remoteUrl, testCase.language) !== testCase.expectedId) {
+                throw new Error(
+                    `Test case failed for language: ${testCase.language}, remoteUrl: ${testCase.remoteUrl}, expectedId: ${testCase.expectedId}`,
+                );
+            }
+        });
+    }
+}
+
+window.compilerExplorerFrontendTesting.add(new RemoteIdTests());
diff --git a/test/options-handler.ts b/test/options-handler.ts
index a8d4653dc..8e36e18b7 100644
--- a/test/options-handler.ts
+++ b/test/options-handler.ts
@@ -37,6 +37,9 @@ import {BaseTool} from '../lib/tooling/base-tool.js';
 import {CompilerInfo} from '../types/compiler.interfaces.js';
 import {LanguageKey} from '../types/languages.interfaces.js';
 
+import {getRemoteId} from '../shared/remote-utils.js';
+import {UrlTestCases} from '../shared/url-testcases.js';
+
 import {makeFakeCompilerInfo} from './utils.js';
 
 const languages = {
@@ -593,4 +596,13 @@ describe('Options handler', () => {
 
         expect(librariesUrl).toEqual('https://godbolt.org:443/gpu/api/libraries/c++');
     });
+
+    describe('getRemoteId', () => {
+        UrlTestCases.forEach(testCase => {
+            it(`should generate remote ID for URL "${testCase.remoteUrl}" with language "${testCase.language}"`, () => {
+                const result = getRemoteId(testCase.remoteUrl, testCase.language);
+                expect(result).toBe(testCase.expectedId);
+            });
+        });
+    });
 });