mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 18:22:48 +03:00
Compare commits
1 Commits
release/v3
...
fix/codeql
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc7a9c369c |
@@ -0,0 +1 @@
|
||||
- **docs:** add management authentication terminology guide ([#7786](https://github.com/diegosouzapw/OmniRoute/issues/7786))
|
||||
41
.claude/worktrees/feat-7786/docs/guides/MANAGEMENT-AUTH.md
Normal file
41
.claude/worktrees/feat-7786/docs/guides/MANAGEMENT-AUTH.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Management Authentication
|
||||
|
||||
OmniRoute uses four distinct credential families for management access. This guide
|
||||
distinguishes them by purpose, scope, and locality.
|
||||
|
||||
| Credential | Scope | Locality | Use Case |
|
||||
|-------------------------|--------------------|---------------|-----------------------------------|
|
||||
| Dashboard JWT session | Full management | Localhost | Web dashboard login |
|
||||
| CLI machine-id token | Full management | Per-machine | `omniroute` CLI commands |
|
||||
| Scoped `oma_` token | Configurable scope | External | Automation / CI / API access |
|
||||
| Manage-scope API key | `manage` scope | External | Management API calls |
|
||||
|
||||
## Dashboard JWT Session
|
||||
|
||||
Generated on dashboard login (`/api/auth/login`). Stored in HTTP-only cookie.
|
||||
Valid for the session duration. Cannot be used from external hosts.
|
||||
|
||||
## CLI Machine-ID Token
|
||||
|
||||
Created by `omniroute auth login` on first use. Stored in `~/.omniroute/auth.json`.
|
||||
Used by the CLI for all management operations. Tied to the machine identity.
|
||||
|
||||
## Scoped `oma_` Access Token
|
||||
|
||||
Created via dashboard or CLI with configurable scopes (e.g., `manage`, `read`).
|
||||
Format: `oma_<random-hex>`. Used for programmatic access from external systems.
|
||||
|
||||
## Manage-Scope API Key
|
||||
|
||||
Standard API key with the `manage` scope enabled. Created in dashboard API Keys page.
|
||||
Used for management API calls from external hosts.
|
||||
|
||||
## Header Examples
|
||||
|
||||
```
|
||||
Authorization: Bearer oma_abc123def456
|
||||
Authorization: Bearer <standard-api-key-with-manage-scope>
|
||||
Cookie: omniroute_session=<jwt-token>
|
||||
```
|
||||
|
||||
See `docs/reference/API_REFERENCE.md` for endpoint-specific auth requirements.
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, it } from "node:test";
|
||||
import { ok } from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
describe("Management auth documentation (#7786)", () => {
|
||||
const docPath = "docs/guides/MANAGEMENT-AUTH.md";
|
||||
const content = readFileSync(docPath, "utf-8");
|
||||
|
||||
it("exists and has content", () => {
|
||||
ok(content.length > 500, "should have substantial content");
|
||||
ok(content.includes("Dashboard JWT session"));
|
||||
ok(content.includes("CLI machine-id token"));
|
||||
ok(content.includes("oma_"));
|
||||
});
|
||||
|
||||
it("documents all four credential families", () => {
|
||||
const families = ["Dashboard JWT", "CLI machine-id", "oma_", "Manage-scope"];
|
||||
for (const f of families) {
|
||||
ok(content.includes(f), `should document ${f}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("mentions relevant auth header examples", () => {
|
||||
ok(content.includes("Authorization"));
|
||||
ok(content.includes("Bearer"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(infra):** add systemd autostart unit for Linux ([#8635](https://github.com/diegosouzapw/OmniRoute/issues/8635))
|
||||
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=OmniRoute AI Proxy
|
||||
After=network.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=$(which omniroute) start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=NODE_ENV=production
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=full
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, it } from "node:test";
|
||||
import { ok } from "node:assert/strict";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
|
||||
describe("Systemd autostart (#8635)", () => {
|
||||
const svcPath = "contrib/systemd/omniroute.service";
|
||||
const content = readFileSync(svcPath, "utf-8");
|
||||
|
||||
it("service file exists", () => {
|
||||
ok(existsSync(svcPath));
|
||||
ok(content.length > 200);
|
||||
});
|
||||
|
||||
it("defines required systemd sections", () => {
|
||||
ok(content.includes("[Unit]"));
|
||||
ok(content.includes("[Service]"));
|
||||
ok(content.includes("[Install]"));
|
||||
});
|
||||
|
||||
it("specifies WantedBy=default.target", () => {
|
||||
ok(content.includes("WantedBy=default.target"));
|
||||
});
|
||||
});
|
||||
1
.eslintcache-probe
Normal file
1
.eslintcache-probe
Normal file
File diff suppressed because one or more lines are too long
4
.fakebin-9475/npm
Executable file
4
.fakebin-9475/npm
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
if [ "$1" = "view" ]; then echo "3.8.99"; exit 0; fi
|
||||
if [ "$1" = "install" ]; then echo "added 1 package"; exit 0; fi
|
||||
exit 0
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -159,7 +159,6 @@ vscode-extension/
|
||||
|
||||
# Empty/dangling files
|
||||
typescript
|
||||
/MAX
|
||||
|
||||
# Gemini Antigravity agent data
|
||||
.gemini/
|
||||
@@ -204,9 +203,6 @@ scripts/i18n/_pending-keys.json
|
||||
.claude/worktrees/
|
||||
.codegraph/
|
||||
|
||||
# Test executable shims belong in the OS temporary directory, not the repository root
|
||||
/.fakebin-*/
|
||||
|
||||
# Fumadocs generated source
|
||||
.source/
|
||||
|
||||
@@ -266,7 +262,6 @@ _artifacts/ # release-green artifacts
|
||||
# ESLint file cache (npm run lint --cache / complexity ratchets)
|
||||
.eslintcache
|
||||
.eslintcache-complexity
|
||||
/.eslintcache-*
|
||||
|
||||
|
||||
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
|
||||
|
||||
1
changelog.d/features/8862-novita-model-catalog.md
Normal file
1
changelog.d/features/8862-novita-model-catalog.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Providers**: expands the Novita AI catalog from a single Llama 3.1 8B entry to 19 curated serving models (DeepSeek V4, Kimi K3, GLM 5.2, MiniMax M3, Qwen3.7 Max, Qwen3 Coder 480B, MiMo V2.5 Pro, gpt-oss-120b, Gemma 4 31B and more), each carrying its real context window, output cap and reasoning flag from the live `/openai/v1/models` listing, and each vision flag confirmed by an actual image request rather than the listing's self-reported modalities
|
||||
1
changelog.d/features/adobe-firefly-reference-images.md
Normal file
1
changelog.d/features/adobe-firefly-reference-images.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(adobe-firefly):** reference-image attach for generate + OpenAI `/v1/images/edits` support (follow-up to #8006). Uploads sources to Firefly storage (`POST /v2/storage/image`), then submits `referenceBlobs` on 3P generate-async (nano multi-ref `usage:general`; gpt-image `usage:subject`). Wire matches live `firefly.adobe.com` captures. Also routes built-in edits to the same path (up to 4 refs).
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(models):** `/v1/models` now publishes one contiguous provider-grouped block per provider instead of interleaved fragments. The catalog is assembled by many independent push loops (auto-combos, named combos, static registry, codex-native, synced, OpenRouter, specialty, custom, alias-backed, connection-fallback), so one provider's models previously landed in several separated blocks. A single stable, provider-grouped sort is applied at serialization, keyed by `owned_by` (canonical owner identity) rather than the model-id prefix — so a single routable public prefix that differs from its owner (e.g. no-auth OpenCode publishing `oc/<model>` while keeping `owned_by: "opencode"`) stays contiguous. Combos are pinned first (preserving #4164); then providers in registry precedence (OAuth → NoAuth → API-key); then unknown providers in locale-independent code-unit order. The sort is stable and pure (reorders rows only, no mutation, no DB/IO), preserving combo `sort_order`, connection priority, custom append-order, and equal-id audio twins.
|
||||
@@ -634,17 +634,17 @@ Two binaries are exposed in `package.json` → `bin`:
|
||||
|
||||
## 7. `tests/`
|
||||
|
||||
| Directory | Type |
|
||||
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `tests/unit/` | Unit tests via Node native test runner (1821 files, plus `api/`, `auth/`, `authz/` subdirs) |
|
||||
| `tests/integration/` | Cross-module + DB-state tests |
|
||||
| `tests/e2e/` | Playwright UI tests |
|
||||
| `tests/protocols-e2e/` | MCP/A2A protocol e2e |
|
||||
| `tests/translator/` | Translator-specific tests |
|
||||
| `tests/security/` | Security regressions |
|
||||
| `tests/load/` | Load / stress tests |
|
||||
| `tests/golden-set/` | Reference outputs for translator regressions |
|
||||
| `tests/helpers/`, `tests/fixtures/`, `tests/manual/` | Support |
|
||||
| Directory | Type |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
|
||||
| `tests/unit/` | Unit tests via Node native test runner (1821 files, plus `api/`, `auth/`, `authz/` subdirs) |
|
||||
| `tests/integration/` | Cross-module + DB-state tests |
|
||||
| `tests/e2e/` | Playwright UI tests |
|
||||
| `tests/protocols-e2e/` | MCP/A2A protocol e2e |
|
||||
| `tests/translator/` | Translator-specific tests |
|
||||
| `tests/security/` | Security regressions |
|
||||
| `tests/load/` | Load / stress tests |
|
||||
| `tests/golden-set/` | Reference outputs for translator regressions |
|
||||
| `tests/helpers/`, `tests/fixtures/`, `tests/manual/`, `tests/scratch_test.mjs` | Support |
|
||||
|
||||
Common commands:
|
||||
|
||||
|
||||
@@ -634,17 +634,17 @@ Dwa binaria są wystawione w `package.json` → `bin`:
|
||||
|
||||
## 7. `tests/`
|
||||
|
||||
| Katalog | Typ |
|
||||
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `tests/unit/` | Testy jednostkowe przez Node native test runner (1821 plików, plus `api/`, `auth/`, `authz/` podkatalogi) |
|
||||
| `tests/integration/` | Testy cross-module + stan DB |
|
||||
| `tests/e2e/` | Playwright UI tests |
|
||||
| `tests/protocols-e2e/` | MCP/A2A protocol e2e |
|
||||
| `tests/translator/` | Translator-specific tests |
|
||||
| `tests/security/` | Security regressions |
|
||||
| `tests/load/` | Load / stress tests |
|
||||
| `tests/golden-set/` | Reference outputs for translator regressions |
|
||||
| `tests/helpers/`, `tests/fixtures/`, `tests/manual/` | Support |
|
||||
| Katalog | Typ |
|
||||
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
|
||||
| `tests/unit/` | Testy jednostkowe przez Node native test runner (1821 plików, plus `api/`, `auth/`, `authz/` podkatalogi) |
|
||||
| `tests/integration/` | Testy cross-module + stan DB |
|
||||
| `tests/e2e/` | Playwright UI tests |
|
||||
| `tests/protocols-e2e/` | MCP/A2A protocol e2e |
|
||||
| `tests/translator/` | Translator-specific tests |
|
||||
| `tests/security/` | Security regressions |
|
||||
| `tests/load/` | Load / stress tests |
|
||||
| `tests/golden-set/` | Reference outputs for translator regressions |
|
||||
| `tests/helpers/`, `tests/fixtures/`, `tests/manual/`, `tests/scratch_test.mjs` | Support |
|
||||
|
||||
Common commands:
|
||||
|
||||
|
||||
@@ -610,17 +610,17 @@ bin/
|
||||
|
||||
## 7. `tests/`
|
||||
|
||||
| 目录 | 类型 |
|
||||
| ---------------------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| `tests/unit/` | Node 原生测试运行器的单元测试(1821 个文件,含 `api/`、`auth/`、`authz/` 子目录) |
|
||||
| `tests/integration/` | 跨模块 + DB 状态测试 |
|
||||
| `tests/e2e/` | Playwright UI 测试 |
|
||||
| `tests/protocols-e2e/` | MCP/A2A 协议端到端 |
|
||||
| `tests/translator/` | 翻译器专用测试 |
|
||||
| `tests/security/` | 安全回归测试 |
|
||||
| `tests/load/` | 负载 / 压力测试 |
|
||||
| `tests/golden-set/` | 翻译器回归参考输出 |
|
||||
| `tests/helpers/`、`tests/fixtures/`、`tests/manual/` | 支撑 |
|
||||
| 目录 | 类型 |
|
||||
| -------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| `tests/unit/` | Node 原生测试运行器的单元测试(1821 个文件,含 `api/`、`auth/`、`authz/` 子目录)|
|
||||
| `tests/integration/` | 跨模块 + DB 状态测试 |
|
||||
| `tests/e2e/` | Playwright UI 测试 |
|
||||
| `tests/protocols-e2e/` | MCP/A2A 协议端到端 |
|
||||
| `tests/translator/` | 翻译器专用测试 |
|
||||
| `tests/security/` | 安全回归测试 |
|
||||
| `tests/load/` | 负载 / 压力测试 |
|
||||
| `tests/golden-set/` | 翻译器回归参考输出 |
|
||||
| `tests/helpers/`、`tests/fixtures/`、`tests/manual/`、`tests/scratch_test.mjs` | 支撑 |
|
||||
|
||||
常用命令:
|
||||
|
||||
|
||||
@@ -627,17 +627,17 @@ bin/
|
||||
|
||||
## 7. `tests/`
|
||||
|
||||
| 目錄 | 類型 |
|
||||
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `tests/unit/` | 透過 Node 原生測試執行器的單元測試(1821 個檔案,加上 `api/`、`auth/`、`authz/` 子目錄) |
|
||||
| `tests/integration/` | 跨模組 + 資料庫狀態測試 |
|
||||
| `tests/e2e/` | Playwright UI 測試 |
|
||||
| `tests/protocols-e2e/` | MCP/A2A 協定 e2e 測試 |
|
||||
| `tests/translator/` | 翻譯器專用測試 |
|
||||
| `tests/security/` | 安全性回歸測試 |
|
||||
| `tests/load/` | 負載/壓力測試 |
|
||||
| `tests/golden-set/` | 翻譯器回歸測試的參考輸出 |
|
||||
| `tests/helpers/`、`tests/fixtures/`、`tests/manual/` | 支援 |
|
||||
| 目錄 | 類型 |
|
||||
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
|
||||
| `tests/unit/` | 透過 Node 原生測試執行器的單元測試(1821 個檔案,加上 `api/`、`auth/`、`authz/` 子目錄) |
|
||||
| `tests/integration/` | 跨模組 + 資料庫狀態測試 |
|
||||
| `tests/e2e/` | Playwright UI 測試 |
|
||||
| `tests/protocols-e2e/` | MCP/A2A 協定 e2e 測試 |
|
||||
| `tests/translator/` | 翻譯器專用測試 |
|
||||
| `tests/security/` | 安全性回歸測試 |
|
||||
| `tests/load/` | 負載/壓力測試 |
|
||||
| `tests/golden-set/` | 翻譯器回歸測試的參考輸出 |
|
||||
| `tests/helpers/`、`tests/fixtures/`、`tests/manual/`、`tests/scratch_test.mjs` | 支援 |
|
||||
|
||||
常用命令:
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { BaseExecutor, type ExecuteInput, type ExecutorExecuteResult } from "./base.ts";
|
||||
import { makeExecutorErrorResult as makeErrorResult } from "../utils/error.ts";
|
||||
import { initTinyCmsWasm, generateSecurePayload } from "./tinycmsSigner.ts";
|
||||
@@ -28,9 +30,9 @@ async function fetchChallenge(uuid: string): Promise<any> {
|
||||
const res = await fetch(CHALLENGE_URL, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"uuid": uuid,
|
||||
uuid: uuid,
|
||||
"x-origin": "https://gov.freegpt.win",
|
||||
"Accept": "application/json",
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
||||
},
|
||||
});
|
||||
@@ -67,10 +69,11 @@ export class TinyCmsExecutor extends BaseExecutor {
|
||||
const challengeObj = await fetchChallenge(uuid);
|
||||
|
||||
const timestamp = Date.now().toString();
|
||||
const nonceJs =
|
||||
typeof crypto !== "undefined" && crypto.randomUUID
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
// Security context: this nonce is signed into `x-secure-signature` and
|
||||
// reused as the session id, so it must be unpredictable. `node:crypto`
|
||||
// randomUUID() is always available on the supported runtime — never fall
|
||||
// back to Math.random() (CodeQL js/insecure-randomness).
|
||||
const nonceJs = randomUUID();
|
||||
|
||||
const securePayload = generateSecurePayload(
|
||||
uuid,
|
||||
@@ -122,12 +125,7 @@ export class TinyCmsExecutor extends BaseExecutor {
|
||||
transformedBody: bodyObj,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return makeErrorResult(
|
||||
500,
|
||||
`TinyCMS Error: ${err.message}`,
|
||||
body,
|
||||
CHAT_URL
|
||||
);
|
||||
return makeErrorResult(500, `TinyCMS Error: ${err.message}`, body, CHAT_URL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,12 +237,15 @@ function trustedEnvironmentText(parsed: CodexParsedRequest): string {
|
||||
}
|
||||
|
||||
function decodeXmlText(value: string): string {
|
||||
// `&` MUST be decoded last: decoding it first produces a bare `&` that the
|
||||
// later passes re-consume, so `&quot;` would collapse to `"` instead of the
|
||||
// literal `"` (double-unescape — CodeQL js/double-escaping).
|
||||
return value
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll(""", '"')
|
||||
.replaceAll("'", "'");
|
||||
.replaceAll("'", "'")
|
||||
.replaceAll("&", "&");
|
||||
}
|
||||
|
||||
function uniqueAbsolutePaths(values: string[], field: string): string[] {
|
||||
|
||||
26
scripts/ad-hoc/delete-non-green-runs.mjs
Normal file
26
scripts/ad-hoc/delete-non-green-runs.mjs
Normal file
@@ -0,0 +1,26 @@
|
||||
import { execSync } from "child_process";
|
||||
|
||||
try {
|
||||
console.log("Fetching workflow runs...");
|
||||
const output = execSync("gh run list --limit 100 --json status,conclusion,databaseId", {
|
||||
encoding: "utf8",
|
||||
});
|
||||
const runs = JSON.parse(output);
|
||||
|
||||
console.log(`Found ${runs.length} runs.`);
|
||||
let count = 0;
|
||||
for (const run of runs) {
|
||||
if (run.conclusion !== "success") {
|
||||
console.log(`Deleting run ID ${run.databaseId} with conclusion '${run.conclusion}'...`);
|
||||
try {
|
||||
execSync(`gh run delete ${run.databaseId}`);
|
||||
count++;
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete run ID ${run.databaseId}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`Deleted ${count} runs successfully.`);
|
||||
} catch (error) {
|
||||
console.error("Error executing script:", error);
|
||||
}
|
||||
58
scripts/ad-hoc/fetch_prs.js
Normal file
58
scripts/ad-hoc/fetch_prs.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const REPO = "diegosouzapw/OmniRoute";
|
||||
const artifactsDir =
|
||||
process.env.ARTIFACTS_DIR ||
|
||||
path.join(process.cwd(), "artifacts");
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 1. Get PR numbers
|
||||
console.log("Fetching open PR numbers...");
|
||||
const prNumbersOutput = execSync(
|
||||
`gh pr list --repo ${REPO} --state open --limit 500 --json number --jq '.[].number'`,
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
const prNumbers = prNumbersOutput.trim().split("\n").map(Number).filter(Boolean);
|
||||
console.log(`Found ${prNumbers.length} open PRs:`, prNumbers);
|
||||
|
||||
if (!fs.existsSync(artifactsDir)) {
|
||||
fs.mkdirSync(artifactsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 2. Fetch metadata and diff for each PR
|
||||
for (const prNum of prNumbers) {
|
||||
console.log(`\n--- Fetching PR #${prNum} ---`);
|
||||
|
||||
// Metadata
|
||||
try {
|
||||
const metadataCmd = `gh pr view ${prNum} --repo ${REPO} --json number,title,author,headRefName,baseRefName,body,createdAt,additions,deletions,files`;
|
||||
const metadataJson = execSync(metadataCmd, { encoding: "utf-8" });
|
||||
const metadataPath = path.join(artifactsDir, `pr_${prNum}_meta.json`);
|
||||
fs.writeFileSync(metadataPath, metadataJson);
|
||||
console.log(`Saved metadata to ${metadataPath}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch metadata for PR #${prNum}:`, err.message);
|
||||
}
|
||||
|
||||
// Diff
|
||||
try {
|
||||
const diffCmd = `gh pr diff ${prNum} --repo ${REPO}`;
|
||||
const diffText = execSync(diffCmd, { encoding: "utf-8", maxBuffer: 100 * 1024 * 1024 });
|
||||
const diffPath = path.join("/tmp", `pr${prNum}.diff`);
|
||||
fs.writeFileSync(diffPath, diffText);
|
||||
console.log(`Saved diff to ${diffPath} (Size: ${diffText.length} bytes)`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch diff for PR #${prNum}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\nAll PR data fetched successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error during PR fetching:", error);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
280
scripts/ad-hoc/resolve_all_conflicts.js
Normal file
280
scripts/ad-hoc/resolve_all_conflicts.js
Normal file
@@ -0,0 +1,280 @@
|
||||
import fs from "fs";
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
|
||||
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
||||
|
||||
const filesToCheckoutOurs = [
|
||||
".source/browser.ts",
|
||||
".source/server.ts",
|
||||
"package-lock.json",
|
||||
"electron/package-lock.json",
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/page.tsx",
|
||||
"src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx",
|
||||
"src/lib/db/contextHandoffs.ts",
|
||||
"src/app/api/keys/groups/[id]/keys/route.ts",
|
||||
"src/app/api/keys/groups/[id]/permissions/route.ts",
|
||||
"src/app/api/keys/groups/[id]/route.ts",
|
||||
"src/app/api/keys/groups/route.ts",
|
||||
"src/app/api/middleware/hooks/[name]/route.ts",
|
||||
"src/app/api/middleware/hooks/route.ts",
|
||||
"src/app/api/relay/tokens/[id]/route.ts",
|
||||
"src/app/api/relay/tokens/route.ts",
|
||||
"src/app/api/playground/simulate-route/route.ts",
|
||||
];
|
||||
|
||||
function runCmd(cmd) {
|
||||
console.log(`Running: ${cmd}`);
|
||||
return execSync(cmd, { cwd: projectRoot, encoding: "utf-8" });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 1. Checkout ours for the files where HEAD is the preferred up-to-date state
|
||||
for (const file of filesToCheckoutOurs) {
|
||||
try {
|
||||
runCmd(`git checkout --ours "${file}"`);
|
||||
runCmd(`git add "${file}"`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to checkout --ours for ${file}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Resolve .dockerignore (keep release/v3.8.4 doc rules)
|
||||
try {
|
||||
runCmd("git checkout --theirs .dockerignore");
|
||||
runCmd("git add .dockerignore");
|
||||
} catch (err) {
|
||||
console.error("Failed to resolve .dockerignore:", err.message);
|
||||
}
|
||||
|
||||
// 3. Resolve docs/reference/ENVIRONMENT.md (keep release/v3.8.4 table formatting)
|
||||
try {
|
||||
runCmd("git checkout --theirs docs/reference/ENVIRONMENT.md");
|
||||
runCmd("git add docs/reference/ENVIRONMENT.md");
|
||||
} catch (err) {
|
||||
console.error("Failed to resolve docs/reference/ENVIRONMENT.md:", err.message);
|
||||
}
|
||||
|
||||
// 4. Resolve open-sse/executors/index.ts (keep both ClaudeWebExecutor and InnerAiExecutor)
|
||||
const execIndexFile = path.join(projectRoot, "open-sse/executors/index.ts");
|
||||
if (fs.existsSync(execIndexFile)) {
|
||||
let content = fs.readFileSync(execIndexFile, "utf-8");
|
||||
|
||||
// Resolve imports conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nimport \{ ClaudeWebExecutor \} from "\.\/claude-web\.ts";\r?\n=======\r?\nimport \{ InnerAiExecutor \} from "\.\/inner-ai\.ts";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { ClaudeWebExecutor } from "./claude-web.ts";\nimport { InnerAiExecutor } from "./inner-ai.ts";'
|
||||
);
|
||||
|
||||
// Resolve executor registration conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+"claude-web": new ClaudeWebExecutor\(\),\r?\n\s+"cw-web": new ClaudeWebExecutor\(\), \/\/ Alias\r?\n=======\r?\n\s+"inner-ai": new InnerAiExecutor\(\),\r?\n\s+"in-ai": new InnerAiExecutor\(\), \/\/ Alias\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' "claude-web": new ClaudeWebExecutor(),\n "cw-web": new ClaudeWebExecutor(), // Alias\n "inner-ai": new InnerAiExecutor(),\n "in-ai": new InnerAiExecutor(), // Alias'
|
||||
);
|
||||
|
||||
fs.writeFileSync(execIndexFile, content);
|
||||
runCmd("git add open-sse/executors/index.ts");
|
||||
}
|
||||
|
||||
// 7. Resolve src/app/api/providers/[id]/models/route.ts (combine imports)
|
||||
const modelsRoute = path.join(projectRoot, "src/app/api/providers/[id]/models/route.ts");
|
||||
if (fs.existsSync(modelsRoute)) {
|
||||
let content = fs.readFileSync(modelsRoute, "utf-8");
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n=======\r?\nimport \{ sanitizeErrorMessage \} from "@omniroute\/open-sse\/utils\/error";\r?\nimport \{ getStaticQoderModels \} from "@omniroute\/open-sse\/services\/qoderCli\.ts";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";\nimport { getStaticQoderModels } from "@omniroute/open-sse/services/qoderCli.ts";'
|
||||
);
|
||||
fs.writeFileSync(modelsRoute, content);
|
||||
runCmd("git add src/app/api/providers/[id]/models/route.ts");
|
||||
}
|
||||
|
||||
// 8. Resolve src/sse/handlers/chat.ts
|
||||
const sseChat = path.join(projectRoot, "src/sse/handlers/chat.ts");
|
||||
if (fs.existsSync(sseChat)) {
|
||||
let content = fs.readFileSync(sseChat, "utf-8");
|
||||
|
||||
// Resolve comment / modelStr conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n=======\r?\n\s+\/\/ `let` because the middleware-hook pipeline \(line ~319\) may reassign this\r?\n\s+\/\/ when a hook rewrites the target model\. Previously declared `const`, which\r?\n\s+\/\/ broke turbopack\/strict-mode builds \(PR #2670 regression\)\.\r?\n>>>>>>> release\/v3\.8\.4\r?\n\s+let modelStr = body\.model;/g,
|
||||
" // `let` because the middleware-hook pipeline (line ~319) may reassign this\n // when a hook rewrites the target model. Previously declared `const`, which\n // broke turbopack/strict-mode builds (PR [PR #2670](file:///home/diegosouzapw/dev/proxys/OmniRoute/package.json#L2670) regression).\n let modelStr = body.model;"
|
||||
);
|
||||
|
||||
// Resolve trafficType / modelAbortSignal conflict (1st occurrence)
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+trafficType\?: "production" \| "shadow";\r?\n=======\r?\n\s+modelAbortSignal\?: AbortSignal \| null;\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' trafficType?: "production" | "shadow";\n modelAbortSignal?: AbortSignal | null;'
|
||||
);
|
||||
|
||||
fs.writeFileSync(sseChat, content);
|
||||
runCmd("git add src/sse/handlers/chat.ts");
|
||||
}
|
||||
|
||||
// 9. Resolve bin/cli/tray/autostart.mjs (keep execFileSync, combine ignoreFailure and systemd CI fallback)
|
||||
const autostart = path.join(projectRoot, "bin/cli/tray/autostart.mjs");
|
||||
if (fs.existsSync(autostart)) {
|
||||
let content = fs.readFileSync(autostart, "utf-8");
|
||||
|
||||
// runUserSystemctl conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+\} catch \{\r?\n=======\r?\n\s+\} catch \(err\) \{\r?\n\s+if \(!ignoreFailure\) throw err;\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
` } catch (err) { \n if (!ignoreFailure) throw err;`
|
||||
);
|
||||
|
||||
// isSystemdServiceEnabled conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+return false;\r?\n=======\r?\n\s+\/\/ systemctl --user can't query the bus \(headless environments \/ CI runners\)\.\r?\n\s+\/\/ Treat the presence of the unit file as the source of truth, matching the\r?\n\s+\/\/ fallback used in enableLinux\(\) where unit-file existence counts as success\.\r?\n\s+return true;\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
` // systemctl --user can't query the bus (headless environments / CI runners).\n // Treat the presence of the unit file as the source of truth, matching the\n // fallback used in enableLinux() where unit-file existence counts as success.\n return true;`
|
||||
);
|
||||
|
||||
fs.writeFileSync(autostart, content);
|
||||
runCmd("git add bin/cli/tray/autostart.mjs");
|
||||
}
|
||||
|
||||
// 10. Resolve electron/package.json
|
||||
const electronPkg = path.join(projectRoot, "electron/package.json");
|
||||
if (fs.existsSync(electronPkg)) {
|
||||
let content = fs.readFileSync(electronPkg, "utf-8");
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+"electron": "\^42\.2\.0",\r?\n\s+"electron-builder": "\^26\.11\.0"\r?\n=======\r?\n\s+"electron": "\^41\.2\.0",\r?\n\s+"electron-builder": "\^26\.11\.1"\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' "electron": "^42.2.0",\n "electron-builder": "^26.11.1"'
|
||||
);
|
||||
fs.writeFileSync(electronPkg, content);
|
||||
runCmd("git add electron/package.json");
|
||||
}
|
||||
|
||||
// 11. Resolve .github/workflows/ci.yml
|
||||
const ciYaml = path.join(projectRoot, ".github/workflows/ci.yml");
|
||||
if (fs.existsSync(ciYaml)) {
|
||||
let content = fs.readFileSync(ciYaml, "utf-8");
|
||||
|
||||
// Run c8 over shard title
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+rm -rf coverage-shard coverage-shard-report\r?\n=======\r?\n\s+# `--temp-directory` \(writable via NODE_V8_COVERAGE\) is what the merge\r?\n\s+# job reads with `c8 report --temp-directory \.\.\.`\. Using `--output-dir`\r?\n\s+# only produces the final json \*report\* and leaves the raw v8 files in\r?\n\s+# `coverage\/tmp`, so uploading `coverage-shard\/` was empty\. Pin the temp\r?\n\s+# dir so the raw coverage files live there and the artifact upload picks\r?\n\s+# them up regardless of `--test-force-exit` timing\.\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" rm -rf coverage-shard coverage-shard-report\n # `--temp-directory` (writable via NODE_V8_COVERAGE) is what the merge\n # job reads with `c8 report --temp-directory ...`. Using `--output-dir`\n # only produces the final json *report* and leaves the raw v8 files in\n # `coverage/tmp`, so uploading `coverage-shard/` was empty. Pin the temp\n # dir so the raw coverage files live there and the artifact upload picks\n # them up regardless of `--test-force-exit` timing."
|
||||
);
|
||||
|
||||
// c8 temp-directory arg
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n=======\r?\n\s+--temp-directory=coverage-shard\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" --temp-directory=coverage-shard"
|
||||
);
|
||||
|
||||
fs.writeFileSync(ciYaml, content);
|
||||
runCmd("git add .github/workflows/ci.yml");
|
||||
}
|
||||
|
||||
// 12. Resolve Dockerfile
|
||||
const dockerfile = path.join(projectRoot, "Dockerfile");
|
||||
if (fs.existsSync(dockerfile)) {
|
||||
let content = fs.readFileSync(dockerfile, "utf-8");
|
||||
|
||||
// FROM node
|
||||
content = content.replace(
|
||||
/FROM node:26\.2\.0-trixie-slim AS builder\r?\nFROM node:24-trixie-slim AS builder/g,
|
||||
"FROM node:24-trixie-slim AS builder"
|
||||
);
|
||||
|
||||
// apt-get cache mounts
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nRUN --mount=type=cache,target=\/var\/cache\/apt,sharing=locked \\\r?\n\s+--mount=type=cache,target=\/var\/lib\/apt\/lists,sharing=locked \\\r?\n\s+apt-get update \\\r?\n=======\r?\nRUN apt-get update \\\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
"RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\\n --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\\n apt-get update \\"
|
||||
);
|
||||
|
||||
// npm ci script ignore and reproducible build check
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nRUN --mount=type=cache,target=\/root\/\.npm \\\r?\n\s+if \[ -f package-lock\.json \]; then \\\r?\n\s+npm ci --no-audit --no-fund --legacy-peer-deps; \\\r?\n\s+else \\\r?\n\s+npm install --no-audit --no-fund --legacy-peer-deps; \\\r?\n\s+fi\r?\n=======\r?\n# `--ignore-scripts` blocks the install\/postinstall hooks of dependencies,[\s\S]*?RUN npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
`# --ignore-scripts blocks the install/postinstall hooks of dependencies,
|
||||
# closing the supply-chain attack surface where a transitive dep can run
|
||||
# arbitrary code at install time. OmniRoute's own postinstall (
|
||||
# better-sqlite3 binary touchups, @swc/helpers copy) is only needed when
|
||||
# a packaged app/node_modules is unpacked — inside the Docker builder we
|
||||
# are doing a fresh native-platform install, so dropping the scripts is safe.
|
||||
#
|
||||
# We REQUIRE a committed package-lock.json so resolved dependency versions
|
||||
# are reproducible.
|
||||
RUN test -f package-lock.json \\
|
||||
|| (echo "package-lock.json is required for reproducible Docker builds" >&2 && exit 1)
|
||||
RUN --mount=type=cache,target=/root/.npm \\
|
||||
npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts`
|
||||
);
|
||||
|
||||
// npm global install
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nRUN --mount=type=cache,target=\/root\/\.npm \\\r?\n\s+npm install -g --no-audit --no-fund @openai\/codex @anthropic-ai\/claude-code droid openclaw@latest\r?\n=======\r?\nRUN npm install -g --no-audit --no-fund @openai\/codex @anthropic-ai\/claude-code droid openclaw@latest\r?\n\r?\nUSER node\r?\n\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
"RUN --mount=type=cache,target=/root/.npm \\\n npm install -g --no-audit --no-fund @openai/codex @anthropic-ai/claude-code droid openclaw@latest\n\nUSER node"
|
||||
);
|
||||
|
||||
fs.writeFileSync(dockerfile, content);
|
||||
runCmd("git add Dockerfile");
|
||||
}
|
||||
|
||||
// 13. Resolve open-sse/services/combo.ts
|
||||
const openSseCombo = path.join(projectRoot, "open-sse/services/combo.ts");
|
||||
if (fs.existsSync(openSseCombo)) {
|
||||
let content = fs.readFileSync(openSseCombo, "utf-8");
|
||||
|
||||
// IntentClassifierConfig imports
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nimport \{\r?\n\s+classifyWithConfig,\r?\n\s+DEFAULT_INTENT_CONFIG,\r?\n\s+type IntentClassifierConfig,\r?\n\} from "\.\/intentClassifier\.ts";\r?\n=======\r?\nimport \{ notifyWebhookEvent \} from "\.\.\/\.\.\/src\/lib\/webhookDispatcher";\r?\nimport \{ classifyWithConfig, DEFAULT_INTENT_CONFIG \} from "\.\/intentClassifier\.ts";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { notifyWebhookEvent } from "../../src/lib/webhookDispatcher";\nimport {\n classifyWithConfig,\n DEFAULT_INTENT_CONFIG,\n type IntentClassifierConfig,\n} from "./intentClassifier.ts";'
|
||||
);
|
||||
|
||||
// handlePipelineCombo call
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+handleChatCore: handleSingleModel,\r?\n\s+log: \{\r?\n\s+info: log\.info,\r?\n\s+warn: log\.warn,\r?\n\s+error: log\.error \?\? log\.warn,\r?\n\s+\},\r?\n\s+settings: settings \?\? \{\},\r?\n\s+signal: signal \?\? undefined,\r?\n=======\r?\n\s+handleChatCore: handleSingleModelWithTimeout,\r?\n\s+log,\r?\n\s+settings,\r?\n\s+signal,\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" handleChatCore: handleSingleModelWithTimeout,\n log: {\n info: log.info,\n warn: log.warn,\n error: log.error ?? log.warn,\n },\n settings: settings ?? {},\n signal: signal ?? undefined,"
|
||||
);
|
||||
|
||||
// handleSingleModel call in loop
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+const result = await handleSingleModelWrapped\(attemptBody, modelStr, \{\r?\n=======\r?\n\s+const result = await handleSingleModelWithTimeout\(body, modelStr, \{\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" const result = await handleSingleModelWithTimeout(attemptBody, modelStr, {"
|
||||
);
|
||||
|
||||
// recordSessionModelUsage conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+recordSessionModelUsage\([\s\S]*?\);\r?\n\s+\r?\n=======\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" recordSessionModelUsage(\n relayOptions.sessionId,\n combo.name,\n modelStr,\n provider,\n target.connectionId ?? undefined\n );"
|
||||
);
|
||||
|
||||
fs.writeFileSync(openSseCombo, content);
|
||||
runCmd("git add open-sse/services/combo.ts");
|
||||
}
|
||||
|
||||
// 14. Resolve src/app/api/copilot/chat/route.ts
|
||||
const copilotChatRoute = path.join(projectRoot, "src/app/api/copilot/chat/route.ts");
|
||||
if (fs.existsSync(copilotChatRoute)) {
|
||||
let content = fs.readFileSync(copilotChatRoute, "utf-8");
|
||||
|
||||
// Imports conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nimport \{ requireManagementAuth \} from "@\/lib\/api\/requireManagementAuth";\r?\nimport \{ processCopilotChat \} from "@\/lib\/copilot\/engine";\r?\nimport \{ isValidationFailure, validateBody \} from "@\/shared\/validation\/helpers";\r?\nimport \{ sanitizeErrorMessage \} from "@omniroute\/open-sse\/utils\/error\.ts";\r?\n=======\r?\nimport \{ processCopilotChat \} from "@\/lib\/copilot\/engine";\r?\nimport type \{ CopilotRequest \} from "@\/lib\/copilot\/engine";\r?\nimport \{ buildErrorBody \} from "@omniroute\/open-sse\/utils\/error";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { requireManagementAuth } from "@/lib/api/requireManagementAuth";\nimport { processCopilotChat } from "@/lib/copilot/engine";\nimport { isValidationFailure, validateBody } from "@/shared/validation/helpers";\nimport { sanitizeErrorMessage, buildErrorBody } from "@omniroute/open-sse/utils/error.ts";'
|
||||
);
|
||||
|
||||
// Schema content min length
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+content: z\.string\(\)\.min\(1, "message content is required"\),\r?\n=======\r?\n\s+content: z\.string\(\),\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' content: z.string().min(1, "message content is required"),'
|
||||
);
|
||||
|
||||
// POST implementation conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+const authError = await requireManagementAuth\(request\);\r?\n\s+if \(authError\) return authError;\r?\n\r?\n\s+try \{\r?\n\s+const rawBody = await request.json\(\);\r?\n\s+const validation = validateBody\(copilotRequestSchema, rawBody\);\r?\n\s+if \(isValidationFailure\(validation\)\) \{\r?\n\s+return NextResponse\.json\(\{ error: validation\.error \}, \{ status: 400 \}\);\r?\n=======\r?\n\s+try \{\r?\n\s+const raw = await request.json\(\);\r?\n\s+const parsed = copilotRequestSchema\.safeParse\(raw\);\r?\n\s+if \(!parsed\.success\) \{\r?\n\s+return NextResponse\.json\r?\n\s+buildErrorBody\(400, parsed\.error\.issues\[0\]\?\.message \?\? "Invalid request"\),\r?\n\s+\{ status: 400 \}\r?\n\s+\);\r?\n>>>>>>> release\/v3\.8\.4\r?\n\s+\}\r?\n\s+const body = parsed\.data as CopilotRequest;\r?\n\r?\n\s+const response = await processCopilotChat\(body\);/g,
|
||||
" const authError = await requireManagementAuth(request);\n if (authError) return authError;\n\n try {\n const rawBody = await request.json();\n const validation = validateBody(copilotRequestSchema, rawBody);\n if (isValidationFailure(validation)) {\n return NextResponse.json(\n buildErrorBody(400, validation.error),\n { status: 400 }\n );\n }\n const response = await processCopilotChat(validation.data);"
|
||||
);
|
||||
|
||||
// Error handling conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+const message = sanitizeErrorMessage\(error\);\r?\n\s+return NextResponse\.json\(\{ error: `Copilot error: \$\{message\}` \}, \{ status: 500 \}\);\r?\n=======\r?\n\s+\/\/ buildErrorBody\(\) routes through sanitizeErrorMessage\(\), which strips\r?\n\s+\/\/ stack traces and absolute file paths\. Hard rule #12\.\r?\n\s+const message = error instanceof Error \? error\.message : "Unknown error";\r?\n\s+return NextResponse\.json\(buildErrorBody\(500, message\), \{ status: 500 \}\);\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" const message = sanitizeErrorMessage(error);\n return NextResponse.json(buildErrorBody(500, `Copilot error: ${message}`), { status: 500 });"
|
||||
);
|
||||
|
||||
fs.writeFileSync(copilotChatRoute, content);
|
||||
runCmd("git add src/app/api/copilot/chat/route.ts");
|
||||
}
|
||||
|
||||
console.log("Resolutions written and staged!");
|
||||
}
|
||||
|
||||
main();
|
||||
12
scripts/query_all_provider_connections.cjs
Normal file
12
scripts/query_all_provider_connections.cjs
Normal file
@@ -0,0 +1,12 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const dbPath = path.resolve(process.env.USERPROFILE, '.omniroute', 'storage.sqlite');
|
||||
try {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
const rows = db.prepare(`SELECT id, provider, name, auth_type, is_active, last_error, test_status, updated_at FROM provider_connections ORDER BY updated_at DESC LIMIT 200`).all();
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
db.close();
|
||||
} catch (err) {
|
||||
console.error('ERROR', err && err.message);
|
||||
process.exit(2);
|
||||
}
|
||||
12
scripts/query_providers.cjs
Normal file
12
scripts/query_providers.cjs
Normal file
@@ -0,0 +1,12 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const dbPath = path.resolve(process.env.USERPROFILE, '.omniroute', 'storage.sqlite');
|
||||
try {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
const rows = db.prepare(`SELECT id, provider, name, auth_type, is_active, api_key IS NOT NULL as has_api_key, access_token IS NOT NULL as has_access_token, refresh_token IS NOT NULL as has_refresh, last_error, test_status, provider_specific_data, updated_at FROM provider_connections WHERE provider LIKE '%anthropic%' OR provider='anthropic' OR provider LIKE '%claude%' OR provider='claude'`).all();
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
db.close();
|
||||
} catch (err) {
|
||||
console.error('ERROR', err && err.message);
|
||||
process.exit(2);
|
||||
}
|
||||
16
scripts/query_providers.js
Normal file
16
scripts/query_providers.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const Database = require("better-sqlite3");
|
||||
const path = require("path");
|
||||
const dbPath = path.resolve(process.env.USERPROFILE, ".omniroute", "storage.sqlite");
|
||||
try {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT id, provider, name, auth_type, is_active, api_key IS NOT NULL as has_api_key, access_token IS NOT NULL as has_access_token, refresh_token IS NOT NULL as has_refresh, last_error, test_status, provider_specific_data, updated_at FROM provider_connections WHERE provider LIKE '%anthropic%' OR provider='anthropic' OR provider LIKE '%claude%' OR provider='claude'`
|
||||
)
|
||||
.all();
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
db.close();
|
||||
} catch (err) {
|
||||
console.error("ERROR", err && err.message);
|
||||
process.exit(2);
|
||||
}
|
||||
4
tests/scratch_test.mjs
Normal file
4
tests/scratch_test.mjs
Normal file
@@ -0,0 +1,4 @@
|
||||
import { getDbInstance } from "../src/lib/db/core.ts";
|
||||
import { runMigrations, getMigrationStatus } from "../src/lib/db/migrationRunner.ts";
|
||||
const db = getDbInstance();
|
||||
console.log(getMigrationStatus(db).pending);
|
||||
83
tests/unit/chatgpt-web-environment-double-unescape.test.ts
Normal file
83
tests/unit/chatgpt-web-environment-double-unescape.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* CodeQL alert 811 — js/double-escaping (HIGH) on
|
||||
* `open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/environment.ts`.
|
||||
*
|
||||
* `decodeXmlText()` unescapes the XML entities of the trusted Codex
|
||||
* `<environment_context>` block. It decoded `&` BEFORE `"` / `'`,
|
||||
* so the `&` it produced was re-consumed by a later `replaceAll` and the text
|
||||
* was unescaped twice: `&quot;` collapsed to `"` instead of `"`.
|
||||
*
|
||||
* These values become sandbox `cwd` / `workspace_roots` paths, so a
|
||||
* double-unescape silently rewrites the trusted workspace boundary.
|
||||
* `&` must be decoded LAST.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { extractChatGptTurnEnvironment } from "../../open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/environment.ts";
|
||||
|
||||
function parsedRequestWithCwd(cwdLiteral: string) {
|
||||
const environmentText = [
|
||||
"<environment_context>",
|
||||
` <cwd>${cwdLiteral}</cwd>`,
|
||||
" <sandbox_mode>read-only</sandbox_mode>",
|
||||
"</environment_context>",
|
||||
].join("\n");
|
||||
|
||||
const turnMetadata = { internal_chat_message_metadata_passthrough: { turn_id: "turn-1" } };
|
||||
|
||||
return {
|
||||
context: { tools: [] },
|
||||
_rawBody: {
|
||||
client_metadata: {
|
||||
"x-codex-turn-metadata": JSON.stringify({ thread_id: "thread-1", turn_id: "turn-1" }),
|
||||
},
|
||||
input: [
|
||||
{ type: "message", role: "system", content: [{ type: "input_text", text: "sys" }] },
|
||||
{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: environmentText }],
|
||||
...turnMetadata,
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "hello" }],
|
||||
...turnMetadata,
|
||||
},
|
||||
],
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
}
|
||||
|
||||
test("decoding the trusted Codex environment does not double-unescape &quot;", () => {
|
||||
const env = extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/ws&quot;dir"));
|
||||
assert.equal(
|
||||
env.cwd,
|
||||
"/tmp/ws"dir",
|
||||
'`&quot;` must decode to the literal text `"`, not to a double-unescaped `"`'
|
||||
);
|
||||
});
|
||||
|
||||
test("decoding the trusted Codex environment does not double-unescape &lt; / &#39;", () => {
|
||||
assert.equal(
|
||||
extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/ws&lt;dir")).cwd,
|
||||
"/tmp/ws<dir"
|
||||
);
|
||||
assert.equal(
|
||||
extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/ws&#39;dir")).cwd,
|
||||
"/tmp/ws'dir"
|
||||
);
|
||||
});
|
||||
|
||||
test("single-level XML entities still decode normally", () => {
|
||||
assert.equal(extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/a&b")).cwd, "/tmp/a&b");
|
||||
assert.equal(
|
||||
extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/a"b")).cwd,
|
||||
'/tmp/a"b'
|
||||
);
|
||||
assert.equal(extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/a'b")).cwd, "/tmp/a'b");
|
||||
assert.equal(extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/a>b")).cwd, "/tmp/a>b");
|
||||
});
|
||||
@@ -1,25 +1,19 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const update = await import("../../bin/cli/commands/update.mjs");
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const REAL_VERSION = JSON.parse(
|
||||
readFileSync(path.join(REPO_ROOT, "package.json"), "utf-8")
|
||||
).version;
|
||||
const FAKE_BIN = path.join(REPO_ROOT, ".fakebin-9475");
|
||||
|
||||
test("runUpdateCommand claims success without verifying the running binary version changed (#9475)", async () => {
|
||||
const fakeBin = mkdtempSync(path.join(tmpdir(), "omniroute-cli-update-9475-"));
|
||||
writeFileSync(
|
||||
path.join(fakeBin, "npm"),
|
||||
`#!/usr/bin/env bash
|
||||
if [ "$1" = "view" ]; then echo "3.8.99"; exit 0; fi
|
||||
if [ "$1" = "install" ]; then echo "added 1 package"; exit 0; fi
|
||||
exit 0
|
||||
`,
|
||||
{ mode: 0o755 }
|
||||
);
|
||||
|
||||
const origPath = process.env.PATH;
|
||||
process.env.PATH = fakeBin + path.delimiter + (origPath ?? "");
|
||||
process.env.PATH = FAKE_BIN + path.delimiter + origPath;
|
||||
const stdoutLogs: string[] = [];
|
||||
const origLog = console.log;
|
||||
console.log = function (...args: unknown[]) {
|
||||
@@ -39,8 +33,6 @@ exit 0
|
||||
assert.ok(true);
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
if (origPath === undefined) delete process.env.PATH;
|
||||
else process.env.PATH = origPath;
|
||||
rmSync(fakeBin, { recursive: true, force: true });
|
||||
process.env.PATH = origPath;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,10 +17,7 @@ import assert from "node:assert/strict";
|
||||
import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts";
|
||||
import { REGISTRY } from "../../open-sse/config/providers/index.ts";
|
||||
import { getExecutor, TinyCmsExecutor } from "../../open-sse/executors/index.ts";
|
||||
import {
|
||||
setupDomMocks,
|
||||
type DomMockRestore,
|
||||
} from "../../open-sse/executors/tinycmsSigner.ts";
|
||||
import { setupDomMocks, type DomMockRestore } from "../../open-sse/executors/tinycmsSigner.ts";
|
||||
|
||||
// tinycmsSigner.ts intentionally does NOT install its window/document/canvas
|
||||
// shims as a module-load side effect (see setupDomMocks() there) — doing so
|
||||
@@ -41,9 +38,10 @@ after(() => {
|
||||
// ── Catalog / WEB_COOKIE_PROVIDERS ────────────────────────────────────────────
|
||||
|
||||
test("tinycms-web is present in WEB_COOKIE_PROVIDERS", () => {
|
||||
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)[
|
||||
"tinycms-web"
|
||||
] as Record<string, unknown>;
|
||||
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)["tinycms-web"] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.ok(p, "WEB_COOKIE_PROVIDERS['tinycms-web'] must exist");
|
||||
assert.equal(p.id, "tinycms-web");
|
||||
assert.equal(p.alias, "tcw");
|
||||
@@ -51,9 +49,10 @@ test("tinycms-web is present in WEB_COOKIE_PROVIDERS", () => {
|
||||
});
|
||||
|
||||
test("tinycms-web WEB_COOKIE_PROVIDERS entry is marked as free-tier", () => {
|
||||
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)[
|
||||
"tinycms-web"
|
||||
] as Record<string, unknown>;
|
||||
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)["tinycms-web"] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal(p.hasFree, true);
|
||||
assert.ok(typeof p.freeNote === "string" && (p.freeNote as string).length > 0);
|
||||
assert.ok(typeof p.authHint === "string" && (p.authHint as string).length > 0);
|
||||
@@ -79,10 +78,7 @@ test("tinycms-web registry has all expected models", () => {
|
||||
|
||||
assert.ok(ids.includes("gpt-5-free"), "gpt-5-free must be registered");
|
||||
assert.ok(ids.includes("gpt-5.3-free"), "gpt-5.3-free must be registered");
|
||||
assert.ok(
|
||||
ids.includes("gpt-5.3-thinking-free"),
|
||||
"gpt-5.3-thinking-free must be registered"
|
||||
);
|
||||
assert.ok(ids.includes("gpt-5.3-thinking-free"), "gpt-5.3-thinking-free must be registered");
|
||||
assert.ok(ids.includes("deepseek-v4-flash"), "deepseek-v4-flash must be registered");
|
||||
assert.ok(ids.includes("claude-sonnet-5"), "claude-sonnet-5 must be registered");
|
||||
assert.ok(ids.includes("gemini-3.5-flash"), "gemini-3.5-flash must be registered");
|
||||
@@ -140,10 +136,7 @@ test("TinyCmsExecutor returns 401 when UUID is missing", async () => {
|
||||
assert.equal(result.response.status, 401);
|
||||
const body = await result.response.json();
|
||||
const errMsg = body?.error?.message || "";
|
||||
assert.ok(
|
||||
errMsg.includes("Invalid or missing device UUID"),
|
||||
"error must mention missing UUID"
|
||||
);
|
||||
assert.ok(errMsg.includes("Invalid or missing device UUID"), "error must mention missing UUID");
|
||||
// Hard Rule #12: must NOT leak stack traces
|
||||
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path");
|
||||
});
|
||||
@@ -161,10 +154,7 @@ test("TinyCmsExecutor returns 401 when UUID does not start with 'R'", async () =
|
||||
assert.equal(result.response.status, 401);
|
||||
const body = await result.response.json();
|
||||
const errMsg = body?.error?.message || "";
|
||||
assert.ok(
|
||||
errMsg.includes("Invalid or missing device UUID"),
|
||||
"error must mention missing UUID"
|
||||
);
|
||||
assert.ok(errMsg.includes("Invalid or missing device UUID"), "error must mention missing UUID");
|
||||
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path");
|
||||
});
|
||||
|
||||
@@ -172,7 +162,7 @@ test("TinyCmsExecutor returns the standard executor response envelope on success
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (input) => {
|
||||
const url = String(input);
|
||||
if (url.includes("api64.ipify.org")) {
|
||||
if (new URL(url).hostname === "api64.ipify.org") {
|
||||
return new Response(JSON.stringify({ ip: "127.0.0.1" }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
@@ -217,10 +207,7 @@ test("TinyCmsExecutor returns the standard executor response envelope on success
|
||||
|
||||
test("initTinyCmsWasm module exports expected functions", async () => {
|
||||
const signer = await import("../../open-sse/executors/tinycmsSigner.ts");
|
||||
assert.ok(
|
||||
typeof signer.initTinyCmsWasm === "function",
|
||||
"must export initTinyCmsWasm function"
|
||||
);
|
||||
assert.ok(typeof signer.initTinyCmsWasm === "function", "must export initTinyCmsWasm function");
|
||||
assert.ok(
|
||||
typeof signer.generateSecurePayload === "function",
|
||||
"must export generateSecurePayload function"
|
||||
@@ -254,10 +241,7 @@ test("TinyCmsExecutor sanitizes errors (no stack traces in error response)", asy
|
||||
assert.ok(result.response, "response must be present");
|
||||
const body = await result.response.json();
|
||||
const errMsg = body?.error?.message || "";
|
||||
assert.ok(
|
||||
errMsg.includes("Invalid or missing device UUID"),
|
||||
"error must mention missing UUID"
|
||||
);
|
||||
assert.ok(errMsg.includes("Invalid or missing device UUID"), "error must mention missing UUID");
|
||||
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path (Hard Rule #12)");
|
||||
});
|
||||
|
||||
@@ -274,12 +258,6 @@ test("tinycms-web credential requirement is kind: token with app-config-uuid", a
|
||||
assert.equal(req.credentialName, "app-config-uuid");
|
||||
assert.equal(req.acceptsFullCookieHeader, false);
|
||||
assert.ok(Array.isArray(req.storageKeys), "must have storageKeys array");
|
||||
assert.ok(
|
||||
(req.storageKeys as string[]).includes("apiKey"),
|
||||
"apiKey must be in storageKeys"
|
||||
);
|
||||
assert.ok(
|
||||
(req.storageKeys as string[]).includes("uuid"),
|
||||
"uuid must be in storageKeys"
|
||||
);
|
||||
assert.ok((req.storageKeys as string[]).includes("apiKey"), "apiKey must be in storageKeys");
|
||||
assert.ok((req.storageKeys as string[]).includes("uuid"), "uuid must be in storageKeys");
|
||||
});
|
||||
|
||||
142
tests/unit/tinycms-secure-nonce-randomness.test.ts
Normal file
142
tests/unit/tinycms-secure-nonce-randomness.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* CodeQL alert 806 — js/insecure-randomness (HIGH) on
|
||||
* `open-sse/executors/tinycms.ts`.
|
||||
*
|
||||
* The TinyCMS executor derives `x-secure-nonce` / `x-session-id` from a nonce
|
||||
* that is fed into the upstream request signature (`generateSecurePayload`).
|
||||
* That is a security context, so the nonce must never fall back to
|
||||
* `Math.random()` — a predictable nonce lets an observer replay or forge a
|
||||
* signed request.
|
||||
*
|
||||
* The regression guard runs the executor with a `globalThis.crypto` that has no
|
||||
* `randomUUID` (the exact condition that used to select the `Math.random()`
|
||||
* fallback) and asserts the emitted nonce is still a cryptographically strong
|
||||
* UUID.
|
||||
*/
|
||||
import test, { before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { TinyCmsExecutor } from "../../open-sse/executors/index.ts";
|
||||
import { setupDomMocks, type DomMockRestore } from "../../open-sse/executors/tinycmsSigner.ts";
|
||||
|
||||
let restoreDomMocks: DomMockRestore;
|
||||
|
||||
before(() => {
|
||||
restoreDomMocks = setupDomMocks();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
restoreDomMocks();
|
||||
});
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
test("TinyCMS nonce stays cryptographically strong when globalThis.crypto has no randomUUID", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalCryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto")!;
|
||||
const realCrypto = globalThis.crypto;
|
||||
|
||||
// Keep every other WebCrypto capability, drop only `randomUUID`. This is the
|
||||
// branch that previously fell back to `Math.random()`.
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
configurable: true,
|
||||
value: {
|
||||
getRandomValues: (array: ArrayBufferView) => realCrypto.getRandomValues(array as never),
|
||||
subtle: realCrypto.subtle,
|
||||
},
|
||||
});
|
||||
|
||||
const seenHeaders: Record<string, string>[] = [];
|
||||
|
||||
globalThis.fetch = (async (input: unknown, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (new URL(url).hostname === "api64.ipify.org") {
|
||||
return new Response(JSON.stringify({ ip: "127.0.0.1" }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (new URL(url).pathname === "/api/challenge") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
challenge: "test",
|
||||
challengeId: "challenge-id",
|
||||
expiresAt: Date.now() + 60_000,
|
||||
version: "1",
|
||||
difficulty: 0,
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
seenHeaders.push((init?.headers ?? {}) as Record<string, string>);
|
||||
return new Response("upstream body", { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
await new TinyCmsExecutor().execute({
|
||||
model: "gpt-5-free",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "Rtest-device" },
|
||||
});
|
||||
|
||||
assert.equal(seenHeaders.length, 1, "the executor must reach the chat endpoint exactly once");
|
||||
const headers = seenHeaders[0]!;
|
||||
assert.match(
|
||||
headers["x-secure-nonce"] ?? "",
|
||||
UUID_RE,
|
||||
"x-secure-nonce must be a crypto-strong UUID, never a Math.random() fallback"
|
||||
);
|
||||
assert.match(
|
||||
headers["x-session-id"] ?? "",
|
||||
UUID_RE,
|
||||
"x-session-id must be a crypto-strong UUID, never a Math.random() fallback"
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
Object.defineProperty(globalThis, "crypto", originalCryptoDescriptor);
|
||||
}
|
||||
});
|
||||
|
||||
test("consecutive TinyCMS nonces are unique", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const nonces: string[] = [];
|
||||
|
||||
globalThis.fetch = (async (input: unknown, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (new URL(url).hostname === "api64.ipify.org") {
|
||||
return new Response(JSON.stringify({ ip: "127.0.0.1" }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (new URL(url).pathname === "/api/challenge") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
challenge: "test",
|
||||
challengeId: "challenge-id",
|
||||
expiresAt: Date.now() + 60_000,
|
||||
version: "1",
|
||||
difficulty: 0,
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
nonces.push(((init?.headers ?? {}) as Record<string, string>)["x-secure-nonce"] ?? "");
|
||||
return new Response("upstream body", { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const executor = new TinyCmsExecutor();
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
await executor.execute({
|
||||
model: "gpt-5-free",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "Rtest-device" },
|
||||
});
|
||||
}
|
||||
assert.equal(nonces.length, 3);
|
||||
assert.equal(new Set(nonces).size, 3, "each request must carry a distinct nonce");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -55,14 +55,19 @@ test("#8014: ZaiWebExecutor must POST to the current chat.z.ai v2 chat-completio
|
||||
assert.ok(requested.length > 0, "the direct path must actually reach fetch");
|
||||
|
||||
assert.ok(
|
||||
!requested.includes(STALE_URL),
|
||||
// Exact-URL match (not a substring test): `requested` holds whole URLs.
|
||||
!requested.some((url) => url === STALE_URL),
|
||||
`zai-web executor POSTed to the stale endpoint — matches #8014's model-independent 404 "Not Found"`
|
||||
);
|
||||
|
||||
// The executor also probes the homepage for the frontend version and calls
|
||||
// /api/v1/chats/new first, so pick the completions request by its path.
|
||||
const completions = requested.filter((u) => new URL(u).pathname.endsWith("/chat/completions"));
|
||||
assert.equal(completions.length, 1, `expected exactly one completions request, got ${requested}`);
|
||||
assert.equal(
|
||||
completions.length,
|
||||
1,
|
||||
`expected exactly one completions request, got ${requested}`
|
||||
);
|
||||
assert.equal(
|
||||
new URL(completions[0]).pathname,
|
||||
"/api/v2/chat/completions",
|
||||
|
||||
Reference in New Issue
Block a user