mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 01:43:25 +08:00
fix: surface actionable channel message errors
This commit is contained in:
parent
b42fc411c2
commit
7cbed3aefb
59 changed files with 2405 additions and 607 deletions
|
|
@ -5,7 +5,10 @@ import { join } from 'node:path';
|
|||
import test from 'node:test';
|
||||
|
||||
import { createBotWorkspaceScope } from '../../../src/channels/shared/bot-workspace-store.mjs';
|
||||
import { HarnessClient } from '../../../src/channels/shared/harness-client.mjs';
|
||||
import {
|
||||
HarnessClient,
|
||||
HarnessTurnError,
|
||||
} from '../../../src/channels/shared/harness-client.mjs';
|
||||
import {
|
||||
OUTBOUND_ARTIFACT_TOOL,
|
||||
createOutboundArtifactTool,
|
||||
|
|
@ -313,6 +316,78 @@ function controlledTurn({ sessionId, initialEnd = false, controlExecutor } = {})
|
|||
};
|
||||
}
|
||||
|
||||
test('HarnessClient preserves structured turn failures for channel classification', async () => {
|
||||
for (const [reason, expectedCode, expectedProviderCode] of [
|
||||
[{ kind: 'error', error: { code: 'RATE_LIMIT', message: 'private provider detail' } },
|
||||
'harness-turn-failed', 'RATE_LIMIT'],
|
||||
[{ kind: 'max-tokens' }, 'model-max-tokens', undefined],
|
||||
[{ kind: 'completed' }, 'model-empty-response', undefined],
|
||||
['completed', 'model-empty-response', undefined],
|
||||
['stopped', 'turn-interrupted', undefined],
|
||||
]) {
|
||||
const turn = controlledTurn();
|
||||
const asking = turn.client.ask(turn.id, 'work', { timeoutMs: 2_000 });
|
||||
await turn.admitted;
|
||||
turn.finish({ reason });
|
||||
await assert.rejects(asking, (error) => {
|
||||
assert.ok(error instanceof HarnessTurnError);
|
||||
assert.equal(error.code, expectedCode);
|
||||
assert.equal(error.providerCode, expectedProviderCode);
|
||||
assert.equal(error.promptAccepted, true);
|
||||
assert.doesNotMatch(error.message, /private provider detail/);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('HarnessClient does not treat partial text from a failed turn as success', async () => {
|
||||
for (const [reason, expectedCode, expectedProviderCode] of [
|
||||
[{ kind: 'error', error: { code: 'RATE_LIMIT' } },
|
||||
'harness-turn-failed', 'RATE_LIMIT'],
|
||||
[{ kind: 'error', error: { code: 'CONTENT_FILTER' } },
|
||||
'harness-turn-failed', 'CONTENT_FILTER'],
|
||||
[{ kind: 'max-tokens' }, 'model-max-tokens', undefined],
|
||||
[{ kind: 'blocked' }, 'turn-blocked', undefined],
|
||||
['interrupted', 'turn-interrupted', undefined],
|
||||
['stopped', 'turn-interrupted', undefined],
|
||||
['cancelled', 'turn-interrupted', undefined],
|
||||
['aborted', 'turn-aborted', undefined],
|
||||
]) {
|
||||
const turn = controlledTurn();
|
||||
const asking = turn.client.ask(turn.id, 'work', { timeoutMs: 2_000 });
|
||||
await turn.admitted;
|
||||
turn.finish({ text: 'partial result must not escape', reason });
|
||||
await assert.rejects(asking, (error) => {
|
||||
assert.ok(error instanceof HarnessTurnError);
|
||||
assert.equal(error.code, expectedCode);
|
||||
assert.equal(error.providerCode, expectedProviderCode);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('HarnessClient accepts partial text only for completed or omitted end reasons', async () => {
|
||||
for (const reason of ['completed', { kind: 'completed' }, null]) {
|
||||
const turn = controlledTurn();
|
||||
const asking = turn.client.ask(turn.id, 'work', { timeoutMs: 2_000 });
|
||||
await turn.admitted;
|
||||
turn.finish({ text: 'valid partial result', reason });
|
||||
assert.equal(await asking, 'valid partial result');
|
||||
}
|
||||
});
|
||||
|
||||
test('HarnessClient marks a reply timeout after prompt admission', async () => {
|
||||
const turn = controlledTurn();
|
||||
const asking = turn.client.ask(turn.id, 'work', { timeoutMs: 1 });
|
||||
await turn.admitted;
|
||||
await assert.rejects(asking, (error) => {
|
||||
assert.ok(error instanceof HarnessTurnError);
|
||||
assert.equal(error.code, 'harness-reply-timeout');
|
||||
assert.equal(error.promptAccepted, true);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test('control methods require exact owner identity, key, and Session before any RPC', async () => {
|
||||
const turn = controlledTurn();
|
||||
const owner = {};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue