Files
chat/api/server/index.spec.js
zeekayandHanzo Dev 84b44f0f35 converge(client): typecheck on TypeScript 7 native (tsgo), 14.8x faster
`tsgo --noEmit` now IS `npm run typecheck`. Not an alternative to tsc — the
one way. Same program, same diagnostics, 3.4s instead of 49.6s.

Two config faults were in the way, and tsgo refused to typecheck at all until
both were fixed (it reported 29 config errors and stopped):

- `esModuleInterop: false` — removed in TS7 (TS5108). The option is gone, its
  behaviour is the default, and `allowSyntheticDefaultImports: true` was already
  set, so dropping the line is a no-op for this codebase's default imports.

- `declaration: true` + `declarationDir: "./types"` + `noEmit: false` — dead
  config copied from the library packages. `client/` is an APPLICATION: Vite
  builds it, the typecheck script passes --noEmit, `client/types/` does not
  exist and nothing in the repo references it. Emitting declarations forced
  rootDir inference over `../e2e/**` and `../packages/client/**`, which are
  legitimately outside `client/`, producing 28 spurious TS6059s. Now `noEmit`
  is simply true.

Verified equivalent, not assumed: tsc and tsgo produce the SAME error set on
this tree. The only textual differences are union members printed in a
different order (`"agent" | "prompt"` vs `"prompt" | "agent"`); every file,
code and message matches. 815 (tsc) vs 818 (tsgo) is line-splitting of
multi-line diagnostics, not three new findings.

Those ~815 errors are pre-existing on main and untouched here — i18n key
unions, `pendingManualSkillsByConvoId` atoms, e2e module resolution. They are
not this branch's to fix.

Scope note: `npm run build` is Vite, which transpiles with esbuild and never
invokes a typechecker, so there is no build step to move to tsgo. The workspace
LIBRARY packages still emit .d.ts through rollup+tsc; tsgo's emit is preview and
they are not converted.

Build green: 57.01s.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 17:48:29 -07:00

192 lines
6.5 KiB
JavaScript

const fs = require('fs');
const request = require('supertest');
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
jest.mock('~/server/services/Config', () => ({
loadCustomConfig: jest.fn(() => Promise.resolve({})),
getAppConfig: jest.fn().mockResolvedValue({
paths: {
uploads: '/tmp',
dist: '/tmp/dist',
fonts: '/tmp/fonts',
assets: '/tmp/assets',
},
fileStrategy: 'local',
imageOutputType: 'PNG',
}),
setCachedTools: jest.fn(),
}));
jest.mock('~/app/clients/tools', () => ({
createOpenAIImageTools: jest.fn(() => []),
createYouTubeTools: jest.fn(() => []),
manifestToolMap: {},
toolkits: [],
}));
jest.mock('~/config', () => ({
createMCPServersRegistry: jest.fn(),
createMCPManager: jest.fn().mockResolvedValue({
getAppToolFunctions: jest.fn().mockResolvedValue({}),
}),
}));
describe('Server Configuration', () => {
// Increase the default timeout to allow for Mongo cleanup
jest.setTimeout(30_000);
let mongoServer;
let app;
/** Mocked fs.readFileSync for index.html */
const originalReadFileSync = fs.readFileSync;
beforeAll(() => {
fs.readFileSync = function (filepath, options) {
if (filepath.includes('index.html')) {
return '<!DOCTYPE html><html><head><title>Hanzo Chat</title></head><body><div id="root"></div></body></html>';
}
return originalReadFileSync(filepath, options);
};
});
afterAll(() => {
// Restore original fs.readFileSync
fs.readFileSync = originalReadFileSync;
});
beforeAll(async () => {
// Create the required directories and files for the test
const fs = require('fs');
const path = require('path');
const dirs = ['/tmp/dist', '/tmp/fonts', '/tmp/assets'];
dirs.forEach((dir) => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
fs.writeFileSync(
path.join('/tmp/dist', 'index.html'),
'<!DOCTYPE html><html><head><title>Chat</title></head><body><div id="root"></div></body></html>',
);
mongoServer = await MongoMemoryServer.create();
process.env.MONGO_URI = mongoServer.getUri();
process.env.PORT = '0'; // Use a random available port
app = require('~/server');
// Wait for the app to be healthy
await healthCheckPoll(app);
});
afterAll(async () => {
await mongoServer.stop();
await mongoose.disconnect();
});
it('serves health under the chat namespace', async () => {
const response = await request(app).get('/v1/chat/health');
expect(response.status).toBe(200);
expect(response.text).toBe('OK');
});
it('serves nothing of its own at the top level', async () => {
/* Every one of these used to be a chat route. They now belong to the SPA
catch-all, which answers HTML — so a probe or a caller left on the old
path fails loudly instead of looking green. */
for (const path of ['/health', '/oauth/iam/session', '/oauth/openid']) {
const response = await request(app).get(path);
expect(response.text).not.toBe('OK');
expect(response.headers['content-type']).toMatch(/text\/html/);
}
});
it('404s a missing asset instead of answering it with the SPA shell', async () => {
/* A hashed bundle that does not exist must not come back as 200 text/html.
index.html declares its scripts as `type="module"`, and under nosniff a
module served as text/html is refused outright — so the shell-for-asset
answer blanks the page. It also arrives as a 200, which is cacheable: the
Workbox precache stores it under the asset URL with `revision: null` and
never revalidates, wedging that browser until a hard reload. */
for (const missing of [
'/assets/index.DEADBEEF.js',
'/assets/vendor.NOPE1234.js',
'/assets/index.MISSING1.css',
'/manifest.webmanifest',
]) {
const response = await request(app).get(missing);
expect(response.status).toBe(404);
expect(response.headers['content-type']).not.toMatch(/text\/html/);
}
});
it('still serves the SPA shell for extensionless client routes', async () => {
/* The 404 above keys on a file extension, so deep links must be unaffected. */
for (const route of ['/c/new', '/login', '/search']) {
const response = await request(app).get(route);
expect(response.status).toBe(200);
expect(response.headers['content-type']).toMatch(/text\/html/);
}
});
it('redirects stored image paths into the namespace', async () => {
const response = await request(app).get('/images/65cfb246f7ecadb8b1e8036c/x.png');
expect(response.status).toBe(301);
expect(response.headers.location).toBe('/v1/chat/images/65cfb246f7ecadb8b1e8036c/x.png');
});
it('should not cache index page', async () => {
const response = await request(app).get('/');
expect(response.status).toBe(200);
expect(response.headers['cache-control']).toBe('no-cache, no-store, must-revalidate');
expect(response.headers['pragma']).toBe('no-cache');
expect(response.headers['expires']).toBe('0');
});
it('should return 500 for unknown errors via ErrorController', async () => {
// Testing the error handling here on top of unit tests to ensure the middleware is correctly integrated
// Mock MongoDB operations to fail
const originalFindOne = mongoose.models.User.findOne;
const mockError = new Error('MongoDB operation failed');
mongoose.models.User.findOne = jest.fn().mockImplementation(() => {
throw mockError;
});
try {
const response = await request(app).post('/v1/chat/auth/login').send({
email: '[email protected]',
password: 'password123',
});
expect(response.status).toBe(500);
expect(response.text).toBe('An unknown error occurred.');
} finally {
// Restore original function
mongoose.models.User.findOne = originalFindOne;
}
});
});
// Polls the health endpoint every 30ms for up to 10 seconds to wait for the server to start completely
async function healthCheckPoll(app, retries = 0) {
const maxRetries = Math.floor(10000 / 30); // 10 seconds / 30ms
try {
const response = await request(app).get('/v1/chat/health');
if (response.text === 'OK') {
return; // App is healthy
}
} catch {
// Ignore connection errors during polling
}
if (retries < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, 30));
await healthCheckPoll(app, retries + 1);
} else {
throw new Error('App did not become healthy within 10 seconds.');
}
}