Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ jobs:
# tsc --noEmit fails without it (TS2307) and downstream inferences
# cascade into spurious TS7006 errors, so regenerate before type check.
run: npm run prebuild
- name: Check facts drift
# Fails CI when facts.generated.ts is stale (version, providers, etc.
# differ from the workspace sources). Catches mechanical drift before
# deploy so the website never serves wrong version/provider data.
run: npm run check:facts
- name: Run ESLint
run: npm run lint
- name: TypeScript type check
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ dist/
outputs/
tmp/
backup/
/web/lib/facts.generated.ts

# Reference papers / large research blobs (keep locally if needed, don't ship)
docs/DeepSeek_V4.pdf
Expand Down
171 changes: 171 additions & 0 deletions web/lib/check-facts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* Tests for check-facts.mjs via its re-importable logic.
*
* We test the diffFacts helper (ported from check-facts.mjs) against
* fixture-like objects to prove the stale-version detection works.
* End-to-end `node scripts/check-facts.mjs` exit-code tests are not run
* from vitest since they depend on the actual workspace file tree.
*/
import { describe, it, expect } from "vitest";

// --- inline diffFacts (same logic as check-facts.mjs) ----------------

interface ProviderFact {
id: string;
label: string;
env: string;
}

interface RepoFacts {
[key: string]: unknown;
generatedAt: string;
version: string | null;
crates: string[];
sandboxBackends: string[];
providers: ProviderFact[];
defaultModel: string | null;
nodeEngines: string | null;
toolCount: number | null;
license: string | null;
latestRelease: string | null;
}

function diffFacts(
committed: Record<string, unknown>,
fresh: Record<string, unknown>,
): Array<{ field: string; committed: unknown; fresh: unknown }> {
const checkFields = [
"version",
"crates",
"sandboxBackends",
"providers",
"defaultModel",
"nodeEngines",
"toolCount",
"license",
];
const diffs: Array<{ field: string; committed: unknown; fresh: unknown }> = [];
for (const field of checkFields) {
const a = JSON.stringify(committed[field] ?? null);
const b = JSON.stringify(fresh[field] ?? null);
if (a !== b) {
diffs.push({ field, committed: committed[field], fresh: fresh[field] });
}
}
return diffs;
}

// --- helpers ---------------------------------------------------------

function freshFacts(overrides: Partial<RepoFacts> = {}): RepoFacts {
return {
generatedAt: new Date().toISOString(),
version: "0.8.64",
crates: ["cli", "config", "tui"],
sandboxBackends: ["landlock (Linux)", "seatbelt (macOS)"],
providers: [
{ id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" },
{ id: "anthropic", label: "Anthropic", env: "ANTHROPIC_API_KEY" },
],
defaultModel: "deepseek-v4-pro",
nodeEngines: ">=18",
toolCount: 78,
license: "MIT",
latestRelease: null,
...overrides,
};
}

// --- tests -----------------------------------------------------------

describe("diffFacts (check-facts parity)", () => {
it("returns empty array when facts match", () => {
const committed = freshFacts();
const fresh = freshFacts();
expect(diffFacts(committed, fresh)).toEqual([]);
});

it("detects stale version (0.8.62 vs 0.8.64)", () => {
const committed = freshFacts({ version: "0.8.62" });
const fresh = freshFacts({ version: "0.8.64" });
const diffs = diffFacts(committed, fresh);
expect(diffs).toHaveLength(1);
expect(diffs[0]).toEqual({
field: "version",
committed: "0.8.62",
fresh: "0.8.64",
});
});

it("detects provider list drift (added Anthropic)", () => {
const committed = freshFacts({
providers: [{ id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" }],
});
const fresh = freshFacts({
providers: [
{ id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" },
{ id: "anthropic", label: "Anthropic", env: "ANTHROPIC_API_KEY" },
],
});
const diffs = diffFacts(committed, fresh);
expect(diffs).toHaveLength(1);
expect(diffs[0].field).toBe("providers");
});

it("detects stale default model", () => {
const committed = freshFacts({ defaultModel: "deepseek-v3" });
const fresh = freshFacts({ defaultModel: "deepseek-v4-pro" });
const diffs = diffFacts(committed, fresh);
expect(diffs).toHaveLength(1);
expect(diffs[0].field).toBe("defaultModel");
});

it("detects tool count drift", () => {
const committed = freshFacts({ toolCount: 70 });
const fresh = freshFacts({ toolCount: 78 });
const diffs = diffFacts(committed, fresh);
expect(diffs).toHaveLength(1);
expect(diffs[0].field).toBe("toolCount");
});

it("detects multiple field drifts at once", () => {
const committed = freshFacts({
version: "0.8.62",
toolCount: 70,
});
const fresh = freshFacts({
version: "0.8.64",
toolCount: 78,
});
const diffs = diffFacts(committed, fresh);
expect(diffs).toHaveLength(2);
expect(diffs.map((d) => d.field).sort()).toEqual(["toolCount", "version"]);
});

it("ignores generatedAt and latestRelease changes", () => {
const committed = freshFacts({ generatedAt: "old" });
const fresh = freshFacts({ generatedAt: "new", latestRelease: "v0.8.64" });
// Both generatedAt and latestRelease are excluded from checkFields.
// Version and others match => no diffs.
expect(diffFacts(committed, fresh)).toEqual([]);
});

it("handles null-to-value drift for license", () => {
const committed = freshFacts({ license: null });
const fresh = freshFacts({ license: "MIT" });
const diffs = diffFacts(committed, fresh);
expect(diffs).toHaveLength(1);
expect(diffs[0].field).toBe("license");
});

it("handles empty arrays in committed vs populated arrays in fresh", () => {
const committed = freshFacts({ crates: [], providers: [] });
const fresh = freshFacts({
crates: ["cli"],
providers: [{ id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" }],
});
const diffs = diffFacts(committed, fresh);
expect(diffs).toHaveLength(2);
expect(diffs.map((d) => d.field).sort()).toEqual(["crates", "providers"]);
});
});
184 changes: 184 additions & 0 deletions web/lib/facts.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// AUTO-GENERATED by web/scripts/derive-facts.mjs at prebuild.
// DO NOT EDIT — re-run `npm run prebuild` (or just `npm run build`) after changing the parent repo.
// To override at runtime, write the same shape to KV under key "facts:current".

export interface ProviderFact { id: string; label: string; env: string }

export interface RepoFacts {
generatedAt: string;
version: string | null;
crates: string[];
sandboxBackends: string[];
providers: ProviderFact[];
defaultModel: string | null;
nodeEngines: string | null;
toolCount: number | null;
license: string | null;
latestRelease: string | null;
}

export const FACTS: RepoFacts = {
"generatedAt": "2026-06-23T09:45:11.256Z",
"version": "0.8.64",
"crates": [
"agent",
"app-server",
"cli",
"config",
"core",
"execpolicy",
"hooks",
"mcp",
"protocol",
"release",
"secrets",
"state",
"tools",
"tui",
"whaleflow"
],
"sandboxBackends": [
"bwrap",
"landlock (Linux)",
"process_hardening",
"seatbelt (macOS)",
"seccomp"
],
"providers": [
{
"id": "deepseek",
"label": "DeepSeek",
"env": "DEEPSEEK_API_KEY"
},
{
"id": "nvidia-nim",
"label": "NVIDIA NIM",
"env": "NVIDIA_API_KEY / NVIDIA_NIM_API_KEY"
},
{
"id": "openai",
"label": "OpenAI-compatible",
"env": "OPENAI_API_KEY"
},
{
"id": "atlascloud",
"label": "AtlasCloud",
"env": "ATLASCLOUD_API_KEY"
},
{
"id": "wanjie-ark",
"label": "Wanjie Ark",
"env": "WANJIE_ARK_API_KEY / WANJIE_API_KEY / WANJIE_MAAS_API_KEY"
},
{
"id": "volcengine",
"label": "Volcengine Ark",
"env": "VOLCENGINE_API_KEY / VOLCENGINE_ARK_API_KEY / ARK_API_KEY"
},
{
"id": "openrouter",
"label": "OpenRouter",
"env": "OPENROUTER_API_KEY"
},
{
"id": "xiaomi-mimo",
"label": "Xiaomi MiMo",
"env": "XIAOMI_MIMO_API_KEY / XIAOMI_API_KEY / MIMO_API_KEY"
},
{
"id": "novita",
"label": "Novita AI",
"env": "NOVITA_API_KEY"
},
{
"id": "fireworks",
"label": "Fireworks AI",
"env": "FIREWORKS_API_KEY"
},
{
"id": "siliconflow",
"label": "SiliconFlow",
"env": "SILICONFLOW_API_KEY"
},
{
"id": "siliconflow-CN",
"label": "SiliconFlow CN",
"env": "SILICONFLOW_API_KEY"
},
{
"id": "arcee",
"label": "Arcee AI",
"env": "ARCEE_API_KEY"
},
{
"id": "moonshot",
"label": "Moonshot/Kimi",
"env": "MOONSHOT_API_KEY / KIMI_API_KEY"
},
{
"id": "sglang",
"label": "SGLang",
"env": "SGLANG_API_KEY"
},
{
"id": "vllm",
"label": "vLLM",
"env": "VLLM_API_KEY"
},
{
"id": "ollama",
"label": "Ollama",
"env": "OLLAMA_API_KEY"
},
{
"id": "huggingface",
"label": "Hugging Face",
"env": "HUGGINGFACE_API_KEY / HF_TOKEN"
},
{
"id": "together",
"label": "Together AI",
"env": "TOGETHER_API_KEY"
},
{
"id": "qianfan",
"label": "Baidu Qianfan",
"env": "QIANFAN_API_KEY / BAIDU_QIANFAN_API_KEY"
},
{
"id": "openai-codex",
"label": "OpenAI Codex",
"env": "ChatGPT/Codex OAuth via `codex login` (OPENAI_CODEX_ACCESS_TOKEN / CODEX_ACCESS_TOKEN override)"
},
{
"id": "anthropic",
"label": "Anthropic",
"env": "ANTHROPIC_API_KEY"
},
{
"id": "zai",
"label": "Z.ai",
"env": "ZAI_API_KEY / Z_AI_API_KEY"
},
{
"id": "stepfun",
"label": "StepFun",
"env": "STEPFUN_API_KEY / STEP_API_KEY"
},
{
"id": "minimax",
"label": "MiniMax",
"env": "MINIMAX_API_KEY"
},
{
"id": "deepinfra",
"label": "DeepInfra",
"env": "DEEPINFRA_API_KEY / DEEPINFRA_TOKEN"
}
],
"defaultModel": "deepseek-v4-pro",
"nodeEngines": ">=18",
"toolCount": 78,
"license": "MIT",
"latestRelease": null
};
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"build": "next build",
"start": "next start",
"test": "vitest run",
"check:facts": "node scripts/check-facts.mjs",
"lint": "eslint .",
"preview": "opennextjs-cloudflare preview",
"predeploy": "node scripts/check-kv-id.mjs",
Expand Down
Loading
Loading