mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
fix ci test
This commit is contained in:
parent
37c3a38bb1
commit
d3dfc26dea
7 changed files with 186 additions and 42 deletions
|
|
@ -7,7 +7,7 @@
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import { getProjectHash } from '../utils/paths.js';
|
import { getProjectHash, getLegacyProjectHash } from '../utils/paths.js';
|
||||||
|
|
||||||
export const QWEN_DIR = '.qwen';
|
export const QWEN_DIR = '.qwen';
|
||||||
export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json';
|
export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json';
|
||||||
|
|
@ -90,7 +90,25 @@ export class Storage {
|
||||||
getProjectTempDir(): string {
|
getProjectTempDir(): string {
|
||||||
const hash = getProjectHash(this.getProjectRoot());
|
const hash = getProjectHash(this.getProjectRoot());
|
||||||
const tempDir = Storage.getGlobalTempDir();
|
const tempDir = Storage.getGlobalTempDir();
|
||||||
return path.join(tempDir, hash);
|
const targetDir = path.join(tempDir, hash);
|
||||||
|
|
||||||
|
// Backward compatibility: On Windows, check if legacy directory exists
|
||||||
|
// and migrate it to the new normalized path
|
||||||
|
if (os.platform() === 'win32' && !fs.existsSync(targetDir)) {
|
||||||
|
const legacyHash = getLegacyProjectHash(this.getProjectRoot());
|
||||||
|
const legacyDir = path.join(tempDir, legacyHash);
|
||||||
|
|
||||||
|
if (fs.existsSync(legacyDir) && legacyHash !== hash) {
|
||||||
|
try {
|
||||||
|
// Attempt to rename/migrate the directory
|
||||||
|
fs.renameSync(legacyDir, targetDir);
|
||||||
|
} catch (_error) {
|
||||||
|
// Silent fallback: if migration fails, continue with the new path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureProjectTempDirExists(): void {
|
ensureProjectTempDirExists(): void {
|
||||||
|
|
@ -108,7 +126,25 @@ export class Storage {
|
||||||
getHistoryDir(): string {
|
getHistoryDir(): string {
|
||||||
const hash = getProjectHash(this.getProjectRoot());
|
const hash = getProjectHash(this.getProjectRoot());
|
||||||
const historyDir = path.join(Storage.getGlobalQwenDir(), 'history');
|
const historyDir = path.join(Storage.getGlobalQwenDir(), 'history');
|
||||||
return path.join(historyDir, hash);
|
const targetDir = path.join(historyDir, hash);
|
||||||
|
|
||||||
|
// Backward compatibility: On Windows, check if legacy directory exists
|
||||||
|
// and migrate it to the new normalized path
|
||||||
|
if (os.platform() === 'win32' && !fs.existsSync(targetDir)) {
|
||||||
|
const legacyHash = getLegacyProjectHash(this.getProjectRoot());
|
||||||
|
const legacyDir = path.join(historyDir, legacyHash);
|
||||||
|
|
||||||
|
if (fs.existsSync(legacyDir) && legacyHash !== hash) {
|
||||||
|
try {
|
||||||
|
// Attempt to rename/migrate the directory
|
||||||
|
fs.renameSync(legacyDir, targetDir);
|
||||||
|
} catch (_error) {
|
||||||
|
// Silent fallback: if migration fails, continue with the new path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
getWorkspaceSettingsPath(): string {
|
getWorkspaceSettingsPath(): string {
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ import {
|
||||||
decodeTagName,
|
decodeTagName,
|
||||||
} from './logger.js';
|
} from './logger.js';
|
||||||
import { Storage } from '../config/storage.js';
|
import { Storage } from '../config/storage.js';
|
||||||
|
import { getProjectHash } from '../utils/paths.js';
|
||||||
import { promises as fs, existsSync } from 'node:fs';
|
import { promises as fs, existsSync } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import type { Content } from '@google/genai';
|
import type { Content } from '@google/genai';
|
||||||
|
|
||||||
import crypto from 'node:crypto';
|
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
|
|
||||||
const GEMINI_DIR_NAME = '.qwen';
|
const GEMINI_DIR_NAME = '.qwen';
|
||||||
|
|
@ -34,7 +34,7 @@ const LOG_FILE_NAME = 'logs.json';
|
||||||
const CHECKPOINT_FILE_NAME = 'checkpoint.json';
|
const CHECKPOINT_FILE_NAME = 'checkpoint.json';
|
||||||
|
|
||||||
const projectDir = process.cwd();
|
const projectDir = process.cwd();
|
||||||
const hash = crypto.createHash('sha256').update(projectDir).digest('hex');
|
const hash = getProjectHash(projectDir);
|
||||||
const TEST_HOME_DIR = path.join(os.tmpdir(), 'qwen-core-logger-home');
|
const TEST_HOME_DIR = path.join(os.tmpdir(), 'qwen-core-logger-home');
|
||||||
|
|
||||||
let originalHome: string | undefined;
|
let originalHome: string | undefined;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
shortenPath,
|
shortenPath,
|
||||||
tildeifyPath,
|
tildeifyPath,
|
||||||
getProjectHash,
|
getProjectHash,
|
||||||
|
getLegacyProjectHash,
|
||||||
} from './paths.js';
|
} from './paths.js';
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
|
|
||||||
|
|
@ -848,3 +849,55 @@ describe('getProjectHash', () => {
|
||||||
platformSpy.mockRestore();
|
platformSpy.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getLegacyProjectHash', () => {
|
||||||
|
it('should always be case-sensitive regardless of platform', () => {
|
||||||
|
const platformSpy = vi.spyOn(os, 'platform');
|
||||||
|
|
||||||
|
// Test on Windows - should still be case-sensitive
|
||||||
|
platformSpy.mockReturnValue('win32');
|
||||||
|
const lowerCaseHash = getLegacyProjectHash('c:\\users\\test\\project');
|
||||||
|
const upperCaseHash = getLegacyProjectHash('C:\\USERS\\TEST\\PROJECT');
|
||||||
|
expect(lowerCaseHash).not.toBe(upperCaseHash);
|
||||||
|
|
||||||
|
// Test on Linux - should be case-sensitive
|
||||||
|
platformSpy.mockReturnValue('linux');
|
||||||
|
const lowerCaseHashLinux = getLegacyProjectHash('/home/user/project');
|
||||||
|
const upperCaseHashLinux = getLegacyProjectHash('/HOME/USER/PROJECT');
|
||||||
|
expect(lowerCaseHashLinux).not.toBe(upperCaseHashLinux);
|
||||||
|
|
||||||
|
platformSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate different hash than getProjectHash on Windows with mixed case', () => {
|
||||||
|
const platformSpy = vi.spyOn(os, 'platform');
|
||||||
|
platformSpy.mockReturnValue('win32');
|
||||||
|
|
||||||
|
const mixedCasePath = 'C:\\Users\\Test\\Project';
|
||||||
|
const legacyHash = getLegacyProjectHash(mixedCasePath);
|
||||||
|
const newHash = getProjectHash(mixedCasePath);
|
||||||
|
|
||||||
|
// They should be different because getProjectHash normalizes to lowercase
|
||||||
|
expect(legacyHash).not.toBe(newHash);
|
||||||
|
|
||||||
|
// But both lowercase paths should match the new hash
|
||||||
|
const lowercaseNewHash = getProjectHash(mixedCasePath.toLowerCase());
|
||||||
|
expect(newHash).toBe(lowercaseNewHash);
|
||||||
|
|
||||||
|
platformSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match getProjectHash on non-Windows platforms', () => {
|
||||||
|
const platformSpy = vi.spyOn(os, 'platform');
|
||||||
|
platformSpy.mockReturnValue('linux');
|
||||||
|
|
||||||
|
const testPath = '/home/user/project';
|
||||||
|
const legacyHash = getLegacyProjectHash(testPath);
|
||||||
|
const newHash = getProjectHash(testPath);
|
||||||
|
|
||||||
|
// On non-Windows platforms, both should be identical
|
||||||
|
expect(legacyHash).toBe(newHash);
|
||||||
|
|
||||||
|
platformSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,17 @@ export function getProjectHash(projectRoot: string): string {
|
||||||
return crypto.createHash('sha256').update(normalizedPath).digest('hex');
|
return crypto.createHash('sha256').update(normalizedPath).digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a hash using the legacy algorithm (without case normalization).
|
||||||
|
* This is used for backward compatibility to locate session directories
|
||||||
|
* created before the case-insensitive fix on Windows.
|
||||||
|
* @param projectRoot The absolute path to the project's root directory.
|
||||||
|
* @returns A SHA256 hash of the project root path without normalization.
|
||||||
|
*/
|
||||||
|
export function getLegacyProjectHash(projectRoot: string): string {
|
||||||
|
return crypto.createHash('sha256').update(projectRoot).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a path is a subpath of another path.
|
* Checks if a path is a subpath of another path.
|
||||||
* @param parentPath The parent path.
|
* @param parentPath The parent path.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
import {
|
||||||
|
getProjectHash,
|
||||||
|
getLegacyProjectHash,
|
||||||
|
} from '@qwen-code/qwen-code-core/src/utils/paths.js';
|
||||||
import type { QwenSession, QwenMessage } from './qwenSessionReader.js';
|
import type { QwenSession, QwenMessage } from './qwenSessionReader.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,24 +32,36 @@ export class QwenSessionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate project hash (same as CLI)
|
* Get the session directory for a project with backward compatibility
|
||||||
* Qwen CLI uses SHA256 hash of the project path.
|
|
||||||
* On Windows, paths are case-insensitive, so we normalize to lowercase
|
|
||||||
* to ensure the same physical path always produces the same hash.
|
|
||||||
*/
|
|
||||||
private getProjectHash(workingDir: string): string {
|
|
||||||
// On Windows, normalize path to lowercase for case-insensitive matching
|
|
||||||
const normalizedPath =
|
|
||||||
os.platform() === 'win32' ? workingDir.toLowerCase() : workingDir;
|
|
||||||
return crypto.createHash('sha256').update(normalizedPath).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the session directory for a project
|
|
||||||
*/
|
*/
|
||||||
private getSessionDir(workingDir: string): string {
|
private getSessionDir(workingDir: string): string {
|
||||||
const projectHash = this.getProjectHash(workingDir);
|
const projectHash = getProjectHash(workingDir);
|
||||||
return path.join(this.qwenDir, 'tmp', projectHash, 'chats');
|
const sessionDir = path.join(this.qwenDir, 'tmp', projectHash, 'chats');
|
||||||
|
|
||||||
|
// Backward compatibility: On Windows, check if legacy directory exists
|
||||||
|
// and migrate it to the new normalized path
|
||||||
|
if (os.platform() === 'win32' && !fs.existsSync(sessionDir)) {
|
||||||
|
const legacyHash = getLegacyProjectHash(workingDir);
|
||||||
|
const legacySessionDir = path.join(
|
||||||
|
this.qwenDir,
|
||||||
|
'tmp',
|
||||||
|
legacyHash,
|
||||||
|
'chats',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(legacySessionDir) && legacyHash !== projectHash) {
|
||||||
|
try {
|
||||||
|
// Migrate parent directory (hash directory, not just chats)
|
||||||
|
const newParentDir = path.join(this.qwenDir, 'tmp', projectHash);
|
||||||
|
const legacyParentDir = path.join(this.qwenDir, 'tmp', legacyHash);
|
||||||
|
fs.renameSync(legacyParentDir, newParentDir);
|
||||||
|
} catch (_error) {
|
||||||
|
// Silent fallback: if migration fails, continue with the new path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -92,7 +108,7 @@ export class QwenSessionManager {
|
||||||
// Create session object
|
// Create session object
|
||||||
const session: QwenSession = {
|
const session: QwenSession = {
|
||||||
sessionId,
|
sessionId,
|
||||||
projectHash: this.getProjectHash(workingDir),
|
projectHash: getProjectHash(workingDir),
|
||||||
startTime: messages[0]?.timestamp || new Date().toISOString(),
|
startTime: messages[0]?.timestamp || new Date().toISOString(),
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
messages,
|
messages,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
import * as crypto from 'crypto';
|
import {
|
||||||
|
getProjectHash,
|
||||||
|
getLegacyProjectHash,
|
||||||
|
} from '@qwen-code/qwen-code-core/src/utils/paths.js';
|
||||||
|
|
||||||
export interface QwenMessage {
|
export interface QwenMessage {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -58,8 +61,35 @@ export class QwenSessionReader {
|
||||||
|
|
||||||
if (!allProjects && workingDir) {
|
if (!allProjects && workingDir) {
|
||||||
// Current project only
|
// Current project only
|
||||||
const projectHash = await this.getProjectHash(workingDir);
|
const projectHash = getProjectHash(workingDir);
|
||||||
const chatsDir = path.join(this.qwenDir, 'tmp', projectHash, 'chats');
|
const chatsDir = path.join(this.qwenDir, 'tmp', projectHash, 'chats');
|
||||||
|
|
||||||
|
// Backward compatibility: On Windows, try legacy hash if new directory doesn't exist
|
||||||
|
if (os.platform() === 'win32' && !fs.existsSync(chatsDir)) {
|
||||||
|
const legacyHash = getLegacyProjectHash(workingDir);
|
||||||
|
const legacyChatsDir = path.join(
|
||||||
|
this.qwenDir,
|
||||||
|
'tmp',
|
||||||
|
legacyHash,
|
||||||
|
'chats',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(legacyChatsDir) && legacyHash !== projectHash) {
|
||||||
|
try {
|
||||||
|
// Migrate parent directory
|
||||||
|
const newParentDir = path.join(this.qwenDir, 'tmp', projectHash);
|
||||||
|
const legacyParentDir = path.join(
|
||||||
|
this.qwenDir,
|
||||||
|
'tmp',
|
||||||
|
legacyHash,
|
||||||
|
);
|
||||||
|
fs.renameSync(legacyParentDir, newParentDir);
|
||||||
|
} catch (_error) {
|
||||||
|
// Silent fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const projectSessions = await this.readSessionsFromDir(chatsDir);
|
const projectSessions = await this.readSessionsFromDir(chatsDir);
|
||||||
sessions.push(...projectSessions);
|
sessions.push(...projectSessions);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -177,19 +207,6 @@ export class QwenSessionReader {
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate project hash (needs to be consistent with Qwen CLI)
|
|
||||||
* Qwen CLI uses SHA256 hash of project path.
|
|
||||||
* On Windows, paths are case-insensitive, so we normalize to lowercase
|
|
||||||
* to ensure the same physical path always produces the same hash.
|
|
||||||
*/
|
|
||||||
private async getProjectHash(workingDir: string): Promise<string> {
|
|
||||||
// On Windows, normalize path to lowercase for case-insensitive matching
|
|
||||||
const normalizedPath =
|
|
||||||
os.platform() === 'win32' ? workingDir.toLowerCase() : workingDir;
|
|
||||||
return crypto.createHash('sha256').update(normalizedPath).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get session title (based on first user message)
|
* Get session title (based on first user message)
|
||||||
*/
|
*/
|
||||||
|
|
@ -294,7 +311,7 @@ export class QwenSessionReader {
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectHash = cwd
|
const projectHash = cwd
|
||||||
? await this.getProjectHash(cwd)
|
? getProjectHash(cwd)
|
||||||
: path.basename(path.dirname(path.dirname(filePath)));
|
: path.basename(path.dirname(path.dirname(filePath)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,21 @@ const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const projectRoot = path.resolve(__dirname, '..');
|
const projectRoot = path.resolve(__dirname, '..');
|
||||||
const projectHash = crypto
|
|
||||||
.createHash('sha256')
|
/**
|
||||||
.update(projectRoot)
|
* Generates a unique hash for a project based on its root path.
|
||||||
.digest('hex');
|
* On Windows, paths are case-insensitive, so we normalize to lowercase
|
||||||
|
* to ensure the same physical path always produces the same hash.
|
||||||
|
* This logic must match getProjectHash() in packages/core/src/utils/paths.ts
|
||||||
|
*/
|
||||||
|
function getProjectHash(projectRoot) {
|
||||||
|
// On Windows, normalize path to lowercase for case-insensitive matching
|
||||||
|
const normalizedPath =
|
||||||
|
os.platform() === 'win32' ? projectRoot.toLowerCase() : projectRoot;
|
||||||
|
return crypto.createHash('sha256').update(normalizedPath).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectHash = getProjectHash(projectRoot);
|
||||||
|
|
||||||
// User-level .gemini directory in home
|
// User-level .gemini directory in home
|
||||||
const USER_GEMINI_DIR = path.join(os.homedir(), '.qwen');
|
const USER_GEMINI_DIR = path.join(os.homedir(), '.qwen');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue