Skip to content

Commit dfdd267

Browse files
authored
Backup Dyad on new versions (#595)
1 parent b6fd985 commit dfdd267

11 files changed

+693
-35
lines changed

e2e-tests/backup.spec.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import * as path from "path";
2+
import * as fs from "fs";
3+
import * as crypto from "crypto";
4+
import { testWithConfig, test, PageObject } from "./helpers/test_helper";
5+
import { expect } from "@playwright/test";
6+
7+
const testWithLastVersion = testWithConfig({
8+
preLaunchHook: async ({ userDataDir }) => {
9+
fs.mkdirSync(path.join(userDataDir), { recursive: true });
10+
fs.writeFileSync(path.join(userDataDir, ".last_version"), "0.1.0");
11+
fs.copyFileSync(
12+
path.join(__dirname, "fixtures", "backups", "empty-v0.12.0-beta.1.db"),
13+
path.join(userDataDir, "sqlite.db"),
14+
);
15+
},
16+
});
17+
18+
const testWithMultipleBackups = testWithConfig({
19+
preLaunchHook: async ({ userDataDir }) => {
20+
fs.mkdirSync(path.join(userDataDir), { recursive: true });
21+
// Make sure there's a last version file so the version upgrade is detected.
22+
fs.writeFileSync(path.join(userDataDir, ".last_version"), "0.1.0");
23+
24+
// Create backups directory
25+
const backupsDir = path.join(userDataDir, "backups");
26+
fs.mkdirSync(backupsDir, { recursive: true });
27+
28+
// Create 5 mock backup directories with different timestamps
29+
// These timestamps are in ascending order (oldest to newest)
30+
const mockBackups = [
31+
{
32+
name: "v1.0.0_2023-01-01T10-00-00-000Z_upgrade_from_0.9.0",
33+
timestamp: "2023-01-01T10:00:00.000Z",
34+
version: "1.0.0",
35+
reason: "upgrade_from_0.9.0",
36+
},
37+
{
38+
name: "v1.0.1_2023-01-02T10-00-00-000Z_upgrade_from_1.0.0",
39+
timestamp: "2023-01-02T10:00:00.000Z",
40+
version: "1.0.1",
41+
reason: "upgrade_from_1.0.0",
42+
},
43+
{
44+
name: "v1.0.2_2023-01-03T10-00-00-000Z_upgrade_from_1.0.1",
45+
timestamp: "2023-01-03T10:00:00.000Z",
46+
version: "1.0.2",
47+
reason: "upgrade_from_1.0.1",
48+
},
49+
{
50+
name: "v1.0.3_2023-01-04T10-00-00-000Z_upgrade_from_1.0.2",
51+
timestamp: "2023-01-04T10:00:00.000Z",
52+
version: "1.0.3",
53+
reason: "upgrade_from_1.0.2",
54+
},
55+
{
56+
name: "v1.0.4_2023-01-05T10-00-00-000Z_upgrade_from_1.0.3",
57+
timestamp: "2023-01-05T10:00:00.000Z",
58+
version: "1.0.4",
59+
reason: "upgrade_from_1.0.3",
60+
},
61+
];
62+
63+
// Create each backup directory with realistic structure
64+
for (const backup of mockBackups) {
65+
const backupPath = path.join(backupsDir, backup.name);
66+
fs.mkdirSync(backupPath, { recursive: true });
67+
68+
// Create backup metadata
69+
const metadata = {
70+
version: backup.version,
71+
timestamp: backup.timestamp,
72+
reason: backup.reason,
73+
files: {
74+
settings: true,
75+
database: true,
76+
},
77+
checksums: {
78+
settings: "mock_settings_checksum_" + backup.version,
79+
database: "mock_database_checksum_" + backup.version,
80+
},
81+
};
82+
83+
fs.writeFileSync(
84+
path.join(backupPath, "backup.json"),
85+
JSON.stringify(metadata, null, 2),
86+
);
87+
88+
// Create mock backup files
89+
fs.writeFileSync(
90+
path.join(backupPath, "user-settings.json"),
91+
JSON.stringify({ version: backup.version, mockData: true }, null, 2),
92+
);
93+
94+
fs.writeFileSync(
95+
path.join(backupPath, "sqlite.db"),
96+
`mock_database_content_${backup.version}`,
97+
);
98+
}
99+
},
100+
});
101+
102+
const ensureAppIsRunning = async (po: PageObject) => {
103+
await po.page.waitForSelector("h1");
104+
const text = await po.page.$eval("h1", (el) => el.textContent);
105+
expect(text).toBe("Build your dream app");
106+
};
107+
108+
test("backup is not created for first run", async ({ po }) => {
109+
await ensureAppIsRunning(po);
110+
111+
expect(fs.existsSync(path.join(po.userDataDir, "backups"))).toEqual(false);
112+
});
113+
114+
testWithLastVersion(
115+
"backup is created if version is upgraded",
116+
async ({ po }) => {
117+
await ensureAppIsRunning(po);
118+
119+
const backups = fs.readdirSync(path.join(po.userDataDir, "backups"));
120+
expect(backups).toHaveLength(1);
121+
const backupDir = path.join(po.userDataDir, "backups", backups[0]);
122+
const backupMetadata = JSON.parse(
123+
fs.readFileSync(path.join(backupDir, "backup.json"), "utf8"),
124+
);
125+
126+
expect(backupMetadata.version).toBeDefined();
127+
expect(backupMetadata.timestamp).toBeDefined();
128+
expect(backupMetadata.reason).toBe("upgrade_from_0.1.0");
129+
expect(backupMetadata.files.settings).toBe(true);
130+
expect(backupMetadata.files.database).toBe(true);
131+
expect(backupMetadata.checksums.settings).toBeDefined();
132+
expect(backupMetadata.checksums.database).toBeDefined();
133+
134+
// Compare the backup files to the original files
135+
const originalSettings = fs.readFileSync(
136+
path.join(po.userDataDir, "user-settings.json"),
137+
"utf8",
138+
);
139+
const backupSettings = fs.readFileSync(
140+
path.join(backupDir, "user-settings.json"),
141+
"utf8",
142+
);
143+
expect(cleanSettings(backupSettings)).toEqual(
144+
cleanSettings(originalSettings),
145+
);
146+
147+
// For database, verify the backup file exists and has correct checksum
148+
const backupDbPath = path.join(backupDir, "sqlite.db");
149+
const originalDbPath = path.join(po.userDataDir, "sqlite.db");
150+
151+
expect(fs.existsSync(backupDbPath)).toBe(true);
152+
expect(fs.existsSync(originalDbPath)).toBe(true);
153+
154+
const backupChecksum = calculateChecksum(backupDbPath);
155+
// Verify backup metadata contains the correct checksum
156+
expect(backupMetadata.checksums.database).toBe(backupChecksum);
157+
},
158+
);
159+
160+
testWithMultipleBackups(
161+
"backup cleanup deletes oldest backups when exceeding MAX_BACKUPS",
162+
async ({ po }) => {
163+
await ensureAppIsRunning(po);
164+
165+
const backupsDir = path.join(po.userDataDir, "backups");
166+
const backups = fs.readdirSync(backupsDir);
167+
168+
// Should have only 3 backups remaining (MAX_BACKUPS = 3)
169+
expect(backups).toHaveLength(3);
170+
171+
const expectedRemainingBackups = [
172+
"*",
173+
// These are the two older backups
174+
"v1.0.4_2023-01-05T10-00-00-000Z_upgrade_from_1.0.3",
175+
"v1.0.3_2023-01-04T10-00-00-000Z_upgrade_from_1.0.2",
176+
];
177+
178+
// Check that the expected backups exist
179+
for (let backup of expectedRemainingBackups) {
180+
let expectedBackup = backup;
181+
if (backup === "*") {
182+
expectedBackup = backups[0];
183+
expect(expectedBackup.endsWith("_upgrade_from_0.1.0")).toEqual(true);
184+
} else {
185+
expect(backups).toContain(expectedBackup);
186+
}
187+
188+
// Verify the backup directory and metadata still exist
189+
const backupPath = path.join(backupsDir, expectedBackup);
190+
expect(fs.existsSync(backupPath)).toBe(true);
191+
expect(fs.existsSync(path.join(backupPath, "backup.json"))).toBe(true);
192+
expect(fs.existsSync(path.join(backupPath, "user-settings.json"))).toBe(
193+
true,
194+
);
195+
196+
// The first backup does NOT have a SQLite database because the backup
197+
// manager is run before the DB is initialized.
198+
expect(fs.existsSync(path.join(backupPath, "sqlite.db"))).toBe(
199+
backup !== "*",
200+
);
201+
}
202+
203+
// The 2 oldest backups should have been deleted
204+
const deletedBackups = [
205+
"v1.0.0_2023-01-01T10-00-00-000Z_upgrade_from_0.9.0", // oldest
206+
"v1.0.1_2023-01-02T10-00-00-000Z_upgrade_from_1.0.0", // second oldest
207+
"v1.0.2_2023-01-03T10-00-00-000Z_upgrade_from_1.0.1", // third oldest
208+
];
209+
210+
for (const deletedBackup of deletedBackups) {
211+
expect(backups).not.toContain(deletedBackup);
212+
expect(fs.existsSync(path.join(backupsDir, deletedBackup))).toBe(false);
213+
}
214+
},
215+
);
216+
217+
function cleanSettings(settings: string) {
218+
const parsed = JSON.parse(settings);
219+
delete parsed.hasRunBefore;
220+
delete parsed.isTestMode;
221+
delete parsed.lastShownReleaseNotesVersion;
222+
return parsed;
223+
}
224+
225+
function calculateChecksum(filePath: string): string {
226+
const fileBuffer = fs.readFileSync(filePath);
227+
const hash = crypto.createHash("sha256");
228+
hash.update(fileBuffer);
229+
return hash.digest("hex");
230+
}
Binary file not shown.

e2e-tests/helpers/test_helper.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ class GitHubConnector {
187187
}
188188

189189
export class PageObject {
190-
private userDataDir: string;
190+
public userDataDir: string;
191191
public githubConnector: GitHubConnector;
192192
constructor(
193193
public electronApp: ElectronApplication,
@@ -935,15 +935,27 @@ export class PageObject {
935935
}
936936
}
937937

938+
interface ElectronConfig {
939+
preLaunchHook?: ({ userDataDir }: { userDataDir: string }) => Promise<void>;
940+
}
941+
938942
// From https://github.com/microsoft/playwright/issues/8208#issuecomment-1435475930
939943
//
940944
// Note how we mark the fixture as { auto: true }.
941945
// This way it is always instantiated, even if the test does not use it explicitly.
942946
export const test = base.extend<{
947+
electronConfig: ElectronConfig;
943948
attachScreenshotsToReport: void;
944949
electronApp: ElectronApplication;
945950
po: PageObject;
946951
}>({
952+
electronConfig: [
953+
async ({}, use) => {
954+
// Default configuration - tests can override this fixture
955+
await use({});
956+
},
957+
{ auto: true },
958+
],
947959
po: [
948960
async ({ electronApp }, use) => {
949961
const page = await electronApp.firstWindow();
@@ -976,7 +988,7 @@ export const test = base.extend<{
976988
{ auto: true },
977989
],
978990
electronApp: [
979-
async ({}, use) => {
991+
async ({ electronConfig }, use) => {
980992
// find the latest build in the out directory
981993
const latestBuild = eph.findLatestBuild();
982994
// parse the directory and find paths and other info
@@ -990,15 +1002,15 @@ export const test = base.extend<{
9901002
// This is just a hack to avoid the AI setup screen.
9911003
process.env.OPENAI_API_KEY = "sk-test";
9921004
const baseTmpDir = os.tmpdir();
993-
const USER_DATA_DIR = path.join(
994-
baseTmpDir,
995-
`dyad-e2e-tests-${Date.now()}`,
996-
);
1005+
const userDataDir = path.join(baseTmpDir, `dyad-e2e-tests-${Date.now()}`);
1006+
if (electronConfig.preLaunchHook) {
1007+
await electronConfig.preLaunchHook({ userDataDir });
1008+
}
9971009
const electronApp = await electron.launch({
9981010
args: [
9991011
appInfo.main,
10001012
"--enable-logging",
1001-
`--user-data-dir=${USER_DATA_DIR}`,
1013+
`--user-data-dir=${userDataDir}`,
10021014
],
10031015
executablePath: appInfo.executable,
10041016
// Strong suspicion this is causing issues on Windows with tests hanging due to error:
@@ -1007,7 +1019,7 @@ export const test = base.extend<{
10071019
// dir: "test-results",
10081020
// },
10091021
});
1010-
(electronApp as any).$dyadUserDataDir = USER_DATA_DIR;
1022+
(electronApp as any).$dyadUserDataDir = userDataDir;
10111023

10121024
console.log("electronApp launched!");
10131025
if (showDebugLogs) {
@@ -1064,6 +1076,14 @@ export const test = base.extend<{
10641076
],
10651077
});
10661078

1079+
export function testWithConfig(config: ElectronConfig) {
1080+
return test.extend({
1081+
electronConfig: async ({}, use) => {
1082+
await use(config);
1083+
},
1084+
});
1085+
}
1086+
10671087
// Wrapper that skips tests on Windows platform
10681088
export const testSkipIfWindows = os.platform() === "win32" ? test.skip : test;
10691089

e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-1.aria.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
- img
22
- text: 1 error
3-
- button "Recheck":
3+
- button "Run checks":
44
- img
55
- button "Fix All":
66
- img
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
- paragraph: No problems found
2-
- button "Recheck":
2+
- img
3+
- button "Run checks":
34
- img

e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-1.aria.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
- img
22
- text: 1 error
3-
- button "Recheck":
3+
- button "Run checks":
44
- img
55
- button "Fix All":
66
- img
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
- paragraph: No problems found
2-
- button "Recheck":
2+
- img
3+
- button "Run checks":
34
- img

0 commit comments

Comments
 (0)