Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .fas-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"formatCheckCommand": "npm run format:check",
"formatFixCommand": "npm run format",
"typecheckCommand": "npm run typecheck",
"testCommand": "pnpm --filter ignite-element test && pnpm run test:scripts && pnpm run test:examples",
"testCommand": "pnpm --filter ignite-element test && pnpm run test:scripts && pnpm run test:examples -- --require-covered-packages-match-discovered --covers-package examples/adapters/mobx --covers-package examples/adapters/redux --covers-package examples/adapters/xstate --covers-package examples/agents/smart-home --covers-package examples/apps/form-with-validation --covers-package examples/apps/spa-router",
"replayCommand": "",
"verifyScriptMode": "platform",
"screenshotArtifactDir": ".fas/artifacts/screenshots",
Expand Down
11 changes: 11 additions & 0 deletions .fas/TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1660,6 +1660,17 @@ No active tasks.
- Policy sensitivity: standard
- Blast radius: cross-cutting

### Task: Fix smart-home GAPS #4 by adding focused async/long-running effect coverage for act+ack versus observe-stream settlement before Phase C

- Title: Fix smart-home GAPS #4 by adding focused async/long-running effect coverage for act+ack versus observe-stream settlement before Phase C
- Mode: single-agent
- Status: review
- Owner: reviewer
- Brief: .fas/tasks/fix-smart-home-gaps-4-by-adding-focused-async-long-running-e.md
- Verification lane: fast
- Policy sensitivity: standard
- Blast radius: cross-cutting

## Template

### Task: <short task title>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Fix smart-home GAPS #4 by adding focused async/long-running effect coverage for act+ack versus observe-stream settlement

## Source
Created with `fas create-task` on 2026-06-30.

## Problem
The smart-home dogfood currently proves synchronous command acknowledgement only. GAPS #4 calls out the missing contract case: a command acknowledges before a longer-running scene transition settles, and the later state/event must arrive through the observe stream rather than being folded into `run()`'s acknowledgement result. Add a focused pre-Phase-C example/test for that behavior without building the full terminal-to-browser bridge.

## Acceptance criteria
- The work is tracked in `.fas/TASKS.md`.
- The task has a clear implementation and verification plan before execution starts.
- The smart-home example has an async scene transition command that returns an acknowledgement view before the scene is fully applied.
- `igniteTools(...).observe(...)` observes the later view/event settlement for that transition.
- The focused smart-home runtime test fails before the implementation and passes after it.
- `examples/agents/smart-home/GAPS.md` marks gap #4 fixed and leaves Phase C responsible for cross-runtime bridge gaps only.

## Proposed solution
- Add an XState delayed transition to the smart-home machine for a dedicated async scene command. Keep `run()` act+ack semantics unchanged: the returned observation captures the pending scene at acknowledgement. Use `igniteTools.observe()` in the test to assert the eventual settled view and `scene-applied` event after the delayed transition.

## Alternatives considered
- Build the full Phase C terminal-to-browser bridge now: rejected for this task because the user asked to clear the remaining GAPS first, and #4 can be proven with a smaller smart-home example test.
- Add a bounded `settle` option to `execute()` now: rejected because #4 is a coverage/contract gap; no API need has been proven yet.

## Affected files
- examples/agents/smart-home/src/home.ts
- examples/agents/smart-home/src/agentLoop.test.ts
- examples/agents/smart-home/GAPS.md
- scripts/test-examples.mjs
- scripts/__tests__/test-examples.test.mjs
- package.json
- .fas-config.json

## Scope Amendments
- Scope is intentionally limited to the smart-home example and its gap tracker; no runtime API or provider dialect changes are planned.
- Validation exposed that FAS could not recognize the top-level examples lane as covering non-workspace example package tests. This task also updates the existing example runtime-test lane with explicit covered-package assertions and points FAS at `test:full`, without making examples workspace members.

## Implementation plan
- First add the failing smart-home test for act+ack versus observe-stream settlement.
- Add the minimal async scene transition command and view field needed to make the test pass.
- Update `GAPS.md` to mark #4 fixed and describe what remains deferred to Phase C.
- Add a small example-runner coverage marker so the FAS package-test gate recognizes that full verification covers changed non-workspace example tests.

## Verification plan
- Run the focused smart-home example test.
- Run the script test covering the example runtime-test lane marker.
- Run the full example runtime-test lane.
- Run `fas validate-task` for the inner-loop verification gate.
- Run `.fas/scripts/verify.sh --full` at the final release-quality gate when tracked files change.

## Risks
- XState delayed transitions use timers; keep the delay short and use Vitest fake timers in the focused test to avoid flakiness.
- Do not change existing synchronous `runScene` behavior; add the async command separately so existing dogfood expectations stay stable.

## Dependencies
- None known at task creation.

## Open questions
- None captured at task creation.

## Artifact links
- Planning: `.fas/state/planning.json`
- Task packet: `.fas/state/task-packet.json`
- Commit plan: `.fas/state/commit-plan.json`
- Verification: `.fas/state/verification/latest.json`
- Review: `.fas/state/boundary-review-findings.md`
- Workflow: `.fas/state/workflows/`
21 changes: 13 additions & 8 deletions examples/agents/smart-home/GAPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,19 @@ schema-declared events and derived view transitions with a standard
`unsubscribe()` handle, so the agent loop can stay on one act → observe → act
surface instead of calling runtime `on()` / `watchView()` directly.

## 4. Async / long-running effects (act+ack vs settle) are untested here

Scenes in this example apply **synchronously**, so `run()`'s acknowledgement
snapshot already reflects the full effect. The interesting contract case — `run()`
returns at acknowledgement while the effect settles over time — needs a genuinely
async scene (real-time transition) or a remote actor. Phase C (terminal↔browser
over a transport) is the natural place to exercise it; it will show whether a
bounded `settle` opt-in on `execute()` is warranted (currently deferred).
## 4. ✅ FIXED — async / long-running effects are observed after act+ack

**Was:** scenes in this example applied **synchronously**, so `run()`'s
acknowledgement snapshot already reflected the full effect. The interesting
contract case — `run()` returns at acknowledgement while the effect settles over
time — was untested here.

**Fixed in this PR:** the smart-home now has a delayed `transitionScene` command
that acknowledges immediately with `pendingScene` in the view, then settles via
the runtime observation stream. The focused test proves `run()` keeps act+ack
semantics while `igniteTools(...).observe(...)` receives the later settled view
and `scene-applied` event. Phase C still owns the broader terminal↔browser
transport and cross-runtime bridge gaps.

## 5. Scalar `value`-wrapping costs LLM legibility (known Option D trade-off)

Expand Down
137 changes: 135 additions & 2 deletions examples/agents/smart-home/src/agentLoop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
// AND a stress test of the agent API surface — varied command input schemas,
// the Option D scalar round-trip, the event observation stream, and errors-as-
// values — encoded as always-on assertions.
import { igniteTools } from "ignite-element/tools";
import { igniteTools, isOk } from "ignite-element/tools";
import {
type AnthropicResponse,
anthropic,
} from "ignite-element/tools/anthropic";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { runHomeAgent } from "./agentLoop";
import { createHome, DOORS, ROOMS, SCENES } from "./home";
import { type Model, scriptedModel } from "./model";
Expand Down Expand Up @@ -216,6 +216,139 @@ describe("smart-home agent — scripted session (round-trip, headless)", () => {
expect(home.getView().activeScene).toBe("morning");
});

it("observes a delayed scene after run() acknowledges the pending view", async () => {
vi.useFakeTimers();
const home = createHome();
const tools = igniteTools(home);
const observations: unknown[] = [];
const subscription = tools.observe((observation) => {
observations.push(observation);
});

try {
const result = await tools.run({
name: "transitionScene",
input: "morning",
});

expect(isOk(result)).toBe(true);
if (!isOk(result)) {
throw new Error(
`Expected transitionScene to run: ${result.error.kind}`,
);
}

expect(result.value.events.map((event) => event.type)).not.toContain(
"scene-applied",
);
expect(result.value.view).toMatchObject({
activeScene: null,
pendingScene: "morning",
lights: { living: false, bedroom: false, kitchen: false },
});

const interimResult = await tools.run({
name: "setThermostat",
input: { room: "living", temp: 69 },
});

expect(isOk(interimResult)).toBe(true);
if (!isOk(interimResult)) {
throw new Error(
`Expected setThermostat to run: ${interimResult.error.kind}`,
);
}

expect(interimResult.value.view).toMatchObject({
activeScene: null,
pendingScene: "morning",
thermostat: { living: 69 },
});

await vi.runOnlyPendingTimersAsync();

expect(home.getView()).toMatchObject({
activeScene: "morning",
pendingScene: null,
lights: { living: true, bedroom: true, kitchen: true },
thermostat: { living: 70 },
});
expect(observations).toEqual(
expect.arrayContaining([
{
type: "event",
event: { type: "scene-applied", payload: { scene: "morning" } },
},
expect.objectContaining({
type: "view",
view: expect.objectContaining({
activeScene: "morning",
pendingScene: null,
lights: expect.objectContaining({
living: true,
bedroom: true,
kitchen: true,
}),
}),
}),
]),
);
} finally {
subscription.unsubscribe();
vi.useRealTimers();
}
});

it("restarts a delayed scene when transitionScene is repeated", async () => {
vi.useFakeTimers();
const home = createHome();
const tools = igniteTools(home);

try {
const firstResult = await tools.run({
name: "transitionScene",
input: "morning",
});
expect(isOk(firstResult)).toBe(true);

await vi.advanceTimersByTimeAsync(10);

const secondResult = await tools.run({
name: "transitionScene",
input: "movie",
});

expect(isOk(secondResult)).toBe(true);
if (!isOk(secondResult)) {
throw new Error(
`Expected transitionScene to run: ${secondResult.error.kind}`,
);
}

expect(secondResult.value.view).toMatchObject({
activeScene: null,
pendingScene: "movie",
});

await vi.advanceTimersByTimeAsync(20);

expect(home.getView()).toMatchObject({
activeScene: null,
pendingScene: "movie",
});

await vi.advanceTimersByTimeAsync(5);

expect(home.getView()).toMatchObject({
activeScene: "movie",
pendingScene: null,
lights: { living: false },
});
} finally {
vi.useRealTimers();
}
});

it("returns defensive copies from the derived view", () => {
const home = createHome();
const view = home.getView();
Expand Down
Loading
Loading