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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import test from 'node:test';
|
|
9
|
+
|
|
10
|
+
import { validateDelivery } from '../scripts/application-accounting.mjs';
|
|
11
|
+
|
|
12
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
13
|
+
|
|
14
|
+
test('CLI records a transmitted cloud retry when late receipt evidence invalidates its original preparation eligibility', async t => {
|
|
15
|
+
const stateDir = await mkdtemp(join(tmpdir(), 'accounting-retry-cli-'));
|
|
16
|
+
t.after(() => rm(stateDir, { recursive: true, force: true }));
|
|
17
|
+
const token = 'synthetic-accounting-retry-cli-token-long-enough';
|
|
18
|
+
const intentId = 'prepared-recovery-intent';
|
|
19
|
+
const leaseId = 'synthetic-live-lease';
|
|
20
|
+
const occurredAt = '2026-09-14T10:00:00.000Z';
|
|
21
|
+
const application = { id: 'app', company: 'Example', role: 'Engineer', url: 'https://jobs.example.com/app', source: 'email', status: 'submitted', submittedAt: '2026-09-12T10:00:00.000Z' };
|
|
22
|
+
const failure = validateDelivery({ version: 1, id: 'failure-event', applicationId: 'app', attemptId: 'initial:app', type: 'delivery-failed', occurredAt: '2026-09-13T10:00:00.000Z', evidenceType: 'final-delivery-failure', evidence: 'Synthetic final failure matched to original email.' });
|
|
23
|
+
const receipt = validateDelivery({ version: 1, id: 'late-receipt', applicationId: 'app', attemptId: 'initial:app', type: 'receipt-confirmed', occurredAt, evidenceType: 'employer-acknowledgement', evidence: 'Synthetic employer acknowledgement arrived after recovery was transmitted.' });
|
|
24
|
+
const retry = validateDelivery({ version: 1, id: 'retry-event', applicationId: 'app', attemptId: intentId, type: 'retry-confirmed', channel: 'browser', url: application.url, channelVerifiedAt: occurredAt, approval: 'STANDING AUTHORIZATION', evidenceType: 'browser-confirmation', evidence: 'Synthetic visible ATS success for prepared recovery.', occurredAt });
|
|
25
|
+
const streams = { applications: [application], delivery: [failure, receipt] };
|
|
26
|
+
const confirmations = [];
|
|
27
|
+
const unexpected = [];
|
|
28
|
+
const server = createServer(async (request, response) => {
|
|
29
|
+
const url = new URL(request.url, 'http://127.0.0.1');
|
|
30
|
+
response.setHeader('content-type', 'application/json');
|
|
31
|
+
if (request.headers.authorization !== `Bearer ${token}`) {
|
|
32
|
+
response.writeHead(401).end(JSON.stringify({ error: 'Synthetic authentication required' }));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (request.method === 'GET' && url.pathname === '/v2/status') {
|
|
36
|
+
response.end(JSON.stringify({ apiVersion: 2, backend: 'synthetic-accounting-backend', capabilities: ['application-accounting-v1'], documents: [], streams: [], files: [] }));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (request.method === 'GET' && url.pathname.startsWith('/v2/streams/')) {
|
|
40
|
+
const stream = url.pathname.split('/').at(-1);
|
|
41
|
+
const records = (streams[stream] ?? []).map((value, index) => ({ sequence: index + 1, recordKey: value.id, idempotencyKey: `accounting:${value.id}`, value }));
|
|
42
|
+
const after = Number(url.searchParams.get('after') ?? 0);
|
|
43
|
+
const page = records.filter(record => record.sequence > after);
|
|
44
|
+
response.end(JSON.stringify({ stream, records: page, nextCursor: page.at(-1)?.sequence ?? after }));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (request.method === 'GET' && url.pathname.startsWith('/v2/documents/')) {
|
|
48
|
+
response.writeHead(404).end(JSON.stringify({ error: 'Synthetic document not present' }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (request.method === 'POST' && url.pathname === `/v2/intents/${intentId}/confirm`) {
|
|
52
|
+
let raw = '';
|
|
53
|
+
for await (const chunk of request) raw += chunk;
|
|
54
|
+
const body = JSON.parse(raw);
|
|
55
|
+
confirmations.push(body);
|
|
56
|
+
if (body.leaseId !== leaseId || body.delivery?.attemptId !== intentId) {
|
|
57
|
+
response.writeHead(409).end(JSON.stringify({ error: 'Wrong prepared intent or lease' }));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
streams.delivery.push(body.delivery);
|
|
61
|
+
response.end(JSON.stringify({ confirmed: true, intentId }));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
unexpected.push(`${request.method} ${url.pathname}`);
|
|
65
|
+
response.writeHead(500).end(JSON.stringify({ error: 'Unexpected synthetic backend request' }));
|
|
66
|
+
});
|
|
67
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
68
|
+
t.after(() => new Promise(resolve => server.close(resolve)));
|
|
69
|
+
const backendUrl = `http://127.0.0.1:${server.address().port}`;
|
|
70
|
+
const preload = `const realFetch = globalThis.fetch; globalThis.fetch = (input, init) => { const url = new URL(typeof input === 'string' ? input : input.url); if (url.origin !== 'https://state.example.com') throw new Error('Unexpected non-fixture network request'); return realFetch(process.env.ACCOUNTING_TEST_BACKEND_URL + url.pathname + url.search, init); };`;
|
|
71
|
+
const configPath = join(stateDir, 'cloud-config.json');
|
|
72
|
+
await writeFile(configPath, JSON.stringify({ version: 2, url: 'https://state.example.com', token, clientId: 'synthetic-client' }), { mode: 0o600 });
|
|
73
|
+
await writeFile(join(stateDir, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
74
|
+
for (const [stream, rows] of Object.entries(streams)) {
|
|
75
|
+
await writeFile(join(stateDir, `${stream}.ndjson`), `${rows.map(row => JSON.stringify(row)).join('\n')}\n`, { mode: 0o600 });
|
|
76
|
+
}
|
|
77
|
+
const env = { ...process.env, ACCOUNTING_TEST_BACKEND_URL: backendUrl, JOB_APPLICATION_AGENT_STATE_DIR: stateDir, JOB_APPLICATION_AGENT_CLOUD_CONFIG: configPath, JOB_APPLICATION_AGENT_SOURCE_COMMUNITY_URL: 'http://127.0.0.1:9' };
|
|
78
|
+
const result = await new Promise((resolve, reject) => {
|
|
79
|
+
const child = spawn(process.execPath, ['--import', `data:text/javascript,${encodeURIComponent(preload)}`, script, 'ledger', 'retry', '--stdin'], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
80
|
+
let stdout = '';
|
|
81
|
+
let stderr = '';
|
|
82
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
83
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
84
|
+
child.on('error', reject);
|
|
85
|
+
child.on('close', code => resolve({ code, stdout, stderr }));
|
|
86
|
+
child.stdin.end(JSON.stringify({ ...retry, cloudIntentId: intentId, cloudLeaseId: leaseId }));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
assert.deepEqual(unexpected, []);
|
|
90
|
+
assert.equal(result.code, 0, result.stderr);
|
|
91
|
+
assert.equal(confirmations.length, 1);
|
|
92
|
+
assert.deepEqual(confirmations[0], { delivery: retry, leaseId });
|
|
93
|
+
assert.equal(JSON.parse(result.stdout).recorded, true);
|
|
94
|
+
const localEvents = (await readFile(join(stateDir, 'delivery.ndjson'), 'utf8')).trim().split('\n').map(JSON.parse);
|
|
95
|
+
assert.equal(localEvents.filter(event => event.type === 'retry-confirmed' && event.attemptId === intentId).length, 1);
|
|
96
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
|
|
9
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
10
|
+
// Constant preload: pause immediately before the source command acquires its
|
|
11
|
+
// rounds lock, allowing a real lead command to commit first without timing sleeps.
|
|
12
|
+
const lockBarrierPreload = `
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
14
|
+
import { basename } from 'node:path';
|
|
15
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
16
|
+
const originalOpen = fs.open;
|
|
17
|
+
let intercepted = false;
|
|
18
|
+
fs.open = async function(path, ...args) {
|
|
19
|
+
if (!intercepted && basename(String(path)) === '.rounds.lock') {
|
|
20
|
+
intercepted = true;
|
|
21
|
+
await new Promise((resolve, reject) => {
|
|
22
|
+
process.once('message', message => {
|
|
23
|
+
if (message !== 'release-lock-attempt') return reject(new Error('Unexpected barrier message'));
|
|
24
|
+
process.disconnect();
|
|
25
|
+
resolve();
|
|
26
|
+
});
|
|
27
|
+
process.send('before-rounds-lock');
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return originalOpen.call(fs, path, ...args);
|
|
31
|
+
};
|
|
32
|
+
syncBuiltinESMExports();
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
function launch(env, args, input, preloadPath) {
|
|
36
|
+
const runtime = preloadPath ? ['--import', pathToFileURL(preloadPath).href] : [];
|
|
37
|
+
const child = spawn(process.execPath, [...runtime, script, ...args], {
|
|
38
|
+
env, stdio: preloadPath ? ['pipe', 'pipe', 'pipe', 'ipc'] : ['pipe', 'pipe', 'pipe'],
|
|
39
|
+
});
|
|
40
|
+
const completed = new Promise((resolve, reject) => {
|
|
41
|
+
let stdout = '';
|
|
42
|
+
let stderr = '';
|
|
43
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
44
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
45
|
+
child.once('error', reject);
|
|
46
|
+
child.once('close', code => resolve({ code, stdout, stderr }));
|
|
47
|
+
});
|
|
48
|
+
child.stdin.end(JSON.stringify(input));
|
|
49
|
+
return { child, completed };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const explicitStaleCounts of [false, true]) {
|
|
53
|
+
test(explicitStaleCounts
|
|
54
|
+
? 'source coverage rejects stale count assertions after a lead commits before lock acquisition'
|
|
55
|
+
: 'source coverage derives counts from the lead committed before lock acquisition', { timeout: 15000 }, async t => {
|
|
56
|
+
const dir = await mkdtemp(join(tmpdir(), 'accounting-source-race-'));
|
|
57
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
58
|
+
const preloadPath = join(dir, 'rounds-lock-barrier.mjs');
|
|
59
|
+
await writeFile(preloadPath, lockBarrierPreload);
|
|
60
|
+
await writeFile(join(dir, 'telemetry.json'), JSON.stringify({ enabled: false, disclosed: true }));
|
|
61
|
+
const env = {
|
|
62
|
+
...process.env,
|
|
63
|
+
JOB_APPLICATION_AGENT_STATE_DIR: dir,
|
|
64
|
+
JOB_APPLICATION_AGENT_CLOUD_CONFIG: join(dir, 'absent-cloud-config.json'),
|
|
65
|
+
JOB_APPLICATION_AGENT_SOURCE_COMMUNITY_URL: 'http://127.0.0.1:9',
|
|
66
|
+
};
|
|
67
|
+
const started = await launch(env, ['round', 'start', '--stdin'], { requestedCount: 1 }).completed;
|
|
68
|
+
assert.equal(started.code, 0, started.stderr);
|
|
69
|
+
const { roundId } = JSON.parse(started.stdout);
|
|
70
|
+
const sourceId = 'indeed';
|
|
71
|
+
const source = launch(env, ['round', 'source', '--stdin'], {
|
|
72
|
+
roundId, sourceId, status: 'searched', evidence: 'Synthetic source search.',
|
|
73
|
+
...(explicitStaleCounts ? { reviewedCount: 0, qualifiedCount: 0 } : {}),
|
|
74
|
+
}, preloadPath);
|
|
75
|
+
t.after(() => { if (source.child.exitCode === null) source.child.kill(); });
|
|
76
|
+
await Promise.race([
|
|
77
|
+
new Promise((resolve, reject) => {
|
|
78
|
+
source.child.once('message', message => {
|
|
79
|
+
if (message === 'before-rounds-lock') resolve();
|
|
80
|
+
else reject(new Error('Source sent an unexpected barrier message.'));
|
|
81
|
+
});
|
|
82
|
+
}),
|
|
83
|
+
source.completed.then(result => { throw new Error(`Source exited before the lock barrier: ${result.stderr}`); }),
|
|
84
|
+
]);
|
|
85
|
+
const lead = await launch(env, ['round', 'lead', '--stdin'], {
|
|
86
|
+
id: 'qualified-lead', roundId, sourceId, company: 'Synthetic Example', role: 'Engineer',
|
|
87
|
+
url: 'https://example.test/jobs/123', disposition: 'qualified',
|
|
88
|
+
observedAt: '2026-09-14T10:00:00.000Z', evidence: 'Synthetic qualifying employer requisition.',
|
|
89
|
+
}).completed;
|
|
90
|
+
assert.equal(lead.code, 0, lead.stderr);
|
|
91
|
+
source.child.send('release-lock-attempt');
|
|
92
|
+
const result = await source.completed;
|
|
93
|
+
const stored = (await readFile(join(dir, 'rounds.ndjson'), 'utf8')).split('\n').filter(Boolean).map(JSON.parse);
|
|
94
|
+
const coverage = stored.filter(event => event.type === 'source-checked');
|
|
95
|
+
if (explicitStaleCounts) {
|
|
96
|
+
assert.notEqual(result.code, 0, 'Stale zero-count assertions must not be accepted after one qualified lead commits.');
|
|
97
|
+
assert.match(result.stderr, /count assertions do not match recorded leads/i);
|
|
98
|
+
assert.equal(coverage.length, 0);
|
|
99
|
+
} else {
|
|
100
|
+
assert.equal(result.code, 0, result.stderr);
|
|
101
|
+
const event = JSON.parse(result.stdout);
|
|
102
|
+
assert.equal(event.reviewedCount, 1);
|
|
103
|
+
assert.equal(event.qualifiedCount, 1);
|
|
104
|
+
assert.equal(coverage.length, 1);
|
|
105
|
+
assert.equal(coverage[0].reviewedCount, 1);
|
|
106
|
+
assert.equal(coverage[0].qualifiedCount, 1);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { deliveryProjection, discoveryProjection } from '../scripts/application-accounting.mjs';
|
|
5
|
+
|
|
6
|
+
const occurredAt = '2026-09-13T10:00:00Z';
|
|
7
|
+
|
|
8
|
+
function application(overrides = {}) {
|
|
9
|
+
return { id: 'application-1', company: 'Example', role: 'Engineer', source: 'email', url: 'https://example.com/jobs/123', status: 'submitted', submittedAt: '2026-09-12T10:00:00Z', ...overrides };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function delivery(overrides = {}) {
|
|
13
|
+
return { version: 1, id: 'bounce-1', applicationId: 'application-1', attemptId: 'initial:application-1', type: 'delivery-failed', occurredAt, evidenceType: 'final-delivery-failure', evidence: 'Final recipient failure matched to the sent application message.', ...overrides };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function lead(overrides = {}) {
|
|
17
|
+
return { version: 1, type: 'lead-reviewed', id: 'lead-event-1', roundId: 'round-1', sourceId: 'direct', url: 'https://example.com/jobs/123', company: 'Example', role: 'Engineer', disposition: 'qualified', observedAt: occurredAt, evidence: 'Active employer posting assessed against candidate requirements.', ...overrides };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('email sends count once while their receipt remains unknown', () => {
|
|
21
|
+
const result = deliveryProjection([application()], []);
|
|
22
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
23
|
+
assert.equal(result.receiptUnknownEmailCount, 1);
|
|
24
|
+
assert.equal(result.failedDeliveryCount, 0);
|
|
25
|
+
assert.equal(result.applications[0].receiptUnknown, true);
|
|
26
|
+
assert.equal(result.applications[0].attempts.length, 1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('confirmed browser applications need no email acknowledgement', () => {
|
|
30
|
+
const result = deliveryProjection([application({ source: 'ashby' })], []);
|
|
31
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
32
|
+
assert.equal(result.receiptUnknownEmailCount, 0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('a matched final email failure removes the application from effective totals', () => {
|
|
36
|
+
const result = deliveryProjection([application()], [delivery()]);
|
|
37
|
+
assert.equal(result.effectiveSubmissionCount, 0);
|
|
38
|
+
assert.equal(result.failedDeliveryCount, 1);
|
|
39
|
+
assert.equal(result.receiptUnknownEmailCount, 0);
|
|
40
|
+
assert.equal(result.applications[0].counted, false);
|
|
41
|
+
assert.equal(result.applications[0].failed, true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('notification failure cannot invalidate independent browser confirmation', () => {
|
|
45
|
+
const result = deliveryProjection([application({ source: 'ashby' })], [delivery()]);
|
|
46
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
47
|
+
assert.equal(result.failedDeliveryCount, 0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('temporary delivery delays and ambiguous failures do not silently change totals', () => {
|
|
51
|
+
for (const evidenceType of ['delivery-delay', 'ambiguous']) {
|
|
52
|
+
const result = deliveryProjection([application()], [delivery({ evidenceType })]);
|
|
53
|
+
assert.equal(result.effectiveSubmissionCount, 1, evidenceType);
|
|
54
|
+
assert.equal(result.failedDeliveryCount, 0, evidenceType);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('unrelated bounce and hiring rejection do not invalidate the submitted application', () => {
|
|
59
|
+
const result = deliveryProjection([application({ outcome: 'rejected' })], [delivery({ applicationId: 'another-application', attemptId: 'initial:another-application' })]);
|
|
60
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
61
|
+
assert.equal(result.failedDeliveryCount, 0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('receipt confirmation resolves unknown email receipt', () => {
|
|
65
|
+
const result = deliveryProjection([application()], [delivery({ id: 'receipt-1', type: 'receipt-confirmed', evidenceType: 'employer-acknowledgement' })]);
|
|
66
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
67
|
+
assert.equal(result.receiptUnknownEmailCount, 0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('contradictory receipt and failure remain counted and flag conflict in either arrival order', () => {
|
|
71
|
+
const failure = delivery();
|
|
72
|
+
const receipt = delivery({ id: 'receipt-1', type: 'receipt-confirmed', evidenceType: 'employer-acknowledgement' });
|
|
73
|
+
for (const events of [[failure, receipt], [receipt, failure]]) {
|
|
74
|
+
const result = deliveryProjection([application()], events);
|
|
75
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
76
|
+
assert.equal(result.applications[0].conflict, true);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('explicit correction supersedes a failure without depending on arrival order', () => {
|
|
81
|
+
const failure = delivery();
|
|
82
|
+
const correction = delivery({ id: 'correction-1', type: 'correction', supersedes: failure.id, status: 'receipt-confirmed', evidenceType: 'employer-acknowledgement' });
|
|
83
|
+
for (const events of [[failure, correction], [correction, failure]]) {
|
|
84
|
+
const result = deliveryProjection([application()], events);
|
|
85
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
86
|
+
assert.equal(result.failedDeliveryCount, 0);
|
|
87
|
+
assert.equal(result.applications[0].conflict, false);
|
|
88
|
+
assert.equal(result.receiptUnknownEmailCount, 0);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('correcting a mistaken failure to unknown restores the original email send', () => {
|
|
93
|
+
const result = deliveryProjection([application()], [delivery(), delivery({ id: 'correction-1', type: 'correction', supersedes: 'bounce-1', status: 'unknown', evidenceType: 'ambiguous' })]);
|
|
94
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
95
|
+
assert.equal(result.receiptUnknownEmailCount, 1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a failed email followed by confirmed ATS recovery remains one application with two attempts', () => {
|
|
99
|
+
const retry = delivery({ id: 'retry-1', attemptId: 'replacement-1', type: 'retry-confirmed', channel: 'browser', url: 'https://jobs.ashbyhq.com/example/123', evidenceType: 'browser-confirmation', occurredAt: '2026-09-13T11:00:00Z' });
|
|
100
|
+
const result = deliveryProjection([application()], [delivery(), retry, retry]);
|
|
101
|
+
assert.equal(result.applications.length, 1);
|
|
102
|
+
assert.equal(result.applications[0].attempts.length, 2);
|
|
103
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
104
|
+
assert.equal(result.failedDeliveryCount, 0);
|
|
105
|
+
assert.equal(result.receiptUnknownEmailCount, 0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('replayed delivery event IDs cannot duplicate failures', () => {
|
|
109
|
+
const failure = delivery();
|
|
110
|
+
const result = deliveryProjection([application()], [failure, { ...failure }, failure]);
|
|
111
|
+
assert.equal(result.failedDeliveryCount, 1);
|
|
112
|
+
assert.equal(result.applications[0].attempts.length, 1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('legacy unfamiliar discovery rows remain outside the new projection', () => {
|
|
116
|
+
const result = discoveryProjection([{ id: 'legacy', reviewedCount: 19, qualifiedCount: 2 }, { type: 'unknown-future-record', roundId: 'round-1' }], { roundId: 'round-1' });
|
|
117
|
+
assert.equal(result.reviewedCount, 0);
|
|
118
|
+
assert.equal(result.qualifiedCount, 0);
|
|
119
|
+
assert.equal(result.leads.length, 0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('replayed lead event IDs and other rounds do not inflate reviewed counts', () => {
|
|
123
|
+
const first = lead();
|
|
124
|
+
const result = discoveryProjection([first, { ...first }, lead({ id: 'other-round', roundId: 'round-2' })], { roundId: 'round-1' });
|
|
125
|
+
assert.equal(result.reviewedCount, 1);
|
|
126
|
+
assert.equal(result.qualifiedCount, 1);
|
|
127
|
+
assert.equal(result.uniqueLeadCount, 1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('explicit assessment revision replaces its prior disposition without inflating totals', () => {
|
|
131
|
+
const original = lead();
|
|
132
|
+
const revision = lead({ id: 'lead-event-2', supersedes: original.id, disposition: 'closed-stale', observedAt: '2026-09-14T10:00:00Z' });
|
|
133
|
+
for (const events of [[original, revision], [revision, original]]) {
|
|
134
|
+
const result = discoveryProjection(events, { roundId: 'round-1' });
|
|
135
|
+
assert.equal(result.reviewedCount, 1);
|
|
136
|
+
assert.equal(result.qualifiedCount, 0);
|
|
137
|
+
assert.equal(result.conflicts.length, 0);
|
|
138
|
+
assert.equal(result.leads[0].disposition, 'closed-stale');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('competing assessment revisions surface a conflict regardless of arrival order', () => {
|
|
143
|
+
const first = lead();
|
|
144
|
+
const conflicting = lead({ id: 'lead-event-2', disposition: 'blocked' });
|
|
145
|
+
for (const events of [[first, conflicting], [conflicting, first]]) {
|
|
146
|
+
const result = discoveryProjection(events, { roundId: 'round-1' });
|
|
147
|
+
assert.equal(result.reviewedCount, 1);
|
|
148
|
+
assert.equal(result.conflicts.length, 1);
|
|
149
|
+
assert.equal(result.qualifiedCount, 0);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('cross-source sightings preserve source reviews while deduplicating the requisition', () => {
|
|
154
|
+
const result = discoveryProjection([lead({ employerJobId: 'example:123' }), lead({ id: 'linkedin-sighting', sourceId: 'linkedin', url: 'https://linkedin.com/jobs/view/987', employerJobId: 'example:123' })], { roundId: 'round-1' });
|
|
155
|
+
assert.equal(result.reviewedCount, 2);
|
|
156
|
+
assert.equal(result.qualifiedCount, 2);
|
|
157
|
+
assert.equal(result.uniqueLeadCount, 1);
|
|
158
|
+
assert.equal(result.leads.length, 2);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('distinct employer requisitions remain separate even for the same company and role', () => {
|
|
162
|
+
const result = discoveryProjection([lead({ employerJobId: 'example:123' }), lead({ id: 'second-role', url: 'https://example.com/jobs/456', employerJobId: 'example:456' })], { roundId: 'round-1' });
|
|
163
|
+
assert.equal(result.reviewedCount, 2);
|
|
164
|
+
assert.equal(result.uniqueLeadCount, 2);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('competing corrections cannot silently choose failure over an unknown disposition', () => {
|
|
168
|
+
const original = delivery();
|
|
169
|
+
const unknown = delivery({id:'clear-bounce',type:'correction',supersedes:original.id,status:'unknown'});
|
|
170
|
+
const failed = delivery({id:'keep-bounce',type:'correction',supersedes:original.id,status:'delivery-failed'});
|
|
171
|
+
const result = deliveryProjection([application()], [original, unknown, failed]);
|
|
172
|
+
assert.equal(result.applications[0].conflict, true);
|
|
173
|
+
assert.equal(result.effectiveSubmissionCount, 1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('verified requisition aliases with the same assessment count as one lead without conflict', () => {
|
|
177
|
+
const result = discoveryProjection([lead({employerJobId:'REQ-1'}),lead({id:'alias',url:'https://ats.example.com/REQ-1',employerJobId:'REQ-1'})], {roundId:'round-1'});
|
|
178
|
+
assert.equal(result.reviewedCount,1);
|
|
179
|
+
assert.equal(result.qualifiedCount,1);
|
|
180
|
+
assert.equal(result.conflicts.length,0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('a later verified requisition ID enriches a URL-only lead without inflating counts', () => {
|
|
184
|
+
const first = lead();
|
|
185
|
+
const enriched = lead({ id:'enriched',employerJobId:'REQ-1',supersedes:first.id });
|
|
186
|
+
const result = discoveryProjection([first,enriched],{roundId:'round-1'});
|
|
187
|
+
assert.equal(result.reviewedCount,1);
|
|
188
|
+
assert.equal(result.uniqueLeadCount,1);
|
|
189
|
+
assert.equal(result.leads[0].employerJobId,'REQ-1');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('conflicting retry copies project identically in either arrival order', () => {
|
|
193
|
+
const email = delivery({ id: 'retry-email', type: 'retry-confirmed', attemptId: 'replacement', channel: 'email', evidenceType: 'sent-email' });
|
|
194
|
+
const browser = { ...email, id: 'retry-browser', channel: 'browser', evidenceType: 'browser-confirmation' };
|
|
195
|
+
const forward = deliveryProjection([application()], [delivery(), email, browser]);
|
|
196
|
+
assert.deepEqual(forward, deliveryProjection([application()], [browser, email, delivery()]));
|
|
197
|
+
assert.equal(forward.applications[0].conflict, true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('company formatting does not split verified requisitions or create assessment conflicts', () => {
|
|
201
|
+
const original = lead({ company: 'Example, Inc.', employerJobId: 'REQ-1' });
|
|
202
|
+
const alias = lead({ id: 'alias', company: 'Example Inc', employerJobId: 'req-1', url: 'https://ats.example/jobs/1' });
|
|
203
|
+
const sameSource = discoveryProjection([original, alias]);
|
|
204
|
+
assert.equal(sameSource.reviewedCount, 1);
|
|
205
|
+
assert.equal(sameSource.qualifiedCount, 1);
|
|
206
|
+
const crossSource = discoveryProjection([original, { ...alias, sourceId: 'linkedin' }]);
|
|
207
|
+
assert.equal(crossSource.reviewedCount, 2);
|
|
208
|
+
assert.equal(crossSource.uniqueLeadCount, 1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('URL-only sightings discard tracking while retaining distinct requisition parameters', () => {
|
|
212
|
+
const original = lead({ url: 'https://example.com/careers?jobId=123&trackingId=one' });
|
|
213
|
+
const repeat = lead({ id: 'repeat', url: 'https://example.com/careers?trackingId=two&jobId=123' });
|
|
214
|
+
assert.equal(discoveryProjection([original, repeat]).reviewedCount, 1);
|
|
215
|
+
assert.equal(discoveryProjection([original, { ...repeat, sourceId: 'linkedin' }]).uniqueLeadCount, 1);
|
|
216
|
+
assert.equal(discoveryProjection([original, { ...repeat, url: 'https://example.com/careers?jobId=456' }]).uniqueLeadCount, 2);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('a revision can enrich but cannot remove a verified requisition identity', async () => {
|
|
220
|
+
const { validateLeadReferences } = await import('../scripts/application-accounting.mjs');
|
|
221
|
+
const original = lead({ employerJobId: 'REQ-1' });
|
|
222
|
+
const weakened = lead({ id: 'weakened', supersedes: original.id });
|
|
223
|
+
assert.throws(() => validateLeadReferences(weakened, [original]), /same round, source and requisition/);
|
|
224
|
+
const urlOnly = lead();
|
|
225
|
+
const enriched = lead({ id: 'enriched', employerJobId: 'REQ-1', supersedes: urlOnly.id });
|
|
226
|
+
assert.doesNotThrow(() => validateLeadReferences(enriched, [urlOnly]));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('prepared retry confirmation preserves evidence without reauthorizing its transmission', async () => {
|
|
230
|
+
const { validateDeliveryReferences } = await import('../scripts/application-accounting.mjs');
|
|
231
|
+
const receipt = delivery({ id: 'receipt', type: 'receipt-confirmed', evidenceType: 'employer-acknowledgement' });
|
|
232
|
+
const retry = delivery({ id: 'retry', type: 'retry-confirmed', attemptId: 'replacement', channel: 'browser', evidenceType: 'browser-confirmation' });
|
|
233
|
+
const evidence = [delivery(), receipt];
|
|
234
|
+
assert.throws(() => validateDeliveryReferences(retry, [application()], evidence), /verified failure/);
|
|
235
|
+
assert.doesNotThrow(() => validateDeliveryReferences(retry, [application()], evidence, { preparedRetry: true }));
|
|
236
|
+
assert.throws(() => validateDeliveryReferences({ ...retry, id: 'another' }, [application()], [...evidence, retry], { preparedRetry: true }), /attemptId already exists/);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
for (const parameter of ['id', 'career_job_req_id']) {
|
|
240
|
+
test(`URL-only discovery preserves distinct ${parameter} requisition values`, () => {
|
|
241
|
+
const first = lead({ url: `https://example.com/careers?${parameter}=123&trackingId=one` });
|
|
242
|
+
const repeat = lead({ id: 'repeat', url: `https://example.com/careers?trackingId=two&${parameter}=123` });
|
|
243
|
+
const different = lead({ id: 'different', url: `https://example.com/careers?${parameter}=456&trackingId=three` });
|
|
244
|
+
const result = discoveryProjection([first, repeat, different]);
|
|
245
|
+
assert.equal(result.reviewedCount, 2);
|
|
246
|
+
assert.equal(result.qualifiedCount, 2);
|
|
247
|
+
assert.equal(result.uniqueLeadCount, 2);
|
|
248
|
+
assert.equal(result.conflicts.length, 0);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
test('a distinct requisition at a shared URL cannot undo explicit identity enrichment', async () => {
|
|
253
|
+
const { validateLeadReferences } = await import('../scripts/application-accounting.mjs');
|
|
254
|
+
const original = lead({ id: 'url-observation', url: 'https://example.com/careers' });
|
|
255
|
+
const enriched = lead({ ...original, id: 'verified-requisition', employerJobId: 'REQ-1', supersedes: original.id });
|
|
256
|
+
const another = lead({ ...original, id: 'other-requisition', employerJobId: 'REQ-2' });
|
|
257
|
+
assert.doesNotThrow(() => validateLeadReferences(enriched, [original]));
|
|
258
|
+
assert.doesNotThrow(() => validateLeadReferences(another, [original, enriched]));
|
|
259
|
+
for (const events of [[original, enriched, another], [another, enriched, original]]) {
|
|
260
|
+
const result = discoveryProjection(events);
|
|
261
|
+
assert.equal(result.reviewedCount, 2);
|
|
262
|
+
assert.equal(result.qualifiedCount, 2);
|
|
263
|
+
assert.equal(result.uniqueLeadCount, 2);
|
|
264
|
+
assert.equal(result.conflicts.length, 0);
|
|
265
|
+
assert.deepEqual(result.leads.map(item => item.employerJobId).sort(), ['REQ-1', 'REQ-2']);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('an explicit correction resolves forked identity enrichment by retaining one verified branch ID', async () => {
|
|
270
|
+
const { validateLeadReferences } = await import('../scripts/application-accounting.mjs');
|
|
271
|
+
const original = lead({ id: 'original', url: 'https://example.com/careers' });
|
|
272
|
+
const first = lead({ ...original, id: 'first-branch', employerJobId: 'REQ-1', supersedes: original.id });
|
|
273
|
+
const second = lead({ ...original, id: 'second-branch', employerJobId: 'REQ-2', supersedes: original.id });
|
|
274
|
+
assert.doesNotThrow(() => validateLeadReferences(first, [original]));
|
|
275
|
+
assert.doesNotThrow(() => validateLeadReferences(second, [original, first]));
|
|
276
|
+
const prior = [original, first, second];
|
|
277
|
+
assert.equal(discoveryProjection(prior).conflicts.length, 1);
|
|
278
|
+
const correction = lead({
|
|
279
|
+
...first, id: 'resolved-identity', supersedes: [first.id, second.id],
|
|
280
|
+
evidence: 'Employer posting confirms REQ-1; the second assessment used an incorrect requisition ID.',
|
|
281
|
+
});
|
|
282
|
+
assert.doesNotThrow(() => validateLeadReferences(correction, prior));
|
|
283
|
+
for (const events of [[...prior, correction], [correction, second, first, original]]) {
|
|
284
|
+
const result = discoveryProjection(events);
|
|
285
|
+
assert.equal(result.reviewedCount, 1);
|
|
286
|
+
assert.equal(result.qualifiedCount, 1);
|
|
287
|
+
assert.equal(result.uniqueLeadCount, 1);
|
|
288
|
+
assert.equal(result.conflicts.length, 0);
|
|
289
|
+
assert.equal(result.leads[0].employerJobId, 'REQ-1');
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('an identity correction cannot merge unrelated requisitions at the same company and URL', async () => {
|
|
294
|
+
const { validateLeadReferences } = await import('../scripts/application-accounting.mjs');
|
|
295
|
+
const first = lead({ id: 'first-requisition', employerJobId: 'REQ-1', url: 'https://example.com/careers' });
|
|
296
|
+
const second = lead({ ...first, id: 'second-requisition', employerJobId: 'REQ-2' });
|
|
297
|
+
const correction = lead({ ...first, id: 'invalid-merge', supersedes: [first.id, second.id] });
|
|
298
|
+
assert.throws(() => validateLeadReferences(correction, [first, second]), /same round, source and requisition/);
|
|
299
|
+
const result = discoveryProjection([first, second]);
|
|
300
|
+
assert.equal(result.reviewedCount, 2);
|
|
301
|
+
assert.equal(result.uniqueLeadCount, 2);
|
|
302
|
+
assert.equal(result.conflicts.length, 0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('forked identity enrichments remain one conflicted assessment in either order', () => {
|
|
306
|
+
const original = lead();
|
|
307
|
+
const left = lead({ id: 'left-enrichment', employerJobId: 'REQ-1', supersedes: original.id });
|
|
308
|
+
const right = lead({ id: 'right-enrichment', employerJobId: 'REQ-2', supersedes: original.id });
|
|
309
|
+
for (const events of [[original, left, right], [right, left, original]]) {
|
|
310
|
+
const result = discoveryProjection(events);
|
|
311
|
+
assert.equal(result.reviewedCount, 1);
|
|
312
|
+
assert.equal(result.qualifiedCount, 0);
|
|
313
|
+
assert.equal(result.conflicts.length, 1);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
@@ -542,12 +542,17 @@ test('CLI emits bounded source coverage without private evidence or attribution'
|
|
|
542
542
|
assert.equal(started.code, 0, started.stderr);
|
|
543
543
|
const roundId = JSON.parse(started.stdout).roundId;
|
|
544
544
|
const sourceId = 'community-0123456789abcdef';
|
|
545
|
+
for(let i=0;i<8;i++) {
|
|
546
|
+
const result = await runCli(script,['round','lead','--stdin'],{roundId,sourceId,url:`https://fixture.example/jobs/${i}`,company:`Fixture ${i}`,disposition:i<2?'qualified':'closed-stale',observedAt:'2026-08-01T00:00:00Z',evidence:'Private lead assessment'},env);
|
|
547
|
+
assert.equal(result.code,0,result.stderr);
|
|
548
|
+
}
|
|
545
549
|
const report = await runCli(script, ['round', 'source', '--stdin'], { roundId, sourceId, status: 'searched', reviewedCount: 8, qualifiedCount: 2, evidence: 'Private search query and candidate context', applicationIds: [] }, env);
|
|
546
550
|
assert.equal(report.code, 0, report.stderr);
|
|
547
551
|
const event = captured.find((body) => body.event === 'source_checked');
|
|
548
552
|
assert.ok(event, JSON.stringify(captured));
|
|
549
553
|
assert.deepEqual(event.properties, { sourceId: 'community', status: 'searched', reviewedCount: 8, qualifiedCount: 2 });
|
|
550
554
|
assert.equal(JSON.stringify(captured).includes('Private search query'), false);
|
|
555
|
+
assert.equal(JSON.stringify(captured).includes('Private lead assessment'), false);
|
|
551
556
|
assert.equal(JSON.stringify(captured).includes(roundId), false);
|
|
552
557
|
assert.equal(JSON.stringify(captured).includes(sourceId), false);
|
|
553
558
|
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { buildReview } from '../scripts/job-application.mjs';
|
|
10
|
+
|
|
11
|
+
const now = new Date('2026-09-14T12:00:00Z');
|
|
12
|
+
const applications = (count, submittedAt = '2026-09-14T10:00:00Z') => Array.from({ length: count }, (_, index) => ({
|
|
13
|
+
id: `application-${index}`, company: `Company ${index}`, role: 'Engineer',
|
|
14
|
+
url: `https://example.test/jobs/${index}`, source: 'email', status: 'submitted', submittedAt,
|
|
15
|
+
}));
|
|
16
|
+
const failure = {
|
|
17
|
+
version: 1, id: 'late-bounce', applicationId: 'application-0', attemptId: 'initial:application-0',
|
|
18
|
+
type: 'delivery-failed', occurredAt: '2026-09-14T11:00:00Z',
|
|
19
|
+
evidenceType: 'final-delivery-failure', evidence: 'Final failure matched to the original application email.',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test('late failure does not delay hygiene review after ten more canonical submissions', () => {
|
|
23
|
+
const entries = applications(20);
|
|
24
|
+
const acknowledgements = [{ uniqueSubmissionCount: 10, maturedApplicationCount: 0 }];
|
|
25
|
+
const review = buildReview(entries, [], acknowledgements, now, [failure]);
|
|
26
|
+
assert.equal(review.effectiveSubmissionCount, 19);
|
|
27
|
+
assert.equal(review.submittedSinceLastReview, 10);
|
|
28
|
+
assert.equal(review.reviewDue, true);
|
|
29
|
+
assert.ok(review.reviewReasons.includes('submission-hygiene'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('late failure does not delay mature review while conversion still uses effective applications', () => {
|
|
33
|
+
const entries = applications(40, '2026-01-01T10:00:00Z');
|
|
34
|
+
const acknowledgements = [{ uniqueSubmissionCount: 40, maturedApplicationCount: 20 }];
|
|
35
|
+
const outcomes = [{ id: 'application-1', status: 'interview', occurredAt: '2026-02-01T10:00:00Z' }];
|
|
36
|
+
const review = buildReview(entries, outcomes, acknowledgements, now, [failure]);
|
|
37
|
+
assert.equal(review.maturedApplications, 39);
|
|
38
|
+
assert.equal(review.conversionRates.interview, 2.6);
|
|
39
|
+
assert.equal(review.reviewDue, true);
|
|
40
|
+
assert.deepEqual(review.reviewReasons, ['outcome-effectiveness']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('review acknowledgement checkpoints recorded canonical counts after delivery failure', async (t) => {
|
|
44
|
+
const dir = await mkdtemp(join(tmpdir(), 'review-cadence-'));
|
|
45
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
46
|
+
const entries = applications(20, '2026-01-01T10:00:00Z');
|
|
47
|
+
// A duplicate row must not inflate either checkpoint.
|
|
48
|
+
entries.push({ ...entries[1], id: 'duplicate-row' });
|
|
49
|
+
await writeFile(join(dir, 'applications.ndjson'), entries.map(JSON.stringify).join('\n') + '\n');
|
|
50
|
+
await writeFile(join(dir, 'delivery.ndjson'), JSON.stringify(failure) + '\n');
|
|
51
|
+
await writeFile(join(dir, 'telemetry.json'), JSON.stringify({ enabled: false, disclosed: true }));
|
|
52
|
+
const env = {
|
|
53
|
+
...process.env, JOB_APPLICATION_AGENT_STATE_DIR: dir,
|
|
54
|
+
JOB_APPLICATION_AGENT_CLOUD_CONFIG: join(dir, 'absent.json'),
|
|
55
|
+
JOB_APPLICATION_AGENT_SOURCE_COMMUNITY_URL: 'http://127.0.0.1:9',
|
|
56
|
+
};
|
|
57
|
+
const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
|
|
58
|
+
const ack = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'review-ack', '--stdin'], {
|
|
59
|
+
env, encoding: 'utf8', input: JSON.stringify({ reviewedAt: now.toISOString() }),
|
|
60
|
+
}));
|
|
61
|
+
assert.equal(ack.uniqueSubmissionCount, 20);
|
|
62
|
+
assert.equal(ack.maturedApplicationCount, 20);
|
|
63
|
+
const stored = JSON.parse((await readFile(join(dir, 'reviews.ndjson'), 'utf8')).trim());
|
|
64
|
+
assert.equal(stored.uniqueSubmissionCount, 20);
|
|
65
|
+
assert.equal(stored.maturedApplicationCount, 20);
|
|
66
|
+
});
|