Merge remote-tracking branch 'origin/dev' into codex/fix-cliproxy-v3-snapshot-migration-issue

# Conflicts:
#	src/web-server/usage/cliproxy-usage-transformer.ts
#	tests/unit/web-server/cliproxy-usage-transformer.test.ts
This commit is contained in:
Tam Nhu Tran committed 2026-05-30 16:13:49 -04:00
commit ced9317565
210 files changed
+5627 -1065

No files matched your search

+3 -2
View File
@@ -106,8 +106,9 @@ assert_case "skips at the larger configured window boundary" 0 false 3 \
assert_failure "rejects prerelease target tags" \
--target "v7.80.0-rc.1" --baseline "7.80.0" --window "2"
assert_failure "fails loudly when baseline tag is unavailable after baseline" \
--target "v7.81.2" --baseline "7.81.0" --window "2"
assert_case "skips deprecated publish when baseline tag is unavailable after baseline" 0 false 2 \
"v7.81.2" "7.81.0" "2" \
$'v7.80.0'
echo ""
echo "Dashboard sunset guard tests complete: ${PASS} passed, ${FAIL} failed."
+52
View File
@@ -159,6 +159,54 @@ MOCK_EOF
chmod +x "${MOCK_DIR}/docker"
}
make_mock_docker_platform_raw_index() {
# First inspect returns a multi-arch index; digest inspect returns the
# platform manifest with real layer sizes. This mirrors GHCR OCI output.
cat > "${MOCK_DIR}/docker" <<'MOCK_EOF'
#!/usr/bin/env bash
if [[ "$1" == "buildx" && "$2" == "imagetools" && "$3" == "inspect" ]]; then
ref="$4"
if [[ "$ref" == "mock-image:tag" ]]; then
cat <<'JSON'
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:amd64digest",
"platform": { "os": "linux", "architecture": "amd64" }
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:arm64digest",
"platform": { "os": "linux", "architecture": "arm64" }
}
]
}
JSON
exit 0
fi
if [[ "$ref" == "mock-image:tag@sha256:amd64digest" ]]; then
cat <<'JSON'
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"layers": [
{ "size": 100000000 },
{ "size": 57671680 }
]
}
JSON
exit 0
fi
exit 1
fi
exit 0
MOCK_EOF
chmod +x "${MOCK_DIR}/docker"
}
run_platform_test() {
local name="$1"
local expected_exit="$2"
@@ -190,6 +238,10 @@ run_platform_test "--platform: pass when platform-scoped size < budget" 0 "20971
make_mock_docker_platform "150000000 112000000"
run_platform_test "--platform: fail when platform-scoped size > budget" 1 "209715200"
# --platform raw OCI index: resolve the platform digest then sum manifest layers
make_mock_docker_platform_raw_index
run_platform_test "--platform: resolves raw OCI index before summing layers" 0 "209715200"
# --platform inspect failure → must exit 1 (REV5 regression guard)
make_mock_docker_platform_fail
run_platform_test "--platform: fail loudly when imagetools inspect fails (REV5 guard)" 1 "209715200"
+72 -12
View File
@@ -50,27 +50,87 @@ fi
MAX_MB=$(( MAX_BYTES / 1048576 ))
sum_manifest_layer_sizes() {
local sum
sum="$(jq -sr '
.[0] as $manifest
| if ($manifest | type) == "object" and ($manifest.layers | type) == "array" then
([$manifest.layers[]?.size | numbers] | add) // 0
else
0
end
' 2>/dev/null)" || {
echo "0"
return
}
echo "${sum:-0}"
}
select_platform_digest() {
local os="$1"
local arch="$2"
local variant="$3"
jq -r \
--arg os "$os" \
--arg arch "$arch" \
--arg variant "$variant" \
'
.manifests[]?
| select((.platform.os // "") == $os)
| select((.platform.architecture // "") == $arch)
| select($variant == "" or (.platform.variant // "") == $variant)
| .digest
' 2>/dev/null | head -1 || true
}
if [[ -n "$PLATFORM" ]]; then
# Multi-arch path: sum compressed layer sizes from the registry manifest.
# `docker buildx imagetools inspect --format` returns the manifest JSON for
# the given platform. We sum the `size` field of each layer entry.
# Read raw OCI/Docker manifests directly. Multi-arch tags first resolve the
# requested platform digest from the index, then sum that manifest's layer
# sizes. A single-platform tag can be summed immediately from its raw manifest.
if ! command -v jq >/dev/null 2>&1; then
echo "[X] jq is required for platform-scoped image size inspection" >&2
exit 1
fi
if [[ "$PLATFORM" != */* ]]; then
echo "[X] --platform must use os/arch format, got: ${PLATFORM}" >&2
exit 1
fi
PLATFORM_OS="${PLATFORM%%/*}"
PLATFORM_REST="${PLATFORM#*/}"
PLATFORM_ARCH="${PLATFORM_REST%%/*}"
PLATFORM_VARIANT=""
if [[ "$PLATFORM_REST" == */* ]]; then
PLATFORM_VARIANT="${PLATFORM_REST#*/}"
fi
echo "[i] Inspecting ${IMAGE} for platform ${PLATFORM} via registry manifest..." >&2
ACTUAL_BYTES=$(
docker buildx imagetools inspect "${IMAGE}" \
--format "{{ range .Manifest.Layers }}{{ .Size }} {{ end }}" \
--raw 2>/dev/null \
| tr ' ' '\n' \
| awk 'NF && /^[0-9]+$/ { sum += $1 } END { print sum+0 }' \
2>/dev/null || echo ""
)
RAW_MANIFEST="$(docker buildx imagetools inspect "${IMAGE}" --raw 2>/dev/null || true)"
ACTUAL_BYTES="$(printf '%s' "$RAW_MANIFEST" | sum_manifest_layer_sizes)"
if [[ -z "$ACTUAL_BYTES" || "$ACTUAL_BYTES" == "0" ]]; then
# Fallback: try the platform-specific sub-manifest
PLATFORM_DIGEST="$(printf '%s' "$RAW_MANIFEST" | select_platform_digest "$PLATFORM_OS" "$PLATFORM_ARCH" "$PLATFORM_VARIANT")"
if [[ -n "$PLATFORM_DIGEST" && "$PLATFORM_DIGEST" != "null" ]]; then
# Fallback: inspect the selected platform sub-manifest.
echo "[i] Falling back to platform-scoped manifest ${PLATFORM_DIGEST}..." >&2
RAW_PLATFORM_MANIFEST="$(docker buildx imagetools inspect "${IMAGE}@${PLATFORM_DIGEST}" --raw 2>/dev/null || true)"
ACTUAL_BYTES="$(printf '%s' "$RAW_PLATFORM_MANIFEST" | sum_manifest_layer_sizes)"
else
ACTUAL_BYTES="0"
fi
fi
if [[ -z "$ACTUAL_BYTES" || "$ACTUAL_BYTES" == "0" ]]; then
# Last resort: support older buildx versions that expose layer sizes only
# through the templated manifest object.
echo "[i] Falling back to platform-scoped imagetools inspect..." >&2
ACTUAL_BYTES=$(
docker buildx imagetools inspect "${IMAGE}@$(
docker buildx imagetools inspect "${IMAGE}" \
--format "{{ range .Manifest.Manifests }}{{ if eq .Platform.OS \"$(echo "${PLATFORM}" | cut -d/ -f1)\" }}{{ if eq .Platform.Architecture \"$(echo "${PLATFORM}" | cut -d/ -f2)\" }}{{ .Digest }}{{ end }}{{ end }}{{ end }}" \
--format "{{ range .Manifest.Manifests }}{{ if eq .Platform.OS \"${PLATFORM_OS}\" }}{{ if eq .Platform.Architecture \"${PLATFORM_ARCH}\" }}{{ .Digest }}{{ end }}{{ end }}{{ end }}" \
2>/dev/null | head -1
)" --format "{{ range .Manifest.Layers }}{{ .Size }} {{ end }}" 2>/dev/null \
| tr ' ' '\n' \
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Unit test for network-contract.sh Docker Compose env and args.
# Uses a fake docker binary; no Docker daemon required.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT="${SCRIPT_DIR}/network-contract.sh"
PASS=0
FAIL=0
MOCK_DIR="$(mktemp -d)"
LOG_FILE="${MOCK_DIR}/docker.log"
COMPOSE_FILE="${MOCK_DIR}/compose.yaml"
trap 'rm -rf "$MOCK_DIR"' EXIT
cat > "$COMPOSE_FILE" <<'YAML'
services:
ccs:
image: ${CCS_IMAGE:-ghcr.io/kaitranntt/ccs:latest}
YAML
cat > "${MOCK_DIR}/docker" <<'MOCK'
#!/usr/bin/env bash
set -euo pipefail
printf 'cmd=%s env_image=%s env_dashboard=%s env_cliproxy=%s args=%s\n' \
"$1" "${CCS_IMAGE:-}" "${CCS_DASHBOARD_PORT:-}" "${CCS_CLIPROXY_PORT:-}" "$*" \
>> "$DOCKER_MOCK_LOG"
if [[ "$1" == "compose" ]]; then
if printf '%s\n' "$*" | grep -q 'ps --format json'; then
printf '[{"Service":"ccs","Health":"healthy"}]\n'
fi
exit 0
fi
if [[ "$1" == "network" && "$2" == "inspect" && "$3" == "ccs-net" ]]; then
exit 0
fi
if [[ "$1" == "run" ]]; then
exit 0
fi
exit 1
MOCK
chmod +x "${MOCK_DIR}/docker"
run_test() {
local name="$1"
shift
if "$@"; then
echo "[OK] ${name}"
(( PASS++ )) || true
else
echo "[X] ${name}"
(( FAIL++ )) || true
fi
}
run_contract() {
PATH="${MOCK_DIR}:${PATH}" \
DOCKER_MOCK_LOG="$LOG_FILE" \
bash "$SCRIPT" "$COMPOSE_FILE" "ghcr.io/kaitranntt/ccs:test" >/dev/null
}
echo ""
echo "Running network-contract.sh env tests..."
echo ""
run_contract
run_test "compose up receives safe host ports and image override" \
grep -q 'env_image=ghcr.io/kaitranntt/ccs:test env_dashboard=13001 env_cliproxy=18318 args=compose -f .* up -d --remove-orphans' "$LOG_FILE"
run_test "compose ps receives the same safe host ports" \
grep -q 'env_image=ghcr.io/kaitranntt/ccs:test env_dashboard=13001 env_cliproxy=18318 args=compose -f .* ps --format json' "$LOG_FILE"
run_test "compose down removes volumes and orphans through cleanup trap" \
grep -q 'env_image=ghcr.io/kaitranntt/ccs:test env_dashboard=13001 env_cliproxy=18318 args=compose -f .* down -v --remove-orphans' "$LOG_FILE"
run_test "sibling probes keep the ccs-net DNS contract" \
grep -q 'args=run --rm --network ccs-net curlimages/curl:latest -fsS --max-time 10 http://ccs:8317/' "$LOG_FILE"
run_test "dashboard sibling probe keeps internal port 3000" \
grep -q 'args=run --rm --network ccs-net curlimages/curl:latest -fsS --max-time 10 http://ccs:3000/' "$LOG_FILE"
echo ""
echo "Results: ${PASS} passed, ${FAIL} failed"
echo ""
if [[ "$FAIL" -gt 0 ]]; then
exit 1
fi
+25 -10
View File
@@ -19,6 +19,8 @@ set -euo pipefail
COMPOSE_FILE="${1:-docker/compose.yaml}"
IMAGE_OVERRIDE="${2:-}"
DASHBOARD_HOST_PORT="${CCS_NETWORK_CONTRACT_DASHBOARD_PORT:-${CCS_DASHBOARD_PORT:-13001}}"
CLIPROXY_HOST_PORT="${CCS_NETWORK_CONTRACT_CLIPROXY_PORT:-${CCS_CLIPROXY_PORT:-18318}}"
# ---------------------------------------------------------------------------
# Helpers
@@ -27,22 +29,35 @@ log() { printf '[i] %s\n' "$*"; }
ok() { printf '[OK] %s\n' "$*"; }
err() { printf '[X] %s\n' "$*" >&2; }
compose() {
if [[ -n "$IMAGE_OVERRIDE" ]]; then
CCS_IMAGE="$IMAGE_OVERRIDE" \
CCS_DASHBOARD_PORT="$DASHBOARD_HOST_PORT" \
CCS_CLIPROXY_PORT="$CLIPROXY_HOST_PORT" \
docker compose -f "$COMPOSE_FILE" "$@"
return
fi
CCS_DASHBOARD_PORT="$DASHBOARD_HOST_PORT" \
CCS_CLIPROXY_PORT="$CLIPROXY_HOST_PORT" \
docker compose -f "$COMPOSE_FILE" "$@"
}
cleanup() {
log "Tearing down stack..."
compose down -v --remove-orphans 2>/dev/null || true
}
trap cleanup EXIT
# ---------------------------------------------------------------------------
# Bring stack up; register teardown on any exit
# ---------------------------------------------------------------------------
log "Bringing CCS stack up: $COMPOSE_FILE"
log "Using host ports: dashboard=${DASHBOARD_HOST_PORT}, cliproxy=${CLIPROXY_HOST_PORT}"
if [[ -n "$IMAGE_OVERRIDE" ]]; then
log "Overriding image with: $IMAGE_OVERRIDE"
CCS_IMAGE="$IMAGE_OVERRIDE" docker compose -f "$COMPOSE_FILE" up -d
else
docker compose -f "$COMPOSE_FILE" up -d
fi
cleanup() {
log "Tearing down stack..."
docker compose -f "$COMPOSE_FILE" down -v 2>/dev/null || true
}
trap cleanup EXIT
compose up -d --remove-orphans
# ---------------------------------------------------------------------------
# Wait for healthcheck (max 90s) — use jq instead of python3 for CI portability
@@ -52,7 +67,7 @@ WAIT_MAX=45 # 45 x 2s = 90s
HEALTHY=0
for _i in $(seq 1 "$WAIT_MAX"); do
STATUS=$(
docker compose -f "$COMPOSE_FILE" ps --format json 2>/dev/null \
compose ps --format json 2>/dev/null \
| jq -r 'if type == "array" then .[] else . end | select(.Service != null and (.Service | contains("ccs"))) | .Health // "unknown"' \
2>/dev/null | head -1 || echo "unknown"
)
@@ -31,13 +31,51 @@ afterEach(async () => {
});
describe('cursor daemon lifecycle smoke', () => {
it('requires Anthropic caller auth token when credentials are present', async () => {
const port = 10000 + Math.floor(Math.random() * 50000);
saveCredentials({
accessToken: 'a'.repeat(60),
machineId: '1234567890abcdef1234567890abcdef',
authMethod: 'manual',
importedAt: new Date().toISOString(),
});
const result = await startDaemon({ port, ghost_mode: true });
expect(result.success).toBe(true);
const daemonToken = result.daemonToken as string;
const response = await fetch(`http://127.0.0.1:${port}/v1/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
'x-ccs-cursor-token': daemonToken,
},
body: JSON.stringify({
model: 'claude-sonnet-4.5',
max_tokens: 64,
messages: [{ role: 'user', content: 'hello' }],
}),
});
expect(response.status).toBe(401);
const body = (await response.json()) as {
type?: string;
error?: { type?: string; message?: string };
};
expect(body.type).toBe('error');
expect(body.error?.type).toBe('authentication_error');
expect(body.error?.message).toContain('Invalid Anthropic auth token');
});
it('starts, serves expected routes, and stops cleanly', async () => {
const port = 10000 + Math.floor(Math.random() * 50000);
const result = await startDaemon({ port, ghost_mode: true });
expect(result.success).toBe(true);
expect(result.pid).toBeDefined();
const daemonToken = result.daemonToken as string;
expect(await isDaemonRunning(port)).toBe(true);
expect(await isDaemonRunning(port, daemonToken)).toBe(true);
const modelsResponse = await fetch(`http://127.0.0.1:${port}/v1/models`);
expect(modelsResponse.status).toBe(200);
@@ -47,7 +85,7 @@ describe('cursor daemon lifecycle smoke', () => {
const chatResponse = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'x-ccs-cursor-token': daemonToken },
body: JSON.stringify({
model: 'gpt-4.1',
messages: [{ role: 'user', content: 'hello' }],
@@ -60,6 +98,7 @@ describe('cursor daemon lifecycle smoke', () => {
headers: {
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
'x-ccs-cursor-token': daemonToken,
},
body: JSON.stringify({
model: 'claude-sonnet-4.5',
@@ -78,15 +117,18 @@ describe('cursor daemon lifecycle smoke', () => {
const stopResult = await stopDaemon();
expect(stopResult.success).toBe(true);
expect(await isDaemonRunning(port)).toBe(false);
expect(await isDaemonRunning(port, daemonToken)).toBe(false);
}, 35000);
it('returns 404 for unknown routes', async () => {
const port = 10000 + Math.floor(Math.random() * 50000);
const result = await startDaemon({ port, ghost_mode: true });
expect(result.success).toBe(true);
const daemonToken = result.daemonToken as string;
const response = await fetch(`http://127.0.0.1:${port}/unknown`);
const response = await fetch(`http://127.0.0.1:${port}/unknown`, {
headers: { 'x-ccs-cursor-token': daemonToken },
});
expect(response.status).toBe(404);
});
@@ -103,10 +145,11 @@ describe('cursor daemon lifecycle smoke', () => {
const result = await startDaemon({ port, ghost_mode: true });
expect(result.success).toBe(true);
const daemonToken = result.daemonToken as string;
const response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'x-ccs-cursor-token': daemonToken },
body: JSON.stringify({
model: 'gpt-4.1',
messages: [{ role: 'user', content: 'hello' }],
@@ -122,17 +165,19 @@ describe('cursor daemon lifecycle smoke', () => {
const port = 10000 + Math.floor(Math.random() * 50000);
const result = await startDaemon({ port, ghost_mode: true });
expect(result.success).toBe(true);
const daemonToken = result.daemonToken as string;
const tokenHeader = { 'x-ccs-cursor-token': daemonToken };
const invalidJson = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', ...tokenHeader },
body: '{invalid-json',
});
expect(invalidJson.status).toBe(400);
const invalidSchema = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', ...tokenHeader },
body: JSON.stringify({
model: 'gpt-4.1',
messages: { role: 'user', content: 'hello' },
@@ -145,6 +190,7 @@ describe('cursor daemon lifecycle smoke', () => {
headers: {
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
...tokenHeader,
},
body: JSON.stringify({
model: 'claude-sonnet-4.5',
@@ -163,7 +209,7 @@ describe('cursor daemon lifecycle smoke', () => {
const oversized = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', ...tokenHeader },
body: JSON.stringify({
model: 'gpt-4.1',
messages: [
@@ -6,6 +6,7 @@ import * as path from 'path';
import getPort from 'get-port';
import {
getOpenAICompatProxyStatus,
isOpenAICompatProxyRunning,
listOpenAICompatProxyStatuses,
startOpenAICompatProxy,
stopOpenAICompatProxy,
@@ -919,6 +920,62 @@ describe('openai proxy daemon lifecycle', () => {
expect(secondStart.authToken).not.toBe('stale-token-a');
});
it('stops pid-only profile daemons instead of only deleting their state', async () => {
const port = await getPort();
const settingsPath = path.join(tempDir, 'pid-only-stop.settings.json');
fs.writeFileSync(
settingsPath,
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'https://api.openai.com/v1',
ANTHROPIC_AUTH_TOKEN: 'sk-pid-only-stop',
ANTHROPIC_MODEL: 'gpt-4.1',
},
}),
'utf8'
);
const profile = resolveOpenAICompatProfileConfig('pid-only-stop', settingsPath, {
ANTHROPIC_BASE_URL: 'https://api.openai.com/v1',
ANTHROPIC_AUTH_TOKEN: 'sk-pid-only-stop',
ANTHROPIC_MODEL: 'gpt-4.1',
});
if (!profile) {
throw new Error('Expected pid-only-stop OpenAI-compatible profile');
}
const started = await startOpenAICompatProxy(profile, { port });
expect(started.success).toBe(true);
expect(started.pid).toBeDefined();
const pid = started.pid;
if (!pid) {
throw new Error('Expected pid-only-stop daemon pid');
}
try {
fs.unlinkSync(getOpenAICompatProxySessionPath('pid-only-stop'));
const statuses = await listOpenAICompatProxyStatuses();
expect(statuses).toContainEqual({
running: false,
profileName: 'pid-only-stop',
pid,
});
const stopped = await stopOpenAICompatProxy();
expect(stopped.success).toBe(true);
expect(fs.existsSync(getOpenAICompatProxyPidPath('pid-only-stop'))).toBe(false);
expect(await isOpenAICompatProxyRunning(port, 'pid-only-stop')).toBe(false);
} finally {
try {
process.kill(pid, 'SIGKILL');
} catch {
// Already stopped.
}
}
}, 35000);
it('replaces pid-only proxy state before starting a new daemon', async () => {
const firstPort = await getPort();
const replacementPort = await getPort();
@@ -206,9 +206,8 @@ describe('GET /api/codex/profiles', () => {
// 127.0.0.1 and the test client connects to 127.0.0.1, the built-in fetch
// will always be loopback. We test the guard directly via a separate
// Express app that injects a non-loopback remote address.
const { requireLocalAccessWhenAuthDisabled } = await import(
'../../../src/web-server/middleware/auth-middleware'
);
const { requireLocalAccessWhenAuthDisabled } =
await import('../../../src/web-server/middleware/auth-middleware');
const { isDashboardAuthEnabled } = await import('../../../src/config/config-loader-facade');
if (!isDashboardAuthEnabled()) {
@@ -245,6 +244,43 @@ describe('GET /api/codex/profiles', () => {
}
});
it('returns 403 for loopback remote addresses when host/origin indicate non-local origin', async () => {
const { requireLocalAccessWhenAuthDisabled } =
await import('../../../src/web-server/middleware/auth-middleware');
const { isDashboardAuthEnabled } = await import('../../../src/config/config-loader-facade');
if (!isDashboardAuthEnabled()) {
let guardResult: boolean | undefined;
const testApp = express();
testApp.get('/test', (req, res) => {
Object.defineProperty(req, 'socket', {
value: { remoteAddress: '127.0.0.1' },
writable: true,
configurable: true,
});
req.headers.host = 'attacker.example.test';
req.headers.origin = 'http://attacker.example.test';
guardResult = requireLocalAccessWhenAuthDisabled(req, res, 'localhost only');
if (guardResult) {
res.json({ ok: true });
}
});
const testServer = await new Promise<http.Server>((resolve) => {
const s = testApp.listen(0, '127.0.0.1', () => resolve(s));
});
const testPort = (testServer.address() as { port: number }).port;
const res = await fetch(`http://127.0.0.1:${testPort}/test`);
await new Promise<void>((resolve) => testServer.close(() => resolve()));
expect(res.status).toBe(403);
expect(guardResult).toBe(false);
}
});
it('response body contains no token substrings for a valid profile', async () => {
const instancesDir = path.join(ccsDir, 'codex-instances');
const workDir = path.join(instancesDir, 'work');
+6 -6
View File
@@ -56,7 +56,7 @@ describe('profile reader target sanitization', () => {
}
});
it('normalizes legacy stored codex targets back to claude for profiles and variants', async () => {
it('normalizes legacy invalid stored targets back to claude for profiles and variants', async () => {
const ccsDir = getScopedCcsDir();
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
@@ -64,12 +64,12 @@ describe('profile reader target sanitization', () => {
JSON.stringify(
{
profiles: { demo: '~/.ccs/demo.settings.json' },
profile_targets: { demo: 'codex' },
profile_targets: { demo: 'glm' },
cliproxy: {
routed: {
provider: 'codex',
settings: '~/.ccs/routed.settings.json',
target: 'codex',
target: 'glm',
},
},
},
@@ -94,7 +94,7 @@ describe('profile reader target sanitization', () => {
expect(result.variants[0]?.target).toBe('claude');
});
it('normalizes unified stored codex targets back to claude for profiles and variants', async () => {
it('normalizes unified invalid stored targets back to claude for profiles and variants', async () => {
const ccsDir = getScopedCcsDir();
fs.mkdirSync(ccsDir, { recursive: true });
process.env.CCS_UNIFIED_CONFIG = '1';
@@ -106,7 +106,7 @@ describe('profile reader target sanitization', () => {
' demo:',
' type: api',
' settings: ~/.ccs/demo.settings.json',
' target: codex',
' target: glm',
'cliproxy:',
' oauth_accounts: {}',
' providers: []',
@@ -114,7 +114,7 @@ describe('profile reader target sanitization', () => {
' routed:',
' provider: codex',
' settings: ~/.ccs/routed.settings.json',
' target: codex',
' target: glm',
'',
].join('\n'),
'utf8'
+1
View File
@@ -97,6 +97,7 @@ describe('ProfileDetector', () => {
expect(detector.detectProfileType('gitlab').provider).toBe('gitlab');
expect(detector.detectProfileType('codebuddy').provider).toBe('codebuddy');
expect(detector.detectProfileType('kilo').provider).toBe('kilo');
expect(detector.detectProfileType('qoder').provider).toBe('qoder');
});
it('should detect settings-based profile from unified config', () => {
+11 -4
View File
@@ -1,16 +1,18 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as os from 'os';
import * as path from 'path';
import { CCSXP_CLIPROXY_SHORTCUT_ENV } from '../../../src/targets/codex-cliproxy-provider-config';
const wrapperPath = require.resolve('../../../src/bin/ccsxp-runtime.ts');
const ccsPath = require.resolve('../../../src/ccs.ts');
describe('ccsxp runtime wrapper', () => {
const originalArgv = process.argv;
const originalEntryTarget = process.env.CCS_INTERNAL_ENTRY_TARGET;
const originalCodexHome = process.env.CODEX_HOME;
const originalCcsCodexProfile = process.env.CCS_CODEX_PROFILE;
const originalCcsxpCodexHome = process.env.CCSXP_CODEX_HOME;
const originalEntryTarget = process.env.CCS_INTERNAL_ENTRY_TARGET;
const originalShortcut = process.env[CCSXP_CLIPROXY_SHORTCUT_ENV];
const originalCodexHome = process.env.CODEX_HOME;
const originalCcsCodexProfile = process.env.CCS_CODEX_PROFILE;
const originalCcsxpCodexHome = process.env.CCSXP_CODEX_HOME;
beforeEach(() => {
delete require.cache[wrapperPath];
@@ -25,6 +27,11 @@ describe('ccsxp runtime wrapper', () => {
} else {
process.env.CCS_INTERNAL_ENTRY_TARGET = originalEntryTarget;
}
if (originalShortcut === undefined) {
delete process.env[CCSXP_CLIPROXY_SHORTCUT_ENV];
} else {
process.env[CCSXP_CLIPROXY_SHORTCUT_ENV] = originalShortcut;
}
if (originalCodexHome === undefined) {
delete process.env.CODEX_HOME;
} else {
@@ -143,6 +143,19 @@ describe('ensureSharedConfigSymlink', () => {
expect(stderrChunks.join('')).toContain('symlink unavailable');
});
it('rethrows symlink errors that are not fallback-safe', () => {
fs.writeFileSync(sharedConfigPath, 'model = "gpt-5.5"\n', { mode: 0o600 });
const symlinkSpy = spyOn(fs, 'symlinkSync').mockImplementation(() => {
throw Object.assign(new Error('simulated race'), { code: 'EEXIST' });
});
try {
expect(() => ensureSharedConfigSymlink(profileDir, sharedConfigPath)).toThrow(/simulated race/);
} finally {
symlinkSpy.mockRestore();
}
});
it('preserves edited fallback copies on later repair attempts', () => {
fs.writeFileSync(sharedConfigPath, 'model = "gpt-5.5"\n', { mode: 0o600 });
const linkPath = path.join(profileDir, 'config.toml');
@@ -8,7 +8,7 @@
* - cliproxy-format source → rejects with clear message
* - torn-write retry: truncated JSON twice then full → succeeds on 3rd read
* - persistent torn state → clean error, not silent corruption
* - pgrep mock returning PID → warns + refuses without --force-while-running
* - process-table Codex PID → warns + refuses without --force-while-running
* - --force-while-running bypasses pgrep check
* - --with-history copies history.jsonl + sessions/
* - --with-history default false → not copied
@@ -54,9 +54,9 @@ beforeEach(() => {
process.env.CCS_HOME = ccsHome;
process.env.LEGACY_CODEX_HOME = legacyCodexHome;
// Default: pgrep finds nothing (no Codex running). Tests that need a positive
// result override this per-test. Without this default, `pgrep -f codex` on a
// dev machine matches the Claude Code process itself and exits early with code 7.
// Default: process-table lookup finds nothing (no Codex running). Tests that
// need a positive result override this per-test. This keeps local developer
// processes from affecting import-default tests.
spyOn(childProcess, 'spawnSync').mockReturnValue({
status: 1,
stdout: '',
@@ -78,9 +78,8 @@ afterEach(() => {
});
async function makeCtx() {
const { CodexProfileRegistry } = await import(
'../../../../src/codex-auth/codex-profile-registry'
);
const { CodexProfileRegistry } =
await import('../../../../src/codex-auth/codex-profile-registry');
return {
registry: new CodexProfileRegistry(),
version: '0.0.0-test',
@@ -128,21 +127,10 @@ function captureOutput(): { stderr: string[]; restore: () => void } {
};
}
function mockProcessTable(pgrepStdout: string, psStdout: string) {
function mockProcessTable(psStdout: string) {
spyOn(childProcess, 'spawnSync').mockImplementation(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(cmd: string, _args: string[]): any => {
if (cmd === 'pgrep') {
return {
status: pgrepStdout.trim().length > 0 ? 0 : 1,
stdout: pgrepStdout,
stderr: '',
pid: 0,
output: [],
signal: null,
error: undefined,
};
}
if (cmd === 'ps') {
return {
status: psStdout.trim().length > 0 ? 0 : 1,
@@ -171,9 +159,8 @@ function mockProcessTable(pgrepStdout: string, psStdout: string) {
describe('import-default — missing legacy auth.json', () => {
it('exits with clear error when ~/.codex/auth.json does not exist', async () => {
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
let exitCalled = false;
@@ -201,9 +188,8 @@ describe('import-default — option validation', () => {
it('rejects unsupported flags before importing legacy auth', async () => {
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
let exitCount = 0;
@@ -252,9 +238,8 @@ describe('import-default — profile collision without --force', () => {
// Write valid legacy auth
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
// Pre-create the profile
ctx.registry.createProfile('myprofile');
@@ -284,9 +269,8 @@ describe('import-default — --force overwrites and creates backup', () => {
// Write valid legacy auth
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
// First import (no --force needed since profile doesn't exist)
@@ -338,9 +322,8 @@ describe('import-default — cliproxy-format rejection', () => {
});
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), cliproxyAuth);
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
let exitCalled = false;
@@ -370,9 +353,8 @@ describe('import-default — torn-write retry', () => {
// Write truncated JSON initially — simulates torn write mid-file
fs.writeFileSync(authPath, '{truncated');
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
// After 50ms (before 2nd retry at 100ms) replace with valid JSON
@@ -397,9 +379,8 @@ describe('import-default — torn-write retry', () => {
// Write persistently invalid JSON — all retries will fail
fs.writeFileSync(authPath, '{always-truncated');
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
let exitCalled = false;
@@ -426,9 +407,8 @@ describe('import-default — torn-write retry', () => {
const authPath = path.join(legacyCodexHome, 'auth.json');
fs.writeFileSync(authPath, JSON.stringify({ tokens: { id_token: 'header.not-json.sig' } }));
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
let exitCalled = false;
@@ -455,9 +435,8 @@ describe('import-default — torn-write retry', () => {
const authPath = path.join(legacyCodexHome, 'auth.json');
fs.writeFileSync(authPath, JSON.stringify({ tokens: { id_token: 'h.e30$.s' } }));
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
let exitCalled = false;
@@ -485,9 +464,8 @@ describe('import-default — torn-write retry', () => {
const [header, payload] = VALID_JWT.split('.');
fs.writeFileSync(authPath, JSON.stringify({ tokens: { id_token: `${header}.${payload}.a` } }));
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
let exitCalled = false;
@@ -512,14 +490,13 @@ describe('import-default — torn-write retry', () => {
});
describe('import-default — Codex running detection', () => {
it('warns and refuses when pgrep finds a codex PID', async () => {
it('warns and refuses when process table finds a same-user Codex PID', async () => {
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
mockProcessTable('12345\n', '12345 /usr/local/bin/codex login\n');
mockProcessTable(`12345 ${process.getuid()} /usr/local/bin/codex login\n`);
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
let exitCalled = false;
@@ -546,11 +523,10 @@ describe('import-default — Codex running detection', () => {
it('proceeds with --force-while-running even when Codex is running', async () => {
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
mockProcessTable('12345\n', '12345 /usr/local/bin/codex login\n');
mockProcessTable(`12345 ${process.getuid()} /usr/local/bin/codex login\n`);
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
const restore = silenceConsole();
@@ -567,11 +543,10 @@ describe('import-default — Codex running detection', () => {
it('warns and refuses when Codex is running through a node shim', async () => {
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
mockProcessTable('12345\n', '12345 /usr/bin/node /usr/local/bin/codex login\n');
mockProcessTable(`12345 ${process.getuid()} /usr/bin/node /usr/local/bin/codex login\n`);
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
let exitCalled = false;
@@ -595,14 +570,13 @@ describe('import-default — Codex running detection', () => {
expect(ctx.registry.hasProfile('nodeshim')).toBe(false);
});
it('ignores pgrep false positives that are not Codex executables', async () => {
it('ignores process-table false positives that are not Codex executables', async () => {
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
mockProcessTable('11111\n', '11111 /usr/bin/node /tmp/codex-auth-helper.js\n');
mockProcessTable(`11111 ${process.getuid()} /usr/bin/node /tmp/codex-auth-helper.js\n`);
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
const restore = silenceConsole();
@@ -614,6 +588,47 @@ describe('import-default — Codex running detection', () => {
expect(ctx.registry.hasProfile('falsepositive')).toBe(true);
});
it('ignores same-process codex-runtime invocation paths', async () => {
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
mockProcessTable(
`${process.pid} ${process.getuid()} node /workspace/ccs/dist/bin/codex-runtime.js auth import-default self\n`
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
const restore = silenceConsole();
try {
await handleImportDefaultCodex(ctx, ['selfmatch']);
} finally {
restore();
}
expect(ctx.registry.hasProfile('selfmatch')).toBe(true);
});
it('ignores Codex processes owned by another uid', async () => {
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
const otherUid = process.getuid() + 1;
mockProcessTable(`22222 ${otherUid} /usr/local/bin/codex login\n`);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
const restore = silenceConsole();
try {
await handleImportDefaultCodex(ctx, ['otheruid']);
} finally {
restore();
}
expect(ctx.registry.hasProfile('otheruid')).toBe(true);
});
});
describe('import-default — --with-history', () => {
@@ -624,9 +639,8 @@ describe('import-default — --with-history', () => {
fs.mkdirSync(sessionsDir, { recursive: true });
fs.writeFileSync(path.join(sessionsDir, 'sess1.json'), '{}');
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
const restore = silenceConsole();
@@ -645,9 +659,8 @@ describe('import-default — --with-history', () => {
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
fs.writeFileSync(path.join(legacyCodexHome, 'history.jsonl'), '{"prompt":"hello"}\n');
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
const restore = silenceConsole();
@@ -666,9 +679,8 @@ describe('import-default — atomic write', () => {
it('leaves no tmp file after successful import', async () => {
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
const restore = silenceConsole();
@@ -689,9 +701,8 @@ describe('import-default — happy path end-to-end', () => {
it('registers profile with decoded email in registry', async () => {
fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON);
const { handleImportDefaultCodex } = await import(
'../../../../src/codex-auth/commands/import-default-command'
);
const { handleImportDefaultCodex } =
await import('../../../../src/codex-auth/commands/import-default-command');
const ctx = await makeCtx();
const restore = silenceConsole();
+4 -4
View File
@@ -71,15 +71,15 @@ describe('api-command arg parser', () => {
expect(parsed.target).toBeUndefined();
expect(parsed.errors).toEqual([
'Invalid --target value "invalid-target". Use: claude or droid',
'Invalid --target value "invalid-target". Use: claude, droid, or codex',
]);
});
test('rejects runtime-only codex as a persisted API target value', () => {
test('accepts codex as a persisted API target value', () => {
const parsed = parseApiCommandArgs(['my-api', '--target', 'codex']);
expect(parsed.target).toBeUndefined();
expect(parsed.errors).toEqual(['Invalid --target value "codex". Use: claude or droid']);
expect(parsed.target).toBe('codex');
expect(parsed.errors).toEqual([]);
});
test('collects missing-value error for --target with no value', () => {
+118
View File
@@ -53,4 +53,122 @@ describe('cleanup command', () => {
logSpy.mockRestore();
}
});
it('does not follow a symlinked CCS archive directory during cleanup', async () => {
if (process.platform === 'win32') return;
const ccsLogsDir = getNativeLogsDir();
const archiveDir = getLogArchiveDir();
const victimDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cleanup-victim-'));
fs.mkdirSync(ccsLogsDir, { recursive: true });
fs.writeFileSync(path.join(victimDir, 'keepme.log'), 'do not delete');
fs.symlinkSync(victimDir, archiveDir, 'dir');
const logSpy = spyOn(console, 'log').mockImplementation(() => {});
try {
await handleCleanupCommand(['--force']);
const output = logSpy.mock.calls
.flatMap((call) => call.map((value) => String(value)))
.join('\n');
expect(output).toContain('No CCS or CLIProxy logs found.');
expect(fs.existsSync(path.join(victimDir, 'keepme.log'))).toBe(true);
} finally {
logSpy.mockRestore();
fs.rmSync(victimDir, { recursive: true, force: true });
}
});
it('warns instead of reporting no logs when a cleanup directory cannot be read', async () => {
const archiveDir = getLogArchiveDir();
fs.mkdirSync(archiveDir, { recursive: true });
const originalReaddirSync = fs.readdirSync;
const readdirSpy = spyOn(fs, 'readdirSync').mockImplementation((dirPath, options) => {
if (String(dirPath) === archiveDir) {
throw Object.assign(new Error('permission denied'), { code: 'EACCES' });
}
return originalReaddirSync(dirPath, options as never);
});
const logSpy = spyOn(console, 'log').mockImplementation(() => {});
try {
await handleCleanupCommand(['--dry-run']);
const output = logSpy.mock.calls
.flatMap((call) => call.map((value) => String(value)))
.join('\n');
expect(output).toContain('Could not read CCS Log Archives');
expect(output).toContain('permission denied');
expect(output).not.toContain('No CCS or CLIProxy logs found.');
} finally {
readdirSpy.mockRestore();
logSpy.mockRestore();
}
});
it('warns instead of reporting no error logs when CLIProxy logs cannot be read', async () => {
const cliproxyLogsDir = path.join(getCliproxyDir(), 'logs');
fs.mkdirSync(cliproxyLogsDir, { recursive: true });
const originalReaddirSync = fs.readdirSync;
const readdirSpy = spyOn(fs, 'readdirSync').mockImplementation((dirPath, options) => {
if (String(dirPath) === cliproxyLogsDir) {
throw Object.assign(new Error('disk I/O failed'), { code: 'EIO' });
}
return originalReaddirSync(dirPath, options as never);
});
const logSpy = spyOn(console, 'log').mockImplementation(() => {});
try {
await handleCleanupCommand(['--errors']);
const output = logSpy.mock.calls
.flatMap((call) => call.map((value) => String(value)))
.join('\n');
expect(output).toContain('Could not read CLIProxy logs');
expect(output).toContain('disk I/O failed');
expect(output).not.toContain('No error logs found.');
} finally {
readdirSpy.mockRestore();
logSpy.mockRestore();
}
});
it('warns instead of treating CLIProxy log inspection errors as missing directories', async () => {
const cliproxyLogsDir = path.join(getCliproxyDir(), 'logs');
fs.mkdirSync(cliproxyLogsDir, { recursive: true });
const originalLstatSync = fs.lstatSync;
const lstatSpy = spyOn(fs, 'lstatSync').mockImplementation((targetPath, options) => {
if (String(targetPath) === cliproxyLogsDir) {
throw Object.assign(new Error('stat failed'), { code: 'EIO' });
}
return originalLstatSync(targetPath, options as never);
});
const logSpy = spyOn(console, 'log').mockImplementation(() => {});
try {
await handleCleanupCommand(['--errors']);
const output = logSpy.mock.calls
.flatMap((call) => call.map((value) => String(value)))
.join('\n');
expect(output).toContain('Could not inspect CLIProxy logs');
expect(output).toContain('stat failed');
expect(output).not.toContain('No CLIProxy logs directory found.');
} finally {
lstatSpy.mockRestore();
logSpy.mockRestore();
}
});
});
@@ -84,3 +84,48 @@ describe('cliproxy quota subcommand failure formatting', () => {
expect(resolveDisplayedTier('pro', 'unknown')).toBe('pro');
});
});
describe('cliproxy quota subcommand Codex label formatting', () => {
it('falls back to the cached window label for invalid Codex feature labels', async () => {
const { getCodexWindowDisplayLabel } = await loadQuotaCommandTestExports();
const cases = [
{ featureLabel: '', cadence: '5h', expected: 'Codex Spark (5h)' },
{ featureLabel: ' ', cadence: 'weekly', expected: 'Codex Spark (weekly)' },
{
featureLabel: '\u001b[2J\u001b]52;c;payload\u0007',
cadence: '5h',
expected: 'Codex Spark (5h)',
},
{ featureLabel: { unexpected: true }, cadence: '5h', expected: 'Codex Spark (5h)' },
] as const;
for (const { featureLabel, cadence, expected } of cases) {
const label = getCodexWindowDisplayLabel({
label: 'GPT-5.3-Codex-Spark',
resetAfterSeconds: 3600,
category: 'additional',
cadence,
featureLabel,
} as never);
expect(label).toBe(expected);
}
});
it('removes terminal control characters from cached Codex feature labels', async () => {
const { getCodexWindowDisplayLabel } = await loadQuotaCommandTestExports();
const label = getCodexWindowDisplayLabel({
label: 'ignored',
resetAfterSeconds: 3600,
category: 'additional',
cadence: 'weekly',
featureLabel: '\u001b[2JGPT-5.3-Codex-Spark\u001b]52;c;payload\u0007',
});
expect(label).toBe('Codex Spark (weekly)');
expect(label).not.toContain('\u001b');
expect(label).not.toContain('\u0007');
});
});
@@ -26,11 +26,11 @@ describe('cliproxy variant arg parser', () => {
expect(parsed.errors).toEqual(['Missing value for --target']);
});
test('rejects runtime-only codex as a persisted variant target value', () => {
test('accepts codex as a persisted variant target value', () => {
const parsed = parseProfileArgs(['variant-a', '--target', 'codex']);
expect(parsed.target).toBeUndefined();
expect(parsed.errors).toEqual(['Invalid --target value "codex". Use: claude or droid']);
expect(parsed.target).toBe('codex');
expect(parsed.errors).toEqual([]);
});
test('uses last --target value when repeated', () => {
@@ -85,6 +85,7 @@ describe('completion backend', () => {
expect(values).toContain('gitlab');
expect(values).toContain('codebuddy');
expect(values).toContain('kilo');
expect(values).toContain('qoder');
expect(values).toContain('localglm');
expect(values).toContain('work');
expect(values).toContain('my-codex');
@@ -2,35 +2,32 @@ import { describe, expect, it } from 'bun:test';
import { parseChannelsCommandArgs } from '../../../src/commands/config-channels-command';
describe('config channels command parser', () => {
it('parses selection, unattended mode, and token input', () => {
it('parses selection, unattended mode, and token channel input', () => {
const result = parseChannelsCommandArgs([
'--set',
'telegram,discord',
'--unattended',
'--set-token',
'telegram=telegram-secret',
'telegram',
]);
expect(result.setSelection).toBe('telegram,discord');
expect(result.unattended).toBe(true);
expect(result.setToken).toEqual({
channelId: 'telegram',
token: 'telegram-secret',
});
expect(result.setTokenChannel).toBe('telegram');
});
it('supports inline token assignment, legacy flags, and clear-token variants', () => {
it('supports legacy flags and clear-token variants', () => {
const result = parseChannelsCommandArgs([
'--disable',
'--no-unattended',
'--set-token=abc',
'--set-token=discord',
]);
const clearAll = parseChannelsCommandArgs(['--clear-token']);
const clearOne = parseChannelsCommandArgs(['--clear-token', 'discord']);
expect(result.disable).toBe(true);
expect(result.noUnattended).toBe(true);
expect(result.setToken).toEqual({ channelId: 'discord', token: 'abc' });
expect(result.setTokenChannel).toBe('discord');
expect(clearAll.clearTokenAll).toBe(true);
expect(clearOne.clearTokenChannel).toBe('discord');
});
@@ -61,6 +61,7 @@ describe('help command parity', () => {
expect(rendered.includes('gitlab')).toBe(true);
expect(rendered.includes('codebuddy')).toBe(true);
expect(rendered.includes('kilo')).toBe(true);
expect(rendered.includes('qoder')).toBe(true);
expect(rendered.includes('--gitlab-token-login')).toBe(true);
expect(rendered.includes('--token-login')).toBe(true);
expect(rendered.includes('--gitlab-url <url>')).toBe(true);
@@ -415,6 +415,43 @@ describe('persist command Claude extension parity', () => {
expect(renderedLogs).toContain('Native Codex target: ccsxp or ccs codex --target codex');
});
it('does not fail after writing settings when a cleared env value is deeply nested', async () => {
await writeUnifiedConfig();
const settingsPath = path.join(tempRoot, '.claude', 'settings.json');
await fs.promises.mkdir(path.dirname(settingsPath), { recursive: true });
const deepValue = `${'{"nested":'.repeat(20000)}"leaf"${'}'.repeat(20000)}`;
await fs.promises.writeFile(
settingsPath,
`{"env":{"KEEP_ME":"still-here","ANTHROPIC_AUTH_TOKEN":${deepValue}}}\n`,
'utf8'
);
const originalConsoleLog = console.log;
const capturedLogs: string[] = [];
console.log = (...args: unknown[]) => {
capturedLogs.push(args.map((arg) => String(arg)).join(' '));
};
try {
await withScopedHome(() => handlePersistCommand(['default', '--yes']));
} finally {
console.log = originalConsoleLog;
}
const persisted = JSON.parse(await fs.promises.readFile(settingsPath, 'utf8')) as {
env: Record<string, string>;
};
expect(persisted.env.KEEP_ME).toBe('still-here');
expect(persisted.env.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
const renderedLogs = capturedLogs.join('\n');
expect(renderedLogs).toContain("Profile 'default' written to");
expect(renderedLogs).toContain('Config Receipt');
expect(renderedLogs).toContain('Codex translator URL: not found');
expect(renderedLogs).not.toContain('Failed to write settings');
});
it('warns in the persist receipt when a Codex translator URL remains in settings', async () => {
await writeUnifiedConfig();
@@ -143,6 +143,27 @@ describe('shell-completion command', () => {
);
});
it('completion adapters only delegate to the ccs command on PATH', () => {
const completionScripts = [
'../../../scripts/completion/ccs.bash',
'../../../scripts/completion/ccs.zsh',
'../../../scripts/completion/ccs.fish',
'../../../scripts/completion/ccs.ps1',
];
for (const scriptPath of completionScripts) {
const script = readFileSync(join(import.meta.dir, scriptPath), 'utf8');
expect(script).not.toContain('../..');
expect(script).not.toContain('repo_root');
expect(script).not.toContain('repoRoot');
expect(script).not.toContain('repo_cli');
expect(script).not.toContain('repoCli');
expect(script).not.toContain('node ');
expect(script).not.toContain('& node');
}
});
it('fish completion strips a duplicated partial token before delegating to __complete', () => {
const fishScript = readFileSync(
join(import.meta.dir, '../../../scripts/completion/ccs.fish'),
+1 -1
View File
@@ -197,7 +197,7 @@ describe('isDaemonRunning', () => {
throw new Error('Unable to resolve test server port');
}
const result = await isDaemonRunning(address.port);
const result = await isDaemonRunning(address.port, "bad-token");
expect(result).toBe(false);
} finally {
await new Promise<void>((resolve) => {
@@ -46,11 +46,12 @@ describe('cursor-profile-executor', () => {
sonnet_model: 'cursor-sonnet',
haiku_model: 'cursor-haiku',
},
'test-token',
'/tmp/claude-config'
);
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:20129');
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('cursor-managed');
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('test-token');
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex');
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('cursor-opus');
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('cursor-sonnet');
+34
View File
@@ -150,6 +150,40 @@ describe('aggregateDailyUsage', () => {
expect(result[0].modelBreakdowns[0].provider).toBe('github-copilot');
expect(result[0].modelBreakdowns[0].inputTokens).toBe(3000);
});
test('handles provider names inherited from Object.prototype', () => {
const entries: RawUsageEntry[] = [
createEntry({ model: 'model-a', target: '__proto__' }),
createEntry({ model: 'model-b', target: 'constructor' }),
createEntry({ model: 'model-c', target: 'toString' }),
];
const daily = aggregateDailyUsage(entries);
const hourly = aggregateHourlyUsage(entries);
const monthly = aggregateMonthlyUsage(entries);
const session = aggregateSessionUsage(entries);
expect(daily[0].modelBreakdowns.map((item) => item.provider)).toEqual([
'__proto__',
'constructor',
'tostring',
]);
expect(hourly[0].modelBreakdowns.map((item) => item.provider)).toEqual([
'__proto__',
'constructor',
'tostring',
]);
expect(monthly[0].modelBreakdowns.map((item) => item.provider)).toEqual([
'__proto__',
'constructor',
'tostring',
]);
expect(session[0].modelBreakdowns.map((item) => item.provider)).toEqual([
'__proto__',
'constructor',
'tostring',
]);
});
});
// ============================================================================
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'bun:test';
import { isBareClaudeSubcommandPassthrough } from '../../../src/dispatcher/profile-resolver';
/**
* Bare Claude subcommand passthrough decision (`ccs agents`, `ccs mcp`, ...).
*
* The predicate is consulted only on the profile-not-found path, so a real
* configured profile of the same name is resolved earlier and never reaches
* this gate. These tests pin the decision matrix: forward documented Claude
* subcommands on the claude target, leave everything else alone.
*/
describe('isBareClaudeSubcommandPassthrough', () => {
it('reroutes a bare Claude subcommand on the default (claude) target', () => {
expect(isBareClaudeSubcommandPassthrough('agents', ['agents'])).toBe(true);
expect(isBareClaudeSubcommandPassthrough('mcp', ['mcp'])).toBe(true);
expect(isBareClaudeSubcommandPassthrough('plugin', ['plugin'])).toBe(true);
expect(isBareClaudeSubcommandPassthrough('setup-token', ['setup-token'])).toBe(true);
});
it('reroutes when the subcommand carries its own flags', () => {
expect(
isBareClaudeSubcommandPassthrough('agents', [
'agents',
'--permission-mode',
'bypassPermissions',
])
).toBe(true);
});
it('does not reroute the implicit default profile', () => {
expect(isBareClaudeSubcommandPassthrough('default', [])).toBe(false);
});
it('does not reroute an unknown non-subcommand token', () => {
expect(isBareClaudeSubcommandPassthrough('notaprofile', ['notaprofile'])).toBe(false);
expect(isBareClaudeSubcommandPassthrough('glm', ['glm'])).toBe(false);
});
it('does not reroute when an explicit non-claude target is selected', () => {
expect(isBareClaudeSubcommandPassthrough('agents', ['agents', '--target', 'droid'])).toBe(
false
);
expect(isBareClaudeSubcommandPassthrough('mcp', ['mcp', '--target', 'codex'])).toBe(false);
});
});
+11 -5
View File
@@ -23,6 +23,7 @@ import {
const originalCcsHome = process.env.CCS_HOME;
const originalGraceDays = process.env.CCS_DOCKER_LEGACY_KEY_GRACE_DAYS;
const originalRestoreLegacyKey = process.env.CCS_DOCKER_RESTORE_LEGACY_API_KEY;
const originalEnableLegacyKeyAuth = process.env.CCS_DOCKER_ENABLE_LEGACY_KEY_AUTH;
const tempDirs: string[] = [];
function useTempCcsHome(): string {
@@ -48,6 +49,11 @@ afterEach(() => {
} else {
process.env.CCS_DOCKER_RESTORE_LEGACY_API_KEY = originalRestoreLegacyKey;
}
if (originalEnableLegacyKeyAuth === undefined) {
delete process.env.CCS_DOCKER_ENABLE_LEGACY_KEY_AUTH;
} else {
process.env.CCS_DOCKER_ENABLE_LEGACY_KEY_AUTH = originalEnableLegacyKeyAuth;
}
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
@@ -98,7 +104,7 @@ describe('docker bootstrap auth', () => {
expect(readDockerBootstrapState().state?.legacyKeyGrace).toBeUndefined();
});
it('preserves the legacy key during the default upgrade grace window', () => {
it('tracks legacy key grace but does not enable it in config by default', () => {
useTempCcsHome();
mutateConfig((config) => {
config.cliproxy.auth = {
@@ -118,7 +124,7 @@ describe('docker bootstrap auth', () => {
expect(state?.legacyKeyGrace?.legacyKey).toBe(CCS_INTERNAL_API_KEY);
expect(state?.legacyKeyGrace?.replacementKey).toBe(config.cliproxy.auth?.api_key);
expect(content).toContain(`"${config.cliproxy.auth?.api_key}"`);
expect(content).toContain(`"${CCS_INTERNAL_API_KEY}"`);
expect(content).not.toContain(`"${CCS_INTERNAL_API_KEY}"`);
});
it('honors CCS_DOCKER_LEGACY_KEY_GRACE_DAYS for upgrade expiry', () => {
@@ -188,7 +194,7 @@ describe('docker bootstrap auth', () => {
expect(changed).toBe(true);
expect(content).toContain(`"${generatedKey}"`);
expect(content).toContain(`"${CCS_INTERNAL_API_KEY}"`);
expect(content).not.toContain(`"${CCS_INTERNAL_API_KEY}"`);
});
it('restores the legacy key after an earlier run wrote a no-grace marker', () => {
@@ -212,7 +218,7 @@ describe('docker bootstrap auth', () => {
expect(changed).toBe(true);
expect(content).toContain(`"${generatedKey}"`);
expect(content).toContain(`"${CCS_INTERNAL_API_KEY}"`);
expect(content).not.toContain(`"${CCS_INTERNAL_API_KEY}"`);
});
it('recovers safely from a corrupted marker file during broken-install recovery', () => {
@@ -232,7 +238,7 @@ describe('docker bootstrap auth', () => {
expect(getDockerBootstrapStatePath()).toContain(DOCKER_BOOTSTRAP_STATE_FILENAME);
expect(readDockerBootstrapState().corrupted).toBe(false);
expect(content).toContain(`"${CCS_INTERNAL_API_KEY}"`);
expect(content).not.toContain(`"${CCS_INTERNAL_API_KEY}"`);
});
it('does not treat human custom keys as broken Docker-generated keys', () => {
@@ -0,0 +1,67 @@
import { describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const repoRoot = join(import.meta.dir, '../../..');
describe('docker release workflow context', () => {
test('builds the integrated Dockerfile with the context its COPY paths expect', () => {
const workflow = readFileSync(join(repoRoot, '.github/workflows/docker-release.yml'), 'utf8');
const dockerfile = readFileSync(join(repoRoot, 'docker/Dockerfile.integrated'), 'utf8');
expect(workflow).toMatch(
/Build and push integrated image[\s\S]*context: docker[\s\S]*file: docker\/Dockerfile\.integrated/,
);
expect(dockerfile).toContain('COPY supervisord.conf /etc/supervisord.conf');
expect(dockerfile).toContain('COPY entrypoint-integrated.sh /entrypoint-integrated.sh');
expect(existsSync(join(repoRoot, 'docker/supervisord.conf'))).toBe(true);
expect(existsSync(join(repoRoot, 'docker/entrypoint-integrated.sh'))).toBe(true);
});
test('keeps integrated smoke tests independent from legacy dashboard publish', () => {
const workflow = readFileSync(join(repoRoot, '.github/workflows/docker-release.yml'), 'utf8');
expect(workflow).not.toMatch(/publish-integrated:[\s\S]*?needs:\s*\[?publish-dashboard/);
expect(workflow).toMatch(/smoke-test:[\s\S]*?needs:\s*\[publish-integrated\]/);
});
test('verifies immutable and promoted tags without registry credentials', () => {
const workflow = readFileSync(join(repoRoot, '.github/workflows/docker-release.yml'), 'utf8');
expect(workflow).toMatch(
/Verify anonymous pull access[\s\S]*DOCKER_CONFIG="\$\{CLEAN_DOCKER_CONFIG\}" docker pull/
);
expect(workflow).toMatch(
/Verify promoted tags are anonymously pullable[\s\S]*DOCKER_CONFIG="\$\{CLEAN_DOCKER_CONFIG\}" docker pull/
);
});
test('gives the raw integrated image a Docker healthcheck for release smoke tests', () => {
const dockerfile = readFileSync(join(repoRoot, 'docker/Dockerfile.integrated'), 'utf8');
expect(dockerfile).toContain('HEALTHCHECK');
expect(dockerfile).toContain('127.0.0.1:3000');
expect(dockerfile).toContain('127.0.0.1:8317');
});
test('lets network-contract smoke tests avoid fixed host port collisions', () => {
const compose = readFileSync(join(repoRoot, 'docker/compose.yaml'), 'utf8');
const contractScript = readFileSync(join(repoRoot, 'tests/docker/network-contract.sh'), 'utf8');
expect(compose).toContain('${CCS_DASHBOARD_PORT:-3000}:3000');
expect(compose).toContain('${CCS_CLIPROXY_PORT:-8317}:8317');
expect(contractScript).toContain('CCS_NETWORK_CONTRACT_DASHBOARD_PORT:-${CCS_DASHBOARD_PORT:-13001}');
expect(contractScript).toContain('CCS_NETWORK_CONTRACT_CLIPROXY_PORT:-${CCS_CLIPROXY_PORT:-18318}');
expect(contractScript).toContain('up -d --remove-orphans');
});
test('passes collision-safe compose env through network-contract calls', () => {
const result = spawnSync('bash', ['tests/docker/network-contract-env.test.sh'], {
cwd: repoRoot,
encoding: 'utf8',
});
expect(result.status, result.stdout + result.stderr).toBe(0);
});
});
@@ -0,0 +1,96 @@
import { describe, expect, it } from 'bun:test';
import {
buildReleaseIssueSet,
extractIssueNumbers,
extractPrNumbers,
planIssueCleanup,
} from '../../../scripts/github/stable-release-issue-cleanup-lib.mjs';
describe('stable release issue cleanup', () => {
it('extracts issue numbers from release action verbs', () => {
const text = [
'feat: promote dev to main (#1351), closes #1340 #1341',
'fix: support aliases (#1197)',
'Refs #760',
].join('\n');
expect(extractIssueNumbers(text, { includeRefs: true })).toEqual([760, 1340, 1341]);
expect(extractIssueNumbers(text, { includeRefs: false })).toEqual([1340, 1341]);
});
it('ignores unrelated issue references after the action reference sequence', () => {
const text = [
'fix: guard release parser',
'',
'Fixes #12; see #99 for the follow-up',
'Resolves #13, #14 and #15; related to #100',
].join('\n');
expect(extractIssueNumbers(text, { includeRefs: true })).toEqual([12, 13, 14, 15]);
expect(extractIssueNumbers(text, { includeRefs: false })).toEqual([12, 13, 14, 15]);
});
it('extracts PR numbers from merge and squash commit subjects', () => {
const text = [
'Merge pull request #1392 from kaitranntt/kai/fix/foo',
'fix(analytics): tighten top-bar layout (#1391)',
].join('\n');
expect(extractPrNumbers(text)).toEqual([1391, 1392]);
});
it('combines release body, commit text, and PR body issue references', () => {
const result = buildReleaseIssueSet({
releaseBody: 'closes #100',
commitText: 'fix: thing (#10)',
prText: 'Refs #200\nResolves #300',
});
expect(result.releaseIssues).toEqual([100, 200, 300]);
expect(result.resolvedIssues).toEqual([100, 300]);
});
it('closes dev-released issues when promoted to stable even if the PR used refs', () => {
const actions = planIssueCleanup({
releaseIssues: [760],
resolvedIssues: [],
issueStates: new Map([[760, { state: 'OPEN', labels: ['released-dev'] }]]),
});
expect(actions[0]).toMatchObject({
number: 760,
addReleasedLabel: true,
close: true,
reason: 'promoted from dev to stable',
});
});
it('does not close weak refs unless the issue was already marked released-dev', () => {
const actions = planIssueCleanup({
releaseIssues: [42],
resolvedIssues: [],
issueStates: new Map([[42, { state: 'OPEN', labels: ['enhancement'] }]]),
});
expect(actions[0]).toMatchObject({
number: 42,
addReleasedLabel: false,
close: false,
});
});
it('closes explicitly resolved issues without requiring a manual released label', () => {
const actions = planIssueCleanup({
releaseIssues: [1340],
resolvedIssues: [1340],
issueStates: new Map([[1340, { state: 'OPEN', labels: ['bug'] }]]),
});
expect(actions[0]).toMatchObject({
number: 1340,
addReleasedLabel: true,
close: true,
reason: 'resolved by stable release',
});
});
});
+14
View File
@@ -46,4 +46,18 @@ describe('SSEParser', () => {
?.delta?.content
).toBe('Legacy');
});
it('does not split events when CRLF is split across chunks', () => {
const parser = new SSEParser({ throwOnMalformedJson: true });
expect(parser.parse('data: {"choices":[\r')).toEqual([]);
const events = parser.parse('\ndata: {"delta":{"content":"Hello"}}\r\ndata: ]}\r\n\r\n');
expect(events).toHaveLength(1);
expect(
(events[0]?.data as { choices?: Array<{ delta?: { content?: string } }> })?.choices?.[0]
?.delta?.content
).toBe('Hello');
});
});
@@ -722,6 +722,47 @@ describe('ccs-browser MCP server - navigation and query', () => {
);
});
it('rejects excessive clickCount and repeat values', async () => {
const responses = await runMcpRequests(
[{ id: 'page-1', title: 'Limits Page', currentUrl: 'https://example.com/' }],
[
{
jsonrpc: '2.0',
id: 713,
method: 'tools/call',
params: {
name: 'browser_click',
arguments: {
selector: '#submit',
clickCount: 26,
},
},
},
{
jsonrpc: '2.0',
id: 714,
method: 'tools/call',
params: {
name: 'browser_press_key',
arguments: {
key: 'k',
repeat: 26,
},
},
},
]
);
const clickResponse = responses.find((message) => message.id === 713);
expect((clickResponse?.result as { isError?: boolean }).isError).toBe(true);
expect(getResponseText(clickResponse)).toContain('clickCount must be less than or equal to 25');
const keyResponse = responses.find((message) => message.id === 714);
expect((keyResponse?.result as { isError?: boolean }).isError).toBe(true);
expect(getResponseText(keyResponse)).toContain('repeat must be less than or equal to 25');
});
it('scrolls an element into view with browser_scroll', async () => {
const responses = await runMcpRequests(
[
@@ -79,11 +79,17 @@ describe('ccs-browser MCP server - session and interception', () => {
expect(clickTool?.inputSchema?.properties?.clickCount).toMatchObject({
type: 'integer',
minimum: 1,
maximum: 25,
});
const keyTool = tools.find((tool) => tool.name === 'browser_press_key');
expect(keyTool?.inputSchema?.properties?.key).toMatchObject({ type: 'string' });
expect(keyTool?.inputSchema?.properties?.modifiers).toMatchObject({ type: 'array' });
expect(keyTool?.inputSchema?.properties?.repeat).toMatchObject({
type: 'integer',
minimum: 1,
maximum: 25,
});
const scrollTool = tools.find((tool) => tool.name === 'browser_scroll');
expect(scrollTool?.inputSchema?.properties?.deltaX).toMatchObject({ type: 'number' });
@@ -433,6 +439,45 @@ describe('ccs-browser MCP server - session and interception', () => {
expect(listText).not.toContain('Docs');
});
it('closes a page when Chrome DevTools requires PUT and returns text', async () => {
const responses = await runMcpRequests(
[
{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' },
{ id: 'page-2', title: 'Docs', currentUrl: 'https://example.com/docs' },
],
[
{
jsonrpc: '2.0',
id: 8331,
method: 'tools/call',
params: { name: 'browser_select_page', arguments: { pageIndex: 1 } },
},
{
jsonrpc: '2.0',
id: 8332,
method: 'tools/call',
params: { name: 'browser_close_page', arguments: {} },
},
{
jsonrpc: '2.0',
id: 8333,
method: 'tools/call',
params: { name: 'browser_get_session_info', arguments: {} },
},
],
{ requirePutForClosePage: true, closePageRespondsWithText: true }
);
const closeText = getResponseText(responses.find((message) => message.id === 8332));
expect(closeText).toContain('status: closed');
expect(closeText).toContain('selectedPageId: page-1');
const listText = getResponseText(responses.find((message) => message.id === 8333));
expect(listText).toContain('0. Home');
expect(listText).toContain('selected: true');
expect(listText).not.toContain('Docs');
});
it('keeps the selected page when closing a different page', async () => {
const responses = await runMcpRequests(
[
@@ -780,8 +825,7 @@ describe('ccs-browser MCP server - session and interception', () => {
expect(listText).toContain('requestId: req-1');
expect(listText).toContain('matchedRuleId: rule-1');
expect(listText).toContain('action: fail');
expect(listText).toContain('requestId: req-2');
expect(listText).toContain('action: continue');
expect(listText).not.toContain('requestId: req-2');
});
it('removes rules and recent requests bound to a page after that page is closed', async () => {
@@ -350,6 +350,8 @@ type RunMcpRequestsOptions = {
childEnv?: NodeJS.ProcessEnv;
responseTimeoutMs?: number;
requirePutForNewPage?: boolean;
requirePutForClosePage?: boolean;
closePageRespondsWithText?: boolean;
};
function encodeMessage(message: unknown): string {
@@ -746,6 +748,12 @@ function createMockBrowser(pagesInput: MockPageState[]) {
}
if (req.url?.startsWith('/json/close/')) {
if (options.requirePutForClosePage && req.method !== 'PUT') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'method not allowed' }));
return;
}
const targetId = decodeURIComponent(req.url.slice('/json/close/'.length));
const entry = Array.from(pageStates.entries()).find(([, page]) => page.id === targetId);
if (!entry) {
@@ -754,6 +762,11 @@ function createMockBrowser(pagesInput: MockPageState[]) {
return;
}
pageStates.delete(entry[0]);
if (options.closePageRespondsWithText) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Target is closing');
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: targetId }));
return;
+10 -12
View File
@@ -256,14 +256,14 @@ describe('model-pricing', () => {
expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5
});
it('should calculate Claude Opus 4.7 cache cost consistently across repeat lookups', () => {
it('should calculate Claude Opus 4.7 thinking cost including cache token rates', () => {
const usage: TokenUsage = {
inputTokens: 1_000_000,
outputTokens: 1_000_000,
cacheCreationTokens: 1_000_000,
cacheReadTokens: 1_000_000,
};
const cost = calculateCost(usage, 'claude-opus-4-7');
const cost = calculateCost(usage, 'claude-opus-4-7-thinking');
expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5
});
});
@@ -469,17 +469,15 @@ describe('model-pricing', () => {
});
it('gracefully ignores malformed cached model entries', () => {
setCachedModelsDevRegistry(
{
openai: {
id: 'openai',
models: {
'null-entry': null,
'gpt-5.5': { id: 'gpt-5.5', cost: { input: 5, output: 30 } },
},
setCachedModelsDevRegistry({
openai: {
id: 'openai',
models: {
'null-entry': null,
'gpt-5.5': { id: 'gpt-5.5', cost: { input: 5, output: 30 } },
},
} as unknown as Parameters<typeof setCachedModelsDevRegistry>[0]
);
},
} as unknown as Parameters<typeof setCachedModelsDevRegistry>[0]);
expect(() => getModelPricing('openai/gpt-5.5')).not.toThrow();
expect(getModelPricing('openai/gpt-5.5').inputPerMillion).toBe(5);
+104
View File
@@ -97,6 +97,12 @@ beforeEach(() => {
ANTHROPIC_MODEL: 'sonar-pro',
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
}),
mm: writeSettings('mm', {
ANTHROPIC_BASE_URL: 'https://api.minimax.io/v1',
ANTHROPIC_AUTH_TOKEN: 'minimax_token',
ANTHROPIC_MODEL: 'MiniMax-M2.7',
CCS_DROID_PROVIDER: 'openai',
}),
};
fs.writeFileSync(
@@ -242,4 +248,102 @@ describe('handleProxyMessagesRequest', () => {
expect(closeCalls).toBe(0);
expect(res.statusCode).toBe(502);
});
it('moves system messages into the first user message for MiniMax OpenAI-compatible upstreams', async () => {
const activeProfile = buildProfile('mm');
let capturedBody: unknown;
globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
capturedBody = JSON.parse(String(init?.body));
return new Response(
JSON.stringify({
id: 'chatcmpl_1',
object: 'chat.completion',
created: 1,
model: 'MiniMax-M2.7',
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' } }],
}),
{ status: 200, headers: { 'content-type': 'application/json' } }
);
}) as typeof globalThis.fetch;
const req = new FakeRequest({
'x-api-key': 'local-token',
});
const res = new FakeResponse();
const pending = handleProxyMessagesRequest(
req as never,
res as never,
activeProfile,
'local-token'
);
req.end(
JSON.stringify({
model: 'MiniMax-M2.7',
stream: false,
messages: [
{ role: 'system', content: 'Use Turkish.' },
{ role: 'user', content: 'bu hangi model' },
],
})
);
await pending;
expect(capturedBody).toMatchObject({
model: 'MiniMax-M2.7',
messages: [{ role: 'user', content: 'Use Turkish.\n\nbu hangi model' }],
});
expect((capturedBody as { messages: Array<{ role: string }> }).messages).not.toContainEqual(
expect.objectContaining({ role: 'system' })
);
});
it('strips blank system messages for MiniMax OpenAI-compatible upstreams', async () => {
const activeProfile = buildProfile('mm');
let capturedBody: unknown;
globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
capturedBody = JSON.parse(String(init?.body));
return new Response(
JSON.stringify({
id: 'chatcmpl_1',
object: 'chat.completion',
created: 1,
model: 'MiniMax-M2.7',
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' } }],
}),
{ status: 200, headers: { 'content-type': 'application/json' } }
);
}) as typeof globalThis.fetch;
const req = new FakeRequest({
'x-api-key': 'local-token',
});
const res = new FakeResponse();
const pending = handleProxyMessagesRequest(
req as never,
res as never,
activeProfile,
'local-token'
);
req.end(
JSON.stringify({
model: 'MiniMax-M2.7',
stream: false,
messages: [
{ role: 'system', content: [{ type: 'text', text: '' }] },
{ role: 'user', content: 'bu hangi model' },
],
})
);
await pending;
expect(capturedBody).toMatchObject({
model: 'MiniMax-M2.7',
messages: [{ role: 'user', content: 'bu hangi model' }],
});
expect((capturedBody as { messages: Array<{ role: string }> }).messages).not.toContainEqual(
expect.objectContaining({ role: 'system' })
);
});
});
@@ -43,6 +43,23 @@ describe('ProxyRequestTransformer', () => {
});
});
it('accepts Claude Code system messages in the messages array', () => {
const transformer = new ProxyRequestTransformer();
const result = transformer.transform({
messages: [
{ role: 'user', content: 'hello' },
{ role: 'system', content: [{ type: 'text', text: 'answer tersely' }] },
{ role: 'user', content: 'which model is this?' },
],
});
expect(result.messages).toEqual([
{ role: 'user', content: 'hello' },
{ role: 'system', content: 'answer tersely' },
{ role: 'user', content: 'which model is this?' },
]);
});
it('translates base64 image blocks into OpenAI image_url parts', () => {
const transformer = new ProxyRequestTransformer();
const result = transformer.transform({
@@ -45,14 +45,18 @@ describe('docker dashboard sunset guard', () => {
});
});
test('fails loudly if the baseline tag is missing after the baseline', () => {
expect(() =>
evaluateDashboardSunset({
targetTag: 'v7.81.2',
baselineVersion: '7.81.0',
releaseWindow: 2,
stableTags: ['v7.80.0'],
}),
).toThrow('baseline tag v7.81.0 is missing');
test('skips deprecated publish if the baseline tag is missing after the baseline', () => {
const result = evaluateDashboardSunset({
targetTag: 'v7.81.2',
baselineVersion: '7.81.0',
releaseWindow: 2,
stableTags: ['v7.80.0'],
});
expect(result).toMatchObject({
elapsed: 2,
publish: false,
});
expect(result.reason).toContain('baseline v7.81.0 is missing');
});
});
@@ -44,6 +44,9 @@ describe('PR-Agent review lane migration', () => {
expect(workflow).toContain('CCS_REVIEWER_APP_ID');
expect(workflow).toContain('CCS_REVIEWER_PRIVATE_KEY');
expect(workflow).toContain('id: pr-agent-app-token');
expect(workflow).toContain('permission-issues: write');
expect(workflow).toContain('permission-pull-requests: write');
expect(workflow).toContain('permission-contents: read');
expect(workflow).toContain('GITHUB_TOKEN: ${{ steps.pr-agent-app-token.outputs.token }}');
expect(workflow).not.toContain('GITHUB_TOKEN: ${{ github.token }}');
expect(workflow).not.toContain('uses: anthropics/claude-code-action@v1');
+25
View File
@@ -307,6 +307,31 @@ describe('SharedManager', () => {
});
describe('marketplace registry ownership', () => {
it('skips unstatable shared plugin entries during instance linking', () => {
const manager = new SharedManager();
const instancePath = instanceDir('work');
const sharedPluginsPath = path.join(claudeDir(), 'plugins');
const logSpy = spyOn(console, 'log').mockImplementation(() => {});
fs.mkdirSync(instancePath, { recursive: true });
fs.mkdirSync(sharedPluginsPath, { recursive: true });
fs.symlinkSync(
path.join(sharedPluginsPath, 'missing-plugin'),
path.join(sharedPluginsPath, 'broken-plugin'),
'dir'
);
expect(() => manager.linkSharedDirectories(instancePath)).not.toThrow();
expect(fs.existsSync(path.join(instancePath, 'plugins', 'broken-plugin'))).toBe(false);
expect(
logSpy.mock.calls.some(([message]) =>
String(message).includes(
'Skipping plugins/broken-plugin: unable to inspect shared plugin entry'
)
)
).toBe(true);
});
it('writes global and instance registries with different authoritative install locations', () => {
const globalRegistryPath = path.join(claudeDir(), 'plugins', 'known_marketplaces.json');
ensureMarketplacePayload(claudeDir());
@@ -31,4 +31,14 @@ describe('stale Codex translator settings scanner', () => {
'["custom-key"][0]',
]);
});
it('does not overflow the stack on deeply nested local settings values', () => {
let settings: Record<string, unknown> = { value: 'leaf' };
for (let depth = 0; depth < 20000; depth += 1) {
settings = { nested: settings };
}
expect(() => findCodexTranslatorUrlPaths(settings)).not.toThrow();
expect(findCodexTranslatorUrlPaths(settings)).toEqual([]);
});
});
+28 -1
View File
@@ -1,6 +1,7 @@
import { describe, expect, test } from 'bun:test';
import { CodexAdapter } from '../../../src/targets/codex-adapter';
import { CCSXP_CLIPROXY_SHORTCUT_ENV } from '../../../src/targets/codex-cliproxy-provider-config';
import {
buildCodexBrowserMcpOverrides,
getCodexBrowserMcpServerName,
@@ -97,6 +98,30 @@ describe('CodexAdapter', () => {
).toThrow(/does not advertise --config overrides/);
});
test('rejects ccsxp default launches when codex lacks config override support', () => {
const originalShortcut = process.env[CCSXP_CLIPROXY_SHORTCUT_ENV];
process.env[CCSXP_CLIPROXY_SHORTCUT_ENV] = '1';
try {
expect(() =>
adapter.buildArgs('default', ['--config', 'model_provider="cliproxy"', '--version'], {
profileType: 'default',
binaryInfo: {
path: '/tmp/codex',
needsShell: false,
version: 'codex-cli 0.1.0',
features: [],
},
})
).toThrow(/does not advertise --config overrides/);
} finally {
if (originalShortcut === undefined) {
delete process.env[CCSXP_CLIPROXY_SHORTCUT_ENV];
} else {
process.env[CCSXP_CLIPROXY_SHORTCUT_ENV] = originalShortcut;
}
}
});
test('injects transient config overrides for CCS-backed launches', () => {
const runtimeConfigOverrides = buildCodexBrowserMcpOverrides();
const args = adapter.buildArgs('codex', ['--search'], {
@@ -120,7 +145,9 @@ describe('CodexAdapter', () => {
expect(args).toContain('model_provider="ccs_runtime"');
expect(args).toContain('model_providers.ccs_runtime.env_key="CCS_CODEX_API_KEY"');
expect(args).toContain('model="gpt-5.4"');
expect(args).toContain(`mcp_servers.${getCodexBrowserMcpServerName()}.command=${JSON.stringify(process.platform === 'win32' ? 'npx.cmd' : 'npx')}`);
expect(args).toContain(
`mcp_servers.${getCodexBrowserMcpServerName()}.command=${JSON.stringify(process.platform === 'win32' ? 'npx.cmd' : 'npx')}`
);
expect(args).toContain('model_reasoning_effort="high"');
expect(args[args.length - 1]).toBe('--search');
});
@@ -596,7 +596,9 @@ process.exit(0);
expect(result.status).toBe(0);
expect(fs.existsSync(freshCodexHome)).toBe(true);
expect(fs.statSync(freshCodexHome).isDirectory()).toBe(true);
const codexHomeStat = fs.statSync(freshCodexHome);
expect(codexHomeStat.isDirectory()).toBe(true);
expect(codexHomeStat.mode & 0o777).toBe(0o700);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([
['-c', 'model="gpt-5"', '--version'],
['-c', 'model_reasoning_effort="high"', 'fix failing tests'],
@@ -714,8 +716,10 @@ process.exit(0);
expect(result.status).toBe(0);
expect(result.stdout).toContain('codex-cli 9.9.9-test');
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([
['--config', 'model_provider="cliproxy"', '--version'],
expect(readLoggedCodexCalls(codexArgsLogPath).at(-1)).toEqual([
'--config',
'model_provider="cliproxy"',
'--version',
]);
});
@@ -737,19 +741,17 @@ process.exit(0);
});
expect(result.status).toBe(0);
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
CODEX_HOME: path.join(tmpHome, '.codex'),
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
},
]);
expect(readLoggedCodexEnv(codexEnvLogPath).at(-1)).toEqual({
CODEX_HOME: path.join(tmpHome, '.codex'),
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
});
});
it('routes default ccsxp launches through native Codex with the cliproxy provider override', () => {
@@ -768,24 +770,50 @@ process.exit(0);
});
expect(result.status).toBe(0);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([
['--config', 'model_provider="cliproxy"', 'fix failing tests'],
expect(readLoggedCodexCalls(codexArgsLogPath).at(-1)).toEqual([
'--config',
'model_provider="cliproxy"',
'fix failing tests',
]);
const codexConfig = fs.readFileSync(path.join(tmpHome, '.codex', 'config.toml'), 'utf8');
expect(codexConfig).toContain('[model_providers.cliproxy]');
expect(codexConfig).toContain('env_key = "CLIPROXY_API_KEY"');
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
CODEX_HOME: path.join(tmpHome, '.codex'),
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
},
expect(readLoggedCodexEnv(codexEnvLogPath).at(-1)).toEqual({
CODEX_HOME: path.join(tmpHome, '.codex'),
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
});
});
it('fails fast when ccsxp needs unsupported Codex config overrides', () => {
if (process.platform === 'win32') return;
const result = runCcsxpAlias(['--version'], {
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
CCS_TEST_CODEX_CONFIG_OVERRIDE_STATUS: 'unsupported',
CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test',
CCS_TEST_CODEX_HELP: ' -p, --profile <CONFIG_PROFILE>\n',
});
expect(result.status).toBe(1);
expect(result.stderr).toContain('Codex CLI (codex-cli 9.9.9-test)');
expect(result.stderr).toContain('does not advertise --config overrides');
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([
['-c', 'model="gpt-5"', '--version'],
['--help'],
['--version'],
]);
});
@@ -808,8 +836,10 @@ process.exit(0);
});
expect(result.status).toBe(0);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([
['--config', 'model_provider="cliproxy"', 'fix failing tests'],
expect(readLoggedCodexCalls(codexArgsLogPath).at(-1)).toEqual([
'--config',
'model_provider="cliproxy"',
'fix failing tests',
]);
const codexConfig = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
expect(codexConfig).toContain('model = "gpt-5.5"');
@@ -846,6 +876,32 @@ process.exit(0);
]);
});
it('preserves ccsxp positional model-like arguments after the option terminator', () => {
if (process.platform === 'win32') return;
const result = runCcsxpAlias(['--', '--', '-m', 'gpt-5.5-high-fast', 'prompt text'], {
...process.env,
CI: '1',
NO_COLOR: '1',
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
});
expect(result.status).toBe(0);
const codexCalls = readLoggedCodexCalls(codexArgsLogPath);
expect(codexCalls.at(-1)).toEqual([
'--config',
'model_provider="cliproxy"',
'--',
'-m',
'gpt-5.5-high-fast',
'prompt text',
]);
});
it('normalizes ccsxp native low Codex tuning aliases in config.toml', () => {
if (process.platform === 'win32') return;
@@ -903,24 +959,24 @@ supports_websockets = false
});
expect(result.status).toBe(0);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([
['--config', 'model_provider="cliproxy"', 'fix failing tests'],
expect(readLoggedCodexCalls(codexArgsLogPath).at(-1)).toEqual([
'--config',
'model_provider="cliproxy"',
'fix failing tests',
]);
const codexConfig = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
expect(codexConfig).toContain('env_key = "CCS_CUSTOM_CLIPROXY_TOKEN"');
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
CODEX_HOME: codexHome,
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CCS_CUSTOM_CLIPROXY_TOKEN: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
},
]);
expect(readLoggedCodexEnv(codexEnvLogPath).at(-1)).toEqual({
CODEX_HOME: codexHome,
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CCS_CUSTOM_CLIPROXY_TOKEN: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
});
});
it('preserves a custom cliproxy provider base_url for ccsxp launches', () => {
@@ -954,27 +1010,27 @@ supports_websockets = false
});
expect(result.status).toBe(0);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([
['--config', 'model_provider="cliproxy"', 'fix failing tests'],
expect(readLoggedCodexCalls(codexArgsLogPath).at(-1)).toEqual([
'--config',
'model_provider="cliproxy"',
'fix failing tests',
]);
const codexConfig = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
expect(codexConfig).toContain(
'base_url = "https://cliproxy.example.com/api/provider/codex/responses"'
);
expect(codexConfig).toContain('env_key = "CCS_REMOTE_CLIPROXY_TOKEN"');
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
CODEX_HOME: codexHome,
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CCS_REMOTE_CLIPROXY_TOKEN: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
},
]);
expect(readLoggedCodexEnv(codexEnvLogPath).at(-1)).toEqual({
CODEX_HOME: codexHome,
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CCS_REMOTE_CLIPROXY_TOKEN: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
});
});
it('keeps ccsxp native when the CCS default profile is a Claude account', () => {
@@ -1005,22 +1061,22 @@ supports_websockets = false
});
expect(result.status).toBe(0);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([
['--config', 'model_provider="cliproxy"', 'fix failing tests'],
]);
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
CODEX_HOME: path.join(tmpHome, '.codex'),
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
},
expect(readLoggedCodexCalls(codexArgsLogPath).at(-1)).toEqual([
'--config',
'model_provider="cliproxy"',
'fix failing tests',
]);
expect(readLoggedCodexEnv(codexEnvLogPath).at(-1)).toEqual({
CODEX_HOME: path.join(tmpHome, '.codex'),
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
});
});
it('keeps implicit ccs --target codex launches native when the CCS default is a Claude account', () => {
@@ -1045,6 +1101,7 @@ supports_websockets = false
HOME: tmpHome,
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CODEX_HOME: undefined,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath,
CCS_THINKING: '8192',
@@ -1135,7 +1192,9 @@ supports_websockets = false
});
expect(result.status).toBe(0);
expect(result.stderr).not.toContain('Codex CLI does not support Claude account-based profiles.');
expect(result.stderr).not.toContain(
'Codex CLI does not support Claude account-based profiles.'
);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['fix failing tests']]);
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
@@ -1205,19 +1264,17 @@ supports_websockets = false
});
expect(result.status).toBe(0);
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
{
CODEX_HOME: explicitCodexHome,
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
},
]);
expect(readLoggedCodexEnv(codexEnvLogPath).at(-1)).toEqual({
CODEX_HOME: explicitCodexHome,
CODEX_CI: undefined,
CODEX_MANAGED_BY_BUN: undefined,
CODEX_THREAD_ID: undefined,
ANTHROPIC_BASE_URL: undefined,
CLIPROXY_API_KEY: 'ccs-internal-managed',
CCS_BROWSER_USER_DATA_DIR: undefined,
CCS_BROWSER_PROFILE_DIR: undefined,
CCS_BROWSER_DEVTOOLS_WS_URL: undefined,
});
});
it('fails with a clean CLI error when ccsxp receives a malformed --target flag', () => {
+2 -2
View File
@@ -68,9 +68,9 @@ describe('resolveTargetType', () => {
expect(resolveTargetType([], { target: 'invalid-target' as never })).toBe('claude');
});
it('should ignore runtime-only codex target when it appears in persisted profile config', () => {
it('should use codex from persisted profile config', () => {
process.argv = ['node', 'ccs'];
expect(resolveTargetType([], { target: 'codex' })).toBe('claude');
expect(resolveTargetType([], { target: 'codex' })).toBe('codex');
});
it('should prioritize --target flag over profile config', () => {
+3 -1
View File
@@ -6,9 +6,11 @@ import {
} from '../../../ui/src/lib/cliproxy-version-risk';
describe('cliproxy-version-risk helpers', () => {
it('compares versions while ignoring release suffixes', () => {
it('compares fork release suffixes after core versions', () => {
expect(compareCliproxyVersions('6.6.88', '6.6.81-0')).toBe(1);
expect(compareCliproxyVersions('6.6.81-0', '6.6.81')).toBe(0);
expect(compareCliproxyVersions('7.1.31-1', '7.1.31-0')).toBe(1);
expect(compareCliproxyVersions('7.1.31-0', '7.1.31-1')).toBe(-1);
expect(compareCliproxyVersions('6.6.80', '6.6.81')).toBe(-1);
});
@@ -22,6 +22,7 @@ describe('browser status', () => {
let originalBrowserUserDataDir: string | undefined;
let originalBrowserProfileDir: string | undefined;
let originalBrowserDevtoolsPort: string | undefined;
let originalBrowserEvalMode: string | undefined;
beforeEach(() => {
tempHome = mkdtempSync(join(tmpdir(), 'ccs-browser-status-'));
@@ -29,11 +30,13 @@ describe('browser status', () => {
originalBrowserUserDataDir = process.env.CCS_BROWSER_USER_DATA_DIR;
originalBrowserProfileDir = process.env.CCS_BROWSER_PROFILE_DIR;
originalBrowserDevtoolsPort = process.env.CCS_BROWSER_DEVTOOLS_PORT;
originalBrowserEvalMode = process.env.CCS_BROWSER_EVAL_MODE;
process.env.CCS_HOME = tempHome;
delete process.env.CCS_BROWSER_USER_DATA_DIR;
delete process.env.CCS_BROWSER_PROFILE_DIR;
delete process.env.CCS_BROWSER_DEVTOOLS_PORT;
delete process.env.CCS_BROWSER_EVAL_MODE;
});
afterEach(() => {
@@ -60,6 +63,11 @@ describe('browser status', () => {
} else {
delete process.env.CCS_BROWSER_DEVTOOLS_PORT;
}
if (originalBrowserEvalMode !== undefined) {
process.env.CCS_BROWSER_EVAL_MODE = originalBrowserEvalMode;
} else {
delete process.env.CCS_BROWSER_EVAL_MODE;
}
rmSync(tempHome, { recursive: true, force: true });
});
@@ -290,6 +298,29 @@ describe('browser status', () => {
});
});
it('honors CCS_BROWSER_EVAL_MODE over configured eval_mode', () => {
mutateUnifiedConfig((config) => {
config.browser = {
claude: {
enabled: true,
policy: 'manual',
user_data_dir: '/config-browser',
devtools_port: 9333,
eval_mode: 'readonly',
},
codex: {
enabled: false,
policy: 'manual',
},
};
});
process.env.CCS_BROWSER_EVAL_MODE = 'disabled';
const effective = getEffectiveClaudeBrowserAttachConfig(getBrowserConfig());
expect(effective.evalMode).toBe('disabled');
});
it('returns the same managed attach warning when the configured DevTools port is unreachable', async () => {
const managedDir = join(tempHome, '.ccs', 'browser', 'chrome-user-data');
mkdirSync(managedDir, { recursive: true });
@@ -145,8 +145,16 @@ function registerChildProcessMock(): void {
}));
}
const tempCcsHomes = new Set<string>();
function createTempCcsHome(prefix: string): string {
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempCcsHomes.add(tempHome);
return tempHome;
}
function writeConfigWithAutoUpdatePreference(enabled: boolean): void {
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-auto-update-pref-'));
const tempHome = createTempCcsHome('ccs-auto-update-pref-');
process.env.CCS_HOME = tempHome;
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
@@ -158,7 +166,7 @@ preferences:
}
function writeConfigWithWebSearchSettings(yamlBody: string): void {
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-websearch-env-'));
const tempHome = createTempCcsHome('ccs-websearch-env-');
process.env.CCS_HOME = tempHome;
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
@@ -225,10 +233,8 @@ describe('CLAUDECODE environment stripping', () => {
});
afterEach(async () => {
const tempCcsHome = process.env.CCS_HOME?.startsWith(os.tmpdir())
? process.env.CCS_HOME
: undefined;
if (tempCcsHome) {
const tempCcsHome = process.env.CCS_HOME;
if (tempCcsHome && tempCcsHomes.has(tempCcsHome)) {
await stopOpenAICompatProxy();
}
@@ -277,9 +283,10 @@ describe('CLAUDECODE environment stripping', () => {
}
}
if (tempCcsHome) {
fs.rmSync(tempCcsHome, { recursive: true, force: true });
for (const tempHome of tempCcsHomes) {
fs.rmSync(tempHome, { recursive: true, force: true });
}
tempCcsHomes.clear();
});
it('stripClaudeCodeEnv removes CLAUDECODE case-insensitively', () => {
@@ -20,6 +20,9 @@ describe('glmt deprecation helpers', () => {
expect(isLegacyGlmtBaseUrl('https://api.z.ai/api/coding/paas/v4/chat/completions/')).toBe(
true
);
expect(isLegacyGlmtBaseUrl('https://private.example.internal/v1/chat/completions')).toBe(
false
);
expect(isLegacyGlmtBaseUrl('https://api.z.ai/api/anthropic')).toBe(false);
});
@@ -3,12 +3,20 @@ import {
DEFAULT_IMAGE_ANALYSIS_CONFIG,
type ImageAnalysisConfig,
} from '../../../../src/config/unified-config-types';
import { findModel } from '../../../../src/cliproxy/model-catalog';
import {
canonicalizeImageAnalysisConfig,
resolveImageAnalysisStatus,
} from '../../../../src/utils/hooks/image-analysis-backend-resolver';
describe('image-analysis-backend-resolver', () => {
it('uses a catalog-backed Claude image analysis default model', () => {
const defaultModel = DEFAULT_IMAGE_ANALYSIS_CONFIG.provider_models.claude;
expect(defaultModel).toBe('claude-haiku-4-5-20251001');
expect(findModel('claude', defaultModel)?.id).toBe(defaultModel);
});
it('canonicalizes provider aliases in config', () => {
const config = canonicalizeImageAnalysisConfig({
enabled: true,
@@ -313,6 +313,12 @@ describe('web-server claude-extension-routes', () => {
it('creates a binding and applies managed settings to shared + IDE targets', async () => {
const ideSettingsPath = path.join(tempHome, 'ide', 'vscode', 'settings.json');
const sharedSettingsPath = path.join(tempHome, '.claude', 'settings.json');
fs.mkdirSync(path.dirname(ideSettingsPath), { recursive: true });
fs.mkdirSync(path.dirname(sharedSettingsPath), { recursive: true });
fs.writeFileSync(sharedSettingsPath, JSON.stringify({ env: {} }, null, 2) + '\n', { mode: 0o600 });
fs.writeFileSync(ideSettingsPath, JSON.stringify({}, null, 2) + '\n', { mode: 0o600 });
const createResponse = await fetch(`${baseUrl}/api/claude-extension/bindings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -352,7 +358,6 @@ describe('web-server claude-extension-routes', () => {
expect(applied.sharedSettings.state).toBe('applied');
expect(applied.ideSettings.state).toBe('applied');
const sharedSettingsPath = path.join(tempHome, '.claude', 'settings.json');
const sharedSettings = JSON.parse(fs.readFileSync(sharedSettingsPath, 'utf8')) as {
env?: Record<string, string>;
};
@@ -370,6 +375,9 @@ describe('web-server claude-extension-routes', () => {
)
).toBe(true);
expect(ideSettings['claudeCode.disableLoginPrompt']).toBe(true);
expect(fs.statSync(sharedSettingsPath).mode & 0o777).toBe(0o600);
expect(fs.statSync(ideSettingsPath).mode & 0o777).toBe(0o600);
});
it('resets only managed keys and preserves unrelated shared + IDE settings', async () => {
@@ -6,6 +6,7 @@ import * as path from 'path';
import * as http from 'http';
import type { Server } from 'http';
import cliproxyAuthRoutes from '../../../src/web-server/routes/cliproxy-auth-routes';
import { registerAccountFromToken } from '../../../src/cliproxy/auth/token-manager';
import {
clearQuotaCache,
getCachedQuota,
@@ -593,6 +594,61 @@ describe('cliproxy-auth-routes manual callback nickname persistence', () => {
}
});
it('reauthenticates a targeted existing Codex account when its token file is rewritten', async () => {
const tokenDir = path.join(tempHome, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(tokenDir, { recursive: true });
const tokenPath = path.join(tokenDir, 'codex-existing@example.com.json');
fs.writeFileSync(
tokenPath,
JSON.stringify({ type: 'codex', email: 'existing@example.com', version: 1 }),
'utf8'
);
const initialAccount = registerAccountFromToken('codex', tokenDir, 'work');
expect(initialAccount?.id).toBe('existing@example.com');
mockFetch([
{
url: /\/v0\/management\/codex-auth-url\?is_webui=true$/,
response: {
auth_url: 'https://auth.example.com/authorize?state=state-targeted-rewrite',
state: 'state-targeted-rewrite',
},
},
{
url: /\/v0\/management\/get-auth-status\?state=state-targeted-rewrite$/,
response: { status: 'ok' },
},
]);
const startResponse = await postJson('/api/cliproxy/auth/codex/start-url', {
accountId: 'existing@example.com',
});
expect(startResponse.status).toBe(200);
fs.writeFileSync(
tokenPath,
JSON.stringify({ type: 'codex', email: 'existing@example.com', version: 2 }),
'utf8'
);
const statusResponse = await getJson(
'/api/cliproxy/auth/codex/status?state=state-targeted-rewrite'
);
expect(statusResponse.status).toBe(200);
expect(statusResponse.body).toEqual({
status: 'ok',
account: {
id: 'existing@example.com',
email: 'existing@example.com',
nickname: 'work',
provider: 'codex',
isDefault: true,
},
});
});
it('registers the new account before reporting polled auth success', async () => {
mockFetch([
{
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'bun:test';
import {
getKiroStartIDCValidationError,
getReauthAccountTarget,
getStartAuthFailureMessage,
getStartAuthNicknameError,
getStartUrlUnsupportedReason,
@@ -17,6 +18,7 @@ describe('cliproxy-auth-routes start-url guard', () => {
"Provider 'codebuddy' uses Device Code flow"
);
expect(getStartUrlUnsupportedReason('kilo')).toContain("Provider 'kilo' uses Device Code flow");
expect(getStartUrlUnsupportedReason('qoder')).toContain("Provider 'qoder' uses Device Code flow");
});
it('allows Cursor browser URL auth on start-url', () => {
@@ -142,3 +144,27 @@ describe('cliproxy-auth-routes nickname validation', () => {
).toBeNull();
});
});
describe('cliproxy-auth-routes reauth account targeting', () => {
const existingAccounts = [
{ id: 'codex-user@example.com', nickname: 'work' },
{ id: 'codex-personal@example.com', nickname: 'personal' },
];
it('does not require a target for normal add-account auth', () => {
expect(getReauthAccountTarget(undefined, existingAccounts)).toEqual({});
expect(getReauthAccountTarget('', existingAccounts)).toEqual({});
});
it('resolves an existing account target for reauth', () => {
expect(getReauthAccountTarget('codex-user@example.com', existingAccounts)).toEqual({
account: { id: 'codex-user@example.com', nickname: 'work' },
});
});
it('rejects unknown account targets instead of falling back to ambiguous registration', () => {
expect(getReauthAccountTarget('missing', existingAccounts)).toEqual({
error: "Account 'missing' not found for this provider",
});
});
});
@@ -112,6 +112,21 @@ describe('cliproxy usage syncer', () => {
const snapshotPath = path.join(ccsDir, 'cache', 'cliproxy-usage', 'latest.json');
expect(fs.existsSync(snapshotPath)).toBe(true);
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')) as {
details: Array<Record<string, unknown>>;
};
expect(snapshot.details[0]).not.toHaveProperty('source');
expect(snapshot.details[0]).not.toHaveProperty('authIndex');
if (process.platform !== 'win32') {
const cacheDir = path.join(ccsDir, 'cache');
const cliproxyCacheDir = path.dirname(snapshotPath);
expect(fs.statSync(ccsDir).mode & 0o777).toBe(0o700);
expect(fs.statSync(cacheDir).mode & 0o777).toBe(0o700);
expect(fs.statSync(cliproxyCacheDir).mode & 0o777).toBe(0o700);
expect(fs.statSync(snapshotPath).mode & 0o777).toBe(0o600);
}
const cached = await runWithScopedConfigDir(ccsDir, async () => {
return await loadCachedCliproxyData();
});
@@ -110,6 +110,8 @@ describe('cliproxy usage transformer', () => {
const flat = extractCliproxyUsageHistoryDetails(sampleResponse);
expect(flat).toHaveLength(4);
expect(flat[0].provider).toBe('google');
expect(flat[0]).not.toHaveProperty('source');
expect(flat[0]).not.toHaveProperty('authIndex');
expect(
flat.some(
(entry) => entry.failed === true && entry.inputTokens === 40 && entry.outputTokens === 10
@@ -160,8 +162,8 @@ describe('cliproxy usage transformer', () => {
const legacyDetail = normalizeCliproxyUsageHistoryDetail({
model: incoming.model,
timestamp: incoming.timestamp,
source: incoming.source,
authIndex: incoming.authIndex,
source: 'account-a',
authIndex: '0',
inputTokens: incoming.inputTokens,
outputTokens: incoming.outputTokens,
cacheReadTokens: incoming.cacheReadTokens,
@@ -170,6 +172,8 @@ describe('cliproxy usage transformer', () => {
expect(legacyDetail?.requestCount).toBe(1);
expect(Number.isFinite(legacyDetail?.cost)).toBe(true);
expect(legacyDetail).not.toHaveProperty('source');
expect(legacyDetail).not.toHaveProperty('authIndex');
const merged = mergeCliproxyUsageHistoryDetails([legacyDetail!], [incoming]);
expect(merged).toHaveLength(1);
@@ -177,6 +181,19 @@ describe('cliproxy usage transformer', () => {
expect(merged[0].cost).toBe(incoming.cost);
});
it('strips legacy account identifiers when merging persisted history', () => {
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
const legacyDetail = {
...details[0],
source: 'user@example.com',
authIndex: 'auth-file-7',
};
const merged = mergeCliproxyUsageHistoryDetails([legacyDetail], []);
expect(merged[0]).not.toHaveProperty('source');
expect(merged[0]).not.toHaveProperty('authIndex');
});
it('preserves legitimate duplicate requests when the incoming batch has more occurrences', () => {
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
const repeated = [details[0], { ...details[0] }];
@@ -311,6 +311,28 @@ describe('codex native usage collector', () => {
expect(entries).toHaveLength(2);
});
it('writes native usage caches with owner-only permissions', async () => {
if (process.platform === 'win32') return;
writeCodexRollout(tempRoot);
const cacheDir = getCacheDir();
const previousUmask = process.umask(0o022);
try {
await scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
cacheDir,
});
} finally {
process.umask(previousUmask);
}
const cachePath = path.join(cacheDir, 'codex-native-usage-v1.json');
expect(fs.statSync(cacheDir).mode & 0o777).toBe(0o700);
expect(fs.statSync(cachePath).mode & 0o777).toBe(0o600);
});
it('keeps default and include-cliproxy cache entries separate', async () => {
writeCodexRollout(tempRoot, { modelProvider: 'cliproxy' });
const cacheDir = getCacheDir();
@@ -510,6 +510,17 @@ describe('Cursor Routes Logic', () => {
expect(json.current).toBe('gpt-5.3-codex');
});
it('POST /api/cursor/probe rejects cross-origin requests', async () => {
const res = await fetch(`${baseUrl}/api/cursor/probe`, {
method: 'POST',
headers: { Origin: 'https://attacker.example' },
});
expect(res.status).toBe(403);
const json = (await res.json()) as { error?: string };
expect(json.error).toContain('Cross-origin probe requests are not allowed.');
});
it('POST /api/cursor/probe returns auth failure when credentials are missing', async () => {
const res = await fetch(`${baseUrl}/api/cursor/probe`, { method: 'POST' });
expect(res.status).toBe(401);
@@ -340,4 +340,31 @@ describe('image-analysis routes', () => {
error: 'Profile mapping for "codexProfile" references an unknown backend.',
});
});
it('rejects unsupported provider backends and preserves existing configuration', async () => {
const putResponse = await fetch(`${baseUrl}/api/image-analysis`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
providerModels: {
gemini: 'gemini-3-flash-preview',
evil: 'vision-model',
},
fallbackBackend: 'gemini',
}),
});
expect(putResponse.status).toBe(400);
expect(await putResponse.json()).toEqual({
error: 'Unsupported provider backend "evil".',
});
const getResponse = await fetch(`${baseUrl}/api/image-analysis`);
expect(getResponse.status).toBe(200);
const payload = await getResponse.json();
expect(payload.config.providerModels).toMatchObject({
gemini: 'gemini-3-flash-preview',
ghcp: 'claude-haiku-4.5',
});
});
});
@@ -6,11 +6,20 @@ import profileRoutes from '../../../src/web-server/routes/profile-routes';
describe('profile-routes local runtime readiness', () => {
let server: Server;
let baseUrl = '';
let forcedRemoteAddress = '127.0.0.1';
let originalDashboardAuthEnabled: string | undefined;
const originalFetch = globalThis.fetch;
beforeAll(async () => {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
Object.defineProperty(req.socket, 'remoteAddress', {
value: forcedRemoteAddress,
configurable: true,
});
next();
});
app.use('/api/profiles', profileRoutes);
await new Promise<void>((resolve, reject) => {
@@ -36,6 +45,10 @@ describe('profile-routes local runtime readiness', () => {
});
beforeEach(() => {
originalDashboardAuthEnabled = process.env.CCS_DASHBOARD_AUTH_ENABLED;
process.env.CCS_DASHBOARD_AUTH_ENABLED = 'false';
forcedRemoteAddress = '127.0.0.1';
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
@@ -73,6 +86,12 @@ describe('profile-routes local runtime readiness', () => {
afterEach(() => {
globalThis.fetch = originalFetch;
if (originalDashboardAuthEnabled !== undefined) {
process.env.CCS_DASHBOARD_AUTH_ENABLED = originalDashboardAuthEnabled;
} else {
delete process.env.CCS_DASHBOARD_AUTH_ENABLED;
}
});
it('reports local runtimes as ready when their endpoints respond with models', async () => {
@@ -103,6 +122,30 @@ describe('profile-routes local runtime readiness', () => {
);
});
it('blocks remote readiness probes when dashboard auth is disabled', async () => {
let probeCount = 0;
forcedRemoteAddress = '10.10.0.24';
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
if (url.startsWith(baseUrl)) {
return originalFetch(input);
}
probeCount += 1;
return new Response(JSON.stringify({ models: [{ name: 'gemma4:e4b' }] }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}) as typeof fetch;
const response = await fetch(`${baseUrl}/api/profiles/local-runtime-readiness`);
expect(response.status).toBe(403);
expect(await response.json()).toEqual({
error: 'Local runtime readiness requires localhost access when dashboard auth is disabled.',
});
expect(probeCount).toBe(0);
});
it('reports setup guidance when local endpoints are unavailable', async () => {
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
@@ -7,16 +7,16 @@ describe('route target parsing', () => {
it('accepts valid target values', () => {
expect(parseProfileTarget('claude')).toBe('claude');
expect(parseProfileTarget('DROID')).toBe('droid');
expect(parseProfileTarget('codex')).toBe('codex');
expect(parseVariantTarget(' claude ')).toBe('claude');
expect(parseVariantTarget('droid')).toBe('droid');
expect(parseVariantTarget(' codex ')).toBe('codex');
});
it('returns null for invalid target values', () => {
expect(parseProfileTarget('glm')).toBeNull();
expect(parseProfileTarget('codex')).toBeNull();
expect(parseProfileTarget('')).toBeNull();
expect(parseVariantTarget('factory')).toBeNull();
expect(parseVariantTarget('codex')).toBeNull();
expect(parseVariantTarget(' ')).toBeNull();
});
@@ -83,11 +83,7 @@ function writeAssistantEntriesToDir(baseClaudeDir: string, entries: AssistantFix
},
});
fs.writeFileSync(
path.join(projectDir, `${entry.sessionId}.jsonl`),
`${line}\n`,
'utf-8'
);
fs.writeFileSync(path.join(projectDir, `${entry.sessionId}.jsonl`), `${line}\n`, 'utf-8');
}
}
@@ -222,7 +218,10 @@ describe('usage handlers semantics', () => {
res as never
);
const payload = res.payload as { success: boolean; data: Array<{ hour: string; requests: number }> };
const payload = res.payload as {
success: boolean;
data: Array<{ hour: string; requests: number }>;
};
const targetHour = payload.data.find((row) => row.hour === '2026-03-02 10:00');
expect(targetHour?.requests).toBe(3);
@@ -415,6 +414,19 @@ describe('usage handlers semantics', () => {
});
});
it('rejects non-string profile filters as validation errors', async () => {
for (const profile of [['work', 'default'], { name: 'work' }]) {
const res = createMockResponse();
await handlers.handleSummary({ query: { profile } } as never, res as never);
expect(res.statusCode).toBe(400);
expect(res.payload).toMatchObject({
success: false,
error: 'Invalid profile filter',
});
}
});
it('rejects reversed date ranges before computing summary totals', async () => {
const res = createMockResponse();