Fix Garmin token storage and flaky e2e test

1. Increase garminOauth1Token and garminOauth2Token max length from
   5000 to 20000 characters to accommodate encrypted OAuth tokens.
   Add logic to update existing field constraints in addUserFields().

2. Fix flaky pocketbase-harness e2e test by adding retry logic with
   exponential backoff to createAdminUser() and createTestUser().
   Handles SQLite database lock during PocketBase startup migrations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-14 12:52:01 +00:00
parent 00b84d0b22
commit 6df145d916
3 changed files with 166 additions and 47 deletions

View File

@@ -83,19 +83,52 @@ async function waitForReady(url: string, timeoutMs = 30000): Promise<void> {
}
/**
* Creates the admin superuser using the PocketBase CLI.
* Sleeps for the specified number of milliseconds.
*/
function createAdminUser(
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Creates the admin superuser using the PocketBase CLI.
* Retries on database lock errors since PocketBase may still be running migrations.
*/
async function createAdminUser(
dataDir: string,
email: string,
password: string,
): void {
execSync(
`pocketbase superuser upsert ${email} ${password} --dir=${dataDir}`,
{
stdio: "pipe",
},
);
maxRetries = 5,
): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
execSync(
`pocketbase superuser upsert ${email} ${password} --dir=${dataDir}`,
{
stdio: "pipe",
},
);
return;
} catch (err) {
lastError = err as Error;
const errorMsg = String(lastError.message || lastError);
// Retry on database lock errors
if (
errorMsg.includes("database is locked") ||
errorMsg.includes("SQLITE_BUSY")
) {
await sleep(100 * (attempt + 1)); // Exponential backoff: 100ms, 200ms, 300ms...
continue;
}
// For other errors, throw immediately
throw err;
}
}
throw lastError;
}
/**
@@ -186,6 +219,41 @@ async function setupCollections(pb: PocketBase): Promise<void> {
await setupApiRules(pb);
}
/**
* Retries an async operation with exponential backoff.
*/
async function retryAsync<T>(
operation: () => Promise<T>,
maxRetries = 5,
baseDelayMs = 100,
): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await operation();
} catch (err) {
lastError = err as Error;
const errorMsg = String(lastError.message || lastError);
// Retry on transient errors (database busy, connection issues)
if (
errorMsg.includes("database is locked") ||
errorMsg.includes("SQLITE_BUSY") ||
errorMsg.includes("Failed to create record")
) {
await sleep(baseDelayMs * (attempt + 1));
continue;
}
// For other errors, throw immediately
throw err;
}
}
throw lastError;
}
/**
* Creates the test user with period data.
*/
@@ -199,24 +267,28 @@ async function createTestUser(
fourteenDaysAgo.setDate(fourteenDaysAgo.getDate() - 14);
const lastPeriodDate = fourteenDaysAgo.toISOString().split("T")[0];
// Create the test user
const user = await pb.collection("users").create({
email,
password,
passwordConfirm: password,
emailVisibility: true,
verified: true,
lastPeriodDate,
cycleLength: 28,
notificationTime: "07:00",
timezone: "UTC",
});
// Create the test user (with retry for transient errors)
const user = await retryAsync(() =>
pb.collection("users").create({
email,
password,
passwordConfirm: password,
emailVisibility: true,
verified: true,
lastPeriodDate,
cycleLength: 28,
notificationTime: "07:00",
timezone: "UTC",
}),
);
// Create a period log entry
await pb.collection("period_logs").create({
user: user.id,
startDate: lastPeriodDate,
});
// Create a period log entry (with retry for transient errors)
await retryAsync(() =>
pb.collection("period_logs").create({
user: user.id,
startDate: lastPeriodDate,
}),
);
return user.id;
}
@@ -254,8 +326,8 @@ export async function start(
// Wait for PocketBase to be ready
await waitForReady(url);
// Create admin user via CLI
createAdminUser(dataDir, config.adminEmail, config.adminPassword);
// Create admin user via CLI (with retry for database lock during migrations)
await createAdminUser(dataDir, config.adminEmail, config.adminPassword);
// Connect to PocketBase as admin
const pb = new PocketBase(url);