grepmind-core 0.1.0-alpha
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/LICENSE +190 -0
- package/dist/config/types.d.ts +174 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +137 -0
- package/dist/config/types.js.map +1 -0
- package/dist/git.d.ts +98 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +298 -0
- package/dist/git.js.map +1 -0
- package/dist/git.test.d.ts +7 -0
- package/dist/git.test.d.ts.map +1 -0
- package/dist/git.test.js +242 -0
- package/dist/git.test.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/branch.d.ts +121 -0
- package/dist/indexer/branch.d.ts.map +1 -0
- package/dist/indexer/branch.js +451 -0
- package/dist/indexer/branch.js.map +1 -0
- package/dist/indexer/chunker.d.ts +9 -0
- package/dist/indexer/chunker.d.ts.map +1 -0
- package/dist/indexer/chunker.js +70 -0
- package/dist/indexer/chunker.js.map +1 -0
- package/dist/indexer/chunker.test.d.ts +2 -0
- package/dist/indexer/chunker.test.d.ts.map +1 -0
- package/dist/indexer/chunker.test.js +180 -0
- package/dist/indexer/chunker.test.js.map +1 -0
- package/dist/indexer/code/branch.d.ts +155 -0
- package/dist/indexer/code/branch.d.ts.map +1 -0
- package/dist/indexer/code/branch.js +550 -0
- package/dist/indexer/code/branch.js.map +1 -0
- package/dist/indexer/code/branch.test.d.ts +7 -0
- package/dist/indexer/code/branch.test.d.ts.map +1 -0
- package/dist/indexer/code/branch.test.js +241 -0
- package/dist/indexer/code/branch.test.js.map +1 -0
- package/dist/indexer/code/chunker.d.ts +61 -0
- package/dist/indexer/code/chunker.d.ts.map +1 -0
- package/dist/indexer/code/chunker.js +311 -0
- package/dist/indexer/code/chunker.js.map +1 -0
- package/dist/indexer/code/chunker.test.d.ts +2 -0
- package/dist/indexer/code/chunker.test.d.ts.map +1 -0
- package/dist/indexer/code/chunker.test.js +552 -0
- package/dist/indexer/code/chunker.test.js.map +1 -0
- package/dist/indexer/code/fts.test.d.ts +2 -0
- package/dist/indexer/code/fts.test.d.ts.map +1 -0
- package/dist/indexer/code/fts.test.js +14 -0
- package/dist/indexer/code/fts.test.js.map +1 -0
- package/dist/indexer/code/graph/embedded.d.ts +11 -0
- package/dist/indexer/code/graph/embedded.d.ts.map +1 -0
- package/dist/indexer/code/graph/embedded.js +152 -0
- package/dist/indexer/code/graph/embedded.js.map +1 -0
- package/dist/indexer/code/graph/embedded.test.d.ts +2 -0
- package/dist/indexer/code/graph/embedded.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/embedded.test.js +105 -0
- package/dist/indexer/code/graph/embedded.test.js.map +1 -0
- package/dist/indexer/code/graph/facts.d.ts +11 -0
- package/dist/indexer/code/graph/facts.d.ts.map +1 -0
- package/dist/indexer/code/graph/facts.js +456 -0
- package/dist/indexer/code/graph/facts.js.map +1 -0
- package/dist/indexer/code/graph/facts.test.d.ts +2 -0
- package/dist/indexer/code/graph/facts.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/facts.test.js +181 -0
- package/dist/indexer/code/graph/facts.test.js.map +1 -0
- package/dist/indexer/code/graph/id.d.ts +14 -0
- package/dist/indexer/code/graph/id.d.ts.map +1 -0
- package/dist/indexer/code/graph/id.js +40 -0
- package/dist/indexer/code/graph/id.js.map +1 -0
- package/dist/indexer/code/graph/id.test.d.ts +2 -0
- package/dist/indexer/code/graph/id.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/id.test.js +86 -0
- package/dist/indexer/code/graph/id.test.js.map +1 -0
- package/dist/indexer/code/graph/index.d.ts +133 -0
- package/dist/indexer/code/graph/index.d.ts.map +1 -0
- package/dist/indexer/code/graph/index.js +1876 -0
- package/dist/indexer/code/graph/index.js.map +1 -0
- package/dist/indexer/code/graph/index.test.d.ts +2 -0
- package/dist/indexer/code/graph/index.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/index.test.js +210 -0
- package/dist/indexer/code/graph/index.test.js.map +1 -0
- package/dist/indexer/code/graph/queries.d.ts +22 -0
- package/dist/indexer/code/graph/queries.d.ts.map +1 -0
- package/dist/indexer/code/graph/queries.js +79 -0
- package/dist/indexer/code/graph/queries.js.map +1 -0
- package/dist/indexer/code/graph/queries.test.d.ts +2 -0
- package/dist/indexer/code/graph/queries.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/queries.test.js +108 -0
- package/dist/indexer/code/graph/queries.test.js.map +1 -0
- package/dist/indexer/code/graph/resolver.d.ts +136 -0
- package/dist/indexer/code/graph/resolver.d.ts.map +1 -0
- package/dist/indexer/code/graph/resolver.js +839 -0
- package/dist/indexer/code/graph/resolver.js.map +1 -0
- package/dist/indexer/code/graph/resolver.test.d.ts +2 -0
- package/dist/indexer/code/graph/resolver.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/resolver.test.js +482 -0
- package/dist/indexer/code/graph/resolver.test.js.map +1 -0
- package/dist/indexer/code/graph/semantic.d.ts +33 -0
- package/dist/indexer/code/graph/semantic.d.ts.map +1 -0
- package/dist/indexer/code/graph/semantic.js +279 -0
- package/dist/indexer/code/graph/semantic.js.map +1 -0
- package/dist/indexer/code/graph/semantic.test.d.ts +2 -0
- package/dist/indexer/code/graph/semantic.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/semantic.test.js +127 -0
- package/dist/indexer/code/graph/semantic.test.js.map +1 -0
- package/dist/indexer/code/index.d.ts +404 -0
- package/dist/indexer/code/index.d.ts.map +1 -0
- package/dist/indexer/code/index.js +2070 -0
- package/dist/indexer/code/index.js.map +1 -0
- package/dist/indexer/code/languages/bash.d.ts +14 -0
- package/dist/indexer/code/languages/bash.d.ts.map +1 -0
- package/dist/indexer/code/languages/bash.js +125 -0
- package/dist/indexer/code/languages/bash.js.map +1 -0
- package/dist/indexer/code/languages/css.d.ts +16 -0
- package/dist/indexer/code/languages/css.d.ts.map +1 -0
- package/dist/indexer/code/languages/css.js +204 -0
- package/dist/indexer/code/languages/css.js.map +1 -0
- package/dist/indexer/code/languages/generic.d.ts +61 -0
- package/dist/indexer/code/languages/generic.d.ts.map +1 -0
- package/dist/indexer/code/languages/generic.js +150 -0
- package/dist/indexer/code/languages/generic.js.map +1 -0
- package/dist/indexer/code/languages/graphql.d.ts +13 -0
- package/dist/indexer/code/languages/graphql.d.ts.map +1 -0
- package/dist/indexer/code/languages/graphql.js +180 -0
- package/dist/indexer/code/languages/graphql.js.map +1 -0
- package/dist/indexer/code/languages/html.d.ts +16 -0
- package/dist/indexer/code/languages/html.d.ts.map +1 -0
- package/dist/indexer/code/languages/html.js +138 -0
- package/dist/indexer/code/languages/html.js.map +1 -0
- package/dist/indexer/code/languages/index.d.ts +9 -0
- package/dist/indexer/code/languages/index.d.ts.map +1 -0
- package/dist/indexer/code/languages/index.js +12 -0
- package/dist/indexer/code/languages/index.js.map +1 -0
- package/dist/indexer/code/languages/json.d.ts +12 -0
- package/dist/indexer/code/languages/json.d.ts.map +1 -0
- package/dist/indexer/code/languages/json.js +66 -0
- package/dist/indexer/code/languages/json.js.map +1 -0
- package/dist/indexer/code/languages/registry.d.ts +78 -0
- package/dist/indexer/code/languages/registry.d.ts.map +1 -0
- package/dist/indexer/code/languages/registry.js +72 -0
- package/dist/indexer/code/languages/registry.js.map +1 -0
- package/dist/indexer/code/languages/typescript.d.ts +39 -0
- package/dist/indexer/code/languages/typescript.d.ts.map +1 -0
- package/dist/indexer/code/languages/typescript.js +300 -0
- package/dist/indexer/code/languages/typescript.js.map +1 -0
- package/dist/indexer/code/languages/yaml.d.ts +13 -0
- package/dist/indexer/code/languages/yaml.d.ts.map +1 -0
- package/dist/indexer/code/languages/yaml.js +90 -0
- package/dist/indexer/code/languages/yaml.js.map +1 -0
- package/dist/indexer/code/parser.d.ts +26 -0
- package/dist/indexer/code/parser.d.ts.map +1 -0
- package/dist/indexer/code/parser.js +332 -0
- package/dist/indexer/code/parser.js.map +1 -0
- package/dist/indexer/code/retry.d.ts +58 -0
- package/dist/indexer/code/retry.d.ts.map +1 -0
- package/dist/indexer/code/retry.js +192 -0
- package/dist/indexer/code/retry.js.map +1 -0
- package/dist/indexer/code/tree/builder.d.ts +30 -0
- package/dist/indexer/code/tree/builder.d.ts.map +1 -0
- package/dist/indexer/code/tree/builder.js +132 -0
- package/dist/indexer/code/tree/builder.js.map +1 -0
- package/dist/indexer/code/tree/builder.test.d.ts +2 -0
- package/dist/indexer/code/tree/builder.test.d.ts.map +1 -0
- package/dist/indexer/code/tree/builder.test.js +31 -0
- package/dist/indexer/code/tree/builder.test.js.map +1 -0
- package/dist/indexer/code/tree/cache.d.ts +22 -0
- package/dist/indexer/code/tree/cache.d.ts.map +1 -0
- package/dist/indexer/code/tree/cache.js +85 -0
- package/dist/indexer/code/tree/cache.js.map +1 -0
- package/dist/indexer/code/tree/context.d.ts +32 -0
- package/dist/indexer/code/tree/context.d.ts.map +1 -0
- package/dist/indexer/code/tree/context.js +78 -0
- package/dist/indexer/code/tree/context.js.map +1 -0
- package/dist/indexer/code/tree/embedding.d.ts +9 -0
- package/dist/indexer/code/tree/embedding.d.ts.map +1 -0
- package/dist/indexer/code/tree/embedding.js +53 -0
- package/dist/indexer/code/tree/embedding.js.map +1 -0
- package/dist/indexer/code/tree/embedding.test.d.ts +2 -0
- package/dist/indexer/code/tree/embedding.test.d.ts.map +1 -0
- package/dist/indexer/code/tree/embedding.test.js +57 -0
- package/dist/indexer/code/tree/embedding.test.js.map +1 -0
- package/dist/indexer/code/tree/id.d.ts +3 -0
- package/dist/indexer/code/tree/id.d.ts.map +1 -0
- package/dist/indexer/code/tree/id.js +8 -0
- package/dist/indexer/code/tree/id.js.map +1 -0
- package/dist/indexer/code/tree/index.d.ts +113 -0
- package/dist/indexer/code/tree/index.d.ts.map +1 -0
- package/dist/indexer/code/tree/index.js +1146 -0
- package/dist/indexer/code/tree/index.js.map +1 -0
- package/dist/indexer/code/tree/rename.d.ts +13 -0
- package/dist/indexer/code/tree/rename.d.ts.map +1 -0
- package/dist/indexer/code/tree/rename.js +46 -0
- package/dist/indexer/code/tree/rename.js.map +1 -0
- package/dist/indexer/code/tree/repomap.d.ts +29 -0
- package/dist/indexer/code/tree/repomap.d.ts.map +1 -0
- package/dist/indexer/code/tree/repomap.js +95 -0
- package/dist/indexer/code/tree/repomap.js.map +1 -0
- package/dist/indexer/code/tree/repomap.test.d.ts +2 -0
- package/dist/indexer/code/tree/repomap.test.d.ts.map +1 -0
- package/dist/indexer/code/tree/repomap.test.js +93 -0
- package/dist/indexer/code/tree/repomap.test.js.map +1 -0
- package/dist/indexer/code/tree/stats.d.ts +26 -0
- package/dist/indexer/code/tree/stats.d.ts.map +1 -0
- package/dist/indexer/code/tree/stats.js +49 -0
- package/dist/indexer/code/tree/stats.js.map +1 -0
- package/dist/indexer/code/tree/types.d.ts +186 -0
- package/dist/indexer/code/tree/types.d.ts.map +1 -0
- package/dist/indexer/code/tree/types.js +10 -0
- package/dist/indexer/code/tree/types.js.map +1 -0
- package/dist/indexer/code/wal.d.ts +144 -0
- package/dist/indexer/code/wal.d.ts.map +1 -0
- package/dist/indexer/code/wal.js +283 -0
- package/dist/indexer/code/wal.js.map +1 -0
- package/dist/indexer/embeddings.d.ts +113 -0
- package/dist/indexer/embeddings.d.ts.map +1 -0
- package/dist/indexer/embeddings.js +477 -0
- package/dist/indexer/embeddings.js.map +1 -0
- package/dist/indexer/git-sync.d.ts +117 -0
- package/dist/indexer/git-sync.d.ts.map +1 -0
- package/dist/indexer/git-sync.js +398 -0
- package/dist/indexer/git-sync.js.map +1 -0
- package/dist/indexer/index.d.ts +175 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +1096 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/mocks/mock-reranker.d.ts +12 -0
- package/dist/indexer/mocks/mock-reranker.d.ts.map +1 -0
- package/dist/indexer/mocks/mock-reranker.js +26 -0
- package/dist/indexer/mocks/mock-reranker.js.map +1 -0
- package/dist/indexer/parser.d.ts +8 -0
- package/dist/indexer/parser.d.ts.map +1 -0
- package/dist/indexer/parser.js +44 -0
- package/dist/indexer/parser.js.map +1 -0
- package/dist/indexer/parser.test.d.ts +2 -0
- package/dist/indexer/parser.test.d.ts.map +1 -0
- package/dist/indexer/parser.test.js +197 -0
- package/dist/indexer/parser.test.js.map +1 -0
- package/dist/indexer/reranking.d.ts +71 -0
- package/dist/indexer/reranking.d.ts.map +1 -0
- package/dist/indexer/reranking.integration.test.d.ts +2 -0
- package/dist/indexer/reranking.integration.test.d.ts.map +1 -0
- package/dist/indexer/reranking.integration.test.js +104 -0
- package/dist/indexer/reranking.integration.test.js.map +1 -0
- package/dist/indexer/reranking.js +256 -0
- package/dist/indexer/reranking.js.map +1 -0
- package/dist/indexer/reranking.test.d.ts +2 -0
- package/dist/indexer/reranking.test.d.ts.map +1 -0
- package/dist/indexer/reranking.test.js +130 -0
- package/dist/indexer/reranking.test.js.map +1 -0
- package/dist/indexer/wal/file-storage.d.ts +60 -0
- package/dist/indexer/wal/file-storage.d.ts.map +1 -0
- package/dist/indexer/wal/file-storage.js +277 -0
- package/dist/indexer/wal/file-storage.js.map +1 -0
- package/dist/indexer/wal/file-storage.test.d.ts +8 -0
- package/dist/indexer/wal/file-storage.test.d.ts.map +1 -0
- package/dist/indexer/wal/file-storage.test.js +444 -0
- package/dist/indexer/wal/file-storage.test.js.map +1 -0
- package/dist/indexer/wal/index.d.ts +41 -0
- package/dist/indexer/wal/index.d.ts.map +1 -0
- package/dist/indexer/wal/index.js +61 -0
- package/dist/indexer/wal/index.js.map +1 -0
- package/dist/indexer/wal/integration.test.d.ts +11 -0
- package/dist/indexer/wal/integration.test.d.ts.map +1 -0
- package/dist/indexer/wal/integration.test.js +378 -0
- package/dist/indexer/wal/integration.test.js.map +1 -0
- package/dist/indexer/wal/lancedb-storage.d.ts +72 -0
- package/dist/indexer/wal/lancedb-storage.d.ts.map +1 -0
- package/dist/indexer/wal/lancedb-storage.js +462 -0
- package/dist/indexer/wal/lancedb-storage.js.map +1 -0
- package/dist/indexer/wal/lancedb-storage.test.d.ts +8 -0
- package/dist/indexer/wal/lancedb-storage.test.d.ts.map +1 -0
- package/dist/indexer/wal/lancedb-storage.test.js +415 -0
- package/dist/indexer/wal/lancedb-storage.test.js.map +1 -0
- package/dist/indexer/wal/sync-wal.d.ts +144 -0
- package/dist/indexer/wal/sync-wal.d.ts.map +1 -0
- package/dist/indexer/wal/sync-wal.js +863 -0
- package/dist/indexer/wal/sync-wal.js.map +1 -0
- package/dist/indexer/wal/sync-wal.test.d.ts +8 -0
- package/dist/indexer/wal/sync-wal.test.d.ts.map +1 -0
- package/dist/indexer/wal/sync-wal.test.js +752 -0
- package/dist/indexer/wal/sync-wal.test.js.map +1 -0
- package/dist/indexer/wal/types.d.ts +167 -0
- package/dist/indexer/wal/types.d.ts.map +1 -0
- package/dist/indexer/wal/types.js +12 -0
- package/dist/indexer/wal/types.js.map +1 -0
- package/dist/indexer/watcher.d.ts +36 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +110 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/search/explore.d.ts +62 -0
- package/dist/search/explore.d.ts.map +1 -0
- package/dist/search/explore.js +111 -0
- package/dist/search/explore.js.map +1 -0
- package/dist/search/fts.d.ts +23 -0
- package/dist/search/fts.d.ts.map +1 -0
- package/dist/search/fts.js +64 -0
- package/dist/search/fts.js.map +1 -0
- package/dist/search/fts.test.d.ts +2 -0
- package/dist/search/fts.test.d.ts.map +1 -0
- package/dist/search/fts.test.js +27 -0
- package/dist/search/fts.test.js.map +1 -0
- package/dist/search/grep.d.ts +75 -0
- package/dist/search/grep.d.ts.map +1 -0
- package/dist/search/grep.js +96 -0
- package/dist/search/grep.js.map +1 -0
- package/dist/search/grep.test.d.ts +2 -0
- package/dist/search/grep.test.d.ts.map +1 -0
- package/dist/search/grep.test.js +178 -0
- package/dist/search/grep.test.js.map +1 -0
- package/dist/search/hybrid-grep.d.ts +43 -0
- package/dist/search/hybrid-grep.d.ts.map +1 -0
- package/dist/search/hybrid-grep.js +130 -0
- package/dist/search/hybrid-grep.js.map +1 -0
- package/dist/search/hybrid-grep.test.d.ts +2 -0
- package/dist/search/hybrid-grep.test.d.ts.map +1 -0
- package/dist/search/hybrid-grep.test.js +133 -0
- package/dist/search/hybrid-grep.test.js.map +1 -0
- package/dist/search/rg-executor.d.ts +63 -0
- package/dist/search/rg-executor.d.ts.map +1 -0
- package/dist/search/rg-executor.js +146 -0
- package/dist/search/rg-executor.js.map +1 -0
- package/dist/search/rg-executor.test.d.ts +2 -0
- package/dist/search/rg-executor.test.d.ts.map +1 -0
- package/dist/search/rg-executor.test.js +104 -0
- package/dist/search/rg-executor.test.js.map +1 -0
- package/dist/search/rg-parser/extractor.d.ts +14 -0
- package/dist/search/rg-parser/extractor.d.ts.map +1 -0
- package/dist/search/rg-parser/extractor.js +82 -0
- package/dist/search/rg-parser/extractor.js.map +1 -0
- package/dist/search/rg-parser/extractor.test.d.ts +2 -0
- package/dist/search/rg-parser/extractor.test.d.ts.map +1 -0
- package/dist/search/rg-parser/extractor.test.js +35 -0
- package/dist/search/rg-parser/extractor.test.js.map +1 -0
- package/dist/search/rg-parser/fts-builder.d.ts +7 -0
- package/dist/search/rg-parser/fts-builder.d.ts.map +1 -0
- package/dist/search/rg-parser/fts-builder.js +18 -0
- package/dist/search/rg-parser/fts-builder.js.map +1 -0
- package/dist/search/rg-parser/fts-builder.test.d.ts +2 -0
- package/dist/search/rg-parser/fts-builder.test.d.ts.map +1 -0
- package/dist/search/rg-parser/fts-builder.test.js +26 -0
- package/dist/search/rg-parser/fts-builder.test.js.map +1 -0
- package/dist/search/rg-parser/index.d.ts +36 -0
- package/dist/search/rg-parser/index.d.ts.map +1 -0
- package/dist/search/rg-parser/index.js +83 -0
- package/dist/search/rg-parser/index.js.map +1 -0
- package/dist/search/rg-parser/index.test.d.ts +2 -0
- package/dist/search/rg-parser/index.test.d.ts.map +1 -0
- package/dist/search/rg-parser/index.test.js +34 -0
- package/dist/search/rg-parser/index.test.js.map +1 -0
- package/dist/search/rg-parser/strategy.d.ts +14 -0
- package/dist/search/rg-parser/strategy.d.ts.map +1 -0
- package/dist/search/rg-parser/strategy.js +31 -0
- package/dist/search/rg-parser/strategy.js.map +1 -0
- package/dist/search/rg-parser/strategy.test.d.ts +2 -0
- package/dist/search/rg-parser/strategy.test.d.ts.map +1 -0
- package/dist/search/rg-parser/strategy.test.js +29 -0
- package/dist/search/rg-parser/strategy.test.js.map +1 -0
- package/dist/types.d.ts +345 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/vault.d.ts +84 -0
- package/dist/utils/vault.d.ts.map +1 -0
- package/dist/utils/vault.js +138 -0
- package/dist/utils/vault.js.map +1 -0
- package/dist/utils/vault.test.d.ts +2 -0
- package/dist/utils/vault.test.d.ts.map +1 -0
- package/dist/utils/vault.test.js +153 -0
- package/dist/utils/vault.test.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncWal Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for SyncWal business logic.
|
|
5
|
+
* Covers: task lifecycle, checkpoint, recovery, lock management, cancellation.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
8
|
+
import * as fs from 'node:fs/promises';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import { SyncWal } from './sync-wal.js';
|
|
12
|
+
import { FileWalStorage } from './file-storage.js';
|
|
13
|
+
describe('SyncWal', () => {
|
|
14
|
+
let tempDir;
|
|
15
|
+
let storage;
|
|
16
|
+
let syncWal;
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sync-wal-test-'));
|
|
19
|
+
storage = new FileWalStorage(tempDir);
|
|
20
|
+
// Use small checkpoint interval for testing
|
|
21
|
+
syncWal = new SyncWal(storage, { checkpointInterval: 5 });
|
|
22
|
+
});
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
// Cleanup
|
|
25
|
+
syncWal.stopHeartbeat();
|
|
26
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
+
// INITIALIZATION
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
31
|
+
describe('Initialization', () => {
|
|
32
|
+
it('should initialize with no recovery plan when no previous task', async () => {
|
|
33
|
+
const plan = await syncWal.initialize();
|
|
34
|
+
expect(plan).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
it('should restore LSN from manifest', async () => {
|
|
37
|
+
// Setup manifest with high LSN
|
|
38
|
+
await storage.writeManifest({
|
|
39
|
+
version: 1,
|
|
40
|
+
current: null,
|
|
41
|
+
lastCompleted: null,
|
|
42
|
+
walLsn: 100,
|
|
43
|
+
lastCheckpointLsn: 50,
|
|
44
|
+
});
|
|
45
|
+
await syncWal.initialize();
|
|
46
|
+
// Start a task and check LSN is restored
|
|
47
|
+
const taskId = await syncWal.startTask('notes', 10, 'hash');
|
|
48
|
+
// The new entry should have LSN >= 100
|
|
49
|
+
const entries = await storage.getAllEntries();
|
|
50
|
+
expect(entries[0].lsn).toBeGreaterThanOrEqual(100);
|
|
51
|
+
await syncWal.completeTask(taskId);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
55
|
+
// TASK LIFECYCLE
|
|
56
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
57
|
+
describe('Task Lifecycle', () => {
|
|
58
|
+
it('should start a task and acquire lock', async () => {
|
|
59
|
+
const taskId = await syncWal.startTask('notes', 100, 'manifest-hash');
|
|
60
|
+
expect(taskId).toBeTruthy();
|
|
61
|
+
expect(syncWal.isRunning()).toBe(true);
|
|
62
|
+
expect(syncWal.getCurrentTaskId()).toBe(taskId);
|
|
63
|
+
// Lock should be acquired
|
|
64
|
+
const lock = await storage.getLock();
|
|
65
|
+
expect(lock).not.toBeNull();
|
|
66
|
+
expect(lock?.id).toBe(taskId);
|
|
67
|
+
expect(lock?.type).toBe('notes');
|
|
68
|
+
await syncWal.completeTask(taskId);
|
|
69
|
+
});
|
|
70
|
+
it('should write task:start entry to WAL', async () => {
|
|
71
|
+
const taskId = await syncWal.startTask('notes', 100, 'hash123');
|
|
72
|
+
const entries = await storage.getAllEntries();
|
|
73
|
+
expect(entries).toHaveLength(1);
|
|
74
|
+
expect(entries[0].op).toBe('task:start');
|
|
75
|
+
expect(entries[0].taskId).toBe(taskId);
|
|
76
|
+
expect(entries[0].data?.type).toBe('notes');
|
|
77
|
+
expect(entries[0].data?.totalFiles).toBe(100);
|
|
78
|
+
expect(entries[0].data?.manifestHash).toBe('hash123');
|
|
79
|
+
await syncWal.completeTask(taskId);
|
|
80
|
+
});
|
|
81
|
+
it('should create manifest with current task', async () => {
|
|
82
|
+
const taskId = await syncWal.startTask('notes', 100);
|
|
83
|
+
const manifest = await storage.getManifest();
|
|
84
|
+
expect(manifest?.current).not.toBeNull();
|
|
85
|
+
expect(manifest?.current?.id).toBe(taskId);
|
|
86
|
+
expect(manifest?.current?.type).toBe('notes');
|
|
87
|
+
expect(manifest?.current?.status).toBe('running');
|
|
88
|
+
expect(manifest?.current?.totalFiles).toBe(100);
|
|
89
|
+
await syncWal.completeTask(taskId);
|
|
90
|
+
});
|
|
91
|
+
it('should fail to start second task when one is running', async () => {
|
|
92
|
+
const taskId1 = await syncWal.startTask('notes', 100);
|
|
93
|
+
await expect(syncWal.startTask('code', 50)).rejects.toThrow(/another sync is already running/);
|
|
94
|
+
await syncWal.completeTask(taskId1);
|
|
95
|
+
});
|
|
96
|
+
it('should complete task and release lock', async () => {
|
|
97
|
+
const taskId = await syncWal.startTask('notes', 100);
|
|
98
|
+
await syncWal.completeTask(taskId);
|
|
99
|
+
expect(syncWal.isRunning()).toBe(false);
|
|
100
|
+
expect(await storage.getLock()).toBeNull();
|
|
101
|
+
// Manifest should have lastCompleted
|
|
102
|
+
const manifest = await storage.getManifest();
|
|
103
|
+
expect(manifest?.current).toBeNull();
|
|
104
|
+
expect(manifest?.lastCompleted).not.toBeNull();
|
|
105
|
+
expect(manifest?.lastCompleted?.id).toBe(taskId);
|
|
106
|
+
expect(manifest?.lastCompleted?.status).toBe('completed');
|
|
107
|
+
});
|
|
108
|
+
it('should fail task and release lock', async () => {
|
|
109
|
+
const taskId = await syncWal.startTask('notes', 100);
|
|
110
|
+
await syncWal.failTask(taskId, 'Something went wrong');
|
|
111
|
+
expect(syncWal.isRunning()).toBe(false);
|
|
112
|
+
expect(await storage.getLock()).toBeNull();
|
|
113
|
+
const manifest = await storage.getManifest();
|
|
114
|
+
expect(manifest?.lastCompleted?.status).toBe('failed');
|
|
115
|
+
});
|
|
116
|
+
it('should cancel task and release lock', async () => {
|
|
117
|
+
const taskId = await syncWal.startTask('notes', 100);
|
|
118
|
+
await syncWal.cancelTask(taskId);
|
|
119
|
+
expect(syncWal.isRunning()).toBe(false);
|
|
120
|
+
expect(await storage.getLock()).toBeNull();
|
|
121
|
+
const manifest = await storage.getManifest();
|
|
122
|
+
expect(manifest?.lastCompleted?.status).toBe('cancelled');
|
|
123
|
+
});
|
|
124
|
+
it('should throw when completing non-active task', async () => {
|
|
125
|
+
const taskId = await syncWal.startTask('notes', 100);
|
|
126
|
+
await syncWal.completeTask(taskId);
|
|
127
|
+
await expect(syncWal.completeTask(taskId)).rejects.toThrow(/Task not active/);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
131
|
+
// FILE PROCESSING
|
|
132
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
133
|
+
describe('File Processing', () => {
|
|
134
|
+
it('should record file:done entry', async () => {
|
|
135
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
136
|
+
await syncWal.fileProcessed(taskId, '/test.md', 3);
|
|
137
|
+
const entries = await storage.getAllEntries();
|
|
138
|
+
const fileEntry = entries.find((e) => e.op === 'file:done');
|
|
139
|
+
expect(fileEntry).toBeDefined();
|
|
140
|
+
expect(fileEntry?.data?.path).toBe('/test.md');
|
|
141
|
+
expect(fileEntry?.data?.chunks).toBe(3);
|
|
142
|
+
await syncWal.completeTask(taskId);
|
|
143
|
+
});
|
|
144
|
+
it('should track added vs updated files', async () => {
|
|
145
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
146
|
+
await syncWal.fileProcessed(taskId, '/new.md', 1, 'added');
|
|
147
|
+
await syncWal.fileProcessed(taskId, '/existing.md', 1, 'updated');
|
|
148
|
+
const progress = syncWal.getProgress();
|
|
149
|
+
expect(progress?.added).toBe(1);
|
|
150
|
+
expect(progress?.updated).toBe(1);
|
|
151
|
+
await syncWal.completeTask(taskId);
|
|
152
|
+
});
|
|
153
|
+
it('should record file:fail entry', async () => {
|
|
154
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
155
|
+
await syncWal.fileFailed(taskId, '/bad.md', 'Parse error', 3);
|
|
156
|
+
const entries = await storage.getAllEntries();
|
|
157
|
+
const failEntry = entries.find((e) => e.op === 'file:fail');
|
|
158
|
+
expect(failEntry).toBeDefined();
|
|
159
|
+
expect(failEntry?.data?.path).toBe('/bad.md');
|
|
160
|
+
expect(failEntry?.data?.error).toBe('Parse error');
|
|
161
|
+
expect(failEntry?.data?.retries).toBe(3);
|
|
162
|
+
const progress = syncWal.getProgress();
|
|
163
|
+
expect(progress?.failedCount).toBe(1);
|
|
164
|
+
await syncWal.completeTask(taskId);
|
|
165
|
+
});
|
|
166
|
+
it('should record deleted files', async () => {
|
|
167
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
168
|
+
await syncWal.fileDeleted(taskId, '/removed.md');
|
|
169
|
+
const progress = syncWal.getProgress();
|
|
170
|
+
expect(progress?.deleted).toBe(1);
|
|
171
|
+
await syncWal.completeTask(taskId);
|
|
172
|
+
});
|
|
173
|
+
it('should throw when processing file for non-active task', async () => {
|
|
174
|
+
await expect(syncWal.fileProcessed('non-existent', '/test.md', 1)).rejects.toThrow(/Task not active/);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
178
|
+
// CHECKPOINT
|
|
179
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
180
|
+
describe('Checkpoint', () => {
|
|
181
|
+
it('should create checkpoint after configured interval', async () => {
|
|
182
|
+
// Using checkpointInterval: 5 from config
|
|
183
|
+
const taskId = await syncWal.startTask('notes', 20);
|
|
184
|
+
// Process 5 files to trigger checkpoint
|
|
185
|
+
for (let i = 0; i < 5; i++) {
|
|
186
|
+
await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
|
|
187
|
+
}
|
|
188
|
+
const checkpoint = await storage.getCheckpoint();
|
|
189
|
+
expect(checkpoint).not.toBeNull();
|
|
190
|
+
expect(checkpoint?.task.processedCount).toBe(5);
|
|
191
|
+
await syncWal.completeTask(taskId);
|
|
192
|
+
});
|
|
193
|
+
it('should truncate WAL after checkpoint', async () => {
|
|
194
|
+
const taskId = await syncWal.startTask('notes', 20);
|
|
195
|
+
// Process files to trigger checkpoint
|
|
196
|
+
for (let i = 0; i < 6; i++) {
|
|
197
|
+
await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
|
|
198
|
+
}
|
|
199
|
+
// WAL should be truncated - only entries from checkpoint onwards
|
|
200
|
+
const entries = await storage.getAllEntries();
|
|
201
|
+
// Should have: checkpoint entry + 1 file:done entry (6th file after checkpoint)
|
|
202
|
+
// WAL is truncated BEFORE checkpoint LSN, so checkpoint entry and later entries remain
|
|
203
|
+
expect(entries.length).toBeLessThan(10);
|
|
204
|
+
await syncWal.completeTask(taskId);
|
|
205
|
+
});
|
|
206
|
+
it('should accumulate stats across checkpoints', async () => {
|
|
207
|
+
const taskId = await syncWal.startTask('notes', 15);
|
|
208
|
+
// First batch (triggers checkpoint)
|
|
209
|
+
for (let i = 0; i < 5; i++) {
|
|
210
|
+
await syncWal.fileProcessed(taskId, `/batch1-${i}.md`, 1, 'added');
|
|
211
|
+
}
|
|
212
|
+
// Second batch (triggers another checkpoint)
|
|
213
|
+
for (let i = 0; i < 5; i++) {
|
|
214
|
+
await syncWal.fileProcessed(taskId, `/batch2-${i}.md`, 1, 'updated');
|
|
215
|
+
}
|
|
216
|
+
const progress = syncWal.getProgress();
|
|
217
|
+
expect(progress?.processedFiles).toBe(10);
|
|
218
|
+
expect(progress?.added).toBe(5);
|
|
219
|
+
expect(progress?.updated).toBe(5);
|
|
220
|
+
await syncWal.completeTask(taskId);
|
|
221
|
+
});
|
|
222
|
+
it('should update manifest on checkpoint', async () => {
|
|
223
|
+
const taskId = await syncWal.startTask('notes', 20);
|
|
224
|
+
for (let i = 0; i < 5; i++) {
|
|
225
|
+
await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
|
|
226
|
+
}
|
|
227
|
+
const manifest = await storage.getManifest();
|
|
228
|
+
expect(manifest?.current?.processedFiles).toBe(5);
|
|
229
|
+
await syncWal.completeTask(taskId);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
233
|
+
// RECOVERY
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
235
|
+
describe('Recovery', () => {
|
|
236
|
+
it('should return recovery plan for incomplete task', async () => {
|
|
237
|
+
// Simulate crashed task by writing checkpoint directly
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
await storage.writeCheckpoint({
|
|
240
|
+
version: 1,
|
|
241
|
+
lsn: 100,
|
|
242
|
+
createdAt: now,
|
|
243
|
+
task: {
|
|
244
|
+
id: 'crashed-task',
|
|
245
|
+
type: 'notes',
|
|
246
|
+
status: 'running',
|
|
247
|
+
startedAt: now - 10000,
|
|
248
|
+
totalFiles: 1000,
|
|
249
|
+
processedCount: 500,
|
|
250
|
+
manifestHash: 'hash123',
|
|
251
|
+
},
|
|
252
|
+
stats: {
|
|
253
|
+
added: 200,
|
|
254
|
+
updated: 300,
|
|
255
|
+
deleted: 0,
|
|
256
|
+
failed: [],
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
// Add some WAL entries after checkpoint
|
|
260
|
+
await storage.appendEntry({
|
|
261
|
+
lsn: 101,
|
|
262
|
+
ts: now,
|
|
263
|
+
op: 'file:done',
|
|
264
|
+
taskId: 'crashed-task',
|
|
265
|
+
data: { path: '/file501.md', chunks: 1 },
|
|
266
|
+
});
|
|
267
|
+
await storage.appendEntry({
|
|
268
|
+
lsn: 102,
|
|
269
|
+
ts: now,
|
|
270
|
+
op: 'file:done',
|
|
271
|
+
taskId: 'crashed-task',
|
|
272
|
+
data: { path: '/file502.md', chunks: 1 },
|
|
273
|
+
});
|
|
274
|
+
const plan = await syncWal.recover();
|
|
275
|
+
expect(plan).not.toBeNull();
|
|
276
|
+
expect(plan?.taskId).toBe('crashed-task');
|
|
277
|
+
expect(plan?.type).toBe('notes');
|
|
278
|
+
expect(plan?.totalFiles).toBe(1000);
|
|
279
|
+
expect(plan?.skipCount).toBe(502); // 500 from checkpoint + 2 from WAL
|
|
280
|
+
expect(plan?.manifestHash).toBe('hash123');
|
|
281
|
+
expect(plan?.stats.added).toBe(200);
|
|
282
|
+
expect(plan?.stats.updated).toBe(300);
|
|
283
|
+
});
|
|
284
|
+
it('should return null when no incomplete task', async () => {
|
|
285
|
+
// Complete task checkpoint
|
|
286
|
+
await storage.writeCheckpoint({
|
|
287
|
+
version: 1,
|
|
288
|
+
lsn: 100,
|
|
289
|
+
createdAt: Date.now(),
|
|
290
|
+
task: {
|
|
291
|
+
id: 'completed-task',
|
|
292
|
+
type: 'notes',
|
|
293
|
+
status: 'completed',
|
|
294
|
+
startedAt: Date.now(),
|
|
295
|
+
totalFiles: 100,
|
|
296
|
+
processedCount: 100,
|
|
297
|
+
},
|
|
298
|
+
stats: {
|
|
299
|
+
added: 100,
|
|
300
|
+
updated: 0,
|
|
301
|
+
deleted: 0,
|
|
302
|
+
failed: [],
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
const plan = await syncWal.recover();
|
|
306
|
+
expect(plan).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
it('should resume task from recovery plan', async () => {
|
|
309
|
+
// Setup recovery scenario
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
await storage.writeCheckpoint({
|
|
312
|
+
version: 1,
|
|
313
|
+
lsn: 50,
|
|
314
|
+
createdAt: now,
|
|
315
|
+
task: {
|
|
316
|
+
id: 'resume-task',
|
|
317
|
+
type: 'code',
|
|
318
|
+
status: 'running',
|
|
319
|
+
startedAt: now - 10000,
|
|
320
|
+
totalFiles: 100,
|
|
321
|
+
processedCount: 25,
|
|
322
|
+
manifestHash: 'hash456',
|
|
323
|
+
},
|
|
324
|
+
stats: {
|
|
325
|
+
added: 10,
|
|
326
|
+
updated: 15,
|
|
327
|
+
deleted: 0,
|
|
328
|
+
failed: [{ path: '/bad.ts', error: 'Syntax error', retries: 3 }],
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
const plan = await syncWal.recover();
|
|
332
|
+
expect(plan).not.toBeNull();
|
|
333
|
+
await syncWal.resumeTask(plan);
|
|
334
|
+
expect(syncWal.isRunning()).toBe(true);
|
|
335
|
+
expect(syncWal.getCurrentTaskId()).toBe('resume-task');
|
|
336
|
+
const progress = syncWal.getProgress();
|
|
337
|
+
expect(progress?.type).toBe('code');
|
|
338
|
+
expect(progress?.processedFiles).toBe(25);
|
|
339
|
+
expect(progress?.added).toBe(10);
|
|
340
|
+
expect(progress?.updated).toBe(15);
|
|
341
|
+
expect(progress?.failedCount).toBe(1);
|
|
342
|
+
await syncWal.completeTask('resume-task');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
346
|
+
// LOCK MANAGEMENT
|
|
347
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
348
|
+
describe('Lock Management', () => {
|
|
349
|
+
it('should detect no lock', async () => {
|
|
350
|
+
const status = await syncWal.isLocked();
|
|
351
|
+
expect(status.locked).toBe(false);
|
|
352
|
+
expect(status.stale).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
it('should detect active lock', async () => {
|
|
355
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
356
|
+
const status = await syncWal.isLocked();
|
|
357
|
+
expect(status.locked).toBe(true);
|
|
358
|
+
expect(status.stale).toBe(false);
|
|
359
|
+
expect(status.lock?.id).toBe(taskId);
|
|
360
|
+
await syncWal.completeTask(taskId);
|
|
361
|
+
});
|
|
362
|
+
it('should detect stale lock by heartbeat', async () => {
|
|
363
|
+
// Create lock with old heartbeat
|
|
364
|
+
await storage.acquireLock({
|
|
365
|
+
id: 'old-task',
|
|
366
|
+
type: 'notes',
|
|
367
|
+
pid: process.pid,
|
|
368
|
+
startedAt: Date.now() - 300000,
|
|
369
|
+
heartbeat: Date.now() - 300000, // 5 minutes ago
|
|
370
|
+
});
|
|
371
|
+
// Create SyncWal with short timeout for testing
|
|
372
|
+
const shortTimeoutWal = new SyncWal(storage, { staleLockTimeout: 1000 });
|
|
373
|
+
const status = await shortTimeoutWal.isLocked();
|
|
374
|
+
expect(status.locked).toBe(true);
|
|
375
|
+
expect(status.stale).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
it('should detect stale lock by dead PID', async () => {
|
|
378
|
+
// Create lock with non-existent PID
|
|
379
|
+
await storage.acquireLock({
|
|
380
|
+
id: 'dead-task',
|
|
381
|
+
type: 'notes',
|
|
382
|
+
pid: 999999999, // Very high, unlikely to exist
|
|
383
|
+
startedAt: Date.now(),
|
|
384
|
+
heartbeat: Date.now(),
|
|
385
|
+
});
|
|
386
|
+
const status = await syncWal.isLocked();
|
|
387
|
+
expect(status.locked).toBe(true);
|
|
388
|
+
expect(status.stale).toBe(true);
|
|
389
|
+
});
|
|
390
|
+
it('should force clear stale lock', async () => {
|
|
391
|
+
// Create stale lock
|
|
392
|
+
await storage.acquireLock({
|
|
393
|
+
id: 'stale-task',
|
|
394
|
+
type: 'notes',
|
|
395
|
+
pid: 999999999,
|
|
396
|
+
startedAt: Date.now(),
|
|
397
|
+
heartbeat: Date.now() - 300000,
|
|
398
|
+
});
|
|
399
|
+
const shortTimeoutWal = new SyncWal(storage, { staleLockTimeout: 1000 });
|
|
400
|
+
const cleared = await shortTimeoutWal.forceClearStaleLock();
|
|
401
|
+
expect(cleared).toBe(true);
|
|
402
|
+
expect(await storage.getLock()).toBeNull();
|
|
403
|
+
});
|
|
404
|
+
it('should not force clear active lock', async () => {
|
|
405
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
406
|
+
const cleared = await syncWal.forceClearStaleLock();
|
|
407
|
+
expect(cleared).toBe(false);
|
|
408
|
+
expect((await storage.getLock())?.id).toBe(taskId);
|
|
409
|
+
await syncWal.completeTask(taskId);
|
|
410
|
+
});
|
|
411
|
+
it('should acquire lock by clearing stale lock', async () => {
|
|
412
|
+
// Create stale lock
|
|
413
|
+
await storage.acquireLock({
|
|
414
|
+
id: 'stale-task',
|
|
415
|
+
type: 'notes',
|
|
416
|
+
pid: 999999999,
|
|
417
|
+
startedAt: Date.now(),
|
|
418
|
+
heartbeat: Date.now() - 300000,
|
|
419
|
+
});
|
|
420
|
+
const shortTimeoutWal = new SyncWal(storage, { staleLockTimeout: 1000 });
|
|
421
|
+
const taskId = await shortTimeoutWal.startTask('notes', 10);
|
|
422
|
+
expect(taskId).toBeTruthy();
|
|
423
|
+
expect((await storage.getLock())?.id).toBe(taskId);
|
|
424
|
+
await shortTimeoutWal.completeTask(taskId);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
428
|
+
// CANCELLATION
|
|
429
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
430
|
+
describe('Cancellation', () => {
|
|
431
|
+
it('should request cancellation', async () => {
|
|
432
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
433
|
+
await syncWal.requestCancel(taskId);
|
|
434
|
+
expect(syncWal.isCancelled(taskId)).toBe(true);
|
|
435
|
+
await syncWal.cancelTask(taskId);
|
|
436
|
+
});
|
|
437
|
+
it('should not mark non-requested task as cancelled', async () => {
|
|
438
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
439
|
+
expect(syncWal.isCancelled(taskId)).toBe(false);
|
|
440
|
+
expect(syncWal.isCancelled('other-task')).toBe(false);
|
|
441
|
+
await syncWal.completeTask(taskId);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
445
|
+
// PROGRESS
|
|
446
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
447
|
+
describe('Progress', () => {
|
|
448
|
+
it('should return null when no task running', () => {
|
|
449
|
+
const progress = syncWal.getProgress();
|
|
450
|
+
expect(progress).toBeNull();
|
|
451
|
+
});
|
|
452
|
+
it('should return progress for running task', async () => {
|
|
453
|
+
const taskId = await syncWal.startTask('notes', 100, 'hash');
|
|
454
|
+
await syncWal.fileProcessed(taskId, '/a.md', 1, 'added');
|
|
455
|
+
await syncWal.fileProcessed(taskId, '/b.md', 1, 'updated');
|
|
456
|
+
await syncWal.fileFailed(taskId, '/c.md', 'error', 0);
|
|
457
|
+
const progress = syncWal.getProgress();
|
|
458
|
+
expect(progress).not.toBeNull();
|
|
459
|
+
expect(progress?.taskId).toBe(taskId);
|
|
460
|
+
expect(progress?.type).toBe('notes');
|
|
461
|
+
expect(progress?.status).toBe('running');
|
|
462
|
+
expect(progress?.phase).toBe('indexing');
|
|
463
|
+
expect(progress?.totalFiles).toBe(100);
|
|
464
|
+
expect(progress?.processedFiles).toBe(3);
|
|
465
|
+
expect(progress?.added).toBe(1);
|
|
466
|
+
expect(progress?.updated).toBe(1);
|
|
467
|
+
expect(progress?.failedCount).toBe(1);
|
|
468
|
+
expect(progress?.elapsedMs).toBeGreaterThan(0);
|
|
469
|
+
await syncWal.completeTask(taskId);
|
|
470
|
+
});
|
|
471
|
+
it('should estimate remaining time', async () => {
|
|
472
|
+
const taskId = await syncWal.startTask('notes', 100);
|
|
473
|
+
// Process some files with a small delay
|
|
474
|
+
for (let i = 0; i < 10; i++) {
|
|
475
|
+
await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
|
|
476
|
+
await new Promise((r) => setTimeout(r, 5)); // Small delay
|
|
477
|
+
}
|
|
478
|
+
const progress = syncWal.getProgress();
|
|
479
|
+
expect(progress?.estimatedRemainingMs).toBeDefined();
|
|
480
|
+
expect(progress?.estimatedRemainingMs).toBeGreaterThan(0);
|
|
481
|
+
await syncWal.completeTask(taskId);
|
|
482
|
+
});
|
|
483
|
+
it('should emit progress via callback', async () => {
|
|
484
|
+
const progressUpdates = [];
|
|
485
|
+
syncWal.onProgress((p) => progressUpdates.push(p));
|
|
486
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
487
|
+
// Should have initial progress
|
|
488
|
+
expect(progressUpdates.length).toBeGreaterThan(0);
|
|
489
|
+
await syncWal.completeTask(taskId);
|
|
490
|
+
// Should have final progress with phase 'done'
|
|
491
|
+
const lastProgress = progressUpdates[progressUpdates.length - 1];
|
|
492
|
+
expect(lastProgress.phase).toBe('done');
|
|
493
|
+
});
|
|
494
|
+
it('should update phase', async () => {
|
|
495
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
496
|
+
syncWal.setPhase('deleting');
|
|
497
|
+
expect(syncWal.getProgress()?.phase).toBe('deleting');
|
|
498
|
+
syncWal.setPhase('analyzing');
|
|
499
|
+
expect(syncWal.getProgress()?.phase).toBe('analyzing');
|
|
500
|
+
await syncWal.completeTask(taskId);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
504
|
+
// STATE QUERIES
|
|
505
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
506
|
+
describe('State Queries', () => {
|
|
507
|
+
it('should get manifest', async () => {
|
|
508
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
509
|
+
const manifest = await syncWal.getManifest();
|
|
510
|
+
expect(manifest).not.toBeNull();
|
|
511
|
+
expect(manifest?.current?.id).toBe(taskId);
|
|
512
|
+
await syncWal.completeTask(taskId);
|
|
513
|
+
});
|
|
514
|
+
it('should get current task', async () => {
|
|
515
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
516
|
+
const task = await syncWal.getCurrentTask();
|
|
517
|
+
expect(task).not.toBeNull();
|
|
518
|
+
expect(task?.id).toBe(taskId);
|
|
519
|
+
await syncWal.completeTask(taskId);
|
|
520
|
+
expect(await syncWal.getCurrentTask()).toBeNull();
|
|
521
|
+
});
|
|
522
|
+
it('should get last completed task', async () => {
|
|
523
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
524
|
+
await syncWal.completeTask(taskId);
|
|
525
|
+
const lastCompleted = await syncWal.getLastCompleted();
|
|
526
|
+
expect(lastCompleted).not.toBeNull();
|
|
527
|
+
expect(lastCompleted?.id).toBe(taskId);
|
|
528
|
+
expect(lastCompleted?.status).toBe('completed');
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
532
|
+
// HEARTBEAT
|
|
533
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
534
|
+
describe('Heartbeat', () => {
|
|
535
|
+
it('should update heartbeat periodically', async () => {
|
|
536
|
+
// Use very short heartbeat for testing
|
|
537
|
+
const fastHeartbeatWal = new SyncWal(storage, {
|
|
538
|
+
heartbeatInterval: 50,
|
|
539
|
+
checkpointInterval: 100,
|
|
540
|
+
});
|
|
541
|
+
const taskId = await fastHeartbeatWal.startTask('notes', 10);
|
|
542
|
+
const initialLock = await storage.getLock();
|
|
543
|
+
const initialHeartbeat = initialLock.heartbeat;
|
|
544
|
+
// Wait for heartbeat update
|
|
545
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
546
|
+
const updatedLock = await storage.getLock();
|
|
547
|
+
expect(updatedLock.heartbeat).toBeGreaterThan(initialHeartbeat);
|
|
548
|
+
fastHeartbeatWal.stopHeartbeat();
|
|
549
|
+
await fastHeartbeatWal.completeTask(taskId);
|
|
550
|
+
});
|
|
551
|
+
it('should stop heartbeat on task complete', async () => {
|
|
552
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
553
|
+
await syncWal.completeTask(taskId);
|
|
554
|
+
// Heartbeat should be stopped (no errors when lock is released)
|
|
555
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
559
|
+
// GRACEFUL SHUTDOWN
|
|
560
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
561
|
+
describe('Graceful Shutdown', () => {
|
|
562
|
+
it('should checkpoint and release lock on shutdown', async () => {
|
|
563
|
+
const taskId = await syncWal.startTask('notes', 20);
|
|
564
|
+
// Process some files
|
|
565
|
+
for (let i = 0; i < 3; i++) {
|
|
566
|
+
await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
|
|
567
|
+
}
|
|
568
|
+
await syncWal.shutdown();
|
|
569
|
+
// Lock should be released
|
|
570
|
+
expect(await storage.getLock()).toBeNull();
|
|
571
|
+
// Checkpoint should have latest state
|
|
572
|
+
const checkpoint = await storage.getCheckpoint();
|
|
573
|
+
expect(checkpoint?.task.processedCount).toBe(3);
|
|
574
|
+
});
|
|
575
|
+
it('should handle shutdown with no active task', async () => {
|
|
576
|
+
await expect(syncWal.shutdown()).resolves.not.toThrow();
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
580
|
+
// CLEAR WAL STATE
|
|
581
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
582
|
+
describe('Clear WAL State', () => {
|
|
583
|
+
it('should clear all WAL state and reset to initial', async () => {
|
|
584
|
+
// Start a task and process some files
|
|
585
|
+
const taskId = await syncWal.startTask('notes', 10);
|
|
586
|
+
await syncWal.fileProcessed(taskId, '/test1.md', 1);
|
|
587
|
+
await syncWal.fileProcessed(taskId, '/test2.md', 1);
|
|
588
|
+
// Verify state exists before clear
|
|
589
|
+
expect(await storage.getLock()).not.toBeNull();
|
|
590
|
+
expect(syncWal.isRunning()).toBe(true);
|
|
591
|
+
// Clear all state
|
|
592
|
+
await syncWal.clear();
|
|
593
|
+
// Verify everything is cleared
|
|
594
|
+
expect(await storage.getLock()).toBeNull();
|
|
595
|
+
expect(await storage.getCheckpoint()).toBeNull();
|
|
596
|
+
expect(await storage.getManifest()).toBeNull();
|
|
597
|
+
expect(syncWal.isRunning()).toBe(false);
|
|
598
|
+
expect(syncWal.getCurrentTaskId()).toBeNull();
|
|
599
|
+
// Should be able to start fresh
|
|
600
|
+
const newTaskId = await syncWal.startTask('notes', 5);
|
|
601
|
+
expect(newTaskId).toBeDefined();
|
|
602
|
+
await syncWal.completeTask(newTaskId);
|
|
603
|
+
});
|
|
604
|
+
it('should clear recovery state when stuck in recovery loop', async () => {
|
|
605
|
+
// Simulate stuck recovery state: start task, checkpoint, then simulate crash
|
|
606
|
+
const taskId = await syncWal.startTask('notes', 100);
|
|
607
|
+
for (let i = 0; i < 10; i++) {
|
|
608
|
+
await syncWal.fileProcessed(taskId, `/file-${i}.md`, 1);
|
|
609
|
+
}
|
|
610
|
+
await syncWal.checkpoint();
|
|
611
|
+
// Simulate crash by creating new SyncWal without completing task
|
|
612
|
+
const crashedWal = new SyncWal(storage);
|
|
613
|
+
const recoveryPlan = await crashedWal.recover();
|
|
614
|
+
expect(recoveryPlan).not.toBeNull();
|
|
615
|
+
expect(recoveryPlan?.taskId).toBe(taskId);
|
|
616
|
+
// Clear should remove recovery state
|
|
617
|
+
await crashedWal.clear();
|
|
618
|
+
// No more recovery needed
|
|
619
|
+
const noRecovery = await crashedWal.recover();
|
|
620
|
+
expect(noRecovery).toBeNull();
|
|
621
|
+
});
|
|
622
|
+
it('should handle clear with no active state', async () => {
|
|
623
|
+
// Clear on fresh instance should not throw
|
|
624
|
+
await expect(syncWal.clear()).resolves.not.toThrow();
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
629
|
+
// ERROR HANDLING TESTS
|
|
630
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
631
|
+
describe('Error Handling', () => {
|
|
632
|
+
let tempDir;
|
|
633
|
+
let storage;
|
|
634
|
+
beforeEach(async () => {
|
|
635
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sync-wal-err-'));
|
|
636
|
+
storage = new FileWalStorage(tempDir);
|
|
637
|
+
});
|
|
638
|
+
afterEach(async () => {
|
|
639
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
640
|
+
});
|
|
641
|
+
it('should release lock if startTask fails after acquiring lock', async () => {
|
|
642
|
+
// Create mock storage that fails on appendEntry
|
|
643
|
+
const failingStorage = {
|
|
644
|
+
acquireLock: vi.fn(async () => true),
|
|
645
|
+
releaseLock: vi.fn(async () => { }),
|
|
646
|
+
getLock: vi.fn(async () => null),
|
|
647
|
+
updateHeartbeat: vi.fn(async () => { }),
|
|
648
|
+
appendEntry: vi.fn(async () => {
|
|
649
|
+
throw new Error('WAL write failed');
|
|
650
|
+
}),
|
|
651
|
+
getEntriesFromLsn: vi.fn(async () => []),
|
|
652
|
+
truncateBeforeLsn: vi.fn(async () => { }),
|
|
653
|
+
writeCheckpoint: vi.fn(async () => { }),
|
|
654
|
+
getCheckpoint: vi.fn(async () => null),
|
|
655
|
+
writeManifest: vi.fn(async () => { }),
|
|
656
|
+
getManifest: vi.fn(async () => null),
|
|
657
|
+
clear: vi.fn(async () => { }),
|
|
658
|
+
};
|
|
659
|
+
const wal = new SyncWal(failingStorage);
|
|
660
|
+
// startTask should fail and release the lock
|
|
661
|
+
await expect(wal.startTask('notes', 10)).rejects.toThrow('WAL write failed');
|
|
662
|
+
// Lock should have been released
|
|
663
|
+
expect(failingStorage.releaseLock).toHaveBeenCalled();
|
|
664
|
+
// SyncWal state should be clean
|
|
665
|
+
expect(wal.isRunning()).toBe(false);
|
|
666
|
+
expect(wal.getCurrentTaskId()).toBeNull();
|
|
667
|
+
wal.stopHeartbeat();
|
|
668
|
+
});
|
|
669
|
+
it('should release lock if manifest write fails', async () => {
|
|
670
|
+
// Create mock storage that fails on writeManifest
|
|
671
|
+
const failingStorage = {
|
|
672
|
+
acquireLock: vi.fn(async () => true),
|
|
673
|
+
releaseLock: vi.fn(async () => { }),
|
|
674
|
+
getLock: vi.fn(async () => null),
|
|
675
|
+
updateHeartbeat: vi.fn(async () => { }),
|
|
676
|
+
appendEntry: vi.fn(async () => { }),
|
|
677
|
+
getEntriesFromLsn: vi.fn(async () => []),
|
|
678
|
+
truncateBeforeLsn: vi.fn(async () => { }),
|
|
679
|
+
writeCheckpoint: vi.fn(async () => { }),
|
|
680
|
+
getCheckpoint: vi.fn(async () => null),
|
|
681
|
+
writeManifest: vi.fn(async () => {
|
|
682
|
+
throw new Error('Manifest write failed');
|
|
683
|
+
}),
|
|
684
|
+
getManifest: vi.fn(async () => null),
|
|
685
|
+
clear: vi.fn(async () => { }),
|
|
686
|
+
};
|
|
687
|
+
const wal = new SyncWal(failingStorage);
|
|
688
|
+
await expect(wal.startTask('notes', 10)).rejects.toThrow('Manifest write failed');
|
|
689
|
+
expect(failingStorage.releaseLock).toHaveBeenCalled();
|
|
690
|
+
expect(wal.isRunning()).toBe(false);
|
|
691
|
+
wal.stopHeartbeat();
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
695
|
+
// MOCK STORAGE TESTS
|
|
696
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
697
|
+
describe('SyncWal with Mock Storage', () => {
|
|
698
|
+
it('should work with any WalStorage implementation', async () => {
|
|
699
|
+
// Simple in-memory mock
|
|
700
|
+
const mockEntries = [];
|
|
701
|
+
let mockLock = null;
|
|
702
|
+
let mockCheckpoint = null;
|
|
703
|
+
let mockManifest = null;
|
|
704
|
+
const mockStorage = {
|
|
705
|
+
acquireLock: vi.fn(async (lock) => {
|
|
706
|
+
if (mockLock)
|
|
707
|
+
return false;
|
|
708
|
+
mockLock = lock;
|
|
709
|
+
return true;
|
|
710
|
+
}),
|
|
711
|
+
releaseLock: vi.fn(async (id) => {
|
|
712
|
+
if (mockLock?.id === id)
|
|
713
|
+
mockLock = null;
|
|
714
|
+
}),
|
|
715
|
+
getLock: vi.fn(async () => mockLock),
|
|
716
|
+
updateHeartbeat: vi.fn(async () => { }),
|
|
717
|
+
appendEntry: vi.fn(async (entry) => {
|
|
718
|
+
mockEntries.push(entry);
|
|
719
|
+
}),
|
|
720
|
+
getEntriesFromLsn: vi.fn(async (lsn) => mockEntries.filter((e) => e.lsn >= lsn)),
|
|
721
|
+
truncateBeforeLsn: vi.fn(async (lsn) => {
|
|
722
|
+
const kept = mockEntries.filter((e) => e.lsn >= lsn);
|
|
723
|
+
mockEntries.length = 0;
|
|
724
|
+
mockEntries.push(...kept);
|
|
725
|
+
}),
|
|
726
|
+
writeCheckpoint: vi.fn(async (data) => {
|
|
727
|
+
mockCheckpoint = data;
|
|
728
|
+
}),
|
|
729
|
+
getCheckpoint: vi.fn(async () => mockCheckpoint),
|
|
730
|
+
writeManifest: vi.fn(async (data) => {
|
|
731
|
+
mockManifest = data;
|
|
732
|
+
}),
|
|
733
|
+
getManifest: vi.fn(async () => mockManifest),
|
|
734
|
+
clear: vi.fn(async () => {
|
|
735
|
+
mockEntries.length = 0;
|
|
736
|
+
mockLock = null;
|
|
737
|
+
mockCheckpoint = null;
|
|
738
|
+
mockManifest = null;
|
|
739
|
+
}),
|
|
740
|
+
};
|
|
741
|
+
const wal = new SyncWal(mockStorage, { checkpointInterval: 100 });
|
|
742
|
+
const taskId = await wal.startTask('notes', 50);
|
|
743
|
+
expect(mockStorage.acquireLock).toHaveBeenCalled();
|
|
744
|
+
expect(mockStorage.appendEntry).toHaveBeenCalled();
|
|
745
|
+
expect(mockStorage.writeManifest).toHaveBeenCalled();
|
|
746
|
+
await wal.fileProcessed(taskId, '/test.md', 1);
|
|
747
|
+
await wal.completeTask(taskId);
|
|
748
|
+
expect(mockStorage.releaseLock).toHaveBeenCalled();
|
|
749
|
+
wal.stopHeartbeat();
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
//# sourceMappingURL=sync-wal.test.js.map
|