mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-02 02:38:38 +00:00
Why
- Legacy installs can have a sessions table shape that predates provider/custom_name columns. Running migrateLegacySessionNames first caused its INSERT OR REPLACE INTO sessions (...) to target columns that may not exist and fail during startup migration.
- Some upgraded databases had projects.project_id as plain TEXT instead of a real PRIMARY KEY. That breaks assumptions used by id-based lookups and can allow invalid/duplicate identity semantics over time.
- projectsDb.createProjectPath inferred outcomes from
ow.isArchived, but the upsert path always returns the post-update row with isArchived=0, so archived-reactivation and fresh-create could be misclassified.
- git clone accepted user-controlled URLs directly in argv position, so inputs beginning with - could be interpreted as options instead of a repository argument.
What
- Added
ebuildProjectsTableWithPrimaryKeySchema in migrations: detect table shape via getTableInfo('projects'), verify project_id has pk=1, and rebuild when missing.
- Rebuild flow now creates a canonical projects__new table (project_id TEXT PRIMARY KEY), copies rows with transformation, backfills empty ids via SQLITE_UUID_SQL, deduplicates conflicting ids/paths, then swaps tables inside a transaction.
- Replaced the prior ddColumnToTableIfNotExists(...) + UPDATE project_id sequence with PK-aware detection/rebuild logic so legacy DBs converge to the required schema.
- Reordered migration sequence to run
ebuildSessionsTableWithProjectSchema before migrateLegacySessionNames, ensuring sessions is normalized before legacy session_names merge writes execute.
- Updated projectsDb.createProjectPath to generate an ttemptedId before insert, pass it into the prepared statement, and classify outcomes by comparing returned
ow.project_id to ttemptedId (created vs
eactivated_archived), with no-row remaining ctive_conflict.
- Hardened clone execution by inserting -- before clone URL in git argv and rejecting normalized GitHub URLs that start with - in startCloneProject.
Tests
- Added integration coverage for projectsDb.createProjectPath branches: fresh insert, archived reactivation, and active conflict.
- Added clone service test for option-prefixed githubUrl rejection (INVALID_GITHUB_URL).
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import { closeConnection } from '@/modules/database/connection.js';
|
|
import { initializeDatabase } from '@/modules/database/init-db.js';
|
|
import { projectsDb } from '@/modules/database/repositories/projects.db.js';
|
|
|
|
async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
const tempDirectory = await mkdtemp(path.join(tmpdir(), 'projects-db-'));
|
|
const databasePath = path.join(tempDirectory, 'auth.db');
|
|
|
|
closeConnection();
|
|
process.env.DATABASE_PATH = databasePath;
|
|
await initializeDatabase();
|
|
|
|
try {
|
|
await runTest();
|
|
} finally {
|
|
closeConnection();
|
|
if (previousDatabasePath === undefined) {
|
|
delete process.env.DATABASE_PATH;
|
|
} else {
|
|
process.env.DATABASE_PATH = previousDatabasePath;
|
|
}
|
|
await rm(tempDirectory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
test('projectsDb.createProjectPath returns created for fresh paths', async () => {
|
|
await withIsolatedDatabase(() => {
|
|
const created = projectsDb.createProjectPath('/workspace/new-project');
|
|
|
|
assert.equal(created.outcome, 'created');
|
|
assert.ok(created.project);
|
|
assert.equal(created.project?.project_path, '/workspace/new-project');
|
|
assert.equal(created.project?.isArchived, 0);
|
|
});
|
|
});
|
|
|
|
test('projectsDb.createProjectPath returns reactivated_archived for archived duplicates', async () => {
|
|
await withIsolatedDatabase(() => {
|
|
const initial = projectsDb.createProjectPath('/workspace/archived-project', 'Archived Project');
|
|
assert.equal(initial.outcome, 'created');
|
|
assert.ok(initial.project);
|
|
|
|
projectsDb.updateProjectIsArchived('/workspace/archived-project', true);
|
|
|
|
const reused = projectsDb.createProjectPath('/workspace/archived-project', 'Renamed Project');
|
|
assert.equal(reused.outcome, 'reactivated_archived');
|
|
assert.ok(reused.project);
|
|
assert.equal(reused.project?.project_id, initial.project?.project_id);
|
|
assert.equal(reused.project?.isArchived, 0);
|
|
});
|
|
});
|
|
|
|
test('projectsDb.createProjectPath returns active_conflict for active duplicates', async () => {
|
|
await withIsolatedDatabase(() => {
|
|
const initial = projectsDb.createProjectPath('/workspace/active-project');
|
|
assert.equal(initial.outcome, 'created');
|
|
assert.ok(initial.project);
|
|
|
|
const conflict = projectsDb.createProjectPath('/workspace/active-project');
|
|
assert.equal(conflict.outcome, 'active_conflict');
|
|
assert.ok(conflict.project);
|
|
assert.equal(conflict.project?.project_id, initial.project?.project_id);
|
|
assert.equal(conflict.project?.isArchived, 0);
|
|
});
|
|
});
|