diff --git a/src/operations.js b/src/operations.js index 66ccf41..2f11f43 100644 --- a/src/operations.js +++ b/src/operations.js @@ -1,5 +1,6 @@ import { parseProviderKind, parseWorkspaceMetadata, parseWorkspaceRecord } from './contracts.js'; import { readPolicy } from './policy.js'; +import { readMetadata } from './metadata.js'; import { createDockerProvider } from './providers/docker.js'; import { createKubernetesProvider } from './providers/kubernetes.js'; import { createAppleContainerProvider } from './providers/apple-container.js'; @@ -120,6 +121,23 @@ export function createWorkspaceProviderOperations(options = {}) { } return { projectID, workspaces, failures, completeProviders }; }, + /** + * Whether a workspace still matches the settings in force, read from its own + * metadata without contacting the provider. Kept separate from inspectWorkspace, + * which runs a health check: a surface labelling a list must not pay for one probe + * per row, and the answer here is already recorded in the workspace itself. + */ + async describeWorkspacePolicyState(workspace) { + parseWorkspaceRecord(workspace); + const provider = providerForWorkspace(workspace); + try { + readMetadata(workspace, provider.kind, policy); + return { provider: provider.kind, matchesPolicy: true }; + } catch (error) { + if (error?.code === 'WORKSPACE_POLICY_MISMATCH') return { provider: provider.kind, matchesPolicy: false, code: error.code }; + throw error; + } + }, async inspectWorkspace(workspace) { workspace = await adoptWorkspace(workspace); const provider = providerForWorkspace(workspace); diff --git a/src/operations.test.js b/src/operations.test.js index 44fa600..9cd7bc0 100644 --- a/src/operations.test.js +++ b/src/operations.test.js @@ -43,6 +43,24 @@ describe('workspace provider recovery operations', () => { await Promise.all([rm(stateDirectory, { recursive: true, force: true }), rm(sourceDirectory, { recursive: true, force: true })]); }); + it('reads policy state from the workspace itself without contacting the provider', async () => { + const operations = createWorkspaceProviderOperations({ policy, sourceDirectory }); + const workspace = recoveredWorkspace(); + + await expect(operations.describeWorkspacePolicyState(workspace)).resolves.toMatchObject({ matchesPolicy: true }); + // Labelling a list must not cost one provider probe per row. + expect(provider.list).not.toHaveBeenCalled(); + expect(provider.reconcile).not.toHaveBeenCalled(); + }); + + it('reports a workspace created under settings that are no longer in force', async () => { + const operations = createWorkspaceProviderOperations({ policy, sourceDirectory }); + const workspace = recoveredWorkspace(); + const drifted = { ...workspace, extra: { ...workspace.extra, policyFingerprint: 'b'.repeat(64) } }; + + await expect(operations.describeWorkspacePolicyState(drifted)).resolves.toMatchObject({ matchesPolicy: false, code: 'WORKSPACE_POLICY_MISMATCH' }); + }); + function recoveredWorkspace(overrides = {}) { const identity = { provider: 'docker',