Skip to content

Commit d31e1b1

Browse files
committed
refactor(extensions): enforce api v4 surface
1 parent 6bc2b17 commit d31e1b1

5 files changed

Lines changed: 15 additions & 275 deletions

File tree

src/coding_agent/extensions/api_v4.zig

Lines changed: 0 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -143,125 +143,3 @@ fn ziDefineAction(L_opt: ?*c.lua_State) callconv(.c) c_int {
143143
return 0;
144144
}
145145

146-
const testing = std.testing;
147-
148-
test "api v4 command registration reports v4 name" {
149-
var state = try lua_runtime.LuaState.init(testing.allocator);
150-
defer state.deinit();
151-
152-
var runner = runner_mod.ExtensionRunner.init(testing.allocator, 0);
153-
defer runner.deinit();
154-
155-
install(&state, &runner);
156-
157-
try state.doString(
158-
\\local cases = {
159-
\\ { function() zi.define.command(nil) end, "zi.define.command:" },
160-
\\ { function() zi.define.tool(nil) end, "zi.define.tool:" },
161-
\\}
162-
\\for _, case in ipairs(cases) do
163-
\\ local ok, err = pcall(case[1])
164-
\\ assert(not ok, "expected API call to fail")
165-
\\ assert(string.find(tostring(err), case[2], 1, true), tostring(err))
166-
\\end
167-
, "api_v4_registration_errors");
168-
}
169-
170-
test "api v4 command installs under define" {
171-
var state = try lua_runtime.LuaState.init(testing.allocator);
172-
defer state.deinit();
173-
174-
var runner = runner_mod.ExtensionRunner.init(testing.allocator, 0);
175-
defer runner.deinit();
176-
177-
install(&state, &runner);
178-
179-
const L = state.L;
180-
_ = c.lua_getglobal(L, "zi");
181-
defer c.lua_pop(L, 1);
182-
try testing.expectEqual(c.LUA_TTABLE, c.lua_type(L, -1));
183-
184-
for (exports) |exp| {
185-
if (std.mem.eql(u8, exp.name, "command") or std.mem.eql(u8, exp.name, "tool")) continue;
186-
_ = c.lua_getfield(L, -1, exp.name.ptr);
187-
defer c.lua_pop(L, 1);
188-
try testing.expectEqual(expectedLuaType(exp.kind), c.lua_type(L, -1));
189-
}
190-
191-
try expectNoExtraZiGlobals(L, -1);
192-
193-
try state.doString(
194-
\\assert(type(zi.json.encode) == "function")
195-
\\assert(type(zi.json.decode) == "function")
196-
\\assert(type(zi.define.command) == "function")
197-
\\assert(type(zi.define.tool) == "function")
198-
\\assert(type(zi.define.provider) == "function")
199-
\\assert(type(zi.define.event) == "function")
200-
\\assert(type(zi.define.keybinding) == "function")
201-
\\local expected_json = { encode = true, decode = true }
202-
\\for k, _ in pairs(zi.json) do
203-
\\ assert(expected_json[k], "unexpected zi.json field: " .. tostring(k))
204-
\\end
205-
\\local legacy = {
206-
\\ "register_" .. "tool",
207-
\\ "register_" .. "command",
208-
\\ "register_" .. "keybinding",
209-
\\ "register_" .. "provider",
210-
\\ "unregister_" .. "provider",
211-
\\ "__register_" .. "builtin_tools",
212-
\\ "system", "spawn", "job", "command", "tool", "provider", "on",
213-
\\}
214-
\\for _, name in ipairs(legacy) do
215-
\\ assert(zi[name] == nil, "legacy API exposed: " .. name)
216-
\\end
217-
, "api_v4_perimeter");
218-
}
219-
220-
fn expectedLuaType(kind: ExportKind) c_int {
221-
return switch (kind) {
222-
.function => c.LUA_TFUNCTION,
223-
.table => c.LUA_TTABLE,
224-
};
225-
}
226-
227-
fn expectNoExtraZiGlobals(L: *c.lua_State, table_idx: c_int) !void {
228-
const abs_idx = c.lua_absindex(L, table_idx);
229-
c.lua_pushnil(L);
230-
while (c.lua_next(L, abs_idx) != 0) {
231-
defer c.lua_pop(L, 1);
232-
try testing.expectEqual(c.LUA_TSTRING, c.lua_type(L, -2));
233-
var len: usize = 0;
234-
const ptr = c.lua_tolstring(L, -2, &len) orelse return error.InvalidZiGlobalName;
235-
try testing.expect(exportNameInManifest(ptr[0..len]));
236-
}
237-
}
238-
239-
fn exportNameInManifest(name: []const u8) bool {
240-
if (std.mem.eql(u8, name, "define") or std.mem.eql(u8, name, "version") or std.mem.eql(u8, name, "extension") or std.mem.eql(u8, name, "schema") or std.mem.eql(u8, name, "doc")) return true;
241-
for (exports) |exp| {
242-
if (std.mem.eql(u8, exp.name, name)) return true;
243-
}
244-
return false;
245-
}
246-
247-
test "api v4 json group reports v4 diagnostic names" {
248-
var state = try lua_runtime.LuaState.init(testing.allocator);
249-
defer state.deinit();
250-
251-
var runner = runner_mod.ExtensionRunner.init(testing.allocator, 0);
252-
defer runner.deinit();
253-
254-
install(&state, &runner);
255-
256-
try state.doString(
257-
\\local cases = {
258-
\\ { function() zi.json.encode(function() end) end, "zi.json.encode:" },
259-
\\ { function() zi.json.decode({}) end, "zi.json.decode:" },
260-
\\}
261-
\\for _, case in ipairs(cases) do
262-
\\ local ok, err = pcall(case[1])
263-
\\ assert(not ok, "expected API call to fail")
264-
\\ assert(string.find(tostring(err), case[2], 1, true), tostring(err))
265-
\\end
266-
, "api_v4_json_errors");
267-
}

src/coding_agent/extensions/job_api.zig

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -198,34 +198,7 @@ fn parseStartRequest(allocator: std.mem.Allocator, L: *c.lua_State) !runner_mod.
198198
const stdout_idx = c.lua_absindex(L, -1);
199199
const mode = try readStringField(allocator, L, stdout_idx, "mode", "chunks");
200200
defer allocator.free(mode);
201-
if (std.mem.eql(u8, mode, "ui_frame") or std.mem.eql(u8, mode, "events")) return error.InvalidOptions;
202-
if (false) {
203-
const protocol = try readStringField(allocator, L, stdout_idx, "protocol", "zi-rgba-frame-v1");
204-
defer allocator.free(protocol);
205-
const format: extension_ui.FrameFormat = if (std.mem.eql(u8, protocol, "zi-rgba-frame-v1"))
206-
.rgba8888
207-
else if (std.mem.eql(u8, protocol, "zi-halfblock-rgb-v1"))
208-
.halfblock_rgb
209-
else
210-
return error.InvalidOptions;
211-
const runner = runnerFromUpvalue(L);
212-
const source = runner.currentLoadSource();
213-
const view = try readStringField(allocator, L, stdout_idx, "view", "default");
214-
errdefer allocator.free(view);
215-
const node = try readStringField(allocator, L, stdout_idx, "node", "surface");
216-
errdefer allocator.free(node);
217-
const default_state_owner_id = if (source) |src| src.provenance.state_owner_id else "job";
218-
const state_owner_id = try readStringField(allocator, L, stdout_idx, "state_owner_id", default_state_owner_id);
219-
errdefer allocator.free(state_owner_id);
220-
request.stdout = .{ .ui_frame = .{
221-
.view = view,
222-
.node = node,
223-
.state_owner_id = state_owner_id,
224-
.generation = runner.generation,
225-
.format = format,
226-
.max_frame_bytes = @min(@as(usize, @intCast(readIntegerField(L, stdout_idx, "max_frame_bytes", limits.frame_bytes))), limits.frame_bytes),
227-
} };
228-
} else if (std.mem.eql(u8, mode, "json_lines")) {
201+
if (std.mem.eql(u8, mode, "json_lines")) {
229202
const raw_max_line_bytes = readIntegerField(L, stdout_idx, "max_line_bytes", 1024 * 1024);
230203
if (raw_max_line_bytes <= 0) return error.InvalidOptions;
231204
request.stdout = .{ .json_lines = .{

src/coding_agent/extensions/lua_tool.zig

Lines changed: 7 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1755,7 +1755,7 @@ test "extension command context publishes host-owned editor buffer actions" {
17551755
try testing.expectEqual(@as(usize, 0), store.editor_actions.items.len);
17561756
}
17571757

1758-
test "ctx.ui v4 exposes retained view, surface frame, and notifications" {
1758+
test "extension command publishes retained view frame and notification" {
17591759
var store = TestStateStore{ .allocator = testing.allocator };
17601760
defer store.deinit();
17611761

@@ -1774,128 +1774,21 @@ test "ctx.ui v4 exposes retained view, surface frame, and notifications" {
17741774
defer runner.endLoadContext();
17751775
try state.doString(
17761776
\\zi.define.command({
1777-
\\ name = "ui-v4-perimeter",
1778-
\\ description = "ui-v4-perimeter",
1777+
\\ name = "ui-publish",
1778+
\\ description = "ui-publish",
17791779
\\ run = function(ctx, _)
1780-
\\ assert(ctx.capabilities().ui == true)
1781-
\\ assert(ctx["c" .. "wd"] == nil)
1782-
\\ assert(ctx["has" .. "_ui"] == nil)
1783-
\\ assert(ctx["ed" .. "itor"] == nil)
1784-
\\ assert(ctx.binding == nil)
1785-
\\ assert(ctx.extension == nil)
1786-
\\ assert(ctx["is" .. "_idle"] == nil)
1787-
\\ assert(ctx["ab" .. "ort"] == nil)
1788-
\\ assert(ctx["send" .. "_user_message"] == nil)
1789-
\\ assert(ctx["send" .. "_message"] == nil)
1790-
\\ assert(ctx["append" .. "_entry"] == nil)
1791-
\\ assert(ctx["has" .. "_pending_messages"] == nil)
1792-
\\ assert(ctx.ui ~= nil)
1793-
\\ local seen = {}
1794-
\\ local count = 0
1795-
\\ for k, v in pairs(ctx.ui) do
1796-
\\ seen[k] = true
1797-
\\ count = count + 1
1798-
\\ assert(type(v) == "function" or k == "capabilities" or k == "view" or k == "surface" or k == "notify")
1799-
\\ end
1800-
\\ assert(count == 4)
1801-
\\ assert(seen.view and seen.surface and seen.notify and seen.capabilities)
18021780
\\ ctx.ui.view.set({ id = "demo" })
18031781
\\ ctx.ui.surface.frame({ id = "demo" })
18041782
\\ ctx.ui.notify.show({ id = "notify", message = "demo" })
1805-
\\ ctx.ui.view.set("not-a-table")
1806-
\\ ctx.ui.surface.frame(nil)
18071783
\\ end,
18081784
\\})
1809-
, "register_ui_v4_perimeter_command");
1785+
, "register_ui_publish_command");
18101786

1811-
try runner.dispatchCommand("ui-v4-perimeter", "");
1787+
try runner.dispatchCommand("ui-publish", "");
18121788
try testing.expectEqual(@as(usize, 2), store.render_count);
18131789
try testing.expectEqual(@as(usize, 1), store.frame_count);
18141790
}
18151791

1816-
test "api v4 section 10 conformance surface and negative drift" {
1817-
var store = TestStateStore{ .allocator = testing.allocator };
1818-
defer store.deinit();
1819-
1820-
var state = try lua_runtime.LuaState.init(testing.allocator);
1821-
defer state.deinit();
1822-
var runner = runner_mod.ExtensionRunner.init(testing.allocator, 55);
1823-
defer runner.deinit();
1824-
runner.attachLuaState(&state);
1825-
runner.bindLuaOwnerThread(std.Thread.getCurrentId());
1826-
var provider_registry = ai.provider.Registry.init(testing.allocator);
1827-
defer provider_registry.deinit();
1828-
try bindTestRuntime(&runner, &store, &provider_registry);
1829-
api_v4.install(&state, &runner);
1830-
1831-
runner.beginLoadContext(testLoadSource());
1832-
defer runner.endLoadContext();
1833-
try state.doString(
1834-
\\local function assert_error(fn)
1835-
\\ local ok = pcall(fn)
1836-
\\ assert(not ok, "expected error")
1837-
\\end
1838-
\\local zi_required = {
1839-
\\ "version", "extension", "define", "json", "schema", "doc",
1840-
\\}
1841-
\\for _, k in ipairs(zi_required) do assert(zi[k] ~= nil, k) end
1842-
\\for _, k in ipairs({"command","tool","keybinding","provider","event","action"}) do assert(type(zi.define[k]) == "function", k) end
1843-
\\for _, k in ipairs({"encode","decode"}) do assert(type(zi.json[k]) == "function", k) end
1844-
\\for _, k in ipairs({"object","string","number","integer","boolean","array","enum"}) do assert(type(zi.schema[k]) == "function", k) end
1845-
\\for _, k in ipairs({"schema","version","fragment","span","line","text","markdown","group","marker","step","is_fragment","validate","to_markdown"}) do assert(zi.doc[k] ~= nil, "doc." .. k) end
1846-
\\for _, k in ipairs({"command","tool","provider","unprovider","on","action","keybinding","system","spawn","job"}) do assert(zi[k] == nil, "old zi." .. k) end
1847-
\\assert_error(function() zi.define.command({ name = "bad", desc = "bad", handler = function() end }) end)
1848-
\\assert_error(function() zi.define.tool({ name = "bad", description = "bad", parameters = {}, execute = function() end }) end)
1849-
\\assert_error(function() zi.define.keybinding({ id = "bad", key = "f8", handler = function() end }) end)
1850-
\\local s = zi.schema.object({ properties = {}, required = {} })
1851-
\\assert(type(s.properties) == "table" and type(s.required) == "table")
1852-
\\zi.define.action("accept", function(ctx, event) _action_seen = event.action end)
1853-
\\zi.define.command({
1854-
\\ name = "v4-conformance",
1855-
\\ description = "v4-conformance",
1856-
\\ run = function(ctx, input)
1857-
\\ _v4_step = "forbidden roots"
1858-
\\ for _, path in ipairs({"cwd","binding","extension","send_user_message","send_message","append_entry","has_pending_messages"}) do assert(ctx[path] == nil, path) end
1859-
\\ assert(ctx.ai.stream == nil)
1860-
\\ assert(ctx.events.on == nil)
1861-
\\ assert(ctx.control.shutdown == nil)
1862-
\\ assert(ctx.ui.render == nil and ctx.ui.clear == nil and ctx.ui.frame == nil and ctx.ui.input == nil and ctx.ui.progress == nil)
1863-
\\ assert(ctx.ui.view.patch == nil)
1864-
\\ _v4_step = "caps"
1865-
\\ local caps = ctx.capabilities()
1866-
\\ for _, k in ipairs({"ui","composer","surface","process","ai","agent","session","state","models","keybinding"}) do assert(type(caps[k]) == "boolean", k) end
1867-
\\ assert(caps.input == nil and caps.shutdown == nil)
1868-
\\ local ui_caps = ctx.ui.capabilities()
1869-
\\ for _, k in ipairs({"view","notify","progress","surface","focus","color","markdown","ansi"}) do assert(type(ui_caps[k]) == "boolean", k) end
1870-
\\ assert(ui_caps.input == nil)
1871-
\\ _v4_step = "positive ui"
1872-
\\ ctx.ui.view.set({ id = "panel", slot = "overlay", root = { type = "view", style = { gap = 1 }, children = { { type = "text", text = "ok" } } } })
1873-
\\ ctx.ui.notify.show({ id = "smoke", message = "ok", annotation = "v4", ttl_ms = 1000 })
1874-
\\ ctx.ui.notify.update("smoke", { done = true })
1875-
\\ ctx.ui.notify.clear("smoke")
1876-
\\ _v4_step = "negative notify"
1877-
\\ assert_error(function() ctx.ui.notify.show("hello", {}) end)
1878-
\\ assert_error(function() ctx.ui.notify.show({ message = "hello", annote = "bad" }) end)
1879-
\\ assert_error(function() ctx.ui.notify.show({ message = "hello", ttl = 1 }) end)
1880-
\\ _v4_step = "negative slots"
1881-
\\ assert_error(function() ctx.ui.view.set({ id = "bad", slot = "notification" }) end)
1882-
\\ assert_error(function() ctx.ui.view.set({ id = "bad", slot = "editor.border.top" }) end)
1883-
\\ assert_error(function() ctx.ui.view.set({ id = "bad", slot = "editor.border.bottom" }) end)
1884-
\\ _v4_step = "negative style"
1885-
\\ assert_error(function() ctx.ui.view.set({ id = "bad", slot = "overlay", root = { type = "view", style = { flex_direction = "row" } } }) end)
1886-
\\ assert_error(function() ctx.ui.view.set({ id = "bad", slot = "overlay", root = { type = "view", style = { flex_grow = 1 } } }) end)
1887-
\\ _v4_step = "negative process"
1888-
\\ assert_error(function() ctx.process.run({ "/bin/true" }, { stdio = "terminal" }) end)
1889-
\\ assert_error(function() ctx.process.start({ argv = { "/bin/true" }, stdout = { mode = "events" } }) end)
1890-
\\ assert_error(function() ctx.process.start({ argv = { "/bin/true" }, stdout = { mode = "ui_frame" } }) end)
1891-
\\ end,
1892-
\\})
1893-
, "v4_conformance_register");
1894-
try runner.dispatchCommand("v4-conformance", "");
1895-
try runner.dispatchUiEvent(.{ .state_owner_id = "state-123", .generation = 55, .view = "panel", .action = "accept" });
1896-
try state.doString("assert(_action_seen == 'accept')", "v4_action_verify");
1897-
}
1898-
18991792
test "extension command resumes after system result" {
19001793
var store = TestStateStore{ .allocator = testing.allocator };
19011794
defer store.deinit();
@@ -2355,7 +2248,7 @@ test "ctx.ui.notify publishes notification render" {
23552248
try testing.expectEqual(@as(?u32, 5000), store.render_specs.items[0].notification.?.ttlMs());
23562249
}
23572250

2358-
test "todo command can call ui render perimeter" {
2251+
test "todo command can publish retained view" {
23592252
var store = TestStateStore{ .allocator = testing.allocator };
23602253
defer store.deinit();
23612254

@@ -2375,7 +2268,7 @@ test "todo command can call ui render perimeter" {
23752268
const tool = try buildAgentTool(testing.allocator, &runner, ext_tool);
23762269
var arena = std.heap.ArenaAllocator.init(testing.allocator);
23772270
defer arena.deinit();
2378-
const add_args = try todoArgs(arena.allocator(), "add", "ship perimeter", null);
2271+
const add_args = try todoArgs(arena.allocator(), "add", "ship view", null);
23792272
const add_result = awaitToolExecutionForCompat(tool.start(arena.allocator(), "todo-1", add_args, abort_signal_mod.cancel.Token.none, null, null), arena.allocator(), abort_signal_mod.cancel.Token.none);
23802273
try testing.expect(!add_result.is_error);
23812274

src/coding_agent/extensions/registries/event_registry.zig

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,6 @@ test "EventRegistry subscribes in order and dispatches correct chain" {
143143
try testing.expectEqualStrings("ext-c", me[0].source_id);
144144
}
145145

146-
test "EventKind reserves the v3 event surface" {
147-
try testing.expectEqual(@as(usize, 36), @typeInfo(EventKind).@"enum".fields.len);
148-
}
149-
150146
test "EventKind.semantics matches spec" {
151147
try testing.expectEqual(Semantics.aggregate, EventKind.resources_discover.semantics());
152148
try testing.expectEqual(Semantics.middleware_cancellable, EventKind.tool_call.semantics());

src/coding_agent/extensions/ui.zig

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ pub const JobEvent = struct {
572572
}
573573
};
574574

575-
test "ui v3 render spec clone owns node tree" {
575+
test "ui render spec clone owns node tree" {
576576
const testing = std.testing;
577577
const child = UiNode{ .text = .{ .id = "child", .text = "hello", .style = .{ .tone = .info } } };
578578
const root = UiNode{ .view = .{ .id = "root-node", .style = .{ .flex_direction = .row, .gap = 1 }, .children = @constCast(&[_]UiNode{child}) } };
@@ -587,7 +587,7 @@ test "ui v3 render spec clone owns node tree" {
587587
try testing.expectEqual(@as(usize, 1), cloned.keys.len);
588588
}
589589

590-
test "ui v3 text span clone owns spans" {
590+
test "ui text span clone owns spans" {
591591
const testing = std.testing;
592592
const spans = [_]TextSpan{
593593
.{ .text = "hello", .style = .{ .tone = .accent, .fg = Color.rgb(255, 0, 0), .bold = true }, .link = "https://span.test" },
@@ -608,7 +608,7 @@ test "ui v3 text span clone owns spans" {
608608
try testing.expect(cloned.text.selectable);
609609
}
610610

611-
test "ui v3 render spec clone owns keys" {
611+
test "ui render spec clone owns keys" {
612612
const testing = std.testing;
613613
const spec = RenderSpec{ .state_owner_id = "owner", .generation = 1, .id = "view", .keys = @constCast(&[_]KeyBinding{ .{ .key = "escape", .action = "close" }, .{ .key = "q", .action = "close" } }) };
614614
var cloned = try RenderSpec.clone(testing.allocator, spec);
@@ -619,7 +619,7 @@ test "ui v3 render spec clone owns keys" {
619619
try testing.expectEqualStrings("q", cloned.keys[1].key);
620620
}
621621

622-
test "ui v3 event clone owns payload" {
622+
test "ui event clone owns payload" {
623623
const testing = std.testing;
624624
var cloned = try UiEvent.clone(testing.allocator, .{ .state_owner_id = "owner", .generation = 2, .view = "demo", .node = "root", .action = "close", .key = "escape", .ctrl = true });
625625
defer cloned.deinit(testing.allocator);
@@ -632,7 +632,7 @@ test "ui v3 event clone owns payload" {
632632
try testing.expectEqual(@as(?[]const u8, null), cloned.value);
633633
}
634634

635-
test "ui v3 input clone owns strings" {
635+
test "ui input clone owns strings" {
636636
const testing = std.testing;
637637
var cloned = try UiNode.clone(testing.allocator, .{ .input = .{ .id = "name", .value = "zi", .placeholder = "filter", .style = .{ .tone = .accent }, .on_input = "typing", .on_change = "rename", .on_submit = "accept" } });
638638
defer cloned.deinit(testing.allocator);
@@ -644,15 +644,15 @@ test "ui v3 input clone owns strings" {
644644
try testing.expectEqualStrings("accept", cloned.input.on_submit.?);
645645
}
646646

647-
test "ui v3 event clone owns input value" {
647+
test "ui event clone owns input value" {
648648
const testing = std.testing;
649649
var cloned = try UiEvent.clone(testing.allocator, .{ .state_owner_id = "owner", .generation = 2, .view = "demo", .node = "name", .type = .change, .action = "rename", .value = "zi" });
650650
defer cloned.deinit(testing.allocator);
651651
try testing.expectEqual(UiEventType.change, cloned.type);
652652
try testing.expectEqualStrings("zi", cloned.value.?);
653653
}
654654

655-
test "ui v3 frame byte validation" {
655+
test "ui frame byte validation" {
656656
const testing = std.testing;
657657
const rgba = FrameSpec{ .state_owner_id = "owner", .generation = 1, .view = "v", .node = "n", .width = 2, .height = 2, .format = .rgba8888, .data = &[_]u8{0} ** 16 };
658658
try rgba.validate();

0 commit comments

Comments
 (0)