156 lines
6.1 KiB
TypeScript
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());
|