test: verify self-operated analytics through TEST Gateway and synthetic query fixtures
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* TEST-only Gateway acceptance. Default is a local plan; --execute performs login,
|
||||
* read-only queries and, with --probe-off-command, one deliberately rejected INVENTORY
|
||||
* command after verifying OFF. Never collects, publishes, creates fixtures or changes config.
|
||||
* Credentials: node --env-file=<explicit test-*.env> BEFORE this script; this script does not read credential files.
|
||||
* Required env: CHONGCHONG_ENV=TEST, CHONGCHONG_ADMIN_API_BASE_URL (ends /api),
|
||||
* CHONGCHONG_ADMIN_USERNAME/PASSWORD, CHONGCHONG_MERCHANT_USERNAME/PASSWORD,
|
||||
* SELFOP_READONLY_USERNAME/PASSWORD (read permission only),
|
||||
* SELFOP_DENIED_USERNAME/PASSWORD (platform without read or sync permission).
|
||||
* Example: node --env-file=<test file> <script> --environment TEST --source-account <account>
|
||||
* --warehouse <configured warehouse> --other-warehouse <second configured warehouse>
|
||||
* --report <local json> --synthetic-fixture <fixture notes json> --execute --probe-off-command
|
||||
* Reports contain case IDs, status, machine codes and request/trace IDs only; no payloads,
|
||||
* URLs, credentials or customer identifiers. Partial/missing evidence is BLOCKED (exit 2).
|
||||
* Re-run reads current state and uses a fresh rejection request key. No deletion or SQL.
|
||||
* Register this service-local entry in the maintained script index before release.
|
||||
*/
|
||||
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const ROOT='/platform/v1/last-leg/self-operated-analytics';
|
||||
const READ='platform:self-operated-analytics:list', SYNC='platform:self-operated-analytics:sync';
|
||||
export const READ_ROUTES=Object.freeze(['sku-sales-summary','sku-daily-sales','hot-skus','slow-skus',
|
||||
'inventory-coverage','owners','owner-overview','sync-overview','sync-tasks','sync-runs']);
|
||||
export const SAMPLE_REQUIREMENTS=Object.freeze([
|
||||
'Two configured warehouses with BUSINESS/PUBLISHED outbound and inventory versions; do not collect or publish via this script.',
|
||||
'At least two owners sharing one SKU, with distinguishable quantities and complete/partial/unknown days.',
|
||||
'At least one existing warehouse sync task and attempt for task-detail and task/run warehouse isolation.',
|
||||
]);
|
||||
const demand=(condition,code)=>{if(!condition)throw new Error(code);};
|
||||
const success=r=>r.http===200&&String(r.body?.code)==='200';
|
||||
export const denied=r=>[401,403].includes(r.http)||(r.http===200&&['A0401','A0403'].includes(String(r.body?.code)));
|
||||
export const businessRejected=r=>r.http===200&&['B0001','A0400','A0404','A0409','A0422'].includes(String(r.body?.code));
|
||||
// A wrong scope, generic business error or infrastructure failure is not evidence of the OFF gate.
|
||||
export const offRejected=r=>businessRejected(r)&&r.body?.message==='采集功能未启用,当前环境不允许执行采集或重试';
|
||||
const safeCode=value=>typeof value==='string'&&/^[A-Za-z0-9_-]{1,80}$/.test(value)?value:null;
|
||||
export function evidence(r){return {http:r.http,code:safeCode(String(r.body?.code??'')),
|
||||
requestId:safeCode(r.body?.requestId??r.requestId),traceId:safeCode(r.body?.traceId??r.traceId)};}
|
||||
|
||||
export function parseArguments(args){
|
||||
const o={execute:false,probeOff:false};
|
||||
const names={'--environment':'environment','--source-account':'account','--warehouse':'warehouse',
|
||||
'--other-warehouse':'otherWarehouse','--report':'report','--synthetic-fixture':'syntheticFixture'};
|
||||
const seen=new Set();
|
||||
for(let i=0;i<args.length;i++){
|
||||
const key=args[i];demand(!seen.has(key),'DUPLICATE_ARGUMENT');seen.add(key);
|
||||
if(key==='--execute'){o.execute=true;continue;}
|
||||
if(key==='--probe-off-command'){o.probeOff=true;continue;}
|
||||
demand(names[key]&&args[i+1]&&!args[i+1].startsWith('--'),'INVALID_ARGUMENT');o[names[key]]=args[++i];
|
||||
}
|
||||
demand(o.environment==='TEST','EXPLICIT_TEST_REQUIRED');
|
||||
if(o.execute){demand(o.account&&o.warehouse&&o.report,'SCOPE_AND_REPORT_REQUIRED');}
|
||||
if(o.otherWarehouse)demand(o.otherWarehouse!==o.warehouse,'SECOND_WAREHOUSE_MUST_DIFFER');
|
||||
return o;
|
||||
}
|
||||
export function validateEnvironment(env){
|
||||
demand(env.CHONGCHONG_ENV?.toUpperCase()==='TEST','TEST_ENV_REQUIRED');
|
||||
let base;try{base=new URL(env.CHONGCHONG_ADMIN_API_BASE_URL);}catch{throw new Error('INVALID_GATEWAY');}
|
||||
demand(['https:','http:'].includes(base.protocol)&&!base.username&&!base.password&&!base.search&&!base.hash&&
|
||||
/\/api\/?$/.test(base.pathname),'INVALID_GATEWAY');
|
||||
return base.href.replace(/\/$/,'');
|
||||
}
|
||||
export function plan(){return {environment:'TEST',mode:'PLAN',thirdPartyCalls:false,fixtureWrites:false,
|
||||
routes:READ_ROUTES,authentication:['anonymous','authorized platform','read-only platform','denied platform','merchant'],
|
||||
offProbe:'Opt-in only; rechecks OFF immediately before one rejected INVENTORY command. S0500 is a failure.',
|
||||
syntheticEvidence:'--synthetic-fixture <reviewed notes JSON> enables thirteen numerical/isolation checks; never proves provider coverage.',
|
||||
samples:SAMPLE_REQUIREMENTS,report:'Only case identifiers, outcomes, machine codes and request/trace IDs.'};}
|
||||
const query=(values)=>new URLSearchParams(Object.entries(values).filter(([,v])=>v!==null&&v!==undefined).map(([k,v])=>[k,String(v)])).toString();
|
||||
export function pin(context){
|
||||
demand(context&&context.salesRunId!==null&&context.inventoryRunId!==null&&context.queryAsOf,'CONTEXT_MISSING');
|
||||
return Object.fromEntries(['salesRunId','inventoryRunId','salesScopeHash','inventoryScopeHash','queryAsOf'].map(k=>[k,context[k]]));
|
||||
}
|
||||
function sameContext(a,b){return ['salesRunId','inventoryRunId','salesScopeHash','inventoryScopeHash','queryAsOf'].every(k=>a?.[k]===b?.[k]);}
|
||||
|
||||
export function fixtureDate(anchor,offset=0){
|
||||
demand(/^\d{4}-\d{2}-\d{2}$/.test(anchor??''),'FIXTURE_ANCHOR_REQUIRED');
|
||||
const date=new Date(anchor+'T00:00:00Z');date.setUTCDate(date.getUTCDate()+offset);return date.toISOString().slice(0,10);
|
||||
}
|
||||
export function validateSyntheticNotes(notes,options){
|
||||
demand(notes?.synthetic===true&¬es.fixtureId==='SYNTHETIC-SELFOP-20261012'&¬es.sourceAccount==='lingxing-primary'&&
|
||||
options.account===notes.sourceAccount&&options.warehouse==='SELFOP-TEST-A'&&options.otherWarehouse==='SELFOP-TEST-B'&&
|
||||
notes.currentVersions?.A?.salesRunId&¬es.currentVersions?.B?.inventoryRunId&¬es.oldSalesRunId&&
|
||||
Array.isArray(notes.assertions),'SYNTHETIC_SCOPE_MISMATCH');
|
||||
}
|
||||
// TEST uses Jackson NON_NULL; these nullable OpenAPI fields are optional. Missing is unknown, never zero.
|
||||
const nullish=value=>value===null||value===undefined;
|
||||
const quantity=(actual,expected)=>expected===null?nullish(actual):!nullish(actual)&&Number.isFinite(Number(actual))&&Number(actual)===Number(expected);
|
||||
const fieldsMatch=(actual,expected)=>actual&&Object.entries(expected).every(([key,value])=>value===null?nullish(actual[key]):typeof value==='number'?quantity(actual[key],value):actual[key]===value);
|
||||
|
||||
/** Expectations come from the reviewed synthetic manifest; only GET requests are returned. */
|
||||
export function syntheticCases(notes,context){
|
||||
const d=context.anchorDate,from=fixtureDate(d,-2),until=fixtureDate(d,-1),window={rangePreset:'CUSTOM',startDate:from,endDate:until,bucket:'DAY'};
|
||||
const shared='SYNTH-SHARED-SKU',owner='SYNTH-OWNER-A';
|
||||
const assertion=(endpoint,predicate=()=>true)=>{
|
||||
const rows=notes.assertions.filter(a=>a.endpoint===endpoint&&predicate(a));demand(rows.length===1,'FIXTURE_EXPECTATION_AMBIGUOUS');return rows[0];
|
||||
};
|
||||
const pair=(actual,version,hash)=>actual&&String(actual.salesRunId)===String(version.salesRunId)&&String(actual.inventoryRunId)===String(version.inventoryRunId)&&
|
||||
actual.salesScopeHash===hash&&actual.inventoryScopeHash===hash;
|
||||
const scopedRows=(data,warehouse)=>Array.isArray(data?.records)&&data.records.every(r=>r.warehouseCode===warehouse&&r.sourceAccount===notes.sourceAccount);
|
||||
const cases=[];
|
||||
const add=(id,route,params,verify,{warehouse='SELFOP-TEST-A',version=notes.currentVersions.A,pinned=true}={})=>cases.push({id:'synthetic.'+id,route,
|
||||
params:{sourceAccount:notes.sourceAccount,warehouseCode:warehouse,current:1,size:100,...(pinned?pin(context):{}),...params},
|
||||
verify:data=>['sync-overview'].includes(route)?verify(data):pair(data?.context,version,notes.hashes[warehouse==='SELFOP-TEST-A'?'A':'B'])&&
|
||||
(!pinned||data.context.queryAsOf===context.queryAsOf)&&verify(data)});
|
||||
const owners=assertion('owners').expected;
|
||||
add('owners','owners',{},data=>quantity(data.total,owners.total)&&data.records.length===owners.total&&
|
||||
JSON.stringify(data.records.map(r=>r.customerCode).sort())===JSON.stringify([...owners.customerCodes].sort())&&data.records.every(r=>nullish(r.customerName)));
|
||||
const historical=assertion('sku-sales-summary',a=>a.expected?.records).expected;
|
||||
add('date-range-and-owner-isolation','sku-sales-summary',{...window,sku:shared},data=>scopedRows(data,'SELFOP-TEST-A')&&quantity(data.total,historical.total)&&
|
||||
data.records.length===historical.total&&Object.entries(historical.records).every(([code,expected])=>{
|
||||
const rows=data.records.filter(r=>r.customerCode===code&&r.sku===shared);return rows.length===1&&fieldsMatch(rows[0],expected);
|
||||
}));
|
||||
const daily=assertion('sku-daily-sales').expected;
|
||||
add('daily-exact-axis','sku-daily-sales',{...window,customerCode:owner,sku:shared},data=>data.points?.length===daily.points.length&&
|
||||
data.points.every((p,i)=>p.businessDate===fixtureDate(d,Number(daily.points[i].businessDate.slice(1)))&&
|
||||
quantity(p.outboundQuantity,daily.points[i].outboundQuantity)&&p.dataStatus===daily.points[i].dataStatus)&&
|
||||
quantity(data.summary?.outboundQuantity,daily.summaryOutboundQuantity)&&data.summary.customerCode===owner&&data.summary.sku===shared);
|
||||
const today=assertion('sku-sales-summary',a=>a.params?.rangePreset==='TODAY'&&typeof a.expected==='object').expected;
|
||||
add('today-partial','sku-sales-summary',{rangePreset:'TODAY',sku:shared},data=>scopedRows(data,'SELFOP-TEST-A')&&data.records.length===2&&
|
||||
Object.entries(today).every(([code,expected])=>fieldsMatch(data.records.find(r=>r.customerCode===code&&r.sku===shared),expected)));
|
||||
const old=assertion('sku-sales-summary',a=>a.params?.salesRunId).expected;
|
||||
add('old-version-fixed','sku-sales-summary',{...window,customerCode:owner,sku:shared,salesRunId:notes.oldSalesRunId},data=>
|
||||
scopedRows(data,'SELFOP-TEST-A')&&data.records.length===1&&fieldsMatch(data.records[0],old),
|
||||
{version:{...notes.currentVersions.A,salesRunId:notes.oldSalesRunId}});
|
||||
const inventory=assertion('inventory-coverage').expected;
|
||||
add('inventory-zero-unknown-and-age','inventory-coverage',{},data=>{
|
||||
if(!scopedRows(data,'SELFOP-TEST-A')||!quantity(data.total,inventory.total)||data.records.length!==inventory.total)return false;
|
||||
const row=sku=>data.records.find(r=>r.customerCode===owner&&r.sku===sku);
|
||||
return Object.entries(inventory.sharedByOwner).every(([code,expected])=>fieldsMatch(data.records.find(r=>r.customerCode===code&&r.sku===shared),expected))&&
|
||||
fieldsMatch(row(inventory.zero.sku),{physicalQuantity:0,availableQuantity:0})&&
|
||||
fieldsMatch(row(inventory.unknown.sku),{physicalQuantity:null,availableQuantity:null,inventoryCoverageDays:null})&&
|
||||
row(inventory.unknown.sku)?.quantityStatuses?.physicalQuantity==='UNKNOWN'&&row(inventory.unknown.sku)?.quantityStatuses?.availableQuantity==='UNKNOWN'&&
|
||||
fieldsMatch(row(inventory.new.sku),{observationEligible:false,inventoryCoverageDays:null});
|
||||
});
|
||||
for(const route of ['hot-skus','slow-skus']){
|
||||
const expected=assertion(route).expected;
|
||||
add(route+'-eligibility-order',route,route==='hot-skus'?{windowDays:30}:{},data=>scopedRows(data,'SELFOP-TEST-A')&&quantity(data.total,expected.total)&&
|
||||
data.records.length===expected.total&&data.records.every((r,i)=>r.customerCode===expected.order[i]&&r.sku===expected.sku&&quantity(r.outboundQuantity,expected.outboundQuantities[i])));
|
||||
}
|
||||
const other=assertion('sku-sales-summary',a=>a.params?.warehouseCode==='SELFOP-TEST-B').expected;
|
||||
add('warehouse-b-positive','sku-sales-summary',{rangePreset:'CUSTOM',startDate:until,endDate:until,bucket:'DAY',sku:shared},data=>
|
||||
scopedRows(data,'SELFOP-TEST-B')&&quantity(data.total,other.total)&&data.records.length===1&&fieldsMatch(data.records[0],{customerCode:other.customerCode,outboundQuantity:other.outboundQuantity}),
|
||||
{warehouse:'SELFOP-TEST-B',version:notes.currentVersions.B,pinned:false});
|
||||
add('zero-sales-versus-unknown','sku-daily-sales',{...window,customerCode:owner,sku:'SYNTH-ZERO'},data=>data.points?.length===2&&
|
||||
data.points.every((p,i)=>p.businessDate===fixtureDate(d,i-2)&&p.dataStatus==='COMPLETE'&&quantity(p.outboundQuantity,0)&&quantity(p.observedQuantity,0)));
|
||||
add('pre-existence-is-unknown','sku-daily-sales',{...window,customerCode:owner,sku:'SYNTH-NEW'},data=>data.points?.length===2&&
|
||||
data.points[0].businessDate===from&&data.points[0].dataStatus==='UNKNOWN'&&nullish(data.points[0].outboundQuantity)&&nullish(data.points[0].observedQuantity)&&
|
||||
data.points[1].businessDate===until&&data.points[1].dataStatus==='COMPLETE'&&quantity(data.points[1].outboundQuantity,0));
|
||||
const overview=assertion('sync-overview').expected;
|
||||
for(const [letter,warehouse] of [['A','SELFOP-TEST-A'],['B','SELFOP-TEST-B']]){
|
||||
const expected=overview[letter];
|
||||
const present=new Set((expected.sampleDates??[]).map(date=>fixtureDate(d,Number(date.slice(1)))));
|
||||
const missing=Array.from({length:30},(_,i)=>fixtureDate(d,i-30)).filter(date=>!present.has(date));
|
||||
add('daily-sample-gaps-'+letter,'sync-overview',{},data=>data.collectionMode==='OFF'&&data.warehouses?.some(w=>{
|
||||
const samples=w.inventoryDailySamples;return w.warehouseCode===warehouse&&w.dataFamily==='INVENTORY'&&!w.executionEnabled&&!w.publicationEnabled&&samples&&
|
||||
samples.windowFrom===fixtureDate(d,-30)&&samples.windowUntil===d&&samples.expectedDays===30&&quantity(samples.sampleCount,expected.sampleCount)&&
|
||||
(expected.accumulationStartedOn===null?nullish(samples.accumulationStartedOn):samples.accumulationStartedOn===fixtureDate(d,Number(expected.accumulationStartedOn.slice(1))))&&
|
||||
JSON.stringify(samples.missingDates)===JSON.stringify(missing);
|
||||
}),{warehouse,pinned:false});
|
||||
}
|
||||
return cases;
|
||||
}
|
||||
|
||||
export async function runAcceptance(options,{env=process.env,fetchImpl=fetch,save=()=>{},syntheticNotes}={}){
|
||||
if(!options.execute)return plan();
|
||||
demand(options.environment==='TEST','EXPLICIT_TEST_REQUIRED');
|
||||
const base=validateEnvironment(env),sessions=[];
|
||||
const notes=syntheticNotes??(options.syntheticFixture?JSON.parse(readFileSync(resolve(options.syntheticFixture),'utf8')):null);
|
||||
if(notes)validateSyntheticNotes(notes,options);
|
||||
const result={environment:'TEST',mode:'EXECUTE',evidenceType:notes?'SYNTHETIC_QUERY_FIXTURE_NOT_PROVIDER_EVIDENCE':'HTTP_ACCESS_AND_QUERY_CHECKS',
|
||||
synthetic:!!notes,startedAt:new Date().toISOString(),cases:[],sampleRequirements:SAMPLE_REQUIREMENTS,
|
||||
uncovered:['Actual context expiration after 30 elapsed minutes is deferred; not exercised by backdating queryAsOf.',
|
||||
'Upstream Lingxing coverage, collection and publication transactions, real concurrency and performance are not verified.',
|
||||
'Impersonated platform identity is not prepared or exercised.']};
|
||||
const checkpoint=()=>save(result);
|
||||
const record=(id,status,response,reason)=>{result.cases.push({id,status,...(response?evidence(response):{}),...(reason?{reason}: {})});checkpoint();};
|
||||
const call=async(method,route,session,body)=>{
|
||||
demand(method==='GET'||(method==='POST'&&['/auth/login','/auth/logout',ROOT+'/sync-tasks'].includes(route)),'UNAUTHORIZED_ROUTE');
|
||||
let response;try{response=await fetchImpl(base+route,{method,redirect:'error',signal:AbortSignal.timeout(20000),
|
||||
headers:{Accept:'application/json',...(session?{Authorization:`Bearer ${session.accessToken}`} :{}),
|
||||
...(body===undefined?{}:{'Content-Type':'application/json'})},...(body===undefined?{}:{body:JSON.stringify(body)})});}
|
||||
catch{throw new Error('HTTP_TRANSPORT_FAILED');}
|
||||
let payload;try{payload=await response.json();}catch{throw new Error('HTTP_JSON_REQUIRED');}
|
||||
return {http:response.status,body:payload,requestId:response.headers?.get('x-request-id'),traceId:response.headers?.get('x-trace-id')};
|
||||
};
|
||||
const login=async(label,prefix,check)=>{
|
||||
if(!env[prefix+'_USERNAME']||!env[prefix+'_PASSWORD']){record(label+'.login','BLOCKED',null,'CREDENTIALS_MISSING');return null;}
|
||||
const r=await call('POST','/auth/login',null,{loginId:env[prefix+'_USERNAME'],password:env[prefix+'_PASSWORD']});
|
||||
if(!success(r)||!r.body.data?.accessToken){record(label+'.login','FAIL',r,'LOGIN_FAILED');return null;}
|
||||
const session=r.body.data;sessions.push(session);
|
||||
if(!check(new Set(session.authorities??[]))){record(label+'.login','BLOCKED',r,'PERMISSION_FIXTURE_MISMATCH');return null;}
|
||||
record(label+'.login','PASS',r);return session;
|
||||
};
|
||||
const scoped={sourceAccount:options.account,warehouseCode:options.warehouse,current:1,size:20};
|
||||
const get=(route,session,params=scoped)=>call('GET',ROOT+'/'+route+'?'+query(params),session);
|
||||
const check=async(id,route,session,params,predicate)=>{
|
||||
const r=await get(route,session,params);const passed=predicate(r);record(id,passed?'PASS':'FAIL',r);return passed?r.body.data:null;
|
||||
};
|
||||
try{
|
||||
for(const route of [...READ_ROUTES,'sync-tasks/SELFOP-NOT-EXIST'])
|
||||
await check('anonymous.'+route,route,null,scoped,denied);
|
||||
const admin=await login('platform','CHONGCHONG_ADMIN',a=>a.has(READ)&&a.has(SYNC));
|
||||
const readonly=await login('readonly','SELFOP_READONLY',a=>a.has(READ)&&!a.has(SYNC));
|
||||
const forbidden=await login('denied','SELFOP_DENIED',a=>!a.has(READ)&&!a.has(SYNC));
|
||||
const merchant=await login('merchant','CHONGCHONG_MERCHANT',()=>true);
|
||||
for(const [label,session] of [['denied',forbidden],['merchant',merchant]])if(session){
|
||||
for(const route of [...READ_ROUTES,'sync-tasks/SELFOP-NOT-EXIST'])await check(label+'.'+route,route,session,scoped,denied);
|
||||
}
|
||||
if(!admin){record('business.reads','BLOCKED',null,'AUTHORIZED_PLATFORM_REQUIRED');return result;}
|
||||
const overview=await check('off.overview','sync-overview',admin,scoped,r=>success(r)&&r.body.data?.collectionMode==='OFF'&&
|
||||
r.body.data.warehouses?.length>0&&r.body.data.warehouses.every(w=>w.warehouseCode===options.warehouse&&!w.executionEnabled&&!w.publicationEnabled));
|
||||
if(!overview){record('off.commands','BLOCKED',null,'OFF_PREFLIGHT_REQUIRED');return result;}
|
||||
if(readonly)await check('readonly.query','sync-overview',readonly,scoped,r=>success(r)&&r.body.data.collectionMode==='OFF');
|
||||
const sales=await check('query.sales','sku-sales-summary',admin,{...scoped,rangePreset:'LAST_3_DAYS',bucket:'DAY'},r=>
|
||||
success(r)&&Array.isArray(r.body.data?.records)&&r.body.data.records.every(row=>row.sourceAccount===options.account&&row.warehouseCode===options.warehouse));
|
||||
const context=sales?pin(sales.context):null;
|
||||
const fixed=context?{...scoped,...context}:scoped;
|
||||
for(const route of ['hot-skus','slow-skus','inventory-coverage','owners'])await check('query.'+route,route,admin,fixed,r=>
|
||||
success(r)&&Array.isArray(r.body.data?.records)&&(!context||sameContext(sales.context,r.body.data.context))&&
|
||||
r.body.data.records.every(row=>row.warehouseCode===undefined||row.warehouseCode===options.warehouse));
|
||||
for(const route of ['sync-tasks','sync-runs']){
|
||||
const data=await check('query.'+route,route,admin,scoped,r=>success(r)&&Array.isArray(r.body.data?.records)&&
|
||||
(route!=='sync-tasks'||r.body.data.records.every(t=>t.warehouseCode===options.warehouse&&t.taskRole==='WAREHOUSE_SYNC')));
|
||||
if(route==='sync-runs'){
|
||||
if(data?.records.length){
|
||||
for(const taskNo of new Set(data.records.map(r=>r.taskNo))){
|
||||
if(!taskNo){record('scope.run-warehouse','FAIL',null,'ATTEMPT_TASK_NUMBER_REQUIRED');continue;}
|
||||
await check('scope.run-warehouse','sync-tasks/'+encodeURIComponent(taskNo),admin,scoped,r=>success(r)&&
|
||||
r.body.data.task?.taskNo===taskNo&&r.body.data.task.warehouseCode===options.warehouse);
|
||||
}
|
||||
}else record('query.attempt-sample','BLOCKED',null,'EXISTING_WAREHOUSE_ATTEMPT_REQUIRED');
|
||||
}
|
||||
}
|
||||
await check('scope.foreign-account','sync-overview',admin,{...scoped,sourceAccount:'SELFOP-UNCONFIGURED-'+randomUUID()},businessRejected);
|
||||
await check('scope.foreign-warehouse','sku-sales-summary',admin,{...scoped,warehouseCode:'SELFOP-UNCONFIGURED-'+randomUUID(),rangePreset:'TODAY'},businessRejected);
|
||||
if(context){
|
||||
await check('context.second-page','sku-sales-summary',admin,{...fixed,current:2,rangePreset:'LAST_3_DAYS',bucket:'DAY'},r=>success(r)&&sameContext(sales.context,r.body.data.context));
|
||||
await check('context.future-clock','sku-sales-summary',admin,{...fixed,queryAsOf:new Date(Date.now()+86400000).toISOString(),rangePreset:'TODAY'},businessRejected);
|
||||
if(options.otherWarehouse&&(Number(context.salesRunId)>0||Number(context.inventoryRunId)>0)){
|
||||
const configured=await check('scope.second-warehouse-configured','sync-overview',admin,{...scoped,warehouseCode:options.otherWarehouse},r=>success(r)&&
|
||||
r.body.data.warehouses?.some(w=>w.warehouseCode===options.otherWarehouse));
|
||||
if(configured)await check('context.cross-warehouse','sku-sales-summary',admin,{...fixed,warehouseCode:options.otherWarehouse,rangePreset:'TODAY'},businessRejected);
|
||||
}
|
||||
else record('context.cross-warehouse','BLOCKED',null,'SECOND_CONFIGURED_WAREHOUSE_REQUIRED');
|
||||
}
|
||||
const sample=sales?.records?.[0];
|
||||
if(sample){
|
||||
await check('scope.exact-owner-sku','sku-sales-summary',admin,{...fixed,customerCode:sample.customerCode,sku:sample.sku,rangePreset:'LAST_3_DAYS'},r=>success(r)&&
|
||||
r.body.data.records.length>0&&r.body.data.records.every(row=>row.customerCode===sample.customerCode&&row.sku===sample.sku));
|
||||
await check('query.daily','sku-daily-sales',admin,{...fixed,customerCode:sample.customerCode,sku:sample.sku,rangePreset:'LAST_3_DAYS',bucket:'DAY'},r=>success(r)&&
|
||||
r.body.data.summary?.customerCode===sample.customerCode&&r.body.data.summary?.sku===sample.sku&&r.body.data.points?.length===3&&
|
||||
r.body.data.points.every((p,i,arr)=>['COMPLETE','PARTIAL','UNKNOWN'].includes(p.dataStatus)&&(!i||p.businessDate>arr[i-1].businessDate)&&
|
||||
(p.dataStatus!=='UNKNOWN'||nullish(p.outboundQuantity)&&nullish(p.observedQuantity))));
|
||||
const {current:unusedCurrent,size:unusedSize,...overviewScope}=fixed;
|
||||
await check('query.owner-overview','owner-overview',admin,{...overviewScope,customerCode:sample.customerCode},r=>success(r)&&r.body.data.customerCode===sample.customerCode);
|
||||
const peer=sales.records.find(row=>row.sku===sample.sku&&row.customerCode!==sample.customerCode);
|
||||
if(peer)await check('scope.same-sku-other-owner','sku-sales-summary',admin,{...fixed,customerCode:peer.customerCode,sku:peer.sku,rangePreset:'LAST_3_DAYS'},r=>success(r)&&
|
||||
r.body.data.records.length===1&&r.body.data.records[0].customerCode===peer.customerCode&&r.body.data.records[0].sku===peer.sku&&
|
||||
r.body.data.records[0].observedQuantity===peer.observedQuantity);
|
||||
else record('scope.same-sku-other-owner','BLOCKED',null,'SECOND_OWNER_SAME_SKU_REQUIRED');
|
||||
}else record('query.sample-dependent','BLOCKED',null,'PUBLISHED_SALES_SAMPLE_REQUIRED');
|
||||
if(notes&&sales){
|
||||
demand(sales.context.anchorDate,'FIXTURE_ANCHOR_REQUIRED');
|
||||
for(const scenario of syntheticCases(notes,sales.context))await check(scenario.id,scenario.route,admin,scenario.params,r=>success(r)&&scenario.verify(r.body.data));
|
||||
}else if(notes)record('synthetic.business','BLOCKED',null,'FIRST_SCREEN_CONTEXT_REQUIRED');
|
||||
else result.uncovered.push('Reviewed synthetic fixture numerical, old-version and daily representative assertions were not requested.');
|
||||
const taskPage=await get('sync-tasks',admin,scoped);const task=success(taskPage)?taskPage.body.data.records?.[0]:null;
|
||||
if(task?.taskNo){await check('query.task-detail','sync-tasks/'+encodeURIComponent(task.taskNo),admin,scoped,r=>success(r)&&r.body.data.task?.taskNo===task.taskNo);
|
||||
if(options.otherWarehouse)await check('scope.cross-warehouse-task','sync-tasks/'+encodeURIComponent(task.taskNo),admin,{...scoped,warehouseCode:options.otherWarehouse},businessRejected);
|
||||
}else record('query.task-detail','BLOCKED',null,'EXISTING_WAREHOUSE_TASK_REQUIRED');
|
||||
// Probe is opt-in and OFF is checked again, immediately before any command request.
|
||||
if(options.probeOff){
|
||||
const before=await get('sync-overview',admin,scoped);
|
||||
demand(success(before)&&before.body.data?.collectionMode==='OFF','OFF_CHANGED_STOPPED');
|
||||
const command={requestKey:'off-acceptance-'+randomUUID(),warehouseCode:options.warehouse,action:'INVENTORY'};
|
||||
for(const [label,session] of [['anonymous',null],['readonly',readonly],['denied',forbidden],['merchant',merchant]]){
|
||||
if(label!=='anonymous'&&!session)continue;
|
||||
const r=await call('POST',ROOT+'/sync-tasks',session,command);record('sync.permission.'+label,denied(r)?'PASS':'FAIL',r);
|
||||
demand(!success(r),'UNEXPECTED_ACCEPTANCE_STOPPED');
|
||||
}
|
||||
const immediatelyBefore=await get('sync-overview',admin,scoped);
|
||||
demand(success(immediatelyBefore)&&immediatelyBefore.body.data.collectionMode==='OFF','OFF_CHANGED_STOPPED');
|
||||
const tasksBefore=await get('sync-tasks',admin,scoped);demand(success(tasksBefore),'TASK_BASELINE_REQUIRED');
|
||||
const r=await call('POST',ROOT+'/sync-tasks',admin,command);
|
||||
record('sync.off-controlled-rejection',offRejected(r)?'PASS':'FAIL',r);
|
||||
demand(!success(r),'UNEXPECTED_ACCEPTANCE_STOPPED');
|
||||
const tasksAfter=await get('sync-tasks',admin,scoped);
|
||||
record('sync.off-no-task-created',success(tasksAfter)&&tasksBefore.body.data.total===tasksAfter.body.data.total?'PASS':'FAIL',tasksAfter);
|
||||
}else record('sync.off-controlled-rejection','BLOCKED',null,'OPT_IN_PROBE_REQUIRED');
|
||||
}catch(error){record('run.blocker','FAIL',null,safeCode(error.message)??'UNEXPECTED_ERROR');}
|
||||
finally{
|
||||
for(const session of sessions)if(session.refreshToken){try{const r=await call('POST','/auth/logout',session,{refreshToken:session.refreshToken});record('session.logout',success(r)?'PASS':'FAIL',r);}catch{record('session.logout','FAIL',null,'LOGOUT_FAILED');}}
|
||||
result.finishedAt=new Date().toISOString();result.outcome=result.cases.some(c=>c.status==='FAIL')?'FAIL':result.cases.some(c=>c.status==='BLOCKED')?'BLOCKED':'PASS';checkpoint();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if(process.argv[1]&&import.meta.url===pathToFileURL(resolve(process.argv[1])).href){
|
||||
try{
|
||||
const options=parseArguments(process.argv.slice(2));
|
||||
const save=result=>{if(options.report){mkdirSync(dirname(resolve(options.report)),{recursive:true});writeFileSync(resolve(options.report),JSON.stringify(result,null,2));}};
|
||||
const report=await runAcceptance(options,{save});
|
||||
if(!options.execute)console.log(JSON.stringify(report,null,2));
|
||||
else {console.log(JSON.stringify({outcome:report.outcome,cases:report.cases.length}));process.exitCode=report.outcome==='PASS'?0:report.outcome==='BLOCKED'?2:1;}
|
||||
}catch{console.error('SELFOP_ACCEPTANCE_CONFIGURATION_FAILED');process.exitCode=1;}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {parseArguments,validateEnvironment,runAcceptance,businessRejected,offRejected,evidence,pin,fixtureDate,validateSyntheticNotes,syntheticCases} from './selfoperated-test-env-acceptance.mjs';
|
||||
|
||||
const env={CHONGCHONG_ENV:'TEST',CHONGCHONG_ADMIN_API_BASE_URL:'https://test.example.invalid/api',
|
||||
CHONGCHONG_ADMIN_USERNAME:'admin-secret',CHONGCHONG_ADMIN_PASSWORD:'password-secret',
|
||||
CHONGCHONG_MERCHANT_USERNAME:'merchant-secret',CHONGCHONG_MERCHANT_PASSWORD:'merchant-password-secret',
|
||||
SELFOP_READONLY_USERNAME:'readonly-secret',SELFOP_READONLY_PASSWORD:'readonly-password-secret',
|
||||
SELFOP_DENIED_USERNAME:'denied-secret',SELFOP_DENIED_PASSWORD:'denied-password-secret'};
|
||||
const options={execute:true,environment:'TEST',probeOff:true,account:'fixture-account',warehouse:'W1',otherWarehouse:'W2',report:'unused.json'};
|
||||
const READ='platform:self-operated-analytics:list',SYNC='platform:self-operated-analytics:sync';
|
||||
function fixture({mode='OFF',offCode='B0001',offMessage='采集功能未启用,当前环境不允许执行采集或重试',acceptCommand=false,changeMode=false}={}){
|
||||
const requests=[];
|
||||
const context={salesRunId:'11',inventoryRunId:'22',salesScopeHash:'sales-hash',inventoryScopeHash:'stock-hash',queryAsOf:new Date().toISOString()};
|
||||
const sales=[{sourceAccount:options.account,warehouseCode:'W1',customerCode:'owner-secret-A',sku:'sku-secret',observedQuantity:4},
|
||||
{sourceAccount:options.account,warehouseCode:'W1',customerCode:'owner-secret-B',sku:'sku-secret',observedQuantity:8}];
|
||||
const response=(code='200',data=null,http=200,message='do-not-report-sensitive-message')=>({status:http,headers:new Headers({'x-request-id':'request-1','x-trace-id':'trace-1'}),
|
||||
json:async()=>({code,data,requestId:'request-1',traceId:'trace-1',message})});
|
||||
const fetchImpl=async(url,init)=>{
|
||||
const u=new URL(url),path=u.pathname.substring(4),params=u.searchParams,body=init.body?JSON.parse(init.body):null;
|
||||
requests.push({url,method:init.method,body,headers:init.headers});
|
||||
if(path==='/auth/login'){
|
||||
const label=body.loginId.split('-')[0];const authorities=label==='admin'?[READ,SYNC]:label==='readonly'?[READ]:[];
|
||||
return response('200',{accessToken:'token-secret-'+label,refreshToken:'refresh-secret-'+label,authorities});
|
||||
}
|
||||
if(path==='/auth/logout')return response();
|
||||
const bearer=init.headers.Authorization??'';
|
||||
if(!bearer)return response('A0401',null,401);
|
||||
if(bearer.endsWith('denied')||bearer.endsWith('merchant'))return response('A0403');
|
||||
if(init.method==='POST'){
|
||||
if(bearer.endsWith('readonly'))return response('A0403');
|
||||
return response(acceptCommand?'200':offCode,acceptCommand?{taskNo:'unexpected'}:null,200,offMessage);
|
||||
}
|
||||
const route=path.split('/').at(-1),warehouse=params.get('warehouseCode');
|
||||
if(params.get('sourceAccount')?.startsWith('SELFOP-UNCONFIGURED')||warehouse?.startsWith('SELFOP-UNCONFIGURED'))return response('B0001');
|
||||
if(route==='sync-overview'){
|
||||
const count=requests.filter(r=>new URL(r.url).pathname.endsWith('/sync-overview')).length;
|
||||
return response('200',{collectionMode:changeMode&&count>6?'BUSINESS':mode,warehouses:[{warehouseCode:warehouse,executionEnabled:false,publicationEnabled:false}]});
|
||||
}
|
||||
if(params.has('queryAsOf')&&(params.get('queryAsOf')!==context.queryAsOf||warehouse==='W2'))return response('B0001');
|
||||
if(route==='sku-sales-summary')return response('200',{context,records:params.get('current')==='2'?[]:sales.filter(r=>
|
||||
!params.has('customerCode')||r.customerCode===params.get('customerCode')),total:2});
|
||||
if(route==='sku-daily-sales')return response('200',{context,summary:{customerCode:params.get('customerCode'),sku:params.get('sku')},points:[
|
||||
{businessDate:'2026-10-10',dataStatus:'COMPLETE',outboundQuantity:4,observedQuantity:4},
|
||||
{businessDate:'2026-10-11',dataStatus:'UNKNOWN',outboundQuantity:null,observedQuantity:null},
|
||||
{businessDate:'2026-10-12',dataStatus:'PARTIAL',outboundQuantity:null,observedQuantity:1}]});
|
||||
if(route==='owner-overview')return params.has('current')||params.has('size')?response('B0001'):response('200',{context,customerCode:params.get('customerCode')});
|
||||
if(path.includes('/sync-tasks/'))return warehouse==='W1'?response('200',{task:{taskNo:'TASK-FIXTURE',warehouseCode:'W1'}}):response('B0001');
|
||||
if(route==='sync-tasks')return response('200',{records:[{taskNo:'TASK-FIXTURE',warehouseCode:warehouse,taskRole:'WAREHOUSE_SYNC'}],total:1});
|
||||
if(route==='sync-runs')return response('200',{records:[{taskNo:'TASK-FIXTURE'}],total:1});
|
||||
return response('200',{context,records:[],total:0});
|
||||
};
|
||||
return {requests,fetchImpl};
|
||||
}
|
||||
|
||||
test('explicit TEST execution and independent warehouse are required',()=>{
|
||||
assert.throws(()=>parseArguments(['--environment','prod']),/TEST/);
|
||||
assert.throws(()=>parseArguments(['--environment','TEST','--execute']),/SCOPE/);
|
||||
assert.throws(()=>parseArguments(['--environment','TEST','--warehouse','W1','--other-warehouse','W1']),/DIFFER/);
|
||||
assert.throws(()=>parseArguments(['--environment','TEST','--execute','--execute']),/DUPLICATE/);
|
||||
assert.equal(parseArguments(['--environment','TEST']).execute,false);
|
||||
assert.throws(()=>validateEnvironment({...env,CHONGCHONG_ENV:'DEV'}),/TEST/);
|
||||
assert.throws(()=>validateEnvironment({...env,CHONGCHONG_ADMIN_API_BASE_URL:'https://user:secret@test.example.invalid/api'}),/GATEWAY/);
|
||||
assert.throws(()=>validateEnvironment({...env,CHONGCHONG_ADMIN_API_BASE_URL:'https://test.example.invalid/api?token=secret'}),/GATEWAY/);
|
||||
});
|
||||
test('plan makes no network requests and never reads env files',async()=>{
|
||||
const p=await runAcceptance({execute:false},{env:{},fetchImpl:()=>{throw Error('network');}});
|
||||
assert.equal(p.mode,'PLAN');assert.equal(p.fixtureWrites,false);assert.equal(p.thirdPartyCalls,false);
|
||||
});
|
||||
test('real protocol coverage passes without printing or saving sensitive payloads',async()=>{
|
||||
const f=fixture(),snapshots=[];const result=await runAcceptance(options,{env,fetchImpl:f.fetchImpl,save:r=>snapshots.push(JSON.stringify(r))});
|
||||
assert.equal(result.outcome,'PASS',JSON.stringify(result));
|
||||
for(const id of ['scope.same-sku-other-owner','context.cross-warehouse','sync.off-controlled-rejection','sync.off-no-task-created'])
|
||||
assert.equal(result.cases.find(c=>c.id===id)?.status,'PASS',id);
|
||||
const serialized=snapshots.join('');
|
||||
for(const forbidden of ['password-secret','token-secret','refresh-secret','owner-secret','sku-secret','test.example.invalid','sensitive-message'])assert.ok(!serialized.includes(forbidden),forbidden);
|
||||
const commands=f.requests.filter(r=>r.method==='POST'&&!new URL(r.url).pathname.startsWith('/api/auth/'));
|
||||
assert.equal(commands.length,5);assert.ok(commands.every(r=>new URL(r.url).pathname.endsWith('/sync-tasks')&&r.body.action==='INVENTORY'));
|
||||
assert.equal(f.requests.filter(r=>new URL(r.url).pathname.endsWith('/auth/logout')).length,4);
|
||||
});
|
||||
test('OFF preflight blocks every command when mode is not OFF',async()=>{
|
||||
const f=fixture({mode:'VALIDATION'});const result=await runAcceptance(options,{env,fetchImpl:f.fetchImpl});
|
||||
assert.equal(result.outcome,'FAIL');assert.ok(!f.requests.some(r=>r.method==='POST'&&new URL(r.url).pathname.endsWith('/sync-tasks')));
|
||||
});
|
||||
test('without opt-in no sync POST occurs; missing permission identity stays blocked',async()=>{
|
||||
const f=fixture();const result=await runAcceptance({...options,probeOff:false},{env:{...env,SELFOP_READONLY_PASSWORD:''},fetchImpl:f.fetchImpl});
|
||||
assert.equal(result.outcome,'BLOCKED');assert.ok(!f.requests.some(r=>r.method==='POST'&&new URL(r.url).pathname.endsWith('/sync-tasks')));
|
||||
});
|
||||
test('a changed OFF preflight stops before all sync POST requests',async()=>{
|
||||
const f=fixture({changeMode:true});const result=await runAcceptance(options,{env,fetchImpl:f.fetchImpl});
|
||||
assert.equal(result.outcome,'FAIL');assert.ok(!f.requests.some(r=>r.method==='POST'&&new URL(r.url).pathname.endsWith('/sync-tasks')));
|
||||
assert.equal(result.cases.find(c=>c.id==='run.blocker').reason,'OFF_CHANGED_STOPPED');
|
||||
});
|
||||
test('internal errors never pass as controlled OFF rejection; unexpected acceptance aborts',async()=>{
|
||||
assert.equal(businessRejected({http:200,body:{code:'S0500'}}),false);
|
||||
assert.equal(businessRejected({http:503,body:{code:'B0001'}}),false);
|
||||
assert.equal(offRejected({http:200,body:{code:'B0001',message:'SELFOP_WAREHOUSE_UNCONFIGURED'}}),false);
|
||||
for(const config of [{offCode:'S0500'},{offMessage:'SELFOP_WAREHOUSE_UNCONFIGURED'},{acceptCommand:true}]){
|
||||
const f=fixture(config);const result=await runAcceptance(options,{env,fetchImpl:f.fetchImpl});
|
||||
assert.equal(result.outcome,'FAIL');assert.equal(result.cases.find(c=>c.id==='sync.off-controlled-rejection').status,'FAIL');
|
||||
}
|
||||
});
|
||||
test('evidence whitelist retains diagnosis and drops arbitrary body content',()=>{
|
||||
assert.deepEqual(evidence({http:200,body:{code:'B0001',requestId:'request-2',traceId:'trace-2',password:'secret',data:{accessToken:'secret'}}}),
|
||||
{http:200,code:'B0001',requestId:'request-2',traceId:'trace-2'});
|
||||
assert.equal(evidence({http:500,body:{code:'secret with spaces'}}).code,null);
|
||||
assert.throws(()=>pin({salesRunId:0,inventoryRunId:0}),/CONTEXT/);
|
||||
assert.equal(pin({salesRunId:0,inventoryRunId:0,queryAsOf:'2026-10-12T00:00:00Z'}).salesRunId,0);
|
||||
});
|
||||
|
||||
function syntheticFixture(){
|
||||
const notes={synthetic:true,fixtureId:'SYNTHETIC-SELFOP-20261012',sourceAccount:'lingxing-primary',hashes:{A:'hash-A',B:'hash-B'},
|
||||
currentVersions:{A:{salesRunId:'202',inventoryRunId:'203'},B:{salesRunId:'206',inventoryRunId:'207'}},oldSalesRunId:'201',assertions:[
|
||||
{endpoint:'owners',expected:{total:2,customerCodes:['SYNTH-OWNER-A','SYNTH-OWNER-B']}},
|
||||
{endpoint:'sku-sales-summary',expected:{total:2,records:{'SYNTH-OWNER-A':{outboundQuantity:30,observedQuantity:30,dataStatus:'COMPLETE',completeDayCount:2,partialDayCount:0,unknownDayCount:0},
|
||||
'SYNTH-OWNER-B':{outboundQuantity:10,observedQuantity:10,dataStatus:'COMPLETE'}}}},
|
||||
{endpoint:'sku-daily-sales',expected:{points:[{businessDate:'D-2',outboundQuantity:10,dataStatus:'COMPLETE'},{businessDate:'D-1',outboundQuantity:20,dataStatus:'COMPLETE'}],summaryOutboundQuantity:30}},
|
||||
{endpoint:'sku-sales-summary',params:{rangePreset:'TODAY'},expected:{'SYNTH-OWNER-A':{outboundQuantity:null,observedQuantity:5,dataStatus:'PARTIAL'},'SYNTH-OWNER-B':{outboundQuantity:null,observedQuantity:2,dataStatus:'PARTIAL'}}},
|
||||
{endpoint:'sku-sales-summary',params:{salesRunId:'201'},expected:{outboundQuantity:3,observedQuantity:3,dataStatus:'COMPLETE'}},
|
||||
{endpoint:'inventory-coverage',expected:{total:5,sharedByOwner:{'SYNTH-OWNER-A':{physicalQuantity:60,availableQuantity:60,outboundQuantity30d:30,inventoryCoverageDays:60},
|
||||
'SYNTH-OWNER-B':{physicalQuantity:20,availableQuantity:20,outboundQuantity30d:10,inventoryCoverageDays:60}},zero:{sku:'SYNTH-ZERO'},unknown:{sku:'SYNTH-UNKNOWN'},new:{sku:'SYNTH-NEW'}}},
|
||||
{endpoint:'hot-skus',expected:{total:2,order:['SYNTH-OWNER-A','SYNTH-OWNER-B'],sku:'SYNTH-SHARED-SKU',outboundQuantities:[30,10]}},
|
||||
{endpoint:'slow-skus',expected:{total:2,order:['SYNTH-OWNER-B','SYNTH-OWNER-A'],sku:'SYNTH-SHARED-SKU',outboundQuantities:[10,30]}},
|
||||
{endpoint:'sku-sales-summary',params:{warehouseCode:'SELFOP-TEST-B'},expected:{total:1,customerCode:'SYNTH-OWNER-A',outboundQuantity:999}},
|
||||
{endpoint:'sync-overview',expected:{A:{sampleCount:2,sampleDates:['D-3','D-1'],accumulationStartedOn:'D-3'},B:{sampleCount:0,accumulationStartedOn:null}}},
|
||||
]};
|
||||
const context={...notes.currentVersions.A,salesScopeHash:'hash-A',inventoryScopeHash:'hash-A',queryAsOf:'2026-10-12T04:00:00Z',anchorDate:'2026-10-12'};
|
||||
const cases=syntheticCases(notes,context),payloads=new Map();
|
||||
const row=(customerCode,sku,fields={})=>({sourceAccount:notes.sourceAccount,warehouseCode:'SELFOP-TEST-A',customerCode,sku,...fields});
|
||||
for(const scenario of cases){
|
||||
const id=scenario.id.substring(10),data={context:{...context},records:[],total:0};
|
||||
if(id==='owners'){data.records=['SYNTH-OWNER-A','SYNTH-OWNER-B'].map(customerCode=>({customerCode,customerName:null}));data.total=2;}
|
||||
if(id==='date-range-and-owner-isolation'){data.records=Object.entries(notes.assertions[1].expected.records).map(([code,fields])=>row(code,'SYNTH-SHARED-SKU',fields));data.total=2;}
|
||||
if(id==='daily-exact-axis'){data.summary={customerCode:'SYNTH-OWNER-A',sku:'SYNTH-SHARED-SKU',outboundQuantity:30};data.points=notes.assertions[2].expected.points.map((p,i)=>({...p,businessDate:fixtureDate(context.anchorDate,i-2)}));}
|
||||
if(id==='today-partial'){data.records=Object.entries(notes.assertions[3].expected).map(([code,fields])=>row(code,'SYNTH-SHARED-SKU',fields));data.total=2;}
|
||||
if(id==='old-version-fixed'){data.context.salesRunId='201';data.records=[row('SYNTH-OWNER-A','SYNTH-SHARED-SKU',notes.assertions[4].expected)];data.total=1;}
|
||||
if(id==='inventory-zero-unknown-and-age'){
|
||||
data.records=Object.entries(notes.assertions[5].expected.sharedByOwner).map(([code,fields])=>row(code,'SYNTH-SHARED-SKU',fields));
|
||||
data.records.push(row('SYNTH-OWNER-A','SYNTH-ZERO',{physicalQuantity:0,availableQuantity:0}),
|
||||
row('SYNTH-OWNER-A','SYNTH-UNKNOWN',{physicalQuantity:null,availableQuantity:null,inventoryCoverageDays:null,quantityStatuses:{physicalQuantity:'UNKNOWN',availableQuantity:'UNKNOWN'}}),
|
||||
row('SYNTH-OWNER-A','SYNTH-NEW',{observationEligible:false,inventoryCoverageDays:null}));data.total=5;
|
||||
}
|
||||
if(id==='hot-skus-eligibility-order'||id==='slow-skus-eligibility-order'){
|
||||
const expected=notes.assertions[id.startsWith('hot')?6:7].expected;
|
||||
data.records=expected.order.map((code,i)=>row(code,expected.sku,{outboundQuantity:expected.outboundQuantities[i]}));data.total=2;
|
||||
}
|
||||
if(id==='warehouse-b-positive'){
|
||||
data.context={...context,...notes.currentVersions.B,salesScopeHash:'hash-B',inventoryScopeHash:'hash-B'};
|
||||
data.records=[{...row('SYNTH-OWNER-A','SYNTH-SHARED-SKU',{outboundQuantity:999}),warehouseCode:'SELFOP-TEST-B'}];data.total=1;
|
||||
}
|
||||
if(id==='zero-sales-versus-unknown')data.points=[-2,-1].map(offset=>({businessDate:fixtureDate(context.anchorDate,offset),dataStatus:'COMPLETE',outboundQuantity:'0.000000',observedQuantity:0}));
|
||||
if(id==='pre-existence-is-unknown')data.points=[{businessDate:'2026-10-10',dataStatus:'UNKNOWN',outboundQuantity:null,observedQuantity:null},
|
||||
{businessDate:'2026-10-11',dataStatus:'COMPLETE',outboundQuantity:0}];
|
||||
if(id.startsWith('daily-sample-gaps-')){
|
||||
const letter=id.at(-1),expected=notes.assertions[9].expected[letter],present=new Set((expected.sampleDates??[]).map(p=>fixtureDate(context.anchorDate,Number(p.slice(1)))));
|
||||
data.collectionMode='OFF';data.warehouses=[{warehouseCode:'SELFOP-TEST-'+letter,dataFamily:'INVENTORY',executionEnabled:false,publicationEnabled:false,
|
||||
inventoryDailySamples:{windowFrom:'2026-09-12',windowUntil:'2026-10-12',expectedDays:30,sampleCount:expected.sampleCount,
|
||||
accumulationStartedOn:expected.accumulationStartedOn===null?null:'2026-10-09',missingDates:Array.from({length:30},(_,i)=>fixtureDate(context.anchorDate,i-30)).filter(p=>!present.has(p))}}];
|
||||
}
|
||||
payloads.set(scenario.id,data);
|
||||
}
|
||||
return {notes,context,cases,payloads};
|
||||
}
|
||||
test('reviewed synthetic fixture covers all thirteen business checks with bound old/current version pairs',()=>{
|
||||
const f=syntheticFixture();assert.equal(f.cases.length,13);
|
||||
validateSyntheticNotes(f.notes,{account:'lingxing-primary',warehouse:'SELFOP-TEST-A',otherWarehouse:'SELFOP-TEST-B'});
|
||||
assert.throws(()=>validateSyntheticNotes({...f.notes,synthetic:false},options),/SYNTHETIC/);
|
||||
for(const c of f.cases)assert.equal(c.verify(f.payloads.get(c.id)),true,c.id);
|
||||
const old=f.cases.find(c=>c.id==='synthetic.old-version-fixed');
|
||||
assert.equal(old.params.salesRunId,'201');assert.equal(old.params.inventoryRunId,'203');assert.equal(old.params.queryAsOf,f.context.queryAsOf);
|
||||
assert.ok(f.cases.every(c=>c.route!=='sync-tasks'&&!c.route.includes('publish')));
|
||||
assert.equal(fixtureDate('2026-03-01',-1),'2026-02-28');
|
||||
});
|
||||
test('synthetic checks reject mixed owners/warehouses, silent current-version fallback, zero-filling and ineligible Top rows',()=>{
|
||||
const f=syntheticFixture();
|
||||
const reject=(id,mutate)=>{const c=f.cases.find(c=>c.id==='synthetic.'+id),data=structuredClone(f.payloads.get(c.id));mutate(data);assert.equal(c.verify(data),false,id);};
|
||||
reject('date-range-and-owner-isolation',d=>{d.records[0].observedQuantity+=999;});
|
||||
reject('date-range-and-owner-isolation',d=>{d.records[1].customerCode='SYNTH-OWNER-A';});
|
||||
reject('warehouse-b-positive',d=>{d.records[0].warehouseCode='SELFOP-TEST-A';});
|
||||
reject('old-version-fixed',d=>{d.context.salesRunId='202';});
|
||||
reject('inventory-zero-unknown-and-age',d=>{d.records[3].availableQuantity=0;});
|
||||
reject('pre-existence-is-unknown',d=>{d.points[0].observedQuantity=0;});
|
||||
reject('daily-exact-axis',d=>{d.points[1].businessDate='2026-10-12';});
|
||||
reject('hot-skus-eligibility-order',d=>{d.records[0].sku='SYNTH-NEW';});
|
||||
reject('slow-skus-eligibility-order',d=>{d.records.reverse();});
|
||||
reject('daily-sample-gaps-A',d=>{d.warehouses[0].inventoryDailySamples.missingDates.push('2026-10-11');});
|
||||
reject('daily-sample-gaps-B',d=>{d.warehouses[0].inventoryDailySamples.sampleCount=1;});
|
||||
});
|
||||
test('synthetic report explicitly marks provider evidence and elapsed expiration as unverified without adding a wait blocker',async()=>{
|
||||
const f=syntheticFixture();const result=await runAcceptance({...options,account:'lingxing-primary',warehouse:'SELFOP-TEST-A',otherWarehouse:'SELFOP-TEST-B'},
|
||||
{env:{CHONGCHONG_ENV:'TEST',CHONGCHONG_ADMIN_API_BASE_URL:env.CHONGCHONG_ADMIN_API_BASE_URL},syntheticNotes:f.notes,
|
||||
fetchImpl:async()=>({status:401,headers:new Headers(),json:async()=>({code:'A0401'})})});
|
||||
assert.equal(result.synthetic,true);assert.equal(result.evidenceType,'SYNTHETIC_QUERY_FIXTURE_NOT_PROVIDER_EVIDENCE');
|
||||
assert.ok(result.uncovered.some(item=>item.includes('30 elapsed minutes')));
|
||||
assert.ok(!result.cases.some(c=>c.id==='context.expired'));
|
||||
assert.equal(parseArguments(['--environment','TEST','--synthetic-fixture','notes.json']).syntheticFixture,'notes.json');
|
||||
});
|
||||
test('optional nullable fields survive NON_NULL wire serialization while zero-filling stays rejected',()=>{
|
||||
const f=syntheticFixture();
|
||||
const omitNulls=value=>Array.isArray(value)?value.map(omitNulls):value&&typeof value==='object'?
|
||||
Object.fromEntries(Object.entries(value).filter(([,v])=>v!==null).map(([k,v])=>[k,omitNulls(v)])):value;
|
||||
for(const c of f.cases)assert.equal(c.verify(omitNulls(f.payloads.get(c.id))),true,c.id);
|
||||
for(const id of ['synthetic.today-partial','synthetic.pre-existence-is-unknown','synthetic.inventory-zero-unknown-and-age']){
|
||||
const c=f.cases.find(c=>c.id===id),data=omitNulls(f.payloads.get(id));
|
||||
if(id.endsWith('today-partial'))data.records[0].outboundQuantity=0;
|
||||
else if(id.endsWith('pre-existence-is-unknown'))data.points[0].observedQuantity=0;
|
||||
else data.records[3].availableQuantity=0;
|
||||
assert.equal(c.verify(data),false,id);
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
# 后端脚本管理与全量 HTTP 验收索引
|
||||
|
||||
`../lastleg-service/scripts/selfoperated-test-env-acceptance.mjs`:自营统计 TEST Gateway 验收,默认仅输出计划;显式 `--execute --environment TEST --source-account <账号键> --warehouse <仓库编码> --other-warehouse <第二仓> --report <报告路径>` 才登录并查询。凭据由运行者通过脚本之前的 `node --env-file=<明确TEST文件>` 注入,需平台、只读平台、无权限平台及客户身份。`--probe-off-command` 仅在确认 OFF 后提交预期被拒绝的命令并核对无新增任务;不采集、发布、造数或写 SQL。`--synthetic-fixture <审核后的说明文件>` 验证合成样本,不能证明真实来源覆盖。报告脱敏记录各项结果及请求标识;失败保留报告,重跑使用新报告路径并重新登录,结束退出自身会话,不清理业务样本。离线合同为同目录 `selfoperated-test-env-acceptance.test.mjs`。
|
||||
|
||||
`db/return-consolidated-incremental-20261012.sql`:10月10日/12日退货、签收、清点、换标与按件出库增量汇总,显式 `USE baiyue_lastleg`,MySQL 8.0.16+ 已有业务库可重复执行。本轮补齐签收原包装、尺寸重量来源及库存箱原包装三个字段;逐列判断存在性,不回填签收/清点事实,不改库存数量。连接实例及凭据由执行者明确指定,整份执行、遇错停止;保留执行错误,修复后可重跑,末尾输出字段、索引与约束供核对。托盘重量、体积汇总是应用逻辑,不新增资源库字段;不自动执行到测试或生产环境。
|
||||
|
||||
`procurement-sample-test-env.mjs` 是采购样品正常全链路 TEST 专项入口,默认或 `--plan` 零网络。真实执行使用 `node --env-file="<授权目录>/test-acceptance.env" scripts/procurement-sample-test-env.mjs --execute --environment TEST --run-id <唯一短批次> --warehouse-id <已核对有效海外收货仓>`。只允许当前 TEST 工作区及指定配置;由正式 API 创建独立客户并完成资质,人工充值10元,上传凭证/面单,调用内部 `procurement-sample-fulfillment.mjs` 完成两箱正常履约。充值、文件和采购请求前落盘,结果未知停写;报告独占 `reports/procurement-sample-<批次>/report.json`,不覆盖旧报告,不自动重跑,不执行 SQL/部署/删除。结束退出自身平台与客户会话,保留样本。通过只表示该正常路径,不替代异常、退款、权限、真实页面和请求量验收。离线入口合同为 `procurement-sample-test-env.test.mjs`;内部模块安全合同见模块索引。
|
||||
|
||||
Reference in New Issue
Block a user