fix(team): guard cross-team targets by context
This commit is contained in:
parent
7514bf05eb
commit
636d121f5f
2 changed files with 67 additions and 0 deletions
|
|
@ -3133,11 +3133,18 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
},
|
||||
|
||||
fetchCrossTeamTargets: async () => {
|
||||
const requestScope = captureContextRequestScope(get);
|
||||
set({ crossTeamTargetsLoading: true });
|
||||
try {
|
||||
const targets = await api.crossTeam.listTargets();
|
||||
if (!isContextRequestScopeCurrent(get, requestScope)) {
|
||||
return;
|
||||
}
|
||||
set({ crossTeamTargets: targets, crossTeamTargetsLoading: false });
|
||||
} catch (error) {
|
||||
if (!isContextRequestScopeCurrent(get, requestScope)) {
|
||||
return;
|
||||
}
|
||||
logger.error('fetchCrossTeamTargets failed', error);
|
||||
set({ crossTeamTargets: [], crossTeamTargetsLoading: false });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ const apiMock = vi.hoisted(() => ({
|
|||
review: {
|
||||
invalidateTaskChangeSummaries: vi.fn(async () => undefined),
|
||||
},
|
||||
crossTeam: {
|
||||
listTargets: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
interface TeamSummaryLike {
|
||||
|
|
@ -63,6 +66,11 @@ interface TeamSnapshotLike {
|
|||
processes: [];
|
||||
}
|
||||
|
||||
interface CrossTeamTargetLike {
|
||||
teamName: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const teamSnapshot = (
|
||||
teamName: string,
|
||||
projectPath: string,
|
||||
|
|
@ -176,6 +184,7 @@ describe('team slice context races', () => {
|
|||
apiMock.teams.getTaskChangePresence.mockReset();
|
||||
apiMock.teams.showMessageNotification.mockClear();
|
||||
apiMock.review.invalidateTaskChangeSummaries.mockClear();
|
||||
apiMock.crossTeam.listTargets.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -323,6 +332,57 @@ describe('team slice context races', () => {
|
|||
expect(store.getState().globalTasksLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores cross-team targets loaded for a previous context', async () => {
|
||||
const store = createSliceStore();
|
||||
const localTargets = deferred<CrossTeamTargetLike[]>();
|
||||
apiMock.crossTeam.listTargets.mockReturnValueOnce(localTargets.promise);
|
||||
|
||||
const fetchPromise = store.getState().fetchCrossTeamTargets();
|
||||
expect(store.getState().crossTeamTargetsLoading).toBe(true);
|
||||
|
||||
store.setState({
|
||||
activeContextId: 'ssh-dev',
|
||||
crossTeamTargets: [],
|
||||
crossTeamTargetsLoading: false,
|
||||
});
|
||||
localTargets.resolve([
|
||||
{
|
||||
teamName: 'local-target',
|
||||
displayName: 'Local Target',
|
||||
},
|
||||
]);
|
||||
await fetchPromise;
|
||||
|
||||
expect(store.getState().crossTeamTargets).toEqual([]);
|
||||
expect(store.getState().crossTeamTargetsLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores cross-team targets loaded before a context epoch reset with the same context id', async () => {
|
||||
const store = createSliceStore();
|
||||
const localTargets = deferred<CrossTeamTargetLike[]>();
|
||||
apiMock.crossTeam.listTargets.mockReturnValueOnce(localTargets.promise);
|
||||
|
||||
const fetchPromise = store.getState().fetchCrossTeamTargets();
|
||||
expect(store.getState().crossTeamTargetsLoading).toBe(true);
|
||||
|
||||
invalidateContextScopedRequestEpoch();
|
||||
store.setState({
|
||||
activeContextId: 'local',
|
||||
crossTeamTargets: [],
|
||||
crossTeamTargetsLoading: false,
|
||||
});
|
||||
localTargets.resolve([
|
||||
{
|
||||
teamName: 'old-local-target',
|
||||
displayName: 'Old Local Target',
|
||||
},
|
||||
]);
|
||||
await fetchPromise;
|
||||
|
||||
expect(store.getState().crossTeamTargets).toEqual([]);
|
||||
expect(store.getState().crossTeamTargetsLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores selected team data loaded for a previous context', async () => {
|
||||
const store = createSliceStore();
|
||||
const localData = deferred<TeamSnapshotLike>();
|
||||
|
|
|
|||
Loading…
Reference in a new issue