Problem
The gateway-disconnect regression (Issue A) occurred because RemoteClawApp was missing 3 required host-interface fields, and no test exercised the real class. The existing tests in ui/src/ui/app-lifecycle.node.test.ts and ui/src/ui/app-lifecycle-connect.node.test.ts construct synthetic plain-object hosts with connectGeneration: 0 hand-set in fixtures:
function createHost() {
return {
basePath: "",
client: { stop: vi.fn() },
connectGeneration: 0, // hand-set — matches type contract
...
};
}
Zero tests instantiate the actual RemoteClawApp Lit class. Fixture matches the interface; production class may not. Result: fixture-passes-class-fails gap invisible to grep, coverage tools, and type-checker.
Fix
Add a smoke test at ui/src/ui/app.smoke.test.ts (or equivalent) that:
- Registers the custom element by importing
app.ts
- Creates a
RemoteClawApp instance via document.createElement("remoteclaw-app")
- Asserts every required field of
LifecycleHost, GatewayHost, and ToolStreamHost is !== undefined on the instance
Example
// ui/src/ui/app.smoke.test.ts
import "./app.ts"; // registers the custom element
import { describe, it, expect } from "vitest";
describe("RemoteClawApp instance — host interface compliance", () => {
it("initializes every required LifecycleHost field", () => {
const el = document.createElement("remoteclaw-app") as any;
const required = [
"basePath", "connectGeneration", "tab", "assistantName",
"assistantAvatar", "assistantAgentId", "serverVersion",
"chatHasAutoScrolled", "chatManualRefreshInFlight", "chatLoading",
"chatMessages", "chatToolMessages", "chatStream",
"logsAutoFollow", "logsAtBottom", "logsEntries",
"popStateHandler", "topbarObserver",
];
for (const field of required) {
expect(el[field], `missing LifecycleHost field: ${field}`).not.toBeUndefined();
}
});
it("initializes every required GatewayHost field", () => {
const el = document.createElement("remoteclaw-app") as any;
const required = [
"settings", "password", "clientInstanceId", "client", "connected",
"hello", "lastError", "lastErrorCode", "eventLogBuffer", "eventLog",
"tab", "presenceEntries", "presenceError", "presenceStatus",
"agentsLoading", "agentsList", "agentsError",
"toolsCatalogLoading", "toolsCatalogError", "toolsCatalogResult",
"debugHealth", "assistantName", "assistantAvatar", "assistantAgentId",
"serverVersion", "sessionKey", "chatRunId", "refreshSessionsAfterChat",
"execApprovalQueue", "execApprovalError", "updateAvailable",
];
for (const field of required) {
expect(el[field], `missing GatewayHost field: ${field}`).not.toBeUndefined();
}
});
it("initializes every required ToolStreamHost field", () => {
const el = document.createElement("remoteclaw-app") as any;
const required = [
"sessionKey", "chatRunId", "chatStream", "chatStreamStartedAt",
"chatStreamSegments", "toolStreamById", "toolStreamOrder",
"chatToolMessages", "toolStreamSyncTimer",
];
for (const field of required) {
expect(el[field], `missing ToolStreamHost field: ${field}`).not.toBeUndefined();
}
});
});
Stronger variant (optional)
Parse the host interface at test time (via tsc --emitDeclarationOnly or a type-introspection helper) and auto-derive the required-field list. This way new fields added to the interface are automatically asserted without a test update — closing the gap permanently. Out of scope for the initial PR; consider as a follow-up.
Acceptance criteria
Commit message
test(ui): add smoke test instantiating RemoteClawApp and asserting required host-interface fields — defense-in-depth against sync regressions
References
- Related: Issue A (fix the underlying regression)
- Related: Issue B (remove type-erasure casts that enabled the regression)
- Related: Issue H (extend assertion list when auditing additional host interfaces)
Problem
The gateway-disconnect regression (Issue A) occurred because
RemoteClawAppwas missing 3 required host-interface fields, and no test exercised the real class. The existing tests inui/src/ui/app-lifecycle.node.test.tsandui/src/ui/app-lifecycle-connect.node.test.tsconstruct synthetic plain-object hosts withconnectGeneration: 0hand-set in fixtures:Zero tests instantiate the actual
RemoteClawAppLit class. Fixture matches the interface; production class may not. Result: fixture-passes-class-fails gap invisible to grep, coverage tools, and type-checker.Fix
Add a smoke test at
ui/src/ui/app.smoke.test.ts(or equivalent) that:app.tsRemoteClawAppinstance viadocument.createElement("remoteclaw-app")LifecycleHost,GatewayHost, andToolStreamHostis!== undefinedon the instanceExample
Stronger variant (optional)
Parse the host interface at test time (via
tsc --emitDeclarationOnlyor a type-introspection helper) and auto-derive the required-field list. This way new fields added to the interface are automatically asserted without a test update — closing the gap permanently. Out of scope for the initial PR; consider as a follow-up.Acceptance criteria
ui/src/ui/app.smoke.test.tsexists.LifecycleHost,GatewayHost,ToolStreamHostis defined on a freshRemoteClawAppinstance.pnpm testpasses.Commit message
References