Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
8 changes: 4 additions & 4 deletions apps/server/src/codexAppServerManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import path from "node:path";
import { ApprovalRequestId, ThreadId } from "@t3tools/contracts";

import {
buildCodexInitializeParams,
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
CodexAppServerManager,
classifyCodexStderrLine,
isRecoverableThreadResumeError,
normalizeCodexModelSlug,
} from "./codexAppServerManager";
import {
buildCodexInitializeParams,
readCodexAccountSnapshot,
resolveCodexModelForAccount,
} from "./codexAppServerManager";
} from "./provider/codexAccount";

const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value);

Expand Down Expand Up @@ -250,7 +252,6 @@ describe("readCodexAccountSnapshot", () => {
expect(
readCodexAccountSnapshot({
type: "chatgpt",
email: "plus@example.com",
planType: "plus",
}),
).toEqual({
Expand All @@ -264,7 +265,6 @@ describe("readCodexAccountSnapshot", () => {
expect(
readCodexAccountSnapshot({
type: "chatgpt",
email: "pro@example.com",
planType: "pro",
}),
).toEqual({
Expand Down
91 changes: 6 additions & 85 deletions apps/server/src/codexAppServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ import {
isCodexCliVersionSupported,
parseCodexCliVersion,
} from "./provider/codexCliVersion";
import {
buildCodexInitializeParams,
readCodexAccountSnapshot,
resolveCodexModelForAccount,
type CodexAccountSnapshot,
} from "./provider/codexAccount";

type PendingRequestKey = string;

Expand Down Expand Up @@ -97,23 +103,6 @@ interface JsonRpcNotification {
params?: unknown;
}

type CodexPlanType =
| "free"
| "go"
| "plus"
| "pro"
| "team"
| "business"
| "enterprise"
| "edu"
| "unknown";

interface CodexAccountSnapshot {
readonly type: "apiKey" | "chatgpt" | "unknown";
readonly planType: CodexPlanType | null;
readonly sparkEnabled: boolean;
}

export interface CodexAppServerSendTurnInput {
readonly threadId: ThreadId;
readonly input?: string;
Expand Down Expand Up @@ -162,50 +151,6 @@ const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [
"unknown thread",
"does not exist",
];
const CODEX_DEFAULT_MODEL = "gpt-5.3-codex";
const CODEX_SPARK_MODEL = "gpt-5.3-codex-spark";
const CODEX_SPARK_DISABLED_PLAN_TYPES = new Set<CodexPlanType>(["free", "go", "plus"]);

function asObject(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== "object") {
return undefined;
}
return value as Record<string, unknown>;
}

function asString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}

export function readCodexAccountSnapshot(response: unknown): CodexAccountSnapshot {
const record = asObject(response);
const account = asObject(record?.account) ?? record;
const accountType = asString(account?.type);

if (accountType === "apiKey") {
return {
type: "apiKey",
planType: null,
sparkEnabled: true,
};
}

if (accountType === "chatgpt") {
const planType = (account?.planType as CodexPlanType | null) ?? "unknown";
return {
type: "chatgpt",
planType,
sparkEnabled: !CODEX_SPARK_DISABLED_PLAN_TYPES.has(planType),
};
}

return {
type: "unknown",
planType: null,
sparkEnabled: true,
};
}

export const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Plan Mode (Conversational)

You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions.
Expand Down Expand Up @@ -358,17 +303,6 @@ function mapCodexRuntimeMode(runtimeMode: RuntimeMode): {
};
}

export function resolveCodexModelForAccount(
model: string | undefined,
account: CodexAccountSnapshot,
): string | undefined {
if (model !== CODEX_SPARK_MODEL || account.sparkEnabled) {
return model;
}

return CODEX_DEFAULT_MODEL;
}

/**
* On Windows with `shell: true`, `child.kill()` only terminates the `cmd.exe`
* wrapper, leaving the actual command running. Use `taskkill /T` to kill the
Expand Down Expand Up @@ -402,19 +336,6 @@ export function normalizeCodexModelSlug(
return normalized;
}

export function buildCodexInitializeParams() {
return {
clientInfo: {
name: "t3code_desktop",
title: "T3 Code Desktop",
version: "0.1.0",
},
capabilities: {
experimentalApi: true,
},
} as const;
}

function buildCodexCollaborationMode(input: {
readonly interactionMode?: "default" | "plan";
readonly model?: string;
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/git/Layers/CodexTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function makeFakeCodexBinary(dir: string) {
' printf "%s\\n" "$T3_FAKE_CODEX_STDERR" >&2',
"fi",
'if [ -n "$output_path" ]; then',
' node -e \'const fs=require("node:fs"); const value=process.argv[2] ?? ""; fs.writeFileSync(process.argv[1], Buffer.from(value, "base64"));\' "$output_path" "${T3_FAKE_CODEX_OUTPUT_B64:-e30=}"',
' bun -e \'const fs=require("node:fs"); const value=process.argv[2] ?? ""; fs.writeFileSync(process.argv[1], Buffer.from(value, "base64"));\' "$output_path" "${T3_FAKE_CODEX_OUTPUT_B64:-e30=}"',
"fi",
'exit "${T3_FAKE_CODEX_EXIT_CODE:-0}"',
"",
Expand Down
43 changes: 24 additions & 19 deletions apps/server/src/persistence/NodeSqliteClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,34 @@ import { assert, it } from "@effect/vitest";
import { Effect } from "effect";
import * as SqlClient from "effect/unstable/sql/SqlClient";

import * as SqliteClient from "./NodeSqliteClient.ts";
const supportsNodeSqlite = typeof process.versions.bun !== "string";

const layer = it.layer(SqliteClient.layerMemory());
if (!supportsNodeSqlite) {
it.skip("NodeSqliteClient requires node:sqlite support", () => {});
} else {
const SqliteClient = await import("./NodeSqliteClient.ts");
const layer = it.layer(SqliteClient.layerMemory());

layer("NodeSqliteClient", (it) => {
it.effect("runs prepared queries and returns positional values", () =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
layer("NodeSqliteClient", (it) => {
it.effect("runs prepared queries and returns positional values", () =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;

yield* sql`CREATE TABLE entries(id INTEGER PRIMARY KEY, name TEXT NOT NULL)`;
yield* sql`INSERT INTO entries(name) VALUES (${"alpha"}), (${"beta"})`;
yield* sql`CREATE TABLE entries(id INTEGER PRIMARY KEY, name TEXT NOT NULL)`;
yield* sql`INSERT INTO entries(name) VALUES (${"alpha"}), (${"beta"})`;

const rows = yield* sql<{ readonly id: number; readonly name: string }>`
const rows = yield* sql<{ readonly id: number; readonly name: string }>`
SELECT id, name FROM entries ORDER BY id
`;
assert.equal(rows.length, 2);
assert.equal(rows[0]?.name, "alpha");
assert.equal(rows[1]?.name, "beta");
assert.equal(rows.length, 2);
assert.equal(rows[0]?.name, "alpha");
assert.equal(rows[1]?.name, "beta");

const values = yield* sql`SELECT id, name FROM entries ORDER BY id`.values;
assert.equal(values.length, 2);
assert.equal(values[0]?.[1], "alpha");
assert.equal(values[1]?.[1], "beta");
}),
);
});
const values = yield* sql`SELECT id, name FROM entries ORDER BY id`.values;
assert.equal(values.length, 2);
assert.equal(values[0]?.[1], "alpha");
assert.equal(values[1]?.[1], "beta");
}),
);
});
}
41 changes: 39 additions & 2 deletions apps/server/src/provider/Layers/ProviderHealth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
parseClaudeAuthStatusFromOutput,
readCodexConfigModelProvider,
} from "./ProviderHealth";
import { extractCodexAccountPlan } from "../codexAccount";

// ── Test helpers ────────────────────────────────────────────────────

Expand Down Expand Up @@ -112,12 +113,19 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => {
assert.strictEqual(status.status, "ready");
assert.strictEqual(status.available, true);
assert.strictEqual(status.authStatus, "authenticated");
assert.strictEqual(status.plan, "pro");
}).pipe(
Effect.provide(
mockSpawnerLayer((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 };
if (joined === "login status") {
return {
stdout: '{"authenticated":true,"account":{"planType":"pro"}}\n',
stderr: "",
code: 0,
};
}
throw new Error(`Unexpected args: ${joined}`);
}),
),
Expand Down Expand Up @@ -332,6 +340,17 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => {
assert.strictEqual(parsed.authStatus, "unauthenticated");
});

it("extracts planType from JSON output", () => {
const parsed = parseAuthStatusFromOutput({
stdout: '{"authenticated":true,"account":{"planType":"team"}}\n',
stderr: "",
code: 0,
});
assert.strictEqual(parsed.status, "ready");
assert.strictEqual(parsed.authStatus, "authenticated");
assert.strictEqual(parsed.plan, "team");
});

it("JSON without auth marker is warning", () => {
const parsed = parseAuthStatusFromOutput({
stdout: '[{"ok":true}]\n',
Expand All @@ -343,6 +362,22 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => {
});
});

describe("extractCodexAccountPlan", () => {
it("extracts planType from account/read responses", () => {
assert.strictEqual(
extractCodexAccountPlan({
result: {
account: {
type: "chatgpt",
planType: "pro",
},
},
}),
"pro",
);
});
});

// ── readCodexConfigModelProvider tests ─────────────────────────────

describe("readCodexConfigModelProvider", () => {
Expand Down Expand Up @@ -609,12 +644,14 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => {

it("JSON with loggedIn=true is authenticated", () => {
const parsed = parseClaudeAuthStatusFromOutput({
stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
stdout:
'{"loggedIn":true,"authMethod":"claude.ai","email":"claude@example.com","subscriptionType":"pro"}\n',
stderr: "",
code: 0,
});
assert.strictEqual(parsed.status, "ready");
assert.strictEqual(parsed.authStatus, "authenticated");
assert.strictEqual(parsed.plan, "pro");
});

it("JSON with loggedIn=false is unauthenticated", () => {
Expand Down
Loading
Loading