Some checks are pending
ci / verify (push) Waiting to run
Executors could not see the audit: Grok reported that the owner's AUDIT.md and evidence never reached it, so its scenarios could not be mapped to the audit IDs. Keeping the audit, the acceptance report and the task for each executor in the repository lets Codex, Antigravity and Grok take work and report through git. The live-check scripts are stored as .mjs.txt so the project linter, which type-checks every .mjs file, does not break for everyone; the maintained version of these checks is Grok's harness in tests/e2e. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
213 lines
7.9 KiB
Text
213 lines
7.9 KiB
Text
// Live check of the offline sync branch (5ee7517) on a real PostgreSQL 17.
|
||
import { createRequire } from 'node:module';
|
||
import { spawn } from 'node:child_process';
|
||
import { randomBytes } from 'node:crypto';
|
||
|
||
const REPO = 'C:/Users/Ochenstarik/Agent_projects/krona-staff-sync';
|
||
const require = createRequire(`${REPO}/package.json`);
|
||
const pg = require('pg');
|
||
const { ulid } = require('ulid');
|
||
|
||
const SUPER = 'postgres://postgres:audit-superuser@127.0.0.1:55432/krona_staff_sync';
|
||
const APP = 'postgres://krona_staff_app:audit-app-password@127.0.0.1:55432/krona_staff_sync';
|
||
const PORT = 18081;
|
||
const BASE = `http://127.0.0.1:${PORT}/api/v1/staff`;
|
||
const TENANT = ulid();
|
||
const MANAGER_TOKEN = randomBytes(32).toString('base64url');
|
||
const TERMINAL_TOKEN = randomBytes(32).toString('base64url');
|
||
|
||
async function superQuery(sql, params = []) {
|
||
const client = new pg.Client({ connectionString: SUPER });
|
||
await client.connect();
|
||
try {
|
||
return await client.query(sql, params);
|
||
} finally {
|
||
await client.end();
|
||
}
|
||
}
|
||
|
||
async function call(method, path, { token, body } = {}) {
|
||
const response = await fetch(`${BASE}${path}`, {
|
||
method,
|
||
headers: {
|
||
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
||
...(token ? { authorization: `Bearer ${token}` } : {}),
|
||
},
|
||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||
});
|
||
const text = await response.text();
|
||
try {
|
||
return { status: response.status, json: JSON.parse(text) };
|
||
} catch {
|
||
return { status: response.status, json: text };
|
||
}
|
||
}
|
||
|
||
const brief = (value) => {
|
||
const text = typeof value === 'string' ? value : JSON.stringify(value);
|
||
return text.length > 260 ? `${text.slice(0, 260)}…` : text;
|
||
};
|
||
|
||
// S9: deleting an org unit referenced by a sync device
|
||
const s9Unit = ulid();
|
||
const s9Device = ulid();
|
||
await superQuery(`INSERT INTO staff_org_units (id, tenant_id, code, name) VALUES ($1, $2, 's9', 'S9')`, [
|
||
s9Unit,
|
||
TENANT,
|
||
]);
|
||
const deviceColumns = (
|
||
await superQuery(
|
||
`SELECT column_name, is_nullable, column_default FROM information_schema.columns
|
||
WHERE table_name = 'staff_sync_devices' ORDER BY ordinal_position`,
|
||
)
|
||
).rows;
|
||
const values = {
|
||
id: s9Device,
|
||
tenant_id: TENANT,
|
||
device_code: 's9-device',
|
||
name: 'S9',
|
||
token_hash: 'a'.repeat(64),
|
||
token_hint: 'hint',
|
||
org_unit_id: s9Unit,
|
||
created_by: ulid(),
|
||
status: 'active',
|
||
};
|
||
const insertable = deviceColumns.filter(
|
||
(column) => values[column.column_name] !== undefined,
|
||
);
|
||
await superQuery(
|
||
`INSERT INTO staff_sync_devices (${insertable.map((c) => c.column_name).join(', ')})
|
||
VALUES (${insertable.map((_, index) => `$${index + 1}`).join(', ')})`,
|
||
insertable.map((column) => values[column.column_name]),
|
||
).catch((error) => console.log(`S9 setup: ${error.code} ${error.message}`));
|
||
try {
|
||
await superQuery(`DELETE FROM staff_org_units WHERE id = $1`, [s9Unit]);
|
||
console.log('[S9] не подтверждено — точку с привязанным устройством удалить удалось');
|
||
} catch (error) {
|
||
console.log(`[S9] ПОДТВЕРЖДЕНО — удаление точки с устройством → ${error.code} ${error.message}`);
|
||
}
|
||
|
||
// Seed an employee and a unit for S1
|
||
const orgUnit = ulid();
|
||
const employee = ulid();
|
||
await superQuery(
|
||
`INSERT INTO staff_org_units (id, tenant_id, code, name) VALUES ($1, $2, 'sync-unit', 'Точка')`,
|
||
[orgUnit, TENANT],
|
||
);
|
||
await superQuery(
|
||
`INSERT INTO staff_employees (id, tenant_id, personnel_number, display_name, status)
|
||
VALUES ($1, $2, 'S-001', 'Сотрудник Синхронизации', 'active')`,
|
||
[employee, TENANT],
|
||
);
|
||
|
||
const server = spawn(process.execPath, ['node_modules/tsx/dist/cli.mjs', 'src/server.ts'], {
|
||
cwd: REPO,
|
||
env: {
|
||
...process.env,
|
||
NODE_ENV: 'development',
|
||
HOST: '127.0.0.1',
|
||
PORT: String(PORT),
|
||
LOG_LEVEL: 'silent',
|
||
STAFF_RUNTIME_MODE: 'standalone',
|
||
STANDALONE_ACCESS: 'employee',
|
||
STANDALONE_MANAGER_TOKEN: MANAGER_TOKEN,
|
||
STANDALONE_TERMINAL_TOKEN: TERMINAL_TOKEN,
|
||
DATABASE_URL: APP,
|
||
STANDALONE_TENANT_ID: TENANT,
|
||
STANDALONE_ACTOR_ID: ulid(),
|
||
},
|
||
stdio: 'ignore',
|
||
});
|
||
|
||
try {
|
||
for (let attempt = 0; attempt < 120; attempt += 1) {
|
||
try {
|
||
if ((await fetch(`${BASE}/live`)).ok) break;
|
||
} catch {}
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
}
|
||
|
||
const registered = await call('POST', '/sync/devices', {
|
||
token: MANAGER_TOKEN,
|
||
body: { device_code: 'kiosk-1', name: 'Киоск', org_unit_id: orgUnit },
|
||
});
|
||
const deviceToken = registered.json?.token;
|
||
console.log(`device registration → ${registered.status}; scopes ${JSON.stringify(registered.json?.device?.allowed_scopes)}`);
|
||
|
||
const forged = await call('POST', '/sync/push', {
|
||
token: deviceToken,
|
||
body: {
|
||
commands: [
|
||
{
|
||
command_type: 'upsert_work_session',
|
||
schema_version: 1,
|
||
idempotency_key: `forge-${Date.now()}`,
|
||
correlation_id: ulid(),
|
||
created_at: new Date().toISOString(),
|
||
payload: {
|
||
employee_id: employee,
|
||
org_unit_id: orgUnit,
|
||
business_date: '2026-09-15',
|
||
worked_minutes: 2880,
|
||
status: 'confirmed',
|
||
},
|
||
},
|
||
{
|
||
command_type: 'record_attendance',
|
||
schema_version: 1,
|
||
idempotency_key: `old-${Date.now()}`,
|
||
correlation_id: ulid(),
|
||
created_at: new Date().toISOString(),
|
||
payload: {
|
||
employee_id: employee,
|
||
event_type: 'check_in',
|
||
occurred_at: '2020-01-01T00:00:00Z',
|
||
},
|
||
},
|
||
],
|
||
},
|
||
});
|
||
console.log(`device push → ${forged.status} ${brief(forged.json)}`);
|
||
|
||
const session = await superQuery(
|
||
`SELECT status, worked_minutes, confirmed_by, correction_note
|
||
FROM staff_work_sessions WHERE tenant_id = $1 AND employee_id = $2 AND business_date = '2026-09-15'`,
|
||
[TENANT, employee],
|
||
);
|
||
const oldEvent = await superQuery(
|
||
`SELECT occurred_at FROM staff_attendance_events
|
||
WHERE tenant_id = $1 AND employee_id = $2 AND occurred_at < '2021-01-01'`,
|
||
[TENANT, employee],
|
||
);
|
||
const audit = await superQuery(
|
||
`SELECT count(*)::int AS entries FROM staff_audit_log WHERE tenant_id = $1`,
|
||
[TENANT],
|
||
);
|
||
const row = session.rows[0];
|
||
console.log(
|
||
`[S1] ${row?.status === 'confirmed' ? 'ПОДТВЕРЖДЕНО' : 'не подтверждено'} — смена от устройства: статус ${row?.status}, минут ${row?.worked_minutes}, подтвердил ${row?.confirmed_by ?? 'никто'}, записей в журнале аудита арендатора: ${audit.rows[0].entries}`,
|
||
);
|
||
console.log(
|
||
`[S2] ${oldEvent.rowCount > 0 ? 'ПОДТВЕРЖДЕНО' : 'не подтверждено'} — отметка с временем 2020-01-01 через синхронизацию сохранена: ${oldEvent.rowCount > 0}`,
|
||
);
|
||
|
||
// The forged session flows into payroll (outbox default only in this throwaway database, see N2)
|
||
await superQuery('ALTER TABLE staff_outbox ALTER COLUMN occurred_at SET DEFAULT now()');
|
||
try {
|
||
const rules = await call('GET', '/pay-rules', { token: MANAGER_TOKEN });
|
||
const hourly = (rules.json?.pay_rules ?? []).find((rule) => /hour/.test(rule.code));
|
||
await call('PUT', `/employees/${employee}/pay-assignment`, {
|
||
token: MANAGER_TOKEN,
|
||
body: { pay_rule_id: hourly?.id, org_unit_id: orgUnit, rate_minor: 30000, effective_from: '2026-09-01' },
|
||
});
|
||
const run = await call('POST', '/payroll/calculate', { token: MANAGER_TOKEN, body: { month: '2026-09' } });
|
||
const total = run.json?.total_accrued_minor ?? run.json?.totals?.accrued_minor;
|
||
console.log(
|
||
`[S1→payroll] расчёт сентября → ${run.status}; начислено по поддельной смене: ${total !== undefined ? Number(total) / 100 : brief(run.json)} ₽ (48 ч × 300 ₽)`,
|
||
);
|
||
} finally {
|
||
await superQuery('ALTER TABLE staff_outbox ALTER COLUMN occurred_at DROP DEFAULT');
|
||
}
|
||
} finally {
|
||
server.kill();
|
||
}
|