fix(plugins): omitted metadata in a workspace.create.before result means unchanged

Same handoff as agent.create.before: a legacy hook that returned only
{ source, ... } dropped the caller's metadata and anything an earlier
hook added, and workspace provisioning then defaulted it to {}. The
omitted field is carried forward at each handoff; an explicit {} still
replaces.
This commit is contained in:
Mohamed Boudra
2026-11-02 03:50:36 +01:00
parent 21365c571b
commit 4095d3784d
2 changed files with 41 additions and 0 deletions
@@ -125,6 +125,41 @@ test("a create hook that omits metadata passes on what an earlier hook added", a
expect(seen).toEqual([{ original: true, "new.plugin": { ticket: 42 } }]);
});
test("a workspace create hook that omits metadata passes on what an earlier hook added", async () => {
const hooks = new PluginHookHandlers(() => {});
hooks.addHook("workspace.create.before", async ({ request }) => ({
...request,
metadata: { ...request.metadata, "new.plugin": { ticket: 42 } },
}));
hooks.addHook("workspace.create.before", async ({ request }) => ({ source: request.source }));
const seen: unknown[] = [];
hooks.addHook("workspace.create.before", async ({ request }) => {
seen.push(request.metadata);
});
const result = await hooks.invoke(
"operation",
"workspace.create.before",
{ source: { kind: "directory", path: "/project" }, metadata: { original: true } },
paseo,
);
expect(result).toMatchObject({ metadata: { original: true, "new.plugin": { ticket: 42 } } });
expect(seen).toEqual([{ original: true, "new.plugin": { ticket: 42 } }]);
const replacing = new PluginHookHandlers(() => {});
replacing.addHook("workspace.create.before", async ({ request }) => ({
...request,
metadata: {},
}));
await expect(
replacing.invoke(
"operation",
"workspace.create.before",
{ source: { kind: "directory", path: "/project" }, metadata: { original: true } },
paseo,
),
).resolves.toMatchObject({ metadata: {} });
});
test("a session-open hook that returns its own object without metadata leaves metadata unchanged", async () => {
const hooks = new PluginHookHandlers(() => {});
hooks.addHook("agent.open.before", async ({ request }) => ({
@@ -252,6 +252,12 @@ export async function validateBeforeResult<Name extends keyof PluginWireBeforeRe
// Omitted means unchanged: the next hook sees what this one was handed.
return { ...result, metadata: next.metadata ?? previous.metadata };
}
if (name === "workspace.create.before") {
const previous = beforeSchemas["workspace.create.before"].parse(input);
const next = beforeSchemas["workspace.create.before"].parse(result);
// Omitted means unchanged; an explicit {} still replaces.
return { ...result, metadata: next.metadata ?? previous.metadata };
}
if (name === "worktree.create.before") {
const previous = beforeSchemas["worktree.create.before"].parse(input);
const next = beforeSchemas["worktree.create.before"].parse(result);