test(cowork-service): ecosystem integration tests (H.1-H.12)

This commit is contained in:
Saravana Achu Mac 2026-04-03 13:53:29 -07:00
parent a7468eaa3f
commit 2f8fe13c43

View File

@ -0,0 +1,361 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { createServer, type Server } from 'node:http';
const runIntegration = Boolean(process.env.INTEGRATION_TESTS);
const platformBaseUrl = process.env.PLATFORM_SERVICE_URL ?? 'http://localhost:4003/api';
const coworkBaseUrl = process.env.COWORK_SERVICE_URL ?? 'http://localhost:4009';
const integrationEmail = process.env.INTEGRATION_TEST_EMAIL;
const integrationPassword = process.env.INTEGRATION_TEST_PASSWORD;
const integrationProductId = 'clawcowork';
type JsonRecord = Record<string, unknown>;
function requireIntegrationCredentials(): { email: string; password: string } {
if (!integrationEmail || !integrationPassword) {
throw new Error(
'Set INTEGRATION_TEST_EMAIL and INTEGRATION_TEST_PASSWORD before running integration tests'
);
}
return { email: integrationEmail, password: integrationPassword };
}
async function requestJson<T = JsonRecord>(
url: string,
init: RequestInit = {},
token?: string
): Promise<{ status: number; data: T }> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(init.headers as Record<string, string> | undefined),
};
if (token) headers.authorization = `Bearer ${token}`;
if (!headers['x-product-id'] && url.startsWith(platformBaseUrl)) {
headers['x-product-id'] = integrationProductId;
}
const response = await fetch(url, { ...init, headers });
const text = await response.text();
const data = text ? (JSON.parse(text) as T) : ({} as T);
return { status: response.status, data };
}
async function waitFor(
label: string,
callback: () => Promise<boolean>,
timeoutMs = 45_000,
intervalMs = 2_500
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await callback()) return;
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
throw new Error(`Timed out waiting for ${label}`);
}
describe.runIf(runIntegration)('ecosystem integration (H.1-H.12)', () => {
let token = '';
let webhookServer: Server | null = null;
const webhookDeliveries: JsonRecord[] = [];
let webhookUrl = '';
let webhookSubscriptionId = '';
let marketplaceListingId = '';
let sandboxFlagInitialEnabled = true;
beforeAll(async () => {
const creds = requireIntegrationCredentials();
const login = await requestJson<{
accessToken: string;
user: { id: string; email: string };
}>(`${platformBaseUrl}/auth/login`, {
method: 'POST',
body: JSON.stringify({
email: creds.email,
password: creds.password,
productId: integrationProductId,
}),
});
expect(login.status).toBe(200);
expect(login.data.accessToken).toBeTruthy();
token = login.data.accessToken;
webhookServer = createServer((req, res) => {
const chunks: Buffer[] = [];
req.on('data', chunk => chunks.push(Buffer.from(chunk)));
req.on('end', () => {
try {
webhookDeliveries.push(JSON.parse(Buffer.concat(chunks).toString('utf8')));
} catch {
webhookDeliveries.push({ raw: Buffer.concat(chunks).toString('utf8') });
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
});
});
await new Promise<void>((resolve, reject) => {
webhookServer!.listen(0, '127.0.0.1', err => (err ? reject(err) : resolve()));
});
const address = webhookServer.address();
if (!address || typeof address === 'string') {
throw new Error('Failed to bind webhook test server');
}
webhookUrl = `http://127.0.0.1:${address.port}/webhook`;
});
afterAll(async () => {
if (webhookSubscriptionId) {
await requestJson(
`${platformBaseUrl}/webhooks/subscriptions/${webhookSubscriptionId}`,
{
method: 'DELETE',
},
token
).catch(() => undefined);
}
if (!sandboxFlagInitialEnabled) {
await requestJson(
`${platformBaseUrl}/flags/sandbox_enabled/toggle`,
{
method: 'POST',
},
token
).catch(() => undefined);
}
if (webhookServer) {
await new Promise<void>((resolve, reject) =>
webhookServer!.close(err => (err ? reject(err) : resolve()))
);
}
});
it('H.1: product registry contains clawcowork and flags are seeded', async () => {
const products = await requestJson<{ products: Array<{ productId: string }> }>(
`${platformBaseUrl}/products`
);
expect(products.status).toBe(200);
expect(products.data.products.some(product => product.productId === integrationProductId)).toBe(
true
);
const flags = await requestJson<{ flags: Array<{ key: string }> }>(
`${platformBaseUrl}/flags`,
{},
token
);
expect(flags.status).toBe(200);
expect(flags.data.flags.length).toBeGreaterThanOrEqual(13);
});
it('H.3: login obtains JWT and cowork-service accepts task submission', async () => {
const task = await requestJson<{ id: string }>(
`${coworkBaseUrl}/api/tasks`,
{
method: 'POST',
body: JSON.stringify({
goal: 'Integration smoke test task',
folder: process.cwd(),
model: 'claude-sonnet-4-20250514',
}),
},
token
);
expect(task.status).toBe(201);
expect(task.data.id).toBeTruthy();
});
it('H.3: enterprise SSO lookup endpoint is available', async () => {
const email = process.env.INTEGRATION_SSO_EMAIL ?? integrationEmail!;
const lookup = await requestJson<{ found: boolean; idp: { protocol?: string } | null }>(
`${platformBaseUrl}/auth/enterprise/lookup?email=${encodeURIComponent(email)}`
);
expect(lookup.status).toBe(200);
expect(typeof lookup.data.found).toBe('boolean');
if (lookup.data.found) {
expect(['saml', 'oidc']).toContain(lookup.data.idp?.protocol);
}
});
it('H.4: sandbox_enabled propagates through platform flag polling', async () => {
const before = await requestJson<{ flags: Record<string, boolean> }>(
`${platformBaseUrl}/flags/poll`,
{},
token
);
expect(before.status).toBe(200);
sandboxFlagInitialEnabled = Boolean(before.data.flags.sandbox_enabled);
if (sandboxFlagInitialEnabled) {
const toggle = await requestJson(
`${platformBaseUrl}/flags/sandbox_enabled/toggle`,
{
method: 'POST',
},
token
);
expect(toggle.status).toBe(200);
}
await waitFor('sandbox_enabled=false', async () => {
const polled = await requestJson<{ flags: Record<string, boolean> }>(
`${platformBaseUrl}/flags/poll`,
{},
token
);
return polled.status === 200 && polled.data.flags.sandbox_enabled === false;
});
});
it('H.5: audit entries appear after task submission flush', async () => {
const task = await requestJson<{ id: string }>(
`${coworkBaseUrl}/api/tasks`,
{
method: 'POST',
body: JSON.stringify({
goal: 'Trigger audit flush integration test',
folder: process.cwd(),
model: 'claude-sonnet-4-20250514',
}),
},
token
);
expect(task.status).toBe(201);
await waitFor(
'audit entry for clawcowork',
async () => {
const audit = await requestJson<{ records?: JsonRecord[]; entries?: JsonRecord[] }>(
`${platformBaseUrl}/audit?days=7&limit=25`,
{},
token
);
const entries = audit.data.records ?? audit.data.entries ?? [];
return audit.status === 200 && entries.length > 0;
},
50_000,
5_000
);
});
it('H.6: usage limit check returns a verdict for a small daily budget', async () => {
const verdict = await requestJson<JsonRecord>(
`${coworkBaseUrl}/api/usage/check-limits`,
{
method: 'POST',
body: JSON.stringify({ plan: 'free' }),
},
token
);
expect(verdict.status).toBe(200);
expect('allowed' in verdict.data || 'withinLimits' in verdict.data).toBe(true);
});
it('H.8: telemetry metrics are queryable after submitting activity', async () => {
await requestJson(
`${coworkBaseUrl}/api/tasks`,
{
method: 'POST',
body: JSON.stringify({
goal: 'Trigger telemetry integration test',
folder: process.cwd(),
model: 'claude-sonnet-4-20250514',
}),
},
token
);
await waitFor(
'telemetry metrics',
async () => {
const metrics = await requestJson<JsonRecord>(
`${platformBaseUrl}/telemetry/metrics`,
{},
token
);
return metrics.status === 200;
},
50_000,
5_000
);
});
it('H.9: webhook subscription receives a test delivery', async () => {
const created = await requestJson<{ id: string }>(
`${coworkBaseUrl}/api/webhooks`,
{
method: 'POST',
body: JSON.stringify({
url: webhookUrl,
events: ['user.created', 'task.completed'],
}),
},
token
);
expect(created.status).toBe(201);
webhookSubscriptionId = created.data.id;
const testDelivery = await requestJson(
`${platformBaseUrl}/webhooks/subscriptions/${webhookSubscriptionId}/test`,
{ method: 'POST' },
token
);
expect(testDelivery.status).toBe(200);
await waitFor('webhook delivery', async () => webhookDeliveries.length > 0, 20_000, 1_000);
expect(webhookDeliveries.length).toBeGreaterThan(0);
});
it('H.10: cowork-service proxies extraction requests', async () => {
const models = await requestJson<{ models: string[] }>(`${coworkBaseUrl}/api/extract/models`);
expect(models.status).toBe(200);
expect(models.data.models.length).toBeGreaterThan(0);
const extraction = await requestJson<JsonRecord>(`${coworkBaseUrl}/api/extract`, {
method: 'POST',
body: JSON.stringify({
text:
process.env.INTEGRATION_EXTRACTION_TEXT ??
'Sample extraction payload for Claw Cowork integration testing.',
task: 'entity-extraction',
}),
});
expect(extraction.status).toBe(200);
expect(extraction.data).toBeTruthy();
});
it('H.12: marketplace listing install is recorded', async () => {
const listings = await requestJson<{
items?: Array<{ id: string }>;
listings?: Array<{ id: string }>;
}>(`${coworkBaseUrl}/api/marketplace?limit=10`, {}, token);
expect(listings.status).toBe(200);
const items = listings.data.items ?? listings.data.listings ?? [];
expect(items.length).toBeGreaterThan(0);
marketplaceListingId = items[0].id;
const install = await requestJson<JsonRecord>(
`${coworkBaseUrl}/api/marketplace/${marketplaceListingId}/install`,
{ method: 'POST' },
token
);
expect([200, 201]).toContain(install.status);
await waitFor('marketplace install record', async () => {
const installs = await requestJson<{
items?: Array<{ listingId: string }>;
installs?: Array<{ listingId: string }>;
}>(`${coworkBaseUrl}/api/marketplace/installs`, {}, token);
const records = installs.data.items ?? installs.data.installs ?? [];
return records.some(record => record.listingId === marketplaceListingId);
});
});
});