Skip to content
Merged
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/nightly-release-green.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ jobs:
node scripts/quality/validate-release-green.mjs --json --hermetic --no-static --serial-slow $SUITE_FLAGS 1> slow-report.json 2> >(tee "slow-$SUITE_NAME.log" >&2)
echo "[slow-suite] $SUITE_NAME finished with exit=$? (the verdict is the aggregator's)"

# The per-gate logs go up too. Without them a red shard reports only its FIRST
# failure line — "✖ tests/integration/api-keys.test.ts" — and the actual assertion
# lives in _artifacts/release-green/<gate>.log on a runner that is already gone.
# Both reds from run 35501782210 had to be reproduced locally to be read at all.
- name: Upload the suite report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
Expand All @@ -184,6 +188,7 @@ jobs:
path: |
slow-report.json
slow-${{ matrix.name }}.log
_artifacts/release-green/
retention-days: 14
if-no-files-found: warn

Expand Down
1 change: 1 addition & 0 deletions changelog.d/fixes/sweep-integration-reds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(test):** the first full-CI sweep this release line has ever completed came back red, and both reds were real. `tests/integration/api-routes-critical.test.ts` was making a **live HTTPS request to `aihorde.net`** on every run: `GET /api/v1/models` refreshes the AI Horde image catalog whenever `aihorde` is active, and it is active by default because it is a no-auth provider with no connection row to switch off. The service already exposes `setFetch` for exactly this, and the test now injects a stub. The second red was a false positive of the network guard itself: `tests/integration/api-keys.test.ts` points `CLOUD_URL` at `http://cloud.example` on purpose, to take a failing outbound branch, and the guard counted an RFC 2606 / RFC 6761 reserved name — one that resolves to nothing, anywhere — as "the suite reached the network". Such a name is now **still refused** (nothing leaves the machine, not even a DNS lookup) but is no longer counted as a violation. The shard jobs also upload `_artifacts/release-green/` now: without the per-gate logs a red shard reports only its first failure line and the assertion dies with the runner, which is why both of these had to be reproduced locally to be read at all.
34 changes: 32 additions & 2 deletions tests/_setup/blockNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,30 @@ const loopback = new net.BlockList();
loopback.addSubnet("127.0.0.0", 8, "ipv4");
loopback.addAddress("::1", "ipv6");

/**
* Top-level domains the IETF reserves as permanently unresolvable (RFC 2606 / RFC 6761).
* A name under one of these cannot reach a host: there is no delegation for it, anywhere.
*
* Tests use them on purpose to exercise a FAILING outbound path — `CLOUD_URL` is set to
* `http://cloud.example` in tests/integration/api-keys.test.ts so the cloud-sync branch is
* taken and fails. Counting that as "the suite reached the network" made this guard fail a
* file that never left the machine, which is a false positive on a guard whose value is
* that its reds are real.
*
* This is not a hole: the exemption is not "hosts a test asked for", it is "names that by
* standard resolve to nothing". A provider smuggled in under `.test` would still not be
* reachable, so there is nothing to smuggle.
*/
const UNRESOLVABLE_TLDS = ["test", "example", "invalid", "localhost"];

/** True for a name under an RFC-reserved, permanently unresolvable TLD. */
export function isUnresolvableHost(host: string): boolean {
const normalized = host.trim().replace(/\.$/, "").toLowerCase();
if (net.isIP(normalized)) return false;
const tld = normalized.split(".").pop() ?? "";
return UNRESOLVABLE_TLDS.includes(tld);
}

/** True for loopback IPs (v4, v6, IPv4-mapped v6) and the name `localhost`. */
export function isLoopbackHost(host: string): boolean {
const normalized = host
Expand Down Expand Up @@ -245,9 +269,15 @@ function installNetworkGuard(decision: GuardDecision): NetworkGuard {
function block(host: string, port: string, via: string): NetworkAccessBlockedError {
const testFile = currentTestFile();
const violation: Violation = { host, port, via, testFile, stack: captureStack() };
violations.push(violation);
// Refuse it either way — the connection must never leave the machine, not even as a
// DNS lookup. What an RFC-reserved name changes is only whether it is COUNTED: a test
// that points at `cloud.example` on purpose, to exercise a failing outbound branch,
// has not reached the network and must not fail the file for it.
const counted = !isUnresolvableHost(host);
if (counted) violations.push(violation);
process.stderr.write(
`${LOG_PREFIX} ${decision.mode === "report" ? "REPORT" : "BLOCKED"} ` +
`${LOG_PREFIX} ${decision.mode === "report" ? "REPORT" : "BLOCKED"}` +
`${counted ? "" : " (reserved name — refused, not counted)"} ` +
`host=${host} port=${port} via=${via} file=${testFile}\n${violation.stack}\n`
);
return new NetworkAccessBlockedError(host, port, via, testFile);
Expand Down
15 changes: 15 additions & 0 deletions tests/integration/api-routes-critical.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ const proxiesRoute = await import("../../src/app/api/v1/management/proxies/route
const settingsProxyRoute = await import("../../src/app/api/settings/proxy/route.ts");
const settingsMitmRoute = await import("../../src/app/api/settings/mitm/route.ts");
const v1ModelsRoute = await import("../../src/app/api/v1/models/route.ts");
const { aiHordeImageCatalog } = await import("@omniroute/open-sse/services/aihordeImageCatalog");

// GET /api/v1/models refreshes the AI Horde image catalog whenever `aihorde` is active —
// and it is active by default, because it is a no-auth provider with no connection row to
// switch off. So this file was making a live HTTPS request to aihorde.net on every run
// (3 attempts, counting the retry), which is what tests/_setup/blockNetwork.ts caught.
// The service exposes setFetch precisely for this; the route's own catch keeps the last
// good snapshot, so an empty worker list changes none of the assertions below.
aiHordeImageCatalog.setFetch(
async () =>
new Response(JSON.stringify([]), {
status: 200,
headers: { "content-type": "application/json" },
})
);

const MACHINE_ID = "1234567890abcdef";

Expand Down
42 changes: 42 additions & 0 deletions tests/unit/block-network-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
networkGuard,
resolveGuardMode,
targetFromConnectArgs,
isUnresolvableHost,
} from "../_setup/blockNetwork.ts";

const PROBE = "tests/unit/fixtures/network-guard-probe.ts";
Expand Down Expand Up @@ -247,3 +248,44 @@ test("the wreq-js native transport (outside net.Socket) is blocked too", () => {
/\[network-guard\] BLOCKED host=192\.0\.2\.1 port=443 via=wreq-js\.request/
);
});

// ─── RFC-reserved names are not "the network" ──────────────────────────────

test("names under an RFC-reserved TLD are not counted as reaching the network", () => {
// RFC 2606 / RFC 6761 reserve these as permanently unresolvable, and tests use them to
// exercise a FAILING outbound path on purpose — api-keys.test.ts sets CLOUD_URL to
// http://cloud.example so the cloud-sync branch is taken and fails. Counting that as a
// violation failed a file that never left the machine.
for (const host of [
"cloud.example",
"api.test",
"nothing.invalid",
"foo.localhost",
"DEEP.sub.Example",
"trailing.example.",
]) {
assert.equal(isUnresolvableHost(host), true, `${host} must be treated as unresolvable`);
}
});

test("the exemption does not reach real names or addresses", () => {
// The value of this guard is that its reds are real. An exemption that leaked to
// `aihorde.net` — the live call the sweep actually caught — would destroy that.
for (const host of [
"aihorde.net",
"api.openai.com",
"example.com",
"exampletest",
"192.0.2.1",
"2001:db8::1",
]) {
assert.equal(isUnresolvableHost(host), false, `${host} must still be a violation`);
}
});

test("an unresolvable name is still not loopback", () => {
// Two separate questions, kept separate: `cloud.example` is exempt from the violation
// count, but it is not a local address and must never be treated as one.
assert.equal(isLoopbackHost("cloud.example"), false);
assert.equal(isLoopbackHost("localhost"), true);
});
Loading