Skip to content
298 changes: 277 additions & 21 deletions packages/core/src/core/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
type Mock,
} from 'vitest';

// Force UTC timezone so toLocaleDateString('en-US', ...) produces consistent
// output regardless of the developer's local timezone.
process.env.TZ = 'UTC';

import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
Expand Down Expand Up @@ -147,22 +151,27 @@ vi.mock('../utils/gitUtils.js', async (importOriginal) => {
vi.mock('../utils/nextSpeakerChecker', () => ({
checkNextSpeaker: vi.fn().mockResolvedValue(null),
}));
vi.mock('../utils/environmentContext', () => ({
getEnvironmentContext: vi
.fn()
.mockResolvedValue([{ text: 'Mocked env context' }]),
getInitialChatHistory: vi.fn(async (_config, extraHistory) => [
{
role: 'user',
parts: [{ text: 'Mocked env context' }],
},
{
role: 'model',
parts: [{ text: 'Got it. Thanks for the context!' }],
},
...(extraHistory ?? []),
]),
}));
vi.mock('../utils/environmentContext', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../utils/environmentContext.js')>();
return {
...actual,
getEnvironmentContext: vi
.fn()
.mockResolvedValue([{ text: 'Mocked env context' }]),
getInitialChatHistory: vi.fn(async (_config, extraHistory) => [
{
role: 'user',
parts: [{ text: 'Mocked env context' }],
},
{
role: 'model',
parts: [{ text: 'Got it. Thanks for the context!' }],
},
...(extraHistory ?? []),
]),
};
});
vi.mock('../utils/generateContentResponseUtilities', () => ({
getResponseText: (result: GenerateContentResponse) =>
result.candidates?.[0]?.content?.parts?.map((part) => part.text).join('') ||
Expand Down Expand Up @@ -449,6 +458,7 @@ describe('Gemini Client (client.ts)', () => {
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
__resetActiveGoalStoreForTests();
});
Expand Down Expand Up @@ -1323,6 +1333,12 @@ describe('Gemini Client (client.ts)', () => {

expect(hookSystem.fireSessionStartEvent).toHaveBeenCalledTimes(1);
});

it('should reset lastInjectedDate', async () => {
client['lastInjectedDate'] = 'Friday, June 5, 2026';
await client.resetChat();
expect(client['lastInjectedDate']).toBeUndefined();
});
});

describe('history mutation invalidates FileReadCache', () => {
Expand Down Expand Up @@ -2481,7 +2497,10 @@ Other open files:
expect(mockChat.addHistory).not.toHaveBeenCalled();
expect(mockTurnRunFn).toHaveBeenCalledWith(
'test-model',
[`<system-reminder>\n${expectedContext}\n</system-reminder>\n\nHi`],
[
expect.stringMatching(/^<system-reminder>\nThe current date is:/),
`<system-reminder>\n${expectedContext}\n</system-reminder>\n\nHi`,
],
expect.any(AbortSignal),
);
});
Expand Down Expand Up @@ -3187,7 +3206,10 @@ hello
// The main request should have been called without any memory content
expect(mockTurnRunFn).toHaveBeenCalledWith(
'test-model',
['Quick question'],
[
expect.stringMatching(/^<system-reminder>\nThe current date is:/),
'Quick question',
],
expect.any(AbortSignal),
);

Expand Down Expand Up @@ -3223,7 +3245,10 @@ hello
// The main request should have been called without any memory content
expect(mockTurnRunFn).toHaveBeenCalledWith(
'test-model',
['Quick question'],
[
expect.stringMatching(/^<system-reminder>\nThe current date is:/),
'Quick question',
],
expect.any(AbortSignal),
);
});
Expand Down Expand Up @@ -3280,6 +3305,231 @@ hello
});
});

it('should inject the current date on every UserQuery turn', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The midnight rollover branch of the dedup conditional is untested. The existing "should not inject duplicate date on the same day" test only covers the skip path (today === lastInjectedDate). No test advances the clock past midnight to verify that a changed date triggers re-injection and updates lastInjectedDate — which is the primary scenario the dedup logic was designed for.

Consider adding a test that freezes time at 23:59, sends a query, advances to 00:01, sends another query, and asserts the new date is injected.

— qwen3.7-max via Qwen Code /review

client['lastInjectedDate'] = undefined;
vi.setSystemTime(new Date('2026-06-05T12:00:00Z'));

const mockStream = (async function* () {
yield { type: 'content', value: 'Hello' };
})();
mockTurnRunFn.mockReturnValue(mockStream);

const mockChat: Partial<GeminiChat> = {
addHistory: vi.fn(),
getHistory: vi.fn().mockReturnValue([]),
};
client['chat'] = mockChat as GeminiChat;

const stream = client.sendMessageStream(
[{ text: 'What day is it?' }],
new AbortController().signal,
'prompt-id-date-inject',
);
for await (const _ of stream) {
// consume stream
}

// The first element in the request should be the date reminder
// wrapped in <system-reminder> tags
expect(mockTurnRunFn).toHaveBeenCalledWith(
'test-model',
[
expect.stringMatching(
/^<system-reminder>\nThe current date is:.*June 5, 2026/,
),
'What day is it?',
],
expect.any(AbortSignal),
);
});

it('should not inject duplicate date on the same day', async () => {
client['lastInjectedDate'] = undefined;
vi.setSystemTime(new Date('2026-06-05T12:00:00Z'));

const mockStream1 = (async function* () {
yield { type: 'content', value: 'Hello' };
})();
mockTurnRunFn.mockReturnValue(mockStream1);

const mockChat: Partial<GeminiChat> = {
addHistory: vi.fn(),
getHistory: vi.fn().mockReturnValue([]),
};
client['chat'] = mockChat as GeminiChat;

// First query on June 5 — should inject date
const stream1 = client.sendMessageStream(
[{ text: 'First question' }],
new AbortController().signal,
'prompt-id-date-first',
);
for await (const _ of stream1) {
// consume stream
}

expect(mockTurnRunFn).toHaveBeenLastCalledWith(
'test-model',
[
expect.stringMatching(
/^<system-reminder>\nThe current date is:.*June 5, 2026/,
),
'First question',
],
expect.any(AbortSignal),
);

// Second query same day — should NOT inject date again
const mockStream2 = (async function* () {
yield { type: 'content', value: 'World' };
})();
mockTurnRunFn.mockReturnValue(mockStream2);
mockChat.getHistory = vi.fn().mockReturnValue([
{ role: 'user', parts: [{ text: 'First question' }] },
{ role: 'model', parts: [{ text: 'Hello' }] },
]);

const stream2 = client.sendMessageStream(
[{ text: 'Second question' }],
new AbortController().signal,
'prompt-id-date-second',
);
for await (const _ of stream2) {
// consume stream
}

// Second call should NOT have date prefix (already injected today)
const secondCall = mockTurnRunFn.mock.calls[1];
expect(secondCall[1][0]).toBe('Second question');
});

it('should re-inject date when session spans midnight', async () => {
client['lastInjectedDate'] = undefined;

vi.setSystemTime(new Date('2026-06-04T12:00:00Z'));

const mockStream1 = (async function* () {
yield { type: 'content', value: 'Hello' };
})();
mockTurnRunFn.mockReturnValue(mockStream1);

const mockChat: Partial<GeminiChat> = {
addHistory: vi.fn(),
getHistory: vi.fn().mockReturnValue([]),
};
client['chat'] = mockChat as GeminiChat;

// First query on June 4 — should inject date
const stream1 = client.sendMessageStream(
[{ text: 'Day one' }],
new AbortController().signal,
'prompt-id-date-day-one',
);
for await (const _ of stream1) {
// consume stream
}

expect(mockTurnRunFn).toHaveBeenLastCalledWith(
'test-model',
[
expect.stringMatching(
/^<system-reminder>\nThe current date is:.*June 4, 2026/,
),
'Day one',
],
expect.any(AbortSignal),
);

// Advance to June 5 — date should change
vi.setSystemTime(new Date('2026-06-05T12:00:00Z'));

const mockStream2 = (async function* () {
yield { type: 'content', value: 'New day' };
})();
mockTurnRunFn.mockReturnValue(mockStream2);
mockChat.getHistory = vi.fn().mockReturnValue([
{ role: 'user', parts: [{ text: 'Day one' }] },
{ role: 'model', parts: [{ text: 'Hello' }] },
]);

const stream2 = client.sendMessageStream(
[{ text: 'Day two' }],
new AbortController().signal,
'prompt-id-date-day-two',
);
for await (const _ of stream2) {
// consume stream
}

// New date should be injected with June 5
const secondCall = mockTurnRunFn.mock.calls[1];
expect(secondCall[1][0]).toMatch(
/^<system-reminder>\nThe current date is:.*June 5, 2026/,
);
});

it('should not inject date on Cron turns', async () => {
client['lastInjectedDate'] = undefined;
vi.setSystemTime(new Date('2026-06-05T12:00:00Z'));

const mockStream = (async function* () {
yield { type: 'content', value: 'Cron response' };
})();
mockTurnRunFn.mockReturnValue(mockStream);

const mockChat: Partial<GeminiChat> = {
addHistory: vi.fn(),
getHistory: vi.fn().mockReturnValue([]),
};
client['chat'] = mockChat as GeminiChat;

// Send a Cron message — date should NOT be injected
const stream = client.sendMessageStream(
[{ text: 'cron-task' }],
new AbortController().signal,
'prompt-id-cron',
{ type: SendMessageType.Cron },
);
for await (const _ of stream) {
// consume stream
}

// Date must NOT be present, but other system reminders (e.g. PlanMode)
// may be included, so check that the date reminder is absent
const cronCall = mockTurnRunFn.mock.calls[0];
const cronRequest = cronCall[1].join('\n');
expect(cronRequest).not.toContain(
'<system-reminder>\nThe current date is:',
);

// UserQuery after Cron should still inject date normally
client['lastInjectedDate'] = undefined;
mockChat.getHistory = vi.fn().mockReturnValue([]);
const mockStream2 = (async function* () {
yield { type: 'content', value: 'Hello' };
})();
mockTurnRunFn.mockReturnValue(mockStream2);
const stream2 = client.sendMessageStream(
[{ text: 'User question' }],
new AbortController().signal,
'prompt-id-cron-user',
);
for await (const _ of stream2) {
// consume stream
}

expect(mockTurnRunFn).toHaveBeenLastCalledWith(
'test-model',
[
expect.stringMatching(
/^<system-reminder>\nThe current date is:.*June 5, 2026/,
),
'User question',
],
expect.any(AbortSignal),
);
});

describe('autoSkill: scheduleSkillReview via runManagedAutoMemoryBackgroundTasks', () => {
let mockStreamFn: () => AsyncGenerator<{ type: string; value: string }>;
let mockChat: Partial<GeminiChat>;
Expand Down Expand Up @@ -4028,7 +4278,10 @@ Other open files:
expect(getLastTurnRequestText()).toContain('</system-reminder>');
} else {
expect(mockChat.addHistory).not.toHaveBeenCalled();
expect(getLastTurnRequestText()).not.toContain('<system-reminder>');
// Date reminder uses <system-reminder> too, so check for the IDE-specific one
expect(getLastTurnRequestText()).not.toContain(
"Here is a summary of changes in the user's current editor context",
);
}
},
);
Expand Down Expand Up @@ -4263,7 +4516,10 @@ Other open files:
/* consume */
}

expect(getLastTurnRequestText()).not.toContain('<system-reminder>');
// Date reminder uses <system-reminder> too, so check for IDE-specific one
expect(getLastTurnRequestText()).not.toContain(
"Here is the user's current editor context",
);
expect(client['lastSentIdeContext']).toBeUndefined();
expect(client['forceFullIdeContext']).toBe(true);

Expand Down
Loading
Loading