job-application-agent 3.4.2 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/job-application-agent/SKILL.md +11 -2
- package/job-application-agent/capabilities.json +2 -1
- package/job-application-agent/references/ACCOUNTING.md +104 -0
- package/job-application-agent/references/CLOUD_STATE.md +2 -0
- package/job-application-agent/references/RUNS.md +6 -0
- package/job-application-agent/references/SCHEMAS.md +4 -0
- package/job-application-agent/scripts/application-accounting.mjs +246 -0
- package/job-application-agent/scripts/cloud-state-client.mjs +57 -7
- package/job-application-agent/scripts/job-application.mjs +126 -24
- package/job-application-agent/scripts/version.mjs +1 -1
- package/job-application-agent/tests/accounting-cli.test.mjs +86 -0
- package/job-application-agent/tests/accounting-cloud-client.test.mjs +183 -0
- package/job-application-agent/tests/accounting-retry-cli.test.mjs +96 -0
- package/job-application-agent/tests/accounting-source-race.test.mjs +109 -0
- package/job-application-agent/tests/application-accounting.test.mjs +315 -0
- package/job-application-agent/tests/job-application.test.mjs +5 -0
- package/job-application-agent/tests/review-cadence.test.mjs +66 -0
- package/job-application-agent/tests/workflow-state.test.mjs +18 -2
- package/package.json +2 -2
|
@@ -7,6 +7,7 @@ import { platform } from 'node:os';
|
|
|
7
7
|
import { basename, join, resolve } from 'node:path';
|
|
8
8
|
import { pathToFileURL } from 'node:url';
|
|
9
9
|
|
|
10
|
+
import { ACCOUNTING_CAPABILITY, accountingApplicationKey, canonicalUrl, deliveryProjection, discoveryProjection, validateDelivery, validateDeliveryReferences, validateLead, validateLeadReferences, stableJson } from './application-accounting.mjs';
|
|
10
11
|
import { createSecretStore, migrateLegacyStateDir, resolveStateDir } from './secret-store.mjs';
|
|
11
12
|
import { SourceCommunityClient } from './source-community-client.mjs';
|
|
12
13
|
import { normalizeCommunityJob, normalizeCommunitySource } from './source-community-schema.mjs';
|
|
@@ -506,12 +507,7 @@ function rolesLikelySame(left, right) {
|
|
|
506
507
|
return shared / Math.min(leftTokens.size, rightTokens.size) >= 0.75;
|
|
507
508
|
}
|
|
508
509
|
|
|
509
|
-
|
|
510
|
-
if (entry.employerJobId) return `job:${normalizedText(entry.company)}:${String(entry.employerJobId).toLowerCase()}`;
|
|
511
|
-
if (entry.company && entry.role) return `legacy-role:${normalizedText(entry.company)}:${normalizedText(entry.role)}`;
|
|
512
|
-
if (entry.url) return `url:${normalizeUrl(entry.url)}`;
|
|
513
|
-
return `id:${entry.id ?? fallback}`;
|
|
514
|
-
}
|
|
510
|
+
const canonicalApplicationKey = accountingApplicationKey;
|
|
515
511
|
|
|
516
512
|
function businessDaysBetween(startValue, endValue) {
|
|
517
513
|
const start = new Date(startValue);
|
|
@@ -525,8 +521,10 @@ function businessDaysBetween(startValue, endValue) {
|
|
|
525
521
|
return days;
|
|
526
522
|
}
|
|
527
523
|
|
|
528
|
-
export function buildReview(entries, outcomeEntries = [], acknowledgements = [], now = new Date()) {
|
|
524
|
+
export function buildReview(entries, outcomeEntries = [], acknowledgements = [], now = new Date(), deliveryEvents = []) {
|
|
529
525
|
const submissions = entries.filter((entry) => !Number.isNaN(Date.parse(entry.submittedAt)));
|
|
526
|
+
const delivery = deliveryProjection(submissions, deliveryEvents);
|
|
527
|
+
const effectiveKeys = new Set(submissions.filter((entry, i) => delivery.applications[i].counted).map(canonicalApplicationKey));
|
|
530
528
|
const explicitOutcomes = outcomeEntries.length > 0;
|
|
531
529
|
const outcomes = explicitOutcomes ? outcomeEntries : entries.filter((entry) => ['interview', 'rejected', 'offer', 'withdrawn'].includes(entry.status));
|
|
532
530
|
const groups = new Map();
|
|
@@ -535,7 +533,8 @@ export function buildReview(entries, outcomeEntries = [], acknowledgements = [],
|
|
|
535
533
|
if (!groups.has(key)) groups.set(key, []);
|
|
536
534
|
groups.get(key).push(entry);
|
|
537
535
|
});
|
|
538
|
-
const
|
|
536
|
+
const recordedCanonical = [...groups.values()].map(group => [...group].sort((a, b) => Date.parse(a.submittedAt) - Date.parse(b.submittedAt))[0]);
|
|
537
|
+
const canonical = [...groups].filter(([key]) => effectiveKeys.has(key)).map(([,group]) => [...group].sort((a, b) => Date.parse(a.submittedAt) - Date.parse(b.submittedAt))[0]);
|
|
539
538
|
const outcomesById = new Map();
|
|
540
539
|
for (const outcome of outcomes) {
|
|
541
540
|
if (!outcomesById.has(outcome.id)) outcomesById.set(outcome.id, []);
|
|
@@ -544,7 +543,7 @@ export function buildReview(entries, outcomeEntries = [], acknowledgements = [],
|
|
|
544
543
|
const canonicalOutcomes = [];
|
|
545
544
|
const matureCanonicalOutcomes = [];
|
|
546
545
|
const canonicalInterviewDetails = [];
|
|
547
|
-
for (const group of groups
|
|
546
|
+
for (const [groupKey, group] of groups) {
|
|
548
547
|
const candidates = explicitOutcomes
|
|
549
548
|
? group.flatMap((entry) => outcomesById.get(entry.id) ?? [])
|
|
550
549
|
: group.filter((entry) => ['interview', 'rejected', 'offer', 'withdrawn'].includes(entry.status));
|
|
@@ -559,14 +558,15 @@ export function buildReview(entries, outcomeEntries = [], acknowledgements = [],
|
|
|
559
558
|
source: canonicalApplication.source,
|
|
560
559
|
score: canonicalApplication.score,
|
|
561
560
|
});
|
|
562
|
-
if (businessDaysBetween(canonicalApplication.submittedAt, now) >= 10) matureCanonicalOutcomes.push(latest);
|
|
561
|
+
if (effectiveKeys.has(groupKey) && businessDaysBetween(canonicalApplication.submittedAt, now) >= 10) matureCanonicalOutcomes.push(latest);
|
|
563
562
|
}
|
|
564
563
|
}
|
|
565
564
|
const maturedApplications = canonical.filter((entry) => businessDaysBetween(entry.submittedAt, now) >= 10).length;
|
|
566
565
|
const lastAck = acknowledgements.length ? acknowledgements[acknowledgements.length - 1] : {};
|
|
567
|
-
const
|
|
566
|
+
const recordedMaturedApplicationCount = recordedCanonical.filter(entry => businessDaysBetween(entry.submittedAt, now) >= 10).length;
|
|
567
|
+
const submittedSinceLastReview = Math.max(0, recordedCanonical.length - (lastAck.uniqueSubmissionCount ?? 0));
|
|
568
568
|
const hygieneDue = submittedSinceLastReview >= 10;
|
|
569
|
-
const outcomeDue =
|
|
569
|
+
const outcomeDue = recordedMaturedApplicationCount - (lastAck.maturedApplicationCount ?? 0) >= 20;
|
|
570
570
|
const reviewReasons = [...(hygieneDue ? ['submission-hygiene'] : []), ...(outcomeDue ? ['outcome-effectiveness'] : [])];
|
|
571
571
|
const outcomeCounts = Object.fromEntries(['interview', 'rejected', 'offer', 'withdrawn'].map((status) => [status, canonicalOutcomes.filter((entry) => entry.status === status).length]));
|
|
572
572
|
const matureOutcomeCounts = Object.fromEntries(['interview', 'rejected', 'offer', 'withdrawn'].map((status) => [status, matureCanonicalOutcomes.filter((entry) => entry.status === status).length]));
|
|
@@ -598,9 +598,14 @@ export function buildReview(entries, outcomeEntries = [], acknowledgements = [],
|
|
|
598
598
|
submittedTotal: canonical.length,
|
|
599
599
|
uniqueSubmittedTotal: canonical.length,
|
|
600
600
|
rawSubmissionRows: submissions.length,
|
|
601
|
-
duplicateSubmissionRows: submissions.length -
|
|
601
|
+
duplicateSubmissionRows: submissions.length - groups.size,
|
|
602
|
+
recordedSubmissionCount: groups.size,
|
|
603
|
+
effectiveSubmissionCount: canonical.length,
|
|
604
|
+
failedDeliveryCount: delivery.failedDeliveryCount,
|
|
605
|
+
receiptUnknownEmailCount: delivery.receiptUnknownEmailCount,
|
|
602
606
|
submittedSinceLastReview,
|
|
603
607
|
maturedApplications,
|
|
608
|
+
recordedMaturedApplicationCount,
|
|
604
609
|
outcomeCounts,
|
|
605
610
|
matureOutcomeCounts,
|
|
606
611
|
reasonCounts,
|
|
@@ -1081,7 +1086,7 @@ async function ledgerAdd(entryInput, duplicateOverride, companyReapplyOverride,
|
|
|
1081
1086
|
if (entry.roundId) await cloudState.appendRecord('rounds', { type: 'submission-confirmed', roundId: entry.roundId, applicationId: entry.id, occurredAt: entry.submittedAt }, { recordKey: entry.roundId, idempotencyKey: `round-confirmation:${entry.roundId}:${entry.id}`, occurredAt: entry.submittedAt, queueOnFailure: true });
|
|
1082
1087
|
}
|
|
1083
1088
|
}
|
|
1084
|
-
return { recorded: storedEntry.id, review: buildReview([...entries, storedEntry]), ...(cloudIntent ? { cloudIntent: cloudIntent.intentId } : {}) };
|
|
1089
|
+
return { recorded: storedEntry.id, review: buildReview([...entries, storedEntry], outcomes, [], new Date(), await jsonLines(join(dir, 'delivery.ndjson'))), ...(cloudIntent ? { cloudIntent: cloudIntent.intentId } : {}) };
|
|
1085
1090
|
});
|
|
1086
1091
|
}
|
|
1087
1092
|
|
|
@@ -1107,6 +1112,63 @@ function isoDate(value, label) {
|
|
|
1107
1112
|
return result;
|
|
1108
1113
|
}
|
|
1109
1114
|
|
|
1115
|
+
async function appendAccounting(stream, event, { cloudConfirmed = false } = {}) {
|
|
1116
|
+
const file = join(await ensureStateDir(), `${stream}.ndjson`);
|
|
1117
|
+
const previous = await jsonLines(file);
|
|
1118
|
+
const matches = previous.filter(e => e.id === event.id);
|
|
1119
|
+
if (matches.some(e => stableJson(e) !== stableJson(event))) throw new Error('Conflicting accounting event ID.');
|
|
1120
|
+
if (matches.length) return { recorded: false, duplicate: true, event };
|
|
1121
|
+
await appendFile(file, `${JSON.stringify(event)}\n`, { mode: 0o600 });
|
|
1122
|
+
await chmod(file, 0o600);
|
|
1123
|
+
if (!cloudConfirmed && await cloudState.configured()) await cloudState.appendRecord(stream, event, { recordKey: event.id, idempotencyKey: `accounting:${event.id}`, occurredAt: event.occurredAt ?? event.observedAt, queueOnFailure: true });
|
|
1124
|
+
return { recorded: true, duplicate: false, event };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
async function recordDelivery(input, retry = false) {
|
|
1128
|
+
const { cloudIntentId, cloudLeaseId, ...raw } = object(input, 'delivery');
|
|
1129
|
+
const event = validateDelivery({ ...raw, ...(retry ? { type: 'retry-confirmed' } : {}) });
|
|
1130
|
+
if (!retry && event.type === 'retry-confirmed') throw new Error('Use ledger retry for replacement transmissions.');
|
|
1131
|
+
return withStateLock('applications', () => withStateLock('delivery', async dir => {
|
|
1132
|
+
const applications = await jsonLines(join(dir, 'applications.ndjson'));
|
|
1133
|
+
const events = await jsonLines(join(dir, 'delivery.ndjson'));
|
|
1134
|
+
const cloudRetry = retry && await cloudState.configured();
|
|
1135
|
+
if (cloudRetry && !events.some(e => e.id === event.id) && (!cloudIntentId || !cloudLeaseId || event.attemptId !== cloudIntentId)) throw new Error('Cloud retry requires its intent and live lease; attemptId must equal cloudIntentId.');
|
|
1136
|
+
// Cloud transmission eligibility was checked at intent preparation. The
|
|
1137
|
+
// Worker verifies that intent before we persist the observed transmission.
|
|
1138
|
+
validateDeliveryReferences(event, applications, events, { preparedRetry: cloudRetry });
|
|
1139
|
+
let cloudConfirmed = false;
|
|
1140
|
+
if (cloudRetry && !events.some(e => e.id === event.id)) {
|
|
1141
|
+
await cloudState.confirmRetry(cloudIntentId, event, cloudLeaseId);
|
|
1142
|
+
cloudConfirmed = true;
|
|
1143
|
+
}
|
|
1144
|
+
const result = await appendAccounting('delivery', event, { cloudConfirmed });
|
|
1145
|
+
return { ...result, delivery: deliveryProjection(applications, [...events, event]) };
|
|
1146
|
+
}));
|
|
1147
|
+
}
|
|
1148
|
+
async function deliveryHistory(applicationId) {
|
|
1149
|
+
const dir = await ensureStateDir();
|
|
1150
|
+
const apps = (await jsonLines(join(dir, 'applications.ndjson'))).filter(a => !applicationId || a.id === applicationId);
|
|
1151
|
+
const events = (await jsonLines(join(dir, 'delivery.ndjson'))).filter(e => !applicationId || e.applicationId === applicationId);
|
|
1152
|
+
return { ...deliveryProjection(apps, events), events };
|
|
1153
|
+
}
|
|
1154
|
+
async function recordLead(input) {
|
|
1155
|
+
const event = validateLead(input);
|
|
1156
|
+
knownDiscoverySourceId(event.sourceId, 'lead.sourceId');
|
|
1157
|
+
return withStateLock('rounds', async dir => {
|
|
1158
|
+
const round = await roundStatus(event.roundId);
|
|
1159
|
+
const events = await jsonLines(join(dir, 'discovery.ndjson'));
|
|
1160
|
+
if (round.completed && !event.supersedes && !events.some(e => e.id === event.id)) throw new Error('Completed rounds accept only corrections to existing leads.');
|
|
1161
|
+
validateLeadReferences(event, events);
|
|
1162
|
+
return appendAccounting('discovery', event);
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
async function leadHistory(roundId) {
|
|
1166
|
+
const dir = await ensureStateDir();
|
|
1167
|
+
const id = roundId ?? (await roundStatus()).roundId;
|
|
1168
|
+
const history = (await jsonLines(join(dir, 'discovery.ndjson'))).filter(e => e.roundId === id);
|
|
1169
|
+
return { ...discoveryProjection(history, { roundId: id }), history };
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1110
1172
|
async function roundStart(input) {
|
|
1111
1173
|
const value = object(input, 'round');
|
|
1112
1174
|
const allowed = new Set(['requestedCount', 'startedAt']);
|
|
@@ -1114,6 +1176,7 @@ async function roundStart(input) {
|
|
|
1114
1176
|
const event = {
|
|
1115
1177
|
type: 'started',
|
|
1116
1178
|
roundId: `round-${new Date().toISOString().slice(0, 10)}-${randomUUID()}`,
|
|
1179
|
+
discoveryPolicyVersion: 2,
|
|
1117
1180
|
requestedCount: integer(value.requestedCount, 'round.requestedCount', 1, 1000),
|
|
1118
1181
|
occurredAt: isoDate(value.startedAt, 'round.startedAt'),
|
|
1119
1182
|
};
|
|
@@ -1127,7 +1190,7 @@ function discoveryGroup(id) {
|
|
|
1127
1190
|
return id;
|
|
1128
1191
|
}
|
|
1129
1192
|
|
|
1130
|
-
function discoverySummary(events, applications, completion) {
|
|
1193
|
+
function discoverySummary(events, applications, completion, audit = null) {
|
|
1131
1194
|
const latest = new Map();
|
|
1132
1195
|
const searchedIds = new Set();
|
|
1133
1196
|
const attribution = new Map();
|
|
@@ -1136,7 +1199,10 @@ function discoverySummary(events, applications, completion) {
|
|
|
1136
1199
|
if (event.status === 'searched') searchedIds.add(event.sourceId);
|
|
1137
1200
|
for (const id of event.applicationIds ?? []) attribution.set(id, event.sourceId);
|
|
1138
1201
|
}
|
|
1139
|
-
const sources = [...latest.values()]
|
|
1202
|
+
const sources = [...latest.values()].map(report => {
|
|
1203
|
+
const leads = audit?.leads.filter(lead => lead.sourceId === report.sourceId) ?? [];
|
|
1204
|
+
return audit ? { ...report, reviewedCount: leads.length, qualifiedCount: leads.filter(lead => !lead.conflict && lead.disposition === 'qualified').length, accounting: 'per-lead' } : { ...report, accounting: 'legacy-unverified' };
|
|
1205
|
+
});
|
|
1140
1206
|
const eligible = sources.filter((item) => !['recruiter-inbound', 'user-supplied-leads'].includes(item.sourceId));
|
|
1141
1207
|
const attempted = new Set(eligible.map((item) => discoveryGroup(item.sourceId)));
|
|
1142
1208
|
const searched = new Set(eligible.filter((item) => searchedIds.has(item.sourceId)).map((item) => discoveryGroup(item.sourceId)));
|
|
@@ -1172,17 +1238,23 @@ async function roundSource(input) {
|
|
|
1172
1238
|
const sourceId = knownDiscoverySourceId(value.sourceId, 'coverage.sourceId');
|
|
1173
1239
|
const status = string(value.status, 'coverage.status', 20);
|
|
1174
1240
|
if (!['searched', 'blocked'].includes(status)) throw new Error('coverage.status must be searched or blocked.');
|
|
1175
|
-
const
|
|
1176
|
-
const qualifiedCount = integer(value.qualifiedCount, 'coverage.qualifiedCount', 0, reviewedCount);
|
|
1241
|
+
const roundId = string(value.roundId, 'coverage.roundId', 180);
|
|
1177
1242
|
const blocker = value.blocker == null ? null : string(value.blocker, 'coverage.blocker', 40);
|
|
1178
|
-
if (status === 'blocked' && (!SOURCE_BLOCKERS.has(blocker) || reviewedCount !== 0 || qualifiedCount !== 0)) throw new Error('Blocked sources require a documented blocker and zero counts.');
|
|
1179
1243
|
if (status === 'searched' && blocker != null) throw new Error('Searched sources cannot have a blocker.');
|
|
1180
1244
|
const evidence = string(value.evidence, 'coverage.evidence', 2000);
|
|
1181
1245
|
const applicationIds = value.applicationIds == null ? [] : [...new Set(stringArray(value.applicationIds, 'coverage.applicationIds'))];
|
|
1182
1246
|
if (applicationIds.length > 1000 || (status === 'blocked' && applicationIds.length)) throw new Error('Invalid coverage.applicationIds.');
|
|
1183
1247
|
const event = await withStateLock('rounds', async (dir) => {
|
|
1184
|
-
const round = await roundStatus(
|
|
1248
|
+
const round = await roundStatus(roundId);
|
|
1185
1249
|
if (round.completed) throw new Error('Cannot record coverage for a completed round.');
|
|
1250
|
+
const audit = discoveryProjection(await jsonLines(join(dir, 'discovery.ndjson')), { roundId: value.roundId });
|
|
1251
|
+
const leads = audit.leads.filter(lead => lead.sourceId === sourceId);
|
|
1252
|
+
const derivedReviewed = status === 'blocked' ? 0 : leads.length;
|
|
1253
|
+
const derivedQualified = status === 'blocked' ? 0 : leads.filter(lead => !lead.conflict && lead.disposition === 'qualified').length;
|
|
1254
|
+
const reviewedCount = round.discoveryPolicyVersion === 2 ? derivedReviewed : integer(value.reviewedCount, 'coverage.reviewedCount', 0, 10000);
|
|
1255
|
+
const qualifiedCount = round.discoveryPolicyVersion === 2 ? derivedQualified : integer(value.qualifiedCount, 'coverage.qualifiedCount', 0, reviewedCount);
|
|
1256
|
+
if (round.discoveryPolicyVersion === 2 && ((value.reviewedCount != null && value.reviewedCount !== reviewedCount) || (value.qualifiedCount != null && value.qualifiedCount !== qualifiedCount))) throw new Error('Source count assertions do not match recorded leads.');
|
|
1257
|
+
if (status === 'blocked' && (!SOURCE_BLOCKERS.has(blocker) || reviewedCount !== 0 || qualifiedCount !== 0)) throw new Error('Blocked sources require a documented blocker and zero counts.');
|
|
1186
1258
|
const applications = await jsonLines(join(dir, 'applications.ndjson'));
|
|
1187
1259
|
for (const id of applicationIds) {
|
|
1188
1260
|
const entry = applications.find((item) => item.id === id && item.roundId === round.roundId && item.status === 'submitted');
|
|
@@ -1217,6 +1289,16 @@ function replayAttention(events, roundId = null) {
|
|
|
1217
1289
|
async function attentionList(roundId = null) {
|
|
1218
1290
|
const events = await jsonLines(join(await ensureStateDir(), 'attention.ndjson'));
|
|
1219
1291
|
const items = replayAttention(events, roundId);
|
|
1292
|
+
const dir = await ensureStateDir();
|
|
1293
|
+
const apps = await jsonLines(join(dir, 'applications.ndjson'));
|
|
1294
|
+
const delivery = deliveryProjection(apps, await jsonLines(join(dir, 'delivery.ndjson')));
|
|
1295
|
+
for (const item of delivery.applications.filter(a => a.failed || a.conflict)) {
|
|
1296
|
+
const app = apps.find(a => a.id === item.applicationId);
|
|
1297
|
+
if (roundId && app.roundId !== roundId) continue;
|
|
1298
|
+
items.push({ id: `delivery:${app.id}`, applicationId: app.id, roundId: app.roundId ?? null, url: app.url, stage: 'confirmation', blocker: item.conflict ? 'delivery-conflict' : 'delivery-failed', requiredActions: ['review-delivery'], derived: true });
|
|
1299
|
+
}
|
|
1300
|
+
const discovery = discoveryProjection(await jsonLines(join(dir, 'discovery.ndjson')), { roundId });
|
|
1301
|
+
for (const lead of discovery.leads.filter(l => l.conflict)) items.push({ id: `discovery:${lead.key}`, roundId: lead.roundId, url: lead.url, stage: 'discovery', blocker: 'assessment-conflict', requiredActions: ['review-assessment'], derived: true });
|
|
1220
1302
|
return { count: items.length, items };
|
|
1221
1303
|
}
|
|
1222
1304
|
|
|
@@ -1251,6 +1333,7 @@ async function attentionResolve(input) {
|
|
|
1251
1333
|
for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`Unknown attention resolution property: ${key}.`);
|
|
1252
1334
|
const id = string(value.id, 'attention.id', 180);
|
|
1253
1335
|
const current = await attentionList();
|
|
1336
|
+
if (current.items.some(item => item.id === id && item.derived)) throw new Error('Resolve accounting attention with a delivery correction, verified recovery, or lead revision.');
|
|
1254
1337
|
if (!current.items.some((item) => item.id === id)) throw new Error('Attention item is not active.');
|
|
1255
1338
|
const event = { type: 'resolved', id, resolvedAt: isoDate(value.resolvedAt, 'attention.resolvedAt') };
|
|
1256
1339
|
await appendPrivateEvent('attention', event);
|
|
@@ -1267,7 +1350,11 @@ async function roundStatus(roundId = null) {
|
|
|
1267
1350
|
if (!started) throw new Error('Application round was not found.');
|
|
1268
1351
|
const matching = (await jsonLines(join(dir, 'applications.ndjson'))).filter((entry) => entry.roundId === id && entry.status === 'submitted');
|
|
1269
1352
|
const applications = [...new Map(matching.map((entry, index) => [canonicalApplicationKey(entry, String(index)), entry])).values()];
|
|
1270
|
-
const
|
|
1353
|
+
const delivery = deliveryProjection(matching, await jsonLines(join(dir, 'delivery.ndjson')));
|
|
1354
|
+
const effectiveKeys = new Set(matching.filter((entry,i) => delivery.applications[i].counted).map(canonicalApplicationKey));
|
|
1355
|
+
const effectiveApplications = applications.filter(entry => effectiveKeys.has(canonicalApplicationKey(entry)));
|
|
1356
|
+
const confirmedCount = delivery.effectiveSubmissionCount;
|
|
1357
|
+
const audit = started.discoveryPolicyVersion === 2 ? discoveryProjection(await jsonLines(join(dir, 'discovery.ndjson')), { roundId: id }) : null;
|
|
1271
1358
|
const attention = await attentionList(id);
|
|
1272
1359
|
const completion = events.find((event) => event.type === 'completed' && event.roundId === id);
|
|
1273
1360
|
return {
|
|
@@ -1277,7 +1364,16 @@ async function roundStatus(roundId = null) {
|
|
|
1277
1364
|
remainingCount: Math.max(0, started.requestedCount - confirmedCount),
|
|
1278
1365
|
blockedCount: attention.count,
|
|
1279
1366
|
completed: Boolean(completion),
|
|
1280
|
-
|
|
1367
|
+
discoveryPolicyVersion: started.discoveryPolicyVersion ?? 1,
|
|
1368
|
+
recordedSubmissionCount: delivery.recordedSubmissionCount,
|
|
1369
|
+
effectiveSubmissionCount: confirmedCount,
|
|
1370
|
+
failedDeliveryCount: delivery.failedDeliveryCount,
|
|
1371
|
+
receiptUnknownEmailCount: delivery.receiptUnknownEmailCount,
|
|
1372
|
+
shortfallCount: Math.max(0, started.requestedCount - confirmedCount),
|
|
1373
|
+
needsRecovery: Boolean(completion) && confirmedCount < started.requestedCount,
|
|
1374
|
+
discovery: { ...discoverySummary(events.filter((event) => event.roundId === id), effectiveApplications, completion, audit),
|
|
1375
|
+
...(audit ? { reviewedCount: audit.reviewedCount, qualifiedCount: audit.qualifiedCount, uniqueLeadCount: audit.uniqueLeadCount, dispositionCounts: audit.dispositionCounts, conflicts: audit.conflicts,
|
|
1376
|
+
missingLeadApplicationIds: effectiveApplications.filter(app => !audit.leads.some(lead => !lead.conflict && lead.disposition === 'qualified' && lead.applicationId === app.id && lead.sourceId === (app.discoverySourceId ?? events.filter(e => e.type === 'source-checked' && e.applicationIds?.includes(app.id)).at(-1)?.sourceId) && (normalizedText(lead.company) === normalizedText(app.company) && (lead.employerJobId && app.employerJobId ? lead.employerJobId.toLowerCase() === app.employerJobId.toLowerCase() : canonicalUrl(lead.url) === canonicalUrl(app.url))))).map(app => app.id) } : { accounting: 'legacy-unverified' }) },
|
|
1281
1377
|
startedAt: started.occurredAt,
|
|
1282
1378
|
...(completion ? { completedAt: completion.occurredAt } : {}),
|
|
1283
1379
|
};
|
|
@@ -1294,6 +1390,7 @@ async function roundComplete(input) {
|
|
|
1294
1390
|
if (status.confirmedCount < status.requestedCount) throw new Error(`Round requires ${status.requestedCount} confirmed submissions before completion.`);
|
|
1295
1391
|
if (!status.discovery.coverageSatisfied) throw new Error('Round requires attempts across at least 3 distinct discovery sources, including at least one searched source. Record coverage and blockers with round source --stdin.');
|
|
1296
1392
|
if (status.discovery.unattributedCount) throw new Error('Round requires discovery source attribution for every confirmed submission. Supply missing attribution using round source applicationIds; do not rewrite the ledger.');
|
|
1393
|
+
if (status.discovery.conflicts?.length || status.discovery.missingLeadApplicationIds?.length) throw new Error('Round requires qualified lead records for every submission and resolution of conflicting assessments.');
|
|
1297
1394
|
let explanation = {};
|
|
1298
1395
|
if (status.discovery.concentrationNeedsExplanation || value.concentrationReason != null || value.concentrationEvidence != null) {
|
|
1299
1396
|
if (!CONCENTRATION_REASONS.has(value.concentrationReason)) throw new Error('Source concentration requires a documented concentrationReason and private concentrationEvidence.');
|
|
@@ -1399,7 +1496,7 @@ async function ledgerReview() {
|
|
|
1399
1496
|
const applications = await jsonLines(join(dir, 'applications.ndjson'));
|
|
1400
1497
|
const outcomes = await jsonLines(join(dir, 'outcomes.ndjson'));
|
|
1401
1498
|
const acknowledgements = await jsonLines(join(dir, 'reviews.ndjson'));
|
|
1402
|
-
return buildReview(applications, outcomes, acknowledgements);
|
|
1499
|
+
return buildReview(applications, outcomes, acknowledgements, new Date(), await jsonLines(join(dir, 'delivery.ndjson')));
|
|
1403
1500
|
}
|
|
1404
1501
|
|
|
1405
1502
|
async function ledgerReviewAcknowledge(input) {
|
|
@@ -1407,7 +1504,7 @@ async function ledgerReviewAcknowledge(input) {
|
|
|
1407
1504
|
const reviewedAt = string(value.reviewedAt ?? new Date().toISOString(), 'reviewedAt', 80);
|
|
1408
1505
|
if (Number.isNaN(Date.parse(reviewedAt))) throw new Error('reviewedAt must be an ISO date.');
|
|
1409
1506
|
const review = await ledgerReview();
|
|
1410
|
-
const event = { reviewedAt, uniqueSubmissionCount: review.
|
|
1507
|
+
const event = { reviewedAt, uniqueSubmissionCount: review.recordedSubmissionCount, maturedApplicationCount: review.recordedMaturedApplicationCount };
|
|
1411
1508
|
await withStateLock('reviews', async (dir) => {
|
|
1412
1509
|
const file = join(dir, 'reviews.ndjson');
|
|
1413
1510
|
await appendFile(file, `${JSON.stringify(event)}\n`, { mode: 0o600 });
|
|
@@ -1547,6 +1644,9 @@ async function executeCommand([area, action, value], telemetry, session, communi
|
|
|
1547
1644
|
const event = await telemetryJobAssessed(job, result);
|
|
1548
1645
|
if (event) domainEvents.push(event);
|
|
1549
1646
|
} else if (area === 'ledger' && action === 'check' && value === '--stdin') result = await ledgerCheck(await jsonStdin());
|
|
1647
|
+
else if (area === 'ledger' && action === 'delivery' && value === '--stdin') result = await recordDelivery(await jsonStdin());
|
|
1648
|
+
else if (area === 'ledger' && action === 'retry' && value === '--stdin') result = await recordDelivery(await jsonStdin(), true);
|
|
1649
|
+
else if (area === 'ledger' && action === 'deliveries') result = await deliveryHistory(value);
|
|
1550
1650
|
else if (area === 'ledger' && action === 'add' && value === '--stdin') {
|
|
1551
1651
|
const input = await jsonStdin();
|
|
1552
1652
|
const telemetryDetails = validateSubmissionTelemetry(input.telemetry);
|
|
@@ -1569,6 +1669,8 @@ async function executeCommand([area, action, value], telemetry, session, communi
|
|
|
1569
1669
|
else if (area === 'autonomy' && action === 'preview' && value == null) result = await autonomyStatus();
|
|
1570
1670
|
else if (area === 'autonomy' && action === 'revoke' && value == null) result = await autonomyRevoke();
|
|
1571
1671
|
else if (area === 'round' && action === 'start' && value === '--stdin') result = await roundStart(await jsonStdin());
|
|
1672
|
+
else if (area === 'round' && action === 'lead' && value === '--stdin') result = await recordLead(await jsonStdin());
|
|
1673
|
+
else if (area === 'round' && action === 'leads') result = await leadHistory(value);
|
|
1572
1674
|
else if (area === 'round' && action === 'source' && value === '--stdin') {
|
|
1573
1675
|
result = await roundSource(await jsonStdin());
|
|
1574
1676
|
domainEvents.push({ event: 'source_checked', properties: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SKILL_VERSION = '3.
|
|
1
|
+
export const SKILL_VERSION = '3.5.0';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
9
|
+
async function fixture(t) {
|
|
10
|
+
const dir = await mkdtemp(join(tmpdir(), 'accounting-cli-'));
|
|
11
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
12
|
+
await writeFile(join(dir, 'telemetry.json'), JSON.stringify({ enabled: false, disclosed: true }));
|
|
13
|
+
const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: dir, JOB_APPLICATION_AGENT_CLOUD_CONFIG: join(dir, 'absent.json'), JOB_APPLICATION_AGENT_SOURCE_COMMUNITY_URL: 'http://127.0.0.1:9' };
|
|
14
|
+
const run = (args, input) => JSON.parse(execFileSync(process.execPath, [script, ...args], { env, encoding: 'utf8', input: JSON.stringify(input) }));
|
|
15
|
+
const fail = (args, input) => spawnSync(process.execPath, [script, ...args], { env, encoding: 'utf8', input: JSON.stringify(input) });
|
|
16
|
+
return { dir, run, fail };
|
|
17
|
+
}
|
|
18
|
+
const application = (roundId) => ({ id: 'app', company: 'Example', role: 'Senior Engineer', url: 'https://example.test/jobs/1', source: 'email', discoverySourceId: 'indeed', score: 90, status: 'submitted', submittedAt: '2026-01-01T00:00:00Z', approval: 'STANDING AUTHORIZATION', roundId });
|
|
19
|
+
const failure = { id: 'bounce-1', applicationId: 'app', attemptId: 'initial:app', type: 'delivery-failed', evidenceType: 'final-delivery-failure', evidence: 'Matched final failure for the original recruiting message.', occurredAt: '2026-01-02T00:00:00Z' };
|
|
20
|
+
|
|
21
|
+
test('late delivery failure corrects completed round and review without rewriting applications', async (t) => {
|
|
22
|
+
const {dir, run} = await fixture(t);
|
|
23
|
+
const roundId = 'legacy-round';
|
|
24
|
+
await writeFile(join(dir, 'rounds.ndjson'), [ { type:'started', roundId, requestedCount:1, occurredAt:'2026-01-01T00:00:00Z'}, {type:'completed',roundId,occurredAt:'2026-01-01T01:00:00Z'} ].map(JSON.stringify).join('\n')+'\n');
|
|
25
|
+
const raw = JSON.stringify(application(roundId))+'\n';
|
|
26
|
+
await writeFile(join(dir, 'applications.ndjson'), raw);
|
|
27
|
+
run(['ledger','delivery','--stdin'], failure);
|
|
28
|
+
run(['ledger','delivery','--stdin'], failure);
|
|
29
|
+
const status = run(['round','status',roundId]);
|
|
30
|
+
assert.equal(status.completed,true);
|
|
31
|
+
assert.equal(status.confirmedCount,0);
|
|
32
|
+
assert.equal(status.shortfallCount,1);
|
|
33
|
+
assert.equal(status.needsRecovery,true);
|
|
34
|
+
assert.equal(run(['ledger','review']).submittedTotal,0);
|
|
35
|
+
assert.equal(await readFile(join(dir,'applications.ndjson'),'utf8'),raw);
|
|
36
|
+
assert.equal((await readFile(join(dir,'delivery.ndjson'),'utf8')).trim().split('\n').length,1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('new rounds derive source totals and require qualified lead linkage', async (t) => {
|
|
40
|
+
const {run,fail} = await fixture(t);
|
|
41
|
+
const {roundId} = run(['round','start','--stdin'],{requestedCount:1});
|
|
42
|
+
assert.notEqual(fail(['round','source','--stdin'],{roundId,sourceId:'indeed',status:'searched',reviewedCount:20,qualifiedCount:1,evidence:'Unsupported summary'}).status,0);
|
|
43
|
+
const lead = { id:'lead-1',roundId,sourceId:'indeed',company:'Example',role:'Senior Engineer',url:'https://example.test/jobs/1',disposition:'qualified',evidence:'Meets the evidenced target requirements.',applicationId:'app',observedAt:'2026-01-01T00:00:00Z' };
|
|
44
|
+
run(['round','lead','--stdin'],lead);
|
|
45
|
+
run(['round','lead','--stdin'],lead);
|
|
46
|
+
for(const sourceId of ['indeed','linkedin-jobs-feed','hacker-news-who-is-hiring']) run(['round','source','--stdin'],{roundId,sourceId,status:'searched',evidence:'Actual synthetic search'});
|
|
47
|
+
run(['ledger','add','--stdin'],application(roundId));
|
|
48
|
+
const status=run(['round','status',roundId]);
|
|
49
|
+
assert.equal(status.discovery.sources.find(s=>s.sourceId==='indeed').reviewedCount,1);
|
|
50
|
+
assert.equal(run(['round','leads',roundId]).leads.length,1);
|
|
51
|
+
assert.equal(run(['round','complete','--stdin'],{roundId,concentrationReason:'stronger-fit',concentrationEvidence:'Only this source had a qualified role.'}).completed,true);
|
|
52
|
+
run(['round','lead','--stdin'],{...lead,id:'revision',supersedes:lead.id,disposition:'closed-stale'});
|
|
53
|
+
assert.equal(run(['round','status',roundId]).completed,true);
|
|
54
|
+
assert.equal(run(['round','leads',roundId]).qualifiedCount,0);
|
|
55
|
+
assert.notEqual(fail(['round','lead','--stdin'],{...lead,id:'new-lead',url:'https://different.example/job'}).status,0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
for (const scenario of [
|
|
59
|
+
{ name: 'different requisition IDs at the same URL', application: { employerJobId: 'req-1' }, lead: { employerJobId: 'req-2' }, matched: false },
|
|
60
|
+
{ name: 'different companies sharing a requisition ID and URL', application: { employerJobId: 'req-1' }, lead: { employerJobId: 'req-1', company: 'Another Company' }, matched: false },
|
|
61
|
+
{ name: 'different companies sharing a URL without requisition IDs', application: {}, lead: { company: 'Another Company' }, matched: false },
|
|
62
|
+
{ name: 'matching company and requisition ID across URL aliases', application: { employerJobId: 'req-1' }, lead: { employerJobId: 'req-1', url: 'https://ats.example.test/alias/1' }, matched: true },
|
|
63
|
+
{ name: 'matching company and URL when only one requisition ID is known', application: { employerJobId: 'req-1' }, lead: {}, matched: true },
|
|
64
|
+
]) {
|
|
65
|
+
test(`qualified lead attribution checks ${scenario.name}`, async (t) => {
|
|
66
|
+
const { dir, run, fail } = await fixture(t);
|
|
67
|
+
const { roundId } = run(['round', 'start', '--stdin'], { requestedCount: 1 });
|
|
68
|
+
const app = { ...application(roundId), ...scenario.application };
|
|
69
|
+
await writeFile(join(dir, 'applications.ndjson'), JSON.stringify(app) + '\n');
|
|
70
|
+
run(['round', 'lead', '--stdin'], {
|
|
71
|
+
id: 'qualified-lead', roundId, sourceId: 'indeed', company: 'Example', role: app.role,
|
|
72
|
+
url: app.url, disposition: 'qualified', applicationId: app.id,
|
|
73
|
+
observedAt: '2026-01-01T00:00:00Z', evidence: 'Synthetic verified requisition assessment.',
|
|
74
|
+
...scenario.lead,
|
|
75
|
+
});
|
|
76
|
+
for (const sourceId of ['indeed', 'linkedin-jobs-feed', 'hacker-news-who-is-hiring']) {
|
|
77
|
+
run(['round', 'source', '--stdin'], { roundId, sourceId, status: 'searched', evidence: 'Synthetic search performed.' });
|
|
78
|
+
}
|
|
79
|
+
const status = run(['round', 'status', roundId]);
|
|
80
|
+
assert.deepEqual(status.discovery.missingLeadApplicationIds, scenario.matched ? [] : [app.id]);
|
|
81
|
+
const completion = fail(['round', 'complete', '--stdin'], {
|
|
82
|
+
roundId, concentrationReason: 'stronger-fit', concentrationEvidence: 'Only the selected source had a qualifying opening.',
|
|
83
|
+
});
|
|
84
|
+
assert.equal(completion.status === 0, scenario.matched, completion.stderr);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { CloudStateClient, readCachedCloudProfile, saveCloudConfig } from '../scripts/cloud-state-client.mjs';
|
|
8
|
+
import { deliveryProjection, discoveryProjection, validateDelivery, validateLead } from '../scripts/application-accounting.mjs';
|
|
9
|
+
import worker, { sha256Hex } from '../../state-worker/src/worker.mjs';
|
|
10
|
+
import { createBackup, restoreBackup } from '../../state-worker/src/backup.mjs';
|
|
11
|
+
import { createMemoryD1, hasNodeSqlite } from '../../state-worker/tests/d1-mock.mjs';
|
|
12
|
+
import { createMemoryR2 } from '../../state-worker/tests/r2-mock.mjs';
|
|
13
|
+
|
|
14
|
+
const TOKEN = 'accounting-cloud-client-synthetic-token-long-enough';
|
|
15
|
+
const sqliteTest = hasNodeSqlite ? test : test.skip;
|
|
16
|
+
const now = '2026-09-13T10:00:00.000Z';
|
|
17
|
+
const application = { id: 'app', company: 'Example', role: 'Engineer', source: 'email', url: 'https://example.com/jobs/app', status: 'submitted', submittedAt: '2026-09-12T10:00:00.000Z' };
|
|
18
|
+
const failure = validateDelivery({ version: 1, id: 'failure-event', applicationId: 'app', attemptId: 'initial:app', type: 'delivery-failed', occurredAt: now, evidenceType: 'final-delivery-failure', evidence: 'Synthetic final failure matched to the original application email.' });
|
|
19
|
+
const retry = validateDelivery({ version: 1, id: 'retry-event', applicationId: 'app', attemptId: 'recovery-attempt', type: 'retry-confirmed', channel: 'browser', url: 'https://example.com/jobs/app', channelVerifiedAt: now, approval: 'STANDING AUTHORIZATION', evidenceType: 'browser-confirmation', evidence: 'Synthetic visible ATS success.', occurredAt: '2026-09-13T11:00:00.000Z' });
|
|
20
|
+
const qualifiedLead = validateLead({ version: 1, id: 'lead-event', type: 'lead-reviewed', roundId: 'round-1', sourceId: 'direct', url: application.url, company: application.company, role: application.role, disposition: 'qualified', observedAt: now, evidence: 'Synthetic active employer posting matches requirements.' });
|
|
21
|
+
|
|
22
|
+
async function setup(t) {
|
|
23
|
+
const root = await mkdtemp(join(tmpdir(), 'job-agent-accounting-cloud-'));
|
|
24
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
25
|
+
const stateDir = join(root, 'state');
|
|
26
|
+
const configPath = join(root, 'cloud', 'config.json');
|
|
27
|
+
await mkdir(stateDir, { recursive: true });
|
|
28
|
+
const schema = await readFile(new URL('../../state-worker/migrations/0001_private_state.sql', import.meta.url), 'utf8');
|
|
29
|
+
const DB = createMemoryD1(schema);
|
|
30
|
+
await DB.prepare('INSERT INTO clients (client_id, name, token_hash, created_at) VALUES (?, ?, ?, ?)')
|
|
31
|
+
.bind('client-test', 'Synthetic Accounting Client', sha256Hex(TOKEN), now).run();
|
|
32
|
+
const bindings = { DB, STATE: createMemoryR2(), STATE_TOKEN: 'legacy', LEGACY_WRITES_DISABLED: '1' };
|
|
33
|
+
const fetchImpl = (input, init) => worker.fetch(new Request(input, init), bindings);
|
|
34
|
+
await saveCloudConfig({ version: 2, url: 'https://state.example.com', token: TOKEN, clientId: 'client-test' }, { configPath });
|
|
35
|
+
const client = new CloudStateClient({ stateDir, configPath, fetchImpl });
|
|
36
|
+
return { root, stateDir, configPath, schema, bindings, fetchImpl, client };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function writeRows(stateDir, stream, rows) {
|
|
40
|
+
await writeFile(join(stateDir, `${stream}.ndjson`), `${rows.map(row => JSON.stringify(row)).join('\n')}\n`, { mode: 0o600 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readRows(stateDir, stream) {
|
|
44
|
+
return (await readFile(join(stateDir, `${stream}.ndjson`), 'utf8')).split('\n').filter(Boolean).map(JSON.parse);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
sqliteTest('local and cloud accounting projections agree after repeated bidirectional reconciliation', async t => {
|
|
48
|
+
const ctx = await setup(t);
|
|
49
|
+
await writeRows(ctx.stateDir, 'applications', [application]);
|
|
50
|
+
await writeRows(ctx.stateDir, 'delivery', [failure]);
|
|
51
|
+
await writeRows(ctx.stateDir, 'discovery', [qualifiedLead]);
|
|
52
|
+
const originalProjection = deliveryProjection([application], [failure]);
|
|
53
|
+
assert.equal(originalProjection.effectiveSubmissionCount, 0);
|
|
54
|
+
await ctx.client.reconcile({ dryRun: false });
|
|
55
|
+
const correction = validateDelivery({ version: 1, id: 'correction-event', applicationId: 'app', attemptId: 'initial:app', type: 'correction', supersedes: failure.id, status: 'receipt-confirmed', occurredAt: '2026-09-14T10:00:00.000Z', evidenceType: 'employer-acknowledgement', evidence: 'Synthetic employer acknowledgement corrects the matched failure.' });
|
|
56
|
+
await ctx.client.appendRecord('delivery', correction);
|
|
57
|
+
const downloaded = await ctx.client.reconcile({ dryRun: false });
|
|
58
|
+
assert.equal(downloaded.streams.delivery.cloudOnly, 1);
|
|
59
|
+
const repeated = await ctx.client.reconcile({ dryRun: false });
|
|
60
|
+
assert.equal(repeated.imported, 0);
|
|
61
|
+
assert.equal(repeated.downloaded, 0);
|
|
62
|
+
const localApplications = await readRows(ctx.stateDir, 'applications');
|
|
63
|
+
const localDelivery = await readRows(ctx.stateDir, 'delivery');
|
|
64
|
+
const cloudApplications = (await ctx.client.listStream('applications')).map(row => row.value);
|
|
65
|
+
const cloudDelivery = (await ctx.client.listStream('delivery')).map(row => row.value);
|
|
66
|
+
assert.equal(cloudDelivery.length, 2);
|
|
67
|
+
assert.deepEqual(deliveryProjection(localApplications, localDelivery), deliveryProjection(cloudApplications, cloudDelivery));
|
|
68
|
+
assert.equal(deliveryProjection(localApplications, localDelivery).effectiveSubmissionCount, 1);
|
|
69
|
+
const localDiscovery = await readRows(ctx.stateDir, 'discovery');
|
|
70
|
+
const cloudDiscovery = (await ctx.client.listStream('discovery')).map(row => row.value);
|
|
71
|
+
assert.deepEqual(discoveryProjection(localDiscovery, { roundId: 'round-1' }), discoveryProjection(cloudDiscovery, { roundId: 'round-1' }));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
sqliteTest('default reconciliation migrates failed-email recovery history and exports one application with two delivery records', async t => {
|
|
75
|
+
const ctx = await setup(t);
|
|
76
|
+
await writeRows(ctx.stateDir, 'applications', [application]);
|
|
77
|
+
await writeRows(ctx.stateDir, 'delivery', [failure, retry]);
|
|
78
|
+
const result = await ctx.client.reconcile({ dryRun: false });
|
|
79
|
+
assert.equal(result.imported, 3);
|
|
80
|
+
const again = await ctx.client.reconcile({ dryRun: false });
|
|
81
|
+
assert.equal(again.imported, 0);
|
|
82
|
+
const exported = await ctx.client.exportTo(join(ctx.root, 'private-export.json'));
|
|
83
|
+
const archive = JSON.parse(await readFile(exported.path, 'utf8'));
|
|
84
|
+
assert.equal(archive.streams.applications.length, 1);
|
|
85
|
+
assert.equal(archive.streams.delivery.length, 2);
|
|
86
|
+
assert.ok(archive.streams.delivery.every(row => row.provenance === 'local-reconcile'));
|
|
87
|
+
assert.deepEqual(archive.streams.delivery.map(row => row.value), [failure, retry]);
|
|
88
|
+
assert.equal(deliveryProjection(archive.streams.applications.map(row => row.value), archive.streams.delivery.map(row => row.value)).effectiveSubmissionCount, 1);
|
|
89
|
+
if (process.platform !== 'win32') assert.equal((await stat(exported.path)).mode & 0o777, 0o600);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
sqliteTest('private backup restores delivery attempts and projection without duplicating a second restore', async t => {
|
|
93
|
+
const ctx = await setup(t);
|
|
94
|
+
await writeRows(ctx.stateDir, 'applications', [application]);
|
|
95
|
+
await writeRows(ctx.stateDir, 'delivery', [failure, retry]);
|
|
96
|
+
await ctx.client.reconcile({ dryRun: false });
|
|
97
|
+
const archive = await createBackup(ctx.bindings.DB, now);
|
|
98
|
+
const restored = createMemoryD1(ctx.schema);
|
|
99
|
+
assert.equal((await restoreBackup(restored, archive)).records, 3);
|
|
100
|
+
assert.equal((await restoreBackup(restored, archive)).records, 0);
|
|
101
|
+
const values = async stream => (await restored.prepare('SELECT payload_json FROM records WHERE stream = ? ORDER BY sequence').bind(stream).all()).results.map(row => JSON.parse(row.payload_json));
|
|
102
|
+
const restoredApplications = await values('applications');
|
|
103
|
+
const restoredDelivery = await values('delivery');
|
|
104
|
+
assert.deepEqual(restoredDelivery, [failure, retry]);
|
|
105
|
+
assert.deepEqual(deliveryProjection(restoredApplications, restoredDelivery), deliveryProjection([application], [failure, retry]));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
sqliteTest('an older backend is rejected before any local-only accounting data uploads', async t => {
|
|
109
|
+
const ctx = await setup(t);
|
|
110
|
+
await writeRows(ctx.stateDir, 'applications', [application]);
|
|
111
|
+
await writeRows(ctx.stateDir, 'delivery', [failure]);
|
|
112
|
+
const mutations = [];
|
|
113
|
+
const older = new CloudStateClient({ stateDir: ctx.stateDir, configPath: ctx.configPath, fetchImpl: async (input, init) => {
|
|
114
|
+
if (new URL(input).pathname === '/v2/status') return Response.json({ apiVersion: 2, backend: 'cloudflare-d1-r2', capabilities: [] });
|
|
115
|
+
if (init.method && init.method !== 'GET') mutations.push({ input, method: init.method });
|
|
116
|
+
return ctx.fetchImpl(input, init);
|
|
117
|
+
} });
|
|
118
|
+
await assert.rejects(() => older.reconcile({ dryRun: false }), /backend upgrade required.*application-accounting-v1/i);
|
|
119
|
+
assert.equal(mutations.length, 0);
|
|
120
|
+
assert.equal((await ctx.bindings.DB.prepare('SELECT COUNT(*) AS count FROM records').first()).count, 0);
|
|
121
|
+
assert.deepEqual(await readRows(ctx.stateDir, 'delivery'), [failure]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
sqliteTest('cached accounting permits offline research and durable observation queues while blocking transmission intents', async t => {
|
|
125
|
+
const ctx = await setup(t);
|
|
126
|
+
await ctx.client.putDocument('profile', { name: 'Synthetic Candidate', roleFamilies: ['engineering'] }, 0);
|
|
127
|
+
await ctx.client.refreshProfileCache();
|
|
128
|
+
await writeRows(ctx.stateDir, 'applications', [application]);
|
|
129
|
+
await ctx.client.reconcile({ dryRun: false });
|
|
130
|
+
const offline = new CloudStateClient({ stateDir: ctx.stateDir, configPath: ctx.configPath, fetchImpl: async () => { throw new Error('synthetic network outage'); } });
|
|
131
|
+
await offline.requireAccounting();
|
|
132
|
+
assert.equal((await readCachedCloudProfile(ctx.stateDir)).name, 'Synthetic Candidate');
|
|
133
|
+
assert.deepEqual(await readRows(ctx.stateDir, 'applications'), [application]);
|
|
134
|
+
const queued = await offline.appendRecord('delivery', failure, { queueOnFailure: true });
|
|
135
|
+
assert.equal(queued.queued, true);
|
|
136
|
+
const restarted = new CloudStateClient({ stateDir: ctx.stateDir, configPath: ctx.configPath, fetchImpl: offline.fetchImpl });
|
|
137
|
+
const pending = await restarted.pendingWrites();
|
|
138
|
+
assert.equal(pending.length, 1);
|
|
139
|
+
assert.deepEqual(pending[0].payload.value, failure);
|
|
140
|
+
assert.equal(pending[0].stream, 'delivery');
|
|
141
|
+
if (process.platform !== 'win32') assert.equal((await stat(join(ctx.stateDir, 'cloud-pending.ndjson'))).mode & 0o777, 0o600);
|
|
142
|
+
await assert.rejects(() => restarted.createIntent({ applicationId: 'another-app', canonicalUrl: 'https://example.com/jobs/another', leaseId: 'expired-lease' }), /cloud state unavailable/i);
|
|
143
|
+
assert.equal((await ctx.bindings.DB.prepare('SELECT COUNT(*) AS count FROM application_intents').first()).count, 0);
|
|
144
|
+
await ctx.client.reconcile({dryRun:false});
|
|
145
|
+
assert.equal((await ctx.client.pendingWrites()).length,0);
|
|
146
|
+
assert.equal((await readRows(ctx.stateDir,'delivery')).length,1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
sqliteTest('a delivery replay followed by repeated synchronization preserves one logical evidence event', async t => {
|
|
150
|
+
const ctx = await setup(t);
|
|
151
|
+
await writeRows(ctx.stateDir, 'applications', [application]);
|
|
152
|
+
await writeRows(ctx.stateDir, 'delivery', [failure]);
|
|
153
|
+
await ctx.client.reconcile({ dryRun: false });
|
|
154
|
+
await ctx.client.appendRecord('delivery', failure, { idempotencyKey: 'different-network-replay-id' });
|
|
155
|
+
const sync = await ctx.client.reconcile({ dryRun: false });
|
|
156
|
+
const repeated = await ctx.client.reconcile({ dryRun: false });
|
|
157
|
+
assert.equal(sync.imported + sync.downloaded + repeated.imported + repeated.downloaded, 0);
|
|
158
|
+
assert.equal((await ctx.client.listStream('delivery')).length, 1);
|
|
159
|
+
assert.equal((await readRows(ctx.stateDir, 'delivery')).length, 1);
|
|
160
|
+
assert.equal(deliveryProjection([application], await readRows(ctx.stateDir, 'delivery')).failedDeliveryCount, 1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
sqliteTest('cross-client JSON field ordering does not duplicate the same delivery event during synchronization', async t => {
|
|
164
|
+
const ctx = await setup(t);
|
|
165
|
+
await writeRows(ctx.stateDir, 'applications', [application]);
|
|
166
|
+
await ctx.client.reconcile({ dryRun: false });
|
|
167
|
+
await writeRows(ctx.stateDir, 'delivery', [failure]);
|
|
168
|
+
const reorderedFailure = Object.fromEntries(Object.entries(failure).reverse());
|
|
169
|
+
await ctx.client.appendRecord('delivery', reorderedFailure);
|
|
170
|
+
await ctx.client.reconcile({ dryRun: false });
|
|
171
|
+
await ctx.client.reconcile({ dryRun: false });
|
|
172
|
+
assert.equal((await ctx.client.listStream('delivery')).length, 1);
|
|
173
|
+
assert.equal((await readRows(ctx.stateDir, 'delivery')).length, 1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
sqliteTest('unfamiliar versioned discovery records survive synchronization unchanged', async t => {
|
|
177
|
+
const ctx = await setup(t);
|
|
178
|
+
const legacy = {version:7,type:'legacy-discovery-report',id:'legacy',roundId:'old',results:{reviewed:12}};
|
|
179
|
+
await writeRows(ctx.stateDir,'discovery',[legacy]);
|
|
180
|
+
await ctx.client.reconcile({dryRun:false});
|
|
181
|
+
assert.deepEqual((await ctx.client.listStream('discovery'))[0].value,legacy);
|
|
182
|
+
assert.deepEqual(discoveryProjection(await readRows(ctx.stateDir,'discovery')).leads,[]);
|
|
183
|
+
});
|