fix: scope stateful resources and custom operations to workspaces (#12)#16
Open
zach-snell wants to merge 1 commit intomainfrom
Open
fix: scope stateful resources and custom operations to workspaces (#12)#16zach-snell wants to merge 1 commit intomainfrom
zach-snell wants to merge 1 commit intomainfrom
Conversation
Importing a config into a named workspace correctly stamped mocks with the active workspaceId, but stateful resources and custom operations were always registered under the default workspace "". At request time, StateStore.Get(mock.WorkspaceID, name) and Bridge.GetCustomOperation looked in the wrong bucket and the request 404'd. Two layers were broken: 1. Runtime: pkg/admin/engineclient/client.go ImportConfig didn't accept or forward a workspaceID, so the engine handler always saw an empty ?workspaceId= query and registered everything under "". 2. Persistence: the file store treated (name) as identity instead of (workspaceID, name); the admin and engine each held their own FileStore pointed at the same data.json and raced on save, so the engine's stale view of resources/customOps overwrote admin's freshly written entries on shutdown; mockd start never called LoadFromStore, so persisted resources weren't restored on restart. Changes: - Add Workspace field to CustomOperationConfig (matching the existing one on StatefulResourceConfig); document both as persisted parts of the (workspace, name) identity. - Store interfaces: Delete(ctx, workspaceID, name) and DeleteAll(ctx, workspaceID) for both StatefulResourceStore and CustomOperationStore. Same name across workspaces is now allowed. replace=true scopes to one workspace. - engineclient.Client.ImportConfig accepts workspaceID and appends it as ?workspaceId=. pushImportToEngines threads it through. - handleImportConfig passes the resolved workspaceID to the engine and to persistStatefulResources/persistCustomOperations, which stamp it on each entry before persisting. - Engine handleImportConfig honors per-entity Workspace field with the URL query as fallback default, and dual-writes resources/customOps to its own persistent store so the admin/engine FileStore race on data.json no longer drops them. - config_loader registers persisted custom operations under their own Workspace field instead of "". - mockd start now calls server.LoadFromStore() so persisted stateful resources and custom operations are restored to the runtime engine after a restart. - Admin handler stateful CRUD (handleAddStateResource, handleRegisterCustomOperation, handleDeleteStateResource, handleDeleteCustomOperation) stamps and threads the workspaceID through the file store. - Replace path in handleImportConfig also scopes its mock-store cleanup to the target workspace via MockFilter.WorkspaceID, so a replace import no longer wipes other workspaces' entries. Tests: - pkg/store/file: TestStatefulResourceStore_WorkspaceIsolation and TestCustomOperationStore_WorkspaceIsolation cover (workspace, name) identity, scoped Delete and DeleteAll. - pkg/store/file: extended TestFileStore_Persistence_RoundTrip to assert resources and operations with the same name in different workspaces both round-trip across save+reload. - pkg/admin/engineclient: TestImportConfig_WorkspaceIDInQuery asserts the query parameter is forwarded when set and absent when empty. - pkg/admin: handleImportConfig regression for issue #12 forwards the workspaceId to the engine, plus a cross-workspace replace isolation test that asserts other workspaces' mocks, resources and custom operations all survive a replace import into a different workspace. - All new tests were mutation-tested by temporarily breaking the fix to confirm they catch the regression. - Full ./pkg/... and ./tests/integration/... pass under -race; manual end-to-end with single workspace, two workspaces sharing a table name, and restart-survival all behave correctly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12.
Summary
When importing a config into a named workspace, mocks correctly received the active
workspaceIdbutstatefulResourcesandcustomOperationswere always registered under the default workspace"". At request time,StateStore.Get(mock.WorkspaceID, name)andBridge.GetCustomOperationlooked in the wrong bucket and the request 404'd.There were two layers to the bug:
engineclient.Client.ImportConfigdidn't accept or forward aworkspaceID, so the engine handler always saw an empty?workspaceId=query and registered everything under"".(name)as identity instead of(workspace, name); the admin and engine each held their ownFileStorepointed at the samedata.jsonand raced on save, so the engine's stale view used to overwrite admin's freshly written entries on shutdown;mockd startnever calledLoadFromStore, so persisted resources weren't restored on restart.What changed
Workspacefield added toCustomOperationConfig(matching the existing one onStatefulResourceConfig); both are documented as persisted parts of the(workspace, name)identity.Delete(ctx, workspaceID, name)andDeleteAll(ctx, workspaceID)for bothStatefulResourceStoreandCustomOperationStore. Same name across workspaces is now allowed.replace=truescopes to one workspace.engineclient.Client.ImportConfigaccepts aworkspaceIDand appends it as?workspaceId=.pushImportToEnginesthreads it through.handleImportConfigpasses the resolvedworkspaceIDto the engine and topersistStatefulResources/persistCustomOperations, which stamp it on each entry before persisting.handleImportConfighonors a per-entityWorkspacefield with the URL query as fallback default, and dual-writes resources/customOps to its own persistent store so the admin/engineFileStorerace ondata.jsonno longer drops them.config_loaderregisters persisted custom operations under their ownWorkspacefield instead of"".mockd startnow callsserver.LoadFromStore()so persisted stateful resources and custom operations are restored to the runtime engine after a restart.handleAddStateResource,handleRegisterCustomOperation,handleDeleteStateResource,handleDeleteCustomOperation) stamp and thread the workspaceID through the file store.handleImportConfigalso scopes its mock-store cleanup to the target workspace viaMockFilter.WorkspaceID, so a replace import no longer wipes other workspaces' file-store entries.Test plan
go test -race ./pkg/...— all packages pass, zero racesgo test ./tests/integration/...— all pass (excludingTestBinaryE2E_ExportMocks, which fails identically onorigin/mainand is unrelated)TestStatefulResourceStore_WorkspaceIsolationandTestCustomOperationStore_WorkspaceIsolationcover(workspace, name)identity plus scopedDeleteandDeleteAllTestFileStore_Persistence_RoundTripextended to assert resources and operations with the same name in different workspaces both round-trip across save+reloadTestImportConfig_WorkspaceIDInQueryasserts the engineclient forwards the query parameter when set and omits it when emptyTestHandleImportConfig/forwards_workspaceId_query_parameter_to_engine_on_import_(issue_#12)covers the admin handler end-to-endTestHandleImportConfig/replace_import_preserves_other_workspaces'_resources_(issue_#12)asserts other workspaces' mocks, resources, and custom operations all survive a replace import into a different workspacemockd workspace create+mockd workspace use+mockd import+POST /orders→201(was404onmain)mockd stopandmockd start→POST /orders→201after restart, with the previously created stateful item still presentorderstable name → independent buckets, both reload after restart with[('orders', 'ws_a'), ('orders', 'ws_b')]in the persisteddata.jsonmockd import --replaceinto one workspace leaves the other workspace's persisted resources, custom operations, and mocks untouched in the file storeOut of scope / known limitation
The engine's runtime
ClearMocks()is workspace-unscoped: areplace=trueimport wipes other workspaces' mocks from the engine's in-memory state until the next restart (the admin file store correctly preserves them, so a restart restores them). Issue #12 explicitly notes that mocks already get the correctworkspaceId; this only affects the runtimereplacepath for mocks and is best handled in a follow-up PR.