Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/runtime/__tests__/colony-runtime-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import { join } from 'node:path';
import { describe, it } from 'node:test';

import {
COLONY_RUNTIME_SUMMARY_SCHEMA,
type ColonyRuntimeSummaryV1,
colonyRuntimeSummaryPath,
extractRecentEditPathsFromHookPayload,
extractRecentToolsFromHookPayload,
validateColonyRuntimeSummary,
writeColonyRuntimeSummary,
} from '../colony-runtime-summary.js';
import { colonyRuntimeSummaryV1Fixture } from './fixtures/colony-runtime-summary-v1.js';
import { writeSessionEnd, writeSessionStart } from '../../hooks/session.js';

async function withTempRepo(fn: (cwd: string) => Promise<void>): Promise<void> {
Expand Down Expand Up @@ -37,20 +41,76 @@ describe('colony runtime summary', () => {
await readFile(colonyRuntimeSummaryPath(cwd), 'utf-8'),
) as typeof summary;

assert.equal(persisted.schema, COLONY_RUNTIME_SUMMARY_SCHEMA);
assert.equal(persisted.version, 1);
assert.equal(persisted.runtime, 'omx');
assert.equal(persisted.session_id, 'sess-active');
assert.equal(persisted.agent, 'codex');
assert.equal(persisted.repo_root, cwd);
assert.equal(persisted.cwd, cwd);
assert.equal(persisted.worktree_path, cwd);
assert.equal(persisted.active, true);
assert.equal(persisted.last_seen_at, '2026-05-01T10:00:00.000Z');
assert.equal(persisted.timestamp, '2026-05-01T10:00:00.000Z');
assert.deepEqual(persisted.recent_tools, ['Bash']);
assert.deepEqual(persisted.recent_edit_paths, ['src/runtime/run-state.ts']);
assert.deepEqual(persisted.warnings, []);
});
});

it('matches the Colony-compatible runtime-summary v1 fixture shape', async () => {
validateColonyRuntimeSummary(colonyRuntimeSummaryV1Fixture);

await withTempRepo(async (cwd) => {
await writeColonyRuntimeSummary(cwd, {
sessionId: colonyRuntimeSummaryV1Fixture.session_id,
agent: colonyRuntimeSummaryV1Fixture.agent,
active: colonyRuntimeSummaryV1Fixture.active,
lastSeenAt: colonyRuntimeSummaryV1Fixture.last_seen_at,
recentTools: colonyRuntimeSummaryV1Fixture.recent_tools,
recentEditPaths: colonyRuntimeSummaryV1Fixture.recent_edit_paths,
warnings: colonyRuntimeSummaryV1Fixture.warnings,
});

const persisted = JSON.parse(
await readFile(colonyRuntimeSummaryPath(cwd), 'utf-8'),
) as Record<string, unknown>;

for (const key of Object.keys(colonyRuntimeSummaryV1Fixture)) {
assert.equal(Object.prototype.hasOwnProperty.call(persisted, key), true, key);
}
assert.equal(persisted.schema, colonyRuntimeSummaryV1Fixture.schema);
assert.equal(persisted.runtime, colonyRuntimeSummaryV1Fixture.runtime);
assert.equal(persisted.timestamp, persisted.last_seen_at);
assert.deepEqual(persisted.recent_tools, colonyRuntimeSummaryV1Fixture.recent_tools);
assert.deepEqual(persisted.recent_edit_paths, colonyRuntimeSummaryV1Fixture.recent_edit_paths);
assert.deepEqual(persisted.warnings, colonyRuntimeSummaryV1Fixture.warnings);
});
});

it('rejects malformed summaries before writing', () => {
assert.throws(
() => validateColonyRuntimeSummary({
schema: 'wrong-schema',
version: 1,
runtime: 'omx',
session_id: 'sess-invalid',
agent: 'codex',
repo_root: '/repo',
cwd: '/repo',
branch: 'agent/codex/test',
worktree_path: '/repo',
active: true,
last_seen_at: '2026-05-01T10:00:00.000Z',
timestamp: '2026-05-01T10:00:00.000Z',
recent_tools: [],
recent_edit_paths: [],
warnings: [],
} as unknown as ColonyRuntimeSummaryV1),
/schema must be colony-runtime-summary-v1/,
);
});

it('emits fresh summaries from OMX session lifecycle state', async () => {
await withTempRepo(async (cwd) => {
await writeSessionStart(cwd, 'sess-lifecycle', {
Expand Down
19 changes: 19 additions & 0 deletions src/runtime/__tests__/fixtures/colony-runtime-summary-v1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type ColonyRuntimeSummaryV1 } from '../../colony-runtime-summary.js';

export const colonyRuntimeSummaryV1Fixture: ColonyRuntimeSummaryV1 = {
schema: 'colony-runtime-summary-v1',
version: 1,
runtime: 'omx',
session_id: 'codex@fixture',
agent: 'codex',
repo_root: '/repo',
cwd: '/repo',
branch: 'agent/codex/fixture',
worktree_path: '/repo',
active: true,
last_seen_at: '2026-05-01T10:00:00.000Z',
timestamp: '2026-05-01T10:00:00.000Z',
recent_tools: ['Bash'],
recent_edit_paths: ['src/runtime/run-state.ts'],
warnings: [],
};
49 changes: 47 additions & 2 deletions src/runtime/colony-runtime-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';

const SUMMARY_FILENAME = 'colony-runtime-summary.json';
export const COLONY_RUNTIME_SUMMARY_SCHEMA = 'colony-runtime-summary-v1';
const SUMMARY_VERSION = 1;
const MAX_RECENT_ITEMS = 12;

export interface ColonyRuntimeSummaryV1 {
schema: typeof COLONY_RUNTIME_SUMMARY_SCHEMA;
version: 1;
runtime: 'omx';
session_id: string;
agent: string;
repo_root: string;
cwd: string;
branch: string;
worktree_path: string;
active: boolean;
last_seen_at: string;
timestamp: string;
recent_tools: string[];
recent_edit_paths: string[];
warnings: string[];
Expand All @@ -38,7 +42,7 @@ interface ReadJsonResult {
}

function stateDir(cwd: string): string {
return join(resolve(cwd), '.omx', 'state');
return join(detectWorktreePath(cwd), '.omx', 'state');
}

export function colonyRuntimeSummaryPath(cwd: string): string {
Expand Down Expand Up @@ -137,11 +141,48 @@ function buildWarnings(
return warnings;
}

function isIsoTimestamp(value: string): boolean {
return Number.isFinite(Date.parse(value));
}

function validateStringArray(value: unknown): boolean {
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
}

export function validateColonyRuntimeSummary(summary: ColonyRuntimeSummaryV1): void {
const errors: string[] = [];
if (summary.schema !== COLONY_RUNTIME_SUMMARY_SCHEMA) {
errors.push(`schema must be ${COLONY_RUNTIME_SUMMARY_SCHEMA}`);
}
if (summary.runtime !== 'omx') errors.push('runtime must be omx');
if (summary.version !== SUMMARY_VERSION) errors.push(`version must be ${SUMMARY_VERSION}`);
for (const key of ['session_id', 'agent', 'repo_root', 'cwd', 'worktree_path'] as const) {
if (!safeString(summary[key])) errors.push(`${key} must be a non-empty string`);
}
if (typeof summary.branch !== 'string') errors.push('branch must be a string');
if (typeof summary.active !== 'boolean') errors.push('active must be a boolean');
if (!safeString(summary.last_seen_at) || !isIsoTimestamp(summary.last_seen_at)) {
errors.push('last_seen_at must be an ISO timestamp');
}
if (summary.timestamp !== summary.last_seen_at) {
errors.push('timestamp must match last_seen_at');
}
if (!validateStringArray(summary.recent_tools)) errors.push('recent_tools must be a string array');
if (!validateStringArray(summary.recent_edit_paths)) {
errors.push('recent_edit_paths must be a string array');
}
if (!validateStringArray(summary.warnings)) errors.push('warnings must be a string array');
if (errors.length > 0) {
throw new Error(`Invalid colony runtime summary: ${errors.join('; ')}`);
}
}

export async function writeColonyRuntimeSummary(
cwd: string,
options: WriteColonyRuntimeSummaryOptions = {},
): Promise<ColonyRuntimeSummaryV1> {
const repoRoot = resolve(cwd);
const resolvedCwd = resolve(cwd);
const repoRoot = detectWorktreePath(resolvedCwd);
const summaryPath = colonyRuntimeSummaryPath(repoRoot);
const [sessionRead, existingRead] = await Promise.all([
readJsonIfExists(sessionPath(repoRoot)),
Expand All @@ -160,20 +201,24 @@ export async function writeColonyRuntimeSummary(
: sessionRead.data !== null;

const summary: ColonyRuntimeSummaryV1 = {
schema: COLONY_RUNTIME_SUMMARY_SCHEMA,
version: SUMMARY_VERSION,
runtime: 'omx',
session_id: sessionId,
agent: safeString(options.agent) || safeString(existingRead.data?.agent) || defaultAgent(),
repo_root: repoRoot,
cwd: resolvedCwd,
branch: detectBranch(repoRoot),
worktree_path: detectWorktreePath(repoRoot),
active,
last_seen_at: lastSeenAt,
timestamp: lastSeenAt,
recent_tools: mergeRecent(options.recentTools, existingRead.data?.recent_tools),
recent_edit_paths: mergeRecent(options.recentEditPaths, existingRead.data?.recent_edit_paths),
warnings: buildWarnings(options, sessionRead.malformed, existingRead.malformed),
};

validateColonyRuntimeSummary(summary);
await writeAtomicFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`);
return summary;
}
Expand Down
Loading