mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* fix(chat): preserve Cloud retry across workspace role projections * fix(web): preserve Cloud retry until project authority settles --------- Co-authored-by: lefarcen <935902669@qq.com>
256 lines
9.1 KiB
TypeScript
256 lines
9.1 KiB
TypeScript
import { workspaceIdentityCacheKey } from '../../src/collab/workspace-identity';
|
|
import { workspaceContextFixture } from '../helpers/workspace-context';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
AMR_AUTH_RETRY_CONTINUATION_TTL_MS,
|
|
amrAuthRetryMatchesRouteContext,
|
|
canConsumeAmrAuthRetryContinuation,
|
|
routeStillMatchesAmrAuthRetryContinuation,
|
|
type AmrAuthRetryContinuation,
|
|
} from '../../src/runtime/amr-auth-retry-continuation';
|
|
|
|
const pending: AmrAuthRetryContinuation = {
|
|
projectId: 'project-a',
|
|
conversationId: 'conversation-a',
|
|
assistantId: 'assistant-a',
|
|
workspaceIdentityKey: 'workspace-a:personal:member-a:owner:active:active:true:true',
|
|
originMountId: 'mount-origin',
|
|
accountIdAtArm: null,
|
|
createdAtMs: 1_000,
|
|
};
|
|
|
|
const matchingCandidate = {
|
|
projectId: pending.projectId,
|
|
conversationId: pending.conversationId,
|
|
assistantId: pending.assistantId,
|
|
workspaceIdentityKey: pending.workspaceIdentityKey,
|
|
mountId: 'mount-fresh',
|
|
loggedInAccountId: 'account-a',
|
|
nowMs: 1_001,
|
|
originMountObservedSignedOut: false,
|
|
personalAdoptionWitness: null,
|
|
};
|
|
|
|
const personalAdoptionWitness = {
|
|
workspaceIdentityKey:
|
|
'personal-a:personal:member-personal-a:owner:active:active:true:true',
|
|
workspaceId: 'personal-a',
|
|
workspaceMemberId: 'member-personal-a',
|
|
workspaceType: 'personal' as const,
|
|
memberStatus: 'active' as const,
|
|
};
|
|
|
|
describe('AMR auth retry continuation', () => {
|
|
it('cannot be consumed by the ProjectView mount that armed it', () => {
|
|
expect(canConsumeAmrAuthRetryContinuation(pending, {
|
|
...matchingCandidate,
|
|
mountId: pending.originMountId,
|
|
})).toBe(false);
|
|
});
|
|
|
|
it('is consumable once by a fresh mount with the exact failed turn and authority', () => {
|
|
expect(canConsumeAmrAuthRetryContinuation(pending, matchingCandidate)).toBe(true);
|
|
});
|
|
|
|
it('allows a real none -> Personal adoption on the origin mount only after signed-out was observed', () => {
|
|
const unbound = { ...pending, workspaceIdentityKey: 'none' };
|
|
expect(canConsumeAmrAuthRetryContinuation(unbound, {
|
|
...matchingCandidate,
|
|
workspaceIdentityKey: personalAdoptionWitness.workspaceIdentityKey,
|
|
mountId: pending.originMountId,
|
|
originMountObservedSignedOut: false,
|
|
personalAdoptionWitness,
|
|
})).toBe(false);
|
|
expect(canConsumeAmrAuthRetryContinuation(unbound, {
|
|
...matchingCandidate,
|
|
workspaceIdentityKey: personalAdoptionWitness.workspaceIdentityKey,
|
|
mountId: pending.originMountId,
|
|
originMountObservedSignedOut: true,
|
|
personalAdoptionWitness,
|
|
})).toBe(true);
|
|
});
|
|
|
|
it('rejects none -> none and none -> Team on the origin mount', () => {
|
|
const unbound = { ...pending, workspaceIdentityKey: 'none' };
|
|
expect(canConsumeAmrAuthRetryContinuation(unbound, {
|
|
...matchingCandidate,
|
|
workspaceIdentityKey: 'none',
|
|
mountId: pending.originMountId,
|
|
originMountObservedSignedOut: true,
|
|
personalAdoptionWitness: null,
|
|
})).toBe(false);
|
|
expect(canConsumeAmrAuthRetryContinuation(unbound, {
|
|
...matchingCandidate,
|
|
workspaceIdentityKey: 'team-a:team:member-team-a:owner:active:active:true:true',
|
|
mountId: pending.originMountId,
|
|
originMountObservedSignedOut: true,
|
|
personalAdoptionWitness: {
|
|
...personalAdoptionWitness,
|
|
workspaceIdentityKey: 'team-a:team:member-team-a:owner:active:active:true:true',
|
|
workspaceType: 'team',
|
|
} as unknown as typeof personalAdoptionWitness,
|
|
})).toBe(false);
|
|
});
|
|
|
|
it('rejects an unbound continuation on a fresh unbound mount', () => {
|
|
const unbound = { ...pending, workspaceIdentityKey: 'none' };
|
|
expect(canConsumeAmrAuthRetryContinuation(unbound, {
|
|
...matchingCandidate,
|
|
workspaceIdentityKey: 'none',
|
|
mountId: 'mount-fresh',
|
|
})).toBe(false);
|
|
});
|
|
|
|
it('requires the same-mount Personal adoption to begin signed out', () => {
|
|
expect(canConsumeAmrAuthRetryContinuation(
|
|
{
|
|
...pending,
|
|
workspaceIdentityKey: 'none',
|
|
accountIdAtArm: 'account-a',
|
|
},
|
|
{
|
|
...matchingCandidate,
|
|
workspaceIdentityKey: personalAdoptionWitness.workspaceIdentityKey,
|
|
mountId: pending.originMountId,
|
|
originMountObservedSignedOut: true,
|
|
personalAdoptionWitness,
|
|
},
|
|
)).toBe(false);
|
|
});
|
|
|
|
it('keeps bound Workspace continuations fresh-mount-only after signed-out was observed', () => {
|
|
expect(canConsumeAmrAuthRetryContinuation(pending, {
|
|
...matchingCandidate,
|
|
mountId: pending.originMountId,
|
|
originMountObservedSignedOut: true,
|
|
})).toBe(false);
|
|
});
|
|
|
|
it('expires instead of retrying a stale failed turn', () => {
|
|
expect(canConsumeAmrAuthRetryContinuation(pending, {
|
|
...matchingCandidate,
|
|
nowMs: pending.createdAtMs + AMR_AUTH_RETRY_CONTINUATION_TTL_MS + 1,
|
|
})).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
['projectId', 'project-b'],
|
|
['conversationId', 'conversation-b'],
|
|
['assistantId', 'assistant-b'],
|
|
['workspaceIdentityKey', 'workspace-b:team:member-b:admin:active:active:true:true'],
|
|
] as const)('rejects a %s mismatch', (field, value) => {
|
|
expect(canConsumeAmrAuthRetryContinuation(pending, {
|
|
...matchingCandidate,
|
|
[field]: value,
|
|
})).toBe(false);
|
|
});
|
|
|
|
it('rejects signed-out candidates and account swaps when an account was known at arm time', () => {
|
|
expect(canConsumeAmrAuthRetryContinuation(pending, {
|
|
...matchingCandidate,
|
|
loggedInAccountId: null,
|
|
})).toBe(false);
|
|
expect(canConsumeAmrAuthRetryContinuation(
|
|
{ ...pending, accountIdAtArm: 'account-a' },
|
|
{ ...matchingCandidate, loggedInAccountId: 'account-b' },
|
|
)).toBe(false);
|
|
});
|
|
|
|
it('keeps only the same project route and rejects explicit conversation changes', () => {
|
|
expect(routeStillMatchesAmrAuthRetryContinuation(pending, {
|
|
kind: 'project',
|
|
projectId: pending.projectId,
|
|
conversationId: null,
|
|
})).toBe(true);
|
|
expect(routeStillMatchesAmrAuthRetryContinuation(pending, {
|
|
kind: 'project',
|
|
projectId: pending.projectId,
|
|
conversationId: pending.conversationId,
|
|
})).toBe(true);
|
|
expect(routeStillMatchesAmrAuthRetryContinuation(pending, {
|
|
kind: 'project',
|
|
projectId: pending.projectId,
|
|
conversationId: 'conversation-b',
|
|
})).toBe(false);
|
|
expect(routeStillMatchesAmrAuthRetryContinuation(pending, {
|
|
kind: 'home',
|
|
})).toBe(false);
|
|
});
|
|
});
|
|
|
|
|
|
describe('AMR retry across project-scope and route-directory projections', () => {
|
|
const scope = workspaceContextFixture({
|
|
workspaceId: 'workspace-a',
|
|
workspaceType: 'personal',
|
|
workspaceMemberId: 'member-a',
|
|
role: 'member',
|
|
});
|
|
const owner = { ...scope, role: 'owner' as const };
|
|
const armed: AmrAuthRetryContinuation = {
|
|
...pending,
|
|
workspaceIdentityKey: workspaceIdentityCacheKey(scope),
|
|
workspacePrincipal: {
|
|
workspaceId: scope.workspaceId,
|
|
workspaceType: scope.workspaceType,
|
|
workspaceMemberId: scope.workspaceMemberId,
|
|
},
|
|
};
|
|
|
|
it('preserves the same active principal across member and owner projections', () => {
|
|
expect(workspaceIdentityCacheKey(scope)).not.toBe(workspaceIdentityCacheKey(owner));
|
|
expect(amrAuthRetryMatchesRouteContext(armed, owner)).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
{ workspaceId: 'workspace-b' },
|
|
{ workspaceMemberId: 'member-b' },
|
|
{ workspaceType: 'team' as const },
|
|
])('rejects a different principal: %j', (changedIdentity) => {
|
|
expect(amrAuthRetryMatchesRouteContext(armed, { ...owner, ...changedIdentity })).toBe(false);
|
|
});
|
|
|
|
it('rejects a removed member even if the permission projection has not caught up', () => {
|
|
expect(amrAuthRetryMatchesRouteContext(armed, { ...owner, memberStatus: 'removed' })).toBe(false);
|
|
});
|
|
|
|
it.each(['locked', 'deleted'] as const)('rejects a %s workspace', (lifecycleState) => {
|
|
expect(amrAuthRetryMatchesRouteContext(armed, { ...owner, lifecycleState })).toBe(false);
|
|
});
|
|
|
|
it('rejects revoked write permission for an otherwise unchanged principal', () => {
|
|
expect(amrAuthRetryMatchesRouteContext(armed, {
|
|
...owner,
|
|
permissions: { ...owner.permissions, canWriteSyncedFiles: false },
|
|
})).toBe(false);
|
|
});
|
|
|
|
it('does not invent a principal when the structured witness is missing', () => {
|
|
expect(amrAuthRetryMatchesRouteContext({ ...armed, workspacePrincipal: null }, owner)).toBe(false);
|
|
expect(amrAuthRetryMatchesRouteContext({
|
|
...armed,
|
|
workspacePrincipal: { ...scope, workspaceMemberId: '' },
|
|
}, owner)).toBe(false);
|
|
});
|
|
|
|
it('keeps old inline continuations on their original exact-key boundary', () => {
|
|
const legacy = { ...armed };
|
|
delete legacy.workspacePrincipal;
|
|
expect(amrAuthRetryMatchesRouteContext(legacy, scope)).toBe(true);
|
|
expect(amrAuthRetryMatchesRouteContext(legacy, owner)).toBe(false);
|
|
});
|
|
|
|
it('still requires the exact project-scope permission snapshot when consuming', () => {
|
|
expect(canConsumeAmrAuthRetryContinuation(armed, {
|
|
...matchingCandidate,
|
|
workspaceIdentityKey: workspaceIdentityCacheKey(scope),
|
|
})).toBe(true);
|
|
expect(canConsumeAmrAuthRetryContinuation(armed, {
|
|
...matchingCandidate,
|
|
workspaceIdentityKey: workspaceIdentityCacheKey(owner),
|
|
})).toBe(false);
|
|
});
|
|
});
|