diff --git a/app/src/utils/__tests__/routeUtils.test.ts b/app/src/utils/__tests__/routeUtils.test.ts new file mode 100644 index 000000000..c3563644a --- /dev/null +++ b/app/src/utils/__tests__/routeUtils.test.ts @@ -0,0 +1,94 @@ +import { + stripNeighbourhoodPrefix, + restoreNeighbourhoodPrefix, + stripChannelPrefix, + restoreChannelPrefix, +} from '../routeUtils'; + +describe('stripNeighbourhoodPrefix', () => { + it('strips neighbourhood:// prefix', () => { + expect(stripNeighbourhoodPrefix('neighbourhood://abc123')).toBe('abc123'); + }); + + it('strips private:// prefix', () => { + expect(stripNeighbourhoodPrefix('private://abc123')).toBe('abc123'); + }); + + it('returns bare strings unchanged', () => { + expect(stripNeighbourhoodPrefix('abc123')).toBe('abc123'); + }); + + it('returns empty string for undefined/empty input', () => { + expect(stripNeighbourhoodPrefix(undefined as unknown as string)).toBe(''); + expect(stripNeighbourhoodPrefix('')).toBe(''); + }); +}); + +describe('restoreNeighbourhoodPrefix', () => { + it('prepends neighbourhood:// to bare IDs', () => { + expect(restoreNeighbourhoodPrefix('abc123')).toBe('neighbourhood://abc123'); + }); + + it('passes through neighbourhood:// IDs unchanged', () => { + expect(restoreNeighbourhoodPrefix('neighbourhood://abc123')).toBe('neighbourhood://abc123'); + }); + + it('passes through private:// IDs unchanged', () => { + expect(restoreNeighbourhoodPrefix('private://abc123')).toBe('private://abc123'); + }); + + it('returns empty string for undefined/empty input', () => { + expect(restoreNeighbourhoodPrefix(undefined as unknown as string)).toBe(''); + expect(restoreNeighbourhoodPrefix('')).toBe(''); + }); +}); + +describe('stripChannelPrefix', () => { + it('strips literal:string: prefix', () => { + expect(stripChannelPrefix('literal:string:abc123')).toBe('abc123'); + }); + + it('passes through ad4m://obj/ IDs unchanged', () => { + expect(stripChannelPrefix('ad4m://obj/abc123')).toBe('ad4m://obj/abc123'); + }); + + it('passes through bare strings unchanged', () => { + expect(stripChannelPrefix('abc123')).toBe('abc123'); + }); + + it('returns empty string for undefined/empty input', () => { + expect(stripChannelPrefix(undefined as unknown as string)).toBe(''); + expect(stripChannelPrefix('')).toBe(''); + }); +}); + +describe('restoreChannelPrefix', () => { + it('prepends literal:string: to bare IDs', () => { + expect(restoreChannelPrefix('abc123')).toBe('literal:string:abc123'); + }); + + it('passes through ad4m://obj/ IDs unchanged', () => { + expect(restoreChannelPrefix('ad4m://obj/abc123')).toBe('ad4m://obj/abc123'); + }); + + it('passes through any :// scheme unchanged', () => { + expect(restoreChannelPrefix('neighbourhood://abc123')).toBe('neighbourhood://abc123'); + }); + + it('returns empty string for undefined/empty input', () => { + expect(restoreChannelPrefix(undefined as unknown as string)).toBe(''); + expect(restoreChannelPrefix('')).toBe(''); + }); +}); + +describe('round-trip: stripChannelPrefix → restoreChannelPrefix', () => { + it('round-trips legacy literal:string: IDs', () => { + const original = 'literal:string:abc123'; + expect(restoreChannelPrefix(stripChannelPrefix(original))).toBe(original); + }); + + it('round-trips ad4m://obj/ IDs', () => { + const original = 'ad4m://obj/abc123'; + expect(restoreChannelPrefix(stripChannelPrefix(original))).toBe(original); + }); +}); diff --git a/app/src/utils/routeUtils.ts b/app/src/utils/routeUtils.ts index 56f616521..796d9efc2 100644 --- a/app/src/utils/routeUtils.ts +++ b/app/src/utils/routeUtils.ts @@ -5,6 +5,7 @@ // Strips neighbourhood URL prefix to get clean community ID export function stripNeighbourhoodPrefix(neighbourhoodUrl: string): string { + if (!neighbourhoodUrl) return ''; const prefix = 'neighbourhood://'; const privatePrefix = 'private://'; if (neighbourhoodUrl.startsWith(prefix)) return neighbourhoodUrl.slice(prefix.length); @@ -20,13 +21,20 @@ export function restoreNeighbourhoodPrefix(communityId: string): string { return `neighbourhood://${communityId}`; } -// Strips literal:string: prefix from channel ID +// Strips the legacy literal:string: prefix from a channel ID for use in route params. +// New ad4m://obj/ IDs pass through unchanged — they round-trip via restoreChannelPrefix +// which detects the :// scheme and returns them as-is. export function stripChannelPrefix(channelId: string): string { + if (!channelId) return ''; if (channelId.startsWith('literal:string:')) return channelId.slice('literal:string:'.length); return channelId; } -// Restores literal:string: prefix to channel ID +// Restores the instance-ID prefix from a clean route param. +// IDs that already carry a scheme (ad4m://, literal:, neighbourhood://) pass through unchanged. +// Bare IDs get the legacy literal:string: prefix for backwards compatibility. export function restoreChannelPrefix(channelId: string): string { + if (!channelId) return ''; + if (channelId.includes('://')) return channelId; return `literal:string:${channelId}`; } diff --git a/app/src/views/main/community/channel/Header.vue b/app/src/views/main/community/channel/Header.vue index f8cbd0e63..7ab9c270a 100644 --- a/app/src/views/main/community/channel/Header.vue +++ b/app/src/views/main/community/channel/Header.vue @@ -86,8 +86,9 @@ import { useAppStore, useModalStore, useUiStore, useWebrtcStore } from '@/stores import { stripChannelPrefix } from '@/utils/routeUtils'; import { App, Channel, ChannelSummary } from '@coasys/flux-api'; import { useLiveQuery } from '@coasys/ad4m-vue-hooks'; +import { fluxDebug, fluxDebugWarn } from '@coasys/flux-utils'; import { storeToRefs } from 'pinia'; -import { computed, onActivated, onMounted } from 'vue'; +import { computed, onActivated, onMounted, watch } from 'vue'; import { useRouter } from 'vue-router'; defineOptions({ name: 'Header' }); @@ -124,6 +125,31 @@ const { data: views } = useLiveQuery(App, perspective, { parent: { model: Channel, id: channel.value?.id ?? '' }, }); +// Debug: trace every state transition on the channel-view (plugin tabs) query. +// Symptom we're chasing: on the AD4M typed-RDF-literals branch, the plugin +// tabs disappear silently — the header renders no