feat(campaign): complete durable operator queue
This commit is contained in:
@@ -3,6 +3,8 @@ import test from "node:test";
|
||||
|
||||
import {
|
||||
OPERATOR_QUEUE_REASON,
|
||||
campaignLifecycleTotals,
|
||||
campaignVersionWorkItems,
|
||||
operatorQueueActionBlocks,
|
||||
type OperatorQueueFacts,
|
||||
type OperatorQueuePermissions
|
||||
@@ -12,13 +14,14 @@ const everything: OperatorQueueFacts = {
|
||||
retryable: 2,
|
||||
outcomeUnknown: 1,
|
||||
queueableUnattempted: 3,
|
||||
queued: 4,
|
||||
pausable: 4,
|
||||
paused: 5,
|
||||
cancellable: 14
|
||||
};
|
||||
|
||||
const allPermissions: OperatorQueuePermissions = {
|
||||
canRead: true,
|
||||
canOpenReport: true,
|
||||
canControl: true,
|
||||
canRetry: true,
|
||||
canQueue: true,
|
||||
@@ -28,6 +31,7 @@ const allPermissions: OperatorQueuePermissions = {
|
||||
test("enables every fixed queue action when state and permission allow it", () => {
|
||||
assert.deepEqual(operatorQueueActionBlocks(everything, allPermissions), {
|
||||
details: null,
|
||||
report: null,
|
||||
pause: null,
|
||||
resume: null,
|
||||
retry: null,
|
||||
@@ -40,11 +44,12 @@ test("enables every fixed queue action when state and permission allow it", () =
|
||||
test("retains every unavailable action with a state-specific explanation", () => {
|
||||
assert.deepEqual(
|
||||
operatorQueueActionBlocks(
|
||||
{ retryable: 0, outcomeUnknown: 0, queueableUnattempted: 0, queued: 0, paused: 0, cancellable: 0 },
|
||||
{ retryable: 0, outcomeUnknown: 0, queueableUnattempted: 0, pausable: 0, paused: 0, cancellable: 0 },
|
||||
allPermissions
|
||||
),
|
||||
{
|
||||
details: null,
|
||||
report: null,
|
||||
pause: OPERATOR_QUEUE_REASON.noQueued,
|
||||
resume: OPERATOR_QUEUE_REASON.noPaused,
|
||||
retry: OPERATOR_QUEUE_REASON.noFailed,
|
||||
@@ -58,6 +63,7 @@ test("retains every unavailable action with a state-specific explanation", () =>
|
||||
test("permission explanations take precedence over queue state", () => {
|
||||
const noPermissions: OperatorQueuePermissions = {
|
||||
canRead: false,
|
||||
canOpenReport: false,
|
||||
canControl: false,
|
||||
canRetry: false,
|
||||
canQueue: false,
|
||||
@@ -66,6 +72,43 @@ test("permission explanations take precedence over queue state", () => {
|
||||
const blocks = operatorQueueActionBlocks(everything, noPermissions);
|
||||
assert.deepEqual(
|
||||
Object.values(blocks),
|
||||
Array.from({ length: 7 }, () => OPERATOR_QUEUE_REASON.permissionDenied)
|
||||
Array.from({ length: 8 }, () => OPERATOR_QUEUE_REASON.permissionDenied)
|
||||
);
|
||||
});
|
||||
|
||||
test("allows in-page job selection without report permission while keeping report access disabled", () => {
|
||||
const blocks = operatorQueueActionBlocks(everything, {
|
||||
...allPermissions,
|
||||
canOpenReport: false
|
||||
});
|
||||
assert.equal(blocks.details, null);
|
||||
assert.equal(blocks.report, OPERATOR_QUEUE_REASON.permissionDenied);
|
||||
});
|
||||
|
||||
test("keeps historical active work visible after a new draft becomes current", () => {
|
||||
const items = campaignVersionWorkItems(
|
||||
"campaign-1",
|
||||
"version-2",
|
||||
[
|
||||
{ id: "version-2", version_number: 2 },
|
||||
{ id: "version-1", version_number: 1 }
|
||||
],
|
||||
{
|
||||
"version-2": { state: "draft", queued: 0, failed: 0 },
|
||||
"version-1": { state: "active", queued: 3, failed: 1 }
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(items.map((item) => [item.versionId, item.isCurrent]), [
|
||||
["version-2", true],
|
||||
["version-1", false]
|
||||
]);
|
||||
assert.deepEqual(items[1].summary, { state: "active", queued: 3, failed: 1 });
|
||||
});
|
||||
|
||||
test("campaign-wide lifecycle totals are not multiplied across version rows", () => {
|
||||
assert.deepEqual(campaignLifecycleTotals([
|
||||
{ pausable: 3, paused: 1, cancellable: 4 },
|
||||
{ pausable: 0, paused: 2, cancellable: 2 }
|
||||
]), { pausable: 3, paused: 3, cancellable: 6 });
|
||||
});
|
||||
|
||||
@@ -5,14 +5,72 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const source = readFileSync(resolve(here, "../src/features/operator/OperatorQueuePage.tsx"), "utf8");
|
||||
const translations = readFileSync(resolve(here, "../src/i18n/generatedTranslations.ts"), "utf8");
|
||||
const backendRouter = readFileSync(resolve(here, "../../src/govoplan_campaign/backend/router.py"), "utf8");
|
||||
const responseSecurity = readFileSync(resolve(here, "../../src/govoplan_campaign/backend/response_security.py"), "utf8");
|
||||
|
||||
assert.match(source, /<DataGrid/);
|
||||
assert.match(source, /<TableActionGroup actions=/);
|
||||
for (const action of ["details", "pause", "resume", "retry", "queue-unsent", "reconcile", "cancel"]) {
|
||||
for (const action of ["details", "report", "pause", "resume", "retry", "queue-unsent", "reconcile", "cancel", "open-recipient-report"]) {
|
||||
assert.match(source, new RegExp(`id: "${action}"`));
|
||||
}
|
||||
assert.match(source, /useDeltaWatermarks/);
|
||||
assert.match(source, /useSearchParams/);
|
||||
assert.match(source, /searchParams\.get\("campaign"\)/);
|
||||
assert.match(source, /searchParams\.get\("version"\)/);
|
||||
assert.match(source, /next\.set\("campaign", campaignId\)[\s\S]*next\.set\("version", versionId\)/);
|
||||
assert.match(source, /setSearchParams\(next, \{ replace: true \}\)/);
|
||||
assert.match(source, /window\.setInterval[\s\S]*10_000/);
|
||||
assert.match(source, /window\.setInterval[\s\S]*60_000/);
|
||||
assert.match(source, /<ConfirmDialog[\s\S]*cancelTarget/);
|
||||
assert.doesNotMatch(source, /claim_token|eml_local_path|storage_key|smtp_transport_revision/);
|
||||
assert.match(source, /includeVersions: true/);
|
||||
assert.match(source, /getCampaignSummary\(settings, campaign\.id, version\.id\)/);
|
||||
assert.match(source, /campaignVersionWorkItems/);
|
||||
assert.match(source, /getCampaignJobs\(settings, selectedRow\.campaign\.id,[\s\S]*versionId: selectedVersionId/);
|
||||
assert.doesNotMatch(source, /getCampaignJobDetail|getCampaignJobsDelta|diagnostics/);
|
||||
assert.match(source, /mode: "server"/);
|
||||
assert.match(source, /totalRows: jobs\.total/);
|
||||
assert.match(source, /query=\{jobGridQuery\}/);
|
||||
assert.match(source, /onQueryChange=\{handleJobGridQuery\}/);
|
||||
assert.match(source, /pageSizeOptions: \[25, 50, 100, 200\]/);
|
||||
assert.match(source, /jobsRequestRef\.current/);
|
||||
assert.match(source, /requestId !== jobsRequestRef\.current/);
|
||||
assert.match(source, /selectedQueueActive[\s\S]*backgroundJobsLoadRef/);
|
||||
assert.match(source, /getRowKey=\{\(row\) => `\$\{row\.campaign\.id\}:\$\{rowVersionId\(row\)/);
|
||||
assert.match(source, /row\.campaign\.id === selectedCampaignId && rowVersionId\(row\) === selectedVersionParam/);
|
||||
assert.match(source, /retryCampaignJobs[\s\S]*version_id: rowVersionId\(row\)/);
|
||||
assert.match(source, /sendUnattemptedCampaignJobs[\s\S]*version_id: rowVersionId\(row\)/);
|
||||
assert.match(source, /report\?version=\$\{encodeURIComponent\(rowVersionId\(row\)\)\}&send_status=outcome_unknown/);
|
||||
for (const count of ["queued", "claimed", "sending", "completed"]) {
|
||||
assert.match(source, new RegExp(`row\\.${count}`));
|
||||
assert.match(source, new RegExp(`totals\\.${count}`));
|
||||
}
|
||||
for (const field of ["campaign_version_id", "entry_id", "entry_index", "queue_status", "send_status", "imap_status", "attempt_count", "queued_at", "sent_at", "updated_at", "last_error"]) {
|
||||
assert.match(source, new RegExp(field));
|
||||
}
|
||||
assert.match(source, /deliveryResultSummary[\s\S]*send_status[\s\S]*imap_status/);
|
||||
assert.match(source, /durableResultText[\s\S]*slice\(0, 239\)/);
|
||||
assert.match(source, /permissions\.canOpenReport[\s\S]*you_do_not_have_permission/);
|
||||
assert.doesNotMatch(source, /claim_token|eml_local_path|storage_key|smtp_transport_revision|eml_sha256|message_id_header|smtp_response|provider_response|resolved_attachments/);
|
||||
assert.match(backendRouter, /"last_error": public_delivery_result_message\([\s\S]*last_error=job\.last_error/);
|
||||
assert.match(responseSecurity, /Map persisted provider text to a stable business-safe explanation/);
|
||||
assert.match(responseSecurity, /SMTP delivery failed; an operator can inspect restricted diagnostics\./);
|
||||
assert.doesNotMatch(source, /window\.alert\s*\(/);
|
||||
assert.doesNotMatch(source, /function QueuePressureGrid|function OperatorJob/);
|
||||
assert.doesNotMatch(source, /humanize\(/);
|
||||
|
||||
const localized = new Map([
|
||||
["i18n:govoplan-campaign.claimed.83c87884", ["Claimed", "Beansprucht"]],
|
||||
["i18n:govoplan-campaign.sending.ceafde86", ["Sending", "Im Versand"]],
|
||||
["i18n:govoplan-campaign.delivery_jobs_for_value0.50a94a70", ["Delivery jobs for {value0}", "Versandaufträge für {value0}"]],
|
||||
["i18n:govoplan-campaign.open_recipient_report.d6892ee4", ["Open recipient report", "Empfängerbezogenen Bericht öffnen"]],
|
||||
["i18n:govoplan-campaign.queued_at.1ac179b3", ["Queued at", "Eingereiht am"]],
|
||||
["i18n:govoplan-campaign.sent_at.a6f45556", ["Sent at", "Gesendet am"]],
|
||||
["i18n:govoplan-campaign.current.4fc0e2bc", ["Current", "Aktuell"]],
|
||||
["i18n:govoplan-campaign.historical.bb2fe205", ["Historical", "Historisch"]],
|
||||
["i18n:govoplan-campaign.pause_all_queued_work_for_this_campaign.64ee23cf", ["Pause all queued work for this Campaign", "Alle eingereihten Aufträge dieser Kampagne pausieren"]]
|
||||
]);
|
||||
for (const [key, values] of localized) {
|
||||
assert.equal(translations.split(`"${key}"`).length - 1, 2, `${key} occurs once per language`);
|
||||
for (const value of values) assert(translations.includes(`"${key}": ${JSON.stringify(value)}`));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user