mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
`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]>
192 lines
6.5 KiB
JavaScript
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.');
|
|
}
|
|
}
|