fix(release): withhold unqualified CV review and record gates
This commit is contained in:
@@ -128,3 +128,58 @@ test("bound CV proposals use real owned saves and reject fabricated or stale out
|
||||
expect(state.calls.some((call: { path: string; body: { output_mode?: string } }) => call.path === "/cv/rewrite" && call.body.output_mode === "cv_blocks_json")).toBe(true);
|
||||
await testInfo.attach("synthetic-provider-contract-calls", { body: JSON.stringify(state, null, 2), contentType: "application/json" });
|
||||
});
|
||||
|
||||
test("Production withholds CV review before provider calls or content writes while existing AI remains available", async ({ request, playwright }) => {
|
||||
const productionUrl = `${process.env.CV_AI_READINESS_PRODUCTION_API_URL}/api`;
|
||||
const api = await playwright.request.newContext();
|
||||
try {
|
||||
const login = await api.post(`${productionUrl}/auth/login`, { data: { email: "cv-ai-readiness@example.test", password } });
|
||||
expect(login.status()).toBe(200);
|
||||
// This is an isolated loopback API behind an HTTPS canonical origin. Keep its Secure cookies
|
||||
// unchanged and explicitly transport them across the test-only internal HTTP connection.
|
||||
const cookies = login.headersArray().filter(header => header.name.toLowerCase() === "set-cookie").map(header => header.value.split(";")[0]);
|
||||
const headers: Record<string, string> = { Cookie: cookies.join("; "), "X-CSRF-TOKEN": cookies.find(cookie => cookie.startsWith("XSRF-TOKEN="))!.slice("XSRF-TOKEN=".length) };
|
||||
const me = await api.get(`${productionUrl}/auth/me`, { headers });
|
||||
expect(me.status()).toBe(200);
|
||||
const account = await me.json();
|
||||
expect(account.entitlements.ai).toBe(true);
|
||||
headers["X-Session-Context"] = account.sessionContext;
|
||||
expect((await request.post(`${providerUrl}/fixture/reset`)).status()).toBe(200);
|
||||
|
||||
const profile = { version: "1", contact: { fullName: "Production Gate Fixture", headline: source }, summary: [source] };
|
||||
expect((await api.put(`${productionUrl}/career/profile`, { headers, data: { profile } })).status()).toBe(200);
|
||||
const created = await api.post(`${productionUrl}/cv/variants`, { headers, data: { name: "Withheld AI review", settings: { themeId: "minimal", language: "en", sections: [{ key: "summary" }] } } });
|
||||
expect(created.status()).toBe(200);
|
||||
const cv = await created.json();
|
||||
const snapshot = async () => {
|
||||
const responses = await Promise.all([
|
||||
api.get(`${productionUrl}/cv/variants/${cv.id}`, { headers }), api.get(`${productionUrl}/cv/variants/${cv.id}/versions`, { headers }),
|
||||
api.get(`${productionUrl}/career/profile`, { headers }), api.get(`${productionUrl}/career/profile/versions`, { headers }),
|
||||
]);
|
||||
for (const response of responses) expect(response.status()).toBe(200);
|
||||
return Promise.all(responses.map(response => response.json()));
|
||||
};
|
||||
const before = await snapshot();
|
||||
for (const action of ["improve", "document-improve"]) {
|
||||
const response = await api.post(`${productionUrl}/cv/ai/assist`, { headers, data: {
|
||||
variantId: cv.id, expectedVersion: cv.version, action, language: "en",
|
||||
...(action.startsWith("document-") ? {} : { targetId: "section:summary", text: source }),
|
||||
} });
|
||||
expect(response.status()).toBe(503);
|
||||
expect(await response.json()).toEqual({ code: "cv_ai_unavailable" });
|
||||
}
|
||||
const applied = await api.post(`${productionUrl}/cv/variants/${cv.id}/ai/apply`, { headers, data: { proposalToken: "unused-proposal", result: rewrite } });
|
||||
expect(applied.status()).toBe(503);
|
||||
expect(await applied.json()).toEqual({ code: "cv_ai_unavailable" });
|
||||
expect(await snapshot()).toEqual(before);
|
||||
expect(await (await request.get(`${providerUrl}/fixture/state`)).json()).toEqual({ mode: "supported", counts: { rewrite: 0, evaluate: 0 }, calls: [] });
|
||||
|
||||
expect((await api.get(`${productionUrl}/jobapplications/1/ai/modules`, { headers })).status()).toBe(200);
|
||||
const legacy = await api.post(`${productionUrl}/profile-cv/rewrite-section`, { headers, data: { sourceText: source, language: "en" } });
|
||||
expect(legacy.status()).toBe(200);
|
||||
expect((await legacy.json()).text).toBe(rewrite);
|
||||
expect(await snapshot()).toEqual(before);
|
||||
const state = await (await request.get(`${providerUrl}/fixture/state`)).json();
|
||||
expect(state.counts).toEqual({ rewrite: 1, evaluate: 0 });
|
||||
} finally { await api.dispose(); }
|
||||
});
|
||||
|
||||
@@ -6,12 +6,14 @@ import path from "node:path";
|
||||
// Separate real-HTTP contract suite. Neither process uses the normal browser-suite ports.
|
||||
const apiUrl = "http://127.0.0.1:15313";
|
||||
const providerUrl = "http://127.0.0.1:18133";
|
||||
const productionApiUrl = "http://127.0.0.1:15314";
|
||||
const dataRoot = process.env.CV_AI_READINESS_DATA_ROOT ??= fs.mkdtempSync(path.join(os.tmpdir(), "jobtracker-cv-ai-readiness-"));
|
||||
process.env.CV_AI_READINESS_API_URL = apiUrl;
|
||||
process.env.CV_AI_READINESS_PROVIDER_URL = providerUrl;
|
||||
process.env.CV_AI_READINESS_PRODUCTION_API_URL = productionApiUrl;
|
||||
const quote = (value: string) => `"${value.replaceAll('"', '\\"')}"`;
|
||||
|
||||
export default defineConfig({
|
||||
const config = defineConfig({
|
||||
testDir: "./ai-tests",
|
||||
testMatch: "cv-ai-readiness.spec.ts",
|
||||
workers: 1,
|
||||
@@ -61,3 +63,22 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const webServers = Array.isArray(config.webServer) ? config.webServer : [config.webServer!];
|
||||
const backend = webServers[1];
|
||||
export default defineConfig({
|
||||
...config,
|
||||
webServer: [...webServers, {
|
||||
...backend,
|
||||
// Launch settings otherwise override the Production environment with Development.
|
||||
command: backend.command.replace(" run ", " run --no-launch-profile ").replace(apiUrl, productionApiUrl),
|
||||
url: `${productionApiUrl}/ready`,
|
||||
env: {
|
||||
...backend.env,
|
||||
ASPNETCORE_ENVIRONMENT: "Production",
|
||||
App__PublicBaseUrl: productionApiUrl.replace("http:", "https:"),
|
||||
Data__Root: path.join(dataRoot, "production"),
|
||||
ConnectionStrings__JobTracker: `Data Source=${path.join(dataRoot, "production", "readiness.sqlite")}`,
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user