oh-my-opencode 5.0.0-beta.62 → 5.0.0-beta.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +8 -6
- package/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
- package/dist/cli/index.js +70 -41
- package/dist/cli-node/index.js +70 -41
- package/dist/config/schema/agent-names.d.ts +0 -1
- package/dist/config/schema/browser-automation.d.ts +0 -2
- package/dist/config/schema/oh-my-opencode-config.d.ts +0 -1
- package/dist/hooks/comment-checker/__fixtures__/download-probe.fixture.d.ts +1 -0
- package/dist/index.js +89 -588
- package/dist/oh-my-opencode.schema.json +4 -5
- package/dist/skills/ast-grep/SKILL.md +12 -0
- package/dist/skills/debugging/references/tools/playwright-cli.md +79 -161
- package/dist/skills/frontend/SKILL.md +1 -1
- package/dist/skills/frontend/references/design/aside.md +1 -1
- package/dist/skills/frontend/references/design/clone-from-url.md +1 -1
- package/dist/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/dist/skills/review-work/SKILL.md +1 -1
- package/dist/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/dist/skills/ultimate-browsing/SKILL.md +17 -29
- package/dist/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/dist/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/dist/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/dist/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/dist/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/dist/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/dist/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
- package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -3
- package/dist/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
- package/dist/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/dist/skills/ulw-execute/SKILL.md +1 -1
- package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
- package/dist/skills/ulw-research/SKILL.md +3 -3
- package/dist/skills/visual-qa/AGENTS.md +1 -1
- package/dist/skills/visual-qa/SKILL.md +1 -1
- package/dist/skills/visual-qa/references/browser-setup.md +75 -0
- package/dist/tui.js +18 -11
- package/package.json +16 -23
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +4 -2
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +5 -4
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.d.ts +7 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +7 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +7 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +931 -442
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +20 -12
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +8 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.d.ts +4 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.js +18 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.js +37 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +24 -12
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +6 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +54 -25
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.d.ts +10 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.js +38 -31
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +11 -8
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +28 -12
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.d.ts +335 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.js +109 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/types.d.ts +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +8 -62
- package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.js +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-types.d.ts +9 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +11 -11
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
- package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.d.ts +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.js +41 -0
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-codex-validation.ts +15 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +7 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +26 -14
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +8 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/driver-objective-ack.ts +20 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/evidence-artifacts.ts +45 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +23 -16
- package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +7 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +67 -54
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-goal-factory.ts +63 -31
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +11 -7
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +47 -12
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/manifest.ts +124 -10
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/types.ts +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +8 -59
- package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
- package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-mutations.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-types.ts +13 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +12 -10
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
- package/packages/omo-codex/plugin/components/ulw-loop/src/success-criteria-input.ts +56 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts +37 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +33 -4
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-dx.test.ts +219 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
- package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +15 -7
- package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
- package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
- package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +5 -4
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +6 -4
- package/packages/prompts-core/prompts/ultrawork/codex.md +5 -4
- package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
- package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
- package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
- package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
- package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
- package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
- package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
- package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
- package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
- package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
- package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
- package/dist/skills/visual-qa/references/agent-browser-setup.md +0 -45
- package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
- package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { ulwLoopDir } from "../src/paths.js";
|
|
7
|
+
import { createUlwLoopPlan } from "../src/plan-crud.js";
|
|
8
|
+
import * as io from "../src/plan-io.js";
|
|
9
|
+
import { childWriter, cleanupWriters } from "./fixtures/commit-child.js";
|
|
10
|
+
import { seedCrash } from "./fixtures/commit-crash.js";
|
|
11
|
+
import { leaseClock } from "./fixtures/lease-clock.js";
|
|
12
|
+
|
|
13
|
+
let root: string;
|
|
14
|
+
let dir: string;
|
|
15
|
+
const scope = { sessionId: "commit-log" };
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
root = await mkdtemp(join(tmpdir(), "ulw-commit-"));
|
|
18
|
+
dir = ulwLoopDir(root, scope);
|
|
19
|
+
});
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await cleanupWriters();
|
|
22
|
+
await rm(root, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
function snapshot() {
|
|
25
|
+
return Object.fromEntries(
|
|
26
|
+
["goals.json", "ledger.jsonl", "brief.md"].map((name) => [
|
|
27
|
+
name,
|
|
28
|
+
existsSync(join(dir, name)) ? readFileSync(join(dir, name), "utf8") : null,
|
|
29
|
+
]),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("#given an immutable plan and audit log", () => {
|
|
34
|
+
it("#when (7a) a writer dies after publishing #then reads reconcile without writes and locked reads repair views", async () => {
|
|
35
|
+
const next = await seedCrash(root, scope, false);
|
|
36
|
+
const before = snapshot();
|
|
37
|
+
expect(await io.readUlwLoopPlan(root, scope)).toEqual(next);
|
|
38
|
+
expect(io.readLedger(root, scope).at(-1)?.id).toBe(`${next.revision}-0`);
|
|
39
|
+
expect(snapshot()).toEqual(before);
|
|
40
|
+
await io.withUlwLoopMutationLock(root, scope, () => io.readUlwLoopPlan(root, scope));
|
|
41
|
+
expect(JSON.parse(readFileSync(join(dir, "goals.json"), "utf8"))).toEqual(next);
|
|
42
|
+
expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe(next.brief);
|
|
43
|
+
});
|
|
44
|
+
it("#when (7b) first creation has no goals cache #then existence and reads use the log", async () => {
|
|
45
|
+
const next = await seedCrash(root, scope, true);
|
|
46
|
+
expect(io.planExists(root, scope)).toBe(true);
|
|
47
|
+
expect(await io.readUlwLoopPlan(root, scope)).toEqual(next);
|
|
48
|
+
});
|
|
49
|
+
it("#when (8) a legacy plan first commits #then revision zero and legacy ledger and brief survive", async () => {
|
|
50
|
+
const plan = await createUlwLoopPlan(root, { brief: "legacy" }, scope);
|
|
51
|
+
delete plan.revision;
|
|
52
|
+
delete plan.brief;
|
|
53
|
+
delete plan.ledgerResetRevision;
|
|
54
|
+
await rm(join(dir, "revisions"), { recursive: true, force: true });
|
|
55
|
+
writeFileSync(join(dir, "goals.json"), JSON.stringify(plan));
|
|
56
|
+
writeFileSync(join(dir, "ledger.jsonl"), '{"at":"legacy","kind":"goal_started"}\n{"torn":');
|
|
57
|
+
expect(io.readLedger(root, scope)[0]).toMatchObject({ revision: 0, id: "legacy-1" });
|
|
58
|
+
const { commit } = await import("../src/plan-commit.js");
|
|
59
|
+
await io.withUlwLoopMutationLock(root, scope, async () => {
|
|
60
|
+
const legacy = await io.readUlwLoopPlan(root, scope);
|
|
61
|
+
await commit(root, scope, { plan: legacy, entries: [] });
|
|
62
|
+
});
|
|
63
|
+
expect(readdirSync(join(dir, "revisions"))).toEqual(["00000001.json"]);
|
|
64
|
+
expect(io.readLedger(root, scope)[0]?.id).toBe("legacy-1");
|
|
65
|
+
expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe("legacy\n");
|
|
66
|
+
});
|
|
67
|
+
it("#when (9) force recreation succeeds #then the reset is a new revision with filtered views", async () => {
|
|
68
|
+
const first = await createUlwLoopPlan(root, { brief: "old" }, scope);
|
|
69
|
+
const next = await createUlwLoopPlan(root, { brief: "new", force: true }, scope);
|
|
70
|
+
expect(next.revision).toBe((first.revision ?? 0) + 1);
|
|
71
|
+
expect(next.ledgerResetRevision).toBe(next.revision);
|
|
72
|
+
expect(io.readLedger(root, scope)).toHaveLength(1);
|
|
73
|
+
expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe(next.brief);
|
|
74
|
+
expect(readdirSync(join(dir, "revisions"))).toHaveLength(2);
|
|
75
|
+
});
|
|
76
|
+
it("#when (12) hard links are unsupported #then publication fails closed without a record or view changes", async () => {
|
|
77
|
+
await createUlwLoopPlan(root, { brief: "old" }, scope);
|
|
78
|
+
const before = snapshot();
|
|
79
|
+
const { withCommitHooks, commit } = await import("../src/plan-commit.js");
|
|
80
|
+
await expect(
|
|
81
|
+
withCommitHooks(
|
|
82
|
+
{
|
|
83
|
+
link: () => {
|
|
84
|
+
throw Object.assign(new Error("unsupported"), { code: "EPERM" });
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
() =>
|
|
88
|
+
io.withUlwLoopMutationLock(root, scope, async () => {
|
|
89
|
+
const plan = await io.readUlwLoopPlan(root, scope);
|
|
90
|
+
plan.brief = "forbidden";
|
|
91
|
+
await commit(root, scope, { plan, entries: [] });
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
).rejects.toMatchObject({ code: "ULW_LOOP_PUBLISH_UNSUPPORTED_FS" });
|
|
95
|
+
expect(snapshot()).toEqual(before);
|
|
96
|
+
expect(readdirSync(join(dir, "revisions"))).toHaveLength(1);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("#given paused writers in separate processes", () => {
|
|
101
|
+
it.each(["write", "force"])(
|
|
102
|
+
"#when (5)/(9) B wins %s after A's precheck #then A loses without publishing or changing views",
|
|
103
|
+
async (mode) => {
|
|
104
|
+
await createUlwLoopPlan(root, { brief: "initial" }, scope);
|
|
105
|
+
const { withCommitHooks, commit } = await import("../src/plan-commit.js");
|
|
106
|
+
let successor: ReturnType<typeof snapshot> | undefined;
|
|
107
|
+
let writes = 0;
|
|
108
|
+
const clock = leaseClock();
|
|
109
|
+
await expect(
|
|
110
|
+
withCommitHooks(
|
|
111
|
+
{
|
|
112
|
+
beforeCommit: async () => {
|
|
113
|
+
clock.advance(201);
|
|
114
|
+
const writer = childWriter(root, mode, clock.now());
|
|
115
|
+
expect(await writer.exited).toBe(0);
|
|
116
|
+
successor = snapshot();
|
|
117
|
+
},
|
|
118
|
+
writeView: async () => {
|
|
119
|
+
writes += 1;
|
|
120
|
+
throw new Error("loser wrote a view");
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
() =>
|
|
124
|
+
io.withUlwLoopMutationLock(
|
|
125
|
+
root,
|
|
126
|
+
scope,
|
|
127
|
+
async () => {
|
|
128
|
+
const plan = await io.readUlwLoopPlan(root, scope);
|
|
129
|
+
plan.brief = "A\n";
|
|
130
|
+
await commit(root, scope, { plan, entries: [{ at: "A", kind: "goal_added", message: "A" }] });
|
|
131
|
+
},
|
|
132
|
+
{ clock, leaseMs: 200, heartbeatMs: 0 },
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
).rejects.toMatchObject({ code: "ULW_LOOP_LOCK_LOST" });
|
|
136
|
+
expect(writes).toBe(0);
|
|
137
|
+
expect(snapshot()).toEqual(successor);
|
|
138
|
+
const published = JSON.parse(readFileSync(join(dir, "revisions", "00000002.json"), "utf8"));
|
|
139
|
+
expect(published.plan).toEqual(await io.readUlwLoopPlan(root, scope));
|
|
140
|
+
expect(published.ledger).toHaveLength(1);
|
|
141
|
+
expect(io.readLedger(root, scope).at(-1)).toEqual(published.ledger[0]);
|
|
142
|
+
expect(published.ledger[0].kind).toBe(mode === "force" ? "plan_created" : "goal_added");
|
|
143
|
+
expect(io.readLedger(root, scope).some((entry) => entry.message === "A")).toBe(false);
|
|
144
|
+
expect(readdirSync(join(dir, "revisions"))).toEqual(["00000001.json", "00000002.json"]);
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
it("#when (6) A wins before the valid holder B #then B retries once from A's revision", async () => {
|
|
148
|
+
await createUlwLoopPlan(root, { brief: "initial" }, scope);
|
|
149
|
+
const { withCommitHooks, commit } = await import("../src/plan-commit.js");
|
|
150
|
+
let writer: ReturnType<typeof childWriter> | undefined;
|
|
151
|
+
const clock = leaseClock();
|
|
152
|
+
await withCommitHooks(
|
|
153
|
+
{
|
|
154
|
+
beforeCommit: async () => {
|
|
155
|
+
clock.advance(201);
|
|
156
|
+
writer = childWriter(root, "mirror", clock.now());
|
|
157
|
+
await writer.wait("before");
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
() =>
|
|
161
|
+
io.withUlwLoopMutationLock(
|
|
162
|
+
root,
|
|
163
|
+
scope,
|
|
164
|
+
async () => {
|
|
165
|
+
const plan = await io.readUlwLoopPlan(root, scope);
|
|
166
|
+
plan.codexObjective = "A";
|
|
167
|
+
await commit(root, scope, { plan, entries: [{ at: "A", kind: "goal_added", message: "A" }] });
|
|
168
|
+
},
|
|
169
|
+
{ clock, leaseMs: 200, heartbeatMs: 0 },
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
if (writer === undefined) throw new Error("B not started");
|
|
173
|
+
writer.child.send("resume");
|
|
174
|
+
expect(await writer.exited).toBe(0);
|
|
175
|
+
expect((await io.readUlwLoopPlan(root, scope)).codexObjective).toBe("A|B");
|
|
176
|
+
expect(
|
|
177
|
+
io
|
|
178
|
+
.readLedger(root, scope)
|
|
179
|
+
.slice(-2)
|
|
180
|
+
.map((entry) => entry.message),
|
|
181
|
+
).toEqual(["A", "B"]);
|
|
182
|
+
expect(readdirSync(join(dir, "revisions"))).toHaveLength(3);
|
|
183
|
+
});
|
|
184
|
+
it("#when (7c) a child dies after an acknowledged partial temp write #then every public view remains complete", async () => {
|
|
185
|
+
await createUlwLoopPlan(root, { brief: "initial" }, scope);
|
|
186
|
+
const before = snapshot();
|
|
187
|
+
const writer = childWriter(root, "partial");
|
|
188
|
+
await writer.wait("partial");
|
|
189
|
+
writer.child.kill("SIGKILL");
|
|
190
|
+
await writer.exited;
|
|
191
|
+
const after = snapshot();
|
|
192
|
+
const published = JSON.parse(readFileSync(join(dir, "revisions", "00000002.json"), "utf8"));
|
|
193
|
+
expect([before["goals.json"], `${JSON.stringify(published.plan, null, 2)}\n`]).toContain(after["goals.json"]);
|
|
194
|
+
expect([before["brief.md"], published.plan.brief]).toContain(after["brief.md"]);
|
|
195
|
+
expect(after["ledger.jsonl"]).toBe(before["ledger.jsonl"]);
|
|
196
|
+
for (const line of readFileSync(join(dir, "ledger.jsonl"), "utf8").trim().split("\n"))
|
|
197
|
+
expect(() => JSON.parse(line)).not.toThrow();
|
|
198
|
+
expect(io.readLedger(root, scope).at(-1)?.message).toBe("B");
|
|
199
|
+
});
|
|
200
|
+
it.each(["accepted", "rejected"])(
|
|
201
|
+
"#when (11) %s steering dies after link #then its audit survives and accepted retries dedupe",
|
|
202
|
+
async (mode) => {
|
|
203
|
+
const original = await createUlwLoopPlan(root, { brief: "initial" }, scope);
|
|
204
|
+
const writer = childWriter(root, mode);
|
|
205
|
+
await writer.wait("linked");
|
|
206
|
+
writer.child.kill("SIGKILL");
|
|
207
|
+
await writer.exited;
|
|
208
|
+
expect(io.readLedger(root, scope).at(-1)?.kind).toBe(`steering_${mode}`);
|
|
209
|
+
const prior = await io.findAcceptedSteeringLedgerEntry(root, "crash-key", scope);
|
|
210
|
+
if (mode === "rejected") {
|
|
211
|
+
expect(prior).toBeUndefined();
|
|
212
|
+
expect(await io.readUlwLoopPlan(root, scope)).toEqual({ ...original, revision: 2 });
|
|
213
|
+
} else {
|
|
214
|
+
const { steerUlwLoop } = await import("../src/steering.js");
|
|
215
|
+
const result = await steerUlwLoop(
|
|
216
|
+
root,
|
|
217
|
+
{
|
|
218
|
+
kind: "annotate_ledger",
|
|
219
|
+
source: "cli",
|
|
220
|
+
evidence: "observed",
|
|
221
|
+
rationale: "audit",
|
|
222
|
+
idempotencyKey: "crash-key",
|
|
223
|
+
},
|
|
224
|
+
scope,
|
|
225
|
+
);
|
|
226
|
+
expect(result.deduped).toBe(true);
|
|
227
|
+
expect(io.readLedger(root, scope)).toHaveLength(2);
|
|
228
|
+
expect(readdirSync(join(dir, "revisions"))).toHaveLength(2);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
it("#when (10) retry fails before commit #then no goal_retried audit escapes", async () => {
|
|
233
|
+
const { startNextUlwLoop } = await import("../src/plan-crud.js");
|
|
234
|
+
const plan = await createUlwLoopPlan(root, { brief: "initial" }, scope);
|
|
235
|
+
const { commit, withCommitHooks } = await import("../src/plan-commit.js");
|
|
236
|
+
for (const goal of plan.goals) goal.status = "failed";
|
|
237
|
+
await commit(root, scope, { plan, entries: [] });
|
|
238
|
+
const before = snapshot();
|
|
239
|
+
await expect(
|
|
240
|
+
withCommitHooks(
|
|
241
|
+
{
|
|
242
|
+
beforeCommit: () => {
|
|
243
|
+
throw new Error("stop before commit");
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
() => startNextUlwLoop(root, { retryFailed: true }, scope),
|
|
247
|
+
),
|
|
248
|
+
).rejects.toThrow("stop before commit");
|
|
249
|
+
expect(snapshot()).toEqual(before);
|
|
250
|
+
expect(io.readLedger(root, scope).some((entry) => entry.kind === "goal_retried")).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import type { PreToolUsePayload } from "../src/codex-hook.js";
|
|
7
|
+
import { ulwLoopDir } from "../src/paths.js";
|
|
8
|
+
import { commit, materialize, withCommitHooks } from "../src/plan-commit.js";
|
|
9
|
+
import { createUlwLoopPlan } from "../src/plan-crud.js";
|
|
10
|
+
import {
|
|
11
|
+
planExists,
|
|
12
|
+
readLedger,
|
|
13
|
+
readUlwLoopPlan,
|
|
14
|
+
withMutationLockOptions,
|
|
15
|
+
withUlwLoopMutationLock,
|
|
16
|
+
} from "../src/plan-io.js";
|
|
17
|
+
import { applySpawnBudgetGuards } from "../src/spawn-guard.js";
|
|
18
|
+
import { runStopResumeHook } from "../src/stop-resume-hook.js";
|
|
19
|
+
import { childWriter, cleanupWriters } from "./fixtures/commit-child.js";
|
|
20
|
+
import { leaseClock } from "./fixtures/lease-clock.js";
|
|
21
|
+
|
|
22
|
+
let root: string;
|
|
23
|
+
let dir: string;
|
|
24
|
+
const scope = { sessionId: "commit-log" };
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
root = await mkdtemp(join(tmpdir(), "ulw-recovery-"));
|
|
27
|
+
dir = ulwLoopDir(root, scope);
|
|
28
|
+
});
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
await cleanupWriters();
|
|
31
|
+
vi.unstubAllEnvs();
|
|
32
|
+
await rm(root, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
function signal() {
|
|
35
|
+
let resolve: () => void = () => {
|
|
36
|
+
throw new Error("signal not initialized");
|
|
37
|
+
};
|
|
38
|
+
const promise = new Promise<void>((done, reject) => {
|
|
39
|
+
const timeout = setTimeout(() => reject(new Error("signal timed out")), 5_000);
|
|
40
|
+
resolve = () => {
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
done();
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
return { promise, resolve };
|
|
46
|
+
}
|
|
47
|
+
function views() {
|
|
48
|
+
return ["goals.json", "ledger.jsonl", "brief.md"].map((name) => readFileSync(join(dir, name), "utf8"));
|
|
49
|
+
}
|
|
50
|
+
describe("#given crash recovery through public surfaces", () => {
|
|
51
|
+
it("#when (7b) only revision one exists #then both hooks and existence gates use it", async () => {
|
|
52
|
+
const plan = await createUlwLoopPlan(root, { brief: "unfinished" }, scope);
|
|
53
|
+
for (const name of ["goals.json", "ledger.jsonl", "brief.md"]) await rm(join(dir, name));
|
|
54
|
+
expect(readdirSync(join(dir, "revisions"))).toEqual(["00000001.json"]);
|
|
55
|
+
expect(planExists(root, scope)).toBe(true);
|
|
56
|
+
expect((await readUlwLoopPlan(root, scope)).revision).toBe(1);
|
|
57
|
+
const payload = {
|
|
58
|
+
session_id: scope.sessionId,
|
|
59
|
+
turn_id: "turn",
|
|
60
|
+
cwd: root,
|
|
61
|
+
model: "test",
|
|
62
|
+
permission_mode: "default",
|
|
63
|
+
};
|
|
64
|
+
const stop = {
|
|
65
|
+
...payload,
|
|
66
|
+
hook_event_name: "Stop",
|
|
67
|
+
transcript_path: join(root, "transcript"),
|
|
68
|
+
stop_hook_active: false,
|
|
69
|
+
};
|
|
70
|
+
expect(JSON.parse(runStopResumeHook(stop)).decision).toBe("block");
|
|
71
|
+
expect(JSON.parse(readFileSync(join(dir, `auto-resume-${plan.goals[0]?.id}.json`), "utf8")).ledgerLineCount).toBe(
|
|
72
|
+
1,
|
|
73
|
+
);
|
|
74
|
+
vi.stubEnv("OMO_SPAWN_FANOUT_LIMIT", "1");
|
|
75
|
+
vi.stubEnv("PLUGIN_DATA", join(root, "plugin-data"));
|
|
76
|
+
const spawn: PreToolUsePayload = {
|
|
77
|
+
...payload,
|
|
78
|
+
hook_event_name: "PreToolUse",
|
|
79
|
+
transcript_path: null,
|
|
80
|
+
tool_name: "spawn_agent",
|
|
81
|
+
tool_use_id: "tool",
|
|
82
|
+
tool_input: {},
|
|
83
|
+
};
|
|
84
|
+
expect(applySpawnBudgetGuards(spawn)).toBe("");
|
|
85
|
+
expect(JSON.parse(applySpawnBudgetGuards(spawn)).hookSpecificOutput.permissionDecision).toBe("deny");
|
|
86
|
+
});
|
|
87
|
+
it("#when (7c) two materializers overlap #then every public inode is complete even if a view regresses", async () => {
|
|
88
|
+
const plan = await createUlwLoopPlan(root, { brief: "old" }, scope);
|
|
89
|
+
const old = views();
|
|
90
|
+
await withCommitHooks(
|
|
91
|
+
{
|
|
92
|
+
afterCommit: () => {
|
|
93
|
+
throw new Error("crash");
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
() =>
|
|
97
|
+
commit(root, scope, {
|
|
98
|
+
plan: { ...plan, brief: "middle\n" },
|
|
99
|
+
entries: [{ at: "middle", kind: "goal_added" }],
|
|
100
|
+
}),
|
|
101
|
+
).catch((error: unknown) => {
|
|
102
|
+
expect(error).toBeInstanceOf(Error);
|
|
103
|
+
});
|
|
104
|
+
const middleViews = [
|
|
105
|
+
`${JSON.stringify(await readUlwLoopPlan(root, scope), null, 2)}\n`,
|
|
106
|
+
`${readLedger(root, scope)
|
|
107
|
+
.map((entry) => JSON.stringify(entry))
|
|
108
|
+
.join("\n")}\n`,
|
|
109
|
+
"middle\n",
|
|
110
|
+
];
|
|
111
|
+
const partial = signal();
|
|
112
|
+
const resume = signal();
|
|
113
|
+
const first = withCommitHooks(
|
|
114
|
+
{
|
|
115
|
+
writeView: async (path, content) => {
|
|
116
|
+
if (path.endsWith("ledger.jsonl")) {
|
|
117
|
+
await writeFile(path, content.slice(0, 5));
|
|
118
|
+
partial.resolve();
|
|
119
|
+
await resume.promise;
|
|
120
|
+
}
|
|
121
|
+
await writeFile(path, content);
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
() => materialize(dir),
|
|
125
|
+
);
|
|
126
|
+
await partial.promise;
|
|
127
|
+
expect(readFileSync(join(dir, "ledger.jsonl"), "utf8")).toBe(old[1]);
|
|
128
|
+
const latest = await readUlwLoopPlan(root, scope);
|
|
129
|
+
await commit(root, scope, { plan: { ...latest, brief: "new\n" }, entries: [{ at: "new", kind: "goal_added" }] });
|
|
130
|
+
const newViews = views();
|
|
131
|
+
resume.resolve();
|
|
132
|
+
await first;
|
|
133
|
+
const final = views();
|
|
134
|
+
expect(() => JSON.parse(final[0] ?? "")).not.toThrow();
|
|
135
|
+
for (const line of (final[1] ?? "").trim().split("\n")) expect(() => JSON.parse(line)).not.toThrow();
|
|
136
|
+
for (const [index, content] of final.entries()) expect([middleViews[index], newViews[index]]).toContain(content);
|
|
137
|
+
expect(
|
|
138
|
+
readLedger(root, scope)
|
|
139
|
+
.map((entry) => entry.at)
|
|
140
|
+
.slice(-2),
|
|
141
|
+
).toEqual(["middle", "new"]);
|
|
142
|
+
});
|
|
143
|
+
it("#when (9) a force-recreator loses before its first possible view write #then its successor stays untouched", async () => {
|
|
144
|
+
await createUlwLoopPlan(root, { brief: "old" }, scope);
|
|
145
|
+
let successor: string[] = [];
|
|
146
|
+
let writes = 0;
|
|
147
|
+
const clock = leaseClock();
|
|
148
|
+
await expect(
|
|
149
|
+
withCommitHooks(
|
|
150
|
+
{
|
|
151
|
+
beforeMutation: async () => {
|
|
152
|
+
clock.advance(201);
|
|
153
|
+
const writer = childWriter(root, "force", clock.now());
|
|
154
|
+
expect(await writer.exited).toBe(0);
|
|
155
|
+
successor = views();
|
|
156
|
+
},
|
|
157
|
+
writeView: async (path, content) => {
|
|
158
|
+
writes += 1;
|
|
159
|
+
await writeFile(path, content);
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
() =>
|
|
163
|
+
withMutationLockOptions({ clock, leaseMs: 200, heartbeatMs: 0 }, () =>
|
|
164
|
+
createUlwLoopPlan(root, { brief: "A", force: true }, scope),
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
).rejects.toMatchObject({ code: "ULW_LOOP_LOCK_LOST" });
|
|
168
|
+
expect(writes).toBe(0);
|
|
169
|
+
expect(views()).toEqual(successor);
|
|
170
|
+
expect(readLedger(root, scope)).toHaveLength(1);
|
|
171
|
+
});
|
|
172
|
+
it("#when a locked ledger-only read follows a crash #then all views are repaired without another revision", async () => {
|
|
173
|
+
const plan = await createUlwLoopPlan(root, { brief: "old" }, scope);
|
|
174
|
+
await expect(
|
|
175
|
+
withCommitHooks(
|
|
176
|
+
{
|
|
177
|
+
afterCommit: () => {
|
|
178
|
+
throw new Error("crash");
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
() => commit(root, scope, { plan: { ...plan, brief: "new\n" }, entries: [] }),
|
|
182
|
+
),
|
|
183
|
+
).rejects.toThrow("crash");
|
|
184
|
+
await withUlwLoopMutationLock(root, scope, async () => {
|
|
185
|
+
expect(readLedger(root, scope)).toHaveLength(1);
|
|
186
|
+
});
|
|
187
|
+
expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe("new\n");
|
|
188
|
+
expect(readdirSync(join(dir, "revisions"))).toHaveLength(2);
|
|
189
|
+
});
|
|
190
|
+
});
|