refactor: centralize paired room role normalization (#193)

This commit is contained in:
Eyejoker
2026-05-30 00:03:15 +09:00
committed by GitHub
parent 5fa5b2f684
commit adf9c16b9a
10 changed files with 107 additions and 50 deletions

View File

@@ -9,7 +9,11 @@ import {
inferRoleFromServiceShadow,
resolveRoleServiceShadow,
} from '../role-service-shadow.js';
import type { AgentType, PairedRoomRole } from '../types.js';
import {
normalizePairedRoomRoleOrNull,
type AgentType,
type PairedRoomRole,
} from '../types.js';
import { normalizeStoredAgentType } from './room-registration.js';
interface RequiredRoleMetadataInput {
@@ -336,14 +340,6 @@ export function readCanonicalChannelOwnerLeaseMetadata(input: {
};
}
function normalizeHandoffRole(
role: string | null | undefined,
): PairedRoomRole | null {
return role === 'owner' || role === 'reviewer' || role === 'arbiter'
? role
: null;
}
function assertStoredRoleMatchesServiceShadow(args: {
context: string;
storedRole: PairedRoomRole | null;
@@ -389,11 +385,11 @@ export function fillCanonicalServiceHandoffMetadata(input: {
}): CanonicalServiceHandoffMetadata {
const context = `service_handoffs(${input.id})`;
const sourceRole =
normalizeHandoffRole(input.source_role) ??
normalizeHandoffRole(input.intended_role);
normalizePairedRoomRoleOrNull(input.source_role) ??
normalizePairedRoomRoleOrNull(input.intended_role);
const targetRole =
normalizeHandoffRole(input.target_role) ??
normalizeHandoffRole(input.intended_role);
normalizePairedRoomRoleOrNull(input.target_role) ??
normalizePairedRoomRoleOrNull(input.intended_role);
const storedSourceAgentType = normalizeStoredAgentType(
input.source_agent_type,
);
@@ -541,11 +537,11 @@ export function readCanonicalServiceHandoffMetadata(input: {
}): CanonicalServiceHandoffMetadata {
const context = `service_handoffs(${input.id})`;
const sourceRole =
normalizeHandoffRole(input.source_role) ??
normalizeHandoffRole(input.intended_role);
normalizePairedRoomRoleOrNull(input.source_role) ??
normalizePairedRoomRoleOrNull(input.intended_role);
const targetRole =
normalizeHandoffRole(input.target_role) ??
normalizeHandoffRole(input.intended_role);
normalizePairedRoomRoleOrNull(input.target_role) ??
normalizePairedRoomRoleOrNull(input.intended_role);
const sourceServiceId = readStoredHandoffServiceId({
context,
field: 'source_service_id',

View File

@@ -4,6 +4,7 @@ import { resolveRuntimeAttachmentBaseDirs } from './attachment-base-dirs.js';
import { deliverOpenWorkItem } from './message-runtime-delivery.js';
import { parseVisibleVerdict } from './paired-verdict.js';
import { resolveChannelForDeliveryRole } from './router.js';
import { normalizePairedRoomRole } from './types.js';
import type {
Channel,
OutboundAttachment,
@@ -66,19 +67,6 @@ export type IpcOutboundDeliveryResult =
| 'queued_retry'
| 'skipped_recorded_terminal';
function normalizeDeliveryRole(
senderRole: string | undefined,
): PairedRoomRole | undefined {
if (
senderRole === 'owner' ||
senderRole === 'reviewer' ||
senderRole === 'arbiter'
) {
return senderRole;
}
return undefined;
}
function isTerminalStatusMessage(text: string): boolean {
return parseVisibleVerdict(text) !== 'continue';
}
@@ -143,7 +131,7 @@ export async function deliverIpcOutboundMessage(
deps: IpcOutboundDeliveryDeps,
): Promise<IpcOutboundDeliveryResult> {
const log = deps.log ?? logger;
const deliveryRole = normalizeDeliveryRole(args.senderRole);
const deliveryRole = normalizePairedRoomRole(args.senderRole);
if (
args.runId &&
(deliveryRole === 'reviewer' || deliveryRole === 'arbiter') &&

View File

@@ -9,7 +9,7 @@ import {
isWatchCiTask,
} from '../task-watch-status.js';
import type { IpcDeps, TaskIpcPayload } from '../ipc-types.js';
import type { PairedRoomRole, RegisteredGroup } from '../types.js';
import { normalizePairedRoomRole, type RegisteredGroup } from '../types.js';
type RoomBindings = ReturnType<IpcDeps['roomBindings']>;
type ScheduleType = 'cron' | 'interval' | 'once';
@@ -20,14 +20,6 @@ interface ResolvedScheduleTarget {
room: RegisteredGroup;
}
function normalizePairedRoomRole(
role: string | null | undefined,
): PairedRoomRole | undefined {
return role === 'owner' || role === 'reviewer' || role === 'arbiter'
? role
: undefined;
}
export function handleScheduleTask(
data: TaskIpcPayload,
sourceGroup: string,

View File

@@ -1,6 +1,11 @@
import { describe, expect, it } from 'vitest';
import { normalizeAgentOutputPhase, toVisiblePhase } from './types.js';
import {
normalizeAgentOutputPhase,
normalizePairedRoomRole,
normalizePairedRoomRoleOrNull,
toVisiblePhase,
} from './types.js';
describe('phase helpers', () => {
it('maps agent output phases to visible phases', () => {
@@ -15,3 +20,17 @@ describe('phase helpers', () => {
expect(normalizeAgentOutputPhase('progress')).toBe('progress');
});
});
describe('paired room role helpers', () => {
it('normalizes valid paired room roles', () => {
expect(normalizePairedRoomRole('owner')).toBe('owner');
expect(normalizePairedRoomRole('reviewer')).toBe('reviewer');
expect(normalizePairedRoomRole('arbiter')).toBe('arbiter');
});
it('rejects invalid paired room roles', () => {
expect(normalizePairedRoomRole('single')).toBeUndefined();
expect(normalizePairedRoomRole(undefined)).toBeUndefined();
expect(normalizePairedRoomRoleOrNull('codex')).toBeNull();
});
});

View File

@@ -1,5 +1,20 @@
import {
PAIRED_ROOM_ROLES,
isPairedRoomRole,
normalizePairedRoomRole,
normalizePairedRoomRoleOrNull,
type PairedRoomRole,
} from 'ejclaw-runners-shared';
import type { VisibleVerdict } from './paired-verdict.js';
export {
PAIRED_ROOM_ROLES,
isPairedRoomRole,
normalizePairedRoomRole,
normalizePairedRoomRoleOrNull,
type PairedRoomRole,
};
export interface AgentConfig {
timeout?: number; // Default: 300000 (5 minutes)
// Per-group model/effort overrides (take precedence over global env vars)
@@ -55,8 +70,6 @@ export interface DeleteRecentMessagesByContentOptions {
limit?: number;
}
export type PairedRoomRole = 'owner' | 'reviewer' | 'arbiter';
export type PairedTaskStatus =
| 'active'
| 'review_ready'