Skip to content
76 changes: 50 additions & 26 deletions integration-tests/cli/acp-cron.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,32 @@
* and stream results back to the client via sessionUpdate notifications,
* even after the originating prompt has already returned.
*
* The two tests share one ACP session to stay within 2 minutes total:
* 1. Fast smoke test — cron tools available (no cron fire needed)
* 2. Combined test — create job, verify session responsive, wait for
* cron fire, check content + _meta.source, then clean up
* Uses fake-openai-server for deterministic model responses, eliminating
* model output variance as a failure source. The cron scheduler still
* operates on real minute-boundary timing.
*/

import { spawn } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { createInterface } from 'node:readline';
import { setTimeout as delay } from 'node:timers/promises';
import { describe, it, expect } from 'vitest';
import { TestRig } from '../test-helper.js';
import { TestRig, fakeServerHostOptions } from '../test-helper.js';
import {
startFakeOpenAIServer,
fakeToolCall,
type FakeOpenAIServer,
} from '../fake-openai-server.js';

const REQUEST_TIMEOUT_MS = 60_000;

const IS_SANDBOX =
process.env['QWEN_SANDBOX'] &&
process.env['QWEN_SANDBOX']!.toLowerCase() !== 'false';

const FAKE_SERVER_OPTIONS = fakeServerHostOptions();

type PendingRequest = {
resolve: (value: unknown) => void;
reject: (reason: Error) => void;
Expand Down Expand Up @@ -69,16 +76,20 @@ type PermissionRequest = {
};

/**
* Sets up an ACP test environment with cron support enabled.
* Sets up an ACP test environment with cron support enabled, backed by
* a fake-openai-server for deterministic model responses.
*/
function setupAcpCronTest(rig: TestRig) {
function setupAcpCronTest(rig: TestRig, fakeServer: FakeOpenAIServer) {
const pending = new Map<number, PendingRequest>();
let nextRequestId = 1;
const sessionUpdates: (SessionUpdateNotification & {
receivedAt: number;
})[] = [];
const stderr: string[] = [];

const qwenHome = join(rig.testDir!, '.qwen-home');
mkdirSync(qwenHome, { recursive: true });
Comment thread
qwen-code-dev-bot marked this conversation as resolved.

const agent = spawn(
'node',
[rig.bundlePath, '--acp', '--no-chat-recording'],
Expand All @@ -87,6 +98,13 @@ function setupAcpCronTest(rig: TestRig) {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
QWEN_HOME: qwenHome,
OPENAI_API_KEY: 'fake-key',
OPENAI_BASE_URL: fakeServer.baseUrl,
OPENAI_MODEL: 'fake-model',
QWEN_MODEL: 'fake-model',
NO_PROXY: '127.0.0.1,localhost',
no_proxy: '127.0.0.1,localhost',
},
},
);
Expand Down Expand Up @@ -303,13 +321,29 @@ async function initSession(
const rig = new TestRig();
rig.setup('acp-cron-e2e');

const {
sendRequest,
cleanup,
stderr,
// sessionUpdates available for debugging
waitForSessionUpdate,
} = setupAcpCronTest(rig);
// Only requestIndex 0 is load-bearing: it returns the cron_create
// tool call. The CLI makes internal model calls (tool-call
// classification, suggestion mode) between user-facing turns, so
// later indices do not map 1:1 to the prompts sent below. No
// assertion reads scripted response content, so the default reply
// suffices for every other turn.
const fakeServer = await startFakeOpenAIServer(({ requestIndex }) => {
if (requestIndex === 0) {
return {
toolCalls: [
fakeToolCall('cron_create', {
cron: '*/1 * * * *',
prompt: 'Say CRONFIRE7742 and nothing else',
recurring: true,
}),
],
};
}
return { content: 'Done.' };
}, FAKE_SERVER_OPTIONS);

const { sendRequest, cleanup, stderr, waitForSessionUpdate } =
setupAcpCronTest(rig, fakeServer);

try {
Comment thread
qwen-code-dev-bot marked this conversation as resolved.
Outdated
const sessionId = await initSession(sendRequest, rig.testDir!);
Expand Down Expand Up @@ -367,22 +401,12 @@ async function initSession(
15_000, // should already be here by now
);
expect(cronAgentMsg.receivedAt).toBeGreaterThan(promptDoneAt);

// --- Part 4: Clean up the cron job ---
await sendRequest('session/prompt', {
sessionId,
prompt: [
{
type: 'text',
text: 'Delete all cron jobs using cron_delete.',
},
],
});
} catch (e) {
if (stderr.length) console.error('Agent stderr:', stderr.join(''));
throw e;
Comment thread
qwen-code-dev-bot marked this conversation as resolved.
Outdated
} finally {
await cleanup();
await fakeServer.close();
}
},
{ timeout: 120_000, retry: 0 },
Expand Down
Loading