Files
paperclip/scripts/test-status-object-signed-sleep.mjs
DottaandPaperclip b8d94b3ad1 test(server): verify status and object deadlines across cold sleep (#19347)
## Thinking Path

> - Paperclip manages agents and their useful work.
> - Status cards and external objects can have future refresh deadlines.
> - A stopped tenant needs a durable Cloud alarm to return for those
deadlines.
> - A local quiet report alone does not prove that handoff or later
useful work.
> - Acceptance must exercise the normal producer, authorization, claims
and alarm owner.
> - This PR adds opt-in signed cold-owner acceptance and bounded
launcher controls.

## Linked Issues or Issue Description

Refs #19001 and #19191, which have merged. This PR targets master and
adds acceptance for the existing status and object owners.

**What happened?**

The existing local coverage did not join useful status generation or
built-in GitHub refresh with signed Cloud alarm persistence, physical
child retirement and cold reactivation at the original deadline.

**Expected behavior**

Keep the feature enabled. Complete useful work through its normal owner.
Persist the exact authorized alarm before stopping the child. Wake and
process the same original deadline without losing identity, claims,
retries or useful results.

**Steps to reproduce**

Use the checked-in Node24 launcher with separate owned loopback
App/Cloud test databases, a compiled Cloud checkout and the pinned
Node22 executable. Select one status or object bootstrap, cycle or
identity case. The command validates fresh evidence and database
ownership before starting its guarded worker.

**Paperclip version or commit**

Current candidate `ea911278b22c855d6722e43afc026bfc5c031405` normally
merges fetched master `07a548073ab48f13cb93b2fdc31553a5da88a070`. All
eight owned acceptance files are byte-identical to
`c08e64fa857d39bdb79352e94d156db7a4dbebf3`; all unrelated paths exactly
match master. The guard-argument repair and source-specific historical
cycle proof are preserved.

## What Changed

- Add one named opt-in case per owner/stage/diagnostic combination. Keep
the shared business body and its original deadlines, assertions and
cleanup.
- Drive actual managed App startup, authenticated producers, persisted
claims and the signed Cloud SQL alarm before physical child stop.
- Retain the generated five-secret fixture profile across cold boots.
Store it in private directories and retain only names, hashes and modes
in evidence.
- Add optional query timestamps to the existing proxy and harness.
Existing callers keep their default behavior.
- Add launcher refusal controls and assert its complete child argument
list. Both explicit guard preloads and the exact child `NODE_OPTIONS`
must match.

## Verification

- The current preflight repair has causal proof: the old controls pass
all nine cases with an invalid worker preload flag. The new assertions
reject that same command in both success controls (two failures/seven
passes). Restoring the correct command passes all nine cases. The
original refusal, freshness, symlink and byte-preservation assertions
remain unchanged. All 33 recorded Node PIDs and three groups close;
temporary copies and private directories are removed. These controls
stop at the declared spawn stub. They verify the full command contract
and do not claim to execute Vitest.
- The prior real named status bootstrap passed in 72.942 seconds: one
selected case/six explicit skips. It exercised the actual launcher and
Vitest fork, useful normal generation, current authorization, settled
claims, original 900000ms interval and signed SQL alarm
generation/operation-lock acknowledgement before physical child stop.
Recorded processes and final SQL sessions closed; both successful
databases were normally dropped.
- Earlier normal status and GitHub-object cycles qualified their real
900-second and 300-second original deadlines, cold reactivation and
useful second results. Their complete original failure and recovery
records remain source-specific; no shortened business clock or
manufactured completion is used.
- On current integrated head `ea911278`, own frozen offline setup and
normal shared/SDK, Runner and App declaration prerequisites passed. The
status test and its imported closure pass the affected normal test-tree
compiler program with original options/references. The actual named
normal status bootstrap then passed: one selected case/six skips, 82.202
seconds in Vitest and 85.616 seconds in the launcher. It used the real
checked-in launcher, normal App/SDK/process agent and exact compiled
Cloud `389878364b42dd129835d263f91882c5774d5123`. The original 900000ms
interval and 120000ms alarm lead remain unchanged; useful v1 and signed
SQL alarm generation/operation-lock acknowledgement precede physical App
stop. All six recorded process IDs and the runtime group were absent;
independent final test-database sessions were zero, with no observer
during execution. Both fresh success databases were normally dropped.
This refreshes the changed consumer seams through handoff and stop; it
is not another cold-wake/useful-v2/full-cycle result. Historical cycles
remain tied to their measured source.
- Fresh exact-head CI and complete edited bot reviews are required
before merge. A whole final-tree local build/test or deployed suspension
is not claimed. Local provider, process and S3 boundaries are controlled
fixtures; no live GitHub provider or Railway/Neon suspension claim
follows.

## Risks

- These tests use real SQL and wall clocks and are opt-in. Their
fixtures must stay isolated and retain failed evidence.
- Stable fixture secrets must remain private and must not appear in
logs. Cleanup keeps them when owner closure is uncertain.
- The acceptance body stays shared to preserve measured evidence. The
named registration and command controls make selection failures visible.
- This PR does not change production, schema or feature enablement.

## Model Used

OpenAI `gpt-6.1-sol` produced the delegated acceptance work. OpenAI
Codex, based on GPT-6, performed managing review, source integration and
the preflight argument repair. Both used repository tools and code
execution. The coordinator's exact backend snapshot and context-window
size were not exposed.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used
- [x] I have checked ROADMAP.md and confirmed this does not duplicate
planned core work
- [x] I have searched related PRs and linked the existing foundations
- [x] I have described the issue with the bug-template fields
- [x] I have not referenced internal or instance-local issues or links
- [x] My branch name describes the change
- [x] I have run the stated focused local tests and preserved their
limits
- [x] I have added or updated tests
- [x] I have documented the opt-in command and evidence requirements
- [x] I have documented risks
- [ ] All current-head CI gates are green
- [ ] Current-head Greptile is 5/5 with no open findings
- [x] I will address reviewer findings before merge

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-06 18:15:56 -06:00

77 lines
5.7 KiB
JavaScript

import { createRequire } from 'node:module';
import { spawn } from 'node:child_process';
import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = fileURLToPath(new URL('../', import.meta.url));
const args = process.argv.slice(2);
if (args.length !== 2 || !/^--owner=(status|object)$/.test(args[0]) || !/^--stage=(bootstrap|cycle|identity)$/.test(args[1])) {
throw new Error('Use --owner=status|object --stage=bootstrap|cycle, or --owner=status --stage=identity');
}
if (args[1] === '--stage=identity' && args[0] !== '--owner=status') throw new Error('Identity control requires the status owner');
const diagnostic = process.env.PAPERCLIP_STATUS_OBJECT_SLEEP_DIAGNOSTIC;
const diagnosticOwner = args[0] === '--owner=object' && args[1] === '--stage=bootstrap'
|| args[0] === '--owner=status' && args[1] === '--stage=identity';
if (diagnostic !== undefined && (diagnostic !== '1' || !diagnosticOwner)) {
throw new Error('Diagnostic capture requires object bootstrap or status identity and the explicit value 1');
}
if (!process.version.startsWith('v24.')) throw new Error('Use the actual Node24 executable');
if (process.env.PAPERCLIP_MAINTENANCE_PG0_CONTROLS) throw new Error('PG0 flag is forbidden for signed owner acceptance');
const names = ['PAPERCLIP_SKILL_DEFAULT_CADENCE_TEST_DATABASE_URL', 'PAPERCLIP_SKILL_DEFAULT_CADENCE_CLOUD_TEST_DATABASE_URL'];
const urls = names.map(name => {
const value = process.env[name];
if (!value) throw new Error(`${name} requires an explicit dedicated database`);
const url = new URL(value);
if (!['postgres:', 'postgresql:'].includes(url.protocol) || url.hostname !== '127.0.0.1' || url.port !== '55448'
|| !/^[a-zA-Z0-9_]+_test$/.test(decodeURIComponent(url.pathname.slice(1))) || url.search || url.hash) {
throw new Error(`${name} requires the exact owned loopback database without overrides`);
}
return url;
});
if (urls[0].pathname === urls[1].pathname) throw new Error('App and Cloud fixtures must differ');
if (process.env.PAPERCLIP_TEST_POSTGRES_ADMIN_URL) {
const admin = new URL(process.env.PAPERCLIP_TEST_POSTGRES_ADMIN_URL);
if (!['postgres:', 'postgresql:'].includes(admin.protocol) || admin.hostname !== '127.0.0.1' || admin.port !== '55448'
|| admin.pathname !== '/postgres' || admin.search || admin.hash) throw new Error('Admin URL must be the same loopback55448/postgres cluster');
}
const cloud = process.env.PAPERCLIP_SKILL_DEFAULT_CADENCE_CLOUD_CHECKOUT;
if (!cloud || !path.isAbsolute(cloud) || !existsSync(path.join(cloud, 'dist/sleep/fixtures/default-cadence-bridge.js'))) throw new Error('Set exact compiled Cloud checkout');
if (!path.isAbsolute(process.env.PAPERCLIP_SKILL_DEFAULT_CADENCE_CLOUD_NODE || '')) throw new Error('Set exact Node22 executable');
if (!/^[a-f0-9]{40}$/.test(process.env.PAPERCLIP_SKILL_DEFAULT_CADENCE_CLOUD_SHA || '')) throw new Error('Set exact Cloud source SHA');
const evidence = process.env.PAPERCLIP_SKILL_DEFAULT_CADENCE_EVIDENCE;
if (!evidence || !path.isAbsolute(evidence) || !existsSync(evidence) || !lstatSync(evidence).isDirectory()) {
throw new Error('Set an existing absolute evidence directory without a symlink');
}
const log = process.env.PAPERCLIP_SKILL_DEFAULT_CADENCE_GUARD_LOG;
const logStat = log && path.isAbsolute(log) ? lstatSync(log, { throwIfNoEntry: false }) : undefined;
if (!log || !path.isAbsolute(log) || !existsSync(path.dirname(log))
|| logStat && (!logStat.isFile() || logStat.size !== 0)) throw new Error('Set a fresh absolute guard log');
for (const entry of readdirSync(evidence)) {
const existing = path.join(evidence, entry);
if (path.resolve(existing) !== path.resolve(log) || !lstatSync(existing).isFile() || statSync(existing).size !== 0) {
throw new Error('Owner acceptance requires a fresh evidence directory; existing artifacts must be preserved');
}
}
const require = createRequire(new URL('../server/package.json', import.meta.url));
const guard = path.join(root, 'scripts/fixtures/skill-default-cadence-network-guard.cjs');
delete process.env.DATABASE_URL;
delete process.env.PAPERCLIP_SKILL_DEFAULT_CADENCE_GUARD_ORIGINS;
require(guard);
const vitest = path.join(path.dirname(require.resolve('vitest/package.json')), 'vitest.mjs');
const env = { PATH: process.env.PATH, HOME: process.env.HOME, LANG: process.env.LANG, TMPDIR: process.env.TMPDIR,
NODE_ENV: 'test', NODE_OPTIONS: `--require=${guard}`, PAPERCLIP_SKILL_DEFAULT_CADENCE_GUARD_LOG: log,
PAPERCLIP_STATUS_OBJECT_SLEEP_ACCEPTANCE: '1', PAPERCLIP_STATUS_OBJECT_SLEEP_OWNER: args[0].slice(8),
PAPERCLIP_STATUS_OBJECT_SLEEP_STAGE: args[1].slice(8), PAPERCLIP_MAINTENANCE_KEEP_ARTIFACTS: '1' };
for (const name of [...names, 'PAPERCLIP_STATUS_OBJECT_SLEEP_DIAGNOSTIC', 'PAPERCLIP_SKILL_DEFAULT_CADENCE_CLOUD_CHECKOUT', 'PAPERCLIP_SKILL_DEFAULT_CADENCE_CLOUD_NODE',
'PAPERCLIP_SKILL_DEFAULT_CADENCE_CLOUD_SHA', 'PAPERCLIP_SKILL_DEFAULT_CADENCE_EVIDENCE', 'PAPERCLIP_TEST_POSTGRES_ADMIN_URL']) {
if (process.env[name]) env[name] = process.env[name];
}
const child = spawn(process.execPath, ['--require', guard, vitest, 'run', 'server/src/__tests__/status-object-signed-sleep-acceptance.test.ts',
'--maxWorkers=1', '--no-file-parallelism', `--execArgv=--require=${guard}`], { cwd: root, env, stdio: 'inherit' });
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => child.kill(signal));
child.once('error', error => { console.error(error); process.exitCode = 1; });
child.once('close', (code, signal) => {
const events = readFileSync(log, 'utf8').trim().split('\n').filter(Boolean).map(line => JSON.parse(line));
process.exitCode = events.some(event => event.event === 'blocked' || event.unexpectedAttempts?.length) ? 98 : signal ? 1 : code ?? 1;
});