scripts/dev-fixtures.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
// Seed realistic chat, Agent Builder and issue content into a managed dev
// environment, without a model and without a daemon:
//
// make up C=api,web ARGS="--ephemeral"
// make env-exec ARGS="-- node scripts/dev-fixtures.mjs all"
// make env-exec ARGS="-- node scripts/dev-fixtures.mjs chat issue --json"
//
// Replies come from the daemon API, driven with the environment profile's user
// token: the script registers a fixture runtime, then claims, starts and
// completes each task it caused with canned Markdown, so content appears without
// running a model through `make up C=daemon`. Every run adds new sessions and
// issues and then exits; nothing keeps running.
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
const KINDS = ["chat", "builder", "issue"];
const FIXTURE_DAEMON_ID = "dev-fixtures";
const FIXTURE_AGENT_NAME = "Fixture Agent";
const USAGE = `Usage: node scripts/dev-fixtures.mjs <all | chat | builder | issue>... [--json]
chat a chat session: a user message and an assistant reply with a table, code and a long log line
builder an Agent Builder session with the same kind of reply and a filled-in draft
issue an issue with a wide description and a comment holding a diff
all all three
--json print the created ids and URLs as JSON instead of text
Run through \`make env-exec ARGS="-- node scripts/dev-fixtures.mjs all"\`.`;
function fail(message) {
console.error(`[dev-fixtures] ${message}`);
process.exit(1);
}
function parseCli(argv) {
const kinds = new Set();
let json = false;
for (const arg of argv) {
if (arg === "-h" || arg === "--help") {
console.log(USAGE);
process.exit(0);
} else if (arg === "--json") json = true;
else if (arg === "all") KINDS.forEach((kind) => kinds.add(kind));
else if (KINDS.includes(arg)) kinds.add(arg);
else fail(`unknown argument ${arg}\n\n${USAGE}`);
}
if (kinds.size === 0) fail(`name at least one fixture.\n\n${USAGE}`);
return { kinds: KINDS.filter((kind) => kinds.has(kind)), json };
}
function loadProfile(env) {
if (!env.MULTICA_DEV_PROFILE) fail("MULTICA_DEV_PROFILE is not set; run this through `make env-exec`.");
const home = env.MULTICA_DEV_PROFILES_HOME || join(homedir(), ".multica", "profiles");
const file = join(home, env.MULTICA_DEV_PROFILE, "config.json");
let config;
try {
config = JSON.parse(readFileSync(file, "utf8"));
} catch (error) {
fail(`could not read ${file}: ${error.message}\nRun \`make up\` first; it signs the profile in.`);
}
if (!config.token || !config.workspace_id) fail(`${file} has no token or workspace; re-run \`make up\`.`);
return {
apiUrl: env.PORT ? `http://localhost:${env.PORT}` : config.server_url,
appUrl: env.FRONTEND_PORT ? `http://localhost:${env.FRONTEND_PORT}` : config.app_url,
token: config.token,
workspaceId: config.workspace_id,
};
}
function client({ apiUrl, token, workspaceId }) {
return async function request(method, path, body) {
const response = await fetch(`${apiUrl}${path}`, {
method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
"X-Workspace-ID": workspaceId,
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await response.text();
if (!response.ok) throw new Error(`${method} ${path} → ${response.status} ${text.slice(0, 300)}`);
return text ? JSON.parse(text) : null;
};
}
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
// --------------------------------------------------------------- content ---
const LONG_LOG_LINE =
"2026-11-09T06:48:19.632113Z WARN daemon/claim runtime=4f1c2a7e-0b8d-4c55-9a51-2f0f7a3c9e10 task=0193c2d4-5b6a-7e8f-9a0b-1c2d3e4f5a6b " +
"agent=fixture-agent attempt=3 elapsed_ms=48213 queue_depth=17 lease_expires_at=2026-11-09T06:53:19Z " +
'reason="prepare lease extended twice; workspace checkout still copying node_modules from cache /Users/dev/multica_workspaces/dev/4f1c2a7e/workdir"';
const WIDE_TABLE = `| Runtime | Provider | Region | p50 latency | p95 latency | Error rate | Tasks / day | Queue depth | Last deploy | Owner |
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | --- | --- |
| build-mac-01 | Claude Code | us-west-2 | 1.8 s | 7.4 s | 0.4% | 1,284 | 3 | 2026-11-08 22:14 UTC | platform-infra |
| build-mac-02 | Codex | us-west-2 | 2.1 s | 9.0 s | 1.1% | 973 | 7 | 2026-11-08 22:14 UTC | platform-infra |
| cloud-pool-a | Claude Code | eu-central-1 | 1.2 s | 4.9 s | 0.2% | 4,612 | 0 | 2026-11-09 03:02 UTC | cloud-runtime |
| cloud-pool-b | Gemini CLI | ap-southeast-1 | 2.6 s | 11.3 s | 2.7% | 655 | 12 | 2026-11-07 17:40 UTC | cloud-runtime |
| laptop-dev | OpenCode | local | 0.9 s | 3.1 s | 0.0% | 88 | 0 | 2026-11-09 06:01 UTC | dev-tools |`;
const CODE_BLOCK = `\`\`\`go
// ClaimTaskForRuntime hands the oldest queued task to a runtime, unless the agent is already at its concurrency limit or the task's prepare lease belongs to another runtime.
func (s *TaskService) ClaimTaskForRuntime(ctx context.Context, runtimeID pgtype.UUID) (*db.AgentTaskQueue, error) {
task, err := s.Queries.ClaimNextQueuedTask(ctx, db.ClaimNextQueuedTaskParams{RuntimeID: runtimeID, Now: pgtype.Timestamptz{Time: time.Now(), Valid: true}})
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return &task, err
}
\`\`\``;
const ASSISTANT_REPLY = `Here is how the five runtimes compared over the last 24 hours. **cloud-pool-b** is the outlier: its p95 is more than twice the fleet median and it holds the deepest queue.
${WIDE_TABLE}
The slow claims all come from the same path. The claim query itself is cheap; the time goes into the prepare lease while the checkout copies dependencies:
${CODE_BLOCK}
A representative log line from the slow runtime:
\`\`\`text
${LONG_LOG_LINE}
\`\`\`
Next steps:
1. Pre-warm the dependency cache on \`cloud-pool-b\` so the prepare lease is not extended.
2. Cap the queue at 8 per runtime and spill the rest to \`cloud-pool-a\`.
3. Re-check the p95 after the next deploy.`;
const CHAT_PROMPT =
"Compare our runtimes over the last day: latency, error rate and queue depth. Show me a table, the code path behind slow claims, and one raw log line.";
const BUILDER_PROMPT =
"I want an agent that watches our runtimes, flags the slow ones with a latency table, and proposes a fix with the relevant code.";
const BUILDER_DRAFT = {
name: "Runtime Watch",
description: "Flags slow or failing runtimes with a latency table and proposes a concrete fix.",
instructions:
"## Role\nYou watch the workspace's runtimes and report the ones that are slow or failing.\n\n## Workflow\n1. Compare p50, p95, error rate and queue depth across runtimes.\n2. Present the comparison as a table.\n3. Point at the code path or configuration behind the outlier.\n\n## Output\nA short summary, the table, and numbered next steps.",
conversation_starters: [
{ label: "Daily runtime report", prompt: "Compare every runtime over the last 24 hours and flag the outliers." },
{ label: "Why is a runtime slow?", prompt: "cloud-pool-b has a high p95. Find the cause and propose a fix." },
],
model: "",
skill_ids: [],
permission_scope: "private",
member_ids: [],
};
const BUILDER_REPLY = `${ASSISTANT_REPLY}
I drafted **Runtime Watch** around that report. Should it post to an issue every morning, or only answer when asked?
<agent_draft>${JSON.stringify(BUILDER_DRAFT)}</agent_draft>`;
// Mirrors encodeBuilderInput in packages/core/agents/builder-protocol.ts, so the
// studio decodes the user turn the way it decodes one it sent itself.
function builderInput(runtime) {
return `MULTICA_AGENT_BUILDER_INPUT\n${JSON.stringify(
{
user_request: BUILDER_PROMPT,
current_draft: {
name: "",
description: "",
instructions: "",
conversation_starters: [],
model: "",
skill_ids: [],
permission_scope: "private",
member_ids: [],
},
selected_runtime: { id: runtime.id, name: runtime.name, provider: runtime.provider },
available_runtime_models: null,
available_workspace_skills: [],
available_workspace_members: [],
},
null,
2,
)}`;
}
const ISSUE_DESCRIPTION = `Claims on \`cloud-pool-b\` regularly take longer than 45 seconds, and agents assigned to it start their runs late. The rest of the fleet is unaffected.
## Runtime comparison
${WIDE_TABLE}
## Evidence
\`\`\`text
${LONG_LOG_LINE}
${LONG_LOG_LINE.replace("attempt=3", "attempt=4").replace("48213", "51877")}
\`\`\`
## Where the time goes
${CODE_BLOCK}
## Acceptance
- p95 claim latency on \`cloud-pool-b\` is under 10 seconds for a full day.
- No prepare lease is extended more than once.`;
const ISSUE_COMMENT = `Found it: the prepare lease is extended before the cache copy starts, so a slow copy always needs a second extension. Moving the extension after the copy fixes the latency on my machine.
\`\`\`diff
--- a/server/internal/daemon/prepare.go
+++ b/server/internal/daemon/prepare.go
@@ -112,11 +112,11 @@ func (p *Preparer) Prepare(ctx context.Context, task *Task) error {
- if err := p.client.ExtendPrepareLease(ctx, task.RuntimeID, task.ID); err != nil {
- return fmt.Errorf("extend prepare lease for task %s on runtime %s: %w", task.ID, task.RuntimeID, err)
- }
if err := p.cache.CopyInto(ctx, task.WorkDir, cache.Options{Include: []string{"node_modules", ".turbo", "server/bin"}, Concurrency: 8}); err != nil {
return fmt.Errorf("copy dependency cache into %s: %w", task.WorkDir, err)
}
+ if err := p.client.ExtendPrepareLease(ctx, task.RuntimeID, task.ID); err != nil {
+ return fmt.Errorf("extend prepare lease for task %s on runtime %s: %w", task.ID, task.RuntimeID, err)
+ }
\`\`\`
| Run | Before p95 | After p95 | Lease extensions |
| --- | ---: | ---: | ---: |
| 1 | 48.2 s | 6.1 s | 1 |
| 2 | 51.9 s | 5.8 s | 1 |
| 3 | 47.0 s | 6.4 s | 1 |`;
// -------------------------------------------------------------- fixtures ---
async function ensureRuntime(request, workspaceId) {
const registered = await request("POST", "/api/daemon/register", {
workspace_id: workspaceId,
daemon_id: FIXTURE_DAEMON_ID,
device_name: "Dev fixtures",
cli_version: "dev-fixtures",
runtimes: [{ name: "Fixture runtime", type: "claude", version: "fixture", status: "online" }],
});
const runtime = registered?.runtimes?.[0];
if (!runtime?.id) throw new Error(`runtime registration returned no runtime: ${JSON.stringify(registered).slice(0, 300)}`);
return runtime;
}
async function ensureAgent(request, runtime) {
const agents = (await request("GET", "/api/agents")) ?? [];
const existing = (Array.isArray(agents) ? agents : agents.agents ?? []).find(
(agent) => agent.name === FIXTURE_AGENT_NAME && agent.runtime_id === runtime.id,
);
if (existing) return existing;
return request("POST", "/api/agents", {
name: FIXTURE_AGENT_NAME,
description: "Answers with canned Markdown from scripts/dev-fixtures.mjs.",
instructions: "Fixture agent for local UI evidence. Its replies are written by scripts/dev-fixtures.mjs.",
runtime_id: runtime.id,
visibility: "workspace",
});
}
// A send enqueues the task; the fixture runtime is the only claimer, so the
// expected task arrives within a few polls. Anything else it holds is left
// over from an interrupted run and is answered too, so it cannot block ours.
async function answer(request, runtime, taskId, output) {
const deadline = Date.now() + 30_000;
for (;;) {
const claimed = await request("POST", `/api/daemon/runtimes/${runtime.id}/tasks/claim`, {});
const task = claimed?.task;
if (task) {
await request("POST", `/api/daemon/tasks/${task.id}/start`);
await request("POST", `/api/daemon/tasks/${task.id}/complete`, { output });
if (task.id === taskId) return;
continue;
}
if (Date.now() > deadline) throw new Error(`task ${taskId} was never offered to the fixture runtime`);
await sleep(500);
}
}
async function seedChat({ request, runtime, agent, appUrl, slug }) {
const session = await request("POST", "/api/chat/sessions", {
agent_id: agent.id,
title: "Fixture: runtime latency report",
});
const sent = await request("POST", `/api/chat/sessions/${session.id}/messages`, { content: CHAT_PROMPT });
await answer(request, runtime, sent.task_id, ASSISTANT_REPLY);
return { session_id: session.id, url: `${appUrl}/${slug}/chat?session=${encodeURIComponent(session.id)}` };
}
async function seedBuilder({ request, runtime, appUrl, slug }) {
const created = await request("POST", "/api/agent-builder/sessions", { runtime_id: runtime.id });
const sent = await request("POST", `/api/chat/sessions/${created.session_id}/messages`, {
content: builderInput(runtime),
});
await answer(request, runtime, sent.task_id, BUILDER_REPLY);
return {
session_id: created.session_id,
url: `${appUrl}/${slug}/agents/new/ai/${encodeURIComponent(created.session_id)}`,
};
}
async function seedIssue({ request, appUrl, slug }) {
const issue = await request("POST", "/api/issues", {
title: "Fixture: slow claims on cloud-pool-b",
description: ISSUE_DESCRIPTION,
status: "in_progress",
priority: "high",
});
await request("POST", `/api/issues/${issue.id}/comments`, { content: ISSUE_COMMENT });
return { issue_id: issue.id, identifier: issue.identifier, url: `${appUrl}/${slug}/issues/${issue.identifier}` };
}
async function main() {
const { kinds, json } = parseCli(process.argv.slice(2));
const profile = loadProfile(process.env);
const request = client(profile);
const workspaces = (await request("GET", "/api/workspaces")) ?? [];
const workspace = workspaces.find((ws) => ws.id === profile.workspaceId);
if (!workspace) fail(`the profile's workspace ${profile.workspaceId} is not visible to its token.`);
const context = { request, appUrl: profile.appUrl, slug: workspace.slug };
if (kinds.includes("chat") || kinds.includes("builder")) {
context.runtime = await ensureRuntime(request, profile.workspaceId);
}
if (kinds.includes("chat")) context.agent = await ensureAgent(request, context.runtime);
const seeders = { chat: seedChat, builder: seedBuilder, issue: seedIssue };
const result = {};
for (const kind of kinds) {
result[kind] = await seeders[kind](context);
if (!json) console.log(`[dev-fixtures] ${kind.padEnd(8)}${result[kind].url}`);
}
if (json) console.log(JSON.stringify(result, null, 2));
}
main().catch((error) => fail(error?.stack || String(error)));