Files

166 lines
5.6 KiB
JavaScript
Raw Permalink Normal View History

import express from 'express';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { registerWalkthroughRoutes } from './routes.js';
// These run over real HTTP on purpose. The bug this file exists for was
// invisible to unit tests: the service and the store were both correct, and the
// response was dropped by a disconnect check that misread a healthy request.
const SOURCE = { kind: 'working-tree', scope: 'all' };
describe('walkthrough routes', () => {
let server;
let base;
let releaseJob;
let job;
let lastArgs;
let generateCalls = 0;
const service = {
async getWalkthrough(args) {
lastArgs = args;
return { walkthrough: null, hunks: [], hunkCount: 0, generating: Boolean(job) };
},
async generateWalkthrough(args) {
lastArgs = args;
generateCalls += 1;
if (job) return job;
job = new Promise((resolve) => {
releaseJob = () => resolve({ walkthrough: { title: 'DONE' }, hunks: [], hunkCount: 1 });
}).finally(() => { job = null; });
return job;
},
async cancelWalkthroughGeneration() {
return { cancelled: Boolean(job) };
},
};
const generate = (signal) => fetch(`${base}/api/walkthrough/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ directory: '/repo', source: SOURCE }),
signal,
});
beforeEach(async () => {
job = null;
releaseJob = undefined;
lastArgs = undefined;
const app = express();
app.use(express.json());
registerWalkthroughRoutes(app, { getWalkthroughService: async () => service });
server = app.listen(0);
await new Promise((resolve) => server.once('listening', resolve));
base = `http://127.0.0.1:${server.address().port}`;
});
afterEach(async () => {
// A response a failed test never received keeps its keep-alive socket
// open, and server.close() would wait on it until the hook timeout.
server.closeAllConnections();
await new Promise((resolve) => server.close(resolve));
});
// Resolves once the route has asked the service to generate one more time
// than `seen`. A fixed sleep assumed the request had arrived by then; on a
// loaded runner it had not, and the step that followed acted on a request
// the server had not seen yet.
const untilGenerateCalled = async (seen) => {
for (let attempt = 0; attempt < 300 && generateCalls <= seen; attempt += 1) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
if (generateCalls <= seen) throw new Error('the route never asked the service to generate');
};
it('answers a generation request that nobody interrupted', async () => {
const seen = generateCalls;
const pending = generate();
await untilGenerateCalled(seen);
releaseJob();
const body = await (await pending).json();
expect(body.walkthrough).toEqual({ title: 'DONE' });
});
it('delivers the result to a client that reconnected after a refresh', async () => {
const controller = new AbortController();
const seen = generateCalls;
generate(controller.signal).catch(() => {});
await untilGenerateCalled(seen);
controller.abort();
await new Promise((resolve) => setTimeout(resolve, 20));
// The reloaded page sees work in progress and re-attaches to it.
const read = await (await fetch(
`${base}/api/walkthrough?directory=/repo&source=${encodeURIComponent(JSON.stringify(SOURCE))}`,
)).json();
expect(read.generating).toBe(true);
const seenBeforeReattach = generateCalls;
const reattached = generate();
await untilGenerateCalled(seenBeforeReattach);
releaseJob();
const body = await (await reattached).json();
expect(body.walkthrough).toEqual({ title: 'DONE' });
});
it('rejects a request without a directory before touching the service', async () => {
const response = await fetch(`${base}/api/walkthrough/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source: SOURCE }),
});
expect(response.status).toBe(400);
expect(job).toBeNull();
});
// The language belongs to the request, not to a setting, so both the read
// and the generation have to carry it: readiness is computed from a prompt
// that contains the language instruction.
it('carries the requested language into the service', async () => {
await fetch(
`${base}/api/walkthrough?directory=/repo&language=uk&source=${encodeURIComponent(JSON.stringify(SOURCE))}`,
);
expect(lastArgs.language).toBe('uk');
const seen = generateCalls;
const pending = fetch(`${base}/api/walkthrough/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ directory: '/repo', source: SOURCE, language: 'ja' }),
});
await untilGenerateCalled(seen);
releaseJob();
await pending;
expect(lastArgs.language).toBe('ja');
});
it('ignores a language that is not a string', async () => {
await fetch(
`${base}/api/walkthrough?directory=/repo&language[]=uk&source=${encodeURIComponent(JSON.stringify(SOURCE))}`,
);
expect(lastArgs.language).toBeUndefined();
});
it('cancels through its own endpoint rather than a dropped connection', async () => {
const seen = generateCalls;
generate().catch(() => {});
await untilGenerateCalled(seen);
const response = await fetch(`${base}/api/walkthrough/cancel`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ directory: '/repo', source: SOURCE }),
});
expect(await response.json()).toEqual({ cancelled: true });
releaseJob();
});
});