mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 22:37:03 +00:00
3595a3c034
The server has taken `op_key` on copy and push since TASK-3585; the web dialogs never sent one. Each opening of CopyItemDialog and PushToAgentDialog now mints a key, so a double dispatch replays the first answer instead of copying or publishing twice. A refusal releases the key server-side, so a corrected resend reuses it. A targeted push miss is a completed answer the server stores, so the push dialog mints a new key before it re-arms; otherwise the resend would replay the miss, or 422 for a new target. The client methods take an optional opKey and add `op_key` to the body only when given, so keyless bodies are byte-identical (pinned in pushCopyOpKey.test.ts). Comments that said the copy or push endpoint has no idempotency key now say the call site sends none. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
// TASK-3597 — the copy and push calls carry the server's idempotency key as
|
|
// `op_key` in the body (TASK-3585), and only when the caller passed one: a
|
|
// keyless call keeps the exact body it always sent.
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { api } from './client';
|
|
|
|
function okFetch(body: unknown) {
|
|
return vi.fn(
|
|
async (_url: string, _init?: RequestInit) =>
|
|
new Response(JSON.stringify(body), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
);
|
|
}
|
|
|
|
function sentBody(fn: ReturnType<typeof okFetch>): unknown {
|
|
const init = fn.mock.calls[0]?.[1];
|
|
return JSON.parse(String(init?.body));
|
|
}
|
|
|
|
beforeEach(() => vi.unstubAllGlobals());
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
describe('api.items.push body', () => {
|
|
const ok = { ref: 'TASK-5', workspace: 'ws', pushed: true, message: 'ok' };
|
|
|
|
it('without a target or key: exactly { message }, the pre-S5 broadcast body', async () => {
|
|
const fn = okFetch(ok);
|
|
vi.stubGlobal('fetch', fn);
|
|
await api.items.push('ws', 'task-5', 'hi');
|
|
expect(sentBody(fn)).toStrictEqual({ message: 'hi' });
|
|
});
|
|
|
|
it('an undefined target sends no target_session_id key at all', async () => {
|
|
const fn = okFetch(ok);
|
|
vi.stubGlobal('fetch', fn);
|
|
await api.items.push('ws', 'task-5', 'hi', undefined, 'k-1');
|
|
expect(sentBody(fn)).toStrictEqual({ message: 'hi', op_key: 'k-1' });
|
|
});
|
|
|
|
it('target and key both ride in the body', async () => {
|
|
const fn = okFetch(ok);
|
|
vi.stubGlobal('fetch', fn);
|
|
await api.items.push('ws', 'task-5', 'hi', 'sess-1', 'k-1');
|
|
expect(sentBody(fn)).toStrictEqual({
|
|
message: 'hi',
|
|
target_session_id: 'sess-1',
|
|
op_key: 'k-1'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('api.items.copy body', () => {
|
|
const req = { target_workspace: 'dst', target_collection: 'tasks' };
|
|
|
|
it('without a key: the request exactly as given', async () => {
|
|
const fn = okFetch({});
|
|
vi.stubGlobal('fetch', fn);
|
|
await api.items.copy('ws', 'task-5', req as never);
|
|
expect(sentBody(fn)).toStrictEqual(req);
|
|
});
|
|
|
|
it('with a key: the request plus op_key', async () => {
|
|
const fn = okFetch({});
|
|
vi.stubGlobal('fetch', fn);
|
|
await api.items.copy('ws', 'task-5', req as never, 'k-2');
|
|
expect(sentBody(fn)).toStrictEqual({ ...req, op_key: 'k-2' });
|
|
});
|
|
});
|