Files
Codeman/test/agent-skill-sendwait.test.ts
T
2fe9105bab fix(skill): sendwait joins a multi-line prompt onto one line
sendwait posted its prompt as is. With the mux now keeping line breaks, a
multi-line prompt goes in as a paste, and right after spawn_worker returns it
can land inside Claude Code's Enter-ignore window. Neither the Enter loop
here nor the server's submit check recognises a pasted block that is still on
the composer, so `Review this:\n- item 1` sat unsent while the re-wait ran
out. sendwait now trims each line, drops blank ones and joins the rest with a
space before building the body, the same result as toSingleLine, like every
other waiting sender.

The preamble stamp moves to 1.43.1 so cached preambles pick the change up;
the plugin mirror is regenerated with scripts/sync-plugin.mjs. The new test
runs the real preamble.sh with CURL stubbed and fails without the join.
docs/api-reference.md gets the submit-check sentence the README and wiki
already carry.

Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-21 23:31:05 +02:00

81 lines
2.9 KiB
TypeScript

/**
* @fileoverview `sendwait` in the packaged agent preamble sends one line.
*
* Runs the REAL `skills/codeman/preamble.sh` in bash with `CURL` replaced by a stub
* that records the request body, so it checks what the function puts on the wire,
* not a copy of its logic. The server pastes a multi-line prompt as one block, and
* neither sendwait's Enter loop nor the server's submit check can recognise a pasted
* block still sitting on the composer, so the prompt must arrive as one line, the
* way `toSingleLine` joins it for the other unattended senders.
*
* Port: N/A (no server; the stub answers every request).
*/
import { describe, it, expect } from 'vitest';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { toSingleLine } from '../src/utils/single-line.js';
const PREAMBLE = join(__dirname, '..', 'skills', 'codeman', 'preamble.sh');
// The preamble builds every request body with jq.
let hasJq = true;
try {
execFileSync('jq', ['--version'], { stdio: 'ignore' });
} catch {
hasJq = false;
}
/** The `input` field of the first body sendwait posts for `prompt`. */
function sentInput(prompt: string): string {
// sendwait calls CURL inside $(...), a subshell, so the stub records to a file.
const dir = mkdtempSync(join(tmpdir(), 'codeman-sendwait-'));
const bodies = join(dir, 'bodies');
const script = `
. "$PREAMBLE" >/dev/null
stub() {
while [ "$#" -gt 0 ]; do
if [ "$1" = --data-binary ]; then printf '%s\\n' "$2" >> "$BODIES"; shift; fi
shift
done
printf '%s' '{"data":{"delivered":true,"wait":{"timedOut":false}}}'
}
CURL=(stub)
sendwait worker1234 "$PROMPT" 7 >/dev/null
`;
try {
execFileSync('bash', ['-c', script], {
env: {
PATH: process.env.PATH,
HOME: process.env.HOME,
PREAMBLE,
PROMPT: prompt,
BODIES: bodies,
CODEMAN_API_URL: 'http://127.0.0.1:9',
CODEMAN_SESSION_ID: 'caller-session-0000',
},
});
const first = readFileSync(bodies, 'utf-8').split('\n')[0];
return (JSON.parse(first) as { input: string }).input;
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
describe.skipIf(!hasJq)('preamble sendwait', () => {
it('joins a multi-line prompt onto one line before sending it', () => {
expect(sentInput('Review this:\n- item 1\n- item 2')).toBe('Review this: - item 1 - item 2\r');
});
it('joins the same way as toSingleLine (trimmed lines, blank lines dropped, \\r\\n as a break)', () => {
const prompt = ' first line \r\n\n\t second\n\n \nthird\n';
expect(sentInput(prompt)).toBe(toSingleLine(prompt) + '\r');
});
it('leaves a single-line prompt, glob characters included, as it was', () => {
expect(sentInput('fix * in ?.ts ')).toBe('fix * in ?.ts \r');
});
});