mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
fix(scope): align scope filter with guest-mode write hook
getInScopeSpaceIds() used getCurrentUserId() (null for guests), so guest-created rows stamped `_personal:guest` by the write hook became invisible — empty scene, "App hinzufügen" silently no-op'd because activeSceneIdState resolved to null. Switch to getEffectiveUserId() so the read filter always matches what the hook stamps. Four regression tests cover guest-only, signed-in-no-space, non-personal active space, and personal-sentinel- is-active collapsing to a single id. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e0820331b0
commit
36c427d17e
2 changed files with 80 additions and 11 deletions
|
|
@ -7,8 +7,14 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { applyVisibility, isVisibleToCurrentUser } from './visibility';
|
||||
import { personalSpaceSentinel } from './bootstrap';
|
||||
import { assertModuleAllowed, ModuleNotInSpaceError, ScopeNotReadyError } from './scoped-db';
|
||||
import {
|
||||
assertModuleAllowed,
|
||||
getInScopeSpaceIds,
|
||||
ModuleNotInSpaceError,
|
||||
ScopeNotReadyError,
|
||||
} from './scoped-db';
|
||||
import { setActiveSpace } from './active-space.svelte';
|
||||
import { setCurrentUserId } from '../current-user';
|
||||
import * as currentUser from '../current-user';
|
||||
|
||||
describe('personalSpaceSentinel', () => {
|
||||
|
|
@ -21,6 +27,62 @@ describe('personalSpaceSentinel', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getInScopeSpaceIds', () => {
|
||||
// getInScopeSpaceIds reads `getEffectiveUserId()`, which closes over
|
||||
// the module-level `currentUserId` inside current-user.ts. Spying on
|
||||
// the exported `getCurrentUserId` doesn't intercept that closure —
|
||||
// we need the real setter to change the underlying state.
|
||||
beforeEach(() => {
|
||||
setActiveSpace(null);
|
||||
setCurrentUserId(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActiveSpace(null);
|
||||
setCurrentUserId(null);
|
||||
});
|
||||
|
||||
it('returns the guest sentinel when no one is signed in (guest-mode)', () => {
|
||||
// Regression guard: before the fix, this returned [] which
|
||||
// invisibly hid every guest-created row even though the write
|
||||
// path stamped them with `_personal:guest`. Result: empty scene,
|
||||
// "App hinzufügen" silently no-op'd because activeSceneIdState
|
||||
// resolved to null.
|
||||
expect(getInScopeSpaceIds()).toEqual(['_personal:guest']);
|
||||
});
|
||||
|
||||
it("returns the user's sentinel when signed in without active space", () => {
|
||||
setCurrentUserId('user-abc');
|
||||
expect(getInScopeSpaceIds()).toEqual(['_personal:user-abc']);
|
||||
});
|
||||
|
||||
it('returns [active, sentinel] when a non-personal Space is active', () => {
|
||||
setCurrentUserId('user-abc');
|
||||
setActiveSpace({
|
||||
id: 'space-xyz',
|
||||
slug: 'family',
|
||||
name: 'Family',
|
||||
type: 'family',
|
||||
tier: 'public',
|
||||
role: 'owner',
|
||||
});
|
||||
expect(getInScopeSpaceIds().sort()).toEqual(['_personal:user-abc', 'space-xyz'].sort());
|
||||
});
|
||||
|
||||
it('collapses to [active] when active IS the personal sentinel', () => {
|
||||
setCurrentUserId('user-abc');
|
||||
setActiveSpace({
|
||||
id: '_personal:user-abc',
|
||||
slug: 'personal',
|
||||
name: 'Personal',
|
||||
type: 'personal',
|
||||
tier: 'public',
|
||||
role: 'owner',
|
||||
});
|
||||
expect(getInScopeSpaceIds()).toEqual(['_personal:user-abc']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('visibility', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(currentUser, 'getCurrentUserId').mockReturnValue('me');
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import type { Collection, Table } from 'dexie';
|
||||
import { db } from '../database';
|
||||
import { getCurrentUserId } from '../current-user';
|
||||
import { getEffectiveUserId } from '../current-user';
|
||||
import { getActiveSpaceId } from './active-space.svelte';
|
||||
import { personalSpaceSentinel } from './bootstrap';
|
||||
import { isModuleAllowedInSpace, type SpaceModuleId, type SpaceType } from '@mana/shared-types';
|
||||
|
|
@ -43,20 +43,27 @@ export class ModuleNotInSpaceError extends Error {
|
|||
* Return the set of spaceId values a record must match to be considered
|
||||
* "in scope" right now.
|
||||
*
|
||||
* Lenient during boot: if the active space hasn't loaded yet, falls back
|
||||
* to the user's personal sentinel so records stamped by the v28
|
||||
* migration still render. Returns `[]` only when truly unauthenticated
|
||||
* — that yields an empty-filter (matches nothing), which is the safest
|
||||
* thing a wrapper can do pre-login.
|
||||
* Always uses `getEffectiveUserId()` (which returns the GUEST_USER_ID
|
||||
* sentinel `'guest'` when no user is signed in) so the filter we apply
|
||||
* here matches what the creating-hook in `database.ts` stamps on new
|
||||
* rows. The hook always stamps `_personal:${effectiveUserId}` — if the
|
||||
* filter used `getCurrentUserId()` (which is null for guests), guest-
|
||||
* mode data would be written but never readable until sign-in, which
|
||||
* breaks the first-run "try the app without an account" flow.
|
||||
*
|
||||
* Authenticated path returns either `[active]` (Space-loaded) or
|
||||
* `[active, sentinel]` when both differ (covers the bootstrap window
|
||||
* where a row was stamped with the personal sentinel before
|
||||
* `loadActiveSpace` resolved the real organisation id).
|
||||
*/
|
||||
export function getInScopeSpaceIds(): string[] {
|
||||
const active = getActiveSpaceId();
|
||||
const userId = getCurrentUserId();
|
||||
const sentinel = userId ? personalSpaceSentinel(userId) : null;
|
||||
const effectiveUserId = getEffectiveUserId();
|
||||
const sentinel = personalSpaceSentinel(effectiveUserId);
|
||||
if (active) {
|
||||
return sentinel && sentinel !== active ? [active, sentinel] : [active];
|
||||
return sentinel !== active ? [active, sentinel] : [active];
|
||||
}
|
||||
return sentinel ? [sentinel] : [];
|
||||
return [sentinel];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue