From 1b96dfe0bd9b359df7114be84676083310a001f3 Mon Sep 17 00:00:00 2001 From: Elie Rotenberg Date: Sun, 25 Oct 2026 17:21:54 +0000 Subject: [PATCH 1/2] test(release): assert the exact errors of publication and tag checks Several release control tests accept any failure. The publication checks search the whole error message, which also contains the script text with every expected `::error::` line, and the tag checks accept any rejection or the fixture's own catch-all error. Assert the exact error each failing step reports, matching printed annotations from the start of an output line, and make the tag fixture fail the history check explicitly. release: - Anchor publication step errors to the annotation line they print. - Assert the exit status of steps that fail without output. - Assert the exact tag verification failures, including a synthetic history check failure. --- .../release/controls/publication.spec.test.ts | 84 ++++++++++++------- test/release/controls/tag.spec.test.ts | 37 ++++++-- 2 files changed, 82 insertions(+), 39 deletions(-) diff --git a/test/release/controls/publication.spec.test.ts b/test/release/controls/publication.spec.test.ts index 87fa92b..1c88ef7 100644 --- a/test/release/controls/publication.spec.test.ts +++ b/test/release/controls/publication.spec.test.ts @@ -238,7 +238,7 @@ describe.skipIf(process.platform !== "linux")("registry confirmation", () => { it("rejects a conflicting npm archive immediately", async () => { const fixture = await createFixture({ response: "sha512-other" }); await expect(fixture.run("Confirm npm publication")).rejects.toThrow( - "does not match the approved archive", + /^::error::The npm version does not match the approved archive\./mu, ); expect(await fixture.count()).toBe(1); }); @@ -246,7 +246,7 @@ describe.skipIf(process.platform !== "linux")("registry confirmation", () => { it("stops after six unavailable npm responses", async () => { const fixture = await createFixture({ response: null, failureCount: 10 }); await expect(fixture.run("Confirm npm publication")).rejects.toThrow( - "Cannot confirm npm publication", + /^::error::Cannot confirm npm publication\./mu, ); expect(await fixture.count()).toBe(6); }); @@ -256,7 +256,7 @@ describe.skipIf(process.platform !== "linux")("registry confirmation", () => { async (response) => { const fixture = await createFixture({ response }); await expect(fixture.run("Confirm npm publication")).rejects.toThrow( - "Cannot confirm npm publication", + /^::error::Cannot confirm npm publication\./mu, ); }, ); @@ -295,7 +295,9 @@ describe.skipIf(process.platform !== "linux")("registry confirmation", () => { const fixture = await createFixture({ response }); await expect( fixture.run("Confirm Marketplace publication"), - ).rejects.toThrow("Marketplace version is not visible"); + ).rejects.toThrow( + /^::error::The Marketplace version is not visible yet\./mu, + ); expect(await fixture.count()).toBe(61); }, ); @@ -336,7 +338,7 @@ describe.skipIf(process.platform !== "linux")("publication recovery", () => { directory: process.cwd(), environment: { RELEASE_ENABLED: value }, }), - ).rejects.toThrow(); + ).rejects.toMatchObject({ code: 1 }); }, ); @@ -362,7 +364,9 @@ describe.skipIf(process.platform !== "linux")("publication recovery", () => { httpStatus: 404, uploadFailure: true, }); - await expect(fixture.run("Publish npm package")).rejects.toThrow(); + await expect(fixture.run("Publish npm package")).rejects.toMatchObject({ + code: 1, + }); await expect(fixture.run("Publish npm package")).resolves.toContain( "already exists", ); @@ -376,7 +380,7 @@ describe.skipIf(process.platform !== "linux")("publication recovery", () => { ])("rejects conflicting or malformed npm metadata %j", async (response) => { const fixture = await createFixture({ response }); await expect(fixture.run("Publish npm package")).rejects.toThrow( - "does not match", + /^::error::The existing npm version does not match the approved archive\./mu, ); expect(await fixture.uploadCount()).toBe(0); }); @@ -386,7 +390,7 @@ describe.skipIf(process.platform !== "linux")("publication recovery", () => { async (httpStatus) => { const fixture = await createFixture({ response: {}, httpStatus }); await expect(fixture.run("Publish npm package")).rejects.toThrow( - "Cannot determine", + /^::error::Cannot determine npm publication state\./mu, ); expect(await fixture.uploadCount()).toBe(0); }, @@ -394,7 +398,9 @@ describe.skipIf(process.platform !== "linux")("publication recovery", () => { it("does not publish when npm state cannot be fetched", async () => { const fixture = await createFixture({ response: {}, failureCount: 1 }); - await expect(fixture.run("Publish npm package")).rejects.toThrow(); + await expect(fixture.run("Publish npm package")).rejects.toMatchObject({ + code: 1, + }); expect(await fixture.uploadCount()).toBe(0); }); @@ -419,28 +425,46 @@ describe.skipIf(process.platform !== "linux")("publication recovery", () => { }); it.each([ - "Check existing Marketplace version", - "Confirm Marketplace publication", - ])("rejects conflicting Marketplace bytes during %s", async (name) => { - const fixture = await createFixture({ - response: marketplaceResponse(version), - downloadArchive: "conflicting archive", - }); - await expect(fixture.run(name)).rejects.toThrow("does not match"); - expect(await fixture.output()).toBe(""); - }); + [ + "Check existing Marketplace version", + /^::error::The existing Marketplace version does not match the approved archive\./mu, + ], + [ + "Confirm Marketplace publication", + /^::error::The Marketplace download does not match the approved archive\./mu, + ], + ])( + "rejects conflicting Marketplace bytes during %s", + async (name, message) => { + const fixture = await createFixture({ + response: marketplaceResponse(version), + downloadArchive: "conflicting archive", + }); + await expect(fixture.run(name)).rejects.toThrow(message); + expect(await fixture.output()).toBe(""); + }, + ); it.each([ - "Check existing Marketplace version", - "Confirm Marketplace publication", - ])("rejects unavailable Marketplace downloads during %s", async (name) => { - const fixture = await createFixture({ - response: marketplaceResponse(version), - downloadFailure: true, - }); - await expect(fixture.run(name)).rejects.toThrow(); - expect(await fixture.output()).toBe(""); - }); + [ + "Check existing Marketplace version", + /^::error::The Marketplace version already exists but cannot be downloaded\./mu, + ], + [ + "Confirm Marketplace publication", + /^::error::The Marketplace version is not visible yet\./mu, + ], + ])( + "rejects unavailable Marketplace downloads during %s", + async (name, message) => { + const fixture = await createFixture({ + response: marketplaceResponse(version), + downloadFailure: true, + }); + await expect(fixture.run(name)).rejects.toThrow(message); + expect(await fixture.output()).toBe(""); + }, + ); it.each([{}, { results: null }])( "does not upload on malformed Marketplace lookup %j", @@ -448,7 +472,7 @@ describe.skipIf(process.platform !== "linux")("publication recovery", () => { const fixture = await createFixture({ response }); await expect( fixture.run("Check existing Marketplace version"), - ).rejects.toThrow(); + ).rejects.toThrow("Cannot iterate over null"); expect(await fixture.output()).toBe(""); }, ); diff --git a/test/release/controls/tag.spec.test.ts b/test/release/controls/tag.spec.test.ts index 06f7182..4b0edf4 100644 --- a/test/release/controls/tag.spec.test.ts +++ b/test/release/controls/tag.spec.test.ts @@ -3,6 +3,7 @@ */ import { expect, it, vi } from "vitest"; +import { ZodError } from "zod"; import { verifyReleaseTag } from "../../../scripts/release/tag.js"; const commit = "1".repeat(40); @@ -12,7 +13,7 @@ const tag = "v1.2.3"; const repository = "synthetic/project"; const createFixture = () => { - const gitResultMap = new Map([ + const gitResultMap = new Map([ ["rev-parse HEAD", commit], [`cat-file -t refs/tags/${tag}`, "tag"], [`rev-parse refs/tags/${tag}^{commit}`, commit], @@ -31,6 +32,9 @@ const createFixture = () => { if (result === undefined) { throw new Error("Unexpected Git operation."); } + if (result instanceof Error) { + throw result; + } return result; }); const readGitHubJson = vi.fn(async (path: string) => { @@ -57,19 +61,34 @@ it("accepts one signed tag targeting the workflow checkout on main", async () => }); it.each([ - ["rev-parse HEAD", alternativeSha], - [`cat-file -t refs/tags/${tag}`, "commit"], - [`rev-parse refs/tags/${tag}^{commit}`, alternativeSha], -])("rejects inconsistent local state: %s", async (command, result) => { + [ + "rev-parse HEAD", + alternativeSha, + "The checkout must match the workflow commit.", + ], + [ + `cat-file -t refs/tags/${tag}`, + "commit", + "Publication requires an annotated tag.", + ], + [ + `rev-parse refs/tags/${tag}^{commit}`, + alternativeSha, + "The release tag must target the workflow commit.", + ], +])("rejects inconsistent local state: %s", async (command, result, message) => { const { gitResultMap, verify } = createFixture(); gitResultMap.set(command, result); - await expect(verify()).rejects.toThrow(); + await expect(verify()).rejects.toThrow(message); }); it("rejects a checkout outside main history", async () => { const { gitResultMap, verify } = createFixture(); - gitResultMap.delete(`merge-base --is-ancestor ${commit} origin/main`); - await expect(verify()).rejects.toThrow(); + gitResultMap.set( + `merge-base --is-ancestor ${commit} origin/main`, + new Error("Synthetic merge-base failure."), + ); + await expect(verify()).rejects.toThrow("Synthetic merge-base failure."); }); it("rejects a remote tag move", async () => { @@ -99,5 +118,5 @@ it("rejects a signed tag with a different name", async () => { it("rejects an unverified signature", async () => { const { signature, verify } = createFixture(); signature.verification.verified = false; - await expect(verify()).rejects.toThrow(); + await expect(verify()).rejects.toThrow(ZodError); }); From ecb38d4cb870835baf7e07681b864caa3a4071f9 Mon Sep 17 00:00:00 2001 From: Elie Rotenberg Date: Sun, 25 Oct 2026 17:22:10 +0000 Subject: [PATCH 2/2] chore: replace Biome with Oxfmt and Oxlint The JavaScript tooling ecosystem is adopting Oxc, and the maintainers' other projects are moving their formatting and linting to it. Staying on Biome here would keep a separate toolchain, configuration, and editor setup to maintain. Replace it with Oxfmt for formatting and Oxlint with type-aware rules for linting, keep the rules Biome enforced, and adapt the code and tests to the stricter checks. repository: - Format TypeScript, JSON, Markdown, and YAML with Oxfmt, keeping the previous line width and import grouping. - Sort the top-level keys of the package manifest with the formatter. - Lint with the correctness, suspicious, perf, and pedantic categories plus Oxlint equivalents of Biome's recommended rules. - Give extension host tests a nested TypeScript project so type-aware rules resolve their Mocha types. - Run both tools through the existing check, fix, and format scripts. - Describe the new tools in the agent guide and the coding style skill. - Adjust sources to the new rules without changing behavior. - Type test mocks with their real contracts and assert the exact errors thrown. - Read registered callbacks from mocks instead of no-op placeholders. - Run the packed CLI's signal check as its own POSIX-only test. devcontainer: - Use the Oxc extension for formatting and fixes on save. extension: - Mark the queue poll interval callback as intentionally unawaited. release: - Validate GitHub response chunks before counting them. --- .agents/skills/coding-style/SKILL.md | 4 +- .agents/skills/commit-messages/SKILL.md | 1 + .devcontainer/devcontainer.json | 9 +- .oxfmtrc.json | 13 + .oxlintrc.json | 100 ++ AGENTS.md | 2 +- README.md | 52 +- biome.json | 37 - package.json | 177 ++-- pnpm-lock.yaml | 605 ++++++++++-- pnpm-workspace.yaml | 4 +- scripts/release/preflight.ts | 8 +- scripts/test/extension.ts | 3 +- scripts/test/release.ts | 3 +- src/cli/agent/setup.spec.test.ts | 16 +- src/cli/agent/skill.spec.test.ts | 5 +- src/cli/cli.spec.test.ts | 8 +- src/cli/run.spec.test.ts | 2 +- .../delivery/adapter/editor.e2e.test.ts | 4 +- .../delivery/adapter/editor.spec.test.ts | 19 +- .../adapter/windows/notification.spec.test.ts | 4 +- .../adapter/windows/taskbar.spec.test.ts | 29 +- .../delivery/dispatcher.spec.test.ts | 191 +++- src/extension/delivery/dispatcher.ts | 9 +- src/extension/diagnostic.spec.test.ts | 27 +- src/extension/diagnostic.ts | 4 +- src/extension/extension.ts | 13 +- src/extension/queue/consumer.spec.test.ts | 60 +- src/extension/queue/file-system.spec.test.ts | 22 +- src/extension/queue/file-system.ts | 6 +- .../extension/tsconfig.json | 4 +- src/extension/windows/bridge.spec.test.ts | 8 +- src/extension/windows/bridge.ts | 4 +- src/extension/workspace-consumer.spec.test.ts | 53 +- src/extension/workspace-consumer.ts | 6 +- .../agent/claude-code/adapter.spec.test.ts | 3 +- .../agent/claude-code/config.spec.test.ts | 3 +- src/integration/agent/claude-code/config.ts | 2 +- .../agent/codex/adapter.spec.test.ts | 3 +- .../agent/codex/config.spec.test.ts | 3 +- src/integration/agent/codex/config.ts | 2 +- src/integration/agent/hook.spec.test.ts | 20 +- src/integration/agent/setup.spec.test.ts | 12 +- src/library/notify.spec.test.ts | 2 +- src/protocol/notification.spec.test.ts | 12 +- src/queue/native.ts | 4 +- src/queue/queue.ts | 2 +- src/queue/test/file-system.ts | 3 +- src/workspace/resolution.spec.test.ts | 9 +- src/workspace/runtime.spec.test.ts | 16 +- test/package.spec.test.ts | 929 +++++++++--------- .../release/controls/publication.spec.test.ts | 10 +- test/release/controls/tag.spec.test.ts | 44 +- test/release/vsix.spec.test.ts | 208 ++-- test/vscode.ts | 5 +- 55 files changed, 1737 insertions(+), 1067 deletions(-) create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json delete mode 100644 biome.json rename tsconfig.e2e.json => src/extension/tsconfig.json (50%) diff --git a/.agents/skills/coding-style/SKILL.md b/.agents/skills/coding-style/SKILL.md index 504d131..09bedbf 100644 --- a/.agents/skills/coding-style/SKILL.md +++ b/.agents/skills/coding-style/SKILL.md @@ -5,7 +5,7 @@ description: "TypeScript coding conventions for Busy Octopus. Use when writing, # Busy Octopus Coding Style -These rules cover design choices that TypeScript, Biome, and Vitest do not fully express. Let repository configuration own mechanical formatting and lint rules. Read `.agents/skills/code-docs/SKILL.md` when adding or reviewing code documentation. +These rules cover design choices that TypeScript, Oxlint, Oxfmt, and Vitest do not fully express. Let repository configuration own mechanical formatting and lint rules. Read `.agents/skills/code-docs/SKILL.md` when adding or reviewing code documentation. Do not insert line breaks solely to enforce a source width. Keep each Markdown paragraph and list item on one source line, and use line breaks in code documentation only when they express semantic structure required by the code documentation conventions. @@ -19,7 +19,7 @@ When one style violation is found, scan every in-scope authored file for the sam - Never use `any`. Accept uncertain values as `unknown`, narrow them, and use Zod for untrusted runtime input. - Avoid type assertions. `as const` is allowed. Any other assertion requires a precise explanation and explicit human approval when a sound refactor, narrowing check, `satisfies`, or runtime schema cannot express the invariant. -- Do not add suppression comments such as `@ts-ignore`, `@ts-expect-error`, or Biome ignores without explicit approval. Compile-failure tests may use `@ts-expect-error` when TypeScript checks the directive itself. +- Do not add suppression comments such as `@ts-ignore`, `@ts-expect-error`, Oxlint disable comments, or Oxfmt ignore comments without explicit approval. Compile-failure tests may use `@ts-expect-error` when TypeScript checks the directive itself. - Never change runtime behavior merely to silence static analysis. - Prefer inference for local values. Type public APIs, parameters, generic constraints, and non-obvious return contracts explicitly. - Derive related types with TypeScript utility types instead of duplicating shapes. Give generic parameters descriptive names. diff --git a/.agents/skills/commit-messages/SKILL.md b/.agents/skills/commit-messages/SKILL.md index c3a7341..fcbcb54 100644 --- a/.agents/skills/commit-messages/SKILL.md +++ b/.agents/skills/commit-messages/SKILL.md @@ -3,6 +3,7 @@ name: commit-messages description: "Commit message and pull request description format for the Busy Octopus repository. Use when creating git commits, writing or reviewing commit messages, drafting or reviewing pull request descriptions, preparing a PR body, or publishing a pull request. Keywords: git, commit, conventional commits, pull request, PR description, PR body, GitHub, gh." allowed-tools: Read, Shell, Grep, Glob --- + # Commit and Pull Request Message Guidelines Commit message conventions for the Busy Octopus repository, following [Conventional Commits](https://www.conventionalcommits.org/). diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3f62628..7526c99 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,21 +3,20 @@ "vscode": { "extensions": [ "Anthropic.claude-code", - "biomejs.biome", "github.vscode-github-actions", "openai.chatgpt", + "oxc.oxc-vscode", "redhat.vscode-yaml", "TypeScriptTeam.native-preview", "timonwong.shellcheck" ], "settings": { - "[javascript][javascriptreact][json][jsonc][typescript][typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome", + "[javascript][javascriptreact][json][jsonc][markdown][typescript][typescriptreact][yaml]": { + "editor.defaultFormatter": "oxc.oxc-vscode", "editor.formatOnSave": true }, "editor.codeActionsOnSave": { - "source.fixAll.biome": "explicit", - "source.organizeImports.biome": "explicit" + "source.fixAll.oxc": "explicit" }, "files.exclude": { ".pnpm-store": true diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..e4e77c5 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,13 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 80, + "sortImports": { + "groups": [ + "builtin", + "external", + ["internal", "parent", "sibling", "index"], + "unknown" + ], + "newlinesBetween": false + } +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..bf7b88f --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,100 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "categories": { + "correctness": "error", + "pedantic": "error", + "perf": "error", + "suspicious": "error" + }, + "options": { + "typeAware": true + }, + "overrides": [ + { + // No TypeScript project includes this JavaScript configuration, so + // type-aware rules see only error types in it. + "files": [".vscode-test.mjs"], + "rules": { + "typescript/no-unsafe-argument": "off", + "typescript/no-unsafe-assignment": "off", + "typescript/no-unsafe-call": "off", + "typescript/no-unsafe-member-access": "off" + } + } + ], + "plugins": ["eslint", "oxc", "typescript", "unicorn", "vitest"], + "rules": { + "eslint/default-case-last": "error", + "eslint/max-classes-per-file": "off", + "eslint/max-lines": "off", + "eslint/max-lines-per-function": "off", + "eslint/no-await-in-loop": "off", + "eslint/no-duplicate-imports": [ + "error", + { + "allowSeparateTypeImports": true + } + ], + "eslint/no-extra-label": "error", + "eslint/no-inline-comments": "off", + "eslint/no-label-var": "error", + "eslint/no-labels": "error", + "eslint/no-lone-blocks": "error", + "eslint/no-redeclare": "off", + "eslint/no-sequences": "error", + "eslint/no-template-curly-in-string": "error", + "eslint/no-unused-vars": [ + "error", + { + "args": "all", + "argsIgnorePattern": "^_", + "ignoreRestSiblings": true + } + ], + "eslint/no-var": "error", + "eslint/object-shorthand": ["error", "always"], + "eslint/prefer-arrow-callback": "error", + "eslint/prefer-const": "error", + "eslint/prefer-exponentiation-operator": "error", + "eslint/prefer-numeric-literals": "error", + "eslint/prefer-object-has-own": "error", + "eslint/prefer-regex-literals": "error", + "eslint/prefer-rest-params": "error", + "eslint/prefer-template": "error", + "eslint/require-await": "off", + "eslint/sort-imports": [ + "error", + { + "ignoreCase": true, + "ignoreDeclarationSort": true + } + ], + "oxc/no-const-enum": "error", + "oxc/no-map-spread": "off", + "typescript/adjacent-overload-signatures": "error", + "typescript/consistent-return": "off", + "typescript/consistent-type-exports": "error", + "typescript/consistent-type-imports": "error", + "typescript/dot-notation": "error", + "typescript/no-confusing-void-expression": [ + "error", + { + "ignoreArrowShorthand": true + } + ], + "typescript/no-empty-interface": "error", + "typescript/no-empty-object-type": "error", + "typescript/no-explicit-any": "error", + "typescript/no-non-null-assertion": "error", + "typescript/prefer-function-type": "error", + "typescript/prefer-literal-enum-member": "error", + "typescript/prefer-readonly-parameter-types": "off", + "typescript/require-await": "off", + "typescript/strict-void-return": "off", + "typescript/use-unknown-in-catch-callback-variable": "error", + "unicorn/no-document-cookie": "error", + "unicorn/prefer-node-protocol": "error", + "unicorn/prefer-number-properties": "error", + "vitest/no-conditional-in-test": "off" + } +} diff --git a/AGENTS.md b/AGENTS.md index ffd1789..b187981 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ These rules apply to the entire repository. ## Project - Build Busy Octopus as one flat pnpm package in strict TypeScript unless an independently versioned component genuinely requires a separate package. -- Use Biome for formatting and linting, Vitest for Node tests, the VS Code Test CLI with Mocha and Chai for real extension-host tests, and esbuild for the distributable CLI and VS Code extension bundles. +- Use Oxfmt for formatting, Oxlint with type-aware rules for linting, Vitest for Node tests, the VS Code Test CLI with Mocha and Chai for real extension-host tests, and esbuild for the distributable CLI and VS Code extension bundles. - Publish `busy-octopus` with the `busy-octopus` executable and the `mindedtech.busy-octopus` VS Code extension from one synchronized version. - Keep the public core generic. Codex, Claude Code, and future tools are integrations that adapt their events into notification requests. - Keep the queue protocol internal until the public library contract is stable. diff --git a/README.md b/README.md index 2e6d396..8a57b20 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,10 @@ busy-octopus agent setup claude-code Approve the new hooks: -| Provider | Approval | -| --- | --- | -| Codex | Open `/hooks`, review the Busy Octopus hooks, and trust them. | -| Claude Code | Accept the workspace trust prompt. | +| Provider | Approval | +| ----------- | ------------------------------------------------------------- | +| Codex | Open `/hooks`, review the Busy Octopus hooks, and trust them. | +| Claude Code | Accept the workspace trust prompt. | Run `busy-octopus agent setup --help` for attention notifications and other options. @@ -82,14 +82,14 @@ The test notification uses the normal settings. Disable `busyOctopus.focusSuppre ## Settings -| Setting | Default | Effect when enabled | -| --- | --- | --- | -| `busyOctopus.editor.enable` | `false` | Show notifications in VS Code. | -| `busyOctopus.focusSuppression.enable` | `true` | Suppress notifications while the target workspace window has focus. | -| `busyOctopus.detail.enable` | `true` | Include notification details. | -| `busyOctopus.windows.notification.enable` | `true` | Show native Windows notifications. | -| `busyOctopus.windows.notification.sound.enable` | `false` | Play the default Windows notification sound. | -| `busyOctopus.windows.taskbar.flash.enable` | `true` | Flash the workspace taskbar button on Windows. | +| Setting | Default | Effect when enabled | +| ----------------------------------------------- | ------- | ------------------------------------------------------------------- | +| `busyOctopus.editor.enable` | `false` | Show notifications in VS Code. | +| `busyOctopus.focusSuppression.enable` | `true` | Suppress notifications while the target workspace window has focus. | +| `busyOctopus.detail.enable` | `true` | Include notification details. | +| `busyOctopus.windows.notification.enable` | `true` | Show native Windows notifications. | +| `busyOctopus.windows.notification.sound.enable` | `false` | Play the default Windows notification sound. | +| `busyOctopus.windows.taskbar.flash.enable` | `true` | Flash the workspace taskbar button on Windows. | On Windows, clicking a native notification opens its workspace, including WSL and Dev Container workspaces. Taskbar flashing can target a window after it has received focus once during the extension session. @@ -101,20 +101,20 @@ Notifications are not consumed in Restricted Mode or virtual workspaces. Completion notifications are configured by default: -| Provider | Event | Details | -| --- | --- | --- | -| Codex | `Stop` | Final response, when available. | -| Claude Code | `Stop` | Final response, when available. | -| Claude Code | `StopFailure` | Turn stopped with an error. | +| Provider | Event | Details | +| ----------- | ------------- | ------------------------------- | +| Codex | `Stop` | Final response, when available. | +| Claude Code | `Stop` | Final response, when available. | +| Claude Code | `StopFailure` | Turn stopped with an error. | The `--enable-attention` setup option adds: -| Provider | Event | Message | -| --- | --- | --- | -| Codex | `PreToolUse` for `request_user_input` or `request_user_input_async` | Input required. | -| Claude Code | `PreToolUse` for `AskUserQuestion` | Input required. | -| Claude Code | `Notification` for `permission_prompt` | Approval required. | -| Claude Code | Elicitation and agent-input notifications | Input required. | +| Provider | Event | Message | +| ----------- | ------------------------------------------------------------------- | ------------------ | +| Codex | `PreToolUse` for `request_user_input` or `request_user_input_async` | Input required. | +| Claude Code | `PreToolUse` for `AskUserQuestion` | Input required. | +| Claude Code | `Notification` for `permission_prompt` | Approval required. | +| Claude Code | Elicitation and agent-input notifications | Input required. | Setup preserves unrelated settings and hooks. Its project configuration files may be committed, but every user still needs `busy-octopus` on the agent's `PATH`. @@ -124,10 +124,10 @@ Provider documentation: [Codex hooks](https://learn.chatgpt.com/docs/hooks) and The wrapped command keeps its output and exit status. Notification errors do not change its result. -| Option | Notification content | -| --- | --- | +| Option | Notification content | +| ------------ | ---------------------------------------------- | | `-H, --head` | Complete lines from the start, earliest first. | -| `-T, --tail` | Complete lines from the end, latest first. | +| `-T, --tail` | Complete lines from the end, latest first. | Output is limited to 1,024 characters; a single long line uses its prefix or suffix. The output options are mutually exclusive. Without either option, command output is not included in the notification. Use `-s, --success-only` or `-f, --failure-only` to restrict notifications. diff --git a/biome.json b/biome.json deleted file mode 100644 index 4cc804b..0000000 --- a/biome.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "assist": { - "actions": { - "source": { - "organizeImports": "on" - } - } - }, - "files": { - "ignoreUnknown": true - }, - "formatter": { - "indentStyle": "space" - }, - "linter": { - "rules": { - "preset": "recommended", - "style": { - "useConsistentObjectDefinitions": { - "level": "error", - "options": { - "syntax": "shorthand" - } - } - }, - "nursery": { - "useExhaustiveSwitchCases": "error" - } - } - }, - "vcs": { - "clientKind": "git", - "enabled": true, - "useIgnoreFile": true - } -} diff --git a/package.json b/package.json index 7da3283..8d8be1b 100644 --- a/package.json +++ b/package.json @@ -1,96 +1,32 @@ { "name": "busy-octopus", "displayName": "Busy Octopus", - "icon": "assets/icon.png", - "description": "VS Code extension to notify you when an agent or workspace needs attention", "version": "0.1.4", - "publisher": "mindedtech", - "license": "MIT", - "type": "module", - "engines": { - "node": ">=22", - "vscode": "^1.137.0" - }, - "main": "./dist/extension/extension.cjs", - "activationEvents": [ - "onStartupFinished" - ], - "extensionKind": [ - "ui" - ], - "capabilities": { - "untrustedWorkspaces": { - "supported": false - }, - "virtualWorkspaces": { - "supported": false - } - }, + "description": "VS Code extension to notify you when an agent or workspace needs attention", "categories": [ "Other" ], - "contributes": { - "commands": [ - { - "command": "busyOctopus.showTestNotification", - "title": "Show Test Notification", - "category": "Busy Octopus" - } - ], - "configuration": { - "title": "Busy Octopus", - "properties": { - "busyOctopus.editor.enable": { - "type": "boolean", - "default": false, - "description": "Show Busy Octopus notifications in VS Code." - }, - "busyOctopus.detail.enable": { - "type": "boolean", - "default": true, - "description": "Include notification details." - }, - "busyOctopus.focusSuppression.enable": { - "type": "boolean", - "default": true, - "description": "Suppress notifications while the VS Code window has focus." - }, - "busyOctopus.windows.notification.enable": { - "type": "boolean", - "default": true, - "description": "Show native Windows notifications." - }, - "busyOctopus.windows.notification.sound.enable": { - "type": "boolean", - "default": false, - "description": "Play the default sound for native Windows notifications." - }, - "busyOctopus.windows.taskbar.flash.enable": { - "type": "boolean", - "default": true, - "description": "Flash the workspace taskbar button on Windows." - } - } - } + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/mindedtech/busy-octopus.git" }, + "publisher": "mindedtech", "bin": { "busy-octopus": "./dist/cli/main.js" }, - "exports": { - ".": { - "types": "./dist/library/index.d.ts", - "import": "./dist/library/index.js" - } - }, "files": [ ".agents/skills/busy-octopus/SKILL.md", "assets/icon.png", "dist" ], - "packageManager": "pnpm@12.15.0", - "repository": { - "type": "git", - "url": "git+https://github.com/mindedtech/busy-octopus.git" + "type": "module", + "main": "./dist/extension/extension.cjs", + "exports": { + ".": { + "types": "./dist/library/index.d.ts", + "import": "./dist/library/index.js" + } }, "scripts": { "build": "pnpm clean && pnpm build:lib && pnpm build:cli && pnpm build:extension", @@ -98,14 +34,15 @@ "build:cli": "esbuild src/cli/main.ts --bundle --format=esm --packages=external --platform=node --target=node22 --outfile=dist/cli/main.js", "build:extension": "esbuild src/extension/extension.ts --bundle --external:vscode --format=cjs --minify --platform=node --target=node22 --outfile=dist/extension/extension.cjs", "build:test:extension": "esbuild \"src/extension/**/*.e2e.test.ts\" --bundle --external:vscode --format=cjs --out-extension:.js=.cjs --outbase=src/extension --outdir=dist/test/extension --platform=node --sourcemap --target=node22", - "check": "pnpm check:biome && pnpm check:types && pnpm check:knip", - "check:biome": "biome check .", - "check:fix": "biome check --write .", + "check": "pnpm check:format && pnpm check:lint && pnpm check:types && pnpm check:knip", + "check:fix": "oxfmt && oxlint --fix && oxfmt", + "check:format": "oxfmt --check", "check:knip": "knip", - "check:types": "tsc --project tsconfig.json && tsc --project tsconfig.e2e.json", + "check:lint": "oxlint --deny-warnings", + "check:types": "tsc --project tsconfig.json && tsc --project src/extension/tsconfig.json", "clean": "node --experimental-strip-types scripts/build/clean.ts", "cli": "pnpm build && node dist/cli/main.js", - "format": "biome format --write .", + "format": "oxfmt", "measure:cli:cold-start": "pnpm build && node --experimental-strip-types scripts/measure/cli/cold-start.ts", "package:npm": "pnpm build && node --experimental-strip-types scripts/package/npm.ts", "package:release": "pnpm build && node --experimental-strip-types scripts/package/npm.ts && node --experimental-strip-types scripts/package/extension.ts && node --experimental-strip-types scripts/package/checksum.ts", @@ -122,8 +59,12 @@ "verify": "pnpm install --frozen-lockfile && pnpm check && pnpm test && pnpm test:release:controls && pnpm test:package && pnpm test:vsix && pnpm test:extension && pnpm audit --prod --audit-level high", "verify:release": "pnpm verify && pnpm package:release && pnpm test:release" }, + "dependencies": { + "@commander-js/extra-typings": "15.0.0", + "commander": "15.0.0", + "zod": "4.6.7" + }, "devDependencies": { - "@biomejs/biome": "2.5.15", "@types/chai": "5.2.3", "@types/mocha": "10.0.10", "@types/node": "22.20.5", @@ -136,14 +77,76 @@ "esbuild": "0.28.2", "fflate": "0.8.3", "knip": "6.42.1", + "oxfmt": "0.74.0", + "oxlint": "1.89.0", + "oxlint-tsgolint": "7.0.2005", "sinon": "22.1.0", "typescript": "7.0.2", "vite": "8.3.5", "vitest": "5.0.5" }, - "dependencies": { - "@commander-js/extra-typings": "15.0.0", - "commander": "15.0.0", - "zod": "4.6.7" + "contributes": { + "commands": [ + { + "command": "busyOctopus.showTestNotification", + "title": "Show Test Notification", + "category": "Busy Octopus" + } + ], + "configuration": { + "title": "Busy Octopus", + "properties": { + "busyOctopus.editor.enable": { + "type": "boolean", + "default": false, + "description": "Show Busy Octopus notifications in VS Code." + }, + "busyOctopus.detail.enable": { + "type": "boolean", + "default": true, + "description": "Include notification details." + }, + "busyOctopus.focusSuppression.enable": { + "type": "boolean", + "default": true, + "description": "Suppress notifications while the VS Code window has focus." + }, + "busyOctopus.windows.notification.enable": { + "type": "boolean", + "default": true, + "description": "Show native Windows notifications." + }, + "busyOctopus.windows.notification.sound.enable": { + "type": "boolean", + "default": false, + "description": "Play the default sound for native Windows notifications." + }, + "busyOctopus.windows.taskbar.flash.enable": { + "type": "boolean", + "default": true, + "description": "Flash the workspace taskbar button on Windows." + } + } + } + }, + "activationEvents": [ + "onStartupFinished" + ], + "extensionKind": [ + "ui" + ], + "icon": "assets/icon.png", + "engines": { + "node": ">=22", + "vscode": "^1.137.0" + }, + "packageManager": "pnpm@12.15.0", + "capabilities": { + "untrustedWorkspaces": { + "supported": false + }, + "virtualWorkspaces": { + "supported": false + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c71870..7551c04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,9 +181,6 @@ importers: specifier: 4.6.7 version: 4.6.7 devDependencies: - '@biomejs/biome': - specifier: 2.5.15 - version: 2.5.15 '@types/chai': specifier: 5.2.3 version: 5.2.3 @@ -220,6 +217,15 @@ importers: knip: specifier: 6.42.1 version: 6.42.1 + oxfmt: + specifier: 0.74.0 + version: 0.74.0 + oxlint: + specifier: 1.89.0 + version: 1.89.0(oxlint-tsgolint@7.0.2005) + oxlint-tsgolint: + specifier: 7.0.2005 + version: 7.0.2005 sinon: specifier: 22.1.0 version: 22.1.0 @@ -305,63 +311,6 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@biomejs/biome@2.5.15': - resolution: {integrity: sha512-WZTW4slm/pdkh92K6t/3aEN++44JD1PQ7squ7RCMLI1flHGl43DVOIGXvCjHiZgBMO1V4uYxoFNtqrIA4qGuIQ==} - engines: {node: '>=14.21.3'} - hasBin: true - - '@biomejs/cli-darwin-arm64@2.5.15': - resolution: {integrity: sha512-BZVzFhJ/mUvTLMYbc9x6el0o2Uv5zP7bACYyhFb6fSc1giroXm7PL0a5KqMJ9F+/BnkYRjagD/ROSDVOZ6eapg==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - - '@biomejs/cli-darwin-x64@2.5.15': - resolution: {integrity: sha512-V5Kw63V+fVGNFhFrizDxMbGXAzZCh8kYzJpy3GI6gTapWDg7bZK9oYC9CbVgQSEdTnupz0U5Efz8Ev+5vdBBXw==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - - '@biomejs/cli-linux-arm64-musl@2.5.15': - resolution: {integrity: sha512-tGzZUTcJCV7tj5Adh/Gn6nR4MycqA3iohq2LEcrarAgRBkU0h0OKd2UzCRkOsOYOuUGaUJb4YmqRqyy2feGGSw==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-arm64@2.5.15': - resolution: {integrity: sha512-XaG7P7eeSLYETD3K9grfB0mQmpLQZjygdbsxekWBJAg4am79fGAtRpfRyNnTvXB2fZFNnJZG3nXm7HBat0+VUw==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-linux-x64-musl@2.5.15': - resolution: {integrity: sha512-IlxUcyxilVGPsE008x13pWdkTbU3nQpP2i9b5UrbOYj6goBkKsRX1XB7yJLcV+O1H9LBchIIm4adOaVLZBX0ZQ==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-x64@2.5.15': - resolution: {integrity: sha512-xE4iEW/3LqlYj9GFgGrFsFSH51dEEpUbYWBfeOv0q87WkUDxK2o/HhreSb7qMJqck70RVM6Lg96hgedcAYcOkA==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-win32-arm64@2.5.15': - resolution: {integrity: sha512-2kPKzhNlm8C+Ru3GcO0Me2ODkCBLrVOUNuyi84RJyWDVKAA4+Kjj3jMjL938lF6BRfvltC4vB2nPY9SvN51Ovg==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - - '@biomejs/cli-win32-x64@2.5.15': - resolution: {integrity: sha512-yAzh4UqEImV6Hcy0zjUqNfJAsUhoD64FBQCdTG8Md/Z2wQeVP/ZmJ3XRYAs1g8J9fjTFyl2fCtc5FB5til47+g==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - '@commander-js/extra-typings@15.0.0': resolution: {integrity: sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==} peerDependencies: @@ -803,6 +752,280 @@ packages: cpu: [x64] os: [win32] + '@oxfmt/binding-android-arm-eabi@0.74.0': + resolution: {integrity: sha512-xxQwrf5EPI5zxvuCTW8r6tzCTjPQ+h6+HqJ0iRKRy2zS1yBllt/xLVjoIyUc+FnR7g0a5g1SZv2yyDYhTItLbA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.74.0': + resolution: {integrity: sha512-FJW3sATVvrWbV64al8xofCPZj4500Z/1W0ad7Tg1rcCUyh0EMbuSrsf3jsZS7CEbwQ5fQ0mRhmkc129GFgofSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.74.0': + resolution: {integrity: sha512-TfmNKBD30dGyMu9GCLiaw2rt/z7fms9LS+5cKcNOyWst0QgAs8oZs6wPxMbVaaDVE0/PCdCosgWM1vbu6f5NUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.74.0': + resolution: {integrity: sha512-5Usf8+DfC4Q3o/MD8FkN47GrI6y0KbhQ4KhZ2HGzMZcXjcW1AeldwbUn51aKih+1TVuJFF9qkNm6X1nzm6NYng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.74.0': + resolution: {integrity: sha512-eL+1O3gykw1XihbZh2MRAciQvU2988LByrnjhyo/dmNG74X4oZ7XzigRoWPRFLfJCp8hzRkp/LbEhwe7uuT2eA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.74.0': + resolution: {integrity: sha512-Xt0QtO2hJ4stnul8IL4LJGVLyp0xnc6ccN9fZ30yuubv8FWSRYxWwd1Za62KsEqAfmPSazoS/cLSUUJpja6npA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.74.0': + resolution: {integrity: sha512-/4wYQ0gZl4vUR0QoBTGaay73FmQ1GAjUa9z9101TR2xbGNrg4a+grazlR0yzEOe3/E+PJr5BGfj66MMzm/54vg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.74.0': + resolution: {integrity: sha512-846bKzF+HJd+JkOFh3rxzmYvSq4Cm+GMT+p+mW7S13ZYo6GAAGv8bL8cAOi/XcdeMqxEJDCIMTVQLVBXHMCdig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.74.0': + resolution: {integrity: sha512-nI5FJwZ4Flo6CBdscpwxymZXxO3m3IErQJX55ZS6h+ObfGXu8FostL85L/RWPFaCLnIVE4/WfLw+2q8XvVEeUA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.74.0': + resolution: {integrity: sha512-BoQYKuqUdsKAFAkK6DLBvkdv4+t0hppSm79sB0wIcE2o/eOwNj+6Z3OlKY4q3Eta/tlssP65FaS9vHn8TeF7vQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.74.0': + resolution: {integrity: sha512-FSroRrM12+jCdSQklF9aOxsKpI6wFs4ZCo/1cSYG80RWZS6z4LV+l9tXRMT8lDGu+yohIZBwi9M76FzGPFDE2A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.74.0': + resolution: {integrity: sha512-0VLQD0dhGBLY9Ik/w+otMvD81D0YXi4VDwql65macV+O9UzmSWmUr6CKf+dMtysDeC0PnG3y6kBkRO08BJx52g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.74.0': + resolution: {integrity: sha512-6X7BbZNz34+OHM6Oqjf6TRcUyRn6aRcB4VtIDZAF/uQRyeVhg+G2hv6MlMvmnRekVSyTJ/8rG/u8W2dRFW6glg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.74.0': + resolution: {integrity: sha512-4kYIQK53yJXrxwxSzx8H2bojlf6/KezLGYLRXzV9QCCkoiCsvBTCid9Bg83dUPTLPa3g9K0730f6MKDbck1+OA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.74.0': + resolution: {integrity: sha512-MfWz4wDREjp/JQDmuxtKieix4FHGa1z5rBvB3betdcPwZvf5fqeQrlCm1mmoyuUVQOh9Pt3M61HwN94kJCxw7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.74.0': + resolution: {integrity: sha512-6rmAApkq5s1kbDjvNI6Nl5A+JqQkLORb2sR941pf5HWsrd01WGKyagK/B4vX9+kh1FfvRkNgEu59FHvVk4H8cQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.74.0': + resolution: {integrity: sha512-+5SpRUnZNGeXTyumHnpHa791tQtt04qZs076WT8eTcGF2OJ631VLnf7S0kQSPuDG+PtJM0phadEB2WAr5n1w9g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.74.0': + resolution: {integrity: sha512-1Fg+wYBO3Dsti1QX+/6EYHjeQzLC84tLOAqPu/r7ah3UJlUs9C/DB703Ay9Vcg+8VMzqZA2P5VGhzJraVUD4OQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.74.0': + resolution: {integrity: sha512-y9jXUgPBm2TZIWbQhMYRRYAQM57+r4fbykgc+j24ICIIZl+pcuoNhnq1CRGm3oFiHnEUnZO00qOtSPGQdJ2ziQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@7.0.2005': + resolution: {integrity: sha512-VqDZxcZVjlonNLuuvraVNgDQt+1ndwa4yJo8OqsK4HYh/61ba9IlMStCVfi/aaIqRoDhNbAKC8gdcTrXqnb+Ug==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@7.0.2005': + resolution: {integrity: sha512-4kUfLRNYoVpv0Tm3jPYrvxyfOc/LmdvFC3pKZ4tL6K7lA7K/VS81SAtmxRzbYXYSyb3SBWhwsuX+df4neqzxxA==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@7.0.2005': + resolution: {integrity: sha512-rhUNcPsBlKvhXtrbhbBUidtRBRdJ8fbEHN9H5Q5N59gby18ruGuDA7KUxl7oRnUzp797uykYQhkBDm+0KpfK/Q==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@7.0.2005': + resolution: {integrity: sha512-tKs9yinDQZOHygM0e8EJnjTU3mzJtdfkOZP6Udu82iLMePUSHOhTJ9zzU/mSd1hQxURI4LTRRiFhfzC+PzwdNA==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@7.0.2005': + resolution: {integrity: sha512-VYPnu1gjfbSv+ROhuyNfMs382D4zQU2ZRc8Uc3nuEZo1k2kf+1KfTrg4hrlWh73h9GI+dUWyo+DYCQzUBlPwcQ==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@7.0.2005': + resolution: {integrity: sha512-ekU89cTGy9fpQzuvxIsFHrnoP8XoFjGIRgPEW6eqgY2NYjGy9pwpjF79o04WXRMJFKRrcaVqkZXUUBdLsqpQDw==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.89.0': + resolution: {integrity: sha512-Pflpba5F3gRAQ12sWglRxvWTHEdAusNDvmi4Y5N1T5SY1AGkM7AixOnET8qkrnLJ/F32qAlNokO6eCknT8tmnw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.89.0': + resolution: {integrity: sha512-dYmyD1x7LaPlb79vZWUcpTShk8dI9z1I+IubtqrBxWNJaDhQdPHwcsvg7q5StTU4xWyd9AwPfLaTYFZY8NZgtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.89.0': + resolution: {integrity: sha512-Z/oxOPiIqiS7fItRdxaaOEC+6GD3OpGi0IxoR0dMefzQHolsKLzScw2DNcu/5gUPok3tv+G9mslvYB5bo3i0qg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.89.0': + resolution: {integrity: sha512-eAjTkHSjAxrdCPJxO6y5sPQL5pYLKb2kvxZz9UU7tXnOZk9I/HLtkvx9S7GHY883k+AmL/eC7mqFt+w7WCwk0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.89.0': + resolution: {integrity: sha512-0lNCA9Tl1n1FKpqy2/9ckp0kIuSrtJ/l9bMRgaEhOtM7TsOHshFuwOcvW8oA9teXLMOJT61LjEKWiaHDFGxF/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.89.0': + resolution: {integrity: sha512-OTFEvi9W+JczaZD1Caj17SGCoW8H7TE+fvx0D7ATEWa1Lohn91UMxFQ6r145SiGy/yCxeIwptMys/RAM7gSVSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.89.0': + resolution: {integrity: sha512-fRxgNHvBLswj6MrhW2uBq5UDhPRQlCbHeq/Nf8yuG9zOxhDsXqX/mi2PX1axgsQxMm88el7HLiJAzglAnflYxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.89.0': + resolution: {integrity: sha512-v9ENzHvwrQoYSWTtviH1pOYc7R9UovDxVofnck50cNmPihRRTcrg5X0UlRbLOZ7IVtxPOlh2hb7kDO+FpE8SfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.89.0': + resolution: {integrity: sha512-vfffe8mQ484AzVadlUOFpgL+JDwhvs9hrzxTgQP1FVfi4ZEUUYeffXlzC9dFV7Dgnu5s/KrNKvfmG6HxBd/ISQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.89.0': + resolution: {integrity: sha512-Buonlp4hVS7RGNP5M7bPpGNQrenyzA2C64JcO/oz9C1c6dGOXJ7VEAnaiAMY7voou0JYyCJdvDVlFQc5DZX3DQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.89.0': + resolution: {integrity: sha512-1eVwJJrJ3lYIPT+cm7BcBBZZXY6IQZP/oyonaruTor/MrVyWOpvnwt1oD5CLdsHg5aVBIxXodQSvt+1gK6ck5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.89.0': + resolution: {integrity: sha512-fRQQsKUGShdEL5Fl9o9uhj3grkZbeXKLv0icQ5tspUCNYSDy8wzzEcP1WC2bKdTQ0rSszbKlO3iiHORxV22DLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.89.0': + resolution: {integrity: sha512-dEtYjIy6+fHcjKklsmtmMkZRhwHr2UbzjmpvuPtQYSeN9awQ2hx5JLms5Wvri+8P/NZtD8v5hfgHALUql+/Ycg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.89.0': + resolution: {integrity: sha512-hqjwIj5KOYM8Wjef/p7IruNi1iByLhSzbMBk62y22P/E8q7Jj0Pp+DeUTKTRYyZYCDtU1wEGlnWOnttHbyNIww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.89.0': + resolution: {integrity: sha512-iAQN3e6o9UgUCcdGwqRx/jYcKbG3HK7ur3rH6lTqVcQeNGZ85AeWIg6cgn/edwFQ0ofEZZItMwkzyCVTahnvEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.89.0': + resolution: {integrity: sha512-MNxjEByz3N/7Q4qFQuPWvc14TpN9LoD3pECEDsJTZbP/wyB7U+hduUXEl7QAbbgVezFwsb9sWtNUO6XVBndOhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.89.0': + resolution: {integrity: sha512-xxe+i9wfuFso/VnoL4mfj0UlzE+cfCFiUB06vkXCMyQdgVVmItF5FfAF4tATAktSU6uHruWR+FjqvAT9cHZZzg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.89.0': + resolution: {integrity: sha512-saFy07cO94E664glwgLmKLI40HgTL1PY86y4T6+ud4niX4H0Kdqjcmqmjps7mQNdBytV5guGbXDdyQtRfeLbLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.89.0': + resolution: {integrity: sha512-hLFJ/o0f/dTJh/EW0vmcbREYSQO9tYgfiAAlsOkiLFiJuRRWvYtFELUXN5o2ijpSo/tlO1n+YlWN58tZwPG18A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2143,6 +2366,39 @@ packages: oxc-resolver@11.24.2: resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} + oxfmt@0.74.0: + resolution: {integrity: sha512-G4OP7FgwGYZR5ttRpCOOfJdyRsSyvtn34vSXnjDq6Oqed5Dufg5mB44mank+51nAdHOKuTJ0lj0mksY7L+N61A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@astrojs/compiler-rs': ^0.4.0 || ^0.5.0 + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + '@astrojs/compiler-rs': + optional: true + svelte: + optional: true + vite-plus: + optional: true + + oxlint-tsgolint@7.0.2005: + resolution: {integrity: sha512-6ccPGu4u4J9bwYPHXBFPee3RZrX3BboRdU7k14zRDDB1lvN3O7J/vPRFdaEfa1+CWx4xnQQOp4ctoaF66oO0uw==} + hasBin: true + + oxlint@1.89.0: + resolution: {integrity: sha512-a25hkfo61okGPvJz432fRIPoxlKtNnOVy9w4rzrmGgNm4iVTPwpSQlmdqNWnp6p9QyyPVfAZRgnf5XvSBZFk/A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2003' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2518,6 +2774,10 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinypool@2.2.0: + resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==} + engines: {node: ^20.0.0 || >=22.0.0} + tmp@0.2.7: resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} @@ -2890,41 +3150,6 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@biomejs/biome@2.5.15': - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.15 - '@biomejs/cli-darwin-x64': 2.5.15 - '@biomejs/cli-linux-arm64': 2.5.15 - '@biomejs/cli-linux-arm64-musl': 2.5.15 - '@biomejs/cli-linux-x64': 2.5.15 - '@biomejs/cli-linux-x64-musl': 2.5.15 - '@biomejs/cli-win32-arm64': 2.5.15 - '@biomejs/cli-win32-x64': 2.5.15 - - '@biomejs/cli-darwin-arm64@2.5.15': - optional: true - - '@biomejs/cli-darwin-x64@2.5.15': - optional: true - - '@biomejs/cli-linux-arm64-musl@2.5.15': - optional: true - - '@biomejs/cli-linux-arm64@2.5.15': - optional: true - - '@biomejs/cli-linux-x64-musl@2.5.15': - optional: true - - '@biomejs/cli-linux-x64@2.5.15': - optional: true - - '@biomejs/cli-win32-arm64@2.5.15': - optional: true - - '@biomejs/cli-win32-x64@2.5.15': - optional: true - '@commander-js/extra-typings@15.0.0(commander@15.0.0)': dependencies: commander: 15.0.0 @@ -3186,6 +3411,138 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.24.2': optional: true + '@oxfmt/binding-android-arm-eabi@0.74.0': + optional: true + + '@oxfmt/binding-android-arm64@0.74.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.74.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.74.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.74.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.74.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.74.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.74.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.74.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.74.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.74.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.74.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.74.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.74.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.74.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.74.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.74.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.74.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.74.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@7.0.2005': + optional: true + + '@oxlint-tsgolint/darwin-x64@7.0.2005': + optional: true + + '@oxlint-tsgolint/linux-arm64@7.0.2005': + optional: true + + '@oxlint-tsgolint/linux-x64@7.0.2005': + optional: true + + '@oxlint-tsgolint/win32-arm64@7.0.2005': + optional: true + + '@oxlint-tsgolint/win32-x64@7.0.2005': + optional: true + + '@oxlint/binding-android-arm-eabi@1.89.0': + optional: true + + '@oxlint/binding-android-arm64@1.89.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.89.0': + optional: true + + '@oxlint/binding-darwin-x64@1.89.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.89.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.89.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.89.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.89.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.89.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.89.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.89.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.89.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.89.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.89.0': + optional: true + '@pkgjs/parseargs@0.11.0': optional: true @@ -4545,6 +4902,62 @@ snapshots: '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 + oxfmt@0.74.0: + dependencies: + tinypool: 2.2.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.74.0 + '@oxfmt/binding-android-arm64': 0.74.0 + '@oxfmt/binding-darwin-arm64': 0.74.0 + '@oxfmt/binding-darwin-x64': 0.74.0 + '@oxfmt/binding-freebsd-x64': 0.74.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.74.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.74.0 + '@oxfmt/binding-linux-arm64-gnu': 0.74.0 + '@oxfmt/binding-linux-arm64-musl': 0.74.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.74.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.74.0 + '@oxfmt/binding-linux-riscv64-musl': 0.74.0 + '@oxfmt/binding-linux-s390x-gnu': 0.74.0 + '@oxfmt/binding-linux-x64-gnu': 0.74.0 + '@oxfmt/binding-linux-x64-musl': 0.74.0 + '@oxfmt/binding-openharmony-arm64': 0.74.0 + '@oxfmt/binding-win32-arm64-msvc': 0.74.0 + '@oxfmt/binding-win32-ia32-msvc': 0.74.0 + '@oxfmt/binding-win32-x64-msvc': 0.74.0 + + oxlint-tsgolint@7.0.2005: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 7.0.2005 + '@oxlint-tsgolint/darwin-x64': 7.0.2005 + '@oxlint-tsgolint/linux-arm64': 7.0.2005 + '@oxlint-tsgolint/linux-x64': 7.0.2005 + '@oxlint-tsgolint/win32-arm64': 7.0.2005 + '@oxlint-tsgolint/win32-x64': 7.0.2005 + + oxlint@1.89.0(oxlint-tsgolint@7.0.2005): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.89.0 + '@oxlint/binding-android-arm64': 1.89.0 + '@oxlint/binding-darwin-arm64': 1.89.0 + '@oxlint/binding-darwin-x64': 1.89.0 + '@oxlint/binding-freebsd-x64': 1.89.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.89.0 + '@oxlint/binding-linux-arm-musleabihf': 1.89.0 + '@oxlint/binding-linux-arm64-gnu': 1.89.0 + '@oxlint/binding-linux-arm64-musl': 1.89.0 + '@oxlint/binding-linux-ppc64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-musl': 1.89.0 + '@oxlint/binding-linux-s390x-gnu': 1.89.0 + '@oxlint/binding-linux-x64-gnu': 1.89.0 + '@oxlint/binding-linux-x64-musl': 1.89.0 + '@oxlint/binding-openharmony-arm64': 1.89.0 + '@oxlint/binding-win32-arm64-msvc': 1.89.0 + '@oxlint/binding-win32-ia32-msvc': 1.89.0 + '@oxlint/binding-win32-x64-msvc': 1.89.0 + oxlint-tsgolint: 7.0.2005 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -4971,6 +5384,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tinypool@2.2.0: {} + tmp@0.2.7: {} to-regex-range@5.0.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5cd8005..f07b6dc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,12 +1,12 @@ allowBuilds: - '@vscode/vsce-sign': false + "@vscode/vsce-sign": false esbuild: true keytar: false engineStrict: true minimumReleaseAge: 1440 minimumReleaseAgeStrict: true overrides: - '@azure/identity>@azure/msal-node': 7.1.1 + "@azure/identity>@azure/msal-node": 7.1.1 mocha>diff: 8.0.4 mocha>serialize-javascript: 7.1.3 preferSymlinkedExecutables: true diff --git a/scripts/release/preflight.ts b/scripts/release/preflight.ts index 9afbdd6..ae7fb50 100644 --- a/scripts/release/preflight.ts +++ b/scripts/release/preflight.ts @@ -5,6 +5,7 @@ import { ok, strictEqual } from "node:assert/strict"; import { execFile } from "node:child_process"; import { appendFile } from "node:fs/promises"; +import type { ReadableStreamDefaultReader } from "node:stream/web"; import { promisify } from "node:util"; import { z } from "zod"; import { packageManifest, publicationEnabled } from "../package/metadata.ts"; @@ -60,7 +61,8 @@ const readGitHubJson = async (path: string): Promise => { ok(response.ok, `GitHub returned status ${response.status}.`); ok(response.body !== null, "GitHub returned an empty response."); - const reader = response.body.getReader(); + const reader: ReadableStreamDefaultReader = + response.body.getReader(); const chunkList: Uint8Array[] = []; let byteCount = 0; @@ -70,6 +72,10 @@ const readGitHubJson = async (path: string): Promise => { if (done) { break; } + ok( + value instanceof Uint8Array, + "GitHub returned a non-byte response chunk.", + ); byteCount += value.byteLength; ok( byteCount <= 1_048_576, diff --git a/scripts/test/extension.ts b/scripts/test/extension.ts index eb67a4f..05ad559 100644 --- a/scripts/test/extension.ts +++ b/scripts/test/extension.ts @@ -5,7 +5,6 @@ import { spawn } from "node:child_process"; import { join } from "node:path"; import { cwd, env, execPath, platform } from "node:process"; -import { fileURLToPath } from "node:url"; const runCommand = async ({ argumentList, @@ -45,7 +44,7 @@ const runInVirtualDisplay = async (): Promise => { "--auto-servernum", execPath, "--experimental-strip-types", - fileURLToPath(import.meta.url), + import.meta.filename, ], command: "xvfb-run", environment: { ...env, BUSY_OCTOPUS_TEST_DISPLAY: "1" }, diff --git a/scripts/test/release.ts b/scripts/test/release.ts index eb827b4..c1f159d 100644 --- a/scripts/test/release.ts +++ b/scripts/test/release.ts @@ -4,7 +4,6 @@ import { spawn } from "node:child_process"; import { env, execPath, platform } from "node:process"; -import { fileURLToPath } from "node:url"; const runCommand = async ({ argumentList, @@ -48,7 +47,7 @@ if ( "--auto-servernum", execPath, "--experimental-strip-types", - fileURLToPath(import.meta.url), + import.meta.filename, ], command: "xvfb-run", environment: { ...env, BUSY_OCTOPUS_TEST_DISPLAY: "1" }, diff --git a/src/cli/agent/setup.spec.test.ts b/src/cli/agent/setup.spec.test.ts index 49fbd2f..4ce2136 100644 --- a/src/cli/agent/setup.spec.test.ts +++ b/src/cli/agent/setup.spec.test.ts @@ -2,14 +2,16 @@ * @file Verify agent setup command options. */ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { assert, beforeEach, describe, expect, it, vi } from "vitest"; +import type { setupAgent } from "../../integration/agent/setup.js"; import { runTestCli } from "../test.js"; +import type { loadAgentSkillText } from "./content.js"; const agentSkillText = "# Busy Octopus\n"; const { loadSkill, setup } = vi.hoisted(() => ({ - loadSkill: vi.fn(), - setup: vi.fn(), + loadSkill: vi.fn(), + setup: vi.fn(), })); vi.mock("./content.js", () => ({ loadAgentSkillText: loadSkill })); @@ -42,8 +44,12 @@ describe("agent setup", () => { stderr: "", stdout: "configured\n", }); - expect(setup).toHaveBeenCalledWith({ - confirm: expect.any(Function), + expect(setup).toHaveBeenCalledOnce(); + const { lastCall } = setup.mock; + assert(lastCall !== undefined, "Agent setup must be called."); + const [{ confirm, ...input }] = lastCall; + expect(confirm).toBeTypeOf("function"); + expect(input).toEqual({ enableAttention: false, provider: "codex", skillText, diff --git a/src/cli/agent/skill.spec.test.ts b/src/cli/agent/skill.spec.test.ts index ac0cabf..2fd87a9 100644 --- a/src/cli/agent/skill.spec.test.ts +++ b/src/cli/agent/skill.spec.test.ts @@ -7,9 +7,12 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { runTestCli } from "../test.js"; +import type { loadAgentSkillText } from "./content.js"; const agentSkillText = "# Busy Octopus\n"; -const { loadSkill } = vi.hoisted(() => ({ loadSkill: vi.fn() })); +const { loadSkill } = vi.hoisted(() => ({ + loadSkill: vi.fn(), +})); vi.mock("./content.js", () => ({ loadAgentSkillText: loadSkill })); diff --git a/src/cli/cli.spec.test.ts b/src/cli/cli.spec.test.ts index c1a7f49..de23eae 100644 --- a/src/cli/cli.spec.test.ts +++ b/src/cli/cli.spec.test.ts @@ -23,11 +23,9 @@ describe("program", () => { async (flag) => { const result = await runTestCli([flag]); - expect(result).toEqual({ - exitCode: 0, - stderr: "", - stdout: expect.stringMatching(/^\d+\.\d+\.\d+\n$/u), - }); + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toMatch(/^\d+\.\d+\.\d+\n$/u); }, ); diff --git a/src/cli/run.spec.test.ts b/src/cli/run.spec.test.ts index ff27642..2be9e03 100644 --- a/src/cli/run.spec.test.ts +++ b/src/cli/run.spec.test.ts @@ -4,7 +4,7 @@ import { basename } from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { NotifyInput, notify } from "../library/notify.js"; +import type { notify, NotifyInput } from "../library/notify.js"; import { MAXIMUM_NOTIFICATION_BODY_CODE_POINT_COUNT } from "../protocol/notification.js"; import { runTestCli } from "./test.js"; diff --git a/src/extension/delivery/adapter/editor.e2e.test.ts b/src/extension/delivery/adapter/editor.e2e.test.ts index fa8c19c..571aad9 100644 --- a/src/extension/delivery/adapter/editor.e2e.test.ts +++ b/src/extension/delivery/adapter/editor.e2e.test.ts @@ -5,8 +5,8 @@ import { assert, expect } from "chai"; import { stub } from "sinon"; import { - ConfigurationTarget, commands, + ConfigurationTarget, extensions, window, workspace, @@ -33,7 +33,7 @@ suite("Editor notification delivery", () => { const showInformationMessage = stub( window, "showInformationMessage", - ).resolves(undefined); + ).resolves(); try { await commands.executeCommand("busyOctopus.showTestNotification"); diff --git a/src/extension/delivery/adapter/editor.spec.test.ts b/src/extension/delivery/adapter/editor.spec.test.ts index 8fff23e..5b404f0 100644 --- a/src/extension/delivery/adapter/editor.spec.test.ts +++ b/src/extension/delivery/adapter/editor.spec.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, it, vi } from "vitest"; +import type { window } from "vscode"; import { createNotificationRequest } from "../../../protocol/notification.js"; import type { NotificationDeliveryTarget } from "../adapter.js"; import type { NotificationDeliveryConfig } from "../config.js"; @@ -39,7 +40,8 @@ const config = { describe("EditorNotificationAdapter", () => { it("does not allow editor notifications by default", () => { - const showInformationMessage = vi.fn(); + const showInformationMessage = + vi.fn<(typeof window)["showInformationMessage"]>(); const adapter = new EditorNotificationAdapter({ showInformationMessage, }); @@ -49,7 +51,8 @@ describe("EditorNotificationAdapter", () => { it("allows enabled editor notifications", () => { const adapter = new EditorNotificationAdapter({ - showInformationMessage: vi.fn(), + showInformationMessage: + vi.fn<(typeof window)["showInformationMessage"]>(), }); expect( @@ -61,7 +64,8 @@ describe("EditorNotificationAdapter", () => { }); it("shows notifications", async () => { - const showInformationMessage = vi.fn(); + const showInformationMessage = + vi.fn<(typeof window)["showInformationMessage"]>(); const adapter = new EditorNotificationAdapter({ showInformationMessage, }); @@ -79,7 +83,8 @@ describe("EditorNotificationAdapter", () => { }); it("shows only the title when details are absent", async () => { - const showInformationMessage = vi.fn(); + const showInformationMessage = + vi.fn<(typeof window)["showInformationMessage"]>(); const adapter = new EditorNotificationAdapter({ showInformationMessage }); await adapter.deliver({ @@ -93,7 +98,8 @@ describe("EditorNotificationAdapter", () => { }); it("does not show a notification after cancellation", async () => { - const showInformationMessage = vi.fn(); + const showInformationMessage = + vi.fn<(typeof window)["showInformationMessage"]>(); const abortController = new AbortController(); const adapter = new EditorNotificationAdapter({ showInformationMessage, @@ -112,7 +118,8 @@ describe("EditorNotificationAdapter", () => { }); it("delivers a notification without applying routine policy", async () => { - const showInformationMessage = vi.fn(); + const showInformationMessage = + vi.fn<(typeof window)["showInformationMessage"]>(); const adapter = new EditorNotificationAdapter({ showInformationMessage, }); diff --git a/src/extension/delivery/adapter/windows/notification.spec.test.ts b/src/extension/delivery/adapter/windows/notification.spec.test.ts index f371a9f..077a263 100644 --- a/src/extension/delivery/adapter/windows/notification.spec.test.ts +++ b/src/extension/delivery/adapter/windows/notification.spec.test.ts @@ -56,7 +56,7 @@ describe("WindowsNotificationAdapter", () => { const adapter = new WindowsNotificationAdapter({ appName: "Visual Studio Code", scriptPath: "synthetic-script", - showNotification: vi.fn(), + showNotification: vi.fn(), uriScheme: "vscode", }); @@ -121,7 +121,7 @@ describe("WindowsNotificationAdapter", () => { }); it("rejects an unsupported workspace before invoking PowerShell", async () => { - const showNotification = vi.fn(); + const showNotification = vi.fn(); const adapter = new WindowsNotificationAdapter({ appName: "Visual Studio Code", scriptPath: "synthetic-script", diff --git a/src/extension/delivery/adapter/windows/taskbar.spec.test.ts b/src/extension/delivery/adapter/windows/taskbar.spec.test.ts index 7a84688..ad0def2 100644 --- a/src/extension/delivery/adapter/windows/taskbar.spec.test.ts +++ b/src/extension/delivery/adapter/windows/taskbar.spec.test.ts @@ -2,14 +2,16 @@ * @file Verify exact Windows taskbar attention ownership. */ -import { describe, expect, it, vi } from "vitest"; +import { assert, describe, expect, it, vi } from "vitest"; +import type { Disposable } from "vscode"; import { EditorWindowHandle } from "../../../windows/bridge.js"; import type { NotificationDeliveryConfig } from "../../config.js"; import { WindowsTaskbarAdapter } from "./taskbar.js"; -type TaskbarBridge = ConstructorParameters< +type TaskbarAdapterInput = ConstructorParameters< typeof WindowsTaskbarAdapter ->[0]["bridge"]; +>[0]; +type TaskbarBridge = TaskbarAdapterInput["bridge"]; const windowHandle = EditorWindowHandle.parse("987654"); @@ -25,7 +27,6 @@ const config = { const createAdapter = ({ focus = true }: { focus?: boolean } = {}) => { let currentFocus = focus; - let onFocusChange = (_focus: boolean): void => undefined; const bridge = { captureWindow: vi.fn().mockResolvedValue({ status: "success", @@ -38,16 +39,22 @@ const createAdapter = ({ focus = true }: { focus?: boolean } = {}) => { .fn() .mockResolvedValue({ status: "success" }), } satisfies TaskbarBridge; - const dispose = vi.fn(); + const dispose = vi.fn(); + const onFocusChange = vi.fn(() => ({ + dispose, + })); const adapter = new WindowsTaskbarAdapter({ bridge, - diagnose: vi.fn(), - onFocusChange: (callback) => { - onFocusChange = callback; - return { dispose }; - }, + diagnose: vi.fn(), + onFocusChange, readFocus: () => currentFocus, }); + const [registration] = onFocusChange.mock.calls; + assert( + registration !== undefined, + "Taskbar adapter must register a focus listener.", + ); + const [changeFocus] = registration; return { adapter, @@ -55,7 +62,7 @@ const createAdapter = ({ focus = true }: { focus?: boolean } = {}) => { dispose, setFocus: (value: boolean) => { currentFocus = value; - onFocusChange(value); + changeFocus(value); }, }; }; diff --git a/src/extension/delivery/dispatcher.spec.test.ts b/src/extension/delivery/dispatcher.spec.test.ts index cc18f74..1e77d02 100644 --- a/src/extension/delivery/dispatcher.spec.test.ts +++ b/src/extension/delivery/dispatcher.spec.test.ts @@ -14,6 +14,10 @@ import type { import type { NotificationDeliveryConfig } from "./config.js"; import { NotificationDeliveryDispatcher } from "./dispatcher.js"; +type DispatcherInput = ConstructorParameters< + typeof NotificationDeliveryDispatcher +>[0]; + const request = createNotificationRequest({ body: "Review the result.", notificationId: "synthetic-notification", @@ -65,10 +69,18 @@ describe("NotificationDeliveryDispatcher", () => { title: "Codex", workspace: { ...request.workspace, display }, } satisfies NotificationRequest; - const deliver = vi.fn().mockResolvedValue(undefined); + const deliver = vi + .fn() + .mockResolvedValue(); const dispatcher = new NotificationDeliveryDispatcher({ - adapterList: [{ allow: () => true, deliver, dispose: vi.fn() }], - diagnose: vi.fn(), + adapterList: [ + { + allow: () => true, + deliver, + dispose: vi.fn(), + }, + ], + diagnose: vi.fn(), readConfig: () => config, readFocus: () => false, }); @@ -96,10 +108,18 @@ describe("NotificationDeliveryDispatcher", () => { title: "Input required.", } satisfies NotificationRequest; const deliveryConfig = { ...config, detail: { enable: false } }; - const deliver = vi.fn().mockResolvedValue(undefined); + const deliver = vi + .fn() + .mockResolvedValue(); const dispatcher = new NotificationDeliveryDispatcher({ - adapterList: [{ allow: () => true, deliver, dispose: vi.fn() }], - diagnose: vi.fn(), + adapterList: [ + { + allow: () => true, + deliver, + dispose: vi.fn(), + }, + ], + diagnose: vi.fn(), readConfig: () => deliveryConfig, readFocus: () => false, }); @@ -120,17 +140,25 @@ describe("NotificationDeliveryDispatcher", () => { }); it("continues after an adapter fails", async () => { - const diagnose = vi.fn(); - const readConfig = vi.fn(() => config); - const deliver = vi.fn().mockResolvedValue(undefined); + const diagnose = vi.fn(); + const readConfig = vi.fn(() => config); + const deliver = vi + .fn() + .mockResolvedValue(); const dispatcher = new NotificationDeliveryDispatcher({ adapterList: [ { allow: () => true, - deliver: vi.fn().mockRejectedValue(new Error("synthetic failure")), - dispose: vi.fn(), + deliver: vi + .fn() + .mockRejectedValue(new Error("synthetic failure")), + dispose: vi.fn(), + }, + { + allow: () => true, + deliver, + dispose: vi.fn(), }, - { allow: () => true, deliver, dispose: vi.fn() }, ], diagnose, readConfig, @@ -151,14 +179,16 @@ describe("NotificationDeliveryDispatcher", () => { }); it("does not report successful adapter delivery as a failure", async () => { - const diagnose = vi.fn(); - const readConfig = vi.fn(() => config); + const diagnose = vi.fn(); + const readConfig = vi.fn(() => config); const dispatcher = new NotificationDeliveryDispatcher({ adapterList: [ { allow: () => true, - deliver: vi.fn().mockResolvedValue(undefined), - dispose: vi.fn(), + deliver: vi + .fn() + .mockResolvedValue(), + dispose: vi.fn(), }, ], diagnose, @@ -177,11 +207,19 @@ describe("NotificationDeliveryDispatcher", () => { }); it("suppresses routine delivery while the editor has focus", async () => { - const allow = vi.fn(() => true); - const deliver = vi.fn().mockResolvedValue(undefined); + const allow = vi.fn(() => true); + const deliver = vi + .fn() + .mockResolvedValue(); const dispatcher = new NotificationDeliveryDispatcher({ - adapterList: [{ allow, deliver, dispose: vi.fn() }], - diagnose: vi.fn(), + adapterList: [ + { + allow, + deliver, + dispose: vi.fn(), + }, + ], + diagnose: vi.fn(), readConfig: () => config, readFocus: () => true, }); @@ -197,10 +235,18 @@ describe("NotificationDeliveryDispatcher", () => { }); it("allows explicit focus suppression opt-out", async () => { - const deliver = vi.fn().mockResolvedValue(undefined); + const deliver = vi + .fn() + .mockResolvedValue(); const dispatcher = new NotificationDeliveryDispatcher({ - adapterList: [{ allow: () => true, deliver, dispose: vi.fn() }], - diagnose: vi.fn(), + adapterList: [ + { + allow: () => true, + deliver, + dispose: vi.fn(), + }, + ], + diagnose: vi.fn(), readConfig: () => ({ ...config, focusSuppression: { enable: false } }), readFocus: () => true, }); @@ -217,10 +263,18 @@ describe("NotificationDeliveryDispatcher", () => { }); it("removes details before adapter delivery", async () => { - const deliver = vi.fn().mockResolvedValue(undefined); + const deliver = vi + .fn() + .mockResolvedValue(); const dispatcher = new NotificationDeliveryDispatcher({ - adapterList: [{ allow: () => true, deliver, dispose: vi.fn() }], - diagnose: vi.fn(), + adapterList: [ + { + allow: () => true, + deliver, + dispose: vi.fn(), + }, + ], + diagnose: vi.fn(), readConfig: () => ({ ...config, detail: { enable: false } }), readFocus: () => false, }); @@ -237,10 +291,18 @@ describe("NotificationDeliveryDispatcher", () => { }); it("sanitizes content before adapter delivery", async () => { - const deliver = vi.fn().mockResolvedValue(undefined); + const deliver = vi + .fn() + .mockResolvedValue(); const dispatcher = new NotificationDeliveryDispatcher({ - adapterList: [{ allow: () => true, deliver, dispose: vi.fn() }], - diagnose: vi.fn(), + adapterList: [ + { + allow: () => true, + deliver, + dispose: vi.fn(), + }, + ], + diagnose: vi.fn(), readConfig: () => config, readFocus: () => false, }); @@ -249,7 +311,7 @@ describe("NotificationDeliveryDispatcher", () => { await dispatcher.deliver({ notification: { ...request, - body: "Review\n\u202ethe result.", + body: "Review\n\u202Ethe result.", title: "Agent\tfinished", }, signal, @@ -269,11 +331,19 @@ describe("NotificationDeliveryDispatcher", () => { }); it("does not deliver through an adapter that rejects config", async () => { - const allow = vi.fn(() => false); - const deliver = vi.fn().mockResolvedValue(undefined); + const allow = vi.fn(() => false); + const deliver = vi + .fn() + .mockResolvedValue(); const dispatcher = new NotificationDeliveryDispatcher({ - adapterList: [{ allow, deliver, dispose: vi.fn() }], - diagnose: vi.fn(), + adapterList: [ + { + allow, + deliver, + dispose: vi.fn(), + }, + ], + diagnose: vi.fn(), readConfig: () => config, readFocus: () => false, }); @@ -286,16 +356,18 @@ describe("NotificationDeliveryDispatcher", () => { }); it("reads fresh config for each notification", async () => { - const readConfig = vi.fn(() => config); + const readConfig = vi.fn(() => config); const dispatcher = new NotificationDeliveryDispatcher({ adapterList: [ { allow: () => true, - deliver: vi.fn().mockResolvedValue(undefined), - dispose: vi.fn(), + deliver: vi + .fn() + .mockResolvedValue(), + dispose: vi.fn(), }, ], - diagnose: vi.fn(), + diagnose: vi.fn(), readConfig, readFocus: () => false, }); @@ -309,10 +381,18 @@ describe("NotificationDeliveryDispatcher", () => { it("reports and propagates config failure", async () => { const failure = new Error("synthetic failure"); - const deliver = vi.fn().mockResolvedValue(undefined); - const diagnose = vi.fn(); + const deliver = vi + .fn() + .mockResolvedValue(); + const diagnose = vi.fn(); const dispatcher = new NotificationDeliveryDispatcher({ - adapterList: [{ allow: () => true, deliver, dispose: vi.fn() }], + adapterList: [ + { + allow: () => true, + deliver, + dispose: vi.fn(), + }, + ], diagnose, readConfig: () => { throw failure; @@ -333,14 +413,19 @@ describe("NotificationDeliveryDispatcher", () => { }); it("disposes every adapter", () => { - const disposeList = [vi.fn(), vi.fn()]; + const disposeList = [ + vi.fn(), + vi.fn(), + ]; const dispatcher = new NotificationDeliveryDispatcher({ adapterList: disposeList.map((dispose) => ({ allow: () => true, - deliver: vi.fn().mockResolvedValue(undefined), + deliver: vi + .fn() + .mockResolvedValue(), dispose, })), - diagnose: vi.fn(), + diagnose: vi.fn(), readConfig: () => config, readFocus: () => false, }); @@ -354,18 +439,26 @@ describe("NotificationDeliveryDispatcher", () => { it("propagates adapter policy failures", async () => { const failure = new Error("synthetic failure"); - const deliver = vi.fn().mockResolvedValue(undefined); - const diagnose = vi.fn(); + const deliver = vi + .fn() + .mockResolvedValue(); + const diagnose = vi.fn(); const dispatcher = new NotificationDeliveryDispatcher({ adapterList: [ { allow: () => { throw failure; }, - deliver: vi.fn().mockResolvedValue(undefined), - dispose: vi.fn(), + deliver: vi + .fn() + .mockResolvedValue(), + dispose: vi.fn(), + }, + { + allow: () => true, + deliver, + dispose: vi.fn(), }, - { allow: () => true, deliver, dispose: vi.fn() }, ], diagnose, readConfig: () => config, diff --git a/src/extension/delivery/dispatcher.ts b/src/extension/delivery/dispatcher.ts index 245ef84..7a53554 100644 --- a/src/extension/delivery/dispatcher.ts +++ b/src/extension/delivery/dispatcher.ts @@ -145,15 +145,18 @@ export class NotificationDeliveryDispatcher implements Disposable { sourceName, ...[label, branch] .filter((value) => value !== null) - .map(this.#sanitizeText), + .map((value) => this.#sanitizeText(value)), ].join(" · "), }; }; #sanitizeText = (value: string): string => value - .replace(/[\p{Cc}\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]+/gu, " ") - .replace(/\s+/gu, " ") + .replaceAll( + /[\p{Cc}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+/gu, + " ", + ) + .replaceAll(/\s+/gu, " ") .trim(); #send = async ({ diff --git a/src/extension/diagnostic.spec.test.ts b/src/extension/diagnostic.spec.test.ts index 1cef546..cb71d6c 100644 --- a/src/extension/diagnostic.spec.test.ts +++ b/src/extension/diagnostic.spec.test.ts @@ -3,12 +3,19 @@ */ import { describe, expect, it, vi } from "vitest"; -import { ExtensionDiagnosticReporter } from "./diagnostic.js"; +import type { window } from "vscode"; +import { + type DiagnosticOutput, + ExtensionDiagnosticReporter, +} from "./diagnostic.js"; describe("ExtensionDiagnosticReporter", () => { it("reports each fixed code once", () => { - const output = { dispose: vi.fn(), warn: vi.fn() }; - const showErrorMessage = vi.fn(); + const output = { + dispose: vi.fn(), + warn: vi.fn(), + }; + const showErrorMessage = vi.fn<(typeof window)["showErrorMessage"]>(); const diagnosticReporter = new ExtensionDiagnosticReporter(output, { showErrorMessage, }); @@ -28,8 +35,11 @@ describe("ExtensionDiagnosticReporter", () => { }); it("shows configuration errors once", () => { - const output = { dispose: vi.fn(), warn: vi.fn() }; - const showErrorMessage = vi.fn(); + const output = { + dispose: vi.fn(), + warn: vi.fn(), + }; + const showErrorMessage = vi.fn<(typeof window)["showErrorMessage"]>(); const diagnosticReporter = new ExtensionDiagnosticReporter(output, { showErrorMessage, }); @@ -44,10 +54,13 @@ describe("ExtensionDiagnosticReporter", () => { }); it("disposes its output", () => { - const output = { dispose: vi.fn(), warn: vi.fn() }; + const output = { + dispose: vi.fn(), + warn: vi.fn(), + }; new ExtensionDiagnosticReporter(output, { - showErrorMessage: vi.fn(), + showErrorMessage: vi.fn<(typeof window)["showErrorMessage"]>(), }).dispose(); expect(output.dispose).toHaveBeenCalledOnce(); diff --git a/src/extension/diagnostic.ts b/src/extension/diagnostic.ts index ed46d4e..4c58d11 100644 --- a/src/extension/diagnostic.ts +++ b/src/extension/diagnostic.ts @@ -60,13 +60,13 @@ export class ExtensionDiagnosticReporter implements Disposable { void this.#window.showErrorMessage( "Busy Octopus could not read its notification settings.", ); - return; + break; case "claim-release-error": case "delivery-adapter-error": case "queue-access-error": case "request-consumer-error": case "taskbar-process-error": - return; + break; } }; } diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 277df4d..5f1bd88 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -6,22 +6,22 @@ import { commands, - type ExtensionContext, env, + type ExtensionContext, Uri, - type WorkspaceFolder, window, workspace, + type WorkspaceFolder, } from "vscode"; import { createNotificationRequest } from "../protocol/notification.js"; import { WorkspaceContext, WorkspaceLabel } from "../protocol/workspace.js"; import { NotificationQueue } from "../queue/queue.js"; import { identifyWorkspace } from "../workspace/identity.js"; import { resolveWorkspaceQueue } from "../workspace/queue.js"; +import type { NotificationDeliveryAdapter } from "./delivery/adapter.js"; import { EditorNotificationAdapter } from "./delivery/adapter/editor.js"; import { WindowsNotificationAdapter } from "./delivery/adapter/windows/notification.js"; import { WindowsTaskbarAdapter } from "./delivery/adapter/windows/taskbar.js"; -import type { NotificationDeliveryAdapter } from "./delivery/adapter.js"; import { NotificationDeliveryConfig, type NotificationDeliveryConfigInput, @@ -215,7 +215,7 @@ export const activate = (context: ExtensionContext): void => { const fileSystem = new WorkspaceQueueFileSystem({ fileSystem: workspace.fs, - join: Uri.joinPath, + join: (directory, name) => Uri.joinPath(directory, name), }); const consumer = new ExtensionWorkspaceConsumer({ @@ -231,7 +231,10 @@ export const activate = (context: ExtensionContext): void => { }), queue: createWorkspaceQueue({ fileSystem, folder }), schedule: (callback, intervalMilliseconds) => { - const timer = setInterval(callback, intervalMilliseconds); + const timer = setInterval( + () => void callback(), + intervalMilliseconds, + ); return { dispose: () => clearInterval(timer) }; }, diff --git a/src/extension/queue/consumer.spec.test.ts b/src/extension/queue/consumer.spec.test.ts index e57c604..74eb981 100644 --- a/src/extension/queue/consumer.spec.test.ts +++ b/src/extension/queue/consumer.spec.test.ts @@ -3,9 +3,13 @@ */ import { describe, expect, it, vi } from "vitest"; +import type { Disposable } from "vscode"; import { createNotificationRequest } from "../../protocol/notification.js"; +import type { QueueClaim } from "../../queue/queue.js"; import { WorkspaceQueueConsumer } from "./consumer.js"; +type ConsumerInput = ConstructorParameters[0]; + const request = createNotificationRequest({ body: null, notificationId: "synthetic-notification", @@ -19,22 +23,22 @@ const request = createNotificationRequest({ describe("WorkspaceQueueConsumer", () => { it("completes a request after its consumer succeeds", async () => { - const complete = vi.fn(); - const onRequest = vi.fn().mockResolvedValue(undefined); + const complete = vi.fn(); + const onRequest = vi.fn().mockResolvedValue(); const queue = { - claim: vi.fn().mockResolvedValue({ - abandon: vi.fn(), + claim: vi.fn().mockResolvedValue({ + abandon: vi.fn(), complete, request, }), }; const consumer = new WorkspaceQueueConsumer({ - diagnose: vi.fn(), + diagnose: vi.fn(), intervalMilliseconds: 1_000, onRequest, queue, - schedule: () => ({ dispose: vi.fn() }), + schedule: () => ({ dispose: vi.fn() }), }); await vi.waitFor(() => expect(complete).toHaveBeenCalledOnce()); @@ -44,12 +48,12 @@ describe("WorkspaceQueueConsumer", () => { }); it("abandons a request when its consumer fails", async () => { - const abandon = vi.fn(); - const diagnose = vi.fn(); + const abandon = vi.fn(); + const diagnose = vi.fn(); const queue = { - claim: vi.fn().mockResolvedValue({ + claim: vi.fn().mockResolvedValue({ abandon, - complete: vi.fn(), + complete: vi.fn(), request, }), }; @@ -57,9 +61,11 @@ describe("WorkspaceQueueConsumer", () => { const consumer = new WorkspaceQueueConsumer({ diagnose, intervalMilliseconds: 1_000, - onRequest: vi.fn().mockRejectedValue(new Error("synthetic failure")), + onRequest: vi + .fn() + .mockRejectedValue(new Error("synthetic failure")), queue, - schedule: () => ({ dispose: vi.fn() }), + schedule: () => ({ dispose: vi.fn() }), }); await vi.waitFor(() => expect(abandon).toHaveBeenCalledOnce()); @@ -69,36 +75,38 @@ describe("WorkspaceQueueConsumer", () => { }); it("abandons a claim that arrives after disposal", async () => { - const abandon = vi.fn(); - const claim = Promise.withResolvers<{ - abandon: () => Promise; - complete: () => Promise; - request: typeof request; - } | null>(); + const abandon = vi.fn(); + const claim = Promise.withResolvers(); const consumer = new WorkspaceQueueConsumer({ - diagnose: vi.fn(), + diagnose: vi.fn(), intervalMilliseconds: 1_000, - onRequest: vi.fn(), + onRequest: vi.fn(), queue: { claim: () => claim.promise }, - schedule: () => ({ dispose: vi.fn() }), + schedule: () => ({ dispose: vi.fn() }), }); consumer.dispose(); - claim.resolve({ abandon, complete: vi.fn(), request }); + claim.resolve({ + abandon, + complete: vi.fn(), + request, + }); await vi.waitFor(() => expect(abandon).toHaveBeenCalledOnce()); }); it("reports queue access failures without rejecting the timer callback", async () => { - const diagnose = vi.fn(); + const diagnose = vi.fn(); const consumer = new WorkspaceQueueConsumer({ diagnose, intervalMilliseconds: 1_000, - onRequest: vi.fn(), + onRequest: vi.fn(), queue: { - claim: vi.fn().mockRejectedValue(new Error("synthetic failure")), + claim: vi + .fn() + .mockRejectedValue(new Error("synthetic failure")), }, - schedule: () => ({ dispose: vi.fn() }), + schedule: () => ({ dispose: vi.fn() }), }); await vi.waitFor(() => diff --git a/src/extension/queue/file-system.spec.test.ts b/src/extension/queue/file-system.spec.test.ts index 248a1a6..1ab1b8d 100644 --- a/src/extension/queue/file-system.spec.test.ts +++ b/src/extension/queue/file-system.spec.test.ts @@ -14,17 +14,21 @@ vi.mock("vscode", () => ({ FileType: { Directory: 2, File: 1 }, })); -const createFileSystem = (): WorkspaceFileSystem => ({ - createDirectory: vi.fn(), - delete: vi.fn(), - readDirectory: vi.fn().mockResolvedValue([]), - readFile: vi.fn().mockResolvedValue(new Uint8Array()), - rename: vi.fn(), - stat: vi.fn().mockResolvedValue({ mtime: 10, size: 0, type: FileType.File }), - writeFile: vi.fn(), +type FileSystem = WorkspaceFileSystem; + +const createFileSystem = (): FileSystem => ({ + createDirectory: vi.fn(), + delete: vi.fn(), + readDirectory: vi.fn().mockResolvedValue([]), + readFile: vi.fn().mockResolvedValue(new Uint8Array()), + rename: vi.fn(), + stat: vi + .fn() + .mockResolvedValue({ mtime: 10, size: 0, type: FileType.File }), + writeFile: vi.fn(), }); -const createQueue = (fileSystem: WorkspaceFileSystem) => +const createQueue = (fileSystem: FileSystem) => new WorkspaceQueueFileSystem({ fileSystem, join: (directory, name) => `${directory}/${name}`, diff --git a/src/extension/queue/file-system.ts b/src/extension/queue/file-system.ts index 06b5b5d..2baf1c3 100644 --- a/src/extension/queue/file-system.ts +++ b/src/extension/queue/file-system.ts @@ -46,9 +46,9 @@ const ownershipRaceCodeSet = new Set([ /** * Provide bounded queue operations through the VS Code filesystem API. */ -export class WorkspaceQueueFileSystem - implements QueueFileSystem -{ +export class WorkspaceQueueFileSystem< + Resource, +> implements QueueFileSystem { #fileSystem: WorkspaceFileSystem; join: QueueFileSystem["join"]; diff --git a/tsconfig.e2e.json b/src/extension/tsconfig.json similarity index 50% rename from tsconfig.e2e.json rename to src/extension/tsconfig.json index 1ae3c92..af0512b 100644 --- a/tsconfig.e2e.json +++ b/src/extension/tsconfig.json @@ -1,8 +1,8 @@ { - "extends": "./tsconfig.json", + "extends": "../../tsconfig.json", "compilerOptions": { "types": ["mocha", "node"] }, "exclude": [], - "include": ["src/extension/**/*.e2e.test.ts"] + "include": ["**/*.e2e.test.ts"] } diff --git a/src/extension/windows/bridge.spec.test.ts b/src/extension/windows/bridge.spec.test.ts index 7eb6bfe..36b6c38 100644 --- a/src/extension/windows/bridge.spec.test.ts +++ b/src/extension/windows/bridge.spec.test.ts @@ -3,10 +3,11 @@ */ import type { - ExecFileOptionsWithStringEncoding, execFile, + ExecFileOptionsWithStringEncoding, } from "node:child_process"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ZodError } from "zod"; import { captureEditorWindow, EditorProcessId, @@ -17,10 +18,11 @@ import { type WindowCapture, } from "./bridge.js"; +type ExecFile = typeof execFile; type ExecFileAsync = typeof execFile.__promisify__; const processMock = vi.hoisted(() => { - const execFile = vi.fn(() => undefined); + const execFile = vi.fn(); const execFileAsync = vi.fn(); Object.defineProperty(execFile, Symbol.for("nodejs.util.promisify.custom"), { @@ -83,7 +85,7 @@ describe("Windows PowerShell bridge", () => { await expect( captureEditorWindow({ editorProcessId, scriptPath }), - ).rejects.toThrow(); + ).rejects.toThrow(ZodError); }); it("parses a captured window handle", async () => { diff --git a/src/extension/windows/bridge.ts b/src/extension/windows/bridge.ts index b838640..0f3e67a 100644 --- a/src/extension/windows/bridge.ts +++ b/src/extension/windows/bridge.ts @@ -4,7 +4,7 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; -import { type ZodType, z } from "zod"; +import { z, type ZodType } from "zod"; const execFileAsync = promisify(execFile); const maximumOutputByteCount = 16_384; @@ -21,7 +21,7 @@ export type EditorProcessId = z.infer; export const EditorWindowHandle = z .string() - .regex(/^[1-9]\d*$/) + .regex(/^[1-9]\d*$/u) .brand("EditorWindowHandle") .describe("Windows editor window handle."); export type EditorWindowHandle = z.infer; diff --git a/src/extension/workspace-consumer.spec.test.ts b/src/extension/workspace-consumer.spec.test.ts index 7717087..f3d8df3 100644 --- a/src/extension/workspace-consumer.spec.test.ts +++ b/src/extension/workspace-consumer.spec.test.ts @@ -2,13 +2,18 @@ * @file Verify trusted workspace and folder lifecycle ownership. */ -import { describe, expect, it, vi } from "vitest"; +import { assert, describe, expect, it, type Mock, vi } from "vitest"; +import type { Disposable } from "vscode"; import { ExtensionWorkspaceConsumer } from "./workspace-consumer.js"; type Folder = { uri: { toString: () => string }; }; +type ConsumerInput = ConstructorParameters< + typeof ExtensionWorkspaceConsumer +>[0]; + const createFolder = (value: string): Folder => ({ uri: { toString: () => value }, }); @@ -17,21 +22,25 @@ describe("ExtensionWorkspaceConsumer", () => { it("waits for workspace trust before starting consumers", () => { const folder = createFolder("file:///synthetic-workspace"); let workspaceTrust = false; - let grantTrust = (): void => undefined; - const createConsumer = vi.fn(() => ({ dispose: vi.fn() })); + const createConsumer = vi.fn(() => ({ + dispose: vi.fn(), + })); + const onTrust = vi.fn(() => ({ + dispose: vi.fn(), + })); const consumer = new ExtensionWorkspaceConsumer({ createConsumer, - onFolderListChange: () => ({ dispose: vi.fn() }), - onTrust: (callback) => { - grantTrust = callback; - return { dispose: vi.fn() }; - }, + onFolderListChange: () => ({ dispose: vi.fn() }), + onTrust, readFolderList: () => [folder], readTrust: () => workspaceTrust, }); expect(createConsumer).not.toHaveBeenCalled(); + const { lastCall } = onTrust.mock; + assert(lastCall !== undefined, "Trust callback must be registered."); + const [grantTrust] = lastCall; workspaceTrust = true; grantTrust(); expect(createConsumer).toHaveBeenCalledWith(folder); @@ -41,27 +50,31 @@ describe("ExtensionWorkspaceConsumer", () => { it("tracks folder additions and removals", () => { const firstFolder = createFolder("file:///first"); const secondFolder = createFolder("file:///second"); - let changeFolderList = (): void => undefined; - const disposeMap = new Map>(); + const disposeMap = new Map>(); + const onFolderListChange = vi.fn( + () => ({ dispose: vi.fn() }), + ); const consumer = new ExtensionWorkspaceConsumer({ createConsumer: (folder) => { - const dispose = vi.fn(); + const dispose = vi.fn(); disposeMap.set(folder.uri.toString(), dispose); return { dispose }; }, - onFolderListChange: (onChange) => { - changeFolderList = () => - onChange({ - folderList: { addition: [secondFolder], removal: [firstFolder] }, - }); - return { dispose: vi.fn() }; - }, - onTrust: () => ({ dispose: vi.fn() }), + onFolderListChange, + onTrust: () => ({ dispose: vi.fn() }), readFolderList: () => [firstFolder], readTrust: () => true, }); - changeFolderList(); + const { lastCall } = onFolderListChange.mock; + assert( + lastCall !== undefined, + "Folder change callback must be registered.", + ); + const [changeFolderList] = lastCall; + changeFolderList({ + folderList: { addition: [secondFolder], removal: [firstFolder] }, + }); expect(disposeMap.get(firstFolder.uri.toString())).toHaveBeenCalledOnce(); expect(disposeMap.get(secondFolder.uri.toString())).not.toHaveBeenCalled(); diff --git a/src/extension/workspace-consumer.ts b/src/extension/workspace-consumer.ts index 0f98aeb..58b2fd9 100644 --- a/src/extension/workspace-consumer.ts +++ b/src/extension/workspace-consumer.ts @@ -18,9 +18,9 @@ type WorkspaceFolderChange = { /** * Start queue consumers only after trust and track workspace-folder changes. */ -export class ExtensionWorkspaceConsumer - implements Disposable -{ +export class ExtensionWorkspaceConsumer< + Folder extends WorkspaceFolderReference, +> implements Disposable { #consumerMap = new Map(); #createConsumer: (folder: Folder) => Disposable; #folderListener: Disposable | null = null; diff --git a/src/integration/agent/claude-code/adapter.spec.test.ts b/src/integration/agent/claude-code/adapter.spec.test.ts index aa68237..4ec59f0 100644 --- a/src/integration/agent/claude-code/adapter.spec.test.ts +++ b/src/integration/agent/claude-code/adapter.spec.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, it } from "vitest"; +import { ZodError } from "zod"; import type { NotifyInput } from "../../../library/notify.js"; import { type ClaudeCodeHook, claudeCodeHookAdapter } from "./adapter.js"; @@ -151,7 +152,7 @@ describe("claudeCodeHookAdapter", () => { message: "Synthetic notification.", notification_type: "idle_prompt", }), - ).toThrow(); + ).toThrow(ZodError); }); it("returns no Claude Code output", () => { diff --git a/src/integration/agent/claude-code/config.spec.test.ts b/src/integration/agent/claude-code/config.spec.test.ts index 50b99c7..55a91b3 100644 --- a/src/integration/agent/claude-code/config.spec.test.ts +++ b/src/integration/agent/claude-code/config.spec.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, it } from "vitest"; +import { ZodError } from "zod"; import { type ClaudeCodeConfig, configureClaudeCodeHooks } from "./config.js"; describe("configureClaudeCodeHooks", () => { @@ -173,6 +174,6 @@ describe("configureClaudeCodeHooks", () => { enableAttention: false, input: { hooks: { Stop: {} } }, }), - ).toThrow(); + ).toThrow(ZodError); }); }); diff --git a/src/integration/agent/claude-code/config.ts b/src/integration/agent/claude-code/config.ts index eb6132d..4f17a89 100644 --- a/src/integration/agent/claude-code/config.ts +++ b/src/integration/agent/claude-code/config.ts @@ -88,7 +88,7 @@ const removeHook = ( Object.fromEntries( Object.entries(hookMap).flatMap(([event, groupList]) => { const remainingGroupList = groupList - .map(removeHandler) + .map((group) => removeHandler(group)) .filter((group) => group !== null); return remainingGroupList.length === 0 diff --git a/src/integration/agent/codex/adapter.spec.test.ts b/src/integration/agent/codex/adapter.spec.test.ts index 0ee00fa..ec14d8d 100644 --- a/src/integration/agent/codex/adapter.spec.test.ts +++ b/src/integration/agent/codex/adapter.spec.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, it } from "vitest"; +import { ZodError } from "zod"; import type { NotifyInput } from "../../../library/notify.js"; import { type CodexHook, codexHookAdapter } from "./adapter.js"; @@ -78,7 +79,7 @@ describe("codexHookAdapter", () => { ...stopHook, hook_event_name: "SessionStart", }), - ).toThrow(); + ).toThrow(ZodError); }); it("returns neutral Codex output", () => { diff --git a/src/integration/agent/codex/config.spec.test.ts b/src/integration/agent/codex/config.spec.test.ts index b53aa50..b804658 100644 --- a/src/integration/agent/codex/config.spec.test.ts +++ b/src/integration/agent/codex/config.spec.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, it } from "vitest"; +import { ZodError } from "zod"; import { type CodexConfig, configureCodexHooks } from "./config.js"; describe("configureCodexHooks", () => { @@ -139,6 +140,6 @@ describe("configureCodexHooks", () => { enableAttention: false, input: { hooks: { Stop: {} } }, }), - ).toThrow(); + ).toThrow(ZodError); }); }); diff --git a/src/integration/agent/codex/config.ts b/src/integration/agent/codex/config.ts index cda620e..76fdb22 100644 --- a/src/integration/agent/codex/config.ts +++ b/src/integration/agent/codex/config.ts @@ -80,7 +80,7 @@ const removeHook = ( Object.fromEntries( Object.entries(hookMap).flatMap(([event, groupList]) => { const remainingGroupList = groupList - .map(removeHandler) + .map((group) => removeHandler(group)) .filter((group) => group !== null); return remainingGroupList.length === 0 diff --git a/src/integration/agent/hook.spec.test.ts b/src/integration/agent/hook.spec.test.ts index 31ade0a..05d7808 100644 --- a/src/integration/agent/hook.spec.test.ts +++ b/src/integration/agent/hook.spec.test.ts @@ -17,10 +17,14 @@ const hook = { last_assistant_message: null, } satisfies CodexHook; +type HookRun = Parameters[0]; + describe("runAgentHook", () => { it("publishes provider notification input", async () => { - const notify = vi.fn().mockResolvedValue({ notificationId: "request-1" }); - const warn = vi.fn(); + const notify = vi + .fn() + .mockResolvedValue({ notificationId: "request-1" }); + const warn = vi.fn(); await expect( runAgentHook({ @@ -48,8 +52,8 @@ describe("runAgentHook", () => { ], ["JSON parsing", () => Promise.resolve("{"), "custom at (root)"], ])("fails open after %s failure", async (_domain, readInput, failure) => { - const notify = vi.fn(); - const warn = vi.fn(); + const notify = vi.fn(); + const warn = vi.fn(); await expect( runAgentHook({ @@ -64,8 +68,8 @@ describe("runAgentHook", () => { }); it("fails open after provider validation failure", async () => { - const notify = vi.fn(); - const warn = vi.fn(); + const notify = vi.fn(); + const warn = vi.fn(); await expect( runAgentHook({ @@ -83,9 +87,9 @@ describe("runAgentHook", () => { it("fails open after publication failure", async () => { const notify = vi - .fn() + .fn() .mockRejectedValue(new Error("Synthetic publication failure.")); - const warn = vi.fn(); + const warn = vi.fn(); await expect( runAgentHook({ diff --git a/src/integration/agent/setup.spec.test.ts b/src/integration/agent/setup.spec.test.ts index 3194635..92cf5bb 100644 --- a/src/integration/agent/setup.spec.test.ts +++ b/src/integration/agent/setup.spec.test.ts @@ -18,6 +18,8 @@ import { ClaudeCodeConfig } from "./claude-code/config.js"; import { CodexConfig } from "./codex/config.js"; import { setupAgent } from "./setup.js"; +type AgentSetup = Parameters[0]; + let fixtureDirectory: string; beforeEach(async () => { @@ -30,7 +32,7 @@ afterEach(async () => { describe("setupAgent", () => { it("creates a Codex configuration after confirmation", async () => { - const confirm = vi.fn().mockResolvedValue(true); + const confirm = vi.fn().mockResolvedValue(true); await expect( setupAgent({ @@ -79,7 +81,7 @@ describe("setupAgent", () => { provider: "claude-code", skillText: null, }); - const confirm = vi.fn().mockResolvedValue(true); + const confirm = vi.fn().mockResolvedValue(true); const skillText = "synthetic skill\n"; const { path: workspacePath } = await resolveWorkspace({ directory: fixtureDirectory, @@ -140,7 +142,7 @@ describe("setupAgent", () => { }); const path = join(fixtureDirectory, ".claude", "settings.json"); const configText = await readFile(path, "utf8"); - const confirm = vi.fn(); + const confirm = vi.fn(); await expect( setupAgent({ @@ -238,7 +240,7 @@ describe("setupAgent", () => { it("rejects configuration beyond the byte limit", async () => { const directory = join(fixtureDirectory, ".codex"); const path = join(directory, "hooks.json"); - const confirm = vi.fn(); + const confirm = vi.fn(); await mkdir(directory); await writeFile(path, JSON.stringify({ value: "x".repeat(1_048_577) })); @@ -268,7 +270,7 @@ describe("setupAgent", () => { directory: fixtureDirectory, provider: "codex", }), - ).rejects.toThrow(); + ).rejects.toThrow(SyntaxError); await expect(readFile(path, "utf8")).resolves.toBe("{"); }); }); diff --git a/src/library/notify.spec.test.ts b/src/library/notify.spec.test.ts index 1507dc2..cf796af 100644 --- a/src/library/notify.spec.test.ts +++ b/src/library/notify.spec.test.ts @@ -183,7 +183,7 @@ describe("notify", () => { await expect( notify({ body: null, directory, source: null, title: " " }), - ).rejects.toThrow(); + ).rejects.toThrow("Notification title must not be blank."); await expect( nativeQueueFileSystem.list(queueDirectory, 1), ).rejects.toMatchObject({ diff --git a/src/protocol/notification.spec.test.ts b/src/protocol/notification.spec.test.ts index 63d843a..ea9cc6d 100644 --- a/src/protocol/notification.spec.test.ts +++ b/src/protocol/notification.spec.test.ts @@ -3,7 +3,7 @@ */ import { afterEach, describe, expect, it, vi } from "vitest"; -import { z } from "zod"; +import { z, ZodError } from "zod"; import { createNotificationRequest, MAXIMUM_NOTIFICATION_REQUEST_BYTE_COUNT, @@ -101,7 +101,7 @@ describe("createNotificationRequest", () => { integrationPayload: "synthetic", }; - expect(() => createNotificationRequest(input)).toThrow(); + expect(() => createNotificationRequest(input)).toThrow(ZodError); }); }); @@ -265,7 +265,7 @@ describe("NotificationRequestJson", () => { ["primitive", "42"], ["null", "null"], ])("rejects a raw JSON %s", (_domain, input) => { - expect(() => NotificationRequestJson.decode(input)).toThrow(); + expect(() => NotificationRequestJson.decode(input)).toThrow(ZodError); }); it("returns a content-free error for malformed JSON", () => { @@ -303,8 +303,10 @@ describe("NotificationRequestJson", () => { MAXIMUM_NOTIFICATION_REQUEST_BYTE_COUNT + 1, ); - expect(() => NotificationRequest.parse(request)).toThrow(); - expect(() => NotificationRequestJson.encode(request)).toThrow(); + const message = `Notification request must not exceed ${MAXIMUM_NOTIFICATION_REQUEST_BYTE_COUNT} UTF-8 bytes.`; + + expect(() => NotificationRequest.parse(request)).toThrow(message); + expect(() => NotificationRequestJson.encode(request)).toThrow(message); }); it("checks raw size before parsing malformed JSON", () => { diff --git a/src/queue/native.ts b/src/queue/native.ts index ee624ee..7d8e3c0 100644 --- a/src/queue/native.ts +++ b/src/queue/native.ts @@ -43,7 +43,7 @@ export const nativeQueueFileSystem: QueuePublisherFileSystem = { await utimes(target, time, time); await rm(source, { force: true }); } catch (error: unknown) { - await rm(target, { force: true }).catch(() => undefined); + await rm(target, { force: true }).catch(() => {}); throw error; } return true; @@ -130,7 +130,7 @@ export const nativeQueueFileSystem: QueuePublisherFileSystem = { } throw error; } - await rm(source, { force: true }).catch(() => undefined); + await rm(source, { force: true }).catch(() => {}); return true; }, writeExclusive: async (path, byteList) => { diff --git a/src/queue/queue.ts b/src/queue/queue.ts index 3f1f298..2ac847e 100644 --- a/src/queue/queue.ts +++ b/src/queue/queue.ts @@ -255,7 +255,7 @@ export class NotificationQueuePublisher { ok(false, "Temporary queue request disappeared before publication."); } } catch (error: unknown) { - await this.#fileSystem.remove(temporaryPath).catch(() => undefined); + await this.#fileSystem.remove(temporaryPath).catch(() => {}); throw error; } }; diff --git a/src/queue/test/file-system.ts b/src/queue/test/file-system.ts index 85d3530..48ef0b9 100644 --- a/src/queue/test/file-system.ts +++ b/src/queue/test/file-system.ts @@ -60,7 +60,8 @@ export class MemoryQueueFileSystem implements QueuePublisherFileSystem { _path, ) => {}; - join: QueuePublisherFileSystem["join"] = posix.join; + join: QueuePublisherFileSystem["join"] = (directory, name) => + posix.join(directory, name); list: QueuePublisherFileSystem["list"] = async (path, maximumEntryCount) => { this.#onList?.(maximumEntryCount); diff --git a/src/workspace/resolution.spec.test.ts b/src/workspace/resolution.spec.test.ts index 0c98999..b175894 100644 --- a/src/workspace/resolution.spec.test.ts +++ b/src/workspace/resolution.spec.test.ts @@ -18,9 +18,12 @@ const createRuntime = ({ git?: WorkspaceRuntime["git"]; platform?: WorkspaceRuntime["platform"]; } = {}) => { - const currentDirectoryOperation = vi.fn(() => currentDirectory); - const gitOperation = vi.fn(git); - const canonicalizeOperation = vi.fn(canonicalize); + const currentDirectoryOperation = vi.fn( + () => currentDirectory, + ); + const gitOperation = vi.fn(git); + const canonicalizeOperation = + vi.fn(canonicalize); const runtime: WorkspaceRuntime = { canonicalize: canonicalizeOperation, currentDirectory: currentDirectoryOperation, diff --git a/src/workspace/runtime.spec.test.ts b/src/workspace/runtime.spec.test.ts index 3e32af1..e54e5f6 100644 --- a/src/workspace/runtime.spec.test.ts +++ b/src/workspace/runtime.spec.test.ts @@ -5,20 +5,20 @@ import { afterEach, describe, expect, it, vi } from "vitest"; const { execFileMock } = vi.hoisted(() => ({ - execFileMock: vi.fn( + execFileMock: vi.fn< ( - _file: string, - _argumentList: string[], - _options: object, + file: string, + argumentList: string[], + options: object, callback: ( error: Error | null, output: string, errorOutput: string, ) => void, - ) => { - callback(null, "feature/example\n", ""); - }, - ), + ) => void + >((_file, _argumentList, _options, callback) => { + callback(null, "feature/example\n", ""); + }), })); vi.mock("node:child_process", () => ({ execFile: execFileMock })); diff --git a/test/package.spec.test.ts b/test/package.spec.test.ts index 7913991..1e51e51 100644 --- a/test/package.spec.test.ts +++ b/test/package.spec.test.ts @@ -14,8 +14,8 @@ import { } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, dirname, join } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { assert, expect, it } from "vitest"; +import { pathToFileURL } from "node:url"; +import { afterAll, assert, beforeAll, expect, it } from "vitest"; import { z } from "zod"; import { packageManifest } from "../scripts/package/metadata.js"; import type { ClaudeCodeHook } from "../src/integration/agent/claude-code/adapter.js"; @@ -26,7 +26,7 @@ import { MAXIMUM_PROCESS_OUTPUT_BYTE_COUNT, } from "./process.js"; -const repositoryDirectory = join(dirname(fileURLToPath(import.meta.url)), ".."); +const repositoryDirectory = join(import.meta.dirname, ".."); const artifactDirectory = join( repositoryDirectory, "artifacts", @@ -171,351 +171,317 @@ const findQueueRequest = async ({ return null; }; -it("verifies the packed library and CLI", { timeout: 30_000 }, async () => { - const fixtureDirectory = await mkdtemp( - join(tmpdir(), "busy-octopus-package-"), - ); - const runtimeTemporaryDirectory = join(fixtureDirectory, "runtime"); - const queueRoot = join(runtimeTemporaryDirectory, "busy-octopus", "v1"); - const workspaceDirectory = join(fixtureDirectory, "synthetic-workspace"); - const notificationId = `package-notification-${randomUUID()}`; +const notificationId = `package-notification-${randomUUID()}`; - try { - await rm(artifactDirectory, { force: true, recursive: true }); - await mkdir(artifactDirectory, { recursive: true }); - await mkdir(runtimeTemporaryDirectory); - await mkdir(workspaceDirectory); - await runPnpm({ argumentList: ["build"] }); - - expect( - PackResult.parse( - JSON.parse( - await runPnpm({ - argumentList: ["pack", "--out", tarballPath, "--json"], - }), - ), - ) - .files.map(({ path }) => path) - .toSorted(), - ).toEqual(packageFileList.toSorted()); - - await writeFile( - join(fixtureDirectory, "package.json"), - JSON.stringify({ - name: "busy-octopus-package-fixture", - private: true, - type: "module", - dependencies: { - "busy-octopus": pathToFileURL(tarballPath).href, - }, - }), - ); - await runPnpm({ - argumentList: ["install", "--prefer-offline", "--ignore-workspace"], - directory: fixtureDirectory, - }); - - const runtimeEnvironment = { - ...process.env, - TEMP: runtimeTemporaryDirectory, - TMP: runtimeTemporaryDirectory, - TMPDIR: runtimeTemporaryDirectory, - }; - await expect( - runPnpm({ - argumentList: [ - "exec", - "busy-octopus", - "doctor", - "--directory", - workspaceDirectory, - ], - directory: fixtureDirectory, - environment: runtimeEnvironment, +let cliPath = ""; +let fixtureDirectory = ""; +let packFileList: string[] = []; +let queueRoot = ""; +let runtimeEnvironment: NodeJS.ProcessEnv = {}; +let workspaceDirectory = ""; + +beforeAll(async () => { + fixtureDirectory = await mkdtemp(join(tmpdir(), "busy-octopus-package-")); + const runtimeTemporaryDirectory = join(fixtureDirectory, "runtime"); + cliPath = join( + fixtureDirectory, + "node_modules", + "busy-octopus", + "dist", + "cli", + "main.js", + ); + queueRoot = join(runtimeTemporaryDirectory, "busy-octopus", "v1"); + runtimeEnvironment = { + ...process.env, + TEMP: runtimeTemporaryDirectory, + TMP: runtimeTemporaryDirectory, + TMPDIR: runtimeTemporaryDirectory, + }; + workspaceDirectory = join(fixtureDirectory, "synthetic-workspace"); + + await rm(artifactDirectory, { force: true, recursive: true }); + await mkdir(artifactDirectory, { recursive: true }); + await mkdir(runtimeTemporaryDirectory); + await mkdir(workspaceDirectory); + await runPnpm({ argumentList: ["build"] }); + + packFileList = PackResult.parse( + JSON.parse( + await runPnpm({ + argumentList: ["pack", "--out", tarballPath, "--json"], }), - ).resolves.toBe("Workspace resolution: ok\nQueue routing: ok\n"); - expect(await readQueueRoot(queueRoot)).toEqual([]); - - const cliPath = join( - fixtureDirectory, - "node_modules", - "busy-octopus", - "dist", - "cli", - "main.js", - ); - const skillText = await readFile( - join( - repositoryDirectory, - ".agents", - "skills", - "busy-octopus", - "SKILL.md", - ), - "utf8", - ); - const skillResult = spawnSync( - process.execPath, - [cliPath, "agent", "skill"], - { - cwd: fixtureDirectory, - encoding: "utf8", - maxBuffer: MAXIMUM_PROCESS_OUTPUT_BYTE_COUNT, - timeout: 5_000, - env: runtimeEnvironment, - windowsHide: true, + ), + ).files.map(({ path }) => path); + + await writeFile( + join(fixtureDirectory, "package.json"), + JSON.stringify({ + name: "busy-octopus-package-fixture", + private: true, + type: "module", + dependencies: { + "busy-octopus": pathToFileURL(tarballPath).href, }, - ); + }), + ); + await runPnpm({ + argumentList: ["install", "--prefer-offline", "--ignore-workspace"], + directory: fixtureDirectory, + }); +}, 30_000); + +afterAll(async () => { + const queueResult = await findQueueRequest({ notificationId, queueRoot }); + if (queueResult !== null) { + await rm(queueResult.directory, { force: true, recursive: true }); + } + await Promise.all([ + rm(fixtureDirectory, { force: true, recursive: true }), + rm(artifactDirectory, { force: true, recursive: true }), + ]); +}); + +it("verifies the packed library and CLI", { timeout: 30_000 }, async () => { + expect(packFileList.toSorted()).toEqual(packageFileList.toSorted()); + + await expect( + runPnpm({ + argumentList: [ + "exec", + "busy-octopus", + "doctor", + "--directory", + workspaceDirectory, + ], + directory: fixtureDirectory, + environment: runtimeEnvironment, + }), + ).resolves.toBe("Workspace resolution: ok\nQueue routing: ok\n"); + expect(await readQueueRoot(queueRoot)).toEqual([]); - expect(skillResult.error).toBeUndefined(); - expect(skillResult.status).toBe(0); - expect(skillResult.signal).toBeNull(); - expect(skillResult.stdout).toBe(skillText); - expect(skillResult.stderr).toBe(""); + const skillText = await readFile( + join(repositoryDirectory, ".agents", "skills", "busy-octopus", "SKILL.md"), + "utf8", + ); + const skillResult = spawnSync(process.execPath, [cliPath, "agent", "skill"], { + cwd: fixtureDirectory, + encoding: "utf8", + maxBuffer: MAXIMUM_PROCESS_OUTPUT_BYTE_COUNT, + timeout: 5_000, + env: runtimeEnvironment, + windowsHide: true, + }); - const commandResult = spawnSync( + expect(skillResult.error).toBeUndefined(); + expect(skillResult.status).toBe(0); + expect(skillResult.signal).toBeNull(); + expect(skillResult.stdout).toBe(skillText); + expect(skillResult.stderr).toBe(""); + + const commandResult = spawnSync( + process.execPath, + [ + cliPath, + "run", + "--tail", + "--", process.execPath, - [ - cliPath, - "run", - "--tail", - "--", - process.execPath, - "-e", - 'process.stdout.write("synthetic stdout\\n"); process.stderr.write("synthetic stderr\\n"); process.exit(7)', + "-e", + 'process.stdout.write("synthetic stdout\\n"); process.stderr.write("synthetic stderr\\n"); process.exit(7)', + ], + { + cwd: fixtureDirectory, + encoding: "utf8", + maxBuffer: MAXIMUM_PROCESS_OUTPUT_BYTE_COUNT, + timeout: 5_000, + env: runtimeEnvironment, + windowsHide: true, + }, + ); + + expect(commandResult.error).toBeUndefined(); + expect(commandResult.status).toBe(7); + expect(commandResult.signal).toBeNull(); + expect(commandResult.stdout).toBe("synthetic stdout\n"); + expect(commandResult.stderr).toBe("synthetic stderr\n"); + + const commandQueuePathList = await readQueuePathList(queueRoot); + expect(commandQueuePathList).toHaveLength(1); + const [commandQueuePath] = commandQueuePathList; + assert( + commandQueuePath !== undefined, + "Command request path must be available.", + ); + const commandRequest = await parseQueueRequest(commandQueuePath); + assert( + commandRequest !== null, + "Command result must reach a workspace queue.", + ); + expect(commandRequest).toMatchObject({ + source: { kind: "command", name: basename(process.execPath) }, + title: "Failed with exit code 7.", + } satisfies Partial>); + expect(commandRequest.body?.split("\n").toSorted()).toEqual([ + "synthetic stderr", + "synthetic stdout", + ]); + + const cliNotificationId = `cli-notification-${randomUUID()}`; + await expect( + runPnpm({ + argumentList: [ + "exec", + "busy-octopus", + "notify", + "--title", + "Synthetic CLI notification", + "--body", + "Review the synthetic CLI result.", + "--directory", + workspaceDirectory, + "--notification-id", + cliNotificationId, + "--source-kind", + "test", + "--source-name", + "Synthetic CLI runner", ], - { - cwd: fixtureDirectory, - encoding: "utf8", - maxBuffer: MAXIMUM_PROCESS_OUTPUT_BYTE_COUNT, - timeout: 5_000, - env: runtimeEnvironment, - windowsHide: true, - }, - ); + directory: fixtureDirectory, + environment: runtimeEnvironment, + }), + ).resolves.toBe(`${cliNotificationId}\n`); + const cliQueueResult = await findQueueRequest({ + notificationId: cliNotificationId, + queueRoot, + }); + assert(cliQueueResult !== null, "CLI request must reach a workspace queue."); + expect(cliQueueResult.request).toMatchObject({ + notificationId: cliNotificationId, + title: "Synthetic CLI notification", + body: "Review the synthetic CLI result.", + source: { kind: "test", name: "Synthetic CLI runner" }, + }); - expect(commandResult.error).toBeUndefined(); - expect(commandResult.status).toBe(7); - expect(commandResult.signal).toBeNull(); - expect(commandResult.stdout).toBe("synthetic stdout\n"); - expect(commandResult.stderr).toBe("synthetic stderr\n"); - - const commandQueuePathList = await readQueuePathList(queueRoot); - expect(commandQueuePathList).toHaveLength(1); - const [commandQueuePath] = commandQueuePathList; - assert( - commandQueuePath !== undefined, - "Command request path must be available.", - ); - const commandRequest = await parseQueueRequest(commandQueuePath); - assert( - commandRequest !== null, - "Command result must reach a workspace queue.", - ); - expect(commandRequest).toMatchObject({ - source: { kind: "command", name: basename(process.execPath) }, - title: "Failed with exit code 7.", - } satisfies Partial>); - expect(commandRequest.body?.split("\n").toSorted()).toEqual([ - "synthetic stderr", - "synthetic stdout", - ]); - if (process.platform !== "win32") { - const signalResult = spawnSync( - process.execPath, - [ - cliPath, - "run", - "--success-only", - "--", - process.execPath, - "-e", - 'process.kill(process.pid, "SIGTERM")', - ], + await expect( + runPnpm({ + argumentList: [ + "exec", + "busy-octopus", + "agent", + "setup", + "codex", + "--skill", + "--directory", + workspaceDirectory, + "--yes", + ], + directory: fixtureDirectory, + environment: runtimeEnvironment, + }), + ).resolves.toBe("configured\n"); + await expect( + readFile( + join(workspaceDirectory, ".agents", "skills", "busy-octopus", "SKILL.md"), + "utf8", + ), + ).resolves.toBe(skillText); + expect( + JSON.parse( + await readFile(join(workspaceDirectory, ".codex", "hooks.json"), "utf8"), + ), + ).toMatchObject({ + hooks: { + Stop: [ { - cwd: fixtureDirectory, - encoding: "utf8", - maxBuffer: MAXIMUM_PROCESS_OUTPUT_BYTE_COUNT, - timeout: 5_000, - env: runtimeEnvironment, - windowsHide: true, + hooks: [ + { + command: "busy-octopus agent hook codex", + timeout: 2, + type: "command", + }, + ], }, - ); - - expect(signalResult.error).toBeUndefined(); - expect(signalResult.status).toBeNull(); - expect(signalResult.signal).toBe("SIGTERM"); - expect(signalResult.stdout).toBe(""); - expect(signalResult.stderr).toBe(""); - } + ], + }, + }); - const cliNotificationId = `cli-notification-${randomUUID()}`; - await expect( - runPnpm({ - argumentList: [ - "exec", - "busy-octopus", - "notify", - "--title", - "Synthetic CLI notification", - "--body", - "Review the synthetic CLI result.", - "--directory", - workspaceDirectory, - "--notification-id", - cliNotificationId, - "--source-kind", - "test", - "--source-name", - "Synthetic CLI runner", - ], - directory: fixtureDirectory, - environment: runtimeEnvironment, - }), - ).resolves.toBe(`${cliNotificationId}\n`); - const cliQueueResult = await findQueueRequest({ - notificationId: cliNotificationId, + expect( + runPnpmInput({ + argumentList: ["exec", "busy-octopus", "agent", "hook", "codex"], + directory: fixtureDirectory, + environment: runtimeEnvironment, + input: JSON.stringify({ + session_id: "package-session", + transcript_path: null, + cwd: workspaceDirectory, + hook_event_name: "Stop", + model: "gpt-synthetic", + turn_id: "package-turn", + stop_hook_active: false, + last_assistant_message: "Synthetic Codex result.", + } satisfies CodexHook), + }), + ).toEqual({ stderr: "", stdout: "{}\n" }); + expect( + await findQueueRequest({ + notificationId: "codex-turn-255c7515a88c50aa9915122472e60b11", queueRoot, - }); - assert( - cliQueueResult !== null, - "CLI request must reach a workspace queue.", - ); - expect(cliQueueResult.request).toMatchObject({ - notificationId: cliNotificationId, - title: "Synthetic CLI notification", - body: "Review the synthetic CLI result.", - source: { kind: "test", name: "Synthetic CLI runner" }, - }); - - await expect( - runPnpm({ - argumentList: [ - "exec", - "busy-octopus", - "agent", - "setup", - "codex", - "--skill", - "--directory", - workspaceDirectory, - "--yes", - ], - directory: fixtureDirectory, - environment: runtimeEnvironment, - }), - ).resolves.toBe("configured\n"); - await expect( - readFile( - join( - workspaceDirectory, - ".agents", - "skills", - "busy-octopus", - "SKILL.md", - ), - "utf8", - ), - ).resolves.toBe(skillText); - expect( - JSON.parse( - await readFile( - join(workspaceDirectory, ".codex", "hooks.json"), - "utf8", - ), - ), - ).toMatchObject({ - hooks: { - Stop: [ - { - hooks: [ - { - command: "busy-octopus agent hook codex", - timeout: 2, - type: "command", - }, - ], - }, - ], - }, - }); - - expect( - runPnpmInput({ - argumentList: ["exec", "busy-octopus", "agent", "hook", "codex"], - directory: fixtureDirectory, - environment: runtimeEnvironment, - input: JSON.stringify({ - session_id: "package-session", - transcript_path: null, - cwd: workspaceDirectory, - hook_event_name: "Stop", - model: "gpt-synthetic", - turn_id: "package-turn", - stop_hook_active: false, - last_assistant_message: "Synthetic Codex result.", - } satisfies CodexHook), - }), - ).toEqual({ stderr: "", stdout: "{}\n" }); - expect( - await findQueueRequest({ - notificationId: "codex-turn-255c7515a88c50aa9915122472e60b11", - queueRoot, - }), - ).toMatchObject({ - request: { - body: "Synthetic Codex result.", - source: { kind: "agent", name: "Codex" }, - title: "Codex", - }, - }); + }), + ).toMatchObject({ + request: { + body: "Synthetic Codex result.", + source: { kind: "agent", name: "Codex" }, + title: "Codex", + }, + }); - expect( - runPnpmInput({ - argumentList: ["exec", "busy-octopus", "agent", "hook", "claude-code"], - directory: fixtureDirectory, - environment: runtimeEnvironment, - input: JSON.stringify({ - session_id: "package-session", - prompt_id: "123e4567-e89b-42d3-a456-426614174000", - transcript_path: "/synthetic/transcript.jsonl", - cwd: workspaceDirectory, - hook_event_name: "Stop", - stop_hook_active: false, - last_assistant_message: "Synthetic Claude Code result.", - } satisfies ClaudeCodeHook), - }), - ).toEqual({ stderr: "", stdout: "" }); - expect( - await findQueueRequest({ - notificationId: "claude-turn-64a66c74259f1ae0046dab1ec55d0130", - queueRoot, - }), - ).toMatchObject({ - request: { - body: "Synthetic Claude Code result.", - source: { kind: "agent", name: "Claude Code" }, - title: "Claude Code", - }, - }); + expect( + runPnpmInput({ + argumentList: ["exec", "busy-octopus", "agent", "hook", "claude-code"], + directory: fixtureDirectory, + environment: runtimeEnvironment, + input: JSON.stringify({ + session_id: "package-session", + prompt_id: "123e4567-e89b-42d3-a456-426614174000", + transcript_path: "/synthetic/transcript.jsonl", + cwd: workspaceDirectory, + hook_event_name: "Stop", + stop_hook_active: false, + last_assistant_message: "Synthetic Claude Code result.", + } satisfies ClaudeCodeHook), + }), + ).toEqual({ stderr: "", stdout: "" }); + expect( + await findQueueRequest({ + notificationId: "claude-turn-64a66c74259f1ae0046dab1ec55d0130", + queueRoot, + }), + ).toMatchObject({ + request: { + body: "Synthetic Claude Code result.", + source: { kind: "agent", name: "Claude Code" }, + title: "Claude Code", + }, + }); - expect( - runPnpmInput({ - argumentList: ["exec", "busy-octopus", "agent", "hook", "codex"], - directory: fixtureDirectory, - environment: runtimeEnvironment, - input: "{", - }), - ).toEqual({ - stderr: - "busy-octopus: unable to process the agent hook: custom at (root).\n", - stdout: "{}\n", - }); + expect( + runPnpmInput({ + argumentList: ["exec", "busy-octopus", "agent", "hook", "codex"], + directory: fixtureDirectory, + environment: runtimeEnvironment, + input: "{", + }), + ).toEqual({ + stderr: + "busy-octopus: unable to process the agent hook: custom at (root).\n", + stdout: "{}\n", + }); - const javascriptPath = join(fixtureDirectory, "consumer.mjs"); - await writeFile( - javascriptPath, - `import { deepStrictEqual, ok } from "node:assert/strict"; + const javascriptPath = join(fixtureDirectory, "consumer.mjs"); + await writeFile( + javascriptPath, + `import { deepStrictEqual, ok } from "node:assert/strict"; const library = await import("busy-octopus"); deepStrictEqual(Object.keys(library), ["notify"]); @@ -525,9 +491,9 @@ try { await import("busy-octopus/dist/queue/queue.js"); } catch (error) { deepImportUnavailable = - error instanceof Error && - "code" in error && - error.code === "ERR_PACKAGE_PATH_NOT_EXPORTED"; + error instanceof Error && + "code" in error && + error.code === "ERR_PACKAGE_PATH_NOT_EXPORTED"; } ok(deepImportUnavailable, "Queue internals must not be importable."); @@ -539,25 +505,25 @@ process.stdout.write(JSON.stringify(await library.notify({ title: "Synthetic package notification", }))); `, - ); - const NotifyResult = z.strictObject({ notificationId: z.string() }); - expect( - NotifyResult.parse( - JSON.parse( - await executeTestCommand({ - argumentList: [javascriptPath, workspaceDirectory, notificationId], - command: process.execPath, - directory: fixtureDirectory, - environment: runtimeEnvironment, - }), - ), - ).notificationId, - ).toBe(notificationId); - - const typescriptPath = join(fixtureDirectory, "consumer.ts"); - await writeFile( - typescriptPath, - `import { + ); + const NotifyResult = z.strictObject({ notificationId: z.string() }); + expect( + NotifyResult.parse( + JSON.parse( + await executeTestCommand({ + argumentList: [javascriptPath, workspaceDirectory, notificationId], + command: process.execPath, + directory: fixtureDirectory, + environment: runtimeEnvironment, + }), + ), + ).notificationId, + ).toBe(notificationId); + + const typescriptPath = join(fixtureDirectory, "consumer.ts"); + await writeFile( + typescriptPath, + `import { notify, type NotificationSource, type NotifyInput, @@ -574,125 +540,142 @@ const input: NotifyInput = { const result: Promise = notify(input); void result; `, - ); - await executeTestCommand({ - argumentList: [ - join(repositoryDirectory, "node_modules", "typescript", "bin", "tsc"), - "--noEmit", - "--strict", - "--exactOptionalPropertyTypes", - "--module", - "NodeNext", - "--moduleResolution", - "NodeNext", - "--target", - "ES2023", - typescriptPath, - ], - command: process.execPath, - directory: fixtureDirectory, - }); - - const queueResult = await findQueueRequest({ notificationId, queueRoot }); - assert( - queueResult !== null, - "Library request must reach a workspace queue.", - ); - expect(queueResult.request.workspace.display.label).toBe( - basename(workspaceDirectory), - ); - expect(queueResult.request.workspace.display.branch).toBeNull(); - expect(basename(queueResult.directory)).toBe( - queueResult.request.workspace.instanceId, - ); - expect(queueResult.directory).toBe(cliQueueResult.directory); + ); + await executeTestCommand({ + argumentList: [ + join(repositoryDirectory, "node_modules", "typescript", "bin", "tsc"), + "--noEmit", + "--strict", + "--exactOptionalPropertyTypes", + "--module", + "NodeNext", + "--moduleResolution", + "NodeNext", + "--target", + "ES2023", + typescriptPath, + ], + command: process.execPath, + directory: fixtureDirectory, + }); - const primaryCheckoutDirectory = join(fixtureDirectory, "primary-checkout"); - const worktreeDirectory = join(fixtureDirectory, "linked-worktree"); - await executeTestCommand({ - argumentList: [ - "clone", - "--local", - "--no-hardlinks", - "--quiet", - repositoryDirectory, - primaryCheckoutDirectory, - ], - command: "git", - directory: fixtureDirectory, - }); - await executeTestCommand({ - argumentList: [ - "-C", - primaryCheckoutDirectory, - "worktree", - "add", - "--quiet", - "--detach", - worktreeDirectory, - ], - command: "git", - directory: fixtureDirectory, - }); - - const primaryNotificationId = `primary-notification-${randomUUID()}`; - const worktreeNotificationId = `linked-notification-${randomUUID()}`; - for (const [directory, checkoutNotificationId] of [ - [join(primaryCheckoutDirectory, "src"), primaryNotificationId], - [join(worktreeDirectory, "src"), worktreeNotificationId], - ] as const) { - await expect( - runPnpm({ - argumentList: [ - "exec", - "busy-octopus", - "notify", - "--title", - "Synthetic checkout notification", - "--directory", - directory, - "--notification-id", - checkoutNotificationId, - ], - directory: fixtureDirectory, - environment: runtimeEnvironment, - }), - ).resolves.toBe(`${checkoutNotificationId}\n`); - } + const queueResult = await findQueueRequest({ notificationId, queueRoot }); + assert(queueResult !== null, "Library request must reach a workspace queue."); + expect(queueResult.request.workspace.display.label).toBe( + basename(workspaceDirectory), + ); + expect(queueResult.request.workspace.display.branch).toBeNull(); + expect(basename(queueResult.directory)).toBe( + queueResult.request.workspace.instanceId, + ); + expect(queueResult.directory).toBe(cliQueueResult.directory); + + const primaryCheckoutDirectory = join(fixtureDirectory, "primary-checkout"); + const worktreeDirectory = join(fixtureDirectory, "linked-worktree"); + await executeTestCommand({ + argumentList: [ + "clone", + "--local", + "--no-hardlinks", + "--quiet", + repositoryDirectory, + primaryCheckoutDirectory, + ], + command: "git", + directory: fixtureDirectory, + }); + await executeTestCommand({ + argumentList: [ + "-C", + primaryCheckoutDirectory, + "worktree", + "add", + "--quiet", + "--detach", + worktreeDirectory, + ], + command: "git", + directory: fixtureDirectory, + }); - const primaryQueueResult = await findQueueRequest({ - notificationId: primaryNotificationId, - queueRoot, - }); - const worktreeQueueResult = await findQueueRequest({ - notificationId: worktreeNotificationId, - queueRoot, - }); - assert( - primaryQueueResult !== null, - "Primary checkout request must reach a workspace queue.", - ); - assert( - worktreeQueueResult !== null, - "Linked worktree request must reach a workspace queue.", - ); - expect(worktreeQueueResult.directory).not.toBe( - primaryQueueResult.directory, - ); - expect(primaryQueueResult.request.workspace.display.label).toBe( - basename(primaryCheckoutDirectory), - ); - expect(worktreeQueueResult.request.workspace.display.label).toBe( - basename(worktreeDirectory), - ); - } finally { - const queueResult = await findQueueRequest({ notificationId, queueRoot }); - if (queueResult !== null) { - await rm(queueResult.directory, { force: true, recursive: true }); - } - await Promise.all([ - rm(fixtureDirectory, { force: true, recursive: true }), - rm(artifactDirectory, { force: true, recursive: true }), - ]); + const primaryNotificationId = `primary-notification-${randomUUID()}`; + const worktreeNotificationId = `linked-notification-${randomUUID()}`; + for (const [directory, checkoutNotificationId] of [ + [join(primaryCheckoutDirectory, "src"), primaryNotificationId], + [join(worktreeDirectory, "src"), worktreeNotificationId], + ] as const) { + await expect( + runPnpm({ + argumentList: [ + "exec", + "busy-octopus", + "notify", + "--title", + "Synthetic checkout notification", + "--directory", + directory, + "--notification-id", + checkoutNotificationId, + ], + directory: fixtureDirectory, + environment: runtimeEnvironment, + }), + ).resolves.toBe(`${checkoutNotificationId}\n`); } + + const primaryQueueResult = await findQueueRequest({ + notificationId: primaryNotificationId, + queueRoot, + }); + const worktreeQueueResult = await findQueueRequest({ + notificationId: worktreeNotificationId, + queueRoot, + }); + assert( + primaryQueueResult !== null, + "Primary checkout request must reach a workspace queue.", + ); + assert( + worktreeQueueResult !== null, + "Linked worktree request must reach a workspace queue.", + ); + expect(worktreeQueueResult.directory).not.toBe(primaryQueueResult.directory); + expect(primaryQueueResult.request.workspace.display.label).toBe( + basename(primaryCheckoutDirectory), + ); + expect(worktreeQueueResult.request.workspace.display.label).toBe( + basename(worktreeDirectory), + ); }); + +it.skipIf(process.platform === "win32")( + "propagates command signal termination through the packed CLI", + () => { + const signalResult = spawnSync( + process.execPath, + [ + cliPath, + "run", + "--success-only", + "--", + process.execPath, + "-e", + 'process.kill(process.pid, "SIGTERM")', + ], + { + cwd: fixtureDirectory, + encoding: "utf8", + maxBuffer: MAXIMUM_PROCESS_OUTPUT_BYTE_COUNT, + timeout: 5_000, + env: runtimeEnvironment, + windowsHide: true, + }, + ); + + expect(signalResult.error).toBeUndefined(); + expect(signalResult.status).toBeNull(); + expect(signalResult.signal).toBe("SIGTERM"); + expect(signalResult.stdout).toBe(""); + expect(signalResult.stderr).toBe(""); + }, +); diff --git a/test/release/controls/publication.spec.test.ts b/test/release/controls/publication.spec.test.ts index 1c88ef7..4cb3809 100644 --- a/test/release/controls/publication.spec.test.ts +++ b/test/release/controls/publication.spec.test.ts @@ -20,11 +20,11 @@ const readStep = (name: string): string => { const block = workflow .split(` - name: ${name}\n`)[1] ?.split("\n - name:")[0]; - const script = block?.match(/^ {8}run: \|\n((?: {10}.*\n|\n)*)/m)?.[1]; + const script = block?.match(/^ {8}run: \|\n((?: {10}.*\n|\n)*)/mu)?.[1]; if (script !== undefined) { - return script.replace(/^ {10}/gm, ""); + return script.replaceAll(/^ {10}/gmu, ""); } - const command = block?.match(/^ {8}run: (.+)$/m)?.[1]; + const command = block?.match(/^ {8}run: (.+)$/mu)?.[1]; assert.isDefined(command); return command; }; @@ -199,11 +199,11 @@ it("orders publishers and keeps downloads bound to the original artifact", () => ); expect( workflow.match( - /artifact-ids: \$\{\{ needs.package.outputs.artifact-id \}\}/g, + /artifact-ids: \$\{\{ needs.package.outputs.artifact-id \}\}/gu, ), ).toHaveLength(3); expect(workflow).toMatch( - /artifact-id: \$\{\{ steps.artifact.outputs.artifact-id \}\}/, + /artifact-id: \$\{\{ steps.artifact.outputs.artifact-id \}\}/u, ); expect(workflow.indexOf("name: Confirm npm publication")).toBeLessThan( workflow.indexOf("name: Publish VS Code extension"), diff --git a/test/release/controls/tag.spec.test.ts b/test/release/controls/tag.spec.test.ts index 4b0edf4..1099440 100644 --- a/test/release/controls/tag.spec.test.ts +++ b/test/release/controls/tag.spec.test.ts @@ -12,6 +12,8 @@ const alternativeSha = "3".repeat(40); const tag = "v1.2.3"; const repository = "synthetic/project"; +type TagVerification = Parameters[0]; + const createFixture = () => { const gitResultMap = new Map([ ["rev-parse HEAD", commit], @@ -27,25 +29,29 @@ const createFixture = () => { object: { sha: commit, type: "commit" }, verification: { verified: true }, }; - const executeGit = vi.fn(async (argumentList: string[]) => { - const result = gitResultMap.get(argumentList.join(" ")); - if (result === undefined) { - throw new Error("Unexpected Git operation."); - } - if (result instanceof Error) { - throw result; - } - return result; - }); - const readGitHubJson = vi.fn(async (path: string) => { - if (path === `/repos/${repository}/git/ref/tags/${tag}`) { - return reference; - } - if (path === `/repos/${repository}/git/tags/${tagSha}`) { - return signature; - } - throw new Error("Unexpected GitHub operation."); - }); + const executeGit = vi.fn( + async (argumentList) => { + const result = gitResultMap.get(argumentList.join(" ")); + if (result === undefined) { + throw new Error("Unexpected Git operation."); + } + if (result instanceof Error) { + throw result; + } + return result; + }, + ); + const readGitHubJson = vi.fn( + async (path) => { + if (path === `/repos/${repository}/git/ref/tags/${tag}`) { + return reference; + } + if (path === `/repos/${repository}/git/tags/${tagSha}`) { + return signature; + } + throw new Error("Unexpected GitHub operation."); + }, + ); return { gitResultMap, diff --git a/test/release/vsix.spec.test.ts b/test/release/vsix.spec.test.ts index 9194b75..c3b2c39 100644 --- a/test/release/vsix.spec.test.ts +++ b/test/release/vsix.spec.test.ts @@ -36,44 +36,47 @@ const vscodeEnvironment = { VSCODE_IPC_HOOK_CLI: undefined, }; -it("installs, activates, disables, reinstalls, and uninstalls", { - timeout: 120_000, -}, async () => { - const extensionDirectory = join(fixtureDirectory, "extensions"); - const profileDirectory = join(fixtureDirectory, "profile"); - const probeDirectory = join(fixtureDirectory, "probe"); - const workspaceDirectory = join(fixtureDirectory, "workspace"); - const testRunnerPath = join(probeDirectory, "test.cjs"); - const profileArgumentList = [ - `--extensions-dir=${extensionDirectory}`, - `--user-data-dir=${profileDirectory}`, - ]; - await Promise.all([ - mkdir(extensionDirectory), - mkdir(profileDirectory), - mkdir(probeDirectory), - mkdir(workspaceDirectory), - ]); - - await Promise.all([ - writeFile( - join(probeDirectory, "package.json"), - JSON.stringify({ - activationEvents: [], - engines: { vscode: packageManifest.engines.vscode }, - main: "extension.cjs", - name: "busy-octopus-release-probe", - publisher: "mindedtech", - version: "0.0.0", - }), - ), - writeFile( - join(probeDirectory, "extension.cjs"), - "exports.activate = () => undefined;\n", - ), - writeFile( - testRunnerPath, - `const vscode = require("vscode"); +it( + "installs, activates, disables, reinstalls, and uninstalls", + { + timeout: 120_000, + }, + async () => { + const extensionDirectory = join(fixtureDirectory, "extensions"); + const profileDirectory = join(fixtureDirectory, "profile"); + const probeDirectory = join(fixtureDirectory, "probe"); + const workspaceDirectory = join(fixtureDirectory, "workspace"); + const testRunnerPath = join(probeDirectory, "test.cjs"); + const profileArgumentList = [ + `--extensions-dir=${extensionDirectory}`, + `--user-data-dir=${profileDirectory}`, + ]; + await Promise.all([ + mkdir(extensionDirectory), + mkdir(profileDirectory), + mkdir(probeDirectory), + mkdir(workspaceDirectory), + ]); + + await Promise.all([ + writeFile( + join(probeDirectory, "package.json"), + JSON.stringify({ + activationEvents: [], + engines: { vscode: packageManifest.engines.vscode }, + main: "extension.cjs", + name: "busy-octopus-release-probe", + publisher: "mindedtech", + version: "0.0.0", + }), + ), + writeFile( + join(probeDirectory, "extension.cjs"), + "exports.activate = () => undefined;\n", + ), + writeFile( + testRunnerPath, + `const vscode = require("vscode"); exports.run = async () => { const extension = vscode.extensions.getExtension(${JSON.stringify(extensionId)}); @@ -96,69 +99,70 @@ exports.run = async () => { } }; `, - ), - ]); - - const runCli = (argumentList: string[]) => - executeVsCodeCli({ - argumentList: [...profileArgumentList, ...argumentList], - executablePath: vscodeExecutablePath, - directory: repositoryDirectory, - environment: vscodeEnvironment, + ), + ]); + + const runCli = (argumentList: string[]) => + executeVsCodeCli({ + argumentList: [...profileArgumentList, ...argumentList], + executablePath: vscodeExecutablePath, + directory: repositoryDirectory, + environment: vscodeEnvironment, + }); + + await runCli(["--install-extension", extensionArchivePath]); + + expect( + (await runCli(["--list-extensions", "--show-versions"])) + .trim() + .split(/\r?\n/u), + ).toContain(extensionVersion); + + await runTests({ + extensionDevelopmentPath: probeDirectory, + extensionTestsEnv: { + BUSY_OCTOPUS_ACTIVATION_EXPECTED: "1", + ELECTRON_RUN_AS_NODE: undefined, + VSCODE_IPC_HOOK_CLI: undefined, + }, + extensionTestsPath: testRunnerPath, + launchArgs: [ + workspaceDirectory, + ...profileArgumentList, + "--disable-telemetry", + ], + vscodeExecutablePath, + }); + await runTests({ + extensionDevelopmentPath: probeDirectory, + extensionTestsEnv: { + BUSY_OCTOPUS_ACTIVATION_EXPECTED: "0", + ELECTRON_RUN_AS_NODE: undefined, + VSCODE_IPC_HOOK_CLI: undefined, + }, + extensionTestsPath: testRunnerPath, + launchArgs: [ + workspaceDirectory, + ...profileArgumentList, + "--disable-extension", + extensionId, + "--disable-telemetry", + ], + vscodeExecutablePath, }); - await runCli(["--install-extension", extensionArchivePath]); - - expect( - (await runCli(["--list-extensions", "--show-versions"])) - .trim() - .split(/\r?\n/u), - ).toContain(extensionVersion); - - await runTests({ - extensionDevelopmentPath: probeDirectory, - extensionTestsEnv: { - BUSY_OCTOPUS_ACTIVATION_EXPECTED: "1", - ELECTRON_RUN_AS_NODE: undefined, - VSCODE_IPC_HOOK_CLI: undefined, - }, - extensionTestsPath: testRunnerPath, - launchArgs: [ - workspaceDirectory, - ...profileArgumentList, - "--disable-telemetry", - ], - vscodeExecutablePath, - }); - await runTests({ - extensionDevelopmentPath: probeDirectory, - extensionTestsEnv: { - BUSY_OCTOPUS_ACTIVATION_EXPECTED: "0", - ELECTRON_RUN_AS_NODE: undefined, - VSCODE_IPC_HOOK_CLI: undefined, - }, - extensionTestsPath: testRunnerPath, - launchArgs: [ - workspaceDirectory, - ...profileArgumentList, - "--disable-extension", - extensionId, - "--disable-telemetry", - ], - vscodeExecutablePath, - }); - - await runCli(["--install-extension", extensionArchivePath, "--force"]); - expect( - (await runCli(["--list-extensions", "--show-versions"])) - .trim() - .split(/\r?\n/u), - ).toContain(extensionVersion); - - await runCli(["--uninstall-extension", extensionId]); - expect( - (await runCli(["--list-extensions", "--show-versions"])) - .trim() - .split(/\r?\n/u), - ).not.toContain(extensionVersion); -}); + await runCli(["--install-extension", extensionArchivePath, "--force"]); + expect( + (await runCli(["--list-extensions", "--show-versions"])) + .trim() + .split(/\r?\n/u), + ).toContain(extensionVersion); + + await runCli(["--uninstall-extension", extensionId]); + expect( + (await runCli(["--list-extensions", "--show-versions"])) + .trim() + .split(/\r?\n/u), + ).not.toContain(extensionVersion); + }, +); diff --git a/test/vscode.ts b/test/vscode.ts index da2afb8..e1f194c 100644 --- a/test/vscode.ts +++ b/test/vscode.ts @@ -64,7 +64,10 @@ export const executeVsCodeCli = async ({ reuseMachineInstall: true, }, ); - ok(command, "VS Code CLI path is unavailable."); + ok( + command !== undefined && command.length > 0, + "VS Code CLI path is unavailable.", + ); return executeTestCommand({ argumentList: [...cliArgumentList, ...argumentList],