Skip to content
Open
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
7 changes: 7 additions & 0 deletions scripts/live-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ export const LIVE_FILES = [
// child binds no port and does no network I/O, so no zone band is needed —
// the entry is for the tripwire alone, like #3890/#5114
"tests/integrity-worker-stderr.test.ts",
// #5241 phase-0 startup-contract stakes: both spawn children — verify-attach
// drives one child per lane (sequential await), latches drives the stderr
// flip child. No shared ports (mock bili server + spawn stubs are listen(0)
// or unbound), so no real zone band is needed — the entries exist for the
// spawn tripwire alone, like #3890's
"tests/contract-v2-stake-latches.test.ts",
"tests/contract-v2-stake-verify-attach.test.ts",
];

/** Band width per live file: the zone ladder drifts +1 per EADDRINUSE retry;
Expand Down
97 changes: 97 additions & 0 deletions tests/contract-v2-stake-health.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* #5241 阶段 0 特征化打桩(不改产品代码):/__bili/health 的应答词汇表
* —— C1 契约(生命周期单源、健康度可表达)改造前的现状基准。
*
* 现状只有两个状态:bootReady 之前 503 {ok:false, reason:"starting"},
* 之后 200 全量快照。没有 degraded / degraded-reason / recovery-etf 字段
* —— sidecar 被闩死、stderr 被闩死、lane 降级直发,这些都发生在进程
* 内部,health 一个字都不说。这正是 C1 要改的:桩先把这个「不可表达」
* 钉死,阶段 1 加字段时本桩的键集断言必须同步更新(有意为之的账本)。
*
* 客户端三态探测(alive/busy/dead,src/liveness.ts)已有专项桩
* tests/issue3654-health-probe-three-state.test.ts,不在此重钉。
*/

import { test, after } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import http from "node:http";
import { handleAdminRoute } from "../src/server/admin.ts";
import { VERSION } from "../src/version.ts";

const HERMETIC = fs.mkdtempSync(path.join(os.tmpdir(), "bili-c2-health-"));
process.env.BILI_CONFIG_FILE = path.join(HERMETIC, "no-config.json");

after(() => {
fs.rmSync(HERMETIC, { recursive: true, force: true });
});

type AdminCtxLike = Parameters<typeof handleAdminRoute>[2];

/** The health branch only reads these fields (auxProxyShot resolves inside a
* try/catch, so a minimal opts is safe). Everything else gets a stub. */
function makeCtx(bootReady: boolean): AdminCtxLike {
return {
opts: { upstream: "https://api.example.invalid" },
core: {},
config: {},
log: () => {},
instanceId: "stake-instance",
instanceStartedAt: 1_710_000_000_000,
proxyWatchers: new Set([4242]),
initialWatcherPid: 4242,
watcherLastSeen: new Map(),
bootReady: () => bootReady,
} as unknown as AdminCtxLike;
}

async function getHealth(bootReady: boolean): Promise<{ status: number; body: Record<string, unknown> }> {
const ctx = makeCtx(bootReady);
const server = http.createServer((req, res) => {
void handleAdminRoute(req, res, ctx).catch(() => {
if (!res.headersSent) { res.statusCode = 500; res.end("{}"); }
});
});
const origin = await new Promise<string>((resolve) => {
server.listen(0, "127.0.0.1", () => {
const addr = server.address() as { port: number };
resolve(`http://127.0.0.1:${addr.port}`);
});
});
try {
const res = await fetch(`${origin}/__bili/health`);
return { status: res.status, body: (await res.json()) as Record<string, unknown> };
} finally {
await new Promise<void>((r) => server.close(() => r()));
}
}

test("pre-boot /__bili/health: exactly 503 {ok:false, reason:\"starting\"} — nothing else is expressible yet", async () => {
const { status, body } = await getHealth(false);
assert.equal(status, 503);
assert.deepEqual(Object.keys(body).sort(), ["ok", "reason"], "the starting answer carries exactly two keys");
assert.deepEqual(body, { ok: false, reason: "starting" });
});

test("post-boot /__bili/health: exactly the 11-field snapshot — no lifecycle/degradation vocabulary exists today", async () => {
const { status, body } = await getHealth(true);
assert.equal(status, 200);
assert.deepEqual(
Object.keys(body).sort(),
["auxProxy", "auxProxySource", "blindTunnels", "commit", "instanceId", "ok", "pid", "startedAt", "upstream", "version", "watchdog"],
"the ready answer carries exactly the current field set (C1: adding lifecycle fields is a deliberate, visible change)",
);
assert.equal(body.ok, true);
assert.equal(body.version, VERSION);
assert.equal(body.instanceId, "stake-instance");
assert.equal(body.startedAt, 1_710_000_000_000);
assert.equal(body.pid, process.pid, "in-process stub server: the pid is this test process");
assert.equal(body.upstream, "https://api.example.invalid");
// The C1 gap, pinned explicitly: today's payload CANNOT say degraded.
for (const absent of ["lifecycle", "state", "mode", "degraded", "degradations", "recoveryEtaMs", "reason"]) {
assert.equal(absent in body, false, `no "${absent}" field exists in today's health vocabulary`);
}
assert.deepEqual(body.watchdog, { armed: true, parentPid: 4242, watchers: [4242] }, "#1322 watchdog shape pinned as-is");
});
144 changes: 144 additions & 0 deletions tests/contract-v2-stake-lane-probe.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* #5241 阶段 0 打桩配套探针:在「干净子进程」里驱动单条宿主车道的
* verifyAttachAndRecover,把结果以一行 JSON 报告给父测试
* (tests/contract-v2-stake-verify-attach.test.ts)。
*
* 为什么子进程:四条车道共享同一个进程的 global fetch、env 和模块级
* 状态,同进程顺序驱动会互相污染(实测:一条车道的后台异步会改写另一条
* 车道的 env/spawn 计数)。生产里每条车道都在自己的宿主进程里启动,
* 子进程隔离就是被测现实本身。
*
* 用法: node --import tsx contract-v2-stake-lane-probe.mts <lane> <scenario>
* lane = pi | omp | opencode | dsh
* scenario = s1 (healthy equal-version mock)
* | s2 (strictly older mock)
* | s3 (dead target, port 9)
* 输出: 单行 JSON { lane, scenario, landed, spawns, shutdowns, envAfter, registerBase }
*/

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import http from "node:http";

const HERMETIC = fs.mkdtempSync(path.join(os.tmpdir(), "bili-c2-probe-"));
process.env.BILI_CONFIG_FILE = path.join(HERMETIC, "no-config.json");
process.env.NODE_TEST_CONTEXT = process.env.NODE_TEST_CONTEXT ?? "contract-stake";
process.env.BILI_ATTACH_EVIDENCE_GRACE_MS = "30";
process.env.BILI_ATTACH_HEALTH_DEADLINE_MS = "150";
delete process.env.BILLION_CONTEXT_ATTACH;
delete process.env.BILLION_CONTEXT_NATIVE;
delete process.env.BILLION_CONTEXT_PROXY;

const [laneName, scenario] = process.argv.slice(2) as [string, string];
if (laneName === undefined || scenario === undefined) {
process.stderr.write("usage: lane-probe.mts <lane> <scenario>\n");
process.exit(2);
}

function startMockBili(version: string): Promise<{ origin: string; shutdowns: () => number }> {
let shutdowns = 0;
const server = http.createServer((req, res) => {
const url = req.url ?? "";
if (req.method === "GET" && url === "/__bili/plugin/manifest") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ version, tools: {} }));
return;
}
if (req.method === "POST" && url === "/__bili/shutdown") {
shutdowns += 1;
res.writeHead(200, { "content-type": "application/json" });
res.end("{}");
return;
}
res.writeHead(404, { "content-type": "application/json" });
res.end("{}");
});
return new Promise((resolve) => {
server.listen(0, "127.0.0.1", () => {
const addr = server.address() as { port: number };
resolve({ origin: `http://127.0.0.1:${addr.port}`, shutdowns: () => shutdowns });
});
});
}

const ni = await import("../src/agent/native-intercept.ts");
const { VERSION } = await import("../src/version.ts");
const counter = { n: 0 };
const countingSpawn = async (): Promise<string | undefined> => {
counter.n += 1;
return "http://127.0.0.1:7777";
};

let mock: { origin: string; shutdowns: () => number } | undefined;
let origin: string;
if (scenario === "s1") { mock = await startMockBili(VERSION); origin = mock.origin; }
else if (scenario === "s2") { mock = await startMockBili("0.0.1"); origin = mock.origin; }
else { origin = "http://127.0.0.1:9"; }

let landed: string | undefined;
let registerBase: string | undefined | "unset";

if (laneName === "pi") {
const pi = await import("../src/agent/pi-native.ts");
ni._resetForTest();
pi._resetNativeStateForTest();
pi._setSpawnForTest(countingSpawn);
pi.armNativePi({ mode: "attach", attachOrigin: origin });
const respawn = pi._stateRespawnForTest();
if (respawn === undefined) throw new Error("pi arm did not expose the respawn seam");
landed = await respawn();
pi._setSpawnForTest(undefined);
} else if (laneName === "omp") {
const omp = await import("../src/agent/omp-native.ts");
ni._resetForTest();
omp._resetNativeStateForTest();
omp._setSpawnForTest(countingSpawn);
omp.armNativeOmp({ mode: "attach", attachOrigin: origin });
const respawn = omp._stateRespawnForTest();
if (respawn === undefined) throw new Error("omp arm did not expose the respawn seam");
landed = await respawn();
omp._setSpawnForTest(undefined);
} else if (laneName === "opencode") {
const oc = await import("../src/agent/opencode-native.ts");
ni._resetForTest();
oc._resetNativeStateForTest();
oc._setSpawnForTest(countingSpawn);
landed = await oc.verifyAttachAndRecover(origin);
oc._setSpawnForTest(undefined);
} else if (laneName === "dsh") {
const dsh = await import("../src/agent/dsh-native.ts");
ni._resetForTest();
dsh._resetRoutedForTest();
dsh._resetRegisterForTest(origin);
dsh._setSpawnForTest(countingSpawn);
// dsh rides a preset origin (issue3544 harness): verify must keep it on
// attach and UNFREE it on spawn fallback.
process.env.BILLION_CONTEXT_PROXY = origin;
process.env.DSH_HOME = path.join(HERMETIC, "dsh-home");
process.env.XDG_STATE_HOME = path.join(HERMETIC, "dsh-state");
landed = await dsh.verifyAttachAndRecover(origin);
registerBase = dsh._registerStateForTest().base ?? "unset";
dsh._setSpawnForTest(undefined);
} else {
process.stderr.write(`unknown lane: ${laneName}\n`);
process.exit(2);
}

// Capture right after the lane ran (no reset helpers in between — the
// reset helpers delete BILLION_CONTEXT_PROXY; process.exit handles timers).
const envAfter = process.env.BILLION_CONTEXT_PROXY ?? null;

const report = {
lane: laneName,
scenario,
landed: landed ?? null,
spawns: counter.n,
shutdowns: mock?.shutdowns() ?? 0,
envAfter,
registerBase: registerBase ?? null,
};
process.stdout.write(`\nPROBE ${JSON.stringify(report)}`);
// Force-exit: lane modules leave background timers (recovery/reprobe loops)
// that would otherwise hold the child open for their full TTL.
process.exit(0);
Loading
Loading