Skip to content

fix: scope stateful resources and custom operations to workspaces (#12)#16

Open
zach-snell wants to merge 1 commit intomainfrom
fix/issue-12-workspace-import
Open

fix: scope stateful resources and custom operations to workspaces (#12)#16
zach-snell wants to merge 1 commit intomainfrom
fix/issue-12-workspace-import

Conversation

@zach-snell
Copy link
Copy Markdown
Contributor

Fixes #12.

Summary

When importing a config into a named workspace, mocks correctly received the active workspaceId but statefulResources and customOperations 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.

There were two layers to the bug:

  1. Runtimeengineclient.Client.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 (workspace, 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 used to overwrite admin's freshly written entries on shutdown; mockd start never called LoadFromStore, so persisted resources weren't restored on restart.

What changed

  • Workspace field added to CustomOperationConfig (matching the existing one on StatefulResourceConfig); both are documented 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 a workspaceID and appends it as ?workspaceId=. pushImportToEngines threads it through.
  • Admin handleImportConfig passes the resolved workspaceID to the engine and to persistStatefulResources / persistCustomOperations, which stamp it on each entry before persisting.
  • Engine handleImportConfig honors a 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 stateful CRUD handlers (handleAddStateResource, handleRegisterCustomOperation, handleDeleteStateResource, handleDeleteCustomOperation) stamp and thread the workspaceID through the file store.
  • The 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' file-store entries.

Test plan

  • go test -race ./pkg/... — all packages pass, zero races
  • go test ./tests/integration/... — all pass (excluding TestBinaryE2E_ExportMocks, which fails identically on origin/main and is unrelated)
  • New unit tests added and mutation-tested to confirm they catch regressions:
    • TestStatefulResourceStore_WorkspaceIsolation and TestCustomOperationStore_WorkspaceIsolation cover (workspace, name) identity plus scoped Delete and DeleteAll
    • TestFileStore_Persistence_RoundTrip extended to assert resources and operations with the same name in different workspaces both round-trip across save+reload
    • TestImportConfig_WorkspaceIDInQuery asserts the engineclient forwards the query parameter when set and omits it when empty
    • TestHandleImportConfig/forwards_workspaceId_query_parameter_to_engine_on_import_(issue_#12) covers the admin handler end-to-end
    • TestHandleImportConfig/replace_import_preserves_other_workspaces'_resources_(issue_#12) asserts other workspaces' mocks, resources, and custom operations all survive a replace import into a different workspace
  • Manual end-to-end against the built binary, replicating the issue's exact reproduction:
    • Single workspace: mockd workspace create + mockd workspace use + mockd import + POST /orders201 (was 404 on main)
    • Same flow plus mockd stop and mockd startPOST /orders201 after restart, with the previously created stateful item still present
    • Two workspaces with the same orders table name → independent buckets, both reload after restart with [('orders', 'ws_a'), ('orders', 'ws_b')] in the persisted data.json
    • mockd import --replace into one workspace leaves the other workspace's persisted resources, custom operations, and mocks untouched in the file store

Out of scope / known limitation

The engine's runtime ClearMocks() is workspace-unscoped: a replace=true import 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 correct workspaceId; this only affects the runtime replace path for mocks and is best handled in a follow-up PR.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Tables and custom operations not found when mocks are imported into a named workspace

1 participant