待测试

This commit is contained in:
2026-07-28 07:58:26 +08:00
parent 1e9e25fe67
commit 6fbcf21024
22 changed files with 1294 additions and 264 deletions
@@ -0,0 +1,57 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$documents = @(Get-ChildItem -LiteralPath $root -File -Filter '*.openapi.json')
if ($documents.Count -ne 1) { throw 'Exactly one current OpenAPI JSON export is required at the repository root' }
$document = Get-Content -Raw -Encoding UTF8 -LiteralPath $documents[0].FullName | ConvertFrom-Json
$operations = @(
foreach ($pathProperty in $document.paths.PSObject.Properties) {
foreach ($methodProperty in $pathProperty.Value.PSObject.Properties) {
if ($methodProperty.Name -in @('get', 'post', 'put', 'delete', 'patch')) {
[PSCustomObject]@{ Path = $pathProperty.Name; Method = $methodProperty.Name }
}
}
}
)
if ($operations.Count -ne 149) { throw "Current OpenAPI operation count drifted: $($operations.Count)" }
function Get-Operation {
param([string]$Path, [string]$Method)
$pathProperty = $document.paths.PSObject.Properties[$Path]
if (-not $pathProperty) { throw "Missing path: $Path" }
$operation = $pathProperty.Value.PSObject.Properties[$Method]
if (-not $operation) { throw "Missing operation: $Method $Path" }
return $operation.Value
}
$videoList = Get-Operation -Path '/genealogy/app/genealogies/{genealogyId}/videos' -Method 'post'
if ($videoList.requestBody.content.'application/json'.schema.'$ref' -ne '#/components/schemas/VideoBody') {
throw 'Video create must consume VideoBody'
}
$videoBody = $document.components.schemas.VideoBody
if ((@($videoBody.required) -join ',') -ne 'videoTitle,videoOssId') {
throw 'VideoBody required fields drifted'
}
if ($videoList.responses.'200'.content.'application/json'.schema) {
throw 'Video create unexpectedly gained a response DTO; review F10 integration'
}
$videoPage = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'pages/family/f10-video-list.vue')
foreach ($token in @('pickAndUploadVideo', 'appApi.createVideo', 'videoTitle', 'videoOssId: receipt.value.ossId')) {
if (-not $videoPage.Contains($token)) { throw "F10 video publish contract missing: $token" }
}
foreach ($forbidden in @('v-model="form.videoOssId"', 'v-model="form.status"', 'v-model="form.durationSeconds"')) {
if ($videoPage.Contains($forbidden)) { throw "F10 must not expose auto or management field: $forbidden" }
}
foreach ($path in @('/genealogy/app/site/articles', '/genealogy/app/site/pages/{pageKey}')) {
$operation = Get-Operation -Path $path -Method 'get'
if ($operation.responses.'200'.content.'application/json'.schema) {
throw "Site content response DTO changed for $path; review M10 integration"
}
}
$auditDocs = @(Get-ChildItem -LiteralPath (Join-Path $root 'docs') -File -Filter '*149*.md')
if ($auditDocs.Count -lt 1) { throw 'Current 149 operation audit document is missing' }
Write-Output 'CURRENT-OPENAPI-INVENTORY-CONTRACT PASS'
+5 -13
View File
@@ -1,8 +1,9 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$json = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'APP.openapi.json') | ConvertFrom-Json
$yaml = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'APP.openapi.yaml')
$documents = @(Get-ChildItem -LiteralPath $root -File -Filter '*.openapi.json')
if ($documents.Count -ne 1) { throw 'Exactly one current OpenAPI JSON export is required at the repository root' }
$json = Get-Content -Raw -Encoding UTF8 -LiteralPath $documents[0].FullName | ConvertFrom-Json
$path = '/genealogy/app/feedback'
$pathProperty = $json.paths.PSObject.Properties[$path]
@@ -33,17 +34,8 @@ foreach ($field in $fields) {
throw "FeedbackBody.$field must be a string"
}
}
foreach ($yamlFact in @(
' /genealogy/app/feedback:',
"`$ref: '#/components/schemas/FeedbackBody'",
' FeedbackBody:',
' - feedbackContent',
' feedbackType:',
' feedbackContent:',
' contactInfo:'
)) {
if (-not $yaml.Contains($yamlFact)) { throw "Feedback YAML fact is missing: $yamlFact" }
if ((@($body.properties.feedbackType.enum) -join ',') -ne 'advice,bug,complaint,other') {
throw 'FeedbackBody.feedbackType must use advice,bug,complaint,other'
}
Write-Output 'FEEDBACK-OPENAPI-CONTRACT PASS'
+62
View File
@@ -0,0 +1,62 @@
"use strict";
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const toDataModuleUrl = (source) =>
`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
const run = async () => {
const rawSource = fs.readFileSync(path.join(__dirname, "../utils/api.js"), "utf8");
const moduleBody = rawSource.slice(rawSource.indexOf("const successCodes"));
const prelude = `
const currentUser = {};
const genealogies = [];
const publicGenealogies = [];
const notifications = [];
const joinApplications = [];
const listFamilyFeedFixtures = () => [];
const listFamilyArticleFixtures = () => [];
const listFamilyAlbumFixtures = () => [];
const listCeremonyFixtures = () => [];
const listGrowthRecordFixtures = () => [];
const listNotificationFixtures = () => [];
const runtimeConfig = { baseUrl: "https://backend-api.ddxcjp.cn", clientId: "client-1", tenantId: "000000" };
const hasRemoteConfig = () => true;
const resolveRuntimeMode = () => "remote";
const AUTH_VERIFICATION_OPERATION = Object.freeze({});
const assertSmsCode = (value) => value;
const GENEALOGY_ACCESS_PRESET = Object.freeze({ MEMBER_ONLY: "MEMBER_ONLY" });
const fromApiGenealogyAccess = () => null;
const session = { getToken: () => "session-1", saveToken() {} };
`;
const requests = [];
globalThis.uni = {
request(options) {
requests.push(options);
queueMicrotask(() => options.success({ statusCode: 200, data: { code: 200, data: {} } }));
return { abort() {} };
},
};
const { appApi } = await import(toDataModuleUrl(`${prelude}\n${moduleBody}`));
await appApi.updateProfile({ sex: "2" });
assert.strictEqual(requests.at(-1).data.sex, "2");
await assert.rejects(appApi.updateProfile({ sex: "男" }), /sex 必须为 0、1 或 2/);
await appApi.submitFeedback({ feedbackType: "bug", feedbackContent: "保存时出现错误" });
assert.strictEqual(requests.at(-1).data.feedbackType, "bug");
await assert.rejects(
appApi.submitFeedback({ feedbackType: "功能问题", feedbackContent: "保存时出现错误" }),
/feedbackType 必须为 advice、bug、complaint 或 other/,
);
delete globalThis.uni;
process.stdout.write("FORM-ENUM-API-RUNTIME-SMOKE PASS\n");
};
run().catch((error) => {
process.stderr.write(`${error.stack || error.message}\n`);
process.exit(1);
});
+2 -1
View File
@@ -17,9 +17,10 @@ foreach ($page in @($g08, $g09, $g10)) {
}
}
foreach ($required in @('appApi.applyToJoin', 'createRequestController', 'applicantName', 'phone', 'relationDesc', 'applyReason', 'inviterUserId')) {
foreach ($required in @('appApi.applyToJoin', 'createRequestController', 'applicantName', 'phone', 'relationDesc', 'applyReason')) {
Assert-Contains $g08 $required "G08 must submit the APP join-application contract field: $required"
}
if ($g08 -match [regex]::Escape('inviterUserId')) { throw 'G08 must not expose a raw inviter user ID without a candidate selector' }
foreach ($retired in @('findGenealogyFixture', 'joinSamples', 'LOCAL_WITHDRAWN', 'applicationSamples')) {
if ($g08 -match [regex]::Escape($retired)) { throw "G08 must not retain local application fixture state: $retired" }
}
+8 -1
View File
@@ -58,7 +58,14 @@ if (($avatar.type -join ',') -ne 'string,null' -or $avatar.pattern -ne '^[1-9][0
foreach ($field in @('birthLunar', 'deathLunar', 'personStatus', 'sex')) {
$schema = $body.properties.$field
if ($schema.type -ne 'string') { throw "LineagePersonBody.$field must be a dictionary string" }
if ($schema.enum) { throw "LineagePersonBody.$field enum changed; update the T-series selector contract" }
}
if ((@($body.properties.sex.enum) -join ',') -ne '0,1,2') { throw 'LineagePersonBody.sex must use 0,1,2' }
if ((@($body.properties.birthLunar.enum) -join ',') -ne '0,1') { throw 'LineagePersonBody.birthLunar must use 0,1' }
if ((@($body.properties.deathLunar.enum) -join ',') -ne '0,1') { throw 'LineagePersonBody.deathLunar must use 0,1' }
if ((@($body.properties.personStatus.enum) -join ',') -ne '0,1,2') { throw 'LineagePersonBody.personStatus must use 0,1,2' }
if ((@($body.required) -notcontains 'bindingMode' -or (@($body.properties.bindingMode.enum) -join ',') -ne 'NONE,SELF,SPECIFIED')) {
throw 'LineagePersonBody.bindingMode must be required and use NONE,SELF,SPECIFIED'
}
Write-Output 'LINEAGE-OPENAPI-CONTRACT PASS'
+6 -1
View File
@@ -27,7 +27,12 @@ foreach ($required in @(
'baseline.value = submittedSnapshot',
'requestController.abort()',
'pageActive = false',
':disabled="feedbackState === ''submitting''"'
':disabled="feedbackState === ''submitting''"',
'value: "bug"',
'value: "advice"',
'value: "complaint"',
'value: "other"',
'@click="selectFeedbackType(type.value)"'
)) {
if (-not $page.Contains($required)) { throw "M07 contract is missing: $required" }
}
+34
View File
@@ -51,13 +51,47 @@ const run = async () => {
assert.strictEqual(requests.at(-1).data.coverOssId, ossId);
await appApi.createAlbumPhoto("1001", "2001", { ossId });
assert.strictEqual(requests.at(-1).data.ossId, ossId);
await appApi.createVideo("1001", {
videoTitle: "清明祭祖",
videoDesc: "家族活动记录",
videoOssId: ossId,
});
assert.deepStrictEqual(requests.at(-1).data, {
videoTitle: "清明祭祖",
videoDesc: "家族活动记录",
videoOssId: ossId,
});
await appApi.createCeremony("1001", { ceremonyType: "祭祖", ceremonyTitle: "清明祭祖", coverOssId: ossId });
assert.strictEqual(requests.at(-1).data.coverOssId, ossId);
await appApi.createCeremony("1001", {
ceremonyType: "祭祖",
ceremonyTitle: "清明祭祖",
locationAddress: "北京市朝阳区测试路",
longitude: "116.4074",
latitude: "39.9042",
status: "0",
});
assert.deepStrictEqual(requests.at(-1).data, {
ceremonyType: "祭祖",
ceremonyTitle: "清明祭祖",
locationAddress: "北京市朝阳区测试路",
longitude: 116.4074,
latitude: 39.9042,
status: "0",
});
await assert.rejects(
appApi.createCeremony("1001", { ceremonyType: "祭祖", ceremonyTitle: "清明祭祖", longitude: 116.4074 }),
/longitude 和 latitude 必须同时提供/,
);
await assert.rejects(
appApi.createAlbumPhoto("1001", "2001", { ossId: 900001 }),
/OSS ID 字符串/,
);
await assert.rejects(
appApi.createVideo("1001", { videoTitle: "测试视频", videoOssId: 900001 }),
/OSS ID 字符串/,
);
delete globalThis.uni;
process.stdout.write("OSS-ID-PAYLOAD-API-RUNTIME-SMOKE PASS\n");
+6
View File
@@ -30,6 +30,12 @@ foreach ($required in @('appApi.getProfile', 'onShow(loadProfile)', 'profile.nic
foreach ($required in @('appApi.getProfile', 'appApi.updateProfile', 'pickAndUploadImage', 'form.nickName', 'form.realName', 'form.sex', 'form.birthday', 'form.email', 'avatarId', 'buildUpdatePayload')) {
Require-Text $m02 $required 'M02'
}
foreach ($required in @('const sexOptions', 'value: "0"', 'value: "1"', 'value: "2"', 'range-key="label"', '@change="changeSex"')) {
Require-Text $m02 $required 'M02'
}
foreach ($forbidden in @('readonly-value', 'form-row--readonly')) {
Forbid-Text $m02 $forbidden 'M02'
}
foreach ($forbidden in @('avatarOssId', 'coverOssId', 'fileId')) {
Forbid-Text $m02 $forbidden 'M02'
}
+7 -2
View File
@@ -6,9 +6,9 @@ function Read-Page([string]$path) { Get-Content -LiteralPath (Join-Path $root $p
$contracts = [ordered]@{
'pages/records/r03-gift-list.vue' = @('appApi.getRelativeRecords', 'relativeId', 'openPage("R04"', 'record-list')
'pages/records/r04-gift-editor.vue' = @('appApi.createRelativeRecord', 'relativeName', 'mediaOssIds', 'pickAndUploadImage')
'pages/records/r05-ritual-list.vue' = @('appApi.getCeremonies', 'ceremonyId', 'openPage("R06"', 'openPage("R07"')
'pages/records/r05-ritual-list.vue' = @('appApi.getCeremonies', 'ceremonyId', 'openPage(', '"R06"', '"R07"')
'pages/records/r06-ritual-detail.vue' = @('appApi.getCeremonyDetail', 'ceremonyId')
'pages/records/r07-ritual-editor.vue' = @('appApi.createCeremony', 'ceremonyType', 'ceremonyTitle')
'pages/records/r07-ritual-editor.vue' = @('appApi.createCeremony', 'ceremonyType', 'ceremonyTitle', 'locationAddress')
'pages/records/r08-growth-journal.vue' = @('appApi.getGrowthRecords', 'appApi.createGrowthRecord', 'recordId', 'recordTitle', 'view.value = "list"')
'pages/records/r09-life-events.vue' = @('hasValidContext', 'returnToRecords')
'pages/records/r10-memo-list.vue' = @('appApi.getMemos', 'appApi.createMemo', 'memoId', 'memoTitle', 'view.value = "list"')
@@ -51,4 +51,9 @@ foreach ($forbidden in @('lifeEvents', 'createLifeEvent', 'saveLifeEvent', 'grow
if ($r09.Contains($forbidden)) { throw "R09 must remain closed without a backend contract: $forbidden" }
}
$r07 = Read-Page 'pages/records/r07-ritual-editor.vue'
foreach ($forbidden in @('form.longitude', 'form.latitude')) {
if ($r07.Contains($forbidden)) { throw "R07 must not expose raw map coordinates without a map picker: $forbidden" }
}
Write-Output 'R-BUSINESS-FLOW-CONTRACT PASS'