mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-17 04:34:46 +00:00
Merge remote-tracking branch 'origin/main' into feature/markdown-table-column-controls
This commit is contained in:
commit
91a414e702
3 changed files with 73 additions and 33 deletions
|
|
@ -364,16 +364,14 @@ describe('ChatRecordingService - auto-title trigger', () => {
|
|||
expect(svc.getCurrentCustomTitle()).toBe('Auto-generated title');
|
||||
expect(svc.getCurrentTitleSource()).toBe('auto');
|
||||
|
||||
// finalize() was called by the constructor — drain the queued async
|
||||
// write before inspecting the mock.
|
||||
await svc.flush();
|
||||
expect(findCustomTitleRecord()).toBeUndefined();
|
||||
|
||||
svc.recordUserMessage([{ text: 'resume work' }]);
|
||||
await svc.flush();
|
||||
|
||||
// The re-appended record must carry titleSource: 'auto', not 'manual'.
|
||||
const finalizeRecord = vi
|
||||
.mocked(jsonl.writeLine)
|
||||
.mock.calls.map((c) => c[1] as ChatRecord)
|
||||
.find((r) => r.type === 'system' && r.subtype === 'custom_title');
|
||||
expect(finalizeRecord?.systemPayload).toEqual({
|
||||
const reanchoredRecord = findCustomTitleRecord();
|
||||
expect(reanchoredRecord?.systemPayload).toEqual({
|
||||
customTitle: 'Auto-generated title',
|
||||
titleSource: 'auto',
|
||||
});
|
||||
|
|
@ -404,12 +402,13 @@ describe('ChatRecordingService - auto-title trigger', () => {
|
|||
expect(svc.getCurrentCustomTitle()).toBe('User chose this');
|
||||
expect(svc.getCurrentTitleSource()).toBe('manual');
|
||||
await svc.flush();
|
||||
expect(findCustomTitleRecord()).toBeUndefined();
|
||||
|
||||
const finalizeRecord = vi
|
||||
.mocked(jsonl.writeLine)
|
||||
.mock.calls.map((c) => c[1] as ChatRecord)
|
||||
.find((r) => r.type === 'system' && r.subtype === 'custom_title');
|
||||
expect(finalizeRecord?.systemPayload).toEqual({
|
||||
svc.recordUserMessage([{ text: 'resume work' }]);
|
||||
await svc.flush();
|
||||
|
||||
const reanchoredRecord = findCustomTitleRecord();
|
||||
expect(reanchoredRecord?.systemPayload).toEqual({
|
||||
customTitle: 'User chose this',
|
||||
titleSource: 'manual',
|
||||
});
|
||||
|
|
@ -438,13 +437,14 @@ describe('ChatRecordingService - auto-title trigger', () => {
|
|||
// `titleSource: 'manual'` we can't actually verify.
|
||||
expect(svc.getCurrentTitleSource()).toBeUndefined();
|
||||
await svc.flush();
|
||||
expect(findCustomTitleRecord()).toBeUndefined();
|
||||
|
||||
const finalizeRecord = vi
|
||||
.mocked(jsonl.writeLine)
|
||||
.mock.calls.map((c) => c[1] as ChatRecord)
|
||||
.find((r) => r.type === 'system' && r.subtype === 'custom_title');
|
||||
svc.recordUserMessage([{ text: 'resume work' }]);
|
||||
await svc.flush();
|
||||
|
||||
const reanchoredRecord = findCustomTitleRecord();
|
||||
// Payload must NOT contain a titleSource field when source is unknown.
|
||||
expect(finalizeRecord?.systemPayload).toEqual({
|
||||
expect(reanchoredRecord?.systemPayload).toEqual({
|
||||
customTitle: 'Legacy title',
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -135,8 +135,9 @@ describe('ChatRecordingService - recordCustomTitle', () => {
|
|||
});
|
||||
|
||||
describe('finalize', () => {
|
||||
it('should re-append cached custom title to EOF', async () => {
|
||||
it('should re-append cached custom title to EOF after new content', async () => {
|
||||
chatRecordingService.recordCustomTitle('my-feature');
|
||||
chatRecordingService.recordUserMessage([{ text: 'new work' }]);
|
||||
await chatRecordingService.flush();
|
||||
vi.mocked(jsonl.writeLine).mockClear();
|
||||
|
||||
|
|
@ -153,6 +154,17 @@ describe('ChatRecordingService - recordCustomTitle', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should not write anything when the title is already the latest record', async () => {
|
||||
chatRecordingService.recordCustomTitle('my-feature');
|
||||
await chatRecordingService.flush();
|
||||
vi.mocked(jsonl.writeLine).mockClear();
|
||||
|
||||
chatRecordingService.finalize();
|
||||
await chatRecordingService.flush();
|
||||
|
||||
expect(jsonl.writeLine).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not write anything when no custom title was set', async () => {
|
||||
chatRecordingService.finalize();
|
||||
await chatRecordingService.flush();
|
||||
|
|
@ -160,9 +172,33 @@ describe('ChatRecordingService - recordCustomTitle', () => {
|
|||
expect(jsonl.writeLine).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not re-append a resumed title without new content', async () => {
|
||||
vi.mocked(mockConfig.getResumedSessionData).mockReturnValue({
|
||||
lastCompletedUuid: null,
|
||||
} as unknown as ReturnType<Config['getResumedSessionData']>);
|
||||
const getSessionTitleInfo = vi.fn().mockReturnValue({
|
||||
title: 'resumed-title',
|
||||
source: 'manual',
|
||||
});
|
||||
(
|
||||
mockConfig as unknown as {
|
||||
getSessionService: () => {
|
||||
getSessionTitleInfo: typeof getSessionTitleInfo;
|
||||
};
|
||||
}
|
||||
).getSessionService = () => ({ getSessionTitleInfo });
|
||||
|
||||
const svc = new ChatRecordingService(mockConfig);
|
||||
svc.finalize();
|
||||
await svc.flush();
|
||||
|
||||
expect(jsonl.writeLine).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should re-append the latest title after multiple renames', async () => {
|
||||
chatRecordingService.recordCustomTitle('first-name');
|
||||
chatRecordingService.recordCustomTitle('second-name');
|
||||
chatRecordingService.recordUserMessage([{ text: 'new work' }]);
|
||||
await chatRecordingService.flush();
|
||||
vi.mocked(jsonl.writeLine).mockClear();
|
||||
|
||||
|
|
@ -288,10 +324,8 @@ describe('ChatRecordingService - recordCustomTitle', () => {
|
|||
).getSessionService = () => ({ getSessionTitleInfo });
|
||||
|
||||
const svc = new ChatRecordingService(mockConfig);
|
||||
// Constructor's finalize re-appends a custom_title record on resume
|
||||
// — clear it out so we can isolate the threshold-triggered re-anchor.
|
||||
await svc.flush();
|
||||
vi.mocked(jsonl.writeLine).mockClear();
|
||||
expect(jsonl.writeLine).not.toHaveBeenCalled();
|
||||
|
||||
const bulkText = 'x'.repeat(2000);
|
||||
for (let i = 0; i < 20; i++) {
|
||||
|
|
|
|||
|
|
@ -564,6 +564,7 @@ export class ChatRecordingService {
|
|||
* returning undefined if the title is beyond both windows).
|
||||
*/
|
||||
private bytesSinceTitleAnchor = 0;
|
||||
private hasNonTitleContentSinceTitleAnchor = false;
|
||||
|
||||
constructor(config: Config) {
|
||||
this.config = config;
|
||||
|
|
@ -577,16 +578,19 @@ export class ChatRecordingService {
|
|||
// resumed. Legacy records (no `titleSource` field) stay `undefined` —
|
||||
// treated as manual for safety without rewriting the JSONL.
|
||||
//
|
||||
// We then re-append a custom_title record to EOF so the title stays
|
||||
// within the tail window that readers scan (guarding against a crash
|
||||
// before the next finalize).
|
||||
// Do not re-append during construction: loading/resuming a session is a
|
||||
// read operation from the user's perspective, and touching the JSONL mtime
|
||||
// would make session lists treat it as fresh activity.
|
||||
if (config.getResumedSessionData()) {
|
||||
try {
|
||||
const sessionService = config.getSessionService();
|
||||
const info = sessionService.getSessionTitleInfo(config.getSessionId());
|
||||
this.currentCustomTitle = info.title;
|
||||
this.currentTitleSource = info.source;
|
||||
this.finalize();
|
||||
if (info.title) {
|
||||
// Prime the threshold so the first real content write re-anchors.
|
||||
this.bytesSinceTitleAnchor = TITLE_REANCHOR_BYTES;
|
||||
}
|
||||
} catch {
|
||||
// Best-effort — don't block construction
|
||||
}
|
||||
|
|
@ -793,9 +797,11 @@ export class ChatRecordingService {
|
|||
private updateTitleAnchorTracking(record: ChatRecord): void {
|
||||
if (record.type === 'system' && record.subtype === 'custom_title') {
|
||||
this.bytesSinceTitleAnchor = 0;
|
||||
this.hasNonTitleContentSinceTitleAnchor = false;
|
||||
return;
|
||||
}
|
||||
if (!this.currentCustomTitle) return;
|
||||
this.hasNonTitleContentSinceTitleAnchor = true;
|
||||
// +1 for the trailing newline jsonl.writeLine appends.
|
||||
this.bytesSinceTitleAnchor +=
|
||||
Buffer.byteLength(JSON.stringify(record), 'utf8') + 1;
|
||||
|
|
@ -1319,13 +1325,10 @@ export class ChatRecordingService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Finalizes the current session by re-appending cached metadata to EOF.
|
||||
*
|
||||
* Call this whenever leaving the current session — whether switching to
|
||||
* another session, shutting down the process, or any other transition.
|
||||
* This single entry point replaces scattered re-append calls and ensures
|
||||
* the custom_title record stays within the last 64KB tail window that
|
||||
* readSessionTitleFromFile() scans.
|
||||
* Finalizes the current session by re-appending cached metadata to EOF, but
|
||||
* only after this recorder has appended non-title content since the last
|
||||
* title anchor. Pure load/resume must remain read-only so session lists do
|
||||
* not treat restored sessions as newly active.
|
||||
*
|
||||
* Best-effort: errors are logged but never thrown.
|
||||
*/
|
||||
|
|
@ -1344,6 +1347,9 @@ export class ChatRecordingService {
|
|||
if (!this.currentCustomTitle) {
|
||||
return;
|
||||
}
|
||||
if (!this.hasNonTitleContentSinceTitleAnchor) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const record: ChatRecord = {
|
||||
...this.createBaseRecord('system'),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue