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
81 changes: 81 additions & 0 deletions apps/desktop/test/genericAdapterSample.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,13 @@ test("generic adapter sample summarizes event poll responses for a watch loop",
assert.equal(state.has_more, true);
assert.equal(state.should_poll_again, true);
assert.equal(state.should_query_result, true);
assert.equal(state.must_query_results, true);
assert.deepEqual(state.next_poll, {
mode: "drain_page",
immediate: true,
after_event_id: "bridge-event-45",
reason: "has_more_events"
});
assert.equal(state.action_events.length, 2);
assert.equal(state.action_state.lifecycle_status, "conflict");
assert.equal(state.action_state.final, true);
Expand All @@ -1319,6 +1326,80 @@ test("generic adapter sample summarizes event poll responses for a watch loop",
rmSync(tempRoot, { recursive: true, force: true });
});

test("generic adapter sample event state asks for result lookup after terminal action", () => {
const tempRoot = mkdtempSync(join(tmpdir(), "nekodrop-generic-adapter-event-terminal-"));
const responsePath = join(tempRoot, "events-response.json");
writeFileSync(responsePath, JSON.stringify({
events_next_after_id: "bridge-event-46",
events_cursor_state: "ok",
events_has_more: false,
events: [
{
kind: "action.updated",
payload: {
request_id: "adapter-send-001",
action_kind: "bundle.send",
status: "succeeded",
reason: null,
target_device_id: "device-a"
}
}
]
}));

const state = JSON.parse(execFileSync(
process.execPath,
[
sampleCli,
"event-state",
"--response",
responsePath,
"--action-request-id",
"adapter-send-001"
],
{ encoding: "utf8" }
));

assert.equal(state.should_poll_again, false);
assert.equal(state.must_query_results, true);
assert.deepEqual(state.next_poll, {
mode: "query_results",
immediate: false,
after_event_id: "bridge-event-46",
reason: "terminal_action_event"
});

rmSync(tempRoot, { recursive: true, force: true });
});

test("generic adapter sample event state resets missing cursors", () => {
const tempRoot = mkdtempSync(join(tmpdir(), "nekodrop-generic-adapter-event-missing-"));
const responsePath = join(tempRoot, "events-response.json");
writeFileSync(responsePath, JSON.stringify({
events_next_after_id: null,
events_cursor_state: "missing",
events_has_more: false,
events: []
}));

const state = JSON.parse(execFileSync(
process.execPath,
[sampleCli, "event-state", "--response", responsePath],
{ encoding: "utf8" }
));

assert.equal(state.should_poll_again, true);
assert.equal(state.must_query_results, false);
assert.deepEqual(state.next_poll, {
mode: "reset_cursor",
immediate: true,
after_event_id: null,
reason: "cursor_missing"
});

rmSync(tempRoot, { recursive: true, force: true });
});

test("generic adapter sample can build action-scoped event poll requests", () => {
const stdout = execFileSync(
process.execPath,
Expand Down
9 changes: 8 additions & 1 deletion docs/examples/generic-adapter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,14 @@ node docs/examples/generic-adapter/generic-adapter.mjs event-state \
--action-request-id adapter-import-001
```

`event-state` 会返回下一次 cursor、是否应立即继续拉取、匹配的 `action.updated` 摘要、收到的 bundle id 数组和 transfer 事件数量。它不会替代 `actions.results`;终态事件出现后仍应按 `action_request_id` 精确查询结果。
`event-state` 会返回下一次 cursor、`next_poll`、匹配的 `action.updated` 摘要、收到的 bundle id 数组和 transfer 事件数量。它不会替代 `actions.results`;终态事件出现后 `must_query_results=true`,仍应按 `action_request_id` 精确查询结果。

`next_poll.mode` 常见值:

- `drain_page`:还有下一页,马上用 `after_event_id` 继续拉。
- `reset_cursor`:cursor 丢失,丢弃本地 cursor 后从 `null` 重拉快照。
- `query_results`:已经看到终态事件,先查 `actions.results`。
- `wait`:没有新终态,按正常间隔或短等待继续。

事件处理建议:

Expand Down
38 changes: 37 additions & 1 deletion docs/examples/generic-adapter/generic-adapter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,7 @@ function eventStateFromEventsResponse(flags) {
.map((event) => event.payload);
const latestAction = actionEvents.at(-1) ?? null;
const latestActionState = latestAction ? actionEventState(latestAction) : null;
const nextPoll = nextEventPollDecision(response, latestActionState);
return {
cursor: nextCursorFromResponse(flags),
stream_window: {
Expand All @@ -1261,11 +1262,13 @@ function eventStateFromEventsResponse(flags) {
},
event_count: events.length,
has_more: Boolean(response.events_has_more),
should_poll_again: Boolean(response.events_has_more) || response.events_cursor_state === "missing",
should_poll_again: nextPoll.immediate,
next_poll: nextPoll,
action_request_id: actionRequestId,
action_state: latestActionState,
action_events: actionEvents.map(actionEventState),
should_query_result: Boolean(latestActionState?.final),
must_query_results: Boolean(latestActionState?.final),
transfer_event_count: transferEvents.length,
bundle_event_count: bundleEvents.length,
received_bundle_ids: bundleEvents
Expand All @@ -1274,6 +1277,39 @@ function eventStateFromEventsResponse(flags) {
};
}

function nextEventPollDecision(response, latestActionState) {
if (response.events_cursor_state === "missing") {
return {
mode: "reset_cursor",
immediate: true,
after_event_id: null,
reason: "cursor_missing"
};
}
if (Boolean(response.events_has_more)) {
return {
mode: "drain_page",
immediate: true,
after_event_id: response.events_next_after_id ?? null,
reason: "has_more_events"
};
}
if (latestActionState?.final) {
return {
mode: "query_results",
immediate: false,
after_event_id: response.events_next_after_id ?? null,
reason: "terminal_action_event"
};
}
return {
mode: "wait",
immediate: false,
after_event_id: response.events_next_after_id ?? null,
reason: latestActionState ? "action_not_terminal" : "no_matching_action_event"
};
}

function actionEventState(event) {
const lifecycle = event.status ?? "unknown";
return {
Expand Down
Loading