mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
225 lines
6.5 KiB
JavaScript
225 lines
6.5 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
|
|
const {
|
|
createStepRunRecord,
|
|
transitionStepRunMutation,
|
|
} = require('@qinglong/runtime-core/step-run');
|
|
const {
|
|
InvalidModelInvocationError,
|
|
createModelInvocationCompletionCommand,
|
|
createModelInvocationMutationIdentity,
|
|
createModelInvocationStartCommand,
|
|
normalizeModelInvocationCompletionCommand,
|
|
normalizeModelInvocationStartCommand,
|
|
} = require('../dist/model-invocation/modelInvocation.js');
|
|
|
|
const NOW = 1_000_000;
|
|
|
|
function readyStepRun() {
|
|
return createStepRunRecord({
|
|
id: 'step-a',
|
|
runId: 'run-a',
|
|
stepKey: 'summarize',
|
|
kind: 'model',
|
|
definitionRef: 'prompt:summary@1',
|
|
definitionDigest: 'a'.repeat(64),
|
|
required: true,
|
|
initialStatus: 'ready',
|
|
inputRef: 'artifact:input-a',
|
|
mutationId: 'create-step-a',
|
|
createdAtMs: NOW - 1,
|
|
});
|
|
}
|
|
|
|
function audit(phase, overrides = {}) {
|
|
return {
|
|
phase,
|
|
projectId: 'project-a',
|
|
runId: 'run-a',
|
|
stepRunId: 'step-a',
|
|
traceId: 'trace-a',
|
|
requestId: 'request-a',
|
|
provider: 'remote',
|
|
model: 'model-a',
|
|
policyRevision: 'policy-1',
|
|
requestDigest: `sha256:${'b'.repeat(64)}`,
|
|
deadlineAtMs: NOW + 10_000,
|
|
inputBytes: 128,
|
|
maxOutputTokens: 64,
|
|
outputBytes: 0,
|
|
usage: null,
|
|
errorCode: null,
|
|
occurredAtMs: NOW,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function startFixture(requestId = 'request-a') {
|
|
const current = readyStepRun();
|
|
const identity = createModelInvocationMutationIdentity(requestId, 'start');
|
|
const mutation = transitionStepRunMutation(
|
|
current,
|
|
{
|
|
expectedVersion: current.version,
|
|
expectedDigest: current.stepRunDigest,
|
|
mutationId: identity.mutationId,
|
|
to: 'running',
|
|
atMs: NOW,
|
|
},
|
|
{
|
|
expectedRunVersion: 1,
|
|
expectedRunEventSequence: 1,
|
|
eventId: identity.eventId,
|
|
dedupeKey: identity.dedupeKey,
|
|
actor: { type: 'executor', id: 'model-gateway' },
|
|
},
|
|
);
|
|
return createModelInvocationStartCommand(
|
|
audit('admitted', { requestId }),
|
|
mutation,
|
|
);
|
|
}
|
|
|
|
function completionFixture({ requestId = 'request-a', errorCode = null } = {}) {
|
|
const startCommand = startFixture(requestId);
|
|
const identity = createModelInvocationMutationIdentity(
|
|
requestId,
|
|
'completion',
|
|
);
|
|
const failed = errorCode !== null;
|
|
const timedOut = errorCode === 'MODEL_INVOCATION_DEADLINE_EXCEEDED';
|
|
const lost =
|
|
errorCode === 'MODEL_INVOCATION_ABORTED' ||
|
|
errorCode === 'MODEL_STREAM_CANCELLED';
|
|
const status = timedOut
|
|
? 'timed_out'
|
|
: lost
|
|
? 'lost'
|
|
: failed
|
|
? 'failed'
|
|
: 'succeeded';
|
|
const mutation = transitionStepRunMutation(
|
|
startCommand.stepRunMutation.stepRun,
|
|
{
|
|
expectedVersion: startCommand.start.startedStepRunVersion,
|
|
expectedDigest: startCommand.start.startedStepRunDigest,
|
|
mutationId: identity.mutationId,
|
|
to: status,
|
|
atMs: NOW + 25,
|
|
...(failed
|
|
? {
|
|
resultCode: timedOut
|
|
? 'model_deadline_exceeded'
|
|
: lost
|
|
? 'model_outcome_unknown'
|
|
: 'model_provider_failed',
|
|
errorSummary: timedOut
|
|
? 'Model invocation deadline exceeded'
|
|
: lost
|
|
? 'Model invocation outcome is unknown'
|
|
: 'Model invocation failed',
|
|
}
|
|
: { outputRef: `model-invocation:${requestId}` }),
|
|
},
|
|
{
|
|
expectedRunVersion: 2,
|
|
expectedRunEventSequence: 2,
|
|
eventId: identity.eventId,
|
|
dedupeKey: identity.dedupeKey,
|
|
actor: { type: 'executor', id: 'model-gateway' },
|
|
},
|
|
);
|
|
const completionAudit = audit(failed ? 'failed' : 'completed', {
|
|
requestId,
|
|
occurredAtMs: NOW + 25,
|
|
outputBytes: failed ? 0 : 12,
|
|
usage: failed ? null : { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
|
|
errorCode,
|
|
});
|
|
return createModelInvocationCompletionCommand(
|
|
startCommand.start,
|
|
completionAudit,
|
|
mutation,
|
|
);
|
|
}
|
|
|
|
test('model invocation reuses one StepRun mutation chain without content', () => {
|
|
const start = startFixture();
|
|
const completion = completionFixture();
|
|
|
|
assert.deepEqual(normalizeModelInvocationStartCommand(start), start);
|
|
assert.deepEqual(
|
|
normalizeModelInvocationCompletionCommand(completion),
|
|
completion,
|
|
);
|
|
assert.equal(start.stepRunMutation.previousStatus, 'ready');
|
|
assert.equal(start.stepRunMutation.stepRun.status, 'running');
|
|
assert.equal(completion.stepRunMutation.previousStatus, 'running');
|
|
assert.equal(completion.stepRunMutation.stepRun.status, 'succeeded');
|
|
assert.equal(completion.completion.outcome, 'succeeded');
|
|
assert.equal(
|
|
JSON.stringify({ start, completion }).includes('top secret prompt'),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('deadline completion is terminal timed_out and not replayable', () => {
|
|
const completion = completionFixture({
|
|
errorCode: 'MODEL_INVOCATION_DEADLINE_EXCEEDED',
|
|
});
|
|
|
|
assert.equal(completion.completion.outcome, 'timed_out');
|
|
assert.equal(completion.stepRunMutation.stepRun.status, 'timed_out');
|
|
assert.equal(
|
|
completion.stepRunMutation.stepRun.resultCode,
|
|
'model_deadline_exceeded',
|
|
);
|
|
});
|
|
|
|
test('unknown provider outcome maps to lost for manual resolution', () => {
|
|
const completion = completionFixture({
|
|
errorCode: 'MODEL_INVOCATION_ABORTED',
|
|
});
|
|
|
|
assert.equal(completion.completion.outcome, 'outcome_unknown');
|
|
assert.equal(completion.stepRunMutation.stepRun.status, 'lost');
|
|
assert.equal(
|
|
completion.stepRunMutation.stepRun.resultCode,
|
|
'model_outcome_unknown',
|
|
);
|
|
});
|
|
|
|
test('max-length invocation IDs derive bounded stable mutation identities', () => {
|
|
const requestId = `r${'x'.repeat(127)}`;
|
|
const first = createModelInvocationMutationIdentity(requestId, 'start');
|
|
const replay = createModelInvocationMutationIdentity(requestId, 'start');
|
|
|
|
assert.deepEqual(first, replay);
|
|
assert.ok(first.mutationId.length <= 128);
|
|
assert.ok(first.eventId.length <= 128);
|
|
assert.ok(first.dedupeKey.length <= 128);
|
|
assert.doesNotThrow(() => startFixture(requestId));
|
|
});
|
|
|
|
test('trace drift and command digest tampering fail closed', () => {
|
|
const completion = completionFixture();
|
|
const drifted = {
|
|
...completion,
|
|
completion: { ...completion.completion, traceId: 'trace-b' },
|
|
};
|
|
const tampered = {
|
|
...completion,
|
|
commandDigest: '0'.repeat(64),
|
|
};
|
|
|
|
assert.throws(
|
|
() => normalizeModelInvocationCompletionCommand(drifted),
|
|
InvalidModelInvocationError,
|
|
);
|
|
assert.throws(
|
|
() => normalizeModelInvocationCompletionCommand(tampered),
|
|
InvalidModelInvocationError,
|
|
);
|
|
});
|