FieldOps/scripts/quality-smoke.ts

156 lines
6.1 KiB
TypeScript

/**
* MY QUALITY smoke test — exercises the operatorSession + qualityDefect
* procedures end-to-end via tRPC callers (same pattern as report-smoke.ts):
* QCP raises a defect -> the badged-in operator sees it -> acknowledges ->
* corrects. Plus role guards and state-machine conflicts.
*
* Run: pnpm tsx scripts/quality-smoke.ts
* Requires: Docker Postgres running + pnpm db:seed already done.
* NOTE: this creates a defect and starts/ends op2's session; re-run db:seed
* afterwards for a pristine demo state.
*/
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { config as loadEnv } from 'dotenv';
const here = path.dirname(fileURLToPath(import.meta.url));
loadEnv({ path: path.resolve(here, '../.env') });
import { prisma } from '../packages/db/src/index.js';
import { appRouter, createTRPCContext } from '../packages/api/src/index.js';
import { createCallerFactory } from '../packages/api/src/trpc.js';
let passed = 0;
let failed = 0;
function ok(label: string) {
console.log(`${label}`);
passed++;
}
function fail(label: string, detail?: string) {
console.error(`${label}${detail ? `${detail}` : ''}`);
failed++;
}
function assert(condition: boolean, label: string, detail?: string) {
if (condition) ok(label);
else fail(label, detail);
}
function codeOf(err: unknown): string | undefined {
return (err as { code?: string }).code;
}
async function makeCaller(email: string) {
const user = await prisma.user.findFirst({ where: { email } });
if (!user) throw new Error(`User ${email} not found — run pnpm db:seed`);
const ctx = await createTRPCContext({
user: {
id: user.id,
email: user.email,
role: user.role as 'ADMIN' | 'SUPERVISOR' | 'QUALITY' | 'OPERATOR',
tenantId: user.tenantId,
},
headers: new Headers(),
});
return createCallerFactory(appRouter)(ctx);
}
async function main() {
console.log('\nMY QUALITY smoke — running assertions against the real procedures…\n');
const qcp = await makeCaller('qcp@demo.local');
const op1 = await makeCaller('op1@demo.local');
const op2 = await makeCaller('op2@demo.local');
// 1. op1 has a seeded active session at CTR04
const session = await op1.operatorSession.current();
assert(!!session, 'op1 has an active session');
assert(session?.workstation.code === 'CTR04', `op1 session at CTR04 (got ${session?.workstation.code})`);
const workstationId = session!.workstationId;
// 2. QCP raises a defect at op1's workstation
const created = await qcp.qualityDefect.create({
workstationId,
defectType: 'Smoke — aperto',
location: 'Banco traseiro',
description: 'Defeito de teste criado pelo smoke.',
rfsCode: 'RFS-SMOKE',
});
assert(created.status === 'OPEN', `created defect is OPEN (got ${created.status})`);
assert(created.workstation.code === 'CTR04', 'created defect at CTR04');
const defectId = created.id;
// 3. op1 sees it in forMyStation
const mine = await op1.qualityDefect.forMyStation();
assert(mine.some((d) => d.id === defectId), 'op1 sees the new defect at their station');
// 4. op2 (no session at CTR04) does NOT see it
const op2current = await op2.operatorSession.current();
if (!op2current) {
const op2defects = await op2.qualityDefect.forMyStation();
assert(op2defects.length === 0, 'op2 (not badged in) sees no defects');
} else {
ok('op2 already had a session (skipping no-session check)');
}
// 5. op1 acknowledges (OPEN -> ACKNOWLEDGED)
const ack = await op1.qualityDefect.acknowledge({ id: defectId });
assert(ack.status === 'ACKNOWLEDGED', `acknowledge -> ACKNOWLEDGED (got ${ack.status})`);
assert(ack.acknowledgedBy?.email === 'op1@demo.local', 'acknowledgedBy is op1');
// 6. acknowledge again -> CONFLICT
try {
await op1.qualityDefect.acknowledge({ id: defectId });
fail('re-acknowledge -> CONFLICT', 'no error thrown');
} catch (err) {
assert(codeOf(err) === 'CONFLICT', 're-acknowledge -> CONFLICT', `got ${codeOf(err)}`);
}
// 7. op1 corrects (ACKNOWLEDGED -> CORRECTED)
const corrected = await op1.qualityDefect.correct({ id: defectId, correctionNote: 'Corrigido no smoke.' });
assert(corrected.status === 'CORRECTED', `correct -> CORRECTED (got ${corrected.status})`);
assert(corrected.correctedBy?.email === 'op1@demo.local', 'correctedBy is op1');
// 8. correct again -> CONFLICT
try {
await op1.qualityDefect.correct({ id: defectId });
fail('re-correct -> CONFLICT', 'no error thrown');
} catch (err) {
assert(codeOf(err) === 'CONFLICT', 're-correct -> CONFLICT', `got ${codeOf(err)}`);
}
// 9. corrected defect drops out of the operator's default forMyStation (OPEN+ACK)
const mineAfter = await op1.qualityDefect.forMyStation();
assert(!mineAfter.some((d) => d.id === defectId), 'CORRECTED defect no longer in forMyStation default');
// 10. operator may NOT create a defect (requireRole QUALITY/ADMIN)
try {
await op1.qualityDefect.create({ workstationId, defectType: 'x', description: 'nope nope' });
fail('operator create -> FORBIDDEN', 'no error thrown');
} catch (err) {
assert(codeOf(err) === 'FORBIDDEN', 'operator create -> FORBIDDEN', `got ${codeOf(err)}`);
}
// 11. QCP queue includes the defect we created
const queue = await qcp.qualityDefect.queue({ statuses: ['CORRECTED'] });
assert(queue.some((d) => d.id === defectId), 'QCP queue lists the corrected defect');
// 12. operatorSession start/end roundtrip on op2
const startedAt2 = await op2.operatorSession.start({ workstationId });
assert(startedAt2.workstationId === workstationId, 'op2 badge-in starts a session');
const op2now = await op2.operatorSession.current();
assert(op2now?.id === startedAt2.id, 'op2 current() returns the started session');
await op2.operatorSession.end();
const op2ended = await op2.operatorSession.current();
assert(op2ended === null, 'op2 badge-out clears the active session');
console.log(`\n${passed} passed, ${failed} failed.\n`);
if (failed > 0) process.exit(1);
}
main()
.catch((err) => {
console.error('Quality smoke failed:', err);
process.exit(1);
})
.finally(() => prisma.$disconnect());