visual-journal / src /lib /agent-state-runtime.test.ts
misonL's picture
Deploy 09324ad to Docker Space
b1cfe1b verified
Raw
History Blame
10.1 kB
import { runAgentStateStartupRecovery } from '../instrumentation';
import { MemoryAgentStateStore } from './agent-state-memory';
import {
ensureAgentStateStoreReady,
getAgentStateStore,
readAgentDatabaseUrl,
recoverAgentStateOnStartup,
resetAgentStateStoreForTests,
setAgentStateStoreFactoryForTests
} from './agent-state-runtime';
import type { AgentStateStore } from './agent-state-store';
import type { ImageShareStateStore } from './share-store';
import assert from 'node:assert/strict';
import { afterEach, describe, it } from 'node:test';
afterEach(() => {
setAgentStateStoreFactoryForTests(undefined);
resetAgentStateStoreForTests();
});
describe('agent-state-runtime recovery scheduling', () => {
it('creates a memory store for ephemeral deployments', () => {
const store = getAgentStateStore({ AGENT_STATE_BACKEND: 'memory' });
assert.ok(store instanceof MemoryAgentStateStore);
});
it('throttles request-time recovery checks by interval', async () => {
const store = createFakeStore();
setAgentStateStoreFactoryForTests(() => store);
const env = {
AGENT_STATE_BACKEND: 'sqlite',
AGENT_SQLITE_PATH: 'agent.sqlite',
AGENT_RECOVERY_INTERVAL_MS: '1000'
};
await ensureAgentStateStoreReady(env, new Date('2026-05-12T00:00:00.000Z'));
await ensureAgentStateStoreReady(env, new Date('2026-05-12T00:00:00.500Z'));
await ensureAgentStateStoreReady(env, new Date('2026-05-12T00:00:01.001Z'));
assert.equal(store.recoveryCalls, 2);
});
it('runs share cleanup with the request-time recovery cycle', async () => {
const store = createFakeStore();
setAgentStateStoreFactoryForTests(() => store);
const env = {
AGENT_STATE_BACKEND: 'sqlite',
AGENT_SQLITE_PATH: 'agent.sqlite',
AGENT_RECOVERY_INTERVAL_MS: '1000'
};
await ensureAgentStateStoreReady(env, new Date('2026-05-12T00:00:00.000Z'));
await ensureAgentStateStoreReady(env, new Date('2026-05-12T00:00:00.500Z'));
await ensureAgentStateStoreReady(env, new Date('2026-05-12T00:00:01.001Z'));
assert.equal(store.shareCleanupCalls, 2);
});
it('always runs explicit startup recovery', async () => {
const store = createFakeStore();
setAgentStateStoreFactoryForTests(() => store);
await recoverAgentStateOnStartup({ AGENT_STATE_BACKEND: 'sqlite', AGENT_SQLITE_PATH: 'agent.sqlite' });
await recoverAgentStateOnStartup({ AGENT_STATE_BACKEND: 'sqlite', AGENT_SQLITE_PATH: 'agent.sqlite' });
assert.equal(store.recoveryCalls, 2);
assert.equal(store.shareCleanupCalls, 2);
});
it('allows the next request to retry recovery after a failed recovery attempt', async () => {
const store = createFakeStore({ failFirstRecovery: true });
setAgentStateStoreFactoryForTests(() => store);
const env = {
AGENT_STATE_BACKEND: 'sqlite',
AGENT_SQLITE_PATH: 'agent.sqlite',
AGENT_RECOVERY_INTERVAL_MS: '1000'
};
await assert.rejects(
() => ensureAgentStateStoreReady(env, new Date('2026-05-12T00:00:00.000Z')),
/recovery failed/
);
await ensureAgentStateStoreReady(env, new Date('2026-05-12T00:00:00.100Z'));
assert.equal(store.recoveryCalls, 2);
});
it('clears a failed store init so the next request can retry after the environment recovers', async () => {
let shouldFailInit = true;
const store = createFakeStore({ failInit: () => shouldFailInit });
setAgentStateStoreFactoryForTests(() => store);
const env = {
AGENT_STATE_BACKEND: 'sqlite',
AGENT_SQLITE_PATH: 'agent.sqlite'
};
await assert.rejects(
() => ensureAgentStateStoreReady(env, new Date('2026-05-12T00:00:00.000Z')),
/init failed/
);
shouldFailInit = false;
await ensureAgentStateStoreReady(env, new Date('2026-05-12T00:00:00.100Z'));
assert.equal(store.initCalls, 2);
});
});
describe('runAgentStateStartupRecovery', () => {
it('fails startup when agent state recovery fails', async () => {
const logs: Array<{ level: 'info' | 'error'; message: string }> = [];
await assert.rejects(
() =>
runAgentStateStartupRecovery({
recoverAgentStateOnStartup: async () => {
throw new Error('startup recovery failed');
},
appLogger: {
info(message) {
logs.push({ level: 'info', message });
},
error(message) {
logs.push({ level: 'error', message });
}
}
}),
/startup recovery failed/
);
assert.deepEqual(
logs.map((entry) => entry.level),
['info', 'error']
);
});
it('logs startup recovery completion', async () => {
const logs: Array<{ level: 'info' | 'error'; message: string; context?: unknown }> = [];
await runAgentStateStartupRecovery({
recoverAgentStateOnStartup: async () => 3,
appLogger: {
info(message, context) {
logs.push({ level: 'info', message, context });
},
error(message, context) {
logs.push({ level: 'error', message, context });
}
}
});
assert.equal(logs.length, 2);
assert.equal(logs[0]?.message, '开始执行 Agent 状态启动恢复。');
assert.equal(logs[1]?.message, 'Agent 状态启动恢复完成。');
});
});
describe('readAgentDatabaseUrl', () => {
const DB_PASSWORD_FIXTURE = ['database', 'password'].join(' ');
const ENCODED_DB_PASSWORD_FIXTURE = encodeURIComponent(DB_PASSWORD_FIXTURE);
const EXPLICIT_DATABASE_URL_FIXTURE = `postgres://gpt_image:${ENCODED_DB_PASSWORD_FIXTURE}@postgres:5432/gpt_image_playground`;
it('prefers an explicit AGENT_DATABASE_URL', () => {
assert.equal(
readAgentDatabaseUrl({ AGENT_DATABASE_URL: EXPLICIT_DATABASE_URL_FIXTURE }),
EXPLICIT_DATABASE_URL_FIXTURE
);
});
it('falls back to split PostgreSQL fields when AGENT_DATABASE_URL is blank', () => {
assert.equal(
readAgentDatabaseUrl({
AGENT_DATABASE_URL: ' ',
AGENT_DB_HOST: 'postgres',
AGENT_DB_PORT: '5432',
AGENT_DB_NAME: 'gpt_image_playground',
AGENT_DB_USER: 'gpt_image',
AGENT_DB_PASSWORD: DB_PASSWORD_FIXTURE
}),
`postgres://gpt_image:${ENCODED_DB_PASSWORD_FIXTURE}@postgres:5432/gpt_image_playground`
);
});
it('builds a PostgreSQL URL from individual environment fields', () => {
assert.equal(
readAgentDatabaseUrl({
AGENT_DB_HOST: 'postgres',
AGENT_DB_PORT: '5432',
AGENT_DB_NAME: 'gpt_image_playground',
AGENT_DB_USER: 'gpt_image',
AGENT_DB_PASSWORD: DB_PASSWORD_FIXTURE
}),
`postgres://gpt_image:${ENCODED_DB_PASSWORD_FIXTURE}@postgres:5432/gpt_image_playground`
);
});
it('escapes split PostgreSQL user, password, and database fields', () => {
const databaseName = ['gpt', 'image playground'].join('/');
const databaseUser = ['gpt', 'image'].join('@');
const databaseCredential = ['p', 'ss/word:?#'].join('@');
const url = readAgentDatabaseUrl({
AGENT_DB_HOST: 'postgres',
AGENT_DB_PORT: '5432',
AGENT_DB_NAME: databaseName,
AGENT_DB_USER: databaseUser,
AGENT_DB_PASSWORD: databaseCredential
});
assert.equal(
url,
`postgres://${encodeURIComponent(databaseUser)}:${encodeURIComponent(databaseCredential)}@postgres:5432/${encodeURIComponent(databaseName)}`
);
});
});
function createFakeStore(options: { failFirstRecovery?: boolean; failInit?: () => boolean } = {}): AgentStateStore &
ImageShareStateStore & {
recoveryCalls: number;
initCalls: number;
shareCleanupCalls: number;
} {
return {
initCalls: 0,
recoveryCalls: 0,
shareCleanupCalls: 0,
async init() {
this.initCalls += 1;
if (options.failInit?.()) {
throw new Error('init failed');
}
},
async recoverExpiredRequests() {
this.recoveryCalls += 1;
if (options.failFirstRecovery && this.recoveryCalls === 1) {
throw new Error('recovery failed');
}
return 0;
},
async purgeExpiredRequests() {
return 0;
},
async beginRequest() {
throw new Error('not implemented');
},
async refreshRequestLease() {
return false;
},
async saveArtifacts() {},
async completeRequest() {},
async failRequest() {},
async getArtifact() {
return undefined;
},
async getRequest() {
return undefined;
},
async getRequestByIdempotencyKey() {
return undefined;
},
async listArtifactsForRequest() {
return [];
},
async deleteArtifact() {
return false;
},
async createImageShareRecord() {},
async readImageShareRecord() {
return undefined;
},
async deleteExpiredImageShareRecords() {
this.shareCleanupCalls += 1;
return [];
},
async listImageShareRecords() {
return [];
}
};
}