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 () => {
|
fetchCrossTeamTargets: async () => {
|
||||||
|
const requestScope = captureContextRequestScope(get);
|
||||||
set({ crossTeamTargetsLoading: true });
|
set({ crossTeamTargetsLoading: true });
|
||||||
try {
|
try {
|
||||||
const targets = await api.crossTeam.listTargets();
|
const targets = await api.crossTeam.listTargets();
|
||||||
|
if (!isContextRequestScopeCurrent(get, requestScope)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
set({ crossTeamTargets: targets, crossTeamTargetsLoading: false });
|
set({ crossTeamTargets: targets, crossTeamTargetsLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (!isContextRequestScopeCurrent(get, requestScope)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
logger.error('fetchCrossTeamTargets failed', error);
|
logger.error('fetchCrossTeamTargets failed', error);
|
||||||
set({ crossTeamTargets: [], crossTeamTargetsLoading: false });
|
set({ crossTeamTargets: [], crossTeamTargetsLoading: false });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ const apiMock = vi.hoisted(() => ({
|
||||||
review: {
|
review: {
|
||||||
invalidateTaskChangeSummaries: vi.fn(async () => undefined),
|
invalidateTaskChangeSummaries: vi.fn(async () => undefined),
|
||||||
},
|
},
|
||||||
|
crossTeam: {
|
||||||
|
listTargets: vi.fn(),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface TeamSummaryLike {
|
interface TeamSummaryLike {
|
||||||
|
|
@ -63,6 +66,11 @@ interface TeamSnapshotLike {
|
||||||
processes: [];
|
processes: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CrossTeamTargetLike {
|
||||||
|
teamName: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
const teamSnapshot = (
|
const teamSnapshot = (
|
||||||
teamName: string,
|
teamName: string,
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
|
|
@ -176,6 +184,7 @@ describe('team slice context races', () => {
|
||||||
apiMock.teams.getTaskChangePresence.mockReset();
|
apiMock.teams.getTaskChangePresence.mockReset();
|
||||||
apiMock.teams.showMessageNotification.mockClear();
|
apiMock.teams.showMessageNotification.mockClear();
|
||||||
apiMock.review.invalidateTaskChangeSummaries.mockClear();
|
apiMock.review.invalidateTaskChangeSummaries.mockClear();
|
||||||
|
apiMock.crossTeam.listTargets.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
@ -323,6 +332,57 @@ describe('team slice context races', () => {
|
||||||
expect(store.getState().globalTasksLoading).toBe(false);
|
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 () => {
|
it('ignores selected team data loaded for a previous context', async () => {
|
||||||
const store = createSliceStore();
|
const store = createSliceStore();
|
||||||
const localData = deferred<TeamSnapshotLike>();
|
const localData = deferred<TeamSnapshotLike>();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue