Files
PaperClipAI/server/src/services/chat-run-publications.test.ts
T
DottaandPaperclip c2daf906d9 fix: preserve task continuation and question delivery (#17045)
## Thinking Path

> - Paperclip manages agent work and delivers its results.
> - One task can need several runs and a saved workspace.
> - A run can succeed while the task waits for its next run.
> - Chat fallback treated that success as completion and dropped useful
status.
> - Monitor runs also lost their conversation origin, and text-question
cards lost their prompt.
> - This change preserves the task outcome and exact delivery authority
across those boundaries.

## Linked Issues or Issue Description

**What happened?**

A Slack task requested a repository, yielded to a scheduled
continuation, and showed only a generic completion message. The next run
could not publish through the original conversation. A later text
question showed its title and Respond button without the question
context. Workspace recovery could commit the saved monitor result
without restoring its explanation.

**Expected behavior**

The original conversation shows the accepted continuation explanation,
receives the resumed result, and displays the full question when input
is needed. A successful provider run does not claim that unfinished work
is complete. Recovery delivers saved output once without another
provider result.

**Steps to reproduce**

Start an authorized chat task, prepare a repository for a later
admission, save a monitor wait, and let that monitor resume the task.
Repeat with workspace finalization interrupted and with a free-text
question. Inspect the originating conversation and the persisted output.

Related merged work: #16806 and #16975. The open monitor and recovery
changes found during duplicate search address different status and
liveness policies.

## What Changed

- Keep committed monitor summaries visible through the shared board and
chat response resolver. Preserve interaction ownership and comment
reuse.
- Recover a saved monitor explanation after finalization, with no
repeated provider result.
- Prove monitor publication origin from the admitted wake and matching
scheduling receipt. Match company, task, agent, requester, and schedule;
reject ambiguous sources.
- Include monitor continuations in the existing publication scan and use
the canonical task binding. Keep current transport authorization.
- Suppress completion fallback for yielded results. Explain missing
final output without claiming completion.
- Include question summaries, prompts, and help text in modal-backed
cards. Keep answered text private.
- Update execution semantics, capability maps, and the bounded
reliability plan.

## Verification

- Original source fails four composed PostgreSQL regressions: monitor
origin, yielded completion, question body, and saved-summary recovery.
The shared response resolver also fails before the repair.
- Repaired focused cohort: 148 tests pass across publication, question
privacy, scheduler, and native monitor suites. These use real disposable
PostgreSQL; they do not qualify live Slack.
- Full workspace typecheck and production build pass locally. Map checks
and whitespace checks pass.
- All CI gates pass at 1701c48f17.
Greptile reports 5/5. Both review findings are resolved. The local
full-suite run remains separate.
- The added 21-admission lineage and cycle regressions pass with the
complete 11-test database suite.
- The larger live Codex/Claude qualification and deployed staging
acceptance remain in progress. This PR does not claim that milestone is
complete.

## Risks

- Incorrect lineage could disclose output to another conversation. The
resolver requires one exact receipt-backed source and the worker still
checks current delivery authority.
- Status recovery could duplicate output or compete with a question. The
source-run presentation marker, saved-comment reuse, interaction
arbitration, and replay tests protect those boundaries.
- No database migration or public task-status change is required.

## Model Used

OpenAI Codex, GPT-6 family, with repository editing, terminal tests, and
browser inspection. The runtime does not expose an exact deployment
model ID or context-window size.

## 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)
- [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 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>
2026-10-20 20:43:17 -05:00

216 lines
7.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { projectSafeChatPublication } from "./chat-publication-projection.js";
import { safeMilestoneText } from "./chat-run-publications.js";
import { isExplicitExternalAgentComment } from "./issues.js";
describe("chat run milestone projection", () => {
it("allows only safe lifecycle state and text through the shared projection", () => {
expect(
projectSafeChatPublication({
classification: "external",
source: "safe_milestone",
text: "Maya is working…",
progressState: "working",
}),
).toEqual({ text: "Maya is working…", progressState: "working" });
expect(
JSON.stringify(
projectSafeChatPublication({
classification: "external",
source: "safe_milestone",
text: "Maya stopped before completing this turn.",
progressState: "failed",
}),
),
).not.toContain("stderr");
});
it("gives unlinked external identities a safe recovery path when isolation is unavailable", () => {
expect(
safeMilestoneText({
agentName: "Maya",
errorCode: "low_trust_isolation_unavailable",
milestone: "failed",
issueId: "issue-1",
publicBaseUrl: "https://paperclip.example/path",
}),
).toBe(
"Maya couldn't safely start this turn because this task was started for an unlinked external guest and isolated guest execution isn't available. Ask a Paperclip admin to create a private identity link for this account or enable isolated guest execution, then start a new task. Open the task in Paperclip: https://paperclip.example/issues/issue-1",
);
expect(
safeMilestoneText({
agentName: "Maya",
errorCode: "low_trust_isolation_unavailable",
milestone: "failed",
issueId: "issue-1",
publicBaseUrl: null,
}),
).toBe(
"Maya couldn't safely start this turn because this task was started for an unlinked external guest and isolated guest execution isn't available. Ask a Paperclip admin to create a private identity link for this account or enable isolated guest execution, then start a new task. Open the task in Paperclip for details.",
);
});
it("gives permanent integrity failures a safe operator recovery path", () => {
const text = safeMilestoneText({
agentName: "Maya",
errorCode: "native_event_replay_conflict",
milestone: "failed",
issueId: "issue-1",
});
expect(text).toBe(
"Maya couldn't safely continue this turn. A Paperclip admin needs to review the run before it can be retried. Open the task in Paperclip for details.",
);
expect(text).not.toMatch(/digest|source.seq|semantic|replay.conflict/i);
});
it("keeps every other run failure generic outside Paperclip", () => {
expect(
safeMilestoneText({
agentName: "Maya",
errorCode: "provider_secret_in_error_code",
milestone: "failed",
issueId: "issue-1",
}),
).toBe(
"Maya stopped before completing this turn. Open the task in Paperclip for details.",
);
});
it.each([
[null, " Open the task in Paperclip for details."],
[
"https://paperclip.example",
" Open the task in Paperclip: https://paperclip.example/issues/issue-1",
],
])(
"explains retained-session recovery without encouraging duplicate requests (%s)",
(publicBaseUrl, suffix) => {
const text = safeMilestoneText({
agentName: "Maya",
errorCode: "native_session_cleanup_quarantined",
milestone: "failed",
issueId: "issue-1",
publicBaseUrl,
});
expect(text).toBe(
"Maya couldn't start this turn because an earlier session needs recovery. Your request is saved. Ask a Paperclip admin to recover that session before retrying; sending the request again won't repair it." +
suffix,
);
expect(text).not.toMatch(
/quarantin|checkpoint|process|native_session|runnerId|reset/i,
);
expect(
projectSafeChatPublication({
classification: "external",
source: "safe_milestone",
text,
progressState: "failed",
}),
).toEqual({ text, progressState: "failed" });
},
);
it("describes ownership recovery without claiming the retained process stopped", () => {
expect(
safeMilestoneText({
agentName: "Maya",
errorCode: "native_execution_ownership_unverified",
milestone: "waiting_for_input",
issueId: "issue-1",
}),
).toBe(
"Maya needs a Paperclip admin to safely recover this turn before more work can start. Open the task in Paperclip for details.",
);
});
it.each(["server_shutdown_interrupted", "lease_released_before_terminal"])(
"keeps interruption bookkeeping for %s inside Paperclip",
(errorCode) => {
expect(
safeMilestoneText({
agentName: "Maya",
errorCode,
milestone: "failed",
issueId: "issue-1",
}),
).toBe(
"Maya stopped before completing this turn. Open the task in Paperclip for details.",
);
},
);
it("explains an allowlisted native provider capacity failure without exposing provider details", () => {
expect(
safeMilestoneText({
agentName: "Maya",
errorCode: "native_provider_usage_limit",
milestone: "failed",
issueId: "issue-1",
}),
).toBe(
"Maya couldn't complete this turn because the model provider's usage allowance is exhausted. A Paperclip admin needs to restore capacity before retrying. Open the task in Paperclip for details.",
);
});
it("confirms an explicit Slack Stop without claiming an unexpected failure", () => {
expect(
safeMilestoneText({
agentName: "Maya",
errorCode: "slack_session_stopped",
milestone: "failed",
issueId: "issue-1",
}),
).toBe("Maya stopped at your request.");
});
it("does not claim task completion when a successful run has no response", () => {
expect(
safeMilestoneText({
agentName: "Maya",
milestone: "completed",
issueId: "issue-1",
}),
).toBe("Maya has no final response to show yet. Open the task in Paperclip for details.");
});
});
describe("chat agent comment publication authorization", () => {
const metadata = (authorizationReason: string | null) => ({
version: 1 as const,
authorizationReason,
sections: [
{
title: "Authorization",
rows: [
{
type: "key_value" as const,
label: "Reason",
value: authorizationReason ?? "none",
},
],
},
],
});
it.each([
"paperclip_runner_protocol",
"allow_visible_issue_write",
"allow_scoped_agent_write",
"allow_chat_run_presentation",
])("allows an explicitly authored agent reply with reason %s", (reason) => {
expect(isExplicitExternalAgentComment(metadata(reason))).toBe(true);
});
it.each([
"internal_agent_write",
"execution_workspace_branch_reconcile",
"",
null,
])(
"keeps an internal agent comment with reason %s inside Paperclip",
(reason) => {
expect(isExplicitExternalAgentComment(metadata(reason))).toBe(false);
},
);
});