mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 12:45:55 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Connected chats show safe notices when a run starts, waits, finishes, or fails. > - A recurring scan projected these notices even when no eligible work existed. > - Eligibility depends on committed run state, conversation lineage, and current authority. > - Existing database rows can reconstruct that work after restart. > - This PR wakes a scoped worker from those writes and removes lifecycle polling. > - Native progress events keep their separate existing owner until the next migration. ## Linked Issues or Issue Description **What happened?** The chat maintenance timer repeatedly scanned run lifecycle state. Empty and settled instances still queried the database. A run-status event alone cannot replace the scan: prompt receipts, continuation receipts, identity changes, and restored authority can make a run eligible later. **What did you expect?** Committed changes wake projection. Startup reconstructs eligible work. A completed pass stops until another relevant change arrives. Failed work retains recovery without scanning settled history forever. **Steps to reproduce** Configure an automatic chat endpoint. Finish its work, or leave it unused. Observe recurring lifecycle projection queries from the chat maintenance callback. Stacked on #17592. Related foundation: #15625. This PR owns lifecycle milestones only. ## What Changed - Preserve upstream coalesced Cloud membership writes together with locked membership restoration and its committed chat notification. - Signal milestone eligibility from the real Slack account-link transaction. - Register the lifecycle worker with the existing delivery coordinator. Remove lifecycle projection from the periodic chat callback. Keep native progress in a separately named lane and preserve the explicit combined API. - Reconstruct company/issue scopes from the existing selector. Drain bounded batches with company fairness. Retry failures, retain writes through drain/resume, and disarm settled work. - Signal from supported run, presentation, interaction, receipt, lineage, conversation, recovery, and authority writers. Preserve their transaction ownership, including nested savepoints and uncertain settlement. No new table is added. - Preserve provider authorization, authored-final suppression, responsible-user continuity, deduplication, the 24-hour selection window, and raw-output restrictions. Keep provider interpretation in its owner. - Preserve PostgreSQL timestamp precision in lifecycle pagination. Rejected submillisecond candidates must not repeat the same page and starve later notices. - Document producer coverage and alternate-writer dispositions in `doc/plans/2026-10-25-chat-run-milestone-sleep-audit.md`. ## Verification Published head: `1d056f68c5b1005933b9e12de8745bf1c4cbeeca`, targeting master after #17592 merged at `7d75ae0569e41786640cf09647ae8849411d2f22`. A normal merge integrates the actual landed parent. The entire 77-path milestone patch is binary-identical to the locally verified preparation across that ancestry reconciliation; both unrelated master paths are preserved. The four earlier producer/test integration conflicts retain scoped milestone and voice notices, current onboarding completion admission, and expanded identity query/read-scope coverage. - **330 distinct selected cases pass on the verified unchanged patch:** 305 across 14 complete suites (all five milestone worker suites, run identity, board claim, Cloud provisioning, auth session routes, Cloud actor authentication, stale-lock recovery, GitHub credentials, project Git authorization, and app admission wiring), plus 25 selected real chat producer cases. The remaining 1,590 chat integration cases were skipped, not passed. - The real producer cases cover saved milestone discovery, GitHub and Slack identity changes, account-link rollback/reconfirmation, ordinary ingress reactivation, post-close action/delivery receipt settlement, all ten authority-restoration paths including Cloud synchronization, and changed native review metadata. - Worker coverage verifies outer commit, rollback, uncertain-write recovery, restart, bounded batching/fairness, timestamp precision, retries, drain/resume, and settled SQL quiet. Authorization and existing publication deduplication remain with their owning services. - Server production typechecking, build, and module-boundary checks pass. The tests above were executed separately. Production typechecking excludes `src/__tests__` and does not replace those runs. Each database owner uses its own newly created and app-migrated PostgreSQL fixture. - [Current-parent acceptance and preserved failure ledger](https://github.com/paperclipai/paperclip/blob/1d056f68c5b1005933b9e12de8745bf1c4cbeeca/doc/plans/2026-10-25-chat-run-milestone-sleep-audit.md#current-parent-integration-and-acceptance-october-28) records commands, counts, producer scope, conflict dispositions, and limits. Earlier combined-stack counts and failures keep their original revision scope; local acceptance does not erase old CI failures. - No new migration, queue table, dependency, workflow, or shared scheduler contract is introduced. Parent migration 0397, bounded consent recovery, voice cleanup/retention repairs, and runner checkpoint restoration remain present. - **Exact-head Greptile is 5/5 and all 55 current checks are clean.** No Ponytail review is present, which is an explicitly authorized nonblocker; no failing review is waived. Older reviews and parent checks are not current passes. Actual failing Ponytail findings must be fixed; an absent/stopped/paused turn without a verdict is an authorized nonblocker. Active unfinished Ponytail uses its original request/start timer, with a 25-minute monitor cutoff respecting the user's 30-minute maximum. - No complete local repository matrix, live provider tenant, external alarm, physical host/database suspension, or fleet savings claim is made. Native progress remains a separate following slice. ## Risks - A future eligibility writer must notify the existing projection boundary. Independent SQL writers or another active server process are outside this process-local signal contract. - Authority and lineage remain sensitive. The worker uses the existing selectors and publication policy; it does not grant execution or disclosure authority. - Native progress and other publication maintenance still have recurring work. This PR removes one reason, not the entire chat maintenance timer. - Worker quiet does not prove database suspension, host sleep, or external alarm coverage. Cloud sleep blockers remain unchanged. - Merge eligibility requires passing current-head CI and Greptile 5/5 after this actual-master reconciliation. Subsequent stack slices retain their own gates. ## Model Used OpenAI Codex, GPT-6 family. The exact serving model ID and context-window size are not exposed. Used repository inspection, reasoning, shell execution, PostgreSQL tests, browser E2Es and GitHub tools. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details available in this session) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run the documented focused tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
417 lines
17 KiB
TypeScript
417 lines
17 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
createChatReconciliationCoordinator,
|
|
shouldEnablePrivateHostnameGuard,
|
|
} from "../app.ts";
|
|
|
|
describe("createChatReconciliationCoordinator", () => {
|
|
it.each([false, true])(
|
|
"keeps later inbound sweeps independent of GitHub recovery and joins shutdown (rejects: %s)",
|
|
async (rejects) => {
|
|
let release!: () => void;
|
|
const held = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
const failure = new Error("GitHub recovery unavailable");
|
|
const processFailedGitHubWebhookDeliveries = vi.fn(async () => {
|
|
await held;
|
|
if (rejects) throw failure;
|
|
});
|
|
const processPendingDeliveries = vi.fn(async () => undefined);
|
|
const flushPublications = vi.fn(async () => undefined);
|
|
const onError = vi.fn();
|
|
const input = {
|
|
processFailedGitHubWebhookDeliveries,
|
|
processPendingDeliveries,
|
|
flushPublications,
|
|
projectNativeProgress: async () => 0,
|
|
processPendingSlackFileUploadReceipts: async () => undefined,
|
|
processPendingSlackSessionSyncs: async () => undefined,
|
|
onError,
|
|
};
|
|
const coordinator = createChatReconciliationCoordinator(input);
|
|
let draining: Promise<void> | undefined;
|
|
try {
|
|
coordinator.reconcile();
|
|
await vi.waitFor(() => {
|
|
expect(processFailedGitHubWebhookDeliveries).toHaveBeenCalledOnce();
|
|
expect(processPendingDeliveries).toHaveBeenCalledTimes(1);
|
|
});
|
|
// A second sweep must admit work arriving AFTER the first sweep while
|
|
// remote GitHub recovery remains held. Same-sweep parallelism is not enough.
|
|
coordinator.reconcile();
|
|
await vi.waitFor(() => {
|
|
expect(processPendingDeliveries).toHaveBeenCalledTimes(2);
|
|
expect(flushPublications).toHaveBeenCalledTimes(2);
|
|
});
|
|
expect(processFailedGitHubWebhookDeliveries).toHaveBeenCalledOnce();
|
|
coordinator.stop();
|
|
let drained = false;
|
|
draining = coordinator.drain().then(() => {
|
|
drained = true;
|
|
});
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect(drained).toBe(false);
|
|
coordinator.reconcile();
|
|
release();
|
|
await draining;
|
|
expect(drained).toBe(true);
|
|
expect(processFailedGitHubWebhookDeliveries).toHaveBeenCalledOnce();
|
|
expect(processPendingDeliveries).toHaveBeenCalledTimes(2);
|
|
expect(onError).toHaveBeenCalledTimes(rejects ? 1 : 0);
|
|
if (rejects)
|
|
expect(onError).toHaveBeenCalledWith(
|
|
"GitHub webhook recovery",
|
|
failure,
|
|
);
|
|
} finally {
|
|
release();
|
|
coordinator.stop();
|
|
await (draining ?? coordinator.drain());
|
|
}
|
|
},
|
|
);
|
|
|
|
it("keeps receiver and voice inventory out of periodic chat maintenance", () => {
|
|
const source = readFileSync(new URL("../app.ts", import.meta.url), "utf8");
|
|
const maintenance = source.slice(source.indexOf("const chatReconciliation ="), source.indexOf("const unsubscribeChatPublicationSignals"));
|
|
expect(maintenance).not.toContain("chatChannels.reconcileProviderRuntimes()");
|
|
expect(maintenance).not.toContain("chatChannels.voice.");
|
|
expect(source).toContain("chatChannels.registerReceiverWork(deliveryWork,");
|
|
expect(source).toContain("chatChannels.registerTeamsConsentExpiryWork(deliveryWork,");
|
|
const teams = readFileSync(new URL("../services/chat-channels/microsoft-teams/file-delivery.ts", import.meta.url), "utf8");
|
|
expect(source).toContain("chatChannels.registerTeamsAttemptRecoveryWork(deliveryWork,");
|
|
expect(teams).not.toContain("scheduleTeamsFileMaintenance");
|
|
expect(teams).not.toContain("selectDistinct");
|
|
const chat = readFileSync(new URL("../services/chat-channels.ts", import.meta.url), "utf8");
|
|
expect(chat).not.toContain("scheduleTeamsFileMaintenance");
|
|
});
|
|
|
|
it("separates live voice recovery from the remaining periodic maintenance", () => {
|
|
const source = readFileSync(new URL("../app.ts", import.meta.url), "utf8");
|
|
expect(source).toContain("chatChannels.voice.registerRecoveryWork(deliveryWork,");
|
|
expect(source).toContain("chatChannels.voice.registerReplyWork(deliveryWork,");
|
|
expect(source).toContain("chatChannels.voice.inbound.registerCleanupWork(deliveryWork,");
|
|
expect(source).toContain("chatChannels.voice.inbound.registerRetentionWork(deliveryWork,");
|
|
expect(source).toContain("chatChannels.voice.registerReportWork(deliveryWork,");
|
|
expect(source).not.toContain("reconcileVoiceSessions");
|
|
expect(source).not.toContain("chatChannels.voice.reconcile()");
|
|
});
|
|
|
|
it("wires GitHub recovery into its independent lane", () => {
|
|
const source = readFileSync(new URL("../app.ts", import.meta.url), "utf8");
|
|
expect(source).toContain(
|
|
"chatChannels.processFailedGitHubWebhookDeliveries()",
|
|
);
|
|
const service = readFileSync(
|
|
new URL("../services/chat-channels.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const ordinary = service.slice(
|
|
service.indexOf("async function processPendingDeliveries("),
|
|
service.indexOf("async function listResources("),
|
|
);
|
|
expect(ordinary).not.toContain("processFailedGitHubWebhookDeliveries()");
|
|
});
|
|
|
|
it("wires bounded publication refill to commit workers and isolates periodic maintenance", () => {
|
|
const source = readFileSync(new URL("../app.ts", import.meta.url), "utf8");
|
|
expect(source).toContain("registerChatDeliveryWork(deliveryWork, chatChannels,");
|
|
expect(source).toContain("registerChatActionWork(deliveryWork, chatChannels,");
|
|
const maintenance = source.slice(
|
|
source.indexOf("const reconcileChatPublicationMaintenance ="),
|
|
source.indexOf("const chatReconciliation ="),
|
|
);
|
|
expect(maintenance).toContain("chatChannels.processPublicationMaintenance()");
|
|
expect(maintenance).not.toContain("chatChannels.schedulePendingPublications()");
|
|
expect(maintenance).not.toContain("chatChannels.processPendingPublications()");
|
|
const workers = readFileSync(new URL("../services/chat-delivery-work.ts", import.meta.url), "utf8");
|
|
expect(workers).toContain("run: service.scheduleQueuedPublications");
|
|
// The service integration tests hold real publication workers while this
|
|
// scheduled method returns; app shutdown must also join those workers.
|
|
expect(source).toContain("await chatChannels.shutdown()");
|
|
expect(source).not.toContain("chatChannels.processPendingSlackSessionSyncs()");
|
|
expect(source).not.toContain("chatChannels.processPendingChatMaintenance()");
|
|
const service = readFileSync(new URL("../services/chat-channels.ts", import.meta.url), "utf8");
|
|
const actionMaintenance = service.slice(service.indexOf("async function processPendingChatMaintenance("),
|
|
service.indexOf("async function processPendingDeliveries("));
|
|
expect(actionMaintenance).not.toContain("processFailedChatRunRetries(");
|
|
expect(actionMaintenance).not.toContain("processPendingProviderEffects(");
|
|
expect(actionMaintenance).not.toContain("processPendingGitHubWebhookIngress(");
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"isolates a blocked native progress projector and joins its completion (rejects: %s)",
|
|
async (rejects) => {
|
|
let release!: () => void;
|
|
const held = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
const failure = new Error("native progress projection failed");
|
|
const projectNativeProgress = vi
|
|
.fn(async () => 0)
|
|
.mockImplementationOnce(async () => {
|
|
await held;
|
|
if (rejects) throw failure;
|
|
return 1;
|
|
});
|
|
const flushPublications = vi.fn(async () => undefined);
|
|
const onError = vi.fn();
|
|
const coordinator = createChatReconciliationCoordinator({
|
|
projectNativeProgress,
|
|
processPendingDeliveries: async () => undefined,
|
|
flushPublications,
|
|
processPendingSlackFileUploadReceipts: async () => undefined,
|
|
processPendingSlackSessionSyncs: async () => undefined,
|
|
onError,
|
|
});
|
|
try {
|
|
coordinator.reconcile();
|
|
await vi.waitFor(() => {
|
|
expect(projectNativeProgress).toHaveBeenCalledTimes(1);
|
|
expect(flushPublications).toHaveBeenCalledTimes(1);
|
|
});
|
|
// A newly committed final/question must dispatch even while a different
|
|
// issue's projection is blocked. Repeated signals leave one dirty retry.
|
|
coordinator.notifyPublications();
|
|
coordinator.notifyPublications();
|
|
await vi.waitFor(() =>
|
|
expect(flushPublications).toHaveBeenCalledTimes(2),
|
|
);
|
|
expect(projectNativeProgress).toHaveBeenCalledTimes(1);
|
|
let drained = false;
|
|
const draining = coordinator.drain().then(() => {
|
|
drained = true;
|
|
});
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect(drained).toBe(false);
|
|
release();
|
|
await draining;
|
|
expect(projectNativeProgress).toHaveBeenCalledTimes(2);
|
|
expect(onError).toHaveBeenCalledTimes(rejects ? 1 : 0);
|
|
if (rejects)
|
|
expect(onError).toHaveBeenCalledWith("native progress", failure);
|
|
const completedCounts = [
|
|
projectNativeProgress.mock.calls.length,
|
|
flushPublications.mock.calls.length,
|
|
];
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect([
|
|
projectNativeProgress.mock.calls.length,
|
|
flushPublications.mock.calls.length,
|
|
]).toEqual(completedCounts);
|
|
} finally {
|
|
release();
|
|
coordinator.stop();
|
|
await coordinator.drain();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("joins a blocked native progress projector at shutdown without starting dirty follow-ups", async () => {
|
|
let release!: () => void;
|
|
const held = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
const projectNativeProgress = vi.fn(async () => {
|
|
await held;
|
|
return 1;
|
|
});
|
|
const flushPublications = vi.fn(async () => undefined);
|
|
const coordinator = createChatReconciliationCoordinator({
|
|
projectNativeProgress,
|
|
flushPublications,
|
|
processPendingDeliveries: async () => undefined,
|
|
processPendingSlackFileUploadReceipts: async () => undefined,
|
|
processPendingSlackSessionSyncs: async () => undefined,
|
|
onError: vi.fn(),
|
|
});
|
|
try {
|
|
coordinator.reconcile();
|
|
await vi.waitFor(() => {
|
|
expect(projectNativeProgress).toHaveBeenCalledOnce();
|
|
expect(flushPublications).toHaveBeenCalledOnce();
|
|
});
|
|
coordinator.notifyPublications();
|
|
coordinator.stop();
|
|
let drained = false;
|
|
const draining = coordinator.drain().then(() => {
|
|
drained = true;
|
|
});
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect(drained).toBe(false);
|
|
release();
|
|
await draining;
|
|
expect(projectNativeProgress).toHaveBeenCalledOnce();
|
|
expect(flushPublications).toHaveBeenCalledOnce();
|
|
} finally {
|
|
release();
|
|
coordinator.stop();
|
|
await coordinator.drain();
|
|
}
|
|
});
|
|
|
|
it("keeps slow optional recovery from suppressing later publication sweeps", async () => {
|
|
let releaseDelivery!: () => void;
|
|
let releaseSlackStatus!: () => void;
|
|
const deliveryReleased = new Promise<void>((resolve) => {
|
|
releaseDelivery = resolve;
|
|
});
|
|
const slackStatusReleased = new Promise<void>((resolve) => {
|
|
releaseSlackStatus = resolve;
|
|
});
|
|
const processPendingDeliveries = vi.fn(async () => deliveryReleased);
|
|
const flushPublications = vi.fn(async () => undefined);
|
|
const processPendingSlackFileUploadReceipts = vi.fn(async () => undefined);
|
|
const processPendingSlackSessionSyncs = vi.fn(
|
|
async () => slackStatusReleased,
|
|
);
|
|
const onError = vi.fn();
|
|
const coordinator = createChatReconciliationCoordinator({
|
|
projectNativeProgress: async () => 0,
|
|
processPendingDeliveries,
|
|
flushPublications,
|
|
processPendingSlackFileUploadReceipts,
|
|
processPendingSlackSessionSyncs,
|
|
onError,
|
|
});
|
|
|
|
coordinator.reconcile();
|
|
await vi.waitFor(() => {
|
|
expect(processPendingDeliveries).toHaveBeenCalledTimes(1);
|
|
expect(flushPublications).toHaveBeenCalledTimes(1);
|
|
expect(processPendingSlackFileUploadReceipts).toHaveBeenCalledTimes(1);
|
|
expect(processPendingSlackSessionSyncs).toHaveBeenCalledTimes(1);
|
|
});
|
|
coordinator.reconcile();
|
|
await vi.waitFor(() => {
|
|
expect(flushPublications).toHaveBeenCalledTimes(2);
|
|
expect(processPendingSlackFileUploadReceipts).toHaveBeenCalledTimes(2);
|
|
});
|
|
expect(processPendingDeliveries).toHaveBeenCalledTimes(1);
|
|
expect(processPendingSlackSessionSyncs).toHaveBeenCalledTimes(1);
|
|
|
|
releaseDelivery();
|
|
releaseSlackStatus();
|
|
await coordinator.drain();
|
|
expect(onError).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"joins the independent Slack file receipt lane at shutdown (rejects: %s)",
|
|
async (rejects) => {
|
|
let releaseReceipt!: () => void;
|
|
const receiptReleased = new Promise<void>((resolve) => {
|
|
releaseReceipt = resolve;
|
|
});
|
|
const lookupError = new Error("receipt lookup failed");
|
|
const processPendingSlackFileUploadReceipts = vi
|
|
.fn(async () => undefined)
|
|
.mockImplementationOnce(async () => {
|
|
await receiptReleased;
|
|
if (rejects) throw lookupError;
|
|
});
|
|
const processPendingDeliveries = vi.fn(async () => undefined);
|
|
const flushPublications = vi.fn(async () => undefined);
|
|
const processPendingSlackSessionSyncs = vi.fn(async () => undefined);
|
|
const onError = vi.fn();
|
|
const coordinator = createChatReconciliationCoordinator({
|
|
projectNativeProgress: async () => 0,
|
|
processPendingDeliveries,
|
|
flushPublications,
|
|
processPendingSlackFileUploadReceipts,
|
|
processPendingSlackSessionSyncs,
|
|
onError,
|
|
});
|
|
let draining: Promise<void> | undefined;
|
|
try {
|
|
coordinator.reconcile();
|
|
await vi.waitFor(() => {
|
|
expect(processPendingSlackFileUploadReceipts).toHaveBeenCalledTimes(
|
|
1,
|
|
);
|
|
expect(flushPublications).toHaveBeenCalledTimes(1);
|
|
});
|
|
coordinator.reconcile();
|
|
await vi.waitFor(() => {
|
|
expect(processPendingDeliveries).toHaveBeenCalledTimes(2);
|
|
expect(flushPublications).toHaveBeenCalledTimes(2);
|
|
expect(processPendingSlackSessionSyncs).toHaveBeenCalledTimes(2);
|
|
});
|
|
expect(processPendingSlackFileUploadReceipts).toHaveBeenCalledTimes(1);
|
|
|
|
let drained = false;
|
|
draining = coordinator.drain().then(() => {
|
|
drained = true;
|
|
});
|
|
// Flush the promise queue, not a wall-clock delay: the held receipt
|
|
// must still be part of shutdown's joined work after other lanes end.
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect(drained).toBe(false);
|
|
expect(onError).not.toHaveBeenCalled();
|
|
|
|
releaseReceipt();
|
|
await draining;
|
|
expect(drained).toBe(true);
|
|
expect(onError).toHaveBeenCalledTimes(rejects ? 1 : 0);
|
|
if (rejects) {
|
|
expect(onError).toHaveBeenCalledWith(
|
|
"Slack file receipts",
|
|
lookupError,
|
|
);
|
|
}
|
|
|
|
// A completed or failed lookup releases only its own single-flight
|
|
// slot; the next ordinary reconciliation can recover another receipt.
|
|
coordinator.reconcile();
|
|
await coordinator.drain();
|
|
expect(processPendingSlackFileUploadReceipts).toHaveBeenCalledTimes(2);
|
|
expect(onError).toHaveBeenCalledTimes(rejects ? 1 : 0);
|
|
} finally {
|
|
releaseReceipt();
|
|
await draining;
|
|
await coordinator.drain();
|
|
}
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("shouldEnablePrivateHostnameGuard", () => {
|
|
it("enables the hostname guard for local_trusted private deployments", () => {
|
|
expect(
|
|
shouldEnablePrivateHostnameGuard({
|
|
deploymentMode: "local_trusted",
|
|
deploymentExposure: "private",
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not enable the hostname guard for local_trusted public deployments", () => {
|
|
expect(
|
|
shouldEnablePrivateHostnameGuard({
|
|
deploymentMode: "local_trusted",
|
|
deploymentExposure: "public",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("enables the hostname guard for authenticated private deployments", () => {
|
|
expect(
|
|
shouldEnablePrivateHostnameGuard({
|
|
deploymentMode: "authenticated",
|
|
deploymentExposure: "private",
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not enable the hostname guard for authenticated public deployments", () => {
|
|
expect(
|
|
shouldEnablePrivateHostnameGuard({
|
|
deploymentMode: "authenticated",
|
|
deploymentExposure: "public",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|