Files
nextcraft/apps/web/tests/pins.test.ts
T
coreci-nextcraft-po e32e54e7aa
CI / node (push) Successful in 40s
Release / release (push) Successful in 2m57s
CI / python (push) Successful in 4m3s
fix(test): bound the G-328 placement-surface slice at the withdraw gate (the verify-CTA copy is a different, sanctioned surface)
2026-10-21 04:46:50 +00:00

945 lines
39 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
/**
* D-057/G-103 source pins (moved from marketplace-shells /
* marketplace-search / environment-kinds): each test below either
* documents a regression lesson (E87) or guards cross-boundary wiring
* (apps/web ↔ packages/types ↔ apps/ai-service) invisible to typecheck
* and behavioral tests. Paths are anchored on import.meta.url so the
* suite passes from any CWD.
*/
const root = new URL("..", import.meta.url);
function web(path: string): string {
return fileURLToPath(new URL(path, root));
}
// -- E87 regression lesson (G-101) --------------------------------------------
test("E87 regression pin (G-101): generateStaticParams STAYS with ONE placeholder param", async () => {
const jobs = readFileSync(
web("app/(marketplace)/marketplace/jobs/[jobId]/page.tsx"),
"utf8",
);
const employers = readFileSync(
web("app/(marketplace)/marketplace/employers/[employerId]/page.tsx"),
"utf8",
);
for (const [name, src] of [
["jobs/[jobId]", jobs],
["employers/[employerId]", employers],
] as const) {
assert.match(src, /export function generateStaticParams/, `${name}: must keep generateStaticParams`);
assert.match(
src,
/\[{ jobId: '_' }\]|\[{ employerId: '_' }\]/,
`${name}: exactly one placeholder param`,
);
assert.ok(!src.includes("'use client'"), `${name}: server shell (client lives in the component)`);
}
});
// -- cross-boundary wire shapes (packages/types ↔ ai-service py contracts) ------
test("wire pin: PostingSummary mirrors marketplace/base.py field-for-field (dual-schema sync)", async () => {
const typesSrc = readFileSync(web("../../packages/types/marketplace.ts"), "utf8");
const fields = [
"id:", "source:", "external_id:", "employer_id:", "title:",
"description:", "required_competencies:", "skills:", "seniority:",
"category:", "location:", "remote:", "salary_min:", "salary_max:",
"status:", "tags:", "posted_at:",
];
const block = typesSrc.slice(
typesSrc.indexOf("export interface PostingSummary"),
typesSrc.indexOf("export type PostingDetail"),
);
for (const f of fields) {
assert.ok(block.includes(f), `PostingSummary missing base.py field ${f}`);
}
});
test("wire pin: SearchResult/SearchResponse mirror api/marketplace.py search contracts", async () => {
const typesSrc = readFileSync(web("../../packages/types/marketplace.ts"), "utf8");
const pySrc = readFileSync(
web("../ai-service/ai_service/api/marketplace.py"),
"utf8",
);
const contribBlock = typesSrc.slice(
typesSrc.indexOf("export interface SkillContribution"),
typesSrc.indexOf("export interface SearchResult"),
);
for (const field of ["skill:", "overlap:", "contribution:"]) {
assert.ok(contribBlock.includes(field), `SkillContribution missing ${field}`);
}
const resultBlock = typesSrc.slice(
typesSrc.indexOf("export interface SearchResult"),
typesSrc.indexOf("export interface SearchResponse"),
);
for (const field of [
"posting: PostingSummary;",
"match_score: number | null;",
"skill_breakdown: SkillContribution[];",
]) {
assert.ok(resultBlock.includes(field), `SearchResult missing ${field}`);
}
const responseBlock = typesSrc.slice(typesSrc.indexOf("export interface SearchResponse"));
for (const field of [
"results: SearchResult[];",
"query: string;",
"ranked: number;",
]) {
assert.ok(responseBlock.includes(field), `SearchResponse missing ${field}`);
}
for (const pyField of [
"class SkillBreakdownEntry",
"skill: str",
"overlap: int",
"contribution: float",
"class SearchEntry",
"posting: PostingResponse",
"match_score: float | None",
"skill_breakdown: list[SkillBreakdownEntry]",
"class SearchResponse",
"results: list[SearchEntry]",
"query: str",
"ranked: int",
]) {
assert.ok(pySrc.includes(pyField), `py search contract missing ${pyField}`);
}
});
test("wire pin: PostingFacets rides the dual-schema fetch-all (base.py PostingListResponse)", async () => {
const client = readFileSync(web("lib/engine-client.ts"), "utf8");
assert.ok(/facets\?: PostingFacets/.test(client), "PostingsFetchAll must carry facets");
assert.ok(/facets = body\.facets/.test(client), "listPostings must propagate facets");
const wire = readFileSync(web("../../packages/types/marketplace.ts"), "utf8");
assert.ok(/export interface PostingFacets/.test(wire), "PostingFacets type exists");
assert.ok(/facets: PostingFacets;/.test(wire), "PostingPage carries facets");
});
test("wire pin: ProvenanceField merged view + resume gate routes (A-304, dual-schema)", async () => {
const wire = readFileSync(web("../../packages/types/marketplace.ts"), "utf8");
assert.ok(/export interface ProvenanceField/.test(wire), "ProvenanceField type");
assert.ok(/merged\?: Record<string, ProvenanceField>/.test(wire), "EmployerProfile.merged");
const client = readFileSync(web("lib/engine-client.ts"), "utf8");
assert.ok(/export async function uploadResume/.test(client), "uploadResume");
assert.ok(/export async function confirmResume/.test(client), "confirmResume");
assert.ok(/\/v1\/marketplace\/resume/.test(client), "resume route path");
});
test("wire pin: TaskVariant carries REQUIRED environment + test_command from packages/types (a-11)", async () => {
const src = readFileSync(web("../../packages/types/variants.ts"), "utf8");
assert.match(src, /environment:\s*'build' \| 'design' \| 'simulation';/);
assert.match(src, /test_command:\s*string;/);
const envLine = src.split("\n").find((l: string) => l.includes("environment: 'build'"));
assert.ok(envLine && !envLine.includes("?"), "environment must be required");
const cmdLine = src.split("\n").find((l: string) => l.trim().startsWith("test_command"));
assert.ok(cmdLine && !cmdLine.includes("?"), "test_command must be required");
});
// -- honest-score wire guard: match_score exists ONLY on the search wire --------
test("wire pin: no matchScore on marketplace wire shapes (G-106/G-107, honest absent score)", async () => {
const surfaces = [
"components/marketplace/board-client.tsx",
"components/marketplace/job-detail-client.tsx",
"components/marketplace/employer-profile-client.tsx",
"components/marketplace/job-filters.tsx",
];
for (const f of surfaces) {
const src = readFileSync(web(f), "utf8");
assert.ok(!/matchScore/.test(src), `${f} must not reference matchScore`);
}
const typesSrc = readFileSync(web("../../packages/types/marketplace.ts"), "utf8");
const wireBlock = typesSrc.slice(typesSrc.indexOf("export type PostingStatus"));
const scoreFields = wireBlock
.split("\n")
.filter((l) => /^\s*\w+\??:/.test(l))
.filter((l) => /matchScore/i.test(l));
assert.equal(
scoreFields.length,
0,
`wire shapes must not carry matchScore fields: ${scoreFields}`,
);
assert.ok(
/match_score: number \| null;/.test(wireBlock),
"the search wire shape carries the computed match_score",
);
});
// -- SEO boundary: build-time metadata/sitemap limits under output:'export' ----
test("SEO pin (MH-5c/5d): server pages ship metadata + JSON-LD; NO build-time sitemap/robots (D-056)", async () => {
const board = readFileSync(web("app/(marketplace)/marketplace/page.tsx"), "utf8");
assert.ok(/export const metadata/.test(board), "board page metadata");
assert.ok(/ItemList/.test(board), "board ItemList JSON-LD");
// The board JSON-LD enumerates STRUCTURE only — never runtime postings
// (G-109: the export cannot see them honestly).
assert.ok(/numberOfItems: 0/.test(board), "structure-only ItemList");
assert.ok(
/'@type': 'Organization'/.test(board),
"server-rendered Organization JSON-LD on the board",
);
const pricing = readFileSync(web("app/(marketplace)/marketplace/pricing/page.tsx"), "utf8");
assert.ok(/export const metadata/.test(pricing), "pricing page metadata");
const catalog = readFileSync(web("app/(learner)/catalog/page.tsx"), "utf8");
assert.ok(/export const metadata/.test(catalog), "catalog page metadata");
// Runtime-set client JSON-LD (A-411 accepted limitation).
const detail = readFileSync(web("components/marketplace/job-detail-client.tsx"), "utf8");
assert.ok(/'@type': 'JobPosting'/.test(detail), "JobPosting JSON-LD after fetch");
// NO Next app/sitemap.ts (build-time enumeration cannot see runtime
// postings under output:'export' — D-056). ai-service serves both
// dynamically, so no app/robots.ts either.
assert.ok(!existsSync(web("app/sitemap.ts")), "no build-time app/sitemap.ts");
assert.ok(!existsSync(web("app/robots.ts")), "no build-time app/robots.ts");
});
// -- v0.9 billing (REQ-9-002, G-108 dual-schema) --------------------------------
test("wire pin: Package/Plan/CatalogResponse mirror billing/base.py field-for-field (dual-schema sync)", async () => {
const typesSrc = readFileSync(web("../../packages/types/billing.ts"), "utf8");
const pySrc = readFileSync(
web("../ai-service/ai_service/billing/base.py"),
"utf8",
);
const pkgBlock = typesSrc.slice(
typesSrc.indexOf("export interface Package"),
typesSrc.indexOf("export interface Plan"),
);
for (const f of ["id:", "name:", "price_cents:", "posts_limit:", "duration_days:", "features:"]) {
assert.ok(pkgBlock.includes(f), `Package missing base.py field ${f}`);
}
const planBlock = typesSrc.slice(
typesSrc.indexOf("export interface Plan"),
typesSrc.indexOf("export interface CatalogResponse"),
);
for (const f of ["id:", "name:", "price_cents:", "interval:", "features:"]) {
assert.ok(planBlock.includes(f), `Plan missing base.py field ${f}`);
}
// The py side keeps the same fields in its pydantic contracts.
for (const f of ["posts_limit", "duration_days", "price_cents", "interval"]) {
assert.ok(pySrc.includes(f), `billing/base.py missing wire field ${f}`);
}
// D-081: cents discipline is visible in the TS contract comment.
assert.ok(/integer cents/i.test(typesSrc), "D-081 cents documented in billing.ts");
});
test("pricing page: runtime catalog wiring (REQ-9-002/D-079 — no hardcoded tier constants)", async () => {
const pricing = readFileSync(web("app/(marketplace)/marketplace/pricing/page.tsx"), "utf8");
const client = readFileSync(web("components/marketplace/pricing-client.tsx"), "utf8");
// The page is a server shell rendering the client (A-406 pattern).
assert.ok(/<PricingClient \/>/.test(pricing), "page renders PricingClient");
assert.ok(/export const metadata/.test(pricing), "pricing page metadata retained (A-411)");
// MH-2c: the inline tier constants are GONE (data-driven copy).
assert.ok(!/const JOB_PLANS/.test(pricing), "no JOB_PLANS constant on the page");
assert.ok(!/const TALENT_PLANS/.test(pricing), "no TALENT_PLANS constant on the page");
assert.ok(!/const JOB_PLANS/.test(client), "no hardcoded JOB_PLANS in the client");
assert.ok(!/const TALENT_PLANS/.test(client), "no hardcoded TALENT_PLANS in the client");
// The client fetches the catalog with an AbortController + honest states.
assert.ok(/listBillingCatalog/.test(client), "client fetches the catalog");
assert.ok(/AbortController/.test(client), "AbortController discipline");
assert.ok(/Loading pricing/.test(client), "honest loading state");
assert.ok(/Failed to load pricing|error/.test(client), "honest error state");
// D-079: contact-path CTAs — no self-serve purchase path at pilot.
assert.ok(/Contact Sales/.test(client), "enterprise contact CTA");
// Honest empty state (MH-2 "honest states" third arm) + cents display
// (D-081): the empty branch + the cents→dollars conversion exist in
// the source — no render harness in-tree (D-059), so the branch is
// pinned by source (the E87 pin discipline).
assert.ok(/No pricing tiers are configured yet/.test(client), "honest empty state");
assert.ok(/formatPrice/.test(client), "cents formatting helper");
assert.ok(
/cents === ENTERPRISE_CENTS/.test(client),
"price_cents=0 renders as Custom, never as $0 (honest enterprise contact tier)",
);
assert.ok(
!/price_cents \/ 100|price_cents\* *100/.test(client),
"no raw cents arithmetic outside formatPrice",
);
});
// -- v0.10 employer accounts (REQ-10-003, G-108 dual-schema, MH-3a) ---------------
test("wire pin: EmployerAccount/EmployerClaim/token+me shapes mirror employers/base.py field-for-field (dual-schema sync)", async () => {
const typesSrc = readFileSync(web("../../packages/types/employer.ts"), "utf8");
const pySrc = readFileSync(
web("../ai-service/ai_service/employers/base.py"),
"utf8",
);
// EmployerAccount: every pydantic field asserted on BOTH sides.
const acctBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerAccount"),
typesSrc.indexOf("export interface EmployerClaim {"),
);
for (const f of [
"id: string;",
"email: string;",
"employer_id: string | null;",
"created_at: string;",
"email_verified: false;",
]) {
assert.ok(acctBlock.includes(f), `EmployerAccount missing base.py field ${f}`);
}
// D-097: email_verified can never be true (Literal[False] both sides).
assert.ok(!/email_verified: boolean/.test(acctBlock), "email_verified is false, never boolean");
assert.ok(/email_verified: Literal\[False\]/.test(pySrc), "py side pins Literal[False]");
// EmployerClaim (+ the typed evidence dict).
const claimBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerClaim {"),
typesSrc.indexOf("export interface RegisterEmployerRequest {"),
);
for (const f of [
"id: string;",
"account_id: string;",
"employer_id: string;",
"status: EmployerClaimStatus;",
"evidence:",
"created_at: string;",
"decided_by: string | null;",
"decided_at: string | null;",
]) {
assert.ok(claimBlock.includes(f), `EmployerClaim missing base.py field ${f}`);
}
// The advisory evidence dict shape (claim_evidence.py compute_claim_evidence).
const evidenceBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerClaimEvidence"),
typesSrc.indexOf("export interface EmployerAccount"),
);
for (const f of [
"account_email_domain: string;",
"entity_website_domain: string;",
"match: boolean;",
]) {
assert.ok(evidenceBlock.includes(f), `EmployerClaimEvidence missing D-089 field ${f}`);
}
// Request shapes + TokenResponse + EmployerMe.
const reqBlock = typesSrc.slice(
typesSrc.indexOf("export interface RegisterEmployerRequest {"),
typesSrc.indexOf("export interface EmployerTokenResponse"),
);
for (const f of ["email: string;", "password: string;", "employer_id: string;"]) {
assert.ok(reqBlock.includes(f), `employer request shapes missing base.py field ${f}`);
}
const tokenBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerTokenResponse"),
typesSrc.indexOf("export interface EmployerMe"),
);
for (const f of ["token: string;", "expires_at: string;"]) {
assert.ok(tokenBlock.includes(f), `TokenResponse missing base.py field ${f}`);
}
const meBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerMe"),
typesSrc.indexOf("export type OwnPostingStatus"),
);
for (const f of [
"account: EmployerAccount;",
"claimed_employer_id: string | null;",
"claim: EmployerClaim | null;",
]) {
assert.ok(meBlock.includes(f), `EmployerMe missing base.py field ${f}`);
}
// The py side keeps the same fields in its pydantic contracts.
for (const f of [
"class EmployerAccount",
"class EmployerClaim",
"class RegisterEmployerRequest",
"class LoginEmployerRequest",
"class ClaimEmployerRequest",
"class TokenResponse",
"class EmployerMe",
"ClaimStatus",
"PASSWORD_MIN_LENGTH = 8",
]) {
assert.ok(pySrc.includes(f), `employers/base.py missing wire contract ${f}`);
}
// Claim status parity: the TS union mirrors the py Literal.
const tsStatuses = typesSrc.match(
/export type EmployerClaimStatus = '[a-z]+' \| '[a-z]+' \| '[a-z]+'/,
);
assert.ok(tsStatuses, "EmployerClaimStatus union exists");
assert.ok(
tsStatuses && tsStatuses[0].includes("'pending'") && tsStatuses[0].includes("'approved'") && tsStatuses[0].includes("'rejected'"),
"claim status union = pending|approved|rejected (py ClaimStatus)",
);
});
test("wire pin: OwnPostingSummary REUSES PostingSummary + own-postings response mirrors base.py (G-319/D-093)", async () => {
const typesSrc = readFileSync(web("../../packages/types/employer.ts"), "utf8");
const marketplaceSrc = readFileSync(web("../../packages/types/marketplace.ts"), "utf8");
const pySrc = readFileSync(
web("../ai-service/ai_service/employers/base.py"),
"utf8",
);
// G-319 REUSE: composition (extends PostingSummary), never a
// field-for-field duplicate of the PostingSummary fields.
const ownBlock = typesSrc.slice(
typesSrc.indexOf("export interface OwnPostingSummary"),
typesSrc.indexOf("export interface OwnPostingsFacets"),
);
assert.ok(
/export interface OwnPostingSummary extends PostingSummary \{/.test(ownBlock),
"OwnPostingSummary extends PostingSummary (reuse, never duplication)",
);
assert.ok(/applicant_count: number;/.test(ownBlock), "OwnPostingSummary carries applicant_count");
// The PostingSummary fields must NOT be re-listed inside the own block
// (a duplicate would drift — the REUSE contract).
for (const dup of ["external_id:", "required_competencies:", "salary_min:"]) {
assert.ok(!ownBlock.includes(dup), `OwnPostingSummary duplicates PostingSummary field ${dup}`);
}
// The py side subclasses marketplace.base.Posting (the same reuse).
assert.ok(/class OwnPostingSummary\(Posting\)/.test(pySrc), "py OwnPostingSummary subclasses Posting");
// PII boundary (G-319, binding): the contract carries the count only —
// no learner-identifier field name exists anywhere on the TS shape.
assert.ok(!/learner_id|applicant_name|candidate_id/.test(ownBlock), "PII boundary: counts only");
// OwnPostingsFacets: every lifecycle key on both sides.
const facetsBlock = typesSrc.slice(
typesSrc.indexOf("export interface OwnPostingsFacets"),
typesSrc.indexOf("export interface OwnPostingsResponse"),
);
for (const f of ["draft: number;", "published: number;", "closed: number;"]) {
assert.ok(facetsBlock.includes(f), `OwnPostingsFacets missing lifecycle key ${f}`);
}
assert.ok(/draft: int = 0/.test(pySrc) && /published: int = 0/.test(pySrc) && /closed: int = 0/.test(pySrc), "py OwnPostingsFacets carries the same three keys");
// OwnPostingsResponse: list + facets + honest total, both sides.
const respBlock = typesSrc.slice(typesSrc.indexOf("export interface OwnPostingsResponse"));
for (const f of [
"postings: OwnPostingSummary[];",
"facets: OwnPostingsFacets;",
"total: number;",
]) {
assert.ok(respBlock.includes(f), `OwnPostingsResponse missing base.py field ${f}`);
}
assert.ok(/postings: list\[OwnPostingSummary\]/.test(pySrc), "py OwnPostingsResponse carries the postings list");
// PostingSummary itself stays single-sourced in marketplace.ts (the
// wire pin at pins.test.ts:48 owns its field list).
assert.ok(
/export interface PostingSummary \{/.test(marketplaceSrc),
"PostingSummary remains defined once in marketplace.ts",
);
});
test("wire pin: employer types surface through the package index (the billing.ts precedent)", async () => {
const indexSrc = readFileSync(web("../../packages/types/index.ts"), "utf8");
assert.ok(
/export \* from '\.\/employer';/.test(indexSrc),
"index.ts re-exports employer.ts",
);
});
test("wire pin: EmployerPostingCreate/Edit/WriteResponse mirror employers/base.py field-for-field (P4 write contracts)", async () => {
const typesSrc = readFileSync(web("../../packages/types/employer.ts"), "utf8");
const pySrc = readFileSync(
web("../ai-service/ai_service/employers/base.py"),
"utf8",
);
// EmployerPostingCreate: every pydantic field asserted on BOTH sides.
const createBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerPostingCreate"),
typesSrc.indexOf("export interface EmployerPostingEdit"),
);
for (const f of [
"title: string;",
"description?: string;",
"required_competencies?: string[];",
"skills?: string[];",
"seniority?: string | null;",
"category?: string | null;",
"location?: string;",
"remote?: boolean;",
"salary_min?: number | null;",
"salary_max?: number | null;",
]) {
assert.ok(createBlock.includes(f), `EmployerPostingCreate missing base.py field ${f}`);
}
// The employer_id field is deliberately ABSENT (the entity derives
// from the session's claim, never the body — Task 4-1-02).
assert.ok(
!/employer_id/.test(createBlock),
"EmployerPostingCreate never carries employer_id (claim-derived)",
);
// The py side keeps the same fields (base.py EmployerPostingCreate).
const pyCreateBlock = pySrc.slice(
pySrc.indexOf("class EmployerPostingCreate"),
pySrc.indexOf("class EmployerPostingEdit"),
);
for (const f of [
"title: str = Field(min_length=1)",
"description: str =",
"required_competencies: list[str]",
"skills: list[str]",
"seniority: str | None",
"category: str | None",
"location: str =",
"remote: bool =",
"salary_min: int | None",
"salary_max: int | None",
]) {
assert.ok(pyCreateBlock.includes(f), `base.py EmployerPostingCreate missing ${f}`);
}
// EmployerPostingEdit: the partial shape (title/description/skills).
const editBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerPostingEdit"),
typesSrc.indexOf("export interface EmployerPostingWriteResponse"),
);
for (const f of ["title?: string;", "description?: string;", "skills?: string[];"]) {
assert.ok(editBlock.includes(f), `EmployerPostingEdit missing base.py field ${f}`);
}
const pyEditBlock = pySrc.slice(
pySrc.indexOf("class EmployerPostingEdit"),
pySrc.indexOf("class EmployerPostingWriteResponse"),
);
for (const f of [
"title: str | None = Field(default=None, min_length=1)",
"description: str | None = None",
"skills: list[str] | None = None",
]) {
assert.ok(pyEditBlock.includes(f), `base.py EmployerPostingEdit missing ${f}`);
}
// EmployerPostingWriteResponse: the write-verdict shape both sides.
const respBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerPostingWriteResponse"),
);
for (const f of [
"id: string;",
"source: string;",
"external_id: string;",
"employer_id: string;",
"title: string;",
"status: string;",
]) {
assert.ok(respBlock.includes(f), `EmployerPostingWriteResponse missing base.py field ${f}`);
}
const pyRespBlock = pySrc.slice(pySrc.indexOf("class EmployerPostingWriteResponse"));
for (const f of ["id: str", "source: str", "external_id: str", "employer_id: str", "title: str", "status: str"]) {
assert.ok(pyRespBlock.includes(f), `base.py EmployerPostingWriteResponse missing ${f}`);
}
});
// -- v0.10 P5 billing + pricing CTAs (REQ-10-005, Task 5-2-01) ----------------------
test("wire pin: EmployerCheckout/Entitlement/MockComplete shapes mirror employers/base.py field-for-field (P5 billing contracts)", async () => {
const typesSrc = readFileSync(web("../../packages/types/employer.ts"), "utf8");
const pySrc = readFileSync(
web("../ai-service/ai_service/employers/base.py"),
"utf8",
);
// EmployerCheckoutCreate: item_id only (D-076's employer_id body
// field is GONE — the claimed entity is the only buyer).
const createBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerCheckoutCreate"),
typesSrc.indexOf("export interface EmployerCheckoutResponse"),
);
assert.ok(
createBlock.includes("item_id: string;"),
"EmployerCheckoutCreate carries item_id",
);
assert.ok(
!/employer_id/.test(createBlock),
"EmployerCheckoutCreate never carries employer_id (claim-derived buyer)",
);
// EmployerCheckoutResponse: the v0.9 CheckoutResponse shape reused.
const respBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerCheckoutResponse"),
typesSrc.indexOf("export interface EmployerPurchaseEntitlement"),
);
for (const f of [
"session_id: string;",
"provider_session_id: string;",
"url: string;",
"mode: string;",
]) {
assert.ok(respBlock.includes(f), `EmployerCheckoutResponse missing base.py field ${f}`);
}
// The purchase/subscription entitlement summaries — the EXACT keys
// billing/store.py get_entitlement emits (the v0.9 shape reused).
const purchaseBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerPurchaseEntitlement"),
typesSrc.indexOf("export interface EmployerSubscriptionEntitlement"),
);
for (const f of [
"id: string;",
"package_id: string;",
"package_name: string;",
"posts_used: number;",
"posts_limit: number | null;",
"expires_at: string | null;",
]) {
assert.ok(purchaseBlock.includes(f), `EmployerPurchaseEntitlement missing store key ${f}`);
}
const subBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerSubscriptionEntitlement"),
typesSrc.indexOf("export interface EmployerEntitlementResponse"),
);
for (const f of [
"id: string;",
"plan_id: string;",
"plan_name: string;",
"status: string;",
"current_period_end: string | null;",
"past_due_at: string | null;",
]) {
assert.ok(subBlock.includes(f), `EmployerSubscriptionEntitlement missing store key ${f}`);
}
// EmployerEntitlementResponse: honest nulls until a real grant.
const entBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerEntitlementResponse"),
typesSrc.indexOf("export interface EmployerMockCompleteRequest"),
);
assert.ok(entBlock.includes("employer_id: string;"), "employer_id");
assert.ok(
entBlock.includes("purchase: EmployerPurchaseEntitlement | null;"),
"purchase is honestly nullable",
);
assert.ok(
entBlock.includes("subscription: EmployerSubscriptionEntitlement | null;"),
"subscription is honestly nullable",
);
// EmployerMockCompleteRequest/Response (D-082/G-301/G-302).
const reqBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerMockCompleteRequest"),
typesSrc.indexOf("export interface EmployerMockCompleteResponse"),
);
assert.ok(
reqBlock.includes("provider_session_id: string;"),
"the untrusted ID reference (G-302)",
);
const mockBlock = typesSrc.slice(
typesSrc.indexOf("export interface EmployerMockCompleteResponse"),
);
for (const f of ["completed: boolean;", "payment_status: string;"]) {
assert.ok(mockBlock.includes(f), `EmployerMockCompleteResponse missing ${f}`);
}
// The py side keeps the same contracts (base.py P5 classes).
const pyP5Block = pySrc.slice(pySrc.indexOf("class EmployerCheckoutCreate"));
for (const f of [
"class EmployerCheckoutCreate",
"item_id: str = Field(min_length=1)",
"class EmployerCheckoutResponse",
"session_id: str",
"provider_session_id: str",
"url: str",
"mode: str",
"class EmployerEntitlementResponse",
"purchase: dict[str, Any] | None = None",
"subscription: dict[str, Any] | None = None",
"class EmployerMockCompleteRequest",
"class EmployerMockCompleteResponse",
"completed: bool",
"payment_status: str",
]) {
assert.ok(pyP5Block.includes(f), `base.py P5 billing contract missing ${f}`);
}
});
test("wire pin: admin overview shapes mirror employers_admin.py operator contracts (Task 5-2-02, D-094)", async () => {
const typesSrc = readFileSync(web("../../packages/types/employer.ts"), "utf8");
const pySrc = readFileSync(
web("../ai-service/ai_service/api/employers_admin.py"),
"utf8",
);
// AdminEmployerAccount: the accounts-list wire row, both sides.
const acctBlock = typesSrc.slice(
typesSrc.indexOf("export interface AdminEmployerAccount"),
typesSrc.indexOf("export interface AdminAccountsResponse"),
);
for (const f of [
"id: string;",
"email: string;",
"employer_id: string | null;",
"created_at: string;",
"email_verified: false;",
]) {
assert.ok(acctBlock.includes(f), `AdminEmployerAccount missing employers_admin.py field ${f}`);
}
// D-097: the unverified marker can never be true; D-087: the
// password hash is NEVER a wire field.
assert.ok(!/email_verified: boolean/.test(acctBlock), "email_verified is false, never boolean");
assert.ok(!/password_hash/.test(acctBlock), "password_hash never rides the wire (D-087)");
assert.ok(/email_verified: Literal\[False\]/.test(pySrc), "py side pins Literal[False]");
// AdminAccountsResponse / AdminClaimsResponse / ClaimDecisionResponse.
const accountsRespBlock = typesSrc.slice(
typesSrc.indexOf("export interface AdminAccountsResponse"),
typesSrc.indexOf("export interface AdminClaimsResponse"),
);
assert.ok(
accountsRespBlock.includes("accounts: AdminEmployerAccount[];"),
"AdminAccountsResponse.accounts",
);
assert.ok(accountsRespBlock.includes("count: number;"), "AdminAccountsResponse.count");
const claimsRespBlock = typesSrc.slice(
typesSrc.indexOf("export interface AdminClaimsResponse"),
typesSrc.indexOf("export interface ClaimDecisionResponse"),
);
assert.ok(
claimsRespBlock.includes("claims: EmployerClaim[];"),
"the queue reuses the EmployerClaim wire shape",
);
assert.ok(claimsRespBlock.includes("count: number;"), "AdminClaimsResponse.count");
const decisionBlock = typesSrc.slice(
typesSrc.indexOf("export interface ClaimDecisionResponse"),
typesSrc.indexOf("export interface AdminAuditEvent"),
);
assert.ok(decisionBlock.includes("claim: EmployerClaim;"), "ClaimDecisionResponse.claim");
// AdminAuditEvent / AdminAuditResponse — the BillingEventRow shape.
const auditBlock = typesSrc.slice(
typesSrc.indexOf("export interface AdminAuditEvent"),
typesSrc.indexOf("export interface AdminAuditResponse"),
);
for (const f of [
"id: number;",
"account_id: string | null;",
"employer_id: string | null;",
"kind: string;",
"name: string;",
"detail: Record<string, unknown>;",
"operator: string | null;",
"created_at: string | null;",
]) {
assert.ok(auditBlock.includes(f), `AdminAuditEvent missing employers_admin.py field ${f}`);
}
const auditRespBlock = typesSrc.slice(
typesSrc.indexOf("export interface AdminAuditResponse"),
);
assert.ok(auditRespBlock.includes("events: AdminAuditEvent[];"), "AdminAuditResponse.events");
assert.ok(auditRespBlock.includes("count: number;"), "AdminAuditResponse.count");
// The py side keeps the same contracts (employers_admin.py).
for (const f of [
"class AdminEmployerAccount",
"class AdminAccountsResponse",
"class AdminClaimsResponse",
"class ClaimDecisionResponse",
"class AdminAuditEvent",
"class AdminAuditResponse",
"claims: list[EmployerClaim]",
"claim: EmployerClaim",
"events: list[AdminAuditEvent]",
]) {
assert.ok(pySrc.includes(f), `employers_admin.py missing wire contract ${f}`);
}
});
test("pricing pin: purchasable CTAs route to /employer/login; the enterprise Contact Sales pin STAYS (Task 5-2-01, MH-5d)", async () => {
const client = readFileSync(
web("components/marketplace/pricing-client.tsx"),
"utf8",
);
// The employer-login path is the purchasable-tier CTA href.
assert.ok(
/href=\{isEnterprise \? '[^']*' : '\/employer\/login'\}/.test(client),
"purchasable tiers route to the employer-login path",
);
// The enterprise contact CTA is RETAINED (R-61: the pins.test.ts:250
// pin holds — price_cents=0 rows stay operator-mediated).
assert.ok(/Contact Sales/.test(client), "enterprise Contact Sales retained");
assert.ok(
/if \(isEnterprise\) return 'Contact Sales';/.test(client),
"enterprise arm keeps the contact copy",
);
// The operator-mediated-only pilot copy is GONE — replaced by the
// self-serve truth with the admin fallback noted.
assert.ok(
!/operator-mediated during the pilot/.test(client),
"the D-079 operator-mediated copy is superseded",
);
assert.ok(
/operator-assisted checkout remains available as a fallback/.test(client),
"the admin-fallback is noted honestly",
);
assert.ok(/mock provider \(honestly labeled/.test(client), "the pilot mock path is labeled");
});
// v0.11 P3 (D-110): the placement wire pins — placement.ts mirrors
// placements/base.py field-for-field (the G-108 discipline; the
// employer.ts block's template). Every change happens in both places.
test("wire pin: Placement mirrors placements/base.py PlacementRead field-for-field (dual-schema sync)", async () => {
const typesSrc = readFileSync(web("../../packages/types/placement.ts"), "utf8");
const pySrc = readFileSync(
web("../../apps/ai-service/ai_service/placements/base.py"),
"utf8",
);
const block = typesSrc.slice(
typesSrc.indexOf("export interface Placement {"),
typesSrc.indexOf("export interface PlacementEvent"),
);
const fields = [
"id:", "learner_id:", "employer_id:", "posting_id:",
"application_id:", "role_title:", "start_date:", "status:",
"created_at:", "decided_at:",
];
for (const f of fields) {
assert.ok(block.includes(f), `Placement missing base.py field ${f}`);
}
// start_date is a DATE on both sides (D-103) — never a datetime
assert.ok(
/start_date: date/.test(pySrc),
"base.py start_date is a pydantic date",
);
assert.ok(
/start_date: string;/.test(block),
"placement.ts start_date is the ISO date string",
);
});
test("wire pin: ApplicationStatus union mirrors the D-099 matrix (status-union regex pin)", async () => {
const typesSrc = readFileSync(web("../../packages/types/placement.ts"), "utf8");
const block = typesSrc.slice(
typesSrc.indexOf("export type ApplicationStatus"),
typesSrc.indexOf("export type PlacementStatus"),
);
// the seven D-099 statuses, in matrix order
const statuses = [
"'submitted'", "'in_review'", "'interview'", "'offer'",
"'hired'", "'rejected'", "'withdrawn'",
];
for (const status of statuses) {
assert.ok(block.includes(status), `ApplicationStatus missing ${status}`);
}
assert.ok(
!/"submitted"/.test(block),
"snake_case on the wire (no kebab 'in-review')",
);
});
test("wire pin: PlacementOverview mirrors the D-107 computation shape", async () => {
const typesSrc = readFileSync(web("../../packages/types/placement.ts"), "utf8");
const block = typesSrc.slice(
typesSrc.indexOf("export interface PlacementOverview"),
);
const fields = [
"total_active_placements:", "distinct_learners_placed:",
"guiding_goal:", "by_employer:", "by_cohort_month:",
];
for (const f of fields) {
assert.ok(block.includes(f), `PlacementOverview missing ${f}`);
}
assert.ok(
/export const GUIDING_GOAL = 1000;/.test(typesSrc),
"the north star is 1,000 learners",
);
});
test("wire pin: EmployerApplicationSummary carries learner_id per the G-319 supersession (D-105)", async () => {
const typesSrc = readFileSync(web("../../packages/types/placement.ts"), "utf8");
const block = typesSrc.slice(
typesSrc.indexOf("export interface EmployerApplicationSummary"),
typesSrc.indexOf("export interface EmployerApplicationsResponse"),
);
assert.ok(/learner_id: string;/.test(block), "learner_id rides the summary");
// the own-postings LIST stays counts-only: OwnPostingSummary (the
// v0.10 contract) still carries ONLY applicant_count — never a
// learner_id field
const employerTypes = readFileSync(web("../../packages/types/employer.ts"), "utf8");
const ownBlock = employerTypes.slice(
employerTypes.indexOf("export interface OwnPostingSummary"),
employerTypes.indexOf("export interface OwnPostingsFacets"),
);
assert.ok(
!/learner_id/.test(ownBlock),
"the G-319 pin split: OwnPostingSummary stays counts-only",
);
});
// v0.11 P5 (REQ-11-005, D-110): the learner status-read wire pins +
// the A-304 badge-honesty census — no placement badge on the mock
// talent surfaces (D-092 binding: a badge on a mock page is
// fabricated placement); the learner's-own application surface is the
// ONLY placement render.
test("wire pin: LearnerApplicationStatus mirrors the base.py learner status read (D-104)", async () => {
const typesSrc = readFileSync(web("../../packages/types/placement.ts"), "utf8");
const pySrc = readFileSync(
web("../../apps/ai-service/ai_service/placements/base.py"),
"utf8",
);
const summaryBlock = typesSrc.slice(
typesSrc.indexOf("export interface LearnerApplicationSummary"),
typesSrc.indexOf("export interface LearnerApplicationStatus"),
);
for (const f of ["id:", "posting_id:", "learner_id:", "status:", "created_at:"]) {
assert.ok(summaryBlock.includes(f), `LearnerApplicationSummary missing ${f}`);
}
const statusBlock = typesSrc.slice(
typesSrc.indexOf("export interface LearnerApplicationStatus"),
);
assert.ok(/application: LearnerApplicationSummary;/.test(statusBlock));
assert.ok(/events: PlacementEvent\[\];/.test(statusBlock));
assert.ok(/placement: Placement \| null;/.test(statusBlock));
// the backend contract carries the same three arms (base.py)
const pyBlock = pySrc.slice(pySrc.indexOf("class LearnerApplicationStatusResponse"));
assert.ok(/application: LearnerApplicationSummary/.test(pyBlock));
assert.ok(/events: list\[PlacementEventRead\]/.test(pyBlock));
assert.ok(/placement: PlacementRead \| None/.test(pyBlock));
// the mine-read contract (G-322)
const mineBlock = typesSrc.slice(typesSrc.indexOf("export interface MineApplication"));
assert.ok(/application_id: string;/.test(mineBlock));
assert.ok(/status: ApplicationStatus;/.test(mineBlock));
assert.ok(
/class MineApplicationResponse/.test(pySrc),
"base.py carries the mine contract",
);
});
test("pin: A-304 badge honesty — no placement badge on the mock talent surfaces (D-092/D-109)", async () => {
const surfaces = [
"app/(employer)/employer/talent/page.tsx",
"app/(employer)/employer/talent/[candidateId]/page.tsx",
"components/employer/talent-search.tsx",
];
for (const surface of surfaces) {
const src = readFileSync(web(surface), "utf8");
assert.ok(
!/placement/i.test(src),
`${surface}: a badge on a mock surface is fabricated placement (D-092 binding)`,
);
}
// the learner's-own application surface is the ONLY placement render
const jobDetail = readFileSync(
web("components/marketplace/job-detail-client.tsx"),
"utf8",
);
assert.ok(
/appStatus\.placement &&/.test(jobDetail),
"the placement confirmation renders only on the learner's-own application surface",
);
const placementBlock = jobDetail.slice(
jobDetail.indexOf("appStatus.placement &&"),
jobDetail.indexOf("{WITHDRAWABLE_STATUSES.has("),
);
assert.ok(
/Placement recorded/.test(placementBlock),
"G-328: the placement copy says RECORDED",
);
assert.ok(
!/verified/i.test(placementBlock),
"G-328: no verification claim on the placement surface",
);
});