fix: gate rectification rollout to canary users
This commit is contained in:
+19
-8
@@ -79,6 +79,8 @@ RECTIFICATION_V3_CREATE_ENABLED=true
|
||||
RECTIFICATION_V3_MIGRATIONS_READY=false
|
||||
# Set only after the authenticated synthetic smoke passes on this exact image.
|
||||
RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA=
|
||||
# During canary only: one canonical synthetic account UUID. Never print or log it.
|
||||
RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS=
|
||||
|
||||
# Recommended multi-model catalog. The JSON references server-only keys.
|
||||
LLM_DEFAULT_MODEL_ID=deepseek-pro
|
||||
@@ -209,25 +211,33 @@ Run `cd frontend && npx supabase db push --linked` with the authorized project
|
||||
account. Verify the linked migration ledger contains all six versions. Do not
|
||||
print the database URL or any service-role credential. Then set
|
||||
`RECTIFICATION_V3_MIGRATIONS_READY=true`, keep
|
||||
`RECTIFICATION_V3_CREATE_ENABLED=true`, leave
|
||||
`RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA` empty, and deploy the tested Git revision.
|
||||
Creation is available for the authorized smoke account, but the revision is not
|
||||
ready for general rollout until that smoke is recorded.
|
||||
`RECTIFICATION_V3_CREATE_ENABLED=true`, set
|
||||
`RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS` to exactly one canonical UUID for
|
||||
the synthetic account, leave `RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA` empty, and
|
||||
deploy the tested Git revision. Never print, log, copy into a ticket, or return
|
||||
that UUID from health or telemetry. Creation is available only for the
|
||||
allowlisted smoke account; ordinary authenticated users can still resume and
|
||||
finish existing cases but cannot start a paid or legacy-imported case.
|
||||
|
||||
Before the smoke, fetch `https://jyotisha.chat/api/health` and verify the full
|
||||
deployment SHA, healthy dependencies, enabled creation, ready migrations,
|
||||
`syntheticSmoke: pending`, and `readyForNewCases: false`. A missing, abbreviated,
|
||||
malformed, or previous-revision smoke SHA must remain pending.
|
||||
`creationAudience: smoke_only`, `syntheticSmoke: pending`, and
|
||||
`readyForNewCases: false`. A missing, abbreviated, malformed, or
|
||||
previous-revision smoke SHA must remain pending. If the create flag, migration
|
||||
flag, deployment SHA, or strict UUID allowlist is invalid, creation audience
|
||||
must be `paused`, including for the smoke account.
|
||||
|
||||
After the smoke sequence below passes, set
|
||||
`RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA` to the exact deployed 40-character
|
||||
lowercase Git SHA and restart the web container. Then fetch health again and
|
||||
lowercase Git SHA, remove `RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS`, and
|
||||
restart the web container. Then fetch health again and
|
||||
verify all of the following against the revision that passed validation:
|
||||
|
||||
- `deployment.gitCommit` exactly equals the tested 40-character Git SHA;
|
||||
- `rollout.conversationalRectificationV3.protocol` is
|
||||
`conversational-evidence-v3`;
|
||||
- `newCaseCreation` and `migrations` are `enabled` and `ready`;
|
||||
- `creationAudience` is `public`;
|
||||
- `syntheticSmoke` is `matched`;
|
||||
- `readyForNewCases` is `true`;
|
||||
- ordinary health checks remain healthy. The health response must never contain
|
||||
@@ -265,7 +275,8 @@ from telemetry.
|
||||
|
||||
Rollback is forward-compatible and non-destructive. First set
|
||||
`RECTIFICATION_V3_CREATE_ENABLED=false`, clear
|
||||
`RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA`, and redeploy a revision that can still
|
||||
`RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA` and
|
||||
`RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS`, and redeploy a revision that can still
|
||||
read/resume v3. Health must report `newCaseCreation: paused`. This stops only
|
||||
new v3 starts: keep reads, resume, answer, pause, confirmation, and saved-question
|
||||
handoff available for existing cases. Never reverse or delete the v3 migrations,
|
||||
|
||||
@@ -24,4 +24,7 @@ the tested deployment SHA.
|
||||
All five issues remain `verified-local` until the production closure artifacts
|
||||
above are attached. A public 200 response, an unverified browser session, or a
|
||||
local in-memory test cannot change them to `closed`. The deployment sequence and
|
||||
non-destructive rollback are defined in `deploy/README.md`.
|
||||
non-destructive rollback are defined in `deploy/README.md`. The executable
|
||||
creation policy keeps rollout `smoke_only` for one unlogged synthetic account
|
||||
until the smoke SHA matches the exact deployed revision; ordinary users cannot
|
||||
incur a new rectification charge during that canary window.
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import type { RectificationNarrativeGenerator } from "../../../lib/conversational-rectification/narrative-agent.ts";
|
||||
import type { BirthTimeJourneyEngine, RectificationQuestionnaire } from "../../../lib/birth-time-journey-service.ts";
|
||||
import type { CandidateResult, LifeEvent } from "../../../lib/birth-time-evidence.ts";
|
||||
import { conversationalRectificationCreationPolicyFromEnvironment } from "../../../lib/conversational-rectification/creation-policy.ts";
|
||||
import {
|
||||
conversationalRectificationLatencyBucket,
|
||||
createConversationalRectificationTelemetry,
|
||||
@@ -589,8 +590,9 @@ async function createProductionService(
|
||||
store: createSupabaseConversationalRectificationStore(admin),
|
||||
billing: createSupabaseConversationalRectificationBilling(admin),
|
||||
get rectificationPriceCredits() { return priceCredits(); },
|
||||
allowNewCaseCreation:
|
||||
process.env.RECTIFICATION_V3_CREATE_ENABLED?.trim().toLowerCase() !== "false",
|
||||
allowNewCaseCreation: conversationalRectificationCreationPolicyFromEnvironment(
|
||||
authenticated.userId,
|
||||
).allowNewCaseCreation,
|
||||
async loadDeclaredProfile(userId) {
|
||||
return loadProductionConversationalRectificationProfile({
|
||||
async loadProfile(receivedUserId) {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
conversationalRectificationCreationPolicyFromEnvironment,
|
||||
conversationalRectificationDeploymentShaFromEnvironment,
|
||||
} from "../../../lib/conversational-rectification/creation-policy.ts";
|
||||
|
||||
type Check = {
|
||||
status: "ok" | "degraded" | "blocked";
|
||||
@@ -7,12 +11,6 @@ type Check = {
|
||||
};
|
||||
|
||||
const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
|
||||
function deployedGitCommit(): string {
|
||||
return process.env.GITHUB_SHA
|
||||
?? process.env.VERCEL_GIT_COMMIT_SHA
|
||||
?? process.env.NEXT_PUBLIC_GIT_COMMIT
|
||||
?? "unknown";
|
||||
}
|
||||
|
||||
function envCheck(names: string[]): Check {
|
||||
const missing = names.filter((name) => !process.env[name]);
|
||||
@@ -59,12 +57,10 @@ function aggregate(checks: Record<string, Check>) {
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const gitCommit = deployedGitCommit();
|
||||
const rectificationV3CreationEnabled =
|
||||
process.env.RECTIFICATION_V3_CREATE_ENABLED?.trim().toLowerCase() !== "false";
|
||||
const gitCommit = conversationalRectificationDeploymentShaFromEnvironment();
|
||||
const rectificationV3MigrationsReady =
|
||||
process.env.RECTIFICATION_V3_MIGRATIONS_READY?.trim().toLowerCase() === "true";
|
||||
const smokeSha = process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA?.trim().toLowerCase() ?? "";
|
||||
const creationPolicy = conversationalRectificationCreationPolicyFromEnvironment();
|
||||
const checks = {
|
||||
web: { status: "ok" } satisfies Check,
|
||||
supabasePublicConfig: envCheck(["NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY"]),
|
||||
@@ -73,12 +69,8 @@ export async function GET() {
|
||||
jyotishApi: await jyotishApiCheck(),
|
||||
};
|
||||
const status = aggregate(checks);
|
||||
const deploymentHasFullSha = /^[0-9a-f]{40}$/.test(gitCommit);
|
||||
const smokeMatchesDeployment = deploymentHasFullSha && smokeSha === gitCommit;
|
||||
const rectificationV3Ready = status === "ok"
|
||||
&& rectificationV3CreationEnabled
|
||||
&& rectificationV3MigrationsReady
|
||||
&& smokeMatchesDeployment;
|
||||
&& creationPolicy.audience === "public";
|
||||
return NextResponse.json(
|
||||
{
|
||||
status,
|
||||
@@ -89,9 +81,10 @@ export async function GET() {
|
||||
rollout: {
|
||||
conversationalRectificationV3: {
|
||||
protocol: "conversational-evidence-v3",
|
||||
newCaseCreation: rectificationV3CreationEnabled ? "enabled" : "paused",
|
||||
newCaseCreation: creationPolicy.audience === "paused" ? "paused" : "enabled",
|
||||
creationAudience: creationPolicy.audience,
|
||||
migrations: rectificationV3MigrationsReady ? "ready" : "unverified",
|
||||
syntheticSmoke: smokeMatchesDeployment ? "matched" : "pending",
|
||||
syntheticSmoke: creationPolicy.smokeMatchesDeployment ? "matched" : "pending",
|
||||
readyForNewCases: rectificationV3Ready,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
export type ConversationalRectificationCreationAudience =
|
||||
| "paused"
|
||||
| "smoke_only"
|
||||
| "public";
|
||||
|
||||
export type ConversationalRectificationCreationPolicyInput = Readonly<{
|
||||
userId?: string | null;
|
||||
creationEnabled?: string;
|
||||
migrationsReady?: string;
|
||||
deploymentSha?: string;
|
||||
smokeSha?: string;
|
||||
syntheticSmokeUserIds?: string;
|
||||
}>;
|
||||
|
||||
export type ConversationalRectificationCreationPolicy = Readonly<{
|
||||
audience: ConversationalRectificationCreationAudience;
|
||||
allowNewCaseCreation: boolean;
|
||||
smokeMatchesDeployment: boolean;
|
||||
}>;
|
||||
|
||||
const fullDeploymentSha = /^[0-9a-f]{40}$/;
|
||||
const canonicalUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
|
||||
export function conversationalRectificationCreationPolicy(
|
||||
input: ConversationalRectificationCreationPolicyInput,
|
||||
): ConversationalRectificationCreationPolicy {
|
||||
const deploymentSha = input.deploymentSha?.trim() ?? "";
|
||||
const smokeSha = input.smokeSha?.trim() ?? "";
|
||||
const baseGatesOpen = input.creationEnabled?.trim().toLowerCase() === "true"
|
||||
&& input.migrationsReady?.trim().toLowerCase() === "true"
|
||||
&& fullDeploymentSha.test(deploymentSha);
|
||||
if (!baseGatesOpen) {
|
||||
return {
|
||||
audience: "paused",
|
||||
allowNewCaseCreation: false,
|
||||
smokeMatchesDeployment: false,
|
||||
};
|
||||
}
|
||||
|
||||
const smokeMatchesDeployment = smokeSha === deploymentSha;
|
||||
if (smokeMatchesDeployment) {
|
||||
return {
|
||||
audience: "public",
|
||||
allowNewCaseCreation: true,
|
||||
smokeMatchesDeployment: true,
|
||||
};
|
||||
}
|
||||
|
||||
const smokeUsers = new Set(
|
||||
(input.syntheticSmokeUserIds ?? "")
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => canonicalUuid.test(value)),
|
||||
);
|
||||
if (smokeUsers.size === 0) {
|
||||
return {
|
||||
audience: "paused",
|
||||
allowNewCaseCreation: false,
|
||||
smokeMatchesDeployment: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
audience: "smoke_only",
|
||||
allowNewCaseCreation: input.userId != null && smokeUsers.has(input.userId),
|
||||
smokeMatchesDeployment: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function conversationalRectificationCreationPolicyFromEnvironment(
|
||||
userId?: string | null,
|
||||
): ConversationalRectificationCreationPolicy {
|
||||
return conversationalRectificationCreationPolicy({
|
||||
userId,
|
||||
creationEnabled: process.env.RECTIFICATION_V3_CREATE_ENABLED,
|
||||
migrationsReady: process.env.RECTIFICATION_V3_MIGRATIONS_READY,
|
||||
deploymentSha: conversationalRectificationDeploymentShaFromEnvironment(),
|
||||
smokeSha: process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA,
|
||||
syntheticSmokeUserIds: process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS,
|
||||
});
|
||||
}
|
||||
|
||||
export function conversationalRectificationDeploymentShaFromEnvironment(): string {
|
||||
return process.env.GITHUB_SHA
|
||||
?? process.env.VERCEL_GIT_COMMIT_SHA
|
||||
?? process.env.NEXT_PUBLIC_GIT_COMMIT
|
||||
?? "unknown";
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
conversationalRectificationCreationPolicy,
|
||||
} from "../src/lib/conversational-rectification/creation-policy.ts";
|
||||
|
||||
const deploymentSha = "0123456789abcdef0123456789abcdef01234567";
|
||||
const smokeUser = "00000000-0000-4000-8000-000000009001";
|
||||
const ordinaryUser = "00000000-0000-4000-8000-0000000090ab";
|
||||
|
||||
function policy(overrides: Partial<Parameters<typeof conversationalRectificationCreationPolicy>[0]> = {}) {
|
||||
return conversationalRectificationCreationPolicy({
|
||||
userId: ordinaryUser,
|
||||
creationEnabled: "true",
|
||||
migrationsReady: "true",
|
||||
deploymentSha,
|
||||
smokeSha: "",
|
||||
syntheticSmokeUserIds: smokeUser,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test("creation pauses unless every base rollout gate is explicitly valid", () => {
|
||||
for (const overrides of [
|
||||
{ creationEnabled: "false" },
|
||||
{ creationEnabled: undefined },
|
||||
{ migrationsReady: "false" },
|
||||
{ migrationsReady: undefined },
|
||||
{ deploymentSha: "deadbee" },
|
||||
{ deploymentSha: deploymentSha.toUpperCase() },
|
||||
]) {
|
||||
assert.deepEqual(policy(overrides), {
|
||||
audience: "paused",
|
||||
allowNewCaseCreation: false,
|
||||
smokeMatchesDeployment: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("pending smoke admits only strictly allowlisted UUIDs and ignores malformed entries", () => {
|
||||
const allowlist = `bad,${smokeUser},${ordinaryUser.toUpperCase()},not-a-uuid`;
|
||||
assert.deepEqual(policy({ userId: smokeUser, syntheticSmokeUserIds: allowlist }), {
|
||||
audience: "smoke_only",
|
||||
allowNewCaseCreation: true,
|
||||
smokeMatchesDeployment: false,
|
||||
});
|
||||
assert.deepEqual(policy({ userId: ordinaryUser, syntheticSmokeUserIds: allowlist }), {
|
||||
audience: "smoke_only",
|
||||
allowNewCaseCreation: false,
|
||||
smokeMatchesDeployment: false,
|
||||
});
|
||||
assert.deepEqual(policy({ syntheticSmokeUserIds: "bad,not-a-uuid" }), {
|
||||
audience: "paused",
|
||||
allowNewCaseCreation: false,
|
||||
smokeMatchesDeployment: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("matching smoke SHA opens creation to every authenticated user", () => {
|
||||
assert.deepEqual(policy({
|
||||
userId: ordinaryUser,
|
||||
smokeSha: deploymentSha,
|
||||
syntheticSmokeUserIds: "",
|
||||
}), {
|
||||
audience: "public",
|
||||
allowNewCaseCreation: true,
|
||||
smokeMatchesDeployment: true,
|
||||
});
|
||||
});
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
import { createRectificationQuestionHandoffCoordinator } from "../src/lib/rectification-question-handoff.ts";
|
||||
import type { ConversationalRectificationTelemetryPayload } from "../src/lib/birth-time-journey-telemetry.ts";
|
||||
import { createConversationalRectificationTelemetry } from "../src/lib/birth-time-journey-telemetry.ts";
|
||||
import { conversationalRectificationCreationPolicy } from "../src/lib/conversational-rectification/creation-policy.ts";
|
||||
|
||||
const userId = "00000000-0000-4000-8000-000000009001";
|
||||
const caseId = "00000000-0000-4000-8000-000000009002";
|
||||
@@ -736,6 +737,7 @@ test("health exposes deployment identity and explicit v3 rollout readiness witho
|
||||
RECTIFICATION_V3_CREATE_ENABLED: process.env.RECTIFICATION_V3_CREATE_ENABLED,
|
||||
RECTIFICATION_V3_MIGRATIONS_READY: process.env.RECTIFICATION_V3_MIGRATIONS_READY,
|
||||
RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA: process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA,
|
||||
RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS: process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS,
|
||||
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||
SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
@@ -745,6 +747,7 @@ test("health exposes deployment identity and explicit v3 rollout readiness witho
|
||||
process.env.RECTIFICATION_V3_CREATE_ENABLED = "true";
|
||||
process.env.RECTIFICATION_V3_MIGRATIONS_READY = "true";
|
||||
process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA = deploymentSha;
|
||||
process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS = userId;
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL = "https://example.invalid";
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "synthetic-public-key";
|
||||
process.env["SUPABASE_SERVICE_ROLE_KEY"] = "synthetic-runtime-secret-never-return";
|
||||
@@ -759,12 +762,14 @@ test("health exposes deployment identity and explicit v3 rollout readiness witho
|
||||
conversationalRectificationV3: {
|
||||
protocol: "conversational-evidence-v3",
|
||||
newCaseCreation: "enabled",
|
||||
creationAudience: "public",
|
||||
migrations: "ready",
|
||||
syntheticSmoke: "matched",
|
||||
readyForNewCases: true,
|
||||
},
|
||||
});
|
||||
assert.equal(JSON.stringify(body).includes("synthetic-runtime-secret-never-return"), false);
|
||||
assert.equal(JSON.stringify(body).includes(userId), false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
for (const [key, value] of Object.entries(prior)) {
|
||||
@@ -774,10 +779,87 @@ test("health exposes deployment identity and explicit v3 rollout readiness witho
|
||||
}
|
||||
});
|
||||
|
||||
test("pending smoke policy blocks ordinary paid and legacy starts before billing", async () => {
|
||||
const ordinaryUser = "00000000-0000-4000-8000-000000009099";
|
||||
const policyInput = {
|
||||
creationEnabled: "true",
|
||||
migrationsReady: "true",
|
||||
deploymentSha,
|
||||
smokeSha: "",
|
||||
syntheticSmokeUserIds: userId,
|
||||
} as const;
|
||||
const ordinaryPolicy = conversationalRectificationCreationPolicy({
|
||||
...policyInput,
|
||||
userId: ordinaryUser,
|
||||
});
|
||||
assert.equal(ordinaryPolicy.audience, "smoke_only");
|
||||
assert.equal(ordinaryPolicy.allowNewCaseCreation, false);
|
||||
|
||||
for (const legacy of [false, true]) {
|
||||
const backend = createSyntheticBackend({
|
||||
legacy,
|
||||
allowNewCaseCreation: ordinaryPolicy.allowNewCaseCreation,
|
||||
});
|
||||
const handler = createBirthTimeConversationPostHandler({
|
||||
authenticate: async () => ({ userId: ordinaryUser, context: {} }),
|
||||
createService: async () => backend.service,
|
||||
telemetry: () => undefined,
|
||||
});
|
||||
const response = await handler(new Request("https://example.invalid/api/birth-time-conversation", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ type: "start", actionId: caseId }),
|
||||
}));
|
||||
assert.equal(response.status, 503);
|
||||
assert.deepEqual(backend.billing(), {
|
||||
reserveCount: 0,
|
||||
chargeCount: 0,
|
||||
releaseCount: 0,
|
||||
state: null,
|
||||
});
|
||||
assert.equal(backend.cases.size, 0);
|
||||
}
|
||||
|
||||
const smokePolicy = conversationalRectificationCreationPolicy({ ...policyInput, userId });
|
||||
const smokeBackend = createSyntheticBackend({
|
||||
allowNewCaseCreation: smokePolicy.allowNewCaseCreation,
|
||||
});
|
||||
await smokeBackend.service.start(userId, { type: "start", actionId: caseId });
|
||||
assert.deepEqual(smokeBackend.billing(), {
|
||||
reserveCount: 1,
|
||||
chargeCount: 1,
|
||||
releaseCount: 0,
|
||||
state: "charged",
|
||||
});
|
||||
|
||||
const publicPolicy = conversationalRectificationCreationPolicy({
|
||||
...policyInput,
|
||||
userId: ordinaryUser,
|
||||
smokeSha: deploymentSha,
|
||||
syntheticSmokeUserIds: "",
|
||||
});
|
||||
const publicBackend = createSyntheticBackend({
|
||||
allowNewCaseCreation: publicPolicy.allowNewCaseCreation,
|
||||
});
|
||||
await publicBackend.service.start(ordinaryUser, { type: "start", actionId: caseId });
|
||||
assert.equal(publicBackend.billing().chargeCount, 1);
|
||||
});
|
||||
|
||||
test("rollback flag stops only new cases while existing v3 resume stays readable", async () => {
|
||||
const enabled = createSyntheticBackend();
|
||||
await enabled.service.start(userId, { type: "start", actionId: caseId });
|
||||
const backend = createSyntheticBackend({ allowNewCaseCreation: false });
|
||||
const pendingPolicy = conversationalRectificationCreationPolicy({
|
||||
userId,
|
||||
creationEnabled: "true",
|
||||
migrationsReady: "true",
|
||||
deploymentSha,
|
||||
smokeSha: "",
|
||||
syntheticSmokeUserIds: "00000000-0000-4000-8000-000000009099",
|
||||
});
|
||||
assert.equal(pendingPolicy.audience, "smoke_only");
|
||||
const backend = createSyntheticBackend({
|
||||
allowNewCaseCreation: pendingPolicy.allowNewCaseCreation,
|
||||
});
|
||||
backend.cases.set(caseId, enabled.cases.get(caseId)!);
|
||||
const handler = createBirthTimeConversationPostHandler({
|
||||
authenticate: async () => ({ userId, context: {} }),
|
||||
@@ -798,7 +880,30 @@ test("rollback flag stops only new cases while existing v3 resume stays readable
|
||||
type: "resume", caseId, actionId: "00000000-0000-4000-8000-000000009051", turnVersion: 0,
|
||||
});
|
||||
assert.equal(resumed.caseId, caseId);
|
||||
assert.equal(backend.activeTime(), "04:58");
|
||||
let existingTurn = await post(handler, {
|
||||
type: "answer", caseId, actionId: "00000000-0000-4000-8000-000000009052",
|
||||
turnVersion: resumed.turnVersion, domain: "career", answer: "2014年7月第一次正式入职",
|
||||
});
|
||||
existingTurn = await post(handler, {
|
||||
type: "pause", caseId, actionId: "00000000-0000-4000-8000-000000009053",
|
||||
turnVersion: existingTurn.turnVersion,
|
||||
});
|
||||
assert.equal(existingTurn.status, "paused");
|
||||
existingTurn = await post(handler, {
|
||||
type: "answer", caseId, actionId: "00000000-0000-4000-8000-000000009054",
|
||||
turnVersion: existingTurn.turnVersion, domain: "education", answer: "2011年6月大学毕业",
|
||||
});
|
||||
existingTurn = await post(handler, {
|
||||
type: "answer", caseId, actionId: "00000000-0000-4000-8000-000000009055",
|
||||
turnVersion: existingTurn.turnVersion, domain: "relocation", answer: "2018年9月搬到外地生活",
|
||||
});
|
||||
assert.equal(existingTurn.status, "confirming");
|
||||
existingTurn = await post(handler, {
|
||||
type: "confirm", caseId, actionId: "00000000-0000-4000-8000-000000009056",
|
||||
turnVersion: existingTurn.turnVersion, time: "05:18",
|
||||
});
|
||||
assert.equal(existingTurn.status, "completed");
|
||||
assert.equal(backend.activeTime(), "05:18");
|
||||
});
|
||||
|
||||
test("a throwing injected telemetry sink cannot turn a committed request into failure", async () => {
|
||||
|
||||
@@ -42,7 +42,10 @@ function assertWorkflowUsesTestedSha(workflow: string) {
|
||||
}
|
||||
|
||||
test("health endpoint exposes deployment identity for production verification", () => {
|
||||
const source = readFileSync(new URL("../src/app/api/health/route.ts", import.meta.url), "utf8");
|
||||
const source = [
|
||||
readFileSync(new URL("../src/app/api/health/route.ts", import.meta.url), "utf8"),
|
||||
readFileSync(new URL("../src/lib/conversational-rectification/creation-policy.ts", import.meta.url), "utf8"),
|
||||
].join("\n");
|
||||
|
||||
assert.match(source, /deployment:/);
|
||||
assert.match(source, /GITHUB_SHA/);
|
||||
@@ -90,7 +93,7 @@ test("v3 readiness requires healthy dependencies and smoke proof for the exact f
|
||||
"GITHUB_SHA", "NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
||||
"SUPABASE_SERVICE_ROLE_KEY", "OPENAI_API_KEY",
|
||||
"RECTIFICATION_V3_CREATE_ENABLED", "RECTIFICATION_V3_MIGRATIONS_READY",
|
||||
"RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA",
|
||||
"RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA", "RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS",
|
||||
] as const;
|
||||
const prior = Object.fromEntries(keys.map((key) => [key, process.env[key]]));
|
||||
const originalFetch = globalThis.fetch;
|
||||
@@ -104,6 +107,7 @@ test("v3 readiness requires healthy dependencies and smoke proof for the exact f
|
||||
OPENAI_API_KEY: "synthetic-model-key",
|
||||
RECTIFICATION_V3_CREATE_ENABLED: "true",
|
||||
RECTIFICATION_V3_MIGRATIONS_READY: "true",
|
||||
RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS: "00000000-0000-4000-8000-000000009001",
|
||||
});
|
||||
globalThis.fetch = async () => Response.json({ status: "ok" });
|
||||
|
||||
@@ -113,6 +117,7 @@ test("v3 readiness requires healthy dependencies and smoke proof for the exact f
|
||||
status: string;
|
||||
rollout: { conversationalRectificationV3: {
|
||||
syntheticSmoke: string;
|
||||
creationAudience: string;
|
||||
readyForNewCases: boolean;
|
||||
} };
|
||||
};
|
||||
@@ -124,6 +129,7 @@ test("v3 readiness requires healthy dependencies and smoke proof for the exact f
|
||||
assert.deepEqual((await readiness()).rollout.conversationalRectificationV3, {
|
||||
protocol: "conversational-evidence-v3",
|
||||
newCaseCreation: "enabled",
|
||||
creationAudience: "smoke_only",
|
||||
migrations: "ready",
|
||||
syntheticSmoke: "pending",
|
||||
readyForNewCases: false,
|
||||
@@ -131,11 +137,18 @@ test("v3 readiness requires healthy dependencies and smoke proof for the exact f
|
||||
|
||||
process.env.GITHUB_SHA = "deadbee";
|
||||
process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA = "deadbee";
|
||||
assert.equal((await readiness()).rollout.conversationalRectificationV3.readyForNewCases, false);
|
||||
assert.deepEqual((await readiness()).rollout.conversationalRectificationV3, {
|
||||
protocol: "conversational-evidence-v3",
|
||||
newCaseCreation: "paused",
|
||||
creationAudience: "paused",
|
||||
migrations: "ready",
|
||||
syntheticSmoke: "pending",
|
||||
readyForNewCases: false,
|
||||
});
|
||||
|
||||
process.env.GITHUB_SHA = currentSha;
|
||||
process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA = oldSha;
|
||||
assert.equal((await readiness()).rollout.conversationalRectificationV3.syntheticSmoke, "pending");
|
||||
assert.equal((await readiness()).rollout.conversationalRectificationV3.creationAudience, "smoke_only");
|
||||
|
||||
process.env.RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA = currentSha;
|
||||
const ready = await readiness();
|
||||
@@ -143,6 +156,7 @@ test("v3 readiness requires healthy dependencies and smoke proof for the exact f
|
||||
assert.deepEqual(ready.rollout.conversationalRectificationV3, {
|
||||
protocol: "conversational-evidence-v3",
|
||||
newCaseCreation: "enabled",
|
||||
creationAudience: "public",
|
||||
migrations: "ready",
|
||||
syntheticSmoke: "matched",
|
||||
readyForNewCases: true,
|
||||
|
||||
Reference in New Issue
Block a user