canopycms 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/plugin.d.ts +8 -0
- package/dist/auth/plugin.d.ts.map +1 -1
- package/dist/build-mode.d.ts +15 -5
- package/dist/build-mode.d.ts.map +1 -1
- package/dist/build-mode.js +18 -8
- package/dist/build-mode.js.map +1 -1
- package/dist/cli/init.d.ts +2 -2
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +37 -36
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/template-files/ai-config.ts.template +21 -0
- package/dist/cli/template-files/ai-route.ts.template +10 -0
- package/dist/cli/template-files/canopy.ts.template +24 -0
- package/dist/cli/templates.d.ts +5 -1
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +9 -2
- package/dist/cli/templates.js.map +1 -1
- package/dist/config/schemas/config.d.ts +4 -0
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +2 -0
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/types.d.ts +5 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/content-reader.js +2 -2
- package/dist/content-reader.js.map +1 -1
- package/dist/context.js +5 -5
- package/dist/context.js.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +15 -18
- package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
- package/dist/operating-mode/types.d.ts +8 -0
- package/dist/operating-mode/types.d.ts.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/package.json +5 -4
- package/src/cli/init.ts +43 -38
- package/dist/__integration__/fixtures/content-seeds.d.ts +0 -43
- package/dist/__integration__/fixtures/content-seeds.d.ts.map +0 -1
- package/dist/__integration__/fixtures/content-seeds.js +0 -99
- package/dist/__integration__/fixtures/content-seeds.js.map +0 -1
- package/dist/__integration__/fixtures/schemas.d.ts +0 -12
- package/dist/__integration__/fixtures/schemas.d.ts.map +0 -1
- package/dist/__integration__/fixtures/schemas.js +0 -65
- package/dist/__integration__/fixtures/schemas.js.map +0 -1
- package/dist/__integration__/test-utils/api-client.d.ts +0 -123
- package/dist/__integration__/test-utils/api-client.d.ts.map +0 -1
- package/dist/__integration__/test-utils/api-client.js +0 -118
- package/dist/__integration__/test-utils/api-client.js.map +0 -1
- package/dist/__integration__/test-utils/multi-user.d.ts +0 -25
- package/dist/__integration__/test-utils/multi-user.d.ts.map +0 -1
- package/dist/__integration__/test-utils/multi-user.js +0 -105
- package/dist/__integration__/test-utils/multi-user.js.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.d.ts +0 -25
- package/dist/__integration__/test-utils/test-workspace.d.ts.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.js +0 -102
- package/dist/__integration__/test-utils/test-workspace.js.map +0 -1
- package/dist/editor/BranchManager.stories.d.ts +0 -8
- package/dist/editor/BranchManager.stories.d.ts.map +0 -1
- package/dist/editor/BranchManager.stories.js +0 -74
- package/dist/editor/BranchManager.stories.js.map +0 -1
- package/dist/editor/CanopyEditor.stories.d.ts +0 -7
- package/dist/editor/CanopyEditor.stories.d.ts.map +0 -1
- package/dist/editor/CanopyEditor.stories.js +0 -99
- package/dist/editor/CanopyEditor.stories.js.map +0 -1
- package/dist/editor/CommentsPanel.stories.d.ts +0 -10
- package/dist/editor/CommentsPanel.stories.d.ts.map +0 -1
- package/dist/editor/CommentsPanel.stories.js +0 -175
- package/dist/editor/CommentsPanel.stories.js.map +0 -1
- package/dist/editor/Editor.stories.d.ts +0 -7
- package/dist/editor/Editor.stories.d.ts.map +0 -1
- package/dist/editor/Editor.stories.js +0 -95
- package/dist/editor/Editor.stories.js.map +0 -1
- package/dist/editor/EditorPanes.stories.d.ts +0 -7
- package/dist/editor/EditorPanes.stories.d.ts.map +0 -1
- package/dist/editor/EditorPanes.stories.js +0 -116
- package/dist/editor/EditorPanes.stories.js.map +0 -1
- package/dist/editor/EntryNavigator.stories.d.ts +0 -8
- package/dist/editor/EntryNavigator.stories.d.ts.map +0 -1
- package/dist/editor/EntryNavigator.stories.js +0 -42
- package/dist/editor/EntryNavigator.stories.js.map +0 -1
- package/dist/editor/FormRenderer.stories.d.ts +0 -7
- package/dist/editor/FormRenderer.stories.d.ts.map +0 -1
- package/dist/editor/FormRenderer.stories.js +0 -115
- package/dist/editor/FormRenderer.stories.js.map +0 -1
- package/dist/editor/GroupManager.stories.d.ts +0 -19
- package/dist/editor/GroupManager.stories.d.ts.map +0 -1
- package/dist/editor/GroupManager.stories.js +0 -265
- package/dist/editor/GroupManager.stories.js.map +0 -1
- package/dist/editor/PermissionManager.stories.d.ts +0 -20
- package/dist/editor/PermissionManager.stories.d.ts.map +0 -1
- package/dist/editor/PermissionManager.stories.js +0 -506
- package/dist/editor/PermissionManager.stories.js.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.d.ts +0 -10
- package/dist/editor/comments/FieldWrapper.stories.d.ts.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.js +0 -173
- package/dist/editor/comments/FieldWrapper.stories.js.map +0 -1
- package/dist/editor/fields/BlockField.stories.d.ts +0 -7
- package/dist/editor/fields/BlockField.stories.d.ts.map +0 -1
- package/dist/editor/fields/BlockField.stories.js +0 -50
- package/dist/editor/fields/BlockField.stories.js.map +0 -1
- package/dist/editor/fields/fields.stories.d.ts +0 -8
- package/dist/editor/fields/fields.stories.d.ts.map +0 -1
- package/dist/editor/fields/fields.stories.js +0 -34
- package/dist/editor/fields/fields.stories.js.map +0 -1
- package/dist/test-utils/api-test-helpers.d.ts +0 -238
- package/dist/test-utils/api-test-helpers.d.ts.map +0 -1
- package/dist/test-utils/api-test-helpers.js +0 -347
- package/dist/test-utils/api-test-helpers.js.map +0 -1
- package/dist/test-utils/console-spy.d.ts +0 -56
- package/dist/test-utils/console-spy.d.ts.map +0 -1
- package/dist/test-utils/console-spy.js +0 -81
- package/dist/test-utils/console-spy.js.map +0 -1
- package/dist/test-utils/git-helpers.d.ts +0 -21
- package/dist/test-utils/git-helpers.d.ts.map +0 -1
- package/dist/test-utils/git-helpers.js +0 -23
- package/dist/test-utils/git-helpers.js.map +0 -1
- package/dist/test-utils/index.d.ts +0 -5
- package/dist/test-utils/index.d.ts.map +0 -1
- package/dist/test-utils/index.js +0 -4
- package/dist/test-utils/index.js.map +0 -1
- package/src/__integration__/errors/invalid-content.test.ts +0 -238
- package/src/__integration__/errors/permission-denied.test.ts +0 -220
- package/src/__integration__/fixtures/content-seeds.ts +0 -105
- package/src/__integration__/fixtures/schemas.ts +0 -67
- package/src/__integration__/initialization/prod-sim-init.test.ts +0 -139
- package/src/__integration__/permissions/path-permissions.test.ts +0 -314
- package/src/__integration__/permissions/role-permissions.test.ts +0 -354
- package/src/__integration__/permissions/settings-branch-isolation.test.ts +0 -317
- package/src/__integration__/settings/groups-api.test.ts +0 -403
- package/src/__integration__/test-utils/api-client.ts +0 -167
- package/src/__integration__/test-utils/multi-user.ts +0 -129
- package/src/__integration__/test-utils/test-workspace.ts +0 -130
- package/src/__integration__/user/user-context.test.ts +0 -174
- package/src/__integration__/validation/input-validation.test.ts +0 -166
- package/src/__integration__/workflows/api-editing-workflow.test.ts +0 -244
- package/src/__integration__/workflows/conflict-resolution.test.ts +0 -259
- package/src/__integration__/workflows/editing-workflow.test.ts +0 -205
- package/src/__integration__/workflows/review-workflow.test.ts +0 -260
- package/src/ai/__tests__/build.integration.test.ts +0 -224
- package/src/ai/__tests__/generate.integration.test.ts +0 -495
- package/src/ai/__tests__/handler.integration.test.ts +0 -212
- package/src/ai/__tests__/json-to-markdown.test.ts +0 -553
- package/src/ai/generate.ts +0 -410
- package/src/ai/handler.ts +0 -123
- package/src/ai/index.ts +0 -26
- package/src/ai/json-to-markdown.ts +0 -424
- package/src/ai/resolve-branch.ts +0 -34
- package/src/ai/types.ts +0 -160
- package/src/api/AGENTS.md +0 -81
- package/src/api/__test__/mock-client.ts +0 -404
- package/src/api/assets.test.ts +0 -140
- package/src/api/assets.ts +0 -154
- package/src/api/branch-merge.test.ts +0 -163
- package/src/api/branch-merge.ts +0 -113
- package/src/api/branch-review.test.ts +0 -297
- package/src/api/branch-review.ts +0 -136
- package/src/api/branch-status.test.ts +0 -85
- package/src/api/branch-status.ts +0 -153
- package/src/api/branch-withdraw.test.ts +0 -146
- package/src/api/branch-withdraw.ts +0 -81
- package/src/api/branch-workflow.integration.test.ts +0 -578
- package/src/api/branch.test.ts +0 -620
- package/src/api/branch.ts +0 -492
- package/src/api/client.test.ts +0 -349
- package/src/api/client.ts +0 -506
- package/src/api/comments.test.ts +0 -285
- package/src/api/comments.ts +0 -210
- package/src/api/content.test.ts +0 -345
- package/src/api/content.ts +0 -454
- package/src/api/entries.test.ts +0 -1339
- package/src/api/entries.ts +0 -650
- package/src/api/github-sync.ts +0 -144
- package/src/api/groups.test.ts +0 -1013
- package/src/api/groups.ts +0 -375
- package/src/api/guards.test.ts +0 -533
- package/src/api/guards.ts +0 -271
- package/src/api/index.ts +0 -87
- package/src/api/permissions.test.ts +0 -766
- package/src/api/permissions.ts +0 -334
- package/src/api/reference-options.ts +0 -118
- package/src/api/resolve-references.ts +0 -107
- package/src/api/route-builder.ts +0 -289
- package/src/api/schema.test.ts +0 -840
- package/src/api/schema.ts +0 -936
- package/src/api/security.test.ts +0 -233
- package/src/api/settings-helpers.ts +0 -84
- package/src/api/types.ts +0 -40
- package/src/api/user.test.ts +0 -127
- package/src/api/user.ts +0 -42
- package/src/api/validators.test.ts +0 -275
- package/src/api/validators.ts +0 -176
- package/src/asset-store.test.ts +0 -37
- package/src/asset-store.ts +0 -110
- package/src/auth/cache.ts +0 -7
- package/src/auth/caching-auth-plugin.test.ts +0 -154
- package/src/auth/caching-auth-plugin.ts +0 -109
- package/src/auth/context-helpers.ts +0 -75
- package/src/auth/file-based-auth-cache.test.ts +0 -257
- package/src/auth/file-based-auth-cache.ts +0 -279
- package/src/auth/index.ts +0 -12
- package/src/auth/plugin.ts +0 -51
- package/src/auth/types.ts +0 -38
- package/src/authorization/__tests__/branch.test.ts +0 -260
- package/src/authorization/__tests__/content.test.ts +0 -142
- package/src/authorization/__tests__/path.test.ts +0 -133
- package/src/authorization/__tests__/permissions-loader.test.ts +0 -200
- package/src/authorization/branch.ts +0 -94
- package/src/authorization/content.ts +0 -93
- package/src/authorization/groups/index.ts +0 -11
- package/src/authorization/groups/loader.ts +0 -127
- package/src/authorization/groups/schema.ts +0 -48
- package/src/authorization/helpers.ts +0 -48
- package/src/authorization/index.ts +0 -84
- package/src/authorization/path.ts +0 -112
- package/src/authorization/permissions/index.ts +0 -11
- package/src/authorization/permissions/loader.ts +0 -116
- package/src/authorization/permissions/schema.ts +0 -66
- package/src/authorization/test-utils.ts +0 -15
- package/src/authorization/types.ts +0 -66
- package/src/authorization/validation.test.ts +0 -100
- package/src/authorization/validation.ts +0 -62
- package/src/branch-metadata.test.ts +0 -168
- package/src/branch-metadata.ts +0 -166
- package/src/branch-registry.test.ts +0 -248
- package/src/branch-registry.ts +0 -152
- package/src/branch-schema-cache.test.ts +0 -275
- package/src/branch-schema-cache.ts +0 -189
- package/src/branch-workspace.test.ts +0 -183
- package/src/branch-workspace.ts +0 -124
- package/src/build/generate-ai-content.ts +0 -78
- package/src/build/index.ts +0 -8
- package/src/build-mode.ts +0 -27
- package/src/cli/generate-ai-content.ts +0 -100
- package/src/cli/init.test.ts +0 -240
- package/src/cli/templates/canopy.ts.template +0 -55
- package/src/cli/templates.ts +0 -47
- package/src/client.ts +0 -12
- package/src/comment-store.test.ts +0 -442
- package/src/comment-store.ts +0 -301
- package/src/config/__tests__/config.test.ts +0 -513
- package/src/config/flatten.ts +0 -174
- package/src/config/helpers.ts +0 -167
- package/src/config/index.ts +0 -86
- package/src/config/schemas/collection.ts +0 -67
- package/src/config/schemas/config.ts +0 -77
- package/src/config/schemas/field.ts +0 -108
- package/src/config/schemas/media.ts +0 -27
- package/src/config/schemas/permissions.ts +0 -21
- package/src/config/types.ts +0 -321
- package/src/config/validation.ts +0 -70
- package/src/config-test.ts +0 -65
- package/src/config.ts +0 -11
- package/src/content-id-index.test.ts +0 -512
- package/src/content-id-index.ts +0 -479
- package/src/content-reader.test.ts +0 -478
- package/src/content-reader.ts +0 -214
- package/src/content-store.test.ts +0 -1126
- package/src/content-store.ts +0 -793
- package/src/context.ts +0 -111
- package/src/editor/BranchManager.stories.tsx +0 -80
- package/src/editor/BranchManager.test.tsx +0 -324
- package/src/editor/BranchManager.tsx +0 -461
- package/src/editor/CanopyEditor.stories.tsx +0 -128
- package/src/editor/CanopyEditor.test.tsx +0 -81
- package/src/editor/CanopyEditor.tsx +0 -73
- package/src/editor/CanopyEditorPage.test.tsx +0 -59
- package/src/editor/CanopyEditorPage.tsx +0 -25
- package/src/editor/CommentsPanel.stories.tsx +0 -184
- package/src/editor/CommentsPanel.tsx +0 -338
- package/src/editor/Editor.integration.test.tsx +0 -227
- package/src/editor/Editor.stories.tsx +0 -119
- package/src/editor/Editor.tsx +0 -1221
- package/src/editor/EditorPanes.stories.tsx +0 -256
- package/src/editor/EditorPanes.test.tsx +0 -77
- package/src/editor/EditorPanes.tsx +0 -180
- package/src/editor/EntryNavigator.stories.tsx +0 -65
- package/src/editor/EntryNavigator.test.tsx +0 -598
- package/src/editor/EntryNavigator.tsx +0 -665
- package/src/editor/FormRenderer.stories.tsx +0 -212
- package/src/editor/FormRenderer.test.tsx +0 -194
- package/src/editor/FormRenderer.tsx +0 -432
- package/src/editor/GroupManager.stories.tsx +0 -301
- package/src/editor/GroupManager.test.tsx +0 -682
- package/src/editor/GroupManager.tsx +0 -9
- package/src/editor/PermissionManager.stories.tsx +0 -539
- package/src/editor/PermissionManager.test.tsx +0 -864
- package/src/editor/PermissionManager.tsx +0 -12
- package/src/editor/canopy-path.test.ts +0 -23
- package/src/editor/canopy-path.ts +0 -52
- package/src/editor/client-reference-resolver.ts +0 -118
- package/src/editor/comments/BranchComments.tsx +0 -93
- package/src/editor/comments/EntryComments.tsx +0 -94
- package/src/editor/comments/FieldWrapper.stories.tsx +0 -210
- package/src/editor/comments/FieldWrapper.tsx +0 -129
- package/src/editor/comments/InlineCommentThread.test.tsx +0 -384
- package/src/editor/comments/InlineCommentThread.tsx +0 -246
- package/src/editor/comments/ThreadCarousel.test.tsx +0 -393
- package/src/editor/comments/ThreadCarousel.tsx +0 -525
- package/src/editor/components/ConfirmDeleteModal.tsx +0 -49
- package/src/editor/components/EditorContext.tsx +0 -49
- package/src/editor/components/EditorFooter.tsx +0 -47
- package/src/editor/components/EditorHeader.tsx +0 -492
- package/src/editor/components/EditorSidebar.tsx +0 -193
- package/src/editor/components/EntryCreateModal.tsx +0 -193
- package/src/editor/components/RenameEntryModal.tsx +0 -152
- package/src/editor/components/UserBadge.test.tsx +0 -274
- package/src/editor/components/UserBadge.tsx +0 -240
- package/src/editor/components/index.ts +0 -6
- package/src/editor/context/ApiClientContext.tsx +0 -56
- package/src/editor/context/EditorStateContext.tsx +0 -221
- package/src/editor/context/index.ts +0 -40
- package/src/editor/editor-config.test.ts +0 -385
- package/src/editor/editor-config.ts +0 -94
- package/src/editor/editor-utils.test.ts +0 -772
- package/src/editor/editor-utils.ts +0 -303
- package/src/editor/env.ts +0 -4
- package/src/editor/fields/BlockField.stories.tsx +0 -79
- package/src/editor/fields/BlockField.tsx +0 -267
- package/src/editor/fields/CodeField.tsx +0 -41
- package/src/editor/fields/MarkdownField.tsx +0 -205
- package/src/editor/fields/ObjectField.tsx +0 -71
- package/src/editor/fields/ReferenceField.tsx +0 -138
- package/src/editor/fields/SelectField.tsx +0 -76
- package/src/editor/fields/TextField.tsx +0 -35
- package/src/editor/fields/ToggleField.tsx +0 -37
- package/src/editor/fields/fields.stories.tsx +0 -40
- package/src/editor/group-manager/ExternalGroupsTab.tsx +0 -114
- package/src/editor/group-manager/GroupCard.tsx +0 -102
- package/src/editor/group-manager/GroupForm.tsx +0 -66
- package/src/editor/group-manager/InternalGroupsTab.tsx +0 -147
- package/src/editor/group-manager/MemberList.tsx +0 -184
- package/src/editor/group-manager/hooks/useExternalGroupSearch.ts +0 -63
- package/src/editor/group-manager/hooks/useGroupState.ts +0 -134
- package/src/editor/group-manager/hooks/useUserSearch.ts +0 -84
- package/src/editor/group-manager/index.tsx +0 -210
- package/src/editor/group-manager/types.ts +0 -28
- package/src/editor/hooks/README.md +0 -26
- package/src/editor/hooks/__test__/test-utils.tsx +0 -183
- package/src/editor/hooks/index.ts +0 -23
- package/src/editor/hooks/useBranchActions.test.tsx +0 -267
- package/src/editor/hooks/useBranchActions.tsx +0 -121
- package/src/editor/hooks/useBranchManager.test.tsx +0 -391
- package/src/editor/hooks/useBranchManager.tsx +0 -326
- package/src/editor/hooks/useCommentSystem.test.ts +0 -615
- package/src/editor/hooks/useCommentSystem.ts +0 -347
- package/src/editor/hooks/useDraftManager.test.ts +0 -375
- package/src/editor/hooks/useDraftManager.ts +0 -259
- package/src/editor/hooks/useEditorLayout.test.ts +0 -147
- package/src/editor/hooks/useEditorLayout.ts +0 -67
- package/src/editor/hooks/useEntryManager.test.ts +0 -588
- package/src/editor/hooks/useEntryManager.ts +0 -387
- package/src/editor/hooks/useGroupManager.test.ts +0 -277
- package/src/editor/hooks/useGroupManager.ts +0 -139
- package/src/editor/hooks/usePermissionManager.test.ts +0 -211
- package/src/editor/hooks/usePermissionManager.ts +0 -113
- package/src/editor/hooks/useReferenceResolution.ts +0 -248
- package/src/editor/hooks/useSchemaManager.test.ts +0 -370
- package/src/editor/hooks/useSchemaManager.ts +0 -310
- package/src/editor/hooks/useUserContext.tsx +0 -57
- package/src/editor/hooks/useUserMetadata.test.ts +0 -191
- package/src/editor/hooks/useUserMetadata.ts +0 -71
- package/src/editor/permission-manager/GroupSelector.tsx +0 -73
- package/src/editor/permission-manager/PermissionEditor.tsx +0 -321
- package/src/editor/permission-manager/PermissionLevelBadge.tsx +0 -53
- package/src/editor/permission-manager/PermissionTree.tsx +0 -237
- package/src/editor/permission-manager/UserSelector.tsx +0 -95
- package/src/editor/permission-manager/constants.tsx +0 -18
- package/src/editor/permission-manager/hooks/useGroupsAndUsers.ts +0 -153
- package/src/editor/permission-manager/hooks/usePermissionTree.ts +0 -200
- package/src/editor/permission-manager/index.tsx +0 -294
- package/src/editor/permission-manager/types.ts +0 -58
- package/src/editor/permission-manager/utils.ts +0 -179
- package/src/editor/preview-bridge.test.tsx +0 -50
- package/src/editor/preview-bridge.tsx +0 -294
- package/src/editor/schema-editor/CollectionEditor.test.tsx +0 -238
- package/src/editor/schema-editor/CollectionEditor.tsx +0 -520
- package/src/editor/schema-editor/EntryTypeEditor.test.tsx +0 -215
- package/src/editor/schema-editor/EntryTypeEditor.tsx +0 -367
- package/src/editor/schema-editor/index.ts +0 -19
- package/src/editor/setup-test-dom.ts +0 -10
- package/src/editor/test-setup.ts +0 -33
- package/src/editor/theme.tsx +0 -119
- package/src/editor/utils/env.ts +0 -39
- package/src/entry-schema-registry.test.ts +0 -281
- package/src/entry-schema-registry.ts +0 -121
- package/src/entry-schema.ts +0 -84
- package/src/git-manager.test.ts +0 -552
- package/src/git-manager.ts +0 -667
- package/src/github-service.test.ts +0 -312
- package/src/github-service.ts +0 -295
- package/src/http/handler.test.ts +0 -275
- package/src/http/handler.ts +0 -280
- package/src/http/index.ts +0 -11
- package/src/http/router.ts +0 -164
- package/src/http/types.ts +0 -44
- package/src/id.test.ts +0 -48
- package/src/id.ts +0 -22
- package/src/index.ts +0 -26
- package/src/operating-mode/__tests__/strategies.test.ts +0 -511
- package/src/operating-mode/client-safe-strategy.ts +0 -184
- package/src/operating-mode/client-unsafe-strategy.ts +0 -303
- package/src/operating-mode/client.ts +0 -13
- package/src/operating-mode/index.ts +0 -34
- package/src/operating-mode/types.ts +0 -186
- package/src/paths/__tests__/branch.test.ts +0 -53
- package/src/paths/__tests__/normalize.test.ts +0 -141
- package/src/paths/__tests__/resolve.test.ts +0 -207
- package/src/paths/__tests__/validation.test.ts +0 -61
- package/src/paths/branch.ts +0 -115
- package/src/paths/index.ts +0 -73
- package/src/paths/normalize-server.ts +0 -40
- package/src/paths/normalize.ts +0 -107
- package/src/paths/resolve.ts +0 -61
- package/src/paths/test-utils.ts +0 -37
- package/src/paths/types.ts +0 -68
- package/src/paths/validation.test.ts +0 -480
- package/src/paths/validation.ts +0 -391
- package/src/reference-resolver.test.ts +0 -107
- package/src/reference-resolver.ts +0 -157
- package/src/schema/index.ts +0 -29
- package/src/schema/meta-loader.ts +0 -366
- package/src/schema/resolver.ts +0 -83
- package/src/schema/schema-store-types.ts +0 -56
- package/src/schema/schema-store.test.ts +0 -816
- package/src/schema/schema-store.ts +0 -795
- package/src/schema/types.ts +0 -33
- package/src/schema-meta-loader.test.ts +0 -447
- package/src/server.ts +0 -15
- package/src/services.test.ts +0 -559
- package/src/services.ts +0 -373
- package/src/settings-branch-utils.ts +0 -53
- package/src/settings-workspace.ts +0 -156
- package/src/task-queue/README.md +0 -144
- package/src/task-queue/index.ts +0 -14
- package/src/task-queue/task-queue.test.ts +0 -524
- package/src/task-queue/task-queue.ts +0 -514
- package/src/task-queue/types.ts +0 -41
- package/src/test-utils/api-test-helpers.ts +0 -445
- package/src/test-utils/console-spy.test.ts +0 -14
- package/src/test-utils/console-spy.ts +0 -125
- package/src/test-utils/git-helpers.ts +0 -31
- package/src/test-utils/index.ts +0 -4
- package/src/types.ts +0 -54
- package/src/user.ts +0 -118
- package/src/utils/debug.test.ts +0 -114
- package/src/utils/debug.ts +0 -127
- package/src/utils/error.test.ts +0 -92
- package/src/utils/error.ts +0 -83
- package/src/utils/format.ts +0 -12
- package/src/validation/__tests__/field-traversal.test.ts +0 -263
- package/src/validation/deletion-checker.ts +0 -234
- package/src/validation/field-traversal.ts +0 -146
- package/src/validation/reference-validator.ts +0 -168
- package/src/worker/cms-worker-rebase.test.ts +0 -473
- package/src/worker/cms-worker.ts +0 -777
- package/src/worker/integration.test.ts +0 -289
- package/src/worker/task-queue-config.ts +0 -25
- package/src/worker/task-queue.test.ts +0 -452
- package/src/worker/task-queue.ts +0 -58
- /package/{src/cli/templates → dist/cli/template-files}/Dockerfile.cms.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/canopycms.config.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/deploy-cms.yml.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/edit-page.tsx.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/route.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/schemas.ts.template +0 -0
package/src/task-queue/README.md
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
# task-queue
|
|
2
|
-
|
|
3
|
-
A file-based persistent task queue for Node.js. Tasks are JSON files organized in subdirectories by status. Designed for shared filesystems (EFS, NFS) where one process enqueues and another dequeues.
|
|
4
|
-
|
|
5
|
-
No external dependencies — only Node.js stdlib (`fs`, `path`, `crypto`).
|
|
6
|
-
|
|
7
|
-
## Directory layout
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
{taskDir}/
|
|
11
|
-
pending/ tasks ready to be picked up
|
|
12
|
-
processing/ tasks currently being executed
|
|
13
|
-
completed/ tasks that finished successfully
|
|
14
|
-
failed/ tasks that permanently failed (exhausted retries)
|
|
15
|
-
corrupt/ unreadable files moved here for inspection
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Usage
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
import {
|
|
22
|
-
enqueueTask,
|
|
23
|
-
dequeueTask,
|
|
24
|
-
completeTask,
|
|
25
|
-
failTask,
|
|
26
|
-
retryTask,
|
|
27
|
-
recoverOrphanedTasks,
|
|
28
|
-
getTask,
|
|
29
|
-
listTasks,
|
|
30
|
-
getQueueStats,
|
|
31
|
-
} from 'canopycms/task-queue'
|
|
32
|
-
|
|
33
|
-
// Producer (e.g., Lambda, API handler)
|
|
34
|
-
const taskId = await enqueueTask('/mnt/efs/.tasks', {
|
|
35
|
-
action: 'send-email',
|
|
36
|
-
payload: { to: 'user@example.com', subject: 'Hello' },
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
// Consumer (e.g., worker daemon)
|
|
40
|
-
const task = await dequeueTask('/mnt/efs/.tasks')
|
|
41
|
-
if (task) {
|
|
42
|
-
try {
|
|
43
|
-
const result = await sendEmail(task.payload)
|
|
44
|
-
await completeTask('/mnt/efs/.tasks', task.id, result)
|
|
45
|
-
} catch (err) {
|
|
46
|
-
// Retry with exponential backoff, or fail permanently
|
|
47
|
-
if ((task.retryCount ?? 0) < (task.maxRetries ?? 3)) {
|
|
48
|
-
await retryTask('/mnt/efs/.tasks', task.id, err.message)
|
|
49
|
-
} else {
|
|
50
|
-
await failTask('/mnt/efs/.tasks', task.id, err.message)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## API
|
|
57
|
-
|
|
58
|
-
### Core operations
|
|
59
|
-
|
|
60
|
-
| Function | Description |
|
|
61
|
-
| -------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
62
|
-
| `enqueueTask(taskDir, { action, payload, maxRetries? })` | Create a pending task. Returns the task ID. |
|
|
63
|
-
| `dequeueTask(taskDir)` | Get the oldest ready task, move it to processing. Returns `null` if empty. |
|
|
64
|
-
| `completeTask(taskDir, taskId, result)` | Mark a task as completed with a result object. |
|
|
65
|
-
| `failTask(taskDir, taskId, error)` | Mark a task as permanently failed. |
|
|
66
|
-
| `retryTask(taskDir, taskId, error)` | Move a task back to pending with exponential backoff. |
|
|
67
|
-
|
|
68
|
-
### Recovery & maintenance
|
|
69
|
-
|
|
70
|
-
| Function | Description |
|
|
71
|
-
| ------------------------------------------ | ----------------------------------------------------------------------------- |
|
|
72
|
-
| `recoverOrphanedTasks(taskDir, maxAgeMs?)` | Move stale processing tasks back to pending. Call on startup. Default: 5 min. |
|
|
73
|
-
| `cleanupOldTasks(taskDir, maxAgeMs?)` | Delete old completed/failed tasks. Default: 30 days. |
|
|
74
|
-
|
|
75
|
-
### Query (for UIs, monitoring)
|
|
76
|
-
|
|
77
|
-
| Function | Description |
|
|
78
|
-
| ------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
79
|
-
| `getTask(taskDir, taskId)` | Find a task by ID in any status directory. |
|
|
80
|
-
| `listTasks(taskDir, status, limit?)` | List tasks in a status directory, sorted by createdAt. |
|
|
81
|
-
| `getQueueStats(taskDir)` | Count of tasks in each status: `{ pending, processing, completed, failed, corrupt }`. |
|
|
82
|
-
|
|
83
|
-
## Retry behavior
|
|
84
|
-
|
|
85
|
-
Tasks have `retryCount`, `maxRetries`, and `retryAfter` fields:
|
|
86
|
-
|
|
87
|
-
- `retryTask()` increments `retryCount` and sets `retryAfter` with exponential backoff
|
|
88
|
-
- Backoff schedule: 5s, 10s, 20s, 40s, 60s (capped)
|
|
89
|
-
- `dequeueTask()` skips tasks whose `retryAfter` is in the future
|
|
90
|
-
- After `maxRetries` exhausted, use `failTask()` to move to failed/
|
|
91
|
-
|
|
92
|
-
Default `maxRetries` is 3 (configurable per-task at enqueue time).
|
|
93
|
-
|
|
94
|
-
## Crash safety
|
|
95
|
-
|
|
96
|
-
The queue uses a write-then-unlink pattern for state transitions. If the process crashes between these operations, a task could exist in two directories. The queue handles this:
|
|
97
|
-
|
|
98
|
-
- **Dedup on dequeue**: Before executing a dequeued task, checks if it already exists in `completed/` or `failed/`. If so, cleans up the stale copy instead of re-executing.
|
|
99
|
-
- **Dedup on recovery**: `recoverOrphanedTasks()` checks the same before moving an orphaned task back to pending.
|
|
100
|
-
- **Corrupt file handling**: Malformed JSON files are moved to `corrupt/` instead of crashing the consumer.
|
|
101
|
-
|
|
102
|
-
## Logger
|
|
103
|
-
|
|
104
|
-
All functions accept an optional logger as the last argument:
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
const logger = {
|
|
108
|
-
debug(message: string, data?: Record<string, unknown>) {
|
|
109
|
-
console.log(`[TaskQueue] ${message}`, data)
|
|
110
|
-
},
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
await enqueueTask(taskDir, task, logger)
|
|
114
|
-
await dequeueTask(taskDir, logger)
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Omit the logger for silent operation.
|
|
118
|
-
|
|
119
|
-
## Task shape
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
interface Task {
|
|
123
|
-
id: string // UUID, auto-generated
|
|
124
|
-
action: string // arbitrary — the queue doesn't interpret it
|
|
125
|
-
payload: Record<string, unknown> // action-specific data
|
|
126
|
-
status: 'pending' | 'processing' | 'completed' | 'failed'
|
|
127
|
-
createdAt: string // ISO timestamp
|
|
128
|
-
completedAt?: string // set on complete/fail
|
|
129
|
-
result?: Record<string, unknown> // set on complete
|
|
130
|
-
error?: string // set on fail/retry
|
|
131
|
-
retryCount?: number // 0-based
|
|
132
|
-
maxRetries?: number // default 3
|
|
133
|
-
retryAfter?: string // ISO timestamp — skip until then
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## CanopyCMS integration
|
|
138
|
-
|
|
139
|
-
Within CanopyCMS, the worker layer (`src/worker/`) wraps this module with:
|
|
140
|
-
|
|
141
|
-
- **`TaskAction` type** — a union of CMS-specific actions (`push-and-create-pr`, `close-pr`, etc.)
|
|
142
|
-
- **`WorkerTask` alias** — `Task & { action: TaskAction }`
|
|
143
|
-
- **`cmsTaskQueueLogger`** — wired to CanopyCMS's debug logger
|
|
144
|
-
- **`task-queue-config.ts`** — resolves the task directory from CanopyCMS operating mode config
|
package/src/task-queue/index.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export type { Task, TaskStatus, QueueStats, TaskQueueLogger } from './types'
|
|
2
|
-
|
|
3
|
-
export {
|
|
4
|
-
enqueueTask,
|
|
5
|
-
dequeueTask,
|
|
6
|
-
completeTask,
|
|
7
|
-
failTask,
|
|
8
|
-
retryTask,
|
|
9
|
-
recoverOrphanedTasks,
|
|
10
|
-
cleanupOldTasks,
|
|
11
|
-
getTask,
|
|
12
|
-
listTasks,
|
|
13
|
-
getQueueStats,
|
|
14
|
-
} from './task-queue'
|
|
@@ -1,524 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import fs from 'node:fs/promises'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import {
|
|
6
|
-
enqueueTask,
|
|
7
|
-
dequeueTask,
|
|
8
|
-
completeTask,
|
|
9
|
-
failTask,
|
|
10
|
-
retryTask,
|
|
11
|
-
getTask,
|
|
12
|
-
listTasks,
|
|
13
|
-
getQueueStats,
|
|
14
|
-
recoverOrphanedTasks,
|
|
15
|
-
cleanupOldTasks,
|
|
16
|
-
} from './task-queue'
|
|
17
|
-
|
|
18
|
-
describe('Task Queue', () => {
|
|
19
|
-
let tmpDir: string
|
|
20
|
-
|
|
21
|
-
beforeEach(async () => {
|
|
22
|
-
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'taskq-test-'))
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
afterEach(async () => {
|
|
26
|
-
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
// ========================================================================
|
|
30
|
-
// Core lifecycle
|
|
31
|
-
// ========================================================================
|
|
32
|
-
|
|
33
|
-
describe('enqueue', () => {
|
|
34
|
-
it('creates a pending task file with correct fields', async () => {
|
|
35
|
-
const id = await enqueueTask(tmpDir, {
|
|
36
|
-
action: 'deploy',
|
|
37
|
-
payload: { env: 'prod' },
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
const filePath = path.join(tmpDir, 'pending', `${id}.json`)
|
|
41
|
-
const task = JSON.parse(await fs.readFile(filePath, 'utf-8'))
|
|
42
|
-
expect(task.id).toBe(id)
|
|
43
|
-
expect(task.action).toBe('deploy')
|
|
44
|
-
expect(task.payload).toEqual({ env: 'prod' })
|
|
45
|
-
expect(task.status).toBe('pending')
|
|
46
|
-
expect(task.retryCount).toBe(0)
|
|
47
|
-
expect(task.maxRetries).toBe(3)
|
|
48
|
-
expect(task.createdAt).toBeTruthy()
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('generates unique IDs', async () => {
|
|
52
|
-
const id1 = await enqueueTask(tmpDir, { action: 'a', payload: {} })
|
|
53
|
-
const id2 = await enqueueTask(tmpDir, { action: 'b', payload: {} })
|
|
54
|
-
expect(id1).not.toBe(id2)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('accepts any string as action (generic, not a fixed union)', async () => {
|
|
58
|
-
const id = await enqueueTask(tmpDir, {
|
|
59
|
-
action: 'custom-user-defined-action',
|
|
60
|
-
payload: { custom: true },
|
|
61
|
-
})
|
|
62
|
-
const task = await getTask(tmpDir, id)
|
|
63
|
-
expect(task!.action).toBe('custom-user-defined-action')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('allows custom maxRetries', async () => {
|
|
67
|
-
const id = await enqueueTask(tmpDir, {
|
|
68
|
-
action: 'fragile',
|
|
69
|
-
payload: {},
|
|
70
|
-
maxRetries: 10,
|
|
71
|
-
})
|
|
72
|
-
const task = await getTask(tmpDir, id)
|
|
73
|
-
expect(task!.maxRetries).toBe(10)
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
describe('dequeue', () => {
|
|
78
|
-
it('returns oldest task first (FIFO)', async () => {
|
|
79
|
-
const id1 = await enqueueTask(tmpDir, { action: 'first', payload: {} })
|
|
80
|
-
await new Promise((r) => setTimeout(r, 5))
|
|
81
|
-
await enqueueTask(tmpDir, { action: 'second', payload: {} })
|
|
82
|
-
|
|
83
|
-
const task = await dequeueTask(tmpDir)
|
|
84
|
-
expect(task!.id).toBe(id1)
|
|
85
|
-
expect(task!.status).toBe('processing')
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('moves task from pending/ to processing/', async () => {
|
|
89
|
-
const id = await enqueueTask(tmpDir, { action: 'x', payload: {} })
|
|
90
|
-
await dequeueTask(tmpDir)
|
|
91
|
-
|
|
92
|
-
await expect(fs.stat(path.join(tmpDir, 'pending', `${id}.json`))).rejects.toThrow()
|
|
93
|
-
const stat = await fs.stat(path.join(tmpDir, 'processing', `${id}.json`))
|
|
94
|
-
expect(stat.isFile()).toBe(true)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('returns null when queue is empty', async () => {
|
|
98
|
-
expect(await dequeueTask(tmpDir)).toBeNull()
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('returns null when pending directory does not exist', async () => {
|
|
102
|
-
expect(await dequeueTask(path.join(tmpDir, 'nonexistent'))).toBeNull()
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
describe('complete', () => {
|
|
107
|
-
it('moves task to completed/ with result and timestamp', async () => {
|
|
108
|
-
const id = await enqueueTask(tmpDir, { action: 'build', payload: {} })
|
|
109
|
-
await dequeueTask(tmpDir)
|
|
110
|
-
await completeTask(tmpDir, id, { url: 'https://example.com' })
|
|
111
|
-
|
|
112
|
-
const task = JSON.parse(
|
|
113
|
-
await fs.readFile(path.join(tmpDir, 'completed', `${id}.json`), 'utf-8'),
|
|
114
|
-
)
|
|
115
|
-
expect(task.status).toBe('completed')
|
|
116
|
-
expect(task.result.url).toBe('https://example.com')
|
|
117
|
-
expect(task.completedAt).toBeTruthy()
|
|
118
|
-
|
|
119
|
-
await expect(fs.stat(path.join(tmpDir, 'processing', `${id}.json`))).rejects.toThrow()
|
|
120
|
-
})
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
describe('fail', () => {
|
|
124
|
-
it('moves task to failed/ with error and timestamp', async () => {
|
|
125
|
-
const id = await enqueueTask(tmpDir, { action: 'deploy', payload: {} })
|
|
126
|
-
await dequeueTask(tmpDir)
|
|
127
|
-
await failTask(tmpDir, id, 'Connection refused')
|
|
128
|
-
|
|
129
|
-
const task = JSON.parse(await fs.readFile(path.join(tmpDir, 'failed', `${id}.json`), 'utf-8'))
|
|
130
|
-
expect(task.status).toBe('failed')
|
|
131
|
-
expect(task.error).toBe('Connection refused')
|
|
132
|
-
expect(task.completedAt).toBeTruthy()
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
describe('full lifecycle', () => {
|
|
137
|
-
it('enqueue → dequeue → complete', async () => {
|
|
138
|
-
const id = await enqueueTask(tmpDir, {
|
|
139
|
-
action: 'send-email',
|
|
140
|
-
payload: { to: 'a@b.com' },
|
|
141
|
-
})
|
|
142
|
-
const task = await dequeueTask(tmpDir)
|
|
143
|
-
expect(task!.id).toBe(id)
|
|
144
|
-
|
|
145
|
-
await completeTask(tmpDir, id, { sent: true })
|
|
146
|
-
const result = await getTask(tmpDir, id)
|
|
147
|
-
expect(result!.status).toBe('completed')
|
|
148
|
-
|
|
149
|
-
expect(await dequeueTask(tmpDir)).toBeNull()
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('enqueue → dequeue → fail', async () => {
|
|
153
|
-
const id = await enqueueTask(tmpDir, { action: 'deploy', payload: {} })
|
|
154
|
-
await dequeueTask(tmpDir)
|
|
155
|
-
await failTask(tmpDir, id, 'Timeout')
|
|
156
|
-
|
|
157
|
-
const result = await getTask(tmpDir, id)
|
|
158
|
-
expect(result!.status).toBe('failed')
|
|
159
|
-
expect(result!.error).toBe('Timeout')
|
|
160
|
-
})
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
// ========================================================================
|
|
164
|
-
// Issue 2: Retry with exponential backoff
|
|
165
|
-
// ========================================================================
|
|
166
|
-
|
|
167
|
-
describe('retry', () => {
|
|
168
|
-
it('moves task back to pending with incremented retryCount', async () => {
|
|
169
|
-
const id = await enqueueTask(tmpDir, { action: 'push', payload: {} })
|
|
170
|
-
await dequeueTask(tmpDir)
|
|
171
|
-
await retryTask(tmpDir, id, 'Transient error')
|
|
172
|
-
|
|
173
|
-
const task = JSON.parse(
|
|
174
|
-
await fs.readFile(path.join(tmpDir, 'pending', `${id}.json`), 'utf-8'),
|
|
175
|
-
)
|
|
176
|
-
expect(task.status).toBe('pending')
|
|
177
|
-
expect(task.retryCount).toBe(1)
|
|
178
|
-
expect(task.retryAfter).toBeTruthy()
|
|
179
|
-
expect(task.error).toBe('Transient error')
|
|
180
|
-
|
|
181
|
-
await expect(fs.stat(path.join(tmpDir, 'processing', `${id}.json`))).rejects.toThrow()
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('applies exponential backoff (5s, 10s, 20s, cap 60s)', async () => {
|
|
185
|
-
const id = await enqueueTask(tmpDir, { action: 'push', payload: {} })
|
|
186
|
-
|
|
187
|
-
// First retry → 5s backoff
|
|
188
|
-
await dequeueTask(tmpDir)
|
|
189
|
-
const t0 = Date.now()
|
|
190
|
-
await retryTask(tmpDir, id, 'err')
|
|
191
|
-
let task = JSON.parse(await fs.readFile(path.join(tmpDir, 'pending', `${id}.json`), 'utf-8'))
|
|
192
|
-
const delay1 = new Date(task.retryAfter).getTime() - t0
|
|
193
|
-
expect(delay1).toBeGreaterThanOrEqual(4000)
|
|
194
|
-
expect(delay1).toBeLessThanOrEqual(8000)
|
|
195
|
-
|
|
196
|
-
// Manually make retryAfter past so we can dequeue again
|
|
197
|
-
task.retryAfter = new Date(Date.now() - 1000).toISOString()
|
|
198
|
-
await fs.writeFile(path.join(tmpDir, 'pending', `${id}.json`), JSON.stringify(task), 'utf-8')
|
|
199
|
-
|
|
200
|
-
// Second retry → 10s backoff
|
|
201
|
-
await dequeueTask(tmpDir)
|
|
202
|
-
const t1 = Date.now()
|
|
203
|
-
await retryTask(tmpDir, id, 'err2')
|
|
204
|
-
task = JSON.parse(await fs.readFile(path.join(tmpDir, 'pending', `${id}.json`), 'utf-8'))
|
|
205
|
-
expect(task.retryCount).toBe(2)
|
|
206
|
-
const delay2 = new Date(task.retryAfter).getTime() - t1
|
|
207
|
-
expect(delay2).toBeGreaterThanOrEqual(8000)
|
|
208
|
-
expect(delay2).toBeLessThanOrEqual(15000)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it('dequeue skips tasks whose retryAfter is in the future', async () => {
|
|
212
|
-
const id = await enqueueTask(tmpDir, { action: 'push', payload: {} })
|
|
213
|
-
|
|
214
|
-
const filePath = path.join(tmpDir, 'pending', `${id}.json`)
|
|
215
|
-
const content = JSON.parse(await fs.readFile(filePath, 'utf-8'))
|
|
216
|
-
content.retryAfter = new Date(Date.now() + 60_000).toISOString()
|
|
217
|
-
await fs.writeFile(filePath, JSON.stringify(content), 'utf-8')
|
|
218
|
-
|
|
219
|
-
expect(await dequeueTask(tmpDir)).toBeNull()
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
it('dequeue picks up tasks whose retryAfter is in the past', async () => {
|
|
223
|
-
const id = await enqueueTask(tmpDir, { action: 'push', payload: {} })
|
|
224
|
-
|
|
225
|
-
const filePath = path.join(tmpDir, 'pending', `${id}.json`)
|
|
226
|
-
const content = JSON.parse(await fs.readFile(filePath, 'utf-8'))
|
|
227
|
-
content.retryAfter = new Date(Date.now() - 1000).toISOString()
|
|
228
|
-
await fs.writeFile(filePath, JSON.stringify(content), 'utf-8')
|
|
229
|
-
|
|
230
|
-
const task = await dequeueTask(tmpDir)
|
|
231
|
-
expect(task!.id).toBe(id)
|
|
232
|
-
})
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
// ========================================================================
|
|
236
|
-
// Issue 1: Crash dedup — prevent duplicate execution
|
|
237
|
-
// ========================================================================
|
|
238
|
-
|
|
239
|
-
describe('crash dedup', () => {
|
|
240
|
-
it('dequeue skips task that already exists in completed/', async () => {
|
|
241
|
-
const id = await enqueueTask(tmpDir, { action: 'push', payload: {} })
|
|
242
|
-
|
|
243
|
-
// Simulate crash: task exists in both pending and completed
|
|
244
|
-
await fs.mkdir(path.join(tmpDir, 'completed'), { recursive: true })
|
|
245
|
-
await fs.writeFile(
|
|
246
|
-
path.join(tmpDir, 'completed', `${id}.json`),
|
|
247
|
-
JSON.stringify({
|
|
248
|
-
id,
|
|
249
|
-
action: 'push',
|
|
250
|
-
payload: {},
|
|
251
|
-
status: 'completed',
|
|
252
|
-
}),
|
|
253
|
-
'utf-8',
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
const task = await dequeueTask(tmpDir)
|
|
257
|
-
expect(task).toBeNull()
|
|
258
|
-
// Pending copy should be cleaned up
|
|
259
|
-
await expect(fs.stat(path.join(tmpDir, 'pending', `${id}.json`))).rejects.toThrow()
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
it('dequeue skips task that already exists in failed/', async () => {
|
|
263
|
-
const id = await enqueueTask(tmpDir, { action: 'push', payload: {} })
|
|
264
|
-
|
|
265
|
-
await fs.mkdir(path.join(tmpDir, 'failed'), { recursive: true })
|
|
266
|
-
await fs.writeFile(
|
|
267
|
-
path.join(tmpDir, 'failed', `${id}.json`),
|
|
268
|
-
JSON.stringify({ id, action: 'push', payload: {}, status: 'failed' }),
|
|
269
|
-
'utf-8',
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
expect(await dequeueTask(tmpDir)).toBeNull()
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
it('orphan recovery skips tasks already in completed/ (crash between write and unlink)', async () => {
|
|
276
|
-
const id = await enqueueTask(tmpDir, { action: 'push', payload: {} })
|
|
277
|
-
await dequeueTask(tmpDir)
|
|
278
|
-
|
|
279
|
-
// Simulate crash: task in both processing/ and completed/
|
|
280
|
-
await fs.mkdir(path.join(tmpDir, 'completed'), { recursive: true })
|
|
281
|
-
await fs.writeFile(
|
|
282
|
-
path.join(tmpDir, 'completed', `${id}.json`),
|
|
283
|
-
JSON.stringify({ id, status: 'completed' }),
|
|
284
|
-
'utf-8',
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
288
|
-
const recovered = await recoverOrphanedTasks(tmpDir, 0)
|
|
289
|
-
expect(recovered).toBe(0)
|
|
290
|
-
// Processing copy should be cleaned up
|
|
291
|
-
await expect(fs.stat(path.join(tmpDir, 'processing', `${id}.json`))).rejects.toThrow()
|
|
292
|
-
})
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
// ========================================================================
|
|
296
|
-
// Issue 4: Corrupted JSON handling
|
|
297
|
-
// ========================================================================
|
|
298
|
-
|
|
299
|
-
describe('corrupt file handling', () => {
|
|
300
|
-
it('dequeue moves corrupt files to corrupt/ and continues', async () => {
|
|
301
|
-
await fs.mkdir(path.join(tmpDir, 'pending'), { recursive: true })
|
|
302
|
-
await fs.writeFile(path.join(tmpDir, 'pending', 'bad.json'), 'not json {{{', 'utf-8')
|
|
303
|
-
|
|
304
|
-
const validId = await enqueueTask(tmpDir, { action: 'ok', payload: {} })
|
|
305
|
-
|
|
306
|
-
const task = await dequeueTask(tmpDir)
|
|
307
|
-
expect(task!.id).toBe(validId)
|
|
308
|
-
|
|
309
|
-
// Corrupt file moved to corrupt/
|
|
310
|
-
await expect(fs.stat(path.join(tmpDir, 'corrupt', 'bad.json'))).resolves.toBeTruthy()
|
|
311
|
-
await expect(fs.stat(path.join(tmpDir, 'pending', 'bad.json'))).rejects.toThrow()
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
it('dequeue moves files with missing required fields to corrupt/', async () => {
|
|
315
|
-
await fs.mkdir(path.join(tmpDir, 'pending'), { recursive: true })
|
|
316
|
-
// Valid JSON but missing 'id' field
|
|
317
|
-
await fs.writeFile(
|
|
318
|
-
path.join(tmpDir, 'pending', 'noid.json'),
|
|
319
|
-
JSON.stringify({ action: 'x', payload: {} }),
|
|
320
|
-
'utf-8',
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
expect(await dequeueTask(tmpDir)).toBeNull()
|
|
324
|
-
await expect(fs.stat(path.join(tmpDir, 'corrupt', 'noid.json'))).resolves.toBeTruthy()
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
it('completeTask handles corrupt processing file gracefully', async () => {
|
|
328
|
-
await fs.mkdir(path.join(tmpDir, 'processing'), { recursive: true })
|
|
329
|
-
await fs.writeFile(path.join(tmpDir, 'processing', 'bad-id.json'), 'corrupt', 'utf-8')
|
|
330
|
-
|
|
331
|
-
// Should not throw — just log and remove
|
|
332
|
-
await completeTask(tmpDir, 'bad-id', { result: true })
|
|
333
|
-
await expect(fs.stat(path.join(tmpDir, 'processing', 'bad-id.json'))).rejects.toThrow()
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
it('failTask handles corrupt processing file gracefully', async () => {
|
|
337
|
-
await fs.mkdir(path.join(tmpDir, 'processing'), { recursive: true })
|
|
338
|
-
await fs.writeFile(path.join(tmpDir, 'processing', 'bad-id.json'), '{{bad', 'utf-8')
|
|
339
|
-
|
|
340
|
-
await failTask(tmpDir, 'bad-id', 'error')
|
|
341
|
-
await expect(fs.stat(path.join(tmpDir, 'processing', 'bad-id.json'))).rejects.toThrow()
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
it('retryTask handles corrupt processing file gracefully', async () => {
|
|
345
|
-
await fs.mkdir(path.join(tmpDir, 'processing'), { recursive: true })
|
|
346
|
-
await fs.writeFile(path.join(tmpDir, 'processing', 'bad-id.json'), '{{bad', 'utf-8')
|
|
347
|
-
|
|
348
|
-
await retryTask(tmpDir, 'bad-id', 'error')
|
|
349
|
-
await expect(fs.stat(path.join(tmpDir, 'processing', 'bad-id.json'))).rejects.toThrow()
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
it('recovery moves corrupt processing files to corrupt/', async () => {
|
|
353
|
-
await fs.mkdir(path.join(tmpDir, 'processing'), { recursive: true })
|
|
354
|
-
await fs.writeFile(path.join(tmpDir, 'processing', 'bad.json'), 'broken', 'utf-8')
|
|
355
|
-
|
|
356
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
357
|
-
await recoverOrphanedTasks(tmpDir, 0)
|
|
358
|
-
|
|
359
|
-
await expect(fs.stat(path.join(tmpDir, 'corrupt', 'bad.json'))).resolves.toBeTruthy()
|
|
360
|
-
})
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
// ========================================================================
|
|
364
|
-
// Orphan recovery
|
|
365
|
-
// ========================================================================
|
|
366
|
-
|
|
367
|
-
describe('orphan recovery', () => {
|
|
368
|
-
it('moves stale tasks from processing back to pending', async () => {
|
|
369
|
-
const id = await enqueueTask(tmpDir, { action: 'push', payload: {} })
|
|
370
|
-
await dequeueTask(tmpDir)
|
|
371
|
-
|
|
372
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
373
|
-
const recovered = await recoverOrphanedTasks(tmpDir, 0)
|
|
374
|
-
expect(recovered).toBe(1)
|
|
375
|
-
|
|
376
|
-
const task = JSON.parse(
|
|
377
|
-
await fs.readFile(path.join(tmpDir, 'pending', `${id}.json`), 'utf-8'),
|
|
378
|
-
)
|
|
379
|
-
expect(task.status).toBe('pending')
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
it('leaves recent tasks in processing (not stale yet)', async () => {
|
|
383
|
-
await enqueueTask(tmpDir, { action: 'push', payload: {} })
|
|
384
|
-
await dequeueTask(tmpDir)
|
|
385
|
-
|
|
386
|
-
const recovered = await recoverOrphanedTasks(tmpDir, 60_000)
|
|
387
|
-
expect(recovered).toBe(0)
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
it('returns 0 when processing/ does not exist', async () => {
|
|
391
|
-
expect(await recoverOrphanedTasks(tmpDir)).toBe(0)
|
|
392
|
-
})
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
// ========================================================================
|
|
396
|
-
// Issue 6: Cleanup old tasks
|
|
397
|
-
// ========================================================================
|
|
398
|
-
|
|
399
|
-
describe('cleanup', () => {
|
|
400
|
-
it('removes old completed and failed tasks', async () => {
|
|
401
|
-
const id1 = await enqueueTask(tmpDir, { action: 'a', payload: {} })
|
|
402
|
-
await new Promise((r) => setTimeout(r, 5))
|
|
403
|
-
const id2 = await enqueueTask(tmpDir, { action: 'b', payload: {} })
|
|
404
|
-
await dequeueTask(tmpDir)
|
|
405
|
-
await completeTask(tmpDir, id1, {})
|
|
406
|
-
await dequeueTask(tmpDir)
|
|
407
|
-
await failTask(tmpDir, id2, 'err')
|
|
408
|
-
|
|
409
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
410
|
-
const cleaned = await cleanupOldTasks(tmpDir, 0)
|
|
411
|
-
expect(cleaned).toBe(2)
|
|
412
|
-
|
|
413
|
-
await expect(fs.stat(path.join(tmpDir, 'completed', `${id1}.json`))).rejects.toThrow()
|
|
414
|
-
await expect(fs.stat(path.join(tmpDir, 'failed', `${id2}.json`))).rejects.toThrow()
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
it('keeps recent tasks', async () => {
|
|
418
|
-
const id = await enqueueTask(tmpDir, { action: 'a', payload: {} })
|
|
419
|
-
await dequeueTask(tmpDir)
|
|
420
|
-
await completeTask(tmpDir, id, {})
|
|
421
|
-
|
|
422
|
-
const cleaned = await cleanupOldTasks(tmpDir, 60 * 60_000)
|
|
423
|
-
expect(cleaned).toBe(0)
|
|
424
|
-
})
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
// ========================================================================
|
|
428
|
-
// Query APIs (for editor UI)
|
|
429
|
-
// ========================================================================
|
|
430
|
-
|
|
431
|
-
describe('getTask', () => {
|
|
432
|
-
it('finds task in any status directory', async () => {
|
|
433
|
-
const id = await enqueueTask(tmpDir, { action: 'push', payload: {} })
|
|
434
|
-
expect((await getTask(tmpDir, id))!.status).toBe('pending')
|
|
435
|
-
|
|
436
|
-
await dequeueTask(tmpDir)
|
|
437
|
-
expect((await getTask(tmpDir, id))!.status).toBe('processing')
|
|
438
|
-
|
|
439
|
-
await completeTask(tmpDir, id, { ok: true })
|
|
440
|
-
expect((await getTask(tmpDir, id))!.status).toBe('completed')
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
it('returns null for unknown task', async () => {
|
|
444
|
-
expect(await getTask(tmpDir, 'nonexistent')).toBeNull()
|
|
445
|
-
})
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
describe('listTasks', () => {
|
|
449
|
-
it('returns tasks sorted by createdAt', async () => {
|
|
450
|
-
const id1 = await enqueueTask(tmpDir, { action: 'a', payload: {} })
|
|
451
|
-
await new Promise((r) => setTimeout(r, 5))
|
|
452
|
-
const id2 = await enqueueTask(tmpDir, { action: 'b', payload: {} })
|
|
453
|
-
|
|
454
|
-
const tasks = await listTasks(tmpDir, 'pending')
|
|
455
|
-
expect(tasks).toHaveLength(2)
|
|
456
|
-
expect(tasks[0].id).toBe(id1)
|
|
457
|
-
expect(tasks[1].id).toBe(id2)
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
it('respects limit parameter', async () => {
|
|
461
|
-
await enqueueTask(tmpDir, { action: 'a', payload: {} })
|
|
462
|
-
await enqueueTask(tmpDir, { action: 'b', payload: {} })
|
|
463
|
-
await enqueueTask(tmpDir, { action: 'c', payload: {} })
|
|
464
|
-
|
|
465
|
-
const tasks = await listTasks(tmpDir, 'pending', 2)
|
|
466
|
-
expect(tasks).toHaveLength(2)
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
it('returns empty array for nonexistent status directory', async () => {
|
|
470
|
-
expect(await listTasks(tmpDir, 'completed')).toEqual([])
|
|
471
|
-
})
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
describe('getQueueStats', () => {
|
|
475
|
-
it('returns counts for each status', async () => {
|
|
476
|
-
await enqueueTask(tmpDir, { action: 'a', payload: {} })
|
|
477
|
-
await enqueueTask(tmpDir, { action: 'b', payload: {} })
|
|
478
|
-
await enqueueTask(tmpDir, { action: 'c', payload: {} }) // stays pending
|
|
479
|
-
const task1 = await dequeueTask(tmpDir)
|
|
480
|
-
await completeTask(tmpDir, task1!.id, {})
|
|
481
|
-
const task2 = await dequeueTask(tmpDir)
|
|
482
|
-
await failTask(tmpDir, task2!.id, 'err')
|
|
483
|
-
|
|
484
|
-
const stats = await getQueueStats(tmpDir)
|
|
485
|
-
expect(stats.pending).toBe(1)
|
|
486
|
-
expect(stats.processing).toBe(0)
|
|
487
|
-
expect(stats.completed).toBe(1)
|
|
488
|
-
expect(stats.failed).toBe(1)
|
|
489
|
-
expect(stats.corrupt).toBe(0)
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
it('returns all zeros for empty queue', async () => {
|
|
493
|
-
const stats = await getQueueStats(tmpDir)
|
|
494
|
-
expect(stats).toEqual({
|
|
495
|
-
pending: 0,
|
|
496
|
-
processing: 0,
|
|
497
|
-
completed: 0,
|
|
498
|
-
failed: 0,
|
|
499
|
-
corrupt: 0,
|
|
500
|
-
})
|
|
501
|
-
})
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
// ========================================================================
|
|
505
|
-
// Logger interface
|
|
506
|
-
// ========================================================================
|
|
507
|
-
|
|
508
|
-
describe('logger', () => {
|
|
509
|
-
it('calls logger.debug when provided', async () => {
|
|
510
|
-
const messages: string[] = []
|
|
511
|
-
const logger = { debug: (msg: string) => messages.push(msg) }
|
|
512
|
-
|
|
513
|
-
await enqueueTask(tmpDir, { action: 'x', payload: {} }, logger)
|
|
514
|
-
expect(messages).toContain('Enqueued task')
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
it('works silently without a logger', async () => {
|
|
518
|
-
// Should not throw
|
|
519
|
-
const id = await enqueueTask(tmpDir, { action: 'x', payload: {} })
|
|
520
|
-
await dequeueTask(tmpDir)
|
|
521
|
-
await completeTask(tmpDir, id, {})
|
|
522
|
-
})
|
|
523
|
-
})
|
|
524
|
-
})
|