test/cascade-turn-flag.test.js
Rama master · réplica de lectura del repositorio original.
// The CascadeTurn extraction must be invisible when its flag is off.
//
// This drives the real cascadeChat against a scripted loopback LS whose
// planner step grows in place ("A" → "AB") and whose second step arrives
// later. Both flag positions must produce the same text, the same absolute
// stepOffset, and the same number of upstream step reads. The flag-on result
// additionally carries the producer snapshot (per-step cursors + window).
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import http2 from 'node:http2';
Object.assign(process.env, {
GRPC_PROTOCOL: 'connect',
CASCADE_POLL_INTERVAL_MS: '10',
CASCADE_IDLE_GRACE_MS: '1',
CASCADE_MAX_WAIT_MS: '2000',
CASCADE_COLD_STALL_BASE_MS: '2000',
CASCADE_WARM_STALL_MS: '2000',
WINDSURFAPI_USER_JWT: '0',
});
delete process.env.DEVIN_CONNECT;
delete process.env.DEVIN_ONLY;
delete process.env.WINDSURFAPI_CASCADE_TURN;
const { WindsurfClient } = await import('../src/client.js');
const { unwrapRequest, wrapEnvelope, endOfStreamEnvelope } = await import('../src/connect.js');
const {
getField, parseFields, writeMessageField, writeStringField, writeVarintField,
} = await import('../src/proto.js');
function plannerStep(text, status = 3) {
return Buffer.concat([
writeVarintField(1, 15), // CORTEX_STEP_TYPE_PLANNER_RESPONSE
writeVarintField(4, status),
writeMessageField(20, writeStringField(1, text)),
]);
}
function rawStepsResponse(steps) {
return Buffer.concat(steps.map(step => writeMessageField(1, step)));
}
function readStepOffset(payload) {
const field = getField(parseFields(payload), 2, 0);
return field ? Number(field.value) : 0;
}
function respond(stream, headers, body) {
stream.respond({ ':status': 200, 'content-type': headers['content-type'] || 'application/connect+proto' });
stream.end(Buffer.concat([wrapEnvelope(body, { compress: false }), endOfStreamEnvelope()]));
}
async function withFakeLanguageServer(handler, fn) {
const server = http2.createServer();
const sessions = new Set();
server.on('session', (session) => {
sessions.add(session);
session.on('close', () => sessions.delete(session));
});
server.on('stream', handler);
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
try {
return await fn(port);
} finally {
for (const session of sessions) {
try { session.close(); } catch {}
try { session.destroy(); } catch {}
}
await new Promise((resolve) => server.close(resolve));
}
}
/**
* One scripted turn: step 0 is "A" then "AB"; step 1 is "CD". The trajectory
* is ACTIVE until the third steps poll, then IDLE. Returns the observed
* request offsets so the test can compare the two flag positions.
*/
async function runScriptedTurn({ turnFlag }) {
if (turnFlag) process.env.WINDSURFAPI_CASCADE_TURN = '1';
else delete process.env.WINDSURFAPI_CASCADE_TURN;
const state = { stepsCalls: 0, offsets: [] };
try {
return await withFakeLanguageServer((stream, headers) => {
const chunks = [];
stream.on('data', chunk => chunks.push(chunk));
stream.on('end', () => {
const method = String(headers[':path']).split('/').pop();
const payload = unwrapRequest(Buffer.concat(chunks), headers);
let body = Buffer.alloc(0);
if (method === 'StartCascade') body = writeStringField(1, 'flag-cascade');
if (method === 'GetCascadeTrajectorySteps') {
state.stepsCalls += 1;
const offset = readStepOffset(payload);
state.offsets.push(offset);
let trajectory;
if (state.stepsCalls === 1) trajectory = [plannerStep('A', 1)];
else if (state.stepsCalls === 2) trajectory = [plannerStep('AB', 1)];
else trajectory = [plannerStep('AB', 3), plannerStep('CD', 3)];
body = rawStepsResponse(trajectory.slice(offset));
}
if (method === 'GetCascadeTrajectory') {
body = writeVarintField(2, state.stepsCalls >= 3 ? 1 : 2); // IDLE / ACTIVE
}
respond(stream, headers, body);
});
}, async (port) => {
const client = new WindsurfClient('test-api-key', port, 'csrf-token');
const chunks = await client.cascadeChat(
[{ role: 'user', content: 'grow inside a step' }], 0, 'claude-sonnet-4-6',
);
return {
text: chunks.map(chunk => chunk.text || '').join(''),
stepOffset: chunks.stepOffset,
turnSnapshot: chunks.turnSnapshot,
offsets: state.offsets,
stepsCalls: state.stepsCalls,
};
});
} finally {
delete process.env.WINDSURFAPI_CASCADE_TURN;
}
}
describe('CascadeTurn flag-off compatibility', () => {
it('produces identical chunks and poll offsets with the flag on and off', async () => {
const off = await runScriptedTurn({ turnFlag: false });
const on = await runScriptedTurn({ turnFlag: true });
assert.equal(off.text, 'ABCD', 'the default path still re-reads partial-step growth');
assert.equal(on.text, off.text, 'the turn path emits exactly the same text');
assert.equal(on.stepOffset, off.stepOffset, 'both report the same absolute next step');
assert.equal(off.stepOffset, 2);
// Poll COUNT is scheduler-dependent (10ms poll, IDLE needs a settled
// growth window). Compare the common prefix instead of demanding the two
// runs land on the same final tick; the flag must not change the windows.
const common = Math.min(on.offsets.length, off.offsets.length);
assert.ok(common >= 3, 'both runs must poll at least the three scripted windows');
assert.deepEqual(on.offsets.slice(0, common), off.offsets.slice(0, common), 'both poll the same bounded windows');
assert.ok(Math.abs(on.stepsCalls - off.stepsCalls) <= 1, 'the turn path does not add more than a final settling read');
assert.equal(off.turnSnapshot, undefined, 'the default return shape has no producer state');
});
it('exposes the per-step cursors and overlap window on the flag-on snapshot', async () => {
const on = await runScriptedTurn({ turnFlag: true });
const snapshot = on.turnSnapshot;
assert.ok(snapshot, 'flag-on turns attach the producer snapshot');
assert.equal(snapshot.cascadeId, 'flag-cascade');
assert.equal(snapshot.stepOffset, 2);
assert.equal(snapshot.pollFloor, 0);
assert.equal(snapshot.windowStart, 0, 'the next window reaches behind the unread offset');
assert.deepEqual(snapshot.yieldedByStep, [[0, 2], [1, 2]]);
assert.deepEqual(snapshot.thinkingByStep, []);
assert.equal(snapshot.totalYielded, 4);
assert.equal(snapshot.chunks.map(chunk => chunk.text || '').join(''), 'ABCD');
});
});
/**
* A resumed cascade (reuseEntry.stepOffset=5) starts its turn at 5. The turn
* must clamp the overlap window to that floor, never replay steps 0-4, and
* still report the absolute next step.
*/
async function runResumedTurn({ turnFlag }) {
if (turnFlag) process.env.WINDSURFAPI_CASCADE_TURN = '1';
else delete process.env.WINDSURFAPI_CASCADE_TURN;
const offsets = [];
try {
return await withFakeLanguageServer((stream, headers) => {
const chunks = [];
stream.on('data', chunk => chunks.push(chunk));
stream.on('end', () => {
const method = String(headers[':path']).split('/').pop();
const payload = unwrapRequest(Buffer.concat(chunks), headers);
let body = Buffer.alloc(0);
if (method === 'GetCascadeTrajectorySteps') {
const offset = readStepOffset(payload);
offsets.push(offset);
if (offset === 5) body = rawStepsResponse([plannerStep('R5'), plannerStep('R6')]);
}
if (method === 'GetCascadeTrajectory') body = writeVarintField(2, 1); // IDLE
respond(stream, headers, body);
});
}, async (port) => {
const client = new WindsurfClient('test-api-key', port, 'csrf-token');
const chunks = await client.cascadeChat(
[{ role: 'user', content: 'continue' }], 0, 'claude-sonnet-4-6',
{
reuseEntry: {
cascadeId: 'resume-cascade',
sessionId: 'resume-session',
lsPort: port,
apiKey: 'test-api-key',
stepOffset: 5,
generatorOffset: 5,
},
},
);
return {
text: chunks.map(chunk => chunk.text || '').join(''),
stepOffset: chunks.stepOffset,
turnSnapshot: chunks.turnSnapshot,
offsets,
};
});
} finally {
delete process.env.WINDSURFAPI_CASCADE_TURN;
}
}
describe('CascadeTurn flag-on resume floor', () => {
it('starts a resumed turn at reuseEntry.stepOffset and never polls behind it', async () => {
const off = await runResumedTurn({ turnFlag: false });
const on = await runResumedTurn({ turnFlag: true });
assert.equal(off.text, 'R5R6');
assert.equal(on.text, off.text);
assert.equal(on.stepOffset, off.stepOffset);
assert.equal(on.stepOffset, 7);
const common = Math.min(on.offsets.length, off.offsets.length);
assert.ok(common >= 3, 'both runs must poll at least the scripted windows');
assert.deepEqual(on.offsets.slice(0, common), off.offsets.slice(0, common));
assert.ok(on.offsets.every(offset => offset === 5), `polled behind the floor: ${on.offsets.join(',')}`);
assert.equal(on.turnSnapshot.pollFloor, 5);
assert.equal(on.turnSnapshot.stepOffset, 7);
assert.equal(on.turnSnapshot.windowStart, 5, 'the overlap is clamped to the resume floor');
assert.deepEqual(on.turnSnapshot.yieldedByStep, [[5, 2], [6, 2]]);
});
});