diff --git a/apps/desktop/extensions/ai-sidebar/moshpit-drift.test.js b/apps/desktop/extensions/ai-sidebar/moshpit-drift.test.js new file mode 100644 index 0000000..fc2bc1a --- /dev/null +++ b/apps/desktop/extensions/ai-sidebar/moshpit-drift.test.js @@ -0,0 +1,93 @@ +// Both local copies of the resolution policy, against the published one. +// +// These rules exist twice here: moshpit-resolve.ts, and a hand port of it into +// the extension because the extension is plain JS and `pnpm build` is `tsc`, +// which compiles but does not bundle. A package cannot be imported at runtime +// from an unbundled extension, so the copies stay. +// +// moshpit.test.js already compares those two against each other. That catches +// one of them drifting and misses both drifting together — and "both" is the +// likely case, since the same person edits them in the same commit. +// +// So this anchors each of them to @moshcoder/moshpit-resolve instead: the +// published package is the reference, and either copy leaving it is caught. +// Behaviour rather than bytes, because the copies legitimately differ in +// comments, in TypeScript types, and in the chrome-coupled config reader the +// package deliberately does not carry. +import { describe, expect, it } from 'vitest'; + +import * as ext from './moshpit.js'; +import * as ts from '../../src/moshpit-resolve'; +import * as pkg from '@moshcoder/moshpit-resolve'; + +const HOSTNAMES = [ + 'blue.eggs', 'a.b.c', '1.2.3.4', 'localhost', '', 'eggs', 'mosh.eggs', + 'blue.420', '420.blue', '1.420', '192.168', 'x.y', 'A.EGGS.', +]; + +const LOOKUPS = [ + null, + { registered: false, resolved: '', target: null }, + { registered: true, name_registered: false, resolved: 'blue.eggs', target: null }, + { registered: true, name_registered: true, resolved: 'blue.eggs', target: null }, + { registered: true, name_registered: true, resolved: 'blue.eggs', target: '203.0.113.9' }, +]; + +// The three implementations, named so a failure says which one moved. +const IMPLS = [['extension', ext], ['typescript', ts]]; + +describe('every local copy still matches @moshcoder/moshpit-resolve', () => { + for (const [name, impl] of IMPLS) { + describe(name, () => { + it('parses hostnames the same way', () => { + for (const h of HOSTNAMES) { + expect(impl.parseRegistryName(h), h).toEqual(pkg.parseRegistryName(h)); + } + }); + + it('builds the same gateway and parking URLs', () => { + for (const h of ['blue.eggs', 'a.b']) { + expect(impl.gatewayUrlFor(h)).toBe(pkg.gatewayUrlFor(h)); + expect(impl.parkingUrlFor(h)).toBe(pkg.parkingUrlFor(h)); + // A self-hosted pit has to be followed by all of them or none. + expect(impl.gatewayUrlFor(h, 'https://my.pit/')).toBe(pkg.gatewayUrlFor(h, 'https://my.pit/')); + expect(impl.parkingUrlFor(h, 'https://my.pit/')).toBe(pkg.parkingUrlFor(h, 'https://my.pit/')); + } + }); + + it('agrees on the console label', () => { + for (const h of HOSTNAMES) { + expect(impl.consoleUrlFor(h), h).toEqual(pkg.consoleUrlFor(h)); + } + }); + + it('decides resolution identically across the whole input space', () => { + for (const hostname of HOSTNAMES) { + for (const mode of ['clearnet', 'moshpit']) { + for (const clearnetResolves of [true, false]) { + for (const moshpit of LOOKUPS) { + const inputs = { hostname, mode, clearnetResolves, moshpit }; + expect(impl.decideResolution(inputs), JSON.stringify(inputs)) + .toEqual(pkg.decideResolution(inputs)); + } + } + } + } + }); + + it('shares the defaults', () => { + for (const key of ['DEFAULT_REGISTRY_BASE', 'DEFAULT_CONSOLE_BASE', 'DEFAULT_PARKING_BASE', 'CONSOLE_LABEL']) { + expect(impl[key], key).toBe(pkg[key]); + } + }); + }); + } + + it('the package is a subset — it carries no chrome coupling', () => { + // moshpitConfig reads chrome.storage, which is why the package does not + // have it. If it ever appears there, the package has stopped being usable + // outside a browser extension. + expect(pkg.moshpitConfig).toBeUndefined(); + expect(typeof ext.moshpitConfig).toBe('function'); + }); +}); diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ce94db6..a86ce88 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -24,6 +24,7 @@ "chromium:package": "chromium/scripts/package.sh" }, "devDependencies": { + "@moshcoder/moshpit-resolve": "^0.1.1", "typescript": "^5.6.3", "vitest": "^2.1.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8000fc4..6ca71c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: apps/desktop: devDependencies: + '@moshcoder/moshpit-resolve': + specifier: ^0.1.1 + version: 0.1.1 typescript: specifier: ^5.6.3 version: 5.9.3 @@ -1177,6 +1180,11 @@ packages: deepagents: optional: true + '@moshcoder/moshpit-resolve@0.1.1': + resolution: {integrity: sha512-l5IZQ73DFvxlYhdQnA6frl8BepWhibVtSLzucaok4d2G9mbR8Fe7PBKQygxXvILLllc+jyODIxt8qrK+LcbvkA==} + engines: {node: '>=18'} + hasBin: true + '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} @@ -4575,6 +4583,8 @@ snapshots: '@langchain/langgraph': 1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod@4.4.3) deepagents: 1.10.5(langsmith@0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0) + '@moshcoder/moshpit-resolve@0.1.1': {} + '@neon-rs/load@0.0.4': {} '@nodelib/fs.scandir@2.1.5':