mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
fix: stabilize Codex and development dependencies (#798)
* fix: stabilize dependency and Codex integration Resolve development-only dependency advisories, remove the search test teardown race, and validate the Codex 0.144.1 event and health contracts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * chore: keep generated lockfile reviewable Exclude pnpm-lock.yaml from Prettier and restore pnpm's generated formatting after dependency resolution. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: stabilize Codex process lifecycle Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
84ace3e9de
commit
b3eda417ae
11 changed files with 429 additions and 630 deletions
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
pnpm-lock.yaml
|
||||
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the Codex SDK integration to `0.144.1` and pinned patched transitive
|
||||
development-tool dependencies (#792, #795).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Isolated QMD result normalization coverage from unrelated persistent search
|
||||
collections and restored test environment state only after temporary search
|
||||
roots are removed, eliminating the intermittent teardown race (#793).
|
||||
|
||||
## [5.2.1] - 2026-06-29
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ This file defines project-specific rules, context, and lessons learned for AI ag
|
|||
|
||||
- ❌ Used wrong field in backfilled events (`status: "success"` vs `success: true`)
|
||||
- ✅ Match actual runtime schema exactly in test fixtures
|
||||
- ✅ Keep `pnpm-lock.yaml` generated by pnpm; do not format it with Prettier
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -167,7 +167,10 @@ Settings exposes Codex readiness through a dedicated health check:
|
|||
GET /api/settings/codex/health
|
||||
```
|
||||
|
||||
The response reports Codex CLI install/version/auth state, SDK import availability, Codex agent profile readiness, enabled Codex profiles, and recommendations.
|
||||
The response reports Codex CLI install/version/auth state, the installed Codex
|
||||
SDK version and import availability, Codex agent profile readiness, enabled
|
||||
Codex profiles, and recommendations. Veritas Kanban currently validates its
|
||||
stream adapter against `@openai/codex-sdk` 0.144.1 event contracts.
|
||||
|
||||
## MCP And Project Instructions
|
||||
|
||||
|
|
|
|||
792
pnpm-lock.yaml
generated
792
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -14,9 +14,12 @@ allowBuilds:
|
|||
esbuild: true
|
||||
|
||||
overrides:
|
||||
'@babel/core@<7.29.1': 7.29.7
|
||||
'@modelcontextprotocol/sdk>express-rate-limit': 8.5.2
|
||||
'@xmldom/xmldom': 0.8.13
|
||||
'esbuild@<0.28.1': 0.28.1
|
||||
fast-uri: '>=3.1.2'
|
||||
'form-data@<4.0.6': 4.0.6
|
||||
hono: '>=4.12.18'
|
||||
ip-address: '>=10.1.1'
|
||||
postcss: '>=8.5.10'
|
||||
|
|
@ -25,3 +28,5 @@ overrides:
|
|||
minimatch: '>=10.2.3'
|
||||
path-to-regexp: '>=8.4.0'
|
||||
tmp: '>=0.2.6'
|
||||
'undici@>=6.0.0 <7.0.0': 6.27.0
|
||||
'undici@>=7.0.0 <8.0.0': 7.28.0
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"reset-password": "tsx src/scripts/reset-password.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openai/codex-sdk": "0.140.0",
|
||||
"@openai/codex-sdk": "0.144.1",
|
||||
"@veritas-kanban/shared": "workspace:*",
|
||||
"ajv": "^8.20.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ vi.mock('../services/circuit-registry.js', () => ({
|
|||
}));
|
||||
|
||||
import { AgentReadinessError, ClawdbotAgentService } from '../services/clawdbot-agent-service.js';
|
||||
import type { ThreadEvent } from '@openai/codex-sdk';
|
||||
import type { AgentConfig, Task } from '@veritas-kanban/shared';
|
||||
|
||||
const fixtureDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'codex');
|
||||
|
|
@ -109,7 +110,7 @@ function testableService(tmpDir: string): TestableClawdbotAgentService {
|
|||
return service;
|
||||
}
|
||||
|
||||
function createFakeChild(fixturePath: string, exitCode = 0) {
|
||||
function createFakeChild(fixture: string, exitCode = 0) {
|
||||
const child = new EventEmitter() as EventEmitter & {
|
||||
stdout: PassThrough;
|
||||
stderr: PassThrough;
|
||||
|
|
@ -127,10 +128,11 @@ function createFakeChild(fixturePath: string, exitCode = 0) {
|
|||
return true;
|
||||
});
|
||||
|
||||
setImmediate(async () => {
|
||||
child.stdout.write(await fs.readFile(fixturePath, 'utf-8'));
|
||||
child.stdout.end();
|
||||
setTimeout(() => child.emit('close', exitCode, null), 10);
|
||||
queueMicrotask(() => {
|
||||
child.stdout.once('end', () => {
|
||||
child.emit('close', exitCode, null);
|
||||
});
|
||||
child.stdout.end(fixture);
|
||||
});
|
||||
|
||||
return child;
|
||||
|
|
@ -158,7 +160,7 @@ function createControllableChild() {
|
|||
async function waitFor(assertion: () => void): Promise<void> {
|
||||
const started = Date.now();
|
||||
let lastError: unknown;
|
||||
while (Date.now() - started < 3000) {
|
||||
while (Date.now() - started < 10_000) {
|
||||
try {
|
||||
assertion();
|
||||
return;
|
||||
|
|
@ -245,85 +247,93 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('runs the Codex CLI adapter against mocked JSONL and records telemetry', async () => {
|
||||
mockSpawn.mockReturnValue(createFakeChild(path.join(fixtureDir, 'success.jsonl')));
|
||||
const service = testableService(tmpDir);
|
||||
it(
|
||||
'runs the Codex CLI adapter against mocked JSONL and records telemetry',
|
||||
{ timeout: 20_000 },
|
||||
async () => {
|
||||
const fixture = await fs.readFile(path.join(fixtureDir, 'success.jsonl'), 'utf-8');
|
||||
mockSpawn.mockReturnValue(createFakeChild(fixture));
|
||||
const service = testableService(tmpDir);
|
||||
|
||||
const status = await service.startAgent(task.id, 'codex');
|
||||
const status = await service.startAgent(task.id, 'codex');
|
||||
|
||||
expect(status.status).toBe('running');
|
||||
expect(mockSpawn).toHaveBeenCalledWith(
|
||||
'codex',
|
||||
expect.arrayContaining(['exec', '--json', '--sandbox', 'workspace-write']),
|
||||
expect.objectContaining({ cwd: tmpDir, shell: false })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateTask).toHaveBeenCalledWith(
|
||||
task.id,
|
||||
expect.objectContaining({ status: 'done' })
|
||||
expect(status.status).toBe('running');
|
||||
expect(mockSpawn).toHaveBeenCalledWith(
|
||||
'codex',
|
||||
expect.arrayContaining(['exec', '--json', '--sandbox', 'workspace-write']),
|
||||
expect.objectContaining({ cwd: tmpDir, shell: false })
|
||||
);
|
||||
});
|
||||
expect(mockTelemetryEmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'run.tokens',
|
||||
inputTokens: 12,
|
||||
outputTokens: 8,
|
||||
totalTokens: 20,
|
||||
model: 'gpt-5.5',
|
||||
})
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockLogActivity).toHaveBeenCalledWith(
|
||||
'agent_completed',
|
||||
task.id,
|
||||
task.title,
|
||||
expect.objectContaining({ provider: 'codex-cli', success: true }),
|
||||
'codex'
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateTask).toHaveBeenCalledWith(
|
||||
task.id,
|
||||
expect.objectContaining({ status: 'done' })
|
||||
);
|
||||
});
|
||||
expect(mockTelemetryEmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'run.tokens',
|
||||
inputTokens: 12,
|
||||
outputTokens: 8,
|
||||
totalTokens: 20,
|
||||
model: 'gpt-5.5',
|
||||
})
|
||||
);
|
||||
});
|
||||
expect(mockStartStep).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'stream',
|
||||
expect.objectContaining({
|
||||
eventType: 'stream.stdout',
|
||||
stream: 'stdout',
|
||||
provider: 'codex-cli',
|
||||
chunkBytes: expect.any(Number),
|
||||
})
|
||||
);
|
||||
expect(mockStartStep).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'finalize',
|
||||
expect.objectContaining({
|
||||
eventType: 'run.finalizing',
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
success: true,
|
||||
provider: 'codex-cli',
|
||||
})
|
||||
);
|
||||
expect(mockStartStep).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'complete',
|
||||
expect.objectContaining({
|
||||
eventType: 'turn.completed',
|
||||
totalTokens: 20,
|
||||
model: 'gpt-5.5',
|
||||
finalResult: 'Codex completed the task.',
|
||||
})
|
||||
);
|
||||
expect(mockStartStep).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'complete',
|
||||
expect.objectContaining({
|
||||
eventType: 'run.completed',
|
||||
success: true,
|
||||
provider: 'codex-cli',
|
||||
model: 'gpt-5.5',
|
||||
})
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockLogActivity).toHaveBeenCalledWith(
|
||||
'agent_completed',
|
||||
task.id,
|
||||
task.title,
|
||||
expect.objectContaining({ provider: 'codex-cli', success: true }),
|
||||
'codex'
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(service.getAgentStatus(task.id)).toBeNull();
|
||||
});
|
||||
expect(mockStartStep).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'stream',
|
||||
expect.objectContaining({
|
||||
eventType: 'stream.stdout',
|
||||
stream: 'stdout',
|
||||
provider: 'codex-cli',
|
||||
chunkBytes: expect.any(Number),
|
||||
})
|
||||
);
|
||||
expect(mockStartStep).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'finalize',
|
||||
expect.objectContaining({
|
||||
eventType: 'run.finalizing',
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
success: true,
|
||||
provider: 'codex-cli',
|
||||
})
|
||||
);
|
||||
expect(mockStartStep).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'complete',
|
||||
expect.objectContaining({
|
||||
eventType: 'turn.completed',
|
||||
totalTokens: 20,
|
||||
model: 'gpt-5.5',
|
||||
finalResult: 'Codex completed the task.',
|
||||
})
|
||||
);
|
||||
expect(mockStartStep).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'complete',
|
||||
expect.objectContaining({
|
||||
eventType: 'run.completed',
|
||||
success: true,
|
||||
provider: 'codex-cli',
|
||||
model: 'gpt-5.5',
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('maps Codex file events to task deliverables linked to the attempt', async () => {
|
||||
const service = testableService(tmpDir);
|
||||
|
|
@ -359,6 +369,7 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockStartStep).toHaveBeenCalledWith(
|
||||
'attempt_fixture',
|
||||
'execute',
|
||||
|
|
@ -377,6 +388,47 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('accepts every Codex SDK 0.144 event contract consumed by the stream adapter', () => {
|
||||
const service = testableService(tmpDir);
|
||||
const logPath = path.join(tmpDir, 'codex.md');
|
||||
const events = [
|
||||
{ type: 'thread.started', thread_id: 'thread_fixture' },
|
||||
{ type: 'turn.started' },
|
||||
{
|
||||
type: 'turn.completed',
|
||||
usage: {
|
||||
input_tokens: 10,
|
||||
cached_input_tokens: 2,
|
||||
output_tokens: 5,
|
||||
reasoning_output_tokens: 1,
|
||||
},
|
||||
},
|
||||
{ type: 'turn.failed', error: { message: 'fixture failure' } },
|
||||
{
|
||||
type: 'item.started',
|
||||
item: { id: 'item_started', type: 'agent_message', text: 'starting' },
|
||||
},
|
||||
{
|
||||
type: 'item.updated',
|
||||
item: { id: 'item_updated', type: 'agent_message', text: 'working' },
|
||||
},
|
||||
{
|
||||
type: 'item.completed',
|
||||
item: { id: 'item_completed', type: 'agent_message', text: 'finished' },
|
||||
},
|
||||
{ type: 'error', message: 'fixture stream error' },
|
||||
] satisfies ThreadEvent[];
|
||||
|
||||
const parsed = events.map((event) => service.handleCodexEvent(event, logPath));
|
||||
|
||||
expect(parsed[2]?.usage).toEqual({
|
||||
inputTokens: 10,
|
||||
outputTokens: 5,
|
||||
totalTokens: 15,
|
||||
});
|
||||
expect(parsed[6]?.summary).toBe('finished');
|
||||
});
|
||||
|
||||
it('classifies streamed output, retry, and abort lifecycle events in traces', async () => {
|
||||
const service = testableService(tmpDir);
|
||||
const logPath = path.join(tmpDir, 'codex.md');
|
||||
|
|
@ -487,7 +539,8 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
|
||||
await expect(service.startAgent(task.id, 'codex')).rejects.toBeInstanceOf(AgentReadinessError);
|
||||
|
||||
mockSpawn.mockReturnValue(createFakeChild(path.join(fixtureDir, 'success.jsonl')));
|
||||
const fixture = await fs.readFile(path.join(fixtureDir, 'success.jsonl'), 'utf-8');
|
||||
mockSpawn.mockReturnValue(createFakeChild(fixture));
|
||||
|
||||
const status = await service.startAgent(task.id, 'codex', {
|
||||
overrideReason: 'Maintainer approved urgent fix',
|
||||
|
|
@ -514,5 +567,8 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
expect.objectContaining({ status: 'done' })
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(service.getAgentStatus(task.id)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ describe('Settings Codex health route', () => {
|
|||
mockCodexHealthService.getHealth.mockResolvedValue({
|
||||
checkedAt: '2026-05-06T00:00:00.000Z',
|
||||
cli: { installed: true, authenticated: true, version: 'codex-cli 0.128.0' },
|
||||
sdk: { available: true },
|
||||
sdk: { available: true, version: '0.144.1' },
|
||||
agents: { codexCli: true, codexSdk: true, codexCloud: true, enabled: ['codex'] },
|
||||
ready: { cli: true, sdk: true, cloud: true, overall: true },
|
||||
recommendations: [],
|
||||
|
|
@ -166,6 +166,7 @@ describe('Settings Codex health route', () => {
|
|||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.ready.overall).toBe(true);
|
||||
expect(response.body.sdk.version).toBe('0.144.1');
|
||||
expect(mockCodexHealthService.getHealth).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ describe('SearchService', () => {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
process.env = oldEnv;
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
process.env = oldEnv;
|
||||
});
|
||||
|
||||
it('searches task and docs markdown with keyword fallback', async () => {
|
||||
|
|
@ -135,7 +135,10 @@ describe('SearchService', () => {
|
|||
);
|
||||
});
|
||||
|
||||
const result = await new SearchService().search({ query: 'semantic search' });
|
||||
const result = await new SearchService().search({
|
||||
query: 'semantic search',
|
||||
collections: ['tasks-active'],
|
||||
});
|
||||
|
||||
expect(result.backend).toBe('qmd');
|
||||
expect(result.degraded).toBe(false);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { execFile } from 'child_process';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { promisify } from 'util';
|
||||
import { ConfigService } from './config-service.js';
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ export interface CodexHealthStatus {
|
|||
};
|
||||
sdk: {
|
||||
available: boolean;
|
||||
version?: string;
|
||||
error?: string;
|
||||
};
|
||||
agents: {
|
||||
|
|
@ -107,7 +109,15 @@ export class CodexHealthService {
|
|||
private async checkSdk(): Promise<CodexHealthStatus['sdk']> {
|
||||
try {
|
||||
await import('@openai/codex-sdk');
|
||||
return { available: true };
|
||||
const moduleUrl = import.meta.resolve('@openai/codex-sdk');
|
||||
const packageUrl = new URL('../package.json', moduleUrl);
|
||||
const packageMetadata = JSON.parse(await readFile(packageUrl, 'utf8')) as {
|
||||
version?: unknown;
|
||||
};
|
||||
return {
|
||||
available: true,
|
||||
version: typeof packageMetadata.version === 'string' ? packageMetadata.version : undefined,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { available: false, error: error.message || 'Codex SDK is not available' };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue