Files
PaperClipAI/server/scripts/test-slack-sync-protocol.mjs
T
c82211251b test(chat): cover Slack forwarding deadlines through registered owners (#19348)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People answer agent questions through Slack commands, buttons, and
forms.
> - Slack requires a response within three seconds. A modal trigger also
expires after three seconds and can be used once.
> - Durable event delivery does not prove these synchronous response
paths.
> - This pull request exercises the registered app owners and pinned SDK
with original signed requests and controlled forwarding delays.
> - The tests record deadline failures without clearing invalid forms,
extending triggers, or changing production behavior.

## Linked Issues or Issue Description

Refs #19019, which has merged. This PR now targets `master`.

Related work: #8300 handles raw-body capture and plugin event fan-out.
#19328 handles Slack connection onboarding. This PR changes neither
path. I searched related open and closed work and read those PR bodies
and changed paths.

**What happened?**
A forwarding delay can leave an authenticated Slack callback too late to
acknowledge a command, exchange a modal trigger, or return field errors.
The existing direct modal test did not cover the normal registered
ingress or these provider deadlines.

**Expected behavior**
Tests must distinguish useful app persistence from a response that Slack
can still accept. They must preserve current authorization, field
validation, original signatures, and exactly-once effects.

**Steps to reproduce**
Use an owned PostgreSQL test database and the repository launcher below.
Run warm first. The default run adds one real 3500ms delay independently
before command, form-open action, and invalid submission forwarding.

**Paperclip version or commit**
Production remains byte-identical to
`54fa701d41b4ced62f4ff338d39b946015670ae3`. This test-only candidate is
`a3cce4e9868fd3e6e25d9df7d0090899bdf6a204`. It adds a recovery-fixture
setup guard after `dac0010649c435f673172c496b3383e99780e8bf`.

**Deployment mode**
Source-checkout integration test with real PostgreSQL, registered app
owners, and the pinned Slack SDK. Provider API responses and the
heartbeat receipt helper are controlled fixtures. No Slack account,
model process, deployment, or physical tenant suspension was tested.

## What Changed

- Route original signed command, action, and modal requests through the
normal chat service and real SDK runtime.
- Check forged-signature refusal, callback evidence, current-role
refusal, invalid-field errors, one corrected answer, and duplicate
fencing.
- Intercept the pinned SDK HTTP transport. Model trigger expiry and
one-use exchange without changing request timestamps or signatures.
- Observe the real platform exception through the actual bound
`views.open` callable. Delegate once and rethrow the same error.
- Restore the network guard after every setup failure. Attempt all owned
cleanup and preserve the original error before any cleanup errors.
- Guard auxiliary database cleanup when recovery test setup fails before
it creates a client. Keep temporary-cluster cleanup reachable and
preserve the original setup error.
- Add a repository launcher with the current prewire guard, one worker,
explicit test-database validation, and closed warm-only and
action-delayed-only selectors. The default still selects all four cases.

## Verification

- Current candidate `c6ae7957988e220ea096ab678f070d3bd1c5f9c0` normally
merges fetched master `8621bcf53ee2e4bbefb7d193add42dd544ca06e8`. All
three owned protocol/recovery acceptance files are byte-identical to the
preceding head. All other paths exactly match master. The shared Runner
default test conflict uses master's independently landed constant-based
repair. That supersedes this branch's temporary two-literal repair; no
fourth file remains in the PR diff.
- Original CI server4/12 had two stale `gpt-5.6-sol` expectations after
the normal default changed to `gpt-6.1-sol`. The complete failure log is
retained. Before the master repair landed, the focused two-literal
correction passed the whole six-case file with explicit-model,
non-Codex, lifecycle, permission and skill checks intact. That local
result is source-specific; it is not a fresh full merged-tree pass.
- The original guard-leak counterfactual reproduced unrestored global
network functions after setup rejection. The repaired callback passed
four controlled cleanup diagnostics. Cleanup attempts each initialized
owner and restoration even after a failure. It preserves the primary
error first and retains separate cleanup errors.
- A fresh registered SQL/SDK warm case passed on the qualified protocol
source with one persisted answer, 17.15 seconds total and normal
original assertions. Final independent SQL and recorded PID/group audits
were empty. A diagnostic sampler initially hit EPERM, and the worker
exit guard line was absent; those limits remain explicit. The cleanup
diagnostics are local qualification tools, not four newly checked-in CI
tests.
- Delayed modes remain explicit diagnostic commands. Ordinary CI runs
warm. Earlier mode results do not establish a final-source four-case
single-command pass. The provider transport is controlled; no live Slack
display, deployed sleep or wake result is claimed.
- The original server6/12 cleanup-backlog case hit its 15-second
whole-case timeout. Its phase and cause remain unknown. The original E2E
shard received runner shutdown without an assertion verdict. Complete
original logs are retained; neither failure has been labeled a flake or
repaired by extending a deadline.
- Own offline dependency setup passed with an unchanged lockfile. An
omitted private guard dependency initially prevented test entry; that
setup failure is retained separately from product results. The immutable
helper closure corrected it. No local full-repository
typecheck/build/test pass is claimed. Fresh exact-head CI and complete
edited bot reviews are required before merge.

## Risks

This is a test-only change. It changes no production code, credential
flow, schema, API, or sleep eligibility. The existing large integration
fixture remains large; this PR does not cross a file from below 1000
lines to above it and adds no code comments.

The provider transport is synthetic. The tests demonstrate deadline
consequences and app persistence, not that Slack displayed a late
response. They do not make commands or open forms safe while the tenant
sleeps. Other Slack lanes and persistent transports remain open work. CI
may expose additional harness assumptions.

The roadmap's Apps and Connections work is unchanged. This focused
regression complements the existing integration; it adds no separate
feature or provider setup path.

## Model Used

OpenAI, configured model `gpt-6.1-sol` in Codex. The exact served
backend revision and context window are not exposed. Assistance used
reasoning, source inspection, Git, native TypeScript checks, and
controlled integration tests. A coordinating GPT-6-family agent reviewed
boundaries and evidence.

## 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>
Co-authored-by: Codex <noreply@openai.com>
2026-11-06 17:41:04 -06:00

45 lines
2.8 KiB
JavaScript

import { spawn } from 'node:child_process';
import { createRequire } from 'node:module';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const server = fileURLToPath(new URL('../', import.meta.url));
const require = createRequire(new URL('../package.json', import.meta.url));
const guard = path.join(server, 'src/__tests__/helpers/slack-cloud/prewire.cjs');
const args = process.argv.slice(2);
if (args.length > 1 || args.length === 1 && !['--warm-only', '--action-delayed-only'].includes(args[0])) throw new Error('Use --warm-only, --action-delayed-only or no arguments');
const mode = args[0] === '--warm-only' ? 'warm' : 'action-delayed';
const testNamePattern = args.length === 1
? `captures signed Slack forwarding protocol with registered owners \\(${mode}\\)$`
: 'captures signed Slack forwarding protocol with registered owners';
const names = ['PAPERCLIP_SLACK_TEST_DATABASE_URL'];
for (const name of names) {
if (!process.env[name]) throw new Error(`Set ${name} to an owned dedicated PostgreSQL test database`);
let url;
try { url = new URL(process.env[name]); } catch { throw new Error(`${name} must name an explicit loopback *_test database`); }
if (!['postgres:', 'postgresql:'].includes(url.protocol) || !['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname)
|| !/^[a-zA-Z0-9_]+_test$/.test(decodeURIComponent(url.pathname.slice(1)))
|| ['options', 'database', 'dbname'].some(k => url.searchParams.has(k))) throw new Error(`${name} must name an explicit loopback *_test database`);
}
const vitest = path.join(path.dirname(require.resolve('vitest/package.json')), 'vitest.mjs');
const ownedHome = mkdtempSync(path.join(tmpdir(), 'paperclip-slack-protocol-'));
const env = {
PATH: process.env.PATH, HOME: process.env.HOME, CODEX_HOME: process.env.CODEX_HOME,
LANG: process.env.LANG, TMPDIR: ownedHome, PAPERCLIP_HOME: ownedHome,
NODE_ENV: 'test', NODE_OPTIONS: '', SLACK_GATE_EXPECT_PRELOAD: '1',
PAPERCLIP_SLACK_TEST_DATABASE_URL: process.env.PAPERCLIP_SLACK_TEST_DATABASE_URL,
PAPERCLIP_SLACK_PROTOCOL_CAPTURE: '1',
SLACK_GATE_PROCESS_LOG: process.env.SLACK_GATE_PROCESS_LOG,
};
const child = spawn(process.execPath, ['--require', guard, vitest, 'run',
'src/__tests__/chat-channels.integration.test.ts', '--testNamePattern', testNamePattern, '--maxWorkers=1', '--no-file-parallelism', `--execArgv=--require=${guard}`],
{ cwd: server, env, stdio: 'inherit' });
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => child.kill(signal));
child.on('error', error => { console.error(error); process.exitCode = 1; });
child.on('close', (code, signal) => {
rmSync(ownedHome, { recursive: true, force: true });
process.exitCode = signal ? 1 : code ?? 1;
});