mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Constrain app-server generated type boundaries
This commit is contained in:
parent
8e818f5fa8
commit
ad78966154
29 changed files with 394 additions and 105 deletions
|
|
@ -67,9 +67,12 @@ const uiRootImportRestrictions = [
|
|||
const generatedAppServerSourceImportPatterns = importBoundaryPatterns("generated/app-server", "src/generated/app-server", 6);
|
||||
const generatedAppServerTestImportPatterns = importBoundaryPatterns("src/generated/app-server", "src/generated/app-server", 6);
|
||||
const lowerLevelFeatureImportPatterns = importBoundaryPatterns("features", "src/features", 6);
|
||||
const appServerConnectionImportPatterns = importBoundaryPatterns("app-server/connection", "src/app-server/connection", 6);
|
||||
const appServerProtocolConnectionImportPatterns = [...appServerConnectionImportPatterns, "../connection", "../connection/**"];
|
||||
const nonAppServerBannedAppServerProtocolModules = [
|
||||
"catalog",
|
||||
"diagnostics",
|
||||
"file-change",
|
||||
"initialization",
|
||||
"request-input",
|
||||
"runtime-config",
|
||||
|
|
@ -78,11 +81,16 @@ const nonAppServerBannedAppServerProtocolModules = [
|
|||
"thread",
|
||||
"thread-goal",
|
||||
"thread-settings",
|
||||
"turn",
|
||||
"turn-history",
|
||||
];
|
||||
const nonAppServerBannedAppServerProtocolImportPatterns = nonAppServerBannedAppServerProtocolModules.flatMap((moduleName) =>
|
||||
importBoundaryPatterns(`app-server/protocol/${moduleName}`, `src/app-server/protocol/${moduleName}`, 6),
|
||||
);
|
||||
const chatAppServerProtocolBoundaryBannedModules = nonAppServerBannedAppServerProtocolModules.filter((moduleName) => moduleName !== "turn");
|
||||
const chatAppServerProtocolBoundaryBannedImportPatterns = chatAppServerProtocolBoundaryBannedModules.flatMap((moduleName) =>
|
||||
importBoundaryPatterns(`app-server/protocol/${moduleName}`, `src/app-server/protocol/${moduleName}`, 6),
|
||||
);
|
||||
const generatedAppServerThreadImportRestrictions = [
|
||||
{
|
||||
selector:
|
||||
|
|
@ -90,6 +98,17 @@ const generatedAppServerThreadImportRestrictions = [
|
|||
message: "Import generated app-server Thread as AppServerThread, or use the Panel-owned domain Thread model.",
|
||||
},
|
||||
];
|
||||
const turnProtocolGeneratedImportRestrictions = [
|
||||
{
|
||||
selector: "ImportDeclaration[source.value=/generated\\/app-server\\//]:not([source.value=/generated\\/app-server\\/v2\\/ThreadItem$/])",
|
||||
message: "Only generated ThreadItem is allowed in the turn protocol exception. Model other turn payload shapes locally.",
|
||||
},
|
||||
];
|
||||
const chatAppServerProtocolBoundaryFiles = [
|
||||
"src/features/chat/app-server/inbound/notification-plan.ts",
|
||||
"src/features/chat/app-server/mappers/message-stream/streaming-items.ts",
|
||||
"src/features/chat/app-server/mappers/message-stream/turn-items.ts",
|
||||
];
|
||||
const unsafeIteratorRestrictions = [
|
||||
{
|
||||
selector: "MemberExpression[property.name='value'][object.type='CallExpression'][object.callee.property.name='next']",
|
||||
|
|
@ -718,6 +737,11 @@ export default defineConfig([
|
|||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: appServerProtocolConnectionImportPatterns,
|
||||
message:
|
||||
"Keep app-server protocol modules as connection-independent adapters. Put protocol-local projections in protocol modules and let connection/client consume them.",
|
||||
},
|
||||
{
|
||||
group: lowerLevelFeatureImportPatterns,
|
||||
message: "Lower-level modules must not import feature modules. Move shared behavior to shared, domain, or app-server.",
|
||||
|
|
@ -727,6 +751,69 @@ export default defineConfig([
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/app-server/**/*.{ts,tsx}"],
|
||||
ignores: ["src/app-server/connection/**/*.{ts,tsx}", "src/app-server/protocol/**/*.{ts,tsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: generatedAppServerSourceImportPatterns,
|
||||
message:
|
||||
"Keep generated app-server bindings in the raw connection layer or explicit protocol adapters. App-server services, queries, catalogs, and thread helpers should expose domain models or local projections.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/app-server/protocol/**/*.{ts,tsx}"],
|
||||
ignores: ["src/app-server/protocol/turn.ts"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: appServerProtocolConnectionImportPatterns,
|
||||
message:
|
||||
"Keep app-server protocol modules as connection-independent adapters. Put protocol-local projections in protocol modules and let connection/client consume them.",
|
||||
},
|
||||
{
|
||||
group: lowerLevelFeatureImportPatterns,
|
||||
message: "Lower-level modules must not import feature modules. Move shared behavior to shared, domain, or app-server.",
|
||||
},
|
||||
{
|
||||
group: generatedAppServerSourceImportPatterns,
|
||||
message:
|
||||
"Keep generated app-server bindings out of protocol modules except the explicit turn protocol exception. Model app-server payloads with protocol-local projections.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/app-server/protocol/turn.ts"],
|
||||
rules: {
|
||||
...restrictedSyntaxRule(turnProtocolGeneratedImportRestrictions),
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: appServerProtocolConnectionImportPatterns,
|
||||
message:
|
||||
"Keep app-server protocol modules as connection-independent adapters. Put protocol-local projections in protocol modules and let connection/client consume them.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.{ts,tsx}"],
|
||||
ignores: ["src/app-server/**/*.{ts,tsx}"],
|
||||
|
|
@ -749,6 +836,27 @@ export default defineConfig([
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: chatAppServerProtocolBoundaryFiles,
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: chatAppServerProtocolBoundaryBannedImportPatterns,
|
||||
message:
|
||||
"Chat app-server ingestion and message-stream conversion may consume the app-server turn protocol only. Convert other protocol payloads to local or domain models at the boundary.",
|
||||
},
|
||||
{
|
||||
group: generatedAppServerSourceImportPatterns,
|
||||
message: "Keep generated app-server types behind src/app-server; expose Panel-owned models to feature, UI, and reducer code.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["tests/**/*.{ts,tsx}"],
|
||||
ignores: ["tests/app-server/**/*.{ts,tsx}"],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { AppServerClient } from "../connection/client";
|
||||
import type { ModelListResponse } from "../../generated/app-server/v2/ModelListResponse";
|
||||
import {
|
||||
appServerHookOperationFromHookItem,
|
||||
type CatalogModel,
|
||||
hookItemsFromCatalogHooks,
|
||||
modelMetadataFromCatalogModels,
|
||||
skillMetadataFromCatalogSkills,
|
||||
|
|
@ -14,8 +14,8 @@ export interface HookData {
|
|||
errors: string[];
|
||||
}
|
||||
|
||||
interface ModelMetadataClient {
|
||||
listModels(includeHidden: boolean): Promise<ModelListResponse>;
|
||||
export interface ModelMetadataClient {
|
||||
listModels(includeHidden: boolean): Promise<{ data: readonly CatalogModel[] }>;
|
||||
}
|
||||
|
||||
export async function listModelMetadata(client: ModelMetadataClient, options: { includeHidden?: boolean } = {}): Promise<ModelMetadata[]> {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import type { ConfigReadResponse } from "../../generated/app-server/v2/ConfigRea
|
|||
import type { ConfigWriteResponse } from "../../generated/app-server/v2/ConfigWriteResponse";
|
||||
import type { FsReadFileResponse } from "../../generated/app-server/v2/FsReadFileResponse";
|
||||
import type { GetAccountRateLimitsResponse } from "../../generated/app-server/v2/GetAccountRateLimitsResponse";
|
||||
import type { HookTrustStatus } from "../../generated/app-server/v2/HookTrustStatus";
|
||||
import type { HooksListResponse } from "../../generated/app-server/v2/HooksListResponse";
|
||||
import type { CollaborationModeListResponse } from "../../generated/app-server/v2/CollaborationModeListResponse";
|
||||
import type { ListMcpServerStatusParams } from "../../generated/app-server/v2/ListMcpServerStatusParams";
|
||||
|
|
@ -46,9 +45,12 @@ import type { ServerNotification } from "../../generated/app-server/ServerNotifi
|
|||
import type { ServerRequest } from "../../generated/app-server/ServerRequest";
|
||||
import type { JsonValue } from "../../generated/app-server/serde_json/JsonValue";
|
||||
import type { CodexInput } from "../../domain/chat/input";
|
||||
import type { ThreadGoalUpdate } from "../../domain/threads/goal";
|
||||
import type { RuntimeServiceTierRequest, RuntimeSettingsPatch } from "../../domain/runtime/thread-settings";
|
||||
import type { AppServerHookOperation } from "../protocol/catalog";
|
||||
import { additionalContextFromCodexInput, toAppServerUserInput } from "../protocol/request-input";
|
||||
import { appServerThreadGoalUpdate, type ThreadGoalUpdate } from "../protocol/thread-goal";
|
||||
import { appServerRuntimeSettingsPatch, type RuntimeServiceTierRequest, type RuntimeSettingsPatch } from "../protocol/thread-settings";
|
||||
import { appServerThreadGoalUpdate } from "../protocol/thread-goal";
|
||||
import { appServerRuntimeSettingsPatch } from "../protocol/thread-settings";
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
||||
|
||||
|
|
@ -61,12 +63,6 @@ export interface AppServerClientHandlers {
|
|||
|
||||
export type AppServerTransportFactory = (handlers: AppServerTransportHandlers) => AppServerTransport;
|
||||
|
||||
export interface AppServerHookOperation {
|
||||
key: string;
|
||||
currentHash: string;
|
||||
trustStatus: HookTrustStatus;
|
||||
}
|
||||
|
||||
interface AppServerTurnRuntimeOverrides {
|
||||
serviceTier?: RuntimeServiceTierRequest;
|
||||
collaborationMode?: CollaborationMode;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import type { AppServerHookOperation } from "../connection/client";
|
||||
import type { HookItem, ModelMetadata, SkillMetadata } from "../../domain/catalog/metadata";
|
||||
|
||||
export interface CatalogModel {
|
||||
|
|
@ -41,6 +40,12 @@ export interface CatalogHookMetadata {
|
|||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AppServerHookOperation {
|
||||
key: string;
|
||||
currentHash: string;
|
||||
trustStatus: HookItem["trustStatus"];
|
||||
}
|
||||
|
||||
function modelMetadataFromCatalogModel(model: CatalogModel): ModelMetadata {
|
||||
return {
|
||||
id: model.id,
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
import type { FileUpdateChange as GeneratedFileUpdateChange } from "../../generated/app-server/v2/FileUpdateChange";
|
||||
|
||||
export type FileUpdateChange = GeneratedFileUpdateChange;
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
import type { InitializeResponse as AppServerInitializeResponse } from "../../generated/app-server/InitializeResponse";
|
||||
import type { ServerInitialization } from "../../domain/server/initialization";
|
||||
|
||||
interface AppServerInitializeResponse {
|
||||
userAgent: string;
|
||||
codexHome: string;
|
||||
platformFamily: string;
|
||||
platformOs: string;
|
||||
}
|
||||
|
||||
export function appServerInitializationFromResponse(response: AppServerInitializeResponse): ServerInitialization {
|
||||
return {
|
||||
userAgent: response.userAgent,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
import type { AdditionalContextEntry } from "../../generated/app-server/v2/AdditionalContextEntry";
|
||||
import type { UserInput } from "../../generated/app-server/v2/UserInput";
|
||||
import type { CodexInputItem } from "../../domain/chat/input";
|
||||
|
||||
export type { CodexInput, CodexInputItem } from "../../domain/chat/input";
|
||||
type AppServerUserInput =
|
||||
| { type: "text"; text: string; text_elements: [] }
|
||||
| { type: "image"; detail?: "auto" | "low" | "high" | "original"; url: string }
|
||||
| { type: "localImage"; detail?: "auto" | "low" | "high" | "original"; path: string }
|
||||
| { type: "skill"; name: string; path: string }
|
||||
| { type: "mention"; name: string; path: string };
|
||||
|
||||
export function toAppServerUserInput(input: readonly CodexInputItem[]): UserInput[] {
|
||||
interface AppServerAdditionalContextEntry {
|
||||
value: string;
|
||||
kind: "untrusted" | "application";
|
||||
}
|
||||
|
||||
export function toAppServerUserInput(input: readonly CodexInputItem[]): AppServerUserInput[] {
|
||||
return input.flatMap((item) => {
|
||||
if (item.type === "text") return { type: "text", text: item.text, text_elements: [] };
|
||||
if (item.type === "additionalContext") return [];
|
||||
|
|
@ -12,8 +20,10 @@ export function toAppServerUserInput(input: readonly CodexInputItem[]): UserInpu
|
|||
});
|
||||
}
|
||||
|
||||
export function additionalContextFromCodexInput(input: readonly CodexInputItem[]): Record<string, AdditionalContextEntry> | undefined {
|
||||
const additionalContext: Record<string, AdditionalContextEntry> = {};
|
||||
export function additionalContextFromCodexInput(
|
||||
input: readonly CodexInputItem[],
|
||||
): Record<string, AppServerAdditionalContextEntry> | undefined {
|
||||
const additionalContext: Record<string, AppServerAdditionalContextEntry> = {};
|
||||
for (const item of input) {
|
||||
if (item.type !== "additionalContext") continue;
|
||||
if (!item.key || !item.value) continue;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,31 @@
|
|||
import type { GetAccountRateLimitsResponse as AppServerAccountRateLimitsResponse } from "../../generated/app-server/v2/GetAccountRateLimitsResponse";
|
||||
import type { RateLimitSnapshot as AppServerRateLimitSnapshot } from "../../generated/app-server/v2/RateLimitSnapshot";
|
||||
import type { RateLimitWindow as AppServerRateLimitWindow } from "../../generated/app-server/v2/RateLimitWindow";
|
||||
import type { SpendControlLimitSnapshot as AppServerSpendControlLimitSnapshot } from "../../generated/app-server/v2/SpendControlLimitSnapshot";
|
||||
import type { RateLimitSnapshot, RateLimitWindow, SpendControlLimitSnapshot } from "../../domain/runtime/metrics";
|
||||
|
||||
export type { RateLimitSnapshot, ThreadTokenUsage } from "../../domain/runtime/metrics";
|
||||
interface AppServerRateLimitWindow {
|
||||
usedPercent: number;
|
||||
windowDurationMins: number | null;
|
||||
resetsAt: number | null;
|
||||
}
|
||||
|
||||
interface AppServerSpendControlLimitSnapshot {
|
||||
limit: string;
|
||||
used: string;
|
||||
remainingPercent: number;
|
||||
resetsAt: number;
|
||||
}
|
||||
|
||||
interface AppServerRateLimitSnapshot extends Record<string, unknown> {
|
||||
limitId: string | null;
|
||||
limitName: string | null;
|
||||
primary: AppServerRateLimitWindow | null;
|
||||
secondary: AppServerRateLimitWindow | null;
|
||||
individualLimit: AppServerSpendControlLimitSnapshot | null;
|
||||
rateLimitReachedType: string | null;
|
||||
}
|
||||
|
||||
interface AppServerAccountRateLimitsResponse {
|
||||
rateLimits: AppServerRateLimitSnapshot;
|
||||
rateLimitsByLimitId: Record<string, AppServerRateLimitSnapshot | undefined> | null;
|
||||
}
|
||||
|
||||
function rateLimitSnapshotFromAppServerSnapshot(snapshot: AppServerRateLimitSnapshot): RateLimitSnapshot {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
import type { ThreadGoal as AppServerThreadGoal } from "../../generated/app-server/v2/ThreadGoal";
|
||||
import type { ThreadGoalStatus as AppServerThreadGoalStatus } from "../../generated/app-server/v2/ThreadGoalStatus";
|
||||
import type { JsonValue } from "../../generated/app-server/serde_json/JsonValue";
|
||||
import type { ThreadGoal, ThreadGoalStatus, ThreadGoalUpdate } from "../../domain/threads/goal";
|
||||
|
||||
export type { ThreadGoal, ThreadGoalUpdate } from "../../domain/threads/goal";
|
||||
interface AppServerThreadGoal {
|
||||
threadId: string;
|
||||
objective: string;
|
||||
status: AppServerThreadGoalStatus;
|
||||
tokenBudget: number | null;
|
||||
tokensUsed: number;
|
||||
timeUsedSeconds: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
type AppServerThreadGoalStatus = ThreadGoalStatus;
|
||||
type AppServerJsonValue = number | string | boolean | AppServerJsonValue[] | { [key: string]: AppServerJsonValue | undefined } | null;
|
||||
|
||||
export function threadGoalFromAppServerGoal(goal: AppServerThreadGoal | null): ThreadGoal | null {
|
||||
if (!goal) return null;
|
||||
|
|
@ -31,7 +40,7 @@ export function appServerThreadGoalUpdate(update: ThreadGoalUpdate): {
|
|||
};
|
||||
}
|
||||
|
||||
export function appServerThreadGoalUserHistoryItem(text: string): JsonValue {
|
||||
export function appServerThreadGoalUserHistoryItem(text: string): AppServerJsonValue {
|
||||
return {
|
||||
type: "message",
|
||||
role: "user",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import type { ThreadSettingsUpdateParams } from "../../generated/app-server/v2/ThreadSettingsUpdateParams";
|
||||
import type { RuntimeSettingsPatch } from "../../domain/runtime/thread-settings";
|
||||
|
||||
export type { RuntimeServiceTierRequest, RuntimeSettingsPatch } from "../../domain/runtime/thread-settings";
|
||||
|
||||
type AppServerRuntimeSettingsPatch = Omit<ThreadSettingsUpdateParams, "threadId">;
|
||||
type AppServerRuntimeSettingsPatch = Omit<RuntimeSettingsPatch, "collaborationMode"> & {
|
||||
collaborationMode?: {
|
||||
mode: "plan" | "default";
|
||||
settings: {
|
||||
model: string;
|
||||
reasoning_effort: string | null;
|
||||
developer_instructions: string | null;
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
|
||||
export function appServerRuntimeSettingsPatch(update: RuntimeSettingsPatch): AppServerRuntimeSettingsPatch {
|
||||
const { collaborationMode, ...settings } = update;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import type { ThreadItem as GeneratedTurnItem } from "../../generated/app-server/v2/ThreadItem";
|
||||
import type { Turn as GeneratedTurnRecord } from "../../generated/app-server/v2/Turn";
|
||||
import type { UserInput } from "../../generated/app-server/v2/UserInput";
|
||||
import {
|
||||
conversationSummaryFromTranscriptEntries,
|
||||
nonEmptyConversationSummaries,
|
||||
|
|
@ -9,7 +7,50 @@ import {
|
|||
} from "../../domain/threads/transcript";
|
||||
|
||||
export type TurnItem = GeneratedTurnItem;
|
||||
export type TurnRecord = GeneratedTurnRecord;
|
||||
|
||||
type AppServerUserInput =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "image"; url: string }
|
||||
| { type: "localImage"; path: string }
|
||||
| { type: "mention"; name: string; path: string }
|
||||
| { type: "skill"; name: string; path: string };
|
||||
type TurnItemsView = "notLoaded" | "summary" | "full";
|
||||
type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
|
||||
type HttpCodexErrorInfo =
|
||||
| { httpConnectionFailed: { httpStatusCode: number | null } }
|
||||
| { responseStreamConnectionFailed: { httpStatusCode: number | null } }
|
||||
| { responseStreamDisconnected: { httpStatusCode: number | null } }
|
||||
| { responseTooManyFailedAttempts: { httpStatusCode: number | null } };
|
||||
type AppServerCodexErrorInfo =
|
||||
| "contextWindowExceeded"
|
||||
| "usageLimitExceeded"
|
||||
| "serverOverloaded"
|
||||
| "cyberPolicy"
|
||||
| "internalServerError"
|
||||
| "unauthorized"
|
||||
| "badRequest"
|
||||
| "threadRollbackFailed"
|
||||
| "sandboxError"
|
||||
| "other"
|
||||
| HttpCodexErrorInfo
|
||||
| { activeTurnNotSteerable: { turnKind: "review" | "compact" } };
|
||||
|
||||
interface TurnError {
|
||||
message: string;
|
||||
codexErrorInfo: AppServerCodexErrorInfo | null;
|
||||
additionalDetails: string | null;
|
||||
}
|
||||
|
||||
export interface TurnRecord {
|
||||
id: string;
|
||||
items: TurnItem[];
|
||||
itemsView: TurnItemsView;
|
||||
status: TurnStatus;
|
||||
error: TurnError | null;
|
||||
startedAt: number | null;
|
||||
completedAt: number | null;
|
||||
durationMs: number | null;
|
||||
}
|
||||
|
||||
function transcriptEntriesFromTurnRecord(turn: TurnRecord): ThreadTranscriptEntry[] {
|
||||
return turn.items.flatMap((item) => transcriptEntriesFromTurnItem(item, turn));
|
||||
|
|
@ -79,7 +120,7 @@ function transcriptEntriesFromTurnItem(item: TurnItem, turn: TurnRecord): Thread
|
|||
return [];
|
||||
}
|
||||
|
||||
function userInputText(content: UserInput[]): string {
|
||||
function userInputText(content: readonly AppServerUserInput[]): string {
|
||||
const hasText = content.some((item) => item.type === "text" && item.text.length > 0);
|
||||
return content
|
||||
.map((item) => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { readRateLimitMetadataProbe, readRuntimeConfigSnapshot, readSkillMetadat
|
|||
import { listThreads } from "../threads/data";
|
||||
import type { ModelMetadata } from "../../domain/catalog/metadata";
|
||||
import { createServerDiagnostics, diagnosticProbeError, diagnosticProbeOk, diagnosticsWithProbe } from "../../domain/server/diagnostics";
|
||||
import type { SharedServerMetadata } from "../../domain/server/metadata";
|
||||
import type { Thread } from "../../domain/threads/model";
|
||||
import {
|
||||
activeThreadsQueryKey,
|
||||
|
|
@ -16,7 +17,7 @@ import {
|
|||
cloneAppServerQueryContext,
|
||||
type AppServerQueryContext,
|
||||
} from "./keys";
|
||||
import { cloneModelMetadata, cloneSharedServerMetadata, cloneThreads, type SharedServerMetadata } from "./snapshots";
|
||||
import { cloneModelMetadata, cloneSharedServerMetadata, cloneThreads } from "./snapshots";
|
||||
|
||||
const ACTIVE_THREADS_STALE_TIME_MS = 10_000;
|
||||
const APP_SERVER_METADATA_STALE_TIME_MS = 10_000;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ModelMetadata } from "../../domain/catalog/metadata";
|
||||
import type { SharedServerMetadata } from "../../domain/server/metadata";
|
||||
import type { Thread } from "../../domain/threads/model";
|
||||
import type { AppServerObservedQueryResult, AppServerQueryCache } from "./cache";
|
||||
import {
|
||||
|
|
@ -7,7 +8,6 @@ import {
|
|||
cloneAppServerQueryContext,
|
||||
type AppServerQueryContext,
|
||||
} from "./keys";
|
||||
import type { SharedServerMetadata } from "./snapshots";
|
||||
|
||||
export interface AppServerSharedQueriesOptions {
|
||||
cache: AppServerQueryCache;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import type { RateLimitSnapshot } from "../../domain/runtime/metrics";
|
|||
import type { SharedServerMetadata } from "../../domain/server/metadata";
|
||||
import type { Thread } from "../../domain/threads/model";
|
||||
|
||||
export type { SharedServerMetadata } from "../../domain/server/metadata";
|
||||
|
||||
export function cloneThreads(threads: readonly Thread[]): Thread[] {
|
||||
return threads.map((thread) => ({ ...thread }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,16 +5,11 @@ import {
|
|||
type AppServerStartStructuredTurnOptions,
|
||||
} from "../connection/client";
|
||||
import { abortablePromise, throwIfAbortSignalAborted } from "../../shared/lifecycle/abortable";
|
||||
import type { InitializeResponse } from "../../generated/app-server/InitializeResponse";
|
||||
import type { RequestId } from "../../generated/app-server/RequestId";
|
||||
import type { ServerNotification } from "../../generated/app-server/ServerNotification";
|
||||
import type { JsonValue } from "../../generated/app-server/serde_json/JsonValue";
|
||||
import type { ModelListResponse } from "../../generated/app-server/v2/ModelListResponse";
|
||||
import type { ThreadStartResponse } from "../../generated/app-server/v2/ThreadStartResponse";
|
||||
import type { TurnStartResponse } from "../../generated/app-server/v2/TurnStartResponse";
|
||||
import type { RequestId, ServerNotification } from "../connection/rpc-messages";
|
||||
import type { ModelMetadataClient } from "../catalog/data";
|
||||
import { lastAgentMessageTextFromTurnRecord, type TurnItem, type TurnRecord } from "../protocol/turn";
|
||||
|
||||
export type StructuredTurnOutputSchema = JsonValue;
|
||||
export type StructuredTurnOutputSchema = AppServerStartStructuredTurnOptions["outputSchema"];
|
||||
|
||||
type StructuredTurnRuntimeOverride = NonNullable<AppServerStartStructuredTurnOptions["runtime"]>;
|
||||
|
||||
|
|
@ -33,16 +28,14 @@ const DEFAULT_EPHEMERAL_STRUCTURED_TURN_TIMERS: EphemeralStructuredTurnTimers =
|
|||
};
|
||||
|
||||
export interface EphemeralStructuredTurnClient {
|
||||
connect(): Promise<InitializeResponse>;
|
||||
connect(): Promise<unknown>;
|
||||
disconnect(): void;
|
||||
rejectServerRequest(requestId: RequestId, code: number, message: string): void;
|
||||
startEphemeralThread(options: AppServerStartEphemeralThreadOptions): Promise<ThreadStartResponse>;
|
||||
startStructuredTurn(options: AppServerStartStructuredTurnOptions): Promise<TurnStartResponse>;
|
||||
startEphemeralThread(options: AppServerStartEphemeralThreadOptions): Promise<{ thread: { id: string } }>;
|
||||
startStructuredTurn(options: AppServerStartStructuredTurnOptions): Promise<{ turn: TurnRecord }>;
|
||||
}
|
||||
|
||||
export interface EphemeralStructuredTurnRuntimeClient {
|
||||
listModels(includeHidden?: boolean): Promise<ModelListResponse>;
|
||||
}
|
||||
export type EphemeralStructuredTurnRuntimeClient = ModelMetadataClient;
|
||||
|
||||
type EphemeralStructuredTurnRuntimeCapableClient = EphemeralStructuredTurnClient & EphemeralStructuredTurnRuntimeClient;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
import type { RuntimeOverride, RuntimeOverrideSettings } from "../../domain/runtime/overrides";
|
||||
import { runtimeOverride, validatedRuntimeOverrideForModelMetadata } from "../../domain/runtime/overrides";
|
||||
import type { ModelListResponse } from "../../generated/app-server/v2/ModelListResponse";
|
||||
import { listModelMetadata } from "../catalog/data";
|
||||
|
||||
interface RuntimeOverrideModelClient {
|
||||
listModels(includeHidden: boolean): Promise<ModelListResponse>;
|
||||
}
|
||||
import { listModelMetadata, type ModelMetadataClient } from "../catalog/data";
|
||||
|
||||
export async function resolvedRuntimeOverrideForClient(
|
||||
client: RuntimeOverrideModelClient,
|
||||
client: ModelMetadataClient,
|
||||
settings: RuntimeOverrideSettings,
|
||||
): Promise<RuntimeOverride> {
|
||||
const runtime = runtimeOverride(settings);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import type { AppServerClient } from "../connection/client";
|
||||
import {
|
||||
appServerThreadGoalUserHistoryItem,
|
||||
threadGoalFromAppServerGoal,
|
||||
type ThreadGoal,
|
||||
type ThreadGoalUpdate,
|
||||
} from "../protocol/thread-goal";
|
||||
import type { ThreadGoal, ThreadGoalUpdate } from "../../domain/threads/goal";
|
||||
import { appServerThreadGoalUserHistoryItem, threadGoalFromAppServerGoal } from "../protocol/thread-goal";
|
||||
|
||||
export async function readThreadGoal(client: AppServerClient, threadId: string): Promise<ThreadGoal | null> {
|
||||
const response = await client.getThreadGoal(threadId);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import type { AppServerClient } from "../connection/client";
|
||||
import type { SortDirection } from "../../generated/app-server/v2/SortDirection";
|
||||
import type { ThreadRollbackResponse } from "../../generated/app-server/v2/ThreadRollbackResponse";
|
||||
import { threadFromThreadRecord, threadsFromThreadRecords, type ThreadRecord } from "../protocol/thread";
|
||||
import {
|
||||
chronologicalConversationSummariesFromTurnRecords,
|
||||
completedConversationSummariesFromTurnRecords,
|
||||
transcriptEntriesFromTurnRecords,
|
||||
type TurnItem,
|
||||
} from "../protocol/turn";
|
||||
import type { HistoricalTurn } from "../../domain/threads/history";
|
||||
import type { Thread } from "../../domain/threads/model";
|
||||
|
|
@ -16,6 +13,8 @@ import type { ThreadConversationSummary } from "../../domain/threads/transcript"
|
|||
|
||||
const THREAD_LIST_PAGE_LIMIT = 100;
|
||||
|
||||
export type ThreadTurnSortDirection = "asc" | "desc";
|
||||
|
||||
export async function listThreads(client: AppServerClient, cwd: string, options: { archived?: boolean } = {}): Promise<Thread[]> {
|
||||
const archived = options.archived ?? false;
|
||||
const records: ThreadRecord[] = [];
|
||||
|
|
@ -54,7 +53,7 @@ export async function readCompletedConversationSummariesPage(
|
|||
threadId: string,
|
||||
cursor: string | null,
|
||||
limit: number,
|
||||
sortDirection: SortDirection = "asc",
|
||||
sortDirection: ThreadTurnSortDirection = "asc",
|
||||
): Promise<ThreadConversationSummaryPage> {
|
||||
const response = await client.threadTurnsList(threadId, cursor, limit, sortDirection);
|
||||
return {
|
||||
|
|
@ -75,10 +74,17 @@ export async function readReferencedThreadConversationSummaries(
|
|||
export interface ThreadRollbackSnapshot {
|
||||
thread: Thread;
|
||||
cwd: string;
|
||||
turns: readonly HistoricalTurn<TurnItem>[];
|
||||
turns: readonly HistoricalTurn[];
|
||||
}
|
||||
|
||||
function threadRollbackSnapshotFromAppServerResponse(response: ThreadRollbackResponse): ThreadRollbackSnapshot {
|
||||
interface ThreadRollbackResult {
|
||||
readonly thread: ThreadRecord & {
|
||||
readonly cwd: string;
|
||||
readonly turns: readonly HistoricalTurn[];
|
||||
};
|
||||
}
|
||||
|
||||
function threadRollbackSnapshotFromAppServerResponse(response: ThreadRollbackResult): ThreadRollbackSnapshot {
|
||||
return {
|
||||
thread: threadFromThreadRecord(response.thread),
|
||||
cwd: response.thread.cwd,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { activeThreadSettingsAppliedAction } from "../../application/state/actions";
|
||||
import type { McpServerStartupStatus } from "../../../../domain/server/diagnostics";
|
||||
import { threadTokenUsageFromRuntimeUsage } from "../../../../domain/runtime/metrics";
|
||||
import type { FileUpdateChange } from "../../../../app-server/protocol/file-change";
|
||||
import { completedConversationSummaryFromTurnRecord, type TurnItem } from "../../../../app-server/protocol/turn";
|
||||
import type { ServerNotification } from "../../../../app-server/connection/rpc-messages";
|
||||
import { normalizeExplicitThreadName } from "../../../../domain/threads/model";
|
||||
|
|
@ -19,6 +18,7 @@ import {
|
|||
messageStreamItemsFromTurns,
|
||||
shouldSuppressLifecycleItem,
|
||||
} from "../mappers/message-stream/turn-items";
|
||||
import { normalizeFileChanges, type AppServerFileChange } from "../mappers/message-stream/file-changes";
|
||||
import { taskProgressMessageStreamItem } from "../../domain/message-stream/factories/task-progress";
|
||||
import type { MessageStreamItem, MessageStreamItemKind } from "../../domain/message-stream/items";
|
||||
import { goalChangeItem } from "../../domain/message-stream/factories/goal-items";
|
||||
|
|
@ -425,10 +425,10 @@ function completedItemPlan(state: ChatState, item: TurnItem, turnId: string): Ch
|
|||
};
|
||||
}
|
||||
|
||||
function fileChangePlan(itemId: string, turnId: string, changes: FileUpdateChange[], status: string): ChatNotificationPlan {
|
||||
function fileChangePlan(itemId: string, turnId: string, changes: readonly AppServerFileChange[], status: string): ChatNotificationPlan {
|
||||
return actionPlan({
|
||||
type: "message-stream/item-upserted",
|
||||
item: streamingFileChangeMessageStreamItem(itemId, turnId, changes, status),
|
||||
item: streamingFileChangeMessageStreamItem(itemId, turnId, normalizeFileChanges(changes), status),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import type { MessageStreamFileChange } from "../../../domain/message-stream/items";
|
||||
|
||||
export interface AppServerFileChange {
|
||||
readonly path: string;
|
||||
readonly kind: {
|
||||
readonly type: string;
|
||||
};
|
||||
readonly diff: string;
|
||||
}
|
||||
|
||||
export function normalizeFileChanges(changes: readonly AppServerFileChange[]): MessageStreamFileChange[] {
|
||||
return changes.map((change) => ({
|
||||
kind: change.kind.type,
|
||||
path: change.path,
|
||||
diff: change.diff,
|
||||
}));
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
import type { FileUpdateChange } from "../../../../../app-server/protocol/file-change";
|
||||
import type { MessageStreamItem } from "../../../domain/message-stream/items";
|
||||
import { normalizeFileChanges } from "./turn-items";
|
||||
import type { MessageStreamFileChange, MessageStreamItem } from "../../../domain/message-stream/items";
|
||||
|
||||
export function streamingFileChangeMessageStreamItem(
|
||||
itemId: string,
|
||||
turnId: string,
|
||||
changes: FileUpdateChange[],
|
||||
changes: readonly MessageStreamFileChange[],
|
||||
status: string,
|
||||
): MessageStreamItem {
|
||||
return {
|
||||
|
|
@ -16,6 +14,6 @@ export function streamingFileChangeMessageStreamItem(
|
|||
sourceItemId: itemId,
|
||||
provenance: { source: "appServer", channel: "notification", event: "streamingDelta", sourceItemId: itemId },
|
||||
status,
|
||||
changes: normalizeFileChanges(changes),
|
||||
changes,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import type {
|
|||
} from "../../../domain/message-stream/items";
|
||||
import type { MessageStreamItemProvenance } from "../../../domain/message-stream/provenance";
|
||||
import type { HistoricalTurn } from "../../../../../domain/threads/history";
|
||||
import type { FileUpdateChange } from "../../../../../app-server/protocol/file-change";
|
||||
import type { TurnItem } from "../../../../../app-server/protocol/turn";
|
||||
import { definedProp } from "../../../../../utils";
|
||||
import { referencedThreadMetadataFromPrompt, type ReferencedThreadMetadata } from "../../../../../domain/threads/reference";
|
||||
|
|
@ -18,6 +17,7 @@ import { fileMentionsFromInput } from "../../../domain/message-stream/format/fil
|
|||
import { normalizeProposedPlanMarkdown } from "../../../domain/message-stream/format/proposed-plan";
|
||||
import { userMessageDisplayText } from "../../../domain/message-stream/format/user-message-text";
|
||||
import { failedStatusLabel, jsonTargetLabel } from "../../../domain/message-stream/format/item-labels";
|
||||
import { normalizeFileChanges } from "./file-changes";
|
||||
|
||||
type UserMessageItem = Extract<TurnItem, { type: "userMessage" }>;
|
||||
type AgentMessageItem = Extract<TurnItem, { type: "agentMessage" }>;
|
||||
|
|
@ -108,11 +108,11 @@ const STANDARD_TOOL_STATES = {
|
|||
failed: "failed",
|
||||
} as const satisfies ExecutionStateByStatus;
|
||||
|
||||
export function messageStreamItemsFromTurns(turns: readonly HistoricalTurn<TurnItem>[]): MessageStreamItem[] {
|
||||
export function messageStreamItemsFromTurns(turns: readonly HistoricalTurn[]): MessageStreamItem[] {
|
||||
const sortedTurns = [...turns].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
|
||||
const items: MessageStreamItem[] = [];
|
||||
for (const turn of sortedTurns) {
|
||||
for (const item of turn.items) {
|
||||
for (const item of turn.items as readonly TurnItem[]) {
|
||||
const streamItem = messageStreamItemFromTurnItem(item, turn.id);
|
||||
if (streamItem) items.push(streamItem);
|
||||
}
|
||||
|
|
@ -650,14 +650,6 @@ function fileChangeMessageStreamItemFromData(data: FileChangeMessageStreamData,
|
|||
};
|
||||
}
|
||||
|
||||
export function normalizeFileChanges(changes: FileUpdateChange[]): MessageStreamFileChange[] {
|
||||
return changes.map((change) => ({
|
||||
kind: change.kind.type,
|
||||
path: change.path,
|
||||
diff: change.diff,
|
||||
}));
|
||||
}
|
||||
|
||||
export function shouldSuppressLifecycleItem(item: TurnItem): boolean {
|
||||
return item.type === "agentMessage" || item.type === "userMessage";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { AppServerClient } from "../../../../app-server/connection/client";
|
||||
import type { TurnItem } from "../../../../app-server/protocol/turn";
|
||||
import type { ThreadTurnsPage } from "../../../../domain/threads/history";
|
||||
import type { ChatAction, ChatState } from "../state/root-reducer";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
|
|
@ -56,7 +55,7 @@ export class HistoryController {
|
|||
}
|
||||
}
|
||||
|
||||
applyLatestPage(threadId: string, response: ThreadTurnsPage<TurnItem>): boolean {
|
||||
applyLatestPage(threadId: string, response: ThreadTurnsPage): boolean {
|
||||
if (this.state.activeThread.id !== threadId) return false;
|
||||
this.host.setThreadTurnPresence(response.data.length > 0);
|
||||
this.host.showLatestPageAtBottom();
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import {
|
|||
import { AppServerQueryCache } from "../../src/app-server/query/cache";
|
||||
import type { AppServerQueryContext } from "../../src/app-server/query/keys";
|
||||
import { emptyRuntimeConfigSnapshot, type RuntimeConfigSnapshot } from "../../src/domain/runtime/config";
|
||||
import type { RateLimitSnapshot } from "../../src/app-server/protocol/runtime-metrics";
|
||||
import type { SharedServerMetadata } from "../../src/app-server/query/snapshots";
|
||||
import type { RateLimitSnapshot } from "../../src/domain/runtime/metrics";
|
||||
import type { SharedServerMetadata } from "../../src/domain/server/metadata";
|
||||
import type { ModelMetadata, SkillMetadata } from "../../src/domain/catalog/metadata";
|
||||
import type { CatalogModel, CatalogSkillMetadata } from "../../src/app-server/protocol/catalog";
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from "../../../../../src/domain/server/diagnostics";
|
||||
import type { SharedServerMetadata } from "../../../../../src/domain/server/metadata";
|
||||
import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config";
|
||||
import type { RateLimitSnapshot } from "../../../../../src/app-server/protocol/runtime-metrics";
|
||||
import type { RateLimitSnapshot } from "../../../../../src/domain/runtime/metrics";
|
||||
import { threadFromThreadRecord } from "../../../../../src/app-server/protocol/thread";
|
||||
import { createChatServerDiagnosticsActions } from "../../../../../src/features/chat/app-server/actions/diagnostics";
|
||||
import { createChatServerMetadataActions } from "../../../../../src/features/chat/app-server/actions/metadata";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { AppServerClient } from "../../../../../src/app-server/connection/client";
|
||||
import type { CodexInput } from "../../../../../src/app-server/protocol/request-input";
|
||||
import type { CodexInput } from "../../../../../src/domain/chat/input";
|
||||
import type { TurnItem, TurnRecord } from "../../../../../src/app-server/protocol/turn";
|
||||
import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer";
|
||||
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { AppServerClient } from "../../../../../src/app-server/connection/client";
|
||||
import type { CodexInput } from "../../../../../src/app-server/protocol/request-input";
|
||||
import type { CodexInput } from "../../../../../src/domain/chat/input";
|
||||
import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer";
|
||||
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { createResumeController, type ResumeControllerHost } from "../../../../s
|
|||
import type { HistoryController } from "../../../../src/features/chat/application/threads/history-controller";
|
||||
import { ChatResumeWorkTracker } from "../../../../src/features/chat/application/lifecycle";
|
||||
import type { Thread as PanelThread } from "../../../../src/domain/threads/model";
|
||||
import type { ThreadTokenUsage } from "../../../../src/app-server/protocol/runtime-metrics";
|
||||
import type { ThreadTokenUsage } from "../../../../src/domain/runtime/metrics";
|
||||
import type { TurnItem, TurnRecord } from "../../../../src/app-server/protocol/turn";
|
||||
|
||||
type ThreadResumeResponse = Awaited<ReturnType<AppServerClient["resumeThread"]>>;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
|
|||
|
||||
const repoRoot = process.cwd();
|
||||
const ESLINT_STARTUP_TEST_TIMEOUT_MS = 10_000;
|
||||
const generatedAppServerImportRoot = "../../generated" + "/app-server";
|
||||
|
||||
describe("eslint config", () => {
|
||||
it(
|
||||
|
|
@ -131,6 +132,100 @@ export type Turn = TurnRecord;
|
|||
expect(ingestionMessages).not.toContain("no-restricted-imports");
|
||||
});
|
||||
|
||||
it("reports turn protocol imports outside chat app-server ingestion and conversion boundaries", async () => {
|
||||
const messages = await lintSource(
|
||||
"src/features/chat/application/threads/history-controller.ts",
|
||||
`
|
||||
import type { TurnItem } from "../../../../app-server/protocol/turn";
|
||||
|
||||
export type Item = TurnItem;
|
||||
`,
|
||||
);
|
||||
|
||||
expect(messages).toContain("no-restricted-imports");
|
||||
});
|
||||
|
||||
it("reports non-turn protocol imports at chat app-server ingestion and conversion boundaries", async () => {
|
||||
const messages = await lintSource(
|
||||
"src/features/chat/app-server/inbound/notification-plan.ts",
|
||||
`
|
||||
import type { FileUpdateChange } from "../../../../app-server/protocol/file-change";
|
||||
|
||||
export type Change = FileUpdateChange;
|
||||
`,
|
||||
);
|
||||
|
||||
expect(messages).toContain("no-restricted-imports");
|
||||
});
|
||||
|
||||
it("keeps generated app-server bindings out of non-turn protocol modules", async () => {
|
||||
const messages = await lintSource(
|
||||
"src/app-server/protocol/request-input.ts",
|
||||
`
|
||||
import type { UserInput } from "${generatedAppServerImportRoot}/v2/UserInput";
|
||||
|
||||
export type Input = UserInput;
|
||||
`,
|
||||
);
|
||||
|
||||
expect(messages).toContain("no-restricted-imports");
|
||||
});
|
||||
|
||||
it("keeps app-server protocol adapters independent from the connection layer", async () => {
|
||||
const messages = await lintSource(
|
||||
"src/app-server/protocol/catalog.ts",
|
||||
`
|
||||
import type { AppServerClient } from "../connection/client";
|
||||
|
||||
export type Client = AppServerClient;
|
||||
`,
|
||||
);
|
||||
|
||||
expect(messages).toContain("no-restricted-imports");
|
||||
});
|
||||
|
||||
it("keeps generated app-server bindings out of app-server service seams", async () => {
|
||||
const messages = await lintSource(
|
||||
"src/app-server/services/runtime-overrides.ts",
|
||||
`
|
||||
import type { ModelListResponse } from "${generatedAppServerImportRoot}/v2/ModelListResponse";
|
||||
|
||||
export interface RuntimeOverrideModelClient {
|
||||
listModels(includeHidden: boolean): Promise<ModelListResponse>;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
expect(messages).toContain("no-restricted-imports");
|
||||
});
|
||||
|
||||
it("allows generated app-server bindings in the explicit turn protocol exception", async () => {
|
||||
const messages = await lintSource(
|
||||
"src/app-server/protocol/turn.ts",
|
||||
`
|
||||
import type { ThreadItem as GeneratedTurnItem } from "${generatedAppServerImportRoot}/v2/ThreadItem";
|
||||
|
||||
export type TurnItem = GeneratedTurnItem;
|
||||
`,
|
||||
);
|
||||
|
||||
expect(messages).not.toContain("no-restricted-imports");
|
||||
expect(messages).not.toContain("no-restricted-syntax");
|
||||
});
|
||||
|
||||
it("reports non-ThreadItem generated app-server imports in the turn protocol exception", async () => {
|
||||
const messages = await lintSource(
|
||||
"src/app-server/protocol/turn.ts",
|
||||
`
|
||||
import type { UserInput } from "${generatedAppServerImportRoot}/v2/UserInput";
|
||||
|
||||
export type Input = UserInput;
|
||||
`,
|
||||
);
|
||||
|
||||
expect(messages).toContain("no-restricted-syntax");
|
||||
});
|
||||
|
||||
it("reports direct ChatState alias mutation", async () => {
|
||||
const messages = await lintSource(
|
||||
"src/features/chat/domain/runtime/effective.ts",
|
||||
|
|
|
|||
Loading…
Reference in a new issue