mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 18:16:08 +00:00
171 lines
5.2 KiB
TypeScript
171 lines
5.2 KiB
TypeScript
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
|
|
import express from 'express';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import type { Server } from 'http';
|
|
|
|
let server: Server;
|
|
let baseUrl = '';
|
|
let tempDir = '';
|
|
let codexHome = '';
|
|
let originalCodexHome: string | undefined;
|
|
|
|
beforeAll(async () => {
|
|
originalCodexHome = process.env.CODEX_HOME;
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-routes-test-'));
|
|
codexHome = path.join(tempDir, '.codex-home');
|
|
process.env.CODEX_HOME = codexHome;
|
|
|
|
const codexRoutesModule = await import('../../../src/web-server/routes/codex-routes');
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use('/api/codex', codexRoutesModule.default);
|
|
|
|
server = app.listen(0, '127.0.0.1');
|
|
await new Promise<void>((resolve) => server.on('listening', () => resolve()));
|
|
const address = server.address();
|
|
if (!address || typeof address === 'string') {
|
|
throw new Error('Unable to resolve test server port');
|
|
}
|
|
baseUrl = `http://127.0.0.1:${address.port}`;
|
|
});
|
|
|
|
beforeEach(() => {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
fs.mkdirSync(codexHome, { recursive: true });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (server) {
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
}
|
|
|
|
if (originalCodexHome !== undefined) {
|
|
process.env.CODEX_HOME = originalCodexHome;
|
|
} else {
|
|
delete process.env.CODEX_HOME;
|
|
}
|
|
|
|
if (tempDir && fs.existsSync(tempDir)) {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('codex routes', () => {
|
|
it('returns the current raw config snapshot from PATCH /config/patch', async () => {
|
|
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
kind: 'top-level',
|
|
values: {
|
|
model: 'gpt-5.4',
|
|
sandboxMode: 'workspace-write',
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(res.status).toBe(200);
|
|
|
|
const json = (await res.json()) as {
|
|
success: boolean;
|
|
exists: boolean;
|
|
mtime: number;
|
|
rawText: string;
|
|
config: Record<string, unknown> | null;
|
|
parseError: string | null;
|
|
readError: string | null;
|
|
};
|
|
|
|
expect(json.success).toBe(true);
|
|
expect(json.exists).toBe(true);
|
|
expect(json.mtime).toBeGreaterThan(0);
|
|
expect(json.parseError).toBeNull();
|
|
expect(json.readError).toBeNull();
|
|
expect(json.rawText).toContain('model = "gpt-5.4"');
|
|
expect(json.rawText).toContain('sandbox_mode = "workspace-write"');
|
|
expect(json.config?.model).toBe('gpt-5.4');
|
|
|
|
const written = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
|
|
expect(written).toBe(json.rawText);
|
|
});
|
|
|
|
it('returns 409 when PATCH /config/patch receives a stale expectedMtime', async () => {
|
|
const configPath = path.join(codexHome, 'config.toml');
|
|
fs.writeFileSync(configPath, 'model = "gpt-5.3-codex"\n');
|
|
|
|
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
kind: 'feature',
|
|
feature: 'multi_agent',
|
|
enabled: true,
|
|
expectedMtime: 1,
|
|
}),
|
|
});
|
|
|
|
expect(res.status).toBe(409);
|
|
|
|
const json = (await res.json()) as { error: string; mtime: number };
|
|
expect(json.error).toContain('File modified externally.');
|
|
expect(json.mtime).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('allows PATCH /config/patch on an existing config.toml without expectedMtime', async () => {
|
|
fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model = "gpt-5.4"\n');
|
|
|
|
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
kind: 'feature',
|
|
feature: 'multi_agent',
|
|
enabled: true,
|
|
}),
|
|
});
|
|
|
|
expect(res.status).toBe(200);
|
|
|
|
const json = (await res.json()) as {
|
|
success: boolean;
|
|
rawText: string;
|
|
config: Record<string, unknown> | null;
|
|
};
|
|
|
|
expect(json.success).toBe(true);
|
|
expect(json.rawText).toContain('multi_agent = true');
|
|
expect(json.config?.features).toEqual({ multi_agent: true });
|
|
});
|
|
|
|
it('returns 400 when PATCH /config/patch omits kind', async () => {
|
|
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({}),
|
|
});
|
|
|
|
expect(res.status).toBe(400);
|
|
const json = (await res.json()) as { error: string };
|
|
expect(json.error).toBe('kind is required.');
|
|
});
|
|
|
|
it('returns 400 when PATCH /config/patch receives an invalid trust path', async () => {
|
|
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
kind: 'project-trust',
|
|
path: './relative-workspace',
|
|
trustLevel: 'trusted',
|
|
}),
|
|
});
|
|
|
|
expect(res.status).toBe(400);
|
|
const json = (await res.json()) as { error: string };
|
|
expect(json.error).toContain('Project path must be absolute');
|
|
});
|
|
});
|