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
1 change: 1 addition & 0 deletions packages/features/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export {
export {
HostedMacCommandClient,
type HostedMacCommandClientOptions,
MAC_COMMAND_NOT_CLAIMED,
} from "./mac-command-client.js";
export {
automationPath,
Expand Down
113 changes: 28 additions & 85 deletions packages/features/src/mac-command-client.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
/**
* mac-command-client.test.ts -- the Mac's side of the commands its account's agents queue, as the Mac observes it.
*
* The held read names the kinds this Mac runs and answers the commands the
* service claimed for it, a command of a kind this build does not know
* read as its id and kind alone rather than spoiling the read, and it
* outlasts the service's hold before it gives up as nothing; a result is
* posted as the service reads it, and is taken or not.
* The live read names the kinds this Mac runs and the account it is for,
* and reads a refused session as signed out; a claim answers the command
* granted or nothing; a result is posted as the service reads it, and is
* taken or not.
*
* Synthetic bearers, ids, and commands throughout.
*/

import assert from "node:assert/strict";
import { it } from "@effect/vitest";
import {
fakeCloudApi,
HTTP_STATUS,
type RecordedRequest,
recordingHttpClient,
} from "@luke/wire/testing";
import { Effect, Fiber } from "effect";
import { TestClock } from "effect/testing";
import { fakeCloudApi, HTTP_STATUS } from "@luke/wire/testing";
import { Effect } from "effect";
import { accountBearer } from "./account-call.js";
import { LIVE_SIGNED_OUT } from "./live-wire.js";
import { HostedMacCommandClient } from "./mac-command-client.js";
import { HostedMacCommandClient, MAC_COMMAND_NOT_CLAIMED } from "./mac-command-client.js";
import { MAC_COMMAND_KIND, type MacCommand, type MacCommandResult } from "./mac-command-wire.js";
import { HOSTED_API_ERROR } from "./service-wire.js";

Expand All @@ -36,8 +29,6 @@ const COMMAND: MacCommand = {
timeoutSeconds: 60,
};

const NEWER_ID = "22222222-2222-4222-8222-222222222222";

const RESULT: MacCommandResult = {
exitCode: 0,
stdout: "nothing to commit\n",
Expand All @@ -57,60 +48,6 @@ function client(): HostedMacCommandClient {
});
}

/** Lets the client's promise steps run until it has asked, so the deadline is armed before the clock moves. */
function untilAsked(requests: readonly RecordedRequest[]): Effect.Effect<void> {
return Effect.gen(function* () {
while (requests.length === 0) yield* Effect.yieldNow;
});
}

it.effect("a held read names this Mac's kinds and answers the commands claimed for it", () =>
Effect.gen(function* () {
const newer = { id: NEWER_ID, kind: "hologram", timeoutSeconds: 60, beam: true };
const api = fakeCloudApi({
"GET /api/mac-commands": { answer: () => ({ commands: [COMMAND, newer] }) },
});

const kinds = [MAC_COMMAND_KIND.SHELL, MAC_COMMAND_KIND.BROWSER];
const waiting = yield* Effect.provide(client().waiting(kinds), api.layer);

assert.deepEqual(waiting, [COMMAND, { id: NEWER_ID, kind: "hologram" }]);
assert.deepEqual(api.credentials(), ["token-1"]);
assert.equal(api.requests()[0]?.searchParams.get("kinds"), "shell,browser");
}),
);

it.effect(
"a shell command from a service before kinds, which names none, is read as a shell command",
() =>
Effect.gen(function* () {
const { kind: _kind, ...older } = COMMAND;
const api = fakeCloudApi({
"GET /api/mac-commands": { answer: () => ({ commands: [older] }) },
});

const waiting = yield* Effect.provide(client().waiting([MAC_COMMAND_KIND.SHELL]), api.layer);

assert.deepEqual(waiting, [COMMAND]);
}),
);

it.effect("a held read outlasts the service's hold, then gives up as nothing", () =>
Effect.gen(function* () {
const silent = recordingHttpClient(() => new Promise<Response>(() => undefined));

const read = yield* Effect.forkChild(
Effect.provide(client().waiting([MAC_COMMAND_KIND.SHELL]), silent.layer),
);
yield* untilAsked(silent.requests);
yield* TestClock.adjust("34 seconds");
assert.equal(read.pollUnsafe(), undefined, "still held short of its deadline");
yield* TestClock.adjust("1 second");

assert.equal(yield* Fiber.join(read), undefined);
}),
);

it.effect("a result is posted as the service reads it, and is taken or not", () =>
Effect.gen(function* () {
const taken = fakeCloudApi({ "POST /api/mac-commands": { answer: () => ({ settled: true }) } });
Expand Down Expand Up @@ -160,20 +97,26 @@ it.effect(
}),
);

it.effect("a claim answers the command granted to this Mac, and nothing where it was not", () =>
Effect.gen(function* () {
const granted = fakeCloudApi({
"POST /api/mac-commands/claim": { answer: () => ({ command: COMMAND }) },
});
assert.deepEqual(yield* Effect.provide(client().claim(COMMAND_ID), granted.layer), COMMAND);
assert.deepEqual(JSON.parse(granted.requests()[0]?.body ?? ""), { id: COMMAND_ID });
it.effect(
"a claim answers the command granted to this Mac, and not claimed where the service refused it",
() =>
Effect.gen(function* () {
const granted = fakeCloudApi({
"POST /api/mac-commands/claim": { answer: () => ({ command: COMMAND }) },
});
assert.deepEqual(yield* Effect.provide(client().claim(COMMAND_ID), granted.layer), COMMAND);
assert.deepEqual(JSON.parse(granted.requests()[0]?.body ?? ""), { id: COMMAND_ID });

const taken = fakeCloudApi({
"POST /api/mac-commands/claim": {
answer: () => ({ error: HOSTED_API_ERROR.NOT_FOUND }),
status: HTTP_STATUS.NOT_FOUND,
},
});
assert.equal(yield* Effect.provide(client().claim(COMMAND_ID), taken.layer), undefined);
}),
const taken = fakeCloudApi({
"POST /api/mac-commands/claim": {
answer: () => ({ error: HOSTED_API_ERROR.NOT_FOUND }),
status: HTTP_STATUS.NOT_FOUND,
},
});
assert.deepEqual(
yield* Effect.provide(client().claim(COMMAND_ID), taken.layer),
MAC_COMMAND_NOT_CLAIMED,
"a refused claim is told apart from one never answered",
);
}),
);
72 changes: 33 additions & 39 deletions packages/features/src/mac-command-client.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,36 @@
import { Effect } from "effect";
import type * as HttpClient from "effect/http/HttpClient";
import type { CallCredential } from "./account-call.js";
import {
askHosted,
askLive,
HOSTED_DEADLINE,
type HostedClient,
hostedClient,
} from "./hosted-link.js";
import { askHosted, askLive, type HostedClient, hostedClient } from "./hosted-link.js";
import { LIVE_SIGNED_OUT, LIVE_USER_QUERY, type MacCommandsLiveAnswer } from "./live-wire.js";
import {
type MacBrowserResult,
type MacCommand,
type MacCommandKind,
type MacCommandRead,
type MacCommandSettleResult,
macCommandKindsQuery,
} from "./mac-command-wire.js";
import { HOSTED_API_ERROR } from "./service-wire.js";

/**
* mac-command-client.ts -- the Mac's side of the commands the account's agents queue for it: the held read that takes them, and each result posted back.
* mac-command-client.ts -- the Mac's side of the commands the account's agents queue for it: the unclaimed ones of its kinds followed live, each claimed by its id, that the Mac listens, and each result posted back.
*
* Both are procedures of the hosted contract (`contract/mac-commands.ts`)
* over the one account call, so the bearer is read fresh per attempt and a
* 401 is renewed and retried once. The read is held by the service while
* nothing waits, so it is asked under the held deadline, which outlasts the
* hold. Neither call fails: a read the service did not answer is nothing,
* which the caller rests after, and a result it did not take is false,
* since its call has already given up on it. A browser command's result,
* which may carry an image, is posted to its own route. A Mac that follows
* its commands live asks for the unclaimed ones of its kinds sealed (or,
* polling, inline), claims each it hears of by its id, and says it listens;
* a seal the service did not answer is a failure the caller asks again, a
* claim not granted is nothing, and a presence not taken is false.
* Each is a procedure of the hosted contract (`contract/live.ts`,
* `contract/mac-commands.ts`) over the one account call, so the bearer is
* read fresh per attempt and a 401 is renewed and retried once. The Mac
* asks for the unclaimed commands of its kinds sealed (or, polling, inline
* and held until they move) for the account it started for, claims each it
* hears of by its id, and says it listens. No call fails: a read refused as
* signed out is `LIVE_SIGNED_OUT` and any other unanswered a failure the
* caller asks again, a claim refused is `MAC_COMMAND_NOT_CLAIMED` and one
* unanswered nothing, and a presence or a result not taken is false, its
* call having given up on it. A browser
* command's result, which may carry an image, is posted to its own route.
*/

/** What a claim the service refused answers: another Mac took the command, or it waited past the claim window. */
export const MAC_COMMAND_NOT_CLAIMED = { claimed: false } as const;

export interface HostedMacCommandClientOptions {
/** What authorizes each call on the signed-in account, read fresh for every attempt. */
credential: CallCredential;
Expand All @@ -53,20 +48,6 @@ export class HostedMacCommandClient {
});
}

/**
* The commands of the kinds named claimed for this Mac, once one waits or
* the service's hold ends; nothing where the read was not answered.
*/
waiting(
kinds: readonly MacCommandKind[],
): Effect.Effect<readonly MacCommandRead[] | undefined, never, HttpClient.HttpClient> {
const input = { kinds: macCommandKindsQuery(kinds) };
return askHosted(this.#hosted.macCommands.waiting, input, HOSTED_DEADLINE.HELD).pipe(
Effect.map((answer): readonly MacCommandRead[] | undefined => answer.commands),
Effect.orElseSucceed(() => undefined),
);
}

/** One command's result posted back; whether the service took it. */
settle(
id: string,
Expand Down Expand Up @@ -122,11 +103,24 @@ export class HostedMacCommandClient {
);
}

/** One command heard of live, claimed for this Mac; nothing where another took it, it waited too long, or the claim was not answered. */
claim(id: string): Effect.Effect<MacCommand | undefined, never, HttpClient.HttpClient> {
/**
* One command heard of live, claimed for this Mac; `MAC_COMMAND_NOT_CLAIMED`
* where the service refused it (another took it, or it waited too long),
* and nothing where the claim was not answered, which the caller may ask
* again.
*/
claim(
id: string,
): Effect.Effect<
MacCommand | typeof MAC_COMMAND_NOT_CLAIMED | undefined,
never,
HttpClient.HttpClient
> {
return askHosted(this.#hosted.macCommands.claim, { id }).pipe(
Effect.map((answer): MacCommand | undefined => answer.command),
Effect.orElseSucceed(() => undefined),
Effect.map((answer): MacCommand => answer.command),
Effect.catch((failed) =>
Effect.succeed(failed._tag === "HostedRefused" ? MAC_COMMAND_NOT_CLAIMED : undefined),
),
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/host/src/compose-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export function hostAssemblyLayer<const Chosen extends HostPlatformConcern>(
? yield* composeMacCommands({
kernel,
account,
accountId,
enabled: Effect.map(
Effect.orDie(settings.store.snapshot()),
(snapshot) => snapshot.stored.macCommands ?? APP_SETTING_SCHEMA.macCommands.default,
Expand Down
Loading
Loading