fix: fail closed backend capabilities and isolate gate smoke

This commit is contained in:
jb
2026-09-19 10:01:23 -04:00
parent 1371995cbe
commit 5b0076cb20
17 changed files with 344 additions and 81 deletions
+19
View File
@@ -471,4 +471,23 @@ describe("Convex persistence", () => {
else process.env.APP_CAPABILITY_ALLOWLIST = previous;
}
});
test("backend rollout gate fails closed when propagation is missing or malformed", async () => {
const previous = process.env.APP_CAPABILITY_ALLOWLIST;
try {
for (const value of [undefined, "not-a-capability"]) {
if (value === undefined) delete process.env.APP_CAPABILITY_ALLOWLIST;
else process.env.APP_CAPABILITY_ALLOWLIST = value;
process.env.APP_AUTH_ISSUER = "https://auth.example.test";
const t = convexTest({ schema, modules });
const loggedIn = await login(t, { sessionHash: `missing-gate-${value ?? 'unset'}` });
const browser = t.withIdentity({ subject: loggedIn.userId, issuer: "https://auth.example.test", sid: loggedIn.sessionId } as any);
await expect(browser.query(statusQuery("characters"), {})).resolves.toBeNull();
await expect(t.mutation(mutation("bridgeBeginSync"), { ...bridge, userId: loggedIn.userId, characterId: 100 })).rejects.toThrow("asset_sync");
}
} finally {
if (previous === undefined) delete process.env.APP_CAPABILITY_ALLOWLIST;
else process.env.APP_CAPABILITY_ALLOWLIST = previous;
}
});
});
+3
View File
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
import {
BASELINE_RELEASE_CAPABILITIES,
capabilityEnabled,
parseBackendCapabilityAllowlist,
parseCapabilityAllowlist,
releaseIdIsImmutable,
} from '../src/lib/server/release-gate';
@@ -23,6 +24,8 @@ describe('deployment rollout gate', () => {
expect(parseCapabilityAllowlist('')).toEqual([]);
expect(() => parseCapabilityAllowlist('asset_search,typo')).toThrow('Unknown deployment capability');
expect(() => parseCapabilityAllowlist('asset_search,')).toThrow('Unknown deployment capability');
expect(parseBackendCapabilityAllowlist('asset_sync')).toEqual(['asset_sync']);
expect(() => parseBackendCapabilityAllowlist(undefined)).toThrow('required in the backend deployment');
});
test('requires a non-placeholder production release identifier', () => {
+6 -2
View File
@@ -5,14 +5,17 @@ const workflow = readFileSync('.gitea/workflows/ci.yml', 'utf8');
const compose = readFileSync('compose.yaml', 'utf8');
const dockerfile = readFileSync('Dockerfile', 'utf8');
const dockerignore = readFileSync('.dockerignore', 'utf8');
const convexConfig = readFileSync('scripts/configure-convex.ts', 'utf8');
test('Gitea workflow separates PR source checks from trusted container builds', () => {
test('Gitea workflow keeps source checks separate and holds unverified container CI', () => {
expect(workflow).toContain('pull_request:');
expect(workflow).toContain("if: ${{ gitea.event_name == 'push' }}");
expect(workflow).toContain('if: ${{ false }}');
expect(workflow).toContain('GITHUB_RUN_ID is required');
expect(workflow).toContain('RELEASE_ENV=staging');
expect(workflow).toContain('bun run test:release-gates');
expect(workflow).toContain('docker compose config --quiet');
expect(workflow).toContain('docker build --build-arg APP_RELEASE_ID=fixture-ci');
expect(workflow).toContain('GITHUB_RUN_ATTEMPT');
expect(workflow).toContain('--network none');
});
@@ -27,4 +30,5 @@ test('Compose and Docker defaults are isolated and digest-pinned', () => {
for (const pattern of ['*.pem', '*.key', '*.p12', '*.pfx', 'credentials*', 'secrets*', '.npmrc']) {
expect(dockerignore).toContain(pattern);
}
expect(convexConfig).toContain("'APP_CAPABILITY_ALLOWLIST'");
});