pushwork 1.0.0
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/README.md +460 -0
- package/dist/browser/browser-sync-engine.d.ts +64 -0
- package/dist/browser/browser-sync-engine.d.ts.map +1 -0
- package/dist/browser/browser-sync-engine.js +303 -0
- package/dist/browser/browser-sync-engine.js.map +1 -0
- package/dist/browser/filesystem-adapter.d.ts +84 -0
- package/dist/browser/filesystem-adapter.d.ts.map +1 -0
- package/dist/browser/filesystem-adapter.js +413 -0
- package/dist/browser/filesystem-adapter.js.map +1 -0
- package/dist/browser/index.d.ts +36 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +90 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/types.d.ts +70 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +6 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/cli/commands.d.ts +71 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +794 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +19 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +199 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +71 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +314 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/change-detection.d.ts +78 -0
- package/dist/core/change-detection.d.ts.map +1 -0
- package/dist/core/change-detection.js +370 -0
- package/dist/core/change-detection.js.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +22 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/isomorphic-snapshot.d.ts +58 -0
- package/dist/core/isomorphic-snapshot.d.ts.map +1 -0
- package/dist/core/isomorphic-snapshot.js +204 -0
- package/dist/core/isomorphic-snapshot.js.map +1 -0
- package/dist/core/move-detection.d.ts +72 -0
- package/dist/core/move-detection.d.ts.map +1 -0
- package/dist/core/move-detection.js +200 -0
- package/dist/core/move-detection.js.map +1 -0
- package/dist/core/snapshot.d.ts +109 -0
- package/dist/core/snapshot.d.ts.map +1 -0
- package/dist/core/snapshot.js +263 -0
- package/dist/core/snapshot.js.map +1 -0
- package/dist/core/sync-engine.d.ts +110 -0
- package/dist/core/sync-engine.d.ts.map +1 -0
- package/dist/core/sync-engine.js +817 -0
- package/dist/core/sync-engine.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/browser-filesystem.d.ts +26 -0
- package/dist/platform/browser-filesystem.d.ts.map +1 -0
- package/dist/platform/browser-filesystem.js +91 -0
- package/dist/platform/browser-filesystem.js.map +1 -0
- package/dist/platform/filesystem.d.ts +29 -0
- package/dist/platform/filesystem.d.ts.map +1 -0
- package/dist/platform/filesystem.js +65 -0
- package/dist/platform/filesystem.js.map +1 -0
- package/dist/platform/node-filesystem.d.ts +21 -0
- package/dist/platform/node-filesystem.d.ts.map +1 -0
- package/dist/platform/node-filesystem.js +93 -0
- package/dist/platform/node-filesystem.js.map +1 -0
- package/dist/types/config.d.ts +119 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/documents.d.ts +70 -0
- package/dist/types/documents.d.ts.map +1 -0
- package/dist/types/documents.js +23 -0
- package/dist/types/documents.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/snapshot.d.ts +81 -0
- package/dist/types/snapshot.d.ts.map +1 -0
- package/dist/types/snapshot.js +17 -0
- package/dist/types/snapshot.js.map +1 -0
- package/dist/utils/content-similarity.d.ts +53 -0
- package/dist/utils/content-similarity.d.ts.map +1 -0
- package/dist/utils/content-similarity.js +155 -0
- package/dist/utils/content-similarity.js.map +1 -0
- package/dist/utils/content.d.ts +5 -0
- package/dist/utils/content.d.ts.map +1 -0
- package/dist/utils/content.js +30 -0
- package/dist/utils/content.js.map +1 -0
- package/dist/utils/fs-browser.d.ts +57 -0
- package/dist/utils/fs-browser.d.ts.map +1 -0
- package/dist/utils/fs-browser.js +311 -0
- package/dist/utils/fs-browser.js.map +1 -0
- package/dist/utils/fs-node.d.ts +53 -0
- package/dist/utils/fs-node.d.ts.map +1 -0
- package/dist/utils/fs-node.js +220 -0
- package/dist/utils/fs-node.js.map +1 -0
- package/dist/utils/fs.d.ts +62 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +293 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +23 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/isomorphic.d.ts +29 -0
- package/dist/utils/isomorphic.d.ts.map +1 -0
- package/dist/utils/isomorphic.js +139 -0
- package/dist/utils/isomorphic.js.map +1 -0
- package/dist/utils/mime-types.d.ts +13 -0
- package/dist/utils/mime-types.d.ts.map +1 -0
- package/dist/utils/mime-types.js +240 -0
- package/dist/utils/mime-types.js.map +1 -0
- package/dist/utils/network-sync.d.ts +12 -0
- package/dist/utils/network-sync.d.ts.map +1 -0
- package/dist/utils/network-sync.js +149 -0
- package/dist/utils/network-sync.js.map +1 -0
- package/dist/utils/pure.d.ts +25 -0
- package/dist/utils/pure.d.ts.map +1 -0
- package/dist/utils/pure.js +112 -0
- package/dist/utils/pure.js.map +1 -0
- package/dist/utils/repo-factory.d.ts +11 -0
- package/dist/utils/repo-factory.d.ts.map +1 -0
- package/dist/utils/repo-factory.js +77 -0
- package/dist/utils/repo-factory.js.map +1 -0
- package/package.json +83 -0
- package/src/cli/commands.ts +1053 -0
- package/src/cli/index.ts +2 -0
- package/src/cli.ts +287 -0
- package/src/config/index.ts +334 -0
- package/src/core/change-detection.ts +484 -0
- package/src/core/index.ts +5 -0
- package/src/core/move-detection.ts +269 -0
- package/src/core/snapshot.ts +285 -0
- package/src/core/sync-engine.ts +1167 -0
- package/src/index.ts +14 -0
- package/src/types/config.ts +130 -0
- package/src/types/documents.ts +72 -0
- package/src/types/index.ts +8 -0
- package/src/types/snapshot.ts +88 -0
- package/src/utils/content-similarity.ts +194 -0
- package/src/utils/content.ts +28 -0
- package/src/utils/fs.ts +289 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/mime-types.ts +236 -0
- package/src/utils/network-sync.ts +153 -0
- package/src/utils/repo-factory.ts +58 -0
- package/test/README-TESTING-GAPS.md +174 -0
- package/test/integration/README.md +328 -0
- package/test/integration/clone-test.sh +310 -0
- package/test/integration/conflict-resolution-test.sh +309 -0
- package/test/integration/deletion-behavior-test.sh +487 -0
- package/test/integration/deletion-sync-test-simple.sh +193 -0
- package/test/integration/deletion-sync-test.sh +297 -0
- package/test/integration/exclude-patterns.test.ts +152 -0
- package/test/integration/full-integration-test.sh +363 -0
- package/test/integration/sync-deletion.test.ts +339 -0
- package/test/integration/sync-flow.test.ts +309 -0
- package/test/run-tests.sh +225 -0
- package/test/unit/content-similarity.test.ts +236 -0
- package/test/unit/deletion-behavior.test.ts +260 -0
- package/test/unit/enhanced-mime-detection.test.ts +266 -0
- package/test/unit/snapshot.test.ts +431 -0
- package/test/unit/sync-timing.test.ts +178 -0
- package/test/unit/utils.test.ts +368 -0
- package/tools/browser-sync/README.md +116 -0
- package/tools/browser-sync/package.json +44 -0
- package/tools/browser-sync/patchwork.json +1 -0
- package/tools/browser-sync/pnpm-lock.yaml +4202 -0
- package/tools/browser-sync/src/components/BrowserSyncTool.tsx +599 -0
- package/tools/browser-sync/src/index.ts +20 -0
- package/tools/browser-sync/src/polyfills.ts +31 -0
- package/tools/browser-sync/src/styles.css +290 -0
- package/tools/browser-sync/src/types.ts +27 -0
- package/tools/browser-sync/vite.config.ts +25 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import {
|
|
5
|
+
getEnhancedMimeType,
|
|
6
|
+
isEnhancedTextFile,
|
|
7
|
+
shouldForceAsText,
|
|
8
|
+
getMimeType,
|
|
9
|
+
isTextFile,
|
|
10
|
+
} from "../../src/utils";
|
|
11
|
+
|
|
12
|
+
describe("Enhanced MIME Detection", () => {
|
|
13
|
+
let testDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
testDir = await fs.mkdtemp(path.join(tmpdir(), "enhanced-mime-test-"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("Enhanced vs Standard MIME Detection", () => {
|
|
24
|
+
it("should fix TypeScript file MIME detection", async () => {
|
|
25
|
+
const tsFile = path.join(testDir, "test.ts");
|
|
26
|
+
await fs.writeFile(tsFile, "interface User { name: string; }");
|
|
27
|
+
|
|
28
|
+
// Standard MIME detection (broken)
|
|
29
|
+
const standardMime = getMimeType(tsFile);
|
|
30
|
+
const standardIsText = await isTextFile(tsFile);
|
|
31
|
+
|
|
32
|
+
// Enhanced MIME detection (fixed)
|
|
33
|
+
const enhancedMime = getEnhancedMimeType(tsFile);
|
|
34
|
+
const enhancedIsText = await isEnhancedTextFile(tsFile);
|
|
35
|
+
const shouldForce = shouldForceAsText(tsFile);
|
|
36
|
+
|
|
37
|
+
console.log("TypeScript file (.ts) detection:");
|
|
38
|
+
console.log(` Standard MIME: ${standardMime}, Text: ${standardIsText}`);
|
|
39
|
+
console.log(
|
|
40
|
+
` Enhanced MIME: ${enhancedMime}, Text: ${enhancedIsText}, Force: ${shouldForce}`
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Verify the fix
|
|
44
|
+
expect(enhancedMime).toBe("text/typescript"); // Fixed!
|
|
45
|
+
expect(enhancedIsText).toBe(true); // Fixed!
|
|
46
|
+
expect(shouldForce).toBe(true); // Force TypeScript as text
|
|
47
|
+
|
|
48
|
+
// Show the original problem still exists with standard detection
|
|
49
|
+
expect(standardMime).toBe("video/mp2t"); // The original problem
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should fix TSX file MIME detection", async () => {
|
|
53
|
+
const tsxFile = path.join(testDir, "Component.tsx");
|
|
54
|
+
await fs.writeFile(tsxFile, "export const App = () => <div>Hello</div>;");
|
|
55
|
+
|
|
56
|
+
const standardMime = getMimeType(tsxFile);
|
|
57
|
+
const enhancedMime = getEnhancedMimeType(tsxFile);
|
|
58
|
+
const enhancedIsText = await isEnhancedTextFile(tsxFile);
|
|
59
|
+
|
|
60
|
+
console.log("TSX file detection:");
|
|
61
|
+
console.log(` Standard MIME: ${standardMime}`);
|
|
62
|
+
console.log(` Enhanced MIME: ${enhancedMime}, Text: ${enhancedIsText}`);
|
|
63
|
+
|
|
64
|
+
expect(enhancedMime).toBe("text/tsx"); // Fixed!
|
|
65
|
+
expect(enhancedIsText).toBe(true); // Fixed!
|
|
66
|
+
expect(standardMime).toBe("application/octet-stream"); // Original problem
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should handle Vue.js single file components", async () => {
|
|
70
|
+
const vueFile = path.join(testDir, "App.vue");
|
|
71
|
+
await fs.writeFile(
|
|
72
|
+
vueFile,
|
|
73
|
+
`
|
|
74
|
+
<template>
|
|
75
|
+
<div>{{ message }}</div>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script>
|
|
79
|
+
export default {
|
|
80
|
+
data() {
|
|
81
|
+
return { message: 'Hello Vue!' }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<style scoped>
|
|
87
|
+
div { color: blue; }
|
|
88
|
+
</style>
|
|
89
|
+
`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const enhancedMime = getEnhancedMimeType(vueFile);
|
|
93
|
+
const enhancedIsText = await isEnhancedTextFile(vueFile);
|
|
94
|
+
|
|
95
|
+
expect(enhancedMime).toBe("text/vue");
|
|
96
|
+
expect(enhancedIsText).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should handle modern CSS preprocessors correctly", async () => {
|
|
100
|
+
const testCases = [
|
|
101
|
+
{ file: "styles.scss", expectedMime: "text/scss" },
|
|
102
|
+
{ file: "styles.sass", expectedMime: "text/sass" },
|
|
103
|
+
{ file: "styles.less", expectedMime: "text/less" },
|
|
104
|
+
{ file: "styles.styl", expectedMime: "text/stylus" },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
for (const testCase of testCases) {
|
|
108
|
+
const filePath = path.join(testDir, testCase.file);
|
|
109
|
+
await fs.writeFile(
|
|
110
|
+
filePath,
|
|
111
|
+
"$primary-color: #007bff;\n.button { color: $primary-color; }"
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const enhancedMime = getEnhancedMimeType(filePath);
|
|
115
|
+
const enhancedIsText = await isEnhancedTextFile(filePath);
|
|
116
|
+
|
|
117
|
+
expect(enhancedMime).toBe(testCase.expectedMime);
|
|
118
|
+
expect(enhancedIsText).toBe(true);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should handle configuration files by filename", async () => {
|
|
123
|
+
const configFiles = [
|
|
124
|
+
{ filename: "Dockerfile", expectedMime: "text/plain" },
|
|
125
|
+
{ filename: "package.json", expectedMime: "application/json" },
|
|
126
|
+
{ filename: "tsconfig.json", expectedMime: "application/json" },
|
|
127
|
+
{
|
|
128
|
+
filename: "webpack.config.js",
|
|
129
|
+
expectedMime: "application/javascript",
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
for (const config of configFiles) {
|
|
134
|
+
const filePath = path.join(testDir, config.filename);
|
|
135
|
+
await fs.writeFile(filePath, "# Configuration content");
|
|
136
|
+
|
|
137
|
+
const enhancedMime = getEnhancedMimeType(filePath);
|
|
138
|
+
const enhancedIsText = await isEnhancedTextFile(filePath);
|
|
139
|
+
|
|
140
|
+
expect(enhancedMime).toBe(config.expectedMime);
|
|
141
|
+
expect(enhancedIsText).toBe(true);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("Comprehensive Developer File Support", () => {
|
|
147
|
+
it("should correctly handle all common developer file types", async () => {
|
|
148
|
+
const developerFiles = [
|
|
149
|
+
// JavaScript/TypeScript ecosystem
|
|
150
|
+
{ name: "app.js", mime: "application/javascript", text: true },
|
|
151
|
+
{ name: "app.ts", mime: "text/typescript", text: true },
|
|
152
|
+
{ name: "Component.tsx", mime: "text/tsx", text: true },
|
|
153
|
+
{ name: "Component.jsx", mime: "text/jsx", text: true },
|
|
154
|
+
{ name: "types.d.ts", mime: "text/typescript", text: true },
|
|
155
|
+
{ name: "bundle.mjs", mime: "application/javascript", text: true },
|
|
156
|
+
{ name: "server.cjs", mime: "application/javascript", text: true },
|
|
157
|
+
|
|
158
|
+
// Frontend frameworks
|
|
159
|
+
{ name: "App.vue", mime: "text/vue", text: true },
|
|
160
|
+
{ name: "Button.svelte", mime: "text/svelte", text: true },
|
|
161
|
+
|
|
162
|
+
// Stylesheets
|
|
163
|
+
{ name: "styles.scss", mime: "text/scss", text: true },
|
|
164
|
+
{ name: "main.css", mime: "text/css", text: true },
|
|
165
|
+
|
|
166
|
+
// Documentation
|
|
167
|
+
{ name: "README.md", mime: "text/markdown", text: true },
|
|
168
|
+
{ name: "docs.mdx", mime: "text/markdown", text: true },
|
|
169
|
+
|
|
170
|
+
// Config files
|
|
171
|
+
{ name: ".env", mime: "text/plain", text: true },
|
|
172
|
+
{ name: ".gitignore", mime: "text/plain", text: true },
|
|
173
|
+
{ name: "config.toml", mime: "application/toml", text: true },
|
|
174
|
+
|
|
175
|
+
// Source maps and build artifacts
|
|
176
|
+
{ name: "app.js.map", mime: "application/json", text: true },
|
|
177
|
+
|
|
178
|
+
// Binary files (should not be forced as text)
|
|
179
|
+
{ name: "image.png", mime: "image/png", text: false },
|
|
180
|
+
{ name: "font.woff2", mime: "font/woff2", text: false },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
console.log("\nComprehensive Developer File Detection:");
|
|
184
|
+
console.log("=====================================");
|
|
185
|
+
|
|
186
|
+
for (const file of developerFiles) {
|
|
187
|
+
const filePath = path.join(testDir, file.name);
|
|
188
|
+
|
|
189
|
+
// Create appropriate content
|
|
190
|
+
const content = file.text
|
|
191
|
+
? `// Content for ${file.name}\nconst test = true;`
|
|
192
|
+
: Buffer.from([0x89, 0x50, 0x4e, 0x47]); // PNG-like header
|
|
193
|
+
|
|
194
|
+
await fs.writeFile(filePath, content);
|
|
195
|
+
|
|
196
|
+
const enhancedMime = getEnhancedMimeType(filePath);
|
|
197
|
+
const enhancedIsText = await isEnhancedTextFile(filePath);
|
|
198
|
+
const forced = shouldForceAsText(filePath);
|
|
199
|
+
|
|
200
|
+
console.log(
|
|
201
|
+
`${file.name.padEnd(20)} | MIME: ${enhancedMime.padEnd(
|
|
202
|
+
25
|
|
203
|
+
)} | Text: ${enhancedIsText} | Forced: ${forced}`
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
expect(enhancedMime).toBe(file.mime);
|
|
207
|
+
expect(enhancedIsText).toBe(file.text);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("Edge Cases and Fallbacks", () => {
|
|
213
|
+
it("should handle files without extensions", async () => {
|
|
214
|
+
const noExtFile = path.join(testDir, "README");
|
|
215
|
+
await fs.writeFile(noExtFile, "# This is a README file");
|
|
216
|
+
|
|
217
|
+
const enhancedMime = getEnhancedMimeType(noExtFile);
|
|
218
|
+
const enhancedIsText = await isEnhancedTextFile(noExtFile);
|
|
219
|
+
|
|
220
|
+
// Should fall back to content-based detection
|
|
221
|
+
expect(enhancedMime).toBe("application/octet-stream");
|
|
222
|
+
expect(enhancedIsText).toBe(true); // Detected as text by content
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should prioritize custom definitions over standard library", async () => {
|
|
226
|
+
// .ts files are wrongly detected as video/mp2t by standard library
|
|
227
|
+
const tsFile = path.join(testDir, "test.ts");
|
|
228
|
+
await fs.writeFile(tsFile, "const x: string = 'test';");
|
|
229
|
+
|
|
230
|
+
const standardMime = getMimeType(tsFile);
|
|
231
|
+
const enhancedMime = getEnhancedMimeType(tsFile);
|
|
232
|
+
|
|
233
|
+
expect(standardMime).toBe("video/mp2t"); // Wrong
|
|
234
|
+
expect(enhancedMime).toBe("text/typescript"); // Corrected by our custom definitions
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should handle empty files correctly", async () => {
|
|
238
|
+
const emptyFile = path.join(testDir, "empty.ts");
|
|
239
|
+
await fs.writeFile(emptyFile, "");
|
|
240
|
+
|
|
241
|
+
const enhancedMime = getEnhancedMimeType(emptyFile);
|
|
242
|
+
const enhancedIsText = await isEnhancedTextFile(emptyFile);
|
|
243
|
+
|
|
244
|
+
expect(enhancedMime).toBe("text/typescript");
|
|
245
|
+
expect(enhancedIsText).toBe(true); // Empty files should be treated as text
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should ensure TypeScript files are read as strings (integration test)", async () => {
|
|
249
|
+
const tsFile = path.join(testDir, "integration.ts");
|
|
250
|
+
const tsContent = "interface Config { apiUrl: string; timeout: number; }";
|
|
251
|
+
await fs.writeFile(tsFile, tsContent);
|
|
252
|
+
|
|
253
|
+
// Import readFileContent here to test integration
|
|
254
|
+
const { readFileContent } = await import("../../src/utils");
|
|
255
|
+
|
|
256
|
+
const result = await readFileContent(tsFile);
|
|
257
|
+
|
|
258
|
+
// Critical: TypeScript files MUST be read as strings
|
|
259
|
+
expect(typeof result).toBe("string");
|
|
260
|
+
expect(result).toBe(tsContent);
|
|
261
|
+
|
|
262
|
+
// This test would have FAILED before our fix when readFileContent
|
|
263
|
+
// used isTextFile() instead of isEnhancedTextFile()
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as tmp from "tmp";
|
|
4
|
+
import { SnapshotManager } from "../../src/core/snapshot";
|
|
5
|
+
import {
|
|
6
|
+
SyncSnapshot,
|
|
7
|
+
SnapshotFileEntry,
|
|
8
|
+
SnapshotDirectoryEntry,
|
|
9
|
+
} from "../../src/types";
|
|
10
|
+
import { UrlHeads } from "@automerge/automerge-repo";
|
|
11
|
+
|
|
12
|
+
describe("SnapshotManager", () => {
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
let cleanup: () => void;
|
|
15
|
+
let snapshotManager: SnapshotManager;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
const tmpObj = tmp.dirSync({ unsafeCleanup: true });
|
|
19
|
+
tmpDir = tmpObj.name;
|
|
20
|
+
cleanup = tmpObj.removeCallback;
|
|
21
|
+
snapshotManager = new SnapshotManager(tmpDir);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
cleanup();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("exists", () => {
|
|
29
|
+
it("should return false when no snapshot exists", async () => {
|
|
30
|
+
expect(await snapshotManager.exists()).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should return true when snapshot exists", async () => {
|
|
34
|
+
const snapshot = snapshotManager.createEmpty();
|
|
35
|
+
await snapshotManager.save(snapshot);
|
|
36
|
+
|
|
37
|
+
expect(await snapshotManager.exists()).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("createEmpty", () => {
|
|
42
|
+
it("should create an empty snapshot", () => {
|
|
43
|
+
const snapshot = snapshotManager.createEmpty();
|
|
44
|
+
|
|
45
|
+
expect(snapshot.rootPath).toBe(tmpDir);
|
|
46
|
+
expect(snapshot.timestamp).toBeGreaterThan(0);
|
|
47
|
+
expect(snapshot.files.size).toBe(0);
|
|
48
|
+
expect(snapshot.directories.size).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("save and load", () => {
|
|
53
|
+
it("should save and load empty snapshot", async () => {
|
|
54
|
+
const originalSnapshot = snapshotManager.createEmpty();
|
|
55
|
+
|
|
56
|
+
await snapshotManager.save(originalSnapshot);
|
|
57
|
+
const loadedSnapshot = await snapshotManager.load();
|
|
58
|
+
|
|
59
|
+
expect(loadedSnapshot).not.toBeNull();
|
|
60
|
+
expect(loadedSnapshot!.rootPath).toBe(originalSnapshot.rootPath);
|
|
61
|
+
expect(loadedSnapshot!.timestamp).toBe(originalSnapshot.timestamp);
|
|
62
|
+
expect(loadedSnapshot!.files.size).toBe(0);
|
|
63
|
+
expect(loadedSnapshot!.directories.size).toBe(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should save and load snapshot with files", async () => {
|
|
67
|
+
const snapshot = snapshotManager.createEmpty();
|
|
68
|
+
|
|
69
|
+
const fileEntry: SnapshotFileEntry = {
|
|
70
|
+
path: path.join(tmpDir, "test.txt"),
|
|
71
|
+
url: "automerge:test-url" as any,
|
|
72
|
+
head: ["test-head"] as UrlHeads,
|
|
73
|
+
extension: "txt",
|
|
74
|
+
mimeType: "text/plain",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
snapshotManager.updateFileEntry(snapshot, "test.txt", fileEntry);
|
|
78
|
+
|
|
79
|
+
await snapshotManager.save(snapshot);
|
|
80
|
+
const loadedSnapshot = await snapshotManager.load();
|
|
81
|
+
|
|
82
|
+
expect(loadedSnapshot).not.toBeNull();
|
|
83
|
+
expect(loadedSnapshot!.files.size).toBe(1);
|
|
84
|
+
expect(loadedSnapshot!.files.get("test.txt")).toEqual(fileEntry);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should save and load snapshot with directories", async () => {
|
|
88
|
+
const snapshot = snapshotManager.createEmpty();
|
|
89
|
+
|
|
90
|
+
const dirEntry: SnapshotDirectoryEntry = {
|
|
91
|
+
path: path.join(tmpDir, "subdir"),
|
|
92
|
+
url: "automerge:dir-url" as any,
|
|
93
|
+
head: ["dir-head"] as UrlHeads,
|
|
94
|
+
entries: ["file1.txt", "file2.txt"],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
snapshotManager.updateDirectoryEntry(snapshot, "subdir", dirEntry);
|
|
98
|
+
|
|
99
|
+
await snapshotManager.save(snapshot);
|
|
100
|
+
const loadedSnapshot = await snapshotManager.load();
|
|
101
|
+
|
|
102
|
+
expect(loadedSnapshot).not.toBeNull();
|
|
103
|
+
expect(loadedSnapshot!.directories.size).toBe(1);
|
|
104
|
+
expect(loadedSnapshot!.directories.get("subdir")).toEqual(dirEntry);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should return null when loading non-existent snapshot", async () => {
|
|
108
|
+
const loadedSnapshot = await snapshotManager.load();
|
|
109
|
+
expect(loadedSnapshot).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("updateFileEntry", () => {
|
|
114
|
+
it("should add new file entry", () => {
|
|
115
|
+
const snapshot = snapshotManager.createEmpty();
|
|
116
|
+
const originalTimestamp = snapshot.timestamp;
|
|
117
|
+
|
|
118
|
+
const fileEntry: SnapshotFileEntry = {
|
|
119
|
+
path: "/test/path/test.txt",
|
|
120
|
+
url: "automerge:test-url" as any,
|
|
121
|
+
head: ["test-head"] as UrlHeads,
|
|
122
|
+
extension: "txt",
|
|
123
|
+
mimeType: "text/plain",
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Add small delay to ensure timestamp changes
|
|
127
|
+
const startTime = Date.now();
|
|
128
|
+
while (Date.now() === startTime) {
|
|
129
|
+
// Wait for at least 1ms
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
snapshotManager.updateFileEntry(snapshot, "test.txt", fileEntry);
|
|
133
|
+
|
|
134
|
+
expect(snapshot.files.get("test.txt")).toEqual(fileEntry);
|
|
135
|
+
expect(snapshot.timestamp).toBeGreaterThan(originalTimestamp);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should update existing file entry", () => {
|
|
139
|
+
const snapshot = snapshotManager.createEmpty();
|
|
140
|
+
|
|
141
|
+
const fileEntry1: SnapshotFileEntry = {
|
|
142
|
+
path: path.join(tmpDir, "test.txt"),
|
|
143
|
+
url: "automerge:test-url" as any,
|
|
144
|
+
head: ["old-head"] as UrlHeads,
|
|
145
|
+
extension: "txt",
|
|
146
|
+
mimeType: "text/plain",
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const fileEntry2: SnapshotFileEntry = {
|
|
150
|
+
path: path.join(tmpDir, "test.txt"),
|
|
151
|
+
url: "automerge:test-url" as any,
|
|
152
|
+
head: ["new-head"] as UrlHeads,
|
|
153
|
+
extension: "txt",
|
|
154
|
+
mimeType: "text/plain",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
snapshotManager.updateFileEntry(snapshot, "test.txt", fileEntry1);
|
|
158
|
+
snapshotManager.updateFileEntry(snapshot, "test.txt", fileEntry2);
|
|
159
|
+
|
|
160
|
+
expect(snapshot.files.get("test.txt")).toEqual(fileEntry2);
|
|
161
|
+
expect(snapshot.files.size).toBe(1);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("removeFileEntry", () => {
|
|
166
|
+
it("should remove file entry", () => {
|
|
167
|
+
const snapshot = snapshotManager.createEmpty();
|
|
168
|
+
|
|
169
|
+
const fileEntry: SnapshotFileEntry = {
|
|
170
|
+
path: path.join(tmpDir, "test.txt"),
|
|
171
|
+
url: "automerge:test-url" as any,
|
|
172
|
+
head: ["test-head"] as UrlHeads,
|
|
173
|
+
extension: "txt",
|
|
174
|
+
mimeType: "text/plain",
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
snapshotManager.updateFileEntry(snapshot, "test.txt", fileEntry);
|
|
178
|
+
expect(snapshot.files.size).toBe(1);
|
|
179
|
+
|
|
180
|
+
snapshotManager.removeFileEntry(snapshot, "test.txt");
|
|
181
|
+
expect(snapshot.files.size).toBe(0);
|
|
182
|
+
expect(snapshot.files.get("test.txt")).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should not fail when removing non-existent file", () => {
|
|
186
|
+
const snapshot = snapshotManager.createEmpty();
|
|
187
|
+
|
|
188
|
+
snapshotManager.removeFileEntry(snapshot, "nonexistent.txt");
|
|
189
|
+
expect(snapshot.files.size).toBe(0);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("getFilePaths and getDirectoryPaths", () => {
|
|
194
|
+
it("should return all file paths", () => {
|
|
195
|
+
const snapshot = snapshotManager.createEmpty();
|
|
196
|
+
|
|
197
|
+
snapshotManager.updateFileEntry(snapshot, "file1.txt", {
|
|
198
|
+
path: path.join(tmpDir, "file1.txt"),
|
|
199
|
+
url: "automerge:url1" as any,
|
|
200
|
+
head: ["head1"] as UrlHeads,
|
|
201
|
+
extension: "txt",
|
|
202
|
+
mimeType: "text/plain",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
snapshotManager.updateFileEntry(snapshot, "file2.txt", {
|
|
206
|
+
path: path.join(tmpDir, "file2.txt"),
|
|
207
|
+
url: "automerge:url2" as any,
|
|
208
|
+
head: ["head2"] as UrlHeads,
|
|
209
|
+
extension: "txt",
|
|
210
|
+
mimeType: "text/plain",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const filePaths = snapshotManager.getFilePaths(snapshot);
|
|
214
|
+
expect(filePaths.sort()).toEqual(["file1.txt", "file2.txt"]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should return all directory paths", () => {
|
|
218
|
+
const snapshot = snapshotManager.createEmpty();
|
|
219
|
+
|
|
220
|
+
snapshotManager.updateDirectoryEntry(snapshot, "dir1", {
|
|
221
|
+
path: path.join(tmpDir, "dir1"),
|
|
222
|
+
url: "automerge:url1" as any,
|
|
223
|
+
head: ["head1"] as UrlHeads,
|
|
224
|
+
entries: [],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
snapshotManager.updateDirectoryEntry(snapshot, "dir2", {
|
|
228
|
+
path: path.join(tmpDir, "dir2"),
|
|
229
|
+
url: "automerge:url2" as any,
|
|
230
|
+
head: ["head2"] as UrlHeads,
|
|
231
|
+
entries: [],
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const dirPaths = snapshotManager.getDirectoryPaths(snapshot);
|
|
235
|
+
expect(dirPaths.sort()).toEqual(["dir1", "dir2"]);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("isTracked", () => {
|
|
240
|
+
it("should return true for tracked files", () => {
|
|
241
|
+
const snapshot = snapshotManager.createEmpty();
|
|
242
|
+
|
|
243
|
+
snapshotManager.updateFileEntry(snapshot, "test.txt", {
|
|
244
|
+
path: path.join(tmpDir, "test.txt"),
|
|
245
|
+
url: "automerge:url" as any,
|
|
246
|
+
head: ["head"] as UrlHeads,
|
|
247
|
+
extension: "txt",
|
|
248
|
+
mimeType: "text/plain",
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(snapshotManager.isTracked(snapshot, "test.txt")).toBe(true);
|
|
252
|
+
expect(snapshotManager.isTracked(snapshot, "other.txt")).toBe(false);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should return true for tracked directories", () => {
|
|
256
|
+
const snapshot = snapshotManager.createEmpty();
|
|
257
|
+
|
|
258
|
+
snapshotManager.updateDirectoryEntry(snapshot, "subdir", {
|
|
259
|
+
path: path.join(tmpDir, "subdir"),
|
|
260
|
+
url: "automerge:url" as any,
|
|
261
|
+
head: ["head"] as UrlHeads,
|
|
262
|
+
entries: [],
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(snapshotManager.isTracked(snapshot, "subdir")).toBe(true);
|
|
266
|
+
expect(snapshotManager.isTracked(snapshot, "other")).toBe(false);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("getStats", () => {
|
|
271
|
+
it("should return correct statistics", () => {
|
|
272
|
+
const snapshot = snapshotManager.createEmpty();
|
|
273
|
+
|
|
274
|
+
snapshotManager.updateFileEntry(snapshot, "file1.txt", {
|
|
275
|
+
path: path.join(tmpDir, "file1.txt"),
|
|
276
|
+
url: "automerge:url1" as any,
|
|
277
|
+
head: ["head1"] as UrlHeads,
|
|
278
|
+
extension: "txt",
|
|
279
|
+
mimeType: "text/plain",
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
snapshotManager.updateDirectoryEntry(snapshot, "dir1", {
|
|
283
|
+
path: path.join(tmpDir, "dir1"),
|
|
284
|
+
url: "automerge:url2" as any,
|
|
285
|
+
head: ["head2"] as UrlHeads,
|
|
286
|
+
entries: [],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const stats = snapshotManager.getStats(snapshot);
|
|
290
|
+
|
|
291
|
+
expect(stats.files).toBe(1);
|
|
292
|
+
expect(stats.directories).toBe(1);
|
|
293
|
+
expect(stats.timestamp).toBeInstanceOf(Date);
|
|
294
|
+
expect(stats.timestamp.getTime()).toBe(snapshot.timestamp);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("validate", () => {
|
|
299
|
+
it("should validate correct snapshot", () => {
|
|
300
|
+
const snapshot = snapshotManager.createEmpty();
|
|
301
|
+
|
|
302
|
+
const validation = snapshotManager.validate(snapshot);
|
|
303
|
+
|
|
304
|
+
expect(validation.valid).toBe(true);
|
|
305
|
+
expect(validation.errors).toHaveLength(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should detect invalid timestamp", () => {
|
|
309
|
+
const snapshot = snapshotManager.createEmpty();
|
|
310
|
+
snapshot.timestamp = 0;
|
|
311
|
+
|
|
312
|
+
const validation = snapshotManager.validate(snapshot);
|
|
313
|
+
|
|
314
|
+
expect(validation.valid).toBe(false);
|
|
315
|
+
expect(validation.errors).toContain("Invalid timestamp");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should detect missing root path", () => {
|
|
319
|
+
const snapshot = snapshotManager.createEmpty();
|
|
320
|
+
snapshot.rootPath = "";
|
|
321
|
+
|
|
322
|
+
const validation = snapshotManager.validate(snapshot);
|
|
323
|
+
|
|
324
|
+
expect(validation.valid).toBe(false);
|
|
325
|
+
expect(validation.errors).toContain("Missing root path");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("should detect path conflicts", () => {
|
|
329
|
+
const snapshot = snapshotManager.createEmpty();
|
|
330
|
+
|
|
331
|
+
snapshotManager.updateFileEntry(snapshot, "conflict", {
|
|
332
|
+
path: path.join(tmpDir, "conflict"),
|
|
333
|
+
url: "automerge:url1" as any,
|
|
334
|
+
head: ["head1"] as UrlHeads,
|
|
335
|
+
extension: "",
|
|
336
|
+
mimeType: "text/plain",
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
snapshotManager.updateDirectoryEntry(snapshot, "conflict", {
|
|
340
|
+
path: path.join(tmpDir, "conflict"),
|
|
341
|
+
url: "automerge:url2" as any,
|
|
342
|
+
head: ["head2"] as UrlHeads,
|
|
343
|
+
entries: [],
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const validation = snapshotManager.validate(snapshot);
|
|
347
|
+
|
|
348
|
+
expect(validation.valid).toBe(false);
|
|
349
|
+
expect(validation.errors).toContain(
|
|
350
|
+
"Path conflict: conflict exists as both file and directory"
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("backup", () => {
|
|
356
|
+
it("should create backup of existing snapshot", async () => {
|
|
357
|
+
const snapshot = snapshotManager.createEmpty();
|
|
358
|
+
await snapshotManager.save(snapshot);
|
|
359
|
+
|
|
360
|
+
await snapshotManager.backup();
|
|
361
|
+
|
|
362
|
+
// Check that backup file exists
|
|
363
|
+
const syncToolDir = path.join(tmpDir, ".pushwork");
|
|
364
|
+
const files = await fs.readdir(syncToolDir);
|
|
365
|
+
const backupFiles = files.filter((f) =>
|
|
366
|
+
f.startsWith("snapshot.json.backup.")
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
expect(backupFiles.length).toBe(1);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("should not fail when no snapshot exists", async () => {
|
|
373
|
+
await snapshotManager.backup(); // Should not throw
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe("clone", () => {
|
|
378
|
+
it("should create independent copy of snapshot", () => {
|
|
379
|
+
const originalSnapshot = snapshotManager.createEmpty();
|
|
380
|
+
|
|
381
|
+
snapshotManager.updateFileEntry(originalSnapshot, "test.txt", {
|
|
382
|
+
path: path.join(tmpDir, "test.txt"),
|
|
383
|
+
url: "automerge:url" as any,
|
|
384
|
+
head: ["head"] as UrlHeads,
|
|
385
|
+
extension: "txt",
|
|
386
|
+
mimeType: "text/plain",
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const clonedSnapshot = snapshotManager.clone(originalSnapshot);
|
|
390
|
+
|
|
391
|
+
// Modify clone
|
|
392
|
+
snapshotManager.removeFileEntry(clonedSnapshot, "test.txt");
|
|
393
|
+
|
|
394
|
+
// Original should be unchanged
|
|
395
|
+
expect(originalSnapshot.files.size).toBe(1);
|
|
396
|
+
expect(clonedSnapshot.files.size).toBe(0);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("clear", () => {
|
|
401
|
+
it("should clear all data from snapshot", async () => {
|
|
402
|
+
const snapshot = snapshotManager.createEmpty();
|
|
403
|
+
|
|
404
|
+
snapshotManager.updateFileEntry(snapshot, "test.txt", {
|
|
405
|
+
path: path.join(tmpDir, "test.txt"),
|
|
406
|
+
url: "automerge:url" as any,
|
|
407
|
+
head: ["head"] as UrlHeads,
|
|
408
|
+
extension: "txt",
|
|
409
|
+
mimeType: "text/plain",
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
snapshotManager.updateDirectoryEntry(snapshot, "subdir", {
|
|
413
|
+
path: path.join(tmpDir, "subdir"),
|
|
414
|
+
url: "automerge:url" as any,
|
|
415
|
+
head: ["head"] as UrlHeads,
|
|
416
|
+
entries: [],
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const originalTimestamp = snapshot.timestamp;
|
|
420
|
+
|
|
421
|
+
// Add small delay to ensure timestamp difference
|
|
422
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
423
|
+
|
|
424
|
+
snapshotManager.clear(snapshot);
|
|
425
|
+
|
|
426
|
+
expect(snapshot.files.size).toBe(0);
|
|
427
|
+
expect(snapshot.directories.size).toBe(0);
|
|
428
|
+
expect(snapshot.timestamp).toBeGreaterThan(originalTimestamp);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
});
|