diff --git a/api/controllers/openapi/_models.py b/api/controllers/openapi/_models.py index a0eb85903d6..7d5362035cf 100644 --- a/api/controllers/openapi/_models.py +++ b/api/controllers/openapi/_models.py @@ -204,7 +204,6 @@ class WorkspaceSummaryResponse(BaseModel): name: str role: str status: str - current: bool class WorkspaceListResponse(PaginationEnvelope[WorkspaceSummaryResponse]): @@ -222,7 +221,6 @@ class WorkspaceDetailResponse(BaseModel): name: str role: str status: str - current: bool created_at: str | None = None diff --git a/api/controllers/openapi/workspaces.py b/api/controllers/openapi/workspaces.py index 61ad96487fb..24b30c09128 100644 --- a/api/controllers/openapi/workspaces.py +++ b/api/controllers/openapi/workspaces.py @@ -57,7 +57,6 @@ from services.errors.workspace import ( InvalidWorkspaceMemberRoleError, MemberNotInTenantError, RoleAlreadyAssignedError, - WorkspaceNotLinkedError, ) from services.workspace.contracts import WorkspaceInvitation, WorkspaceMemberRecord, WorkspaceSnapshot @@ -87,9 +86,7 @@ class WorkspacesApi(Resource): def get(self, ctx: Context, *, query: WorkspaceListQuery): if isinstance(ctx.subject, ResourceAccessSubject): tenant = ctx.workspace - item = WorkspaceSummaryResponse( - id=str(tenant.id), name=tenant.name, role="", status=tenant.status, current=True - ) + item = WorkspaceSummaryResponse(id=str(tenant.id), name=tenant.name, role="", status=tenant.status) return WorkspaceListResponse.page_of([item], query=query) rows = application_services().workspaces.management.list_memberships(str(ctx.subject.account_id)) return WorkspaceListResponse.page_of([_workspace_summary(row) for row in rows], query=query) @@ -116,33 +113,6 @@ class WorkspaceByIdApi(Resource): return _workspace_detail(row) -@openapi_ns.route("/workspaces/:switch") -class WorkspaceSwitchApi(Resource): - """Server-side switch — equivalent to the console's POST /workspaces/switch.""" - - @endpoint( - op="switch.workspace", - kind=Kind.OBJECT, - summary="Server-side current workspace switch (shared with the web console)", - examples=(Example(title="Make a workspace current on the server", input={"workspace_id": ""}),), - internal=True, - requirements=( - CheckSubject(allowed=(AccountSubject,)), - CheckScope(Scope.WORKSPACE_READ), - CheckWorkspaceMember(), - ), - returns=(200, WorkspaceDetailResponse, "Workspace detail"), - ) - def post(self, ctx: Context, workspace_id: str): - try: - workspace = application_services().workspaces.management.switch_membership( - str(ctx.subject.account_id), workspace_id - ) - except WorkspaceNotLinkedError: - raise NotFound("workspace not found") from None - return _workspace_detail(workspace) - - @openapi_ns.route("/workspaces//members") class WorkspaceMembersApi(Resource): """List + invite members. @@ -320,7 +290,6 @@ def _workspace_summary(tenant: WorkspaceSnapshot) -> WorkspaceSummaryResponse: name=tenant.name, role=tenant.role or "", status=tenant.status, - current=tenant.current, ) @@ -330,6 +299,5 @@ def _workspace_detail(tenant: WorkspaceSnapshot) -> WorkspaceDetailResponse: name=tenant.name, role=tenant.role or "", status=tenant.status, - current=tenant.current, created_at=tenant.created_at.isoformat() if tenant.created_at else None, ) diff --git a/api/openapi/markdown/openapi-openapi.md b/api/openapi/markdown/openapi-openapi.md index 55986bbe15d..b05a1b31cd9 100644 --- a/api/openapi/markdown/openapi-openapi.md +++ b/api/openapi/markdown/openapi-openapi.md @@ -1823,20 +1823,6 @@ Upload a file to use as an input variable when running the app | 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| | default | Error | **application/json**: [ErrorBody](#errorbody)
| -### [POST] /workspaces/{workspace_id}:switch -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| workspace_id | path | | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Workspace detail | **application/json**: [WorkspaceDetailResponse](#workspacedetailresponse)
| -| default | Error | **application/json**: [ErrorBody](#errorbody)
| - --- ### Schemas @@ -3605,7 +3591,6 @@ Page of published versions, newest first; there is no total, `hints` carries the | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | created_at | string | | No | -| current | boolean | | Yes | | id | string | | Yes | | name | string | | Yes | | role | string | | Yes | @@ -3643,7 +3628,6 @@ Strict (extra='forbid'). | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| current | boolean | | Yes | | id | string | | Yes | | name | string | | Yes | | role | string | | Yes | diff --git a/api/services/workspace/service.py b/api/services/workspace/service.py index fe4dd5a87af..80ac05b24a8 100644 --- a/api/services/workspace/service.py +++ b/api/services/workspace/service.py @@ -78,9 +78,6 @@ class WorkspaceService: def find_membership(self, account_id: str, workspace_id: str) -> WorkspaceSnapshot | None: return self._workspaces.find_membership(account_id, workspace_id) - def switch_membership(self, account_id: str, workspace_id: str) -> WorkspaceSnapshot: - return self._workspaces.switch(account_id=account_id, workspace_id=workspace_id) - def iter_member_account_id_batches(self, workspace_id: str, batch_size: int) -> Iterator[tuple[str, ...]]: if batch_size <= 0: raise ValueError("batch_size must be positive") diff --git a/api/tests/test_containers_integration_tests/controllers/openapi/test_workspaces.py b/api/tests/test_containers_integration_tests/controllers/openapi/test_workspaces.py index b082c4197d4..bde2c3da93c 100644 --- a/api/tests/test_containers_integration_tests/controllers/openapi/test_workspaces.py +++ b/api/tests/test_containers_integration_tests/controllers/openapi/test_workspaces.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session from werkzeug.exceptions import NotFound from controllers.openapi._models import WorkspaceListQuery -from controllers.openapi.workspaces import WorkspaceByIdApi, WorkspacesApi, WorkspaceSwitchApi +from controllers.openapi.workspaces import WorkspaceByIdApi, WorkspacesApi from models import Account from models.account import TenantAccountRole from tests.test_containers_integration_tests.controllers.openapi.conftest import add_tenant_for_account, context_for @@ -33,9 +33,6 @@ class TestWorkspacesList: only = result.data[0] assert only.role == TenantAccountRole.OWNER.value assert only.status == "normal" - # Newly-created owner membership is not yet "current"; switching flips it - # (see TestWorkspaceSwitch). - assert only.current is False def test_lists_all_joined_workspaces( self, app: Flask, db_session_with_containers: Session, make_account: Callable[..., Account] @@ -72,7 +69,6 @@ class TestWorkspaceDetail: assert detail.id == tenant.id assert detail.role == TenantAccountRole.OWNER.value - assert detail.current is False assert detail.created_at is not None def test_non_member_detail_is_404_not_403( @@ -97,56 +93,3 @@ class TestWorkspaceDetail: ), someone_elses_ws.id, ) - - -class TestWorkspaceSwitch: - def test_switch_sets_current_and_persists( - self, app: Flask, db_session_with_containers: Session, make_account: Callable[..., Account] - ) -> None: - account = make_account() - owner_tenant = account.current_tenant - assert owner_tenant is not None - target = add_tenant_for_account( - account, session=db_session_with_containers, role="normal", name="Switch Target" - ) - - api = WorkspaceSwitchApi() - with app.test_request_context(f"/openapi/v1/workspaces/{target.id}/switch", method="POST"): - detail = api.post.__handler__( - api, - context_for(account, session=db_session_with_containers, view_args={"workspace_id": target.id}), - target.id, - ) - - # Response reflects the post-switch state. - assert detail.id == target.id - assert detail.current is True - - # And the switch persisted: the previously-current owner workspace is no - # longer current (verified through the real read path). - listing_api = WorkspacesApi() - with app.test_request_context("/openapi/v1/workspaces"): - listing = listing_api.get.__handler__( - listing_api, context_for(account, session=db_session_with_containers), query=WorkspaceListQuery() - ) - by_id = {w.id: w for w in listing.data} - assert by_id[target.id].current is True - assert by_id[owner_tenant.id].current is False - - def test_switch_to_non_member_workspace_is_404( - self, app: Flask, db_session_with_containers: Session, make_account: Callable[..., Account] - ) -> None: - account = make_account() - outsider_ws = make_account().current_tenant - assert outsider_ws is not None - - api = WorkspaceSwitchApi() - with app.test_request_context(f"/openapi/v1/workspaces/{outsider_ws.id}:switch", method="POST"): - with pytest.raises(NotFound): - api.post.__handler__( - api, - context_for( - account, session=db_session_with_containers, view_args={"workspace_id": outsider_ws.id} - ), - outsider_ws.id, - ) diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_resource_access.py b/api/tests/unit_tests/controllers/openapi/auth/test_resource_access.py index 5a9e1aea76b..314852da2e3 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_resource_access.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_resource_access.py @@ -233,19 +233,17 @@ def test_workspace_list_returns_only_token_tenant(resource_fixture: ResourceFixt "name": "Token workspace", "role": "", "status": "normal", - "current": True, } -@pytest.mark.parametrize("endpoint", ["detail", "members", "switch"]) +@pytest.mark.parametrize("endpoint", ["detail", "members"]) def test_other_workspace_endpoints_reject_resource_token(resource_fixture: ResourceFixture, endpoint: str) -> None: - from controllers.openapi.workspaces import WorkspaceByIdApi, WorkspaceMembersApi, WorkspaceSwitchApi + from controllers.openapi.workspaces import WorkspaceByIdApi, WorkspaceMembersApi flask_app, _, _, tenant_id, _ = resource_fixture methods = { "detail": WorkspaceByIdApi().get, "members": WorkspaceMembersApi().get, - "switch": WorkspaceSwitchApi().post, } with flask_app.test_request_context( "/workspaces", headers={"Authorization": "Bearer sk-test", CATALOG_HEADER: catalog_for(flask_app)[1]} diff --git a/api/tests/unit_tests/controllers/openapi/test_auth_matrix.py b/api/tests/unit_tests/controllers/openapi/test_auth_matrix.py index 0f293aa68e0..a43cd441f64 100644 --- a/api/tests/unit_tests/controllers/openapi/test_auth_matrix.py +++ b/api/tests/unit_tests/controllers/openapi/test_auth_matrix.py @@ -269,7 +269,6 @@ ROUTES: tuple[Route, ...] = ( Route("apps.list", "GET", "/apps", frozenset({Trait.ACCOUNT_PRIMARY}), query="workspace_id={workspace_id}"), Route("workspaces.list", "GET", "/workspaces", frozenset({Trait.ACCOUNT_PRIMARY})), Route("workspaces.describe", "GET", "/workspaces/{workspace_id}", frozenset({Trait.ACCOUNT_PRIMARY})), - Route("workspaces.switch", "POST", "/workspaces/{workspace_id}:switch", frozenset({Trait.ACCOUNT_PRIMARY})), Route("workspaces.members.list", "GET", "/workspaces/{workspace_id}/members", frozenset({Trait.ACCOUNT_PRIMARY})), Route( "workspaces.members.invite", @@ -2070,7 +2069,6 @@ EXPECTED_RESPONSE_CODES: dict[tuple[str, str], frozenset[str]] = { ("post", "/workspaces/{workspace_id}/members"): frozenset({"201", "422", "default"}), ("delete", "/workspaces/{workspace_id}/members/{member_id}"): frozenset({"200", "default"}), ("patch", "/workspaces/{workspace_id}/members/{member_id}"): frozenset({"200", "422", "default"}), - ("post", "/workspaces/{workspace_id}:switch"): frozenset({"200", "default"}), ("get", "/apps/{app_id}/webapp/workflow"): frozenset({"200", "default"}), ("patch", "/apps/{app_id}/webapp/workflow"): frozenset({"200", "422", "default"}), ("post", "/apps/{app_id}/webapp:reset"): frozenset({"200", "default"}), diff --git a/api/tests/unit_tests/controllers/openapi/test_catalog.py b/api/tests/unit_tests/controllers/openapi/test_catalog.py index 35d093d5392..fa2ed38db13 100644 --- a/api/tests/unit_tests/controllers/openapi/test_catalog.py +++ b/api/tests/unit_tests/controllers/openapi/test_catalog.py @@ -117,7 +117,6 @@ def test_run_entries_carry_path_bind_kind_and_flags(ops: dict[str, CatalogOp]) - assert ops["upload.console_app.file"]["bind"]["file"] == "file" assert ops["get.console_app"]["bind"]["page"] == "query" assert ops["get.run.event"]["bind"]["continue_on_pause"] == "query" - assert ops["switch.workspace"]["internal"] is True def test_input_schemas_are_flat_shallow_and_described(ops: dict[str, CatalogOp]) -> None: diff --git a/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py b/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py index c0e1c2f757f..e4a58e2b1c3 100644 --- a/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py +++ b/api/tests/unit_tests/controllers/openapi/test_workspaces_members.py @@ -45,7 +45,7 @@ from controllers.openapi._models import MemberInvitePayload, MemberListQuery, Me from controllers.openapi.auth.context import Context from controllers.openapi.auth.loaders import load_caller, load_workspace from controllers.openapi.auth.subjects import AccountSubject -from controllers.openapi.workspaces import WorkspaceMemberApi, WorkspaceMembersApi, WorkspaceSwitchApi +from controllers.openapi.workspaces import WorkspaceMemberApi, WorkspaceMembersApi from libs.oauth_bearer import AuthContext from models import Account, Tenant, TenantAccountJoin from models.account import TenantAccountRole @@ -59,7 +59,6 @@ from services.errors.workspace import ( RoleAlreadyAssignedError, WorkspaceInvitationQuotaError, WorkspaceMemberLicenseQuotaError, - WorkspaceNotLinkedError, ) from tests.unit_tests.account_domain import AccountDomain from tests.unit_tests.controllers.openapi.conftest import AdmittedWorld @@ -221,64 +220,6 @@ def test_invalid_request_is_422_on_the_wire( _assert_validation_422(resp) -# --------------------------------------------------------------------------- -# Switch endpoint behavior -# --------------------------------------------------------------------------- - - -def test_switch_returns_workspace_detail_with_current_true(database_session: Session): - """Happy path: switch service is called, then the workspace+membership - row is re-queried so the returned `current` reflects post-commit state. - """ - ws_id = str(uuid.uuid4()) - acct_id = uuid.uuid4() - api = WorkspaceSwitchApi() - - _persist_workspace( - database_session, - ws_id, - [(str(acct_id), "caller@example.com", TenantAccountRole.OWNER, False)], - ) - - result = api.post.__handler__(api, _context(database_session, acct_id, ws_id), workspace_id=ws_id) - - assert result.id == ws_id - assert result.current is True - membership = database_session.scalar( - select(TenantAccountJoin).where( - TenantAccountJoin.tenant_id == ws_id, - TenantAccountJoin.account_id == str(acct_id), - ) - ) - assert membership is not None - assert membership.current is True - - -def test_switch_404s_when_service_raises_account_not_link_tenant( - monkeypatch: pytest.MonkeyPatch, database_session: Session -): - """If switch_tenant raises (e.g. Tenant.status != NORMAL), the body - surfaces as NotFound, not 500.""" - ws_id = str(uuid.uuid4()) - acct_id = uuid.uuid4() - api = WorkspaceSwitchApi() - - _persist_workspace( - database_session, - ws_id, - [(str(acct_id), "caller@example.com", TenantAccountRole.OWNER, False)], - ) - - monkeypatch.setattr( - workspaces_module.application_services().workspaces.management, - "switch_membership", - Mock(side_effect=WorkspaceNotLinkedError("…")), - ) - - with pytest.raises(NotFound): - api.post.__handler__(api, _context(database_session, acct_id, ws_id), workspace_id=ws_id) - - # --------------------------------------------------------------------------- # Members list # --------------------------------------------------------------------------- diff --git a/packages/contracts/generated/api/openapi/orpc.gen.ts b/packages/contracts/generated/api/openapi/orpc.gen.ts index 14d3691031a..78c7f31e4b1 100644 --- a/packages/contracts/generated/api/openapi/orpc.gen.ts +++ b/packages/contracts/generated/api/openapi/orpc.gen.ts @@ -246,8 +246,6 @@ import { zPostWorkspacesByWorkspaceIdPluginsInstallBody, zPostWorkspacesByWorkspaceIdPluginsInstallPath, zPostWorkspacesByWorkspaceIdPluginsInstallResponse, - zPostWorkspacesByWorkspaceIdSwitchPath, - zPostWorkspacesByWorkspaceIdSwitchResponse, zPostWorkspacesByWorkspaceIdToolProvidersByProviderCredentialsBody, zPostWorkspacesByWorkspaceIdToolProvidersByProviderCredentialsPath, zPostWorkspacesByWorkspaceIdToolProvidersByProviderCredentialsResponse, @@ -2137,21 +2135,6 @@ export const tools = { get: get47, } -export const post30 = oc - .route({ - inputStructure: 'detailed', - method: 'POST', - operationId: 'postWorkspacesByWorkspaceIdSwitch', - path: '/workspaces/{workspace_id}:switch', - tags: ['openapi'], - }) - .input(z.object({ params: zPostWorkspacesByWorkspaceIdSwitchPath })) - .output(zPostWorkspacesByWorkspaceIdSwitchResponse) - -export const switch_ = { - post: post30, -} - export const get48 = oc .route({ inputStructure: 'detailed', @@ -2175,7 +2158,6 @@ export const byWorkspaceId = { plugins: plugins2, toolProviders, tools, - switch: switch_, } export const get49 = oc diff --git a/packages/contracts/generated/api/openapi/types.gen.ts b/packages/contracts/generated/api/openapi/types.gen.ts index 1e30cc53bed..6499d259e50 100644 --- a/packages/contracts/generated/api/openapi/types.gen.ts +++ b/packages/contracts/generated/api/openapi/types.gen.ts @@ -1463,7 +1463,6 @@ export type WorkflowWebAppPatch = { export type WorkspaceDetailResponse = { created_at?: string | null - current: boolean id: string name: string role: string @@ -1491,7 +1490,6 @@ export type WorkspacePayload = { } export type WorkspaceSummaryResponse = { - current: boolean id: string name: string role: string @@ -4002,26 +4000,3 @@ export type GetWorkspacesByWorkspaceIdToolsResponses = { export type GetWorkspacesByWorkspaceIdToolsResponse = GetWorkspacesByWorkspaceIdToolsResponses[keyof GetWorkspacesByWorkspaceIdToolsResponses] - -export type PostWorkspacesByWorkspaceIdSwitchData = { - body?: never - path: { - workspace_id: string - } - query?: never - url: '/workspaces/{workspace_id}:switch' -} - -export type PostWorkspacesByWorkspaceIdSwitchErrors = { - default: ErrorBody -} - -export type PostWorkspacesByWorkspaceIdSwitchError = - PostWorkspacesByWorkspaceIdSwitchErrors[keyof PostWorkspacesByWorkspaceIdSwitchErrors] - -export type PostWorkspacesByWorkspaceIdSwitchResponses = { - 200: WorkspaceDetailResponse -} - -export type PostWorkspacesByWorkspaceIdSwitchResponse = - PostWorkspacesByWorkspaceIdSwitchResponses[keyof PostWorkspacesByWorkspaceIdSwitchResponses] diff --git a/packages/contracts/generated/api/openapi/zod.gen.ts b/packages/contracts/generated/api/openapi/zod.gen.ts index d3b4e16e69d..b3738f7103b 100644 --- a/packages/contracts/generated/api/openapi/zod.gen.ts +++ b/packages/contracts/generated/api/openapi/zod.gen.ts @@ -2056,7 +2056,6 @@ export const zWorkflowWebAppPatch = z.object({ */ export const zWorkspaceDetailResponse = z.object({ created_at: z.string().nullish(), - current: z.boolean(), id: z.string(), name: z.string(), role: z.string(), @@ -2113,7 +2112,6 @@ export const zDeviceTokenResponse = z.object({ * WorkspaceSummaryResponse */ export const zWorkspaceSummaryResponse = z.object({ - current: z.boolean(), id: z.string(), name: z.string(), role: z.string(), @@ -3247,12 +3245,3 @@ export const zGetWorkspacesByWorkspaceIdToolsQuery = z.object({ * Tools */ export const zGetWorkspacesByWorkspaceIdToolsResponse = zToolListResponse - -export const zPostWorkspacesByWorkspaceIdSwitchPath = z.object({ - workspace_id: z.string(), -}) - -/** - * Workspace detail - */ -export const zPostWorkspacesByWorkspaceIdSwitchResponse = zWorkspaceDetailResponse